this 关键字是 JavaScript 中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。但是即使是非常有经验的 JavaScript 开发者也很难说清它到底指向什么。
this 是什么?
指向函数本身?
光从字面意思上来看,很容易让人觉得 this 就是指向函数本身,事实上真是这样吗?我们可以看一个例子。
function foo() {this.count = this.count ? this.count + 1 : 1;}
for (let i = 0; i < 5; i++) {foo();
}
console.log(foo.count); // undefined
可以看到,foo.count 输出的并不是我们期待的5
,而是一开始赋值的0
。也就是说this 其实并没有指向函数本身。
指向作用域?
还有一种比较常见的误解是,this 指向了函数的作用域。
function foo() {
var a = 2;
bar();}
function bar() {console.log(this.a);
}
foo(); // undefined
这段代码中,bar 在 foo 中运行,输出 this.a
得到的却是undefined
。也就是说this 也不是指向函数的作用域的。
这也不是,那也不是,this 到底是什么呢?在函数执行过程中,会创建一个执行上下文(一个记录),this 就是这个上下文中的一个属性,在执行过程中用到。而 this 的指向则是取决于函数在哪里被调用。
this 的绑定规则
this 的绑定有四条可以遵循的规则,下面将一一介绍。
1. 默认绑定
独立函数调用,非严格模式下,指向 window;严格模式下指向 undefined。
这里说的独立函数可以理解成除开后面三种情况的一般函数调用。
// 非严格模式
var name = 'Willem';
function foo() {console.log(this.name);
}
foo(); // Willem
// 执行时启用严格模式
(function() {
'use strict';
foo(); // Willem
bar(); // Cannot read property 'name' of undefined})();
// 函数体使用严格模式
function bar() {
'use strict';
console.log(this.name);
}
上述代码中,分别在普通环境中输出 Willem,说明指向的确实是 window 对象。需要特别注意的一点是:严格模式下指向 undefined 指的是函数体内启用了严格模式,而不是调用时。
2. 隐式绑定
隐式绑定说的是,在函数执行时,是否被某个对象拥有或包含
。换句话说,在函数运行时,是否是作为某个对象的属性的方式运行的,这样说还是不是很清楚,来个栗子:
function foo() {console.log(this.a);
}
var a = 1;
var obj = {
a: 2,
foo
};
obj.foo(); // 2
var obj2 = {
a: 3,
obj
};
obj2.obj.foo(); // 2
示例中,foo 被当做了 obj 的一个属性进行执行,此时 obj 作为了函数的上下文,此时 this 指向了 obj,this.a
等价于obj.a
。在对象属性链式的调用中,只有最后一层会对调用位置产生影响,也就是说最后一层会影响 this 指向。
有很多前端的小伙伴面试时或许还见过这样的题:
function foo() {console.log(this.a);
}
var a = 1;
var obj = {
a: 2,
foo
};
var bar = obj.foo;
bar(); // 1
这是隐式绑定最常见的一个问题,隐式丢失:被隐式绑定的函数会丢失绑定对象。虽然 bar 是对 obj.foo 的一个引用,但实际上引用的还是 foo 函数本身,bar 函数就是一个独立函数的调用,参考第一条,此时this 指向了 window|undefined
。
还有一种经典的回调函数的 this 指向问题也是隐式丢失。
function foo() {console.log(this.a);
}
function doFoo(fn) {fn();
}
var a = 1;
var obj = {
a: 2,
foo
};
doFoo(obj.foo); // 1
小结:在隐式绑定中,赋值的情况下(回调是隐式赋值)需要特别注意隐式丢失的问题。
3. 显示绑定
JavaScript 中的 Function 提供了两个方法 call
和apply
,传入的第一个参数是一个对象,会把 this 绑定到这个对象。如果是传入的是一个原始值(字符串、数字、布尔),会被转换成它的对象形式(new String(), new Boolean(), new Number())。
function foo() {console.log(this.a);
}
var obj = {a: 1};
foo.call(obj); // 1
虽然我们可以使用 call
和apply
显式指定 this 的指向,但是还是会存在丢失绑定的问题。可以通过所谓的 硬绑定(bind 函数)
来解决,这里就不过多赘述了。
4. new
最后要介绍的是使用 new
来做 this 的绑定的修改,有手动实现过 new 的童鞋应该比较清楚,js 中的 new 和其他语言的 new 完全不同。
new 的执行过程:
- 创建一个空对象
- 当前空对象执行原型对接
- 返回函数执行结果或者当前这个空对象
function Foo(a) {this.a = a;}
var bar = new Foo(2);
bar.a; // 2
使用 new 来调用函数时,我们会构造一个新对象并把它绑定到 函数调用中的 this 上。
优先级
最后简单说一下优先级的关系:new > 显示绑定 > 隐式绑定 > 默认绑定。
参考:《你不知道的 JavaScript》
相关:
模拟实现 Javascript 中的 bind 函数
模拟实现 js 中的 new 操作符
模拟实现 Javascript 中的 call 和 apply