关于this
this
是JavaScript的一个关键字,自动定义在所有函数中,难点在于this
的指向。
this
的指向在函数调用时进行绑定,它的context
取决于函数调用时的各种条件,与函数定义位置无关
1 this
的作用
this
可以使不同的context
对象重复使用已经存在、声明的函数,无需针对每个对象编写不同版本的函数
function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, i'm " + identify.call( this );
console.log( greeting );
}
var me = {name: "Kyle"};
var you = {name: "Reader"};
identify.call( me ); // KYLE
identify.call( you ); // READER
speak.call( me ); // "Hello, i'm KYLE"
speak.call( you ); // "Hello, i'm READER"
2 误解
this
并不是指向函数本身在任何情况下,
this
都不指向函数的词法作用域
3 this
是什么?
this
是在函数被调用时发生绑定,其指向取决于函数调用的位置。
当一个函数被调用是,会创建一个执行上下文(context
)。其中包含函数的调用位置(调用栈)、函数的调用方式和传入的参数等信息。this
是context
中的一个属性
4 this
解析
4.1 函数调用位置
函数在程序代码中被调用的位置,清楚调用位置才能明确
this
的指向
确定函数的调用位置:最关键是分析调用栈(为达到当前指向位置所调用的所有函数)。分析调用栈,可以得出真正的调用位置
function baz() {
// 当前调用栈是:baz
// 所以当前调用位置时全局作用域
console.log('baz');
bar(); // <-- bar的调用位置
}
function bar() {
// 当前调用栈是:baz --> bar
// bar的调用位置在baz中
console.log('bar');
foo(); // <-- bar的调用位置
}
function foo() {
// 当前调用栈是:baz --> bar --> foo
// foo的调用位置在bar中
console.log('foo');
}
baz(); // --> baz的调用位置
4.2 this
的绑定规则
在分析清楚调用位置后,根据this
绑定的四条规则决定绑定对象。四条规则分别对应四种不同的函数调用方式
总共有四条绑定规则,其优先级是:默认绑定 < 隐式绑定 < 显式绑定 < new绑定
默认绑定:作为独立调用的函数
隐式绑定:作为对象的方法调用的函数
显式绑定(硬绑定):使用
call()
、apply()
和bind()
方法,强制将对象绑定到函数的this
上new
绑定:
4.2.1 默认绑定
默认绑定指将函数作为独立的函数来调用,默认绑定将this
绑定到全局对象。
分析隐式绑定时,一个对象内部包含一个指向函数的属性,并且通过对象的属性间接引用函数,将this
间接绑定到该对象上
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
a
在全局作用域中声明,是全局对象的一个属性;foo()
使用不带任何修饰的函数进行调用,只能使用默认绑定规则;此时,非严格模式下this
指向全局对象,所有this.a
被解析为全局变量a
-
在严格模式中,
this
不能绑定到全局对象,this
只能绑定到undefined
function foo() { 'use strict'; console.log(this.a); } var a = 2; foo(); // TypeError: this is undefined
4.2.2 隐式绑定
判断函数的调用位置是否有上下文对象,隐式绑定将this
绑定到调用方法的上下文对象上。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2 foo()的调用位置包含上下文对象obj,this隐式绑定到obj对象
-
对象属性引用链中,只有最后一层会影响调用位置
function foo() { console.log(this.a); } var obj = { a: 2, obj2: obj2 }; var obj2 = { a: 42, foo: foo obj.obj2.foo(); // 42 实际是通过obj对象的obj2属性对象来调用foo()函数,this指向obj2
隐式丢失
被隐式绑定的函数会丢失绑定对象,然后应用默认绑定,非严格模式下将this
绑定到全局对象。
-
隐式绑定丢失发生在将隐式绑定的函数赋值给另外的变量,通过改变了来调用函数
function foo() { console.log(this.a); } var obj = { a: 2, foo: foo }; var bar = obj.foo; // 函数别名,传递引用 var a = "global a"; bar(); // "global a",函数的调用位置,bar()其实是一个不带任何修饰的函数调用,所以应用默认的绑定规则
-
在函数中将回调函数作为参数传入时,参数传递是一种隐式赋值(传递引用),所以应用默认绑定规则
function foo() { console.log(this.a); } function doFoo(fn) { // fn是obj.foo函数本身的一个引用 fn(); // fn的调用位置 } var obj = { a: 2, foo: foo }; var bar = obj.foo; // 函数别名,传递引用 var a = "global a"; doFoo(obj.foo); // "global a",传入的函数被隐式赋值,应用默认绑定规则 setTimeout(obj.foo, 100); //"global a",使用语言本身的内置函数时道理相同
4.2.3 显式绑定
在JavaScript中,函数是对象,每个函数对象都定义有call()
、apply()
和bind()
方法,bind()
在ES5中。
call()
、apply()
方法:
第一个方法是一个对象,将该对象绑定到
this
可以直接指定绑定的对象,称为显示绑定
-
call()
、apply()
区别在于其他参数function foo() { console.log(this.a); } var obj = { a: 2, foo: foo }; foo.call(obj); // 2 通过foo.call(obj);,在调用foo()时强制将其this绑定到obj对象
显式绑定仍然会有绑定丢失问题:可以使用显示绑定的一个变形来解决这个问题;
硬绑定
-
创建一个函数
bar()
,在内部调用foo.call(obj)
,强制将foo
的this
绑定到obj
对象上,无论怎样调用bar()
函数,都会手动在obj
对象上调用foo
,因此foo
的this
指向不会改变function foo() { console.log(this.a); } var obj = { a: 2, foo: foo }; var bar = function() { foo.call(obj); } bar(); // 2 setTimeout( bar, 100 ); // 2 // 硬绑定的bar不能再修改它的this指向 bar.call(window); // 2
硬绑定的应用场景
-
创建一个包裹函数,传入所有的参数,并返回接收到的所有值
function foo(sth) { return this.a + sth; } var obj = { a: 2 }; var bar = function() { // 将arguments传入需要调用的函数 return foo.apply(obj, arguments); } bar(3); // 2 + 3 = 5
-
创建一个可以重复使用的函数
function foo(sth) { return this.a + sth; } var obj = { a: 2 }; // 简单的辅助绑定函数 function bind(fn, obj) { return function() { return fn.apply(obj, arguments); } } var bar = bind(foo, obj); bar(3); // 2 + 3 = 5
-
硬绑定是一种非常常见的模式,ES5提供内置
Function.prototype.bind
方法:返回一个硬绑定的新函数,bind(obj)
将参数obj
设置为this
的上下文,并调用原始函数。function foo(sth) { return this.a + sth; } var obj = { a: 2 }; var bar = foo.bind(obj); bar(3); // 2 + 3 = 5
4.2.4 new
绑定
JavaScript中的new
机制与传统面向对象语言不同。JavaScript中构造函数只是使用new
操作符调用的函数,使用new
操作符调用函数时:
创建一个全新对象;
新对象被执行
__proto__
链接新创建的对象被绑定到函数调用时的
this
-
如果函数没有返回其他对象,
new
表达式中的函数调用自动返回新创建的对象function foo(a) { this.a = a; } var bar = new foo(2); console.log(bar.a); // 2
4.3 优先级
判断this
的指向:找到函数的调用位置,并根据优先级判断应用的规则,默认绑定的优先级最低
-
显示绑定的优先级高于隐式绑定:在判断时优先考虑显示绑定
function foo(a) { this.a = a; } var obj1 = { a: 2; foo: foo }; var objb = { a: 4; foo: foo }; obj1.foo(); // 2 obj2.foo(); // 4 obj1.foo.call(obj2); // 4 obj2.foo.call(obj1); // 2
new
绑定的优先级高于隐式绑定:-
new
绑定的优先级的高于显示绑定:bar
被硬绑定到obj1
对象上,但是new bar(3)
并未将obj1.a
修改为4;-
new bar(3)
修改了调用bar()
中的this
,得到一个新对象function foo(a) { this.a = a; } obj1 = {}; var bar = foo.bind(obj1); bar(2); console.log(obj1.a); // 2 var baz = new bar(4); console.log(obj1.a); // 2 console.log(baz.a); // 4
4.4 根据优先级判断函数调用位置应用的规则
函数是否在
new
中调用?如果是,this
绑定新创建的对象函数是否通过
call()
、apply()
显示绑定?或者bind()
硬绑定?如果是,this
指向绑定的对象。函数是否在某个上下文对象中调用?如果是,
this
指向那个上下文对象如果不是上述三种情况,使用默认绑定。严格模式下绑定到
undefined
,非严格模式下绑定到全局对象
4.5 绑定例外
4.5.1 被忽略的this
将null
或undefined
作为this
的绑定对象传入call()
、apply()
和bind()
方法,在调用时被忽略,实际应用默认绑定规则。
使用null
来忽略this
绑定可能产生副作用:安全的做法是传入一个特殊对象,将this
绑定到这个对象不会产生任何副作用。Object.create(null)
。
5 this
词法
ES6中的箭头函数不能使用上述4种规则,而是根据外层(函数或全局)作用域来绝对this
。箭头函数常用于回调函数中。
function foo() {
// 返回一个箭头函数
return (a) => {
// this继承自foo()
console.log(this.a);
}
}
var obj = {
a: 2
};
var obj2 = {
a: 42
};
var bar = foo.call(obj1);
bar.call(obj2); // 2, 不是42