this绑定的分类
- 默认绑定
- 隐式绑定
- 显式绑定
- new绑定
前置知识
this的绑定和函数声明的位置没有任何关系,只取决于函数的实际调用位置
默认绑定
js
function foo () {
console.log(this.a);
}
var a = 5;
foo(); // 5
分析 使用var在全局作用域(window)声明了一个a属性,这跟在window对象中声明一个a属性是等价的,就像硬币的正反两面。由于foo在全局作用域上调用,所以this指向window对象,所以结果是5。这就是默认绑定,默认绑定到window对象。但如果我们将var改为const或者let,结果会是undefined,因为const和let声明的变量并不会挂在window对象上。
隐式绑定
当函数调用有上下文时,应用的就是隐式绑定,此时this一般指向该上下文对象。
js
function foo () {
console.log(this.a);
}
const obj = {a: 'obj:a', foo: foo};
var a = 'global:a'
obj.foo(); // obj:a
分析 obj.foo()虽然是在全局环境调用函数的,但foo的调用上下文是obj,或者说是obj持有了foo,所以此时的this指向了obj上下文对象。
为了加深印象,我们再举个🌰:
js
function foo() {
console.log(this.a);
}
var obj2 = { a: 42, foo: foo, }
var obj1 = { a: 2, obj2: obj2 }
obj1.obj2.foo(); // 42, obj2为foo的直接上下文
分析 由代码可知,obj1包含了obj2,obj2包含了foo,但最后调用foo时,obj2为foo的直接上下文,此时this指向了obj2,它并不会受obj1的影响。
隐式丢失
但隐式绑定在一些场景下会丢失绑定的上下文对象,从而应用了默认绑定。
举个🌰:
js
function foo() {
console.log(this.a);
}
var obj = { a: 2, foo: foo };
var bar = obj.foo;
var a = "oops, global"; // a是全局对象的属性
bar(); // "oops, global"
分析 由于obj.foo的函数引用赋值给了bar,所以实际上bar是foo函数的函数别名,因此,在全局环境调用bar实际上跟在全局环境直接调用foo函数得到的结果是一样的。this都指向了全局window对象。
显式绑定
通过call,apply等来实现显式绑定
js
function foo() {
console.log(this.a);
}
var obj = { a: 2 };
var a = 'global';
foo.call(obj); // 2
分析 显式绑定就相对容易理解很多,往call或者apply传递的第一个参数,实际上就是此函数调用的上下文对象,所以this会指向这个对象。
被忽略的this
如果往call或者apply传递一个null(空上下文),此时this会指向全局对象
举个🌰:
js
function foo() {
console.log(this.a);
}
var a = 2;
foo.call(null); // 2 使用了默认绑定
new绑定
new绑定是js中绑定优先级最高的绑定规则,也是在js继承中常见的。
js
function foo(a) {
this.a = a;
}
var bar = new foo(2);
bar.a // 2;
分析 由于new的过程实际上内部会将构造函数的this指向实例对象,所以this会指向bar。
this绑定优先级
那这四种this绑定,优先级怎么排列呢?
答案是: new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
this绑定的判断规则
当我们的代码中同时出现这四种绑定规则中的两种及以上时,我们应该怎么正确判定this的指向呢?
规则如下:
1.函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象,因为new绑定的优先级最高!
2.函数是否通过call,apply(显示绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
3.函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是该上下文对象。
4.如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象
箭头函数中的this绑定
箭头函数不会使用this的四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的this绑定,而且无法修改。
js
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!!
一些建议
在实际的项目开发中,为了保证代码风格的统一性和便于理解,我们不应混用this绑定的四种规则语法和箭头函数!
总结
this的绑定指向一直是一个令人难以理解的东西,特别是在阅读一些框架源码或者工具函数的时候。为了提升阅读源码的个人能力,了解和掌握this的规则势在必行!通过阅读此篇文章,我们可以很容易掌握this的绑定规则有哪些,以及他们的使用方式和优先级排列。
参考资料
《你不知道的Javascript-上卷》