詳細な研究ノート---このJS
これは開発者が非常に面倒な問題をできるようになるので、これでjsが、何がある場合には綿密な研究では、理解しないように。今、私はこのために、研究ノートを行うにします。
1.コール場所
このバインディングプロセスを理解する前に、我々は最初の呼び出しの場所を理解する必要があります。コール位置が(位置ではなく、宣言)で、コード内の位置の関数が呼び出されます。最後に、このへの参照は、それが何であるかを理解するための唯一の良いコールサイト分析?呼び出し位置を見ると、最も重要なのは(現在の実行位置によって呼び出されるすべての機能を達成するために、ある)コールスタックを分析することです。現在実行前にコール上の位置を呼び出します。次の例:
function baz (){
// 当前调用栈是:baz
//因此,当前调用位置是全局作用域
console.log("baz");
bar();// <-- bar的调用位置
}
function bar(){
// 当前调用栈是 baz -> bar
// 因此,当前调用位置在 baz 中
console.log('bar');
foo();// <-- foo 的调用位置
}
function foo(){
// 当前调用栈是baz -> bar -> foo
// 因此,当前调用位置在bar中
console.log("foo");
}
baz(); // <-- baz的调用位置
2.結合ルール
2.1デフォルトのバインディング
別の関数呼び出し:最も一般的な関数呼び出しを初めて目。ときに、他のルールデフォルトルールが適用できないため、このルールは見ることができます。次の例では:
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
// 在本代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。
// 如果使用严格模式,那么全局对象无法使用默认绑定,因此this会绑定到undefined。
2.2暗黙のバインド
考慮すべきもう一つのルールは、コンテキストオブジェクト、またはオブジェクトが所有または含まれているかどうかを呼び出すための位置があるかどうかです。例えば:
function foo() {
console.log(this.a);
}
var obj = {
a:2,
foo
};
obj.foo(); // 2
まず、ノートにそれはOBJへの参照属性として使用されているか、そのFOO()文のモードで、そして。しかし、直接定義またはオブジェクトobjの参照属性を追加はobj、この関数は厳密に言えばされていない部分を定義します。
しかし、呼び出し位置関数はオブジェクトobjを「所有」または呼び出されたときに、それを「含んで」ということ、そのため、機能OBJコンテキストを参照するために使用されます。
FOO()が呼び出されたときにどのような場合、このモードを呼び出すことで、最終的な結果は、オブジェクトobjにポイントをします。関数参照は、コンテキストオブジェクトを持っている場合は、暗黙のこの結合規則は、コールがコンテキストオブジェクトにバインドされて機能します。だから、this.aとobj.aは同じです。
オブジェクトの属性は、呼び出し元の場所に影響を及ぼしますチェーンの最後のものだけを指します。コードの場合:
function foo() {
console.log(this.a);
}
var obj2 = {
a:42,
foo
};
var obj1 = {
a:2,
obj2
};
obj1.obj2.foo(); // 42
2.2.1暗黙の損失
最も一般的な問題の一つは、この結合の量は、結合オブジェクトの暗黙の結合機能が失われるということです、これはグローバルオブジェクトを置くために結合されるか、または定義されていないので、どうかstrictモードに応じて、デフォルトのバインディングを適用します。次のコードを見てください:
function foo() {
console.log(this.a);
}
var obj = {
a:2,
foo
};
var bar = obj.foo; // 函数别名!
var a = "What?"; // a 是全局对象的属性
bar();//"What?"
バーがobj.foo参照ですが、実際には、それは関数fooそのものであるために言及しているが、今回のでバーは()デフォルトのバインディングを適用するので、任意の修飾子なしで実際の関数呼び出しです。たとえば、次のコールバック関数は、暗黙のうちに失われました:
function foo() {
console.log(this.a);
}
function doFoo(fn){
// fn 其实引用的是foo
fn(); // <- 调用位置
}
var obj = {
a:2,
foo
};
var a = "What?"; // a 是全局对象的属性
doFoo(obj.foo);//"What?"
参数传递其实就是一种隐式赋值,传入函数时也会被隐式赋值,所以结果和上一个例子一样。
2.3 显示绑定
在上面隐式绑定的时候,必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)的绑定到这个对象上。
如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该如何处理?
基本上大部分函数会包含call(..)和apply(..)方法。但是有的时候JavaScript的宿主环境有时候会提供一些非常特殊的函数,可能没有这两个方法,但是极为罕见。
这两个函数的第一个参数是一个对象,会把这个对象绑定到this,接着在调用函数时指定这个this。因为这种方式可以直接指定this的绑定对象,因此我们称之为显示绑定。
上代码:
function foo() {
console.log(this.a);
}
var obj = {
a:2
};
foo.call(obj); // 2
通过foo.call(...),我们可以在调用foo的时候强制将this绑定在obj上。
如果从传入了一个原始值(字符串类型、布尔类型或者数字类型)来当做this的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(...)、new Boolean(...)或者new Number(...)),这通常称为“装箱”。
从this的绑定的角度来说,call(...)和apply(...)是一样的,他们的区别体现在其他的参数上。
不过上述的代码不能很好地解决我们提出的丢失绑定的问题。
2.3.1 硬绑定
不过显示绑定的一个变种可以解决这个问题。 上代码:
function foo() {
console.log(this.a);
}
var obj = {
a:2
};
var bar = function(){
foo.call(obj);
}
var a = '123';
bar(); // 2
setTimeout(bar,10); // 2
bar.call(window); // 2 此时硬绑定的bar不能修改foo的this。foo总会在obj上调用。
由于硬绑定是一种非常常用的模式,所以在ES5中提供了内置方法 bind ,它的用法如下:
function foo(str) {
console.log(this.a, str)
return this.a + str;
}
var obj = {
a: 2
};
var bar = foo.bind(obj);
var b = bar(3);// 2 3
console.log(b);// 5
2.3.2 API调用的“上下文”
第三方库的许多函数,以及javaScript语言和宿主环境中的许多新的内置函数,都提供了一个可选的参数,通常被称为上下文,其作用和bind一样,确保回调函数使用指定的this。上代码:
function foo (item){
console.log(item,this.id);
}
var obj = {
id:"cool"
};
// 调用foo()时把this绑定到obj
[1,2,3].forEach(foo,obj);
// 1 cool 2 cool 3 cool
2.4 new绑定
js中使用new可以构造一个新的对象,使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
-
创建(或者说构造)一个全新的对象; -
创建这个新对象会被执行[[原型]]连接; -
这个新对象会绑定到函数调用的this; -
如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
上代码:
function foo(a) {
this.a = a;
}
var a = 3;
var bar = new foo(2);
console.log(bar.a); // 2
使用new来调用foo()时,会构造一个新对象并绑定到foo()调用中的this上。
3.优先级。
-
毫无疑问,默认绑定的优先级是最低的。 那么隐式绑定和显示绑定谁更高?上代码:
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
可以看到,显式绑定优先级更高,也就是说在判断时应当先考虑是否可以应用显式绑定。
-
new 绑定 VS 隐式绑定:
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
可以看到 new 绑定比隐式绑定优先级高。
-
new 绑定 VS 显示绑定:
new 和 call/apply 无法一起使用,因此无法通过 new foo.call(obj1) 来直接 进行测试。但是我们可以使用硬绑定来测试它俩的优先级。
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
可以看到,new 修改了硬绑定(到 obj1 的)调用 bar(..) 中的 this。因为使用了new 绑定,我们得到了一个名字为 baz 的新对象,并且 baz.a 的值是 3。
总结
现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的 顺序来进行判断:
1.函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。
var bar = new foo()
2.函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是指定的对象
var bar = foo.call(obj2)
3.函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。
var bar = obj1.foo()
4.如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象。
var bar = foo()
4.箭头函数
之前介绍的四条规则已经可以包含所有正常的函数。但是 ES6 中介绍了一种无法使用这些规则的特殊函数类型:箭头函数。 箭头函数并不是使用 function 关键字定义的,而是使用被称为“胖箭头”的操作符 => 定义的。箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。
function foo() {
// 返回一个箭头函数
return (a) => {
//this 继承自 foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !
foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1, bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不 行!)
箭头函数最常用于回调函数中,例如事件处理器或者定时器:
function foo() {
setTimeout(() => {
// 这里的 this 在此法上继承自 foo()
console.log( this.a );
},100);
}
var obj = {
a:2
};
foo.call( obj ); // 2
箭头函数可以像 bind(..) 一样确保函数的 this 被绑定到指定对象,此外,其重要性还体 现在它用更常见的词法作用域取代了传统的 this 机制。实际上,在 ES6 之前我们就已经 在使用一种几乎和箭头函数完全一样的模式。
function foo() {
var self = this; // lexical capture of this
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
虽然 self = this 和箭头函数看起来都可以取代 bind(..),但是从本质上来说,它们想替 代的是 this 机制。
参考资料
-
《你不知道的javaScript》---上卷
你好!我是 JHCan333,公众号:爱生活的前端狗的作者。公众号专注前端工程师方向,包括但不限于技术提高、职业规划、生活品质、个人理财等方面,会持续发布优质文章,从各个方面提升前端开发的幸福感。关注公众号,我们一起向前走!