this 算是 JS 中最复杂的机制之一了,很多时候我们明明觉得,我已经理解了这个关键字,但是实际用来的时候,还是有时候不明白这个 this 到底指向哪里,今天我们就来捋一捋,希望能对你有帮助。
两个误解
其实刚开始学的时候,我也有过疑惑,这个关键字叫 this,那顾名思义,是不是意味着 this 指向自身呢?其实对 this的误解比较常见的有以下两种。
误解一:指向函数自身
初学者刚接触的时候,可能会觉得 this 就是指向函数本身,这就犯了一个错,太执着于 this 的字面含义了。
我们来看看下面这个例子:
例 1:
function foo(num) {
console.log( "foo: " + num ); // 记录 foo 被调用的次数
this.count++;
console.log(this); // Window
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
console.log('count:', foo.count)
// count: 0
在这个 foo 函数中有一个属性 count,我们期望 foo 每次被调用时,count 都能+1,以此来记录 foo 函数调用的次数。
但我们看到,结果并不像预期的那样。可以看到 foo 函数被调用了 4 次,但是 foo.count 却依旧为 0 ,这足以证明 this 指向函数自身是错误的。
但是 foo 上确实有 count 属性呀,那为什么this.count++没有产生预期的效果呢?这是因为这个 this 是指向 window 的,这样的操作在无意间创建了一个全局变量 count,而它的值是 NaN,自然无法修改到 foo 中的 count 属性。
误解二:指向函数的作用域
还有一个误解则是 this 指向了函数的作用域。但事实上,在任何情况下, this 都不会指向函数自身的作用域。
我们再来看另一个例子
例 2:
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log('this.a:', this.a ); // this.a: undefined
}
foo();
咋一看,感觉输入的 a 好像就是 2呀,其实不然,这段代码想联通 foo 和 bar 之间的作用域,从而能使 bar 中也能访问到 foo 中定义的 a,但事实上是不行的,不要把 this 和作用域混为一谈。
四条规则
分析出 this 指向哪里也没有那么难,this 的指向其实是遵循以下四条规则的,碰到实际情况,只需要将这四条规则往里一套就可以了。
请注意,this 是在运行时绑定,而非定义时绑定,运行的上下文不同,this 的指向也会不同,可以简单理解为:谁最后调用了的, this 就指向谁
规则一:默认绑定
默认绑定可以认为是一条最基础的规则,在其他规则无法匹配的情况下,就会匹配到这条规则。
可以认为,在函数调用时,前面没有某个对象在调用,那基本就是默认绑定了,当然像使用 bind 这种去强行改变 this 指向的是例外。
我们可以看看下面这个简单的例子。
例 3:
function foo() {
console.log(this); // Window
console.log( this.a ); // 2
}
var a = 2;
foo();
在调用 foo 函数的时候,this.a 采用了变量 a 的值,这是因为 foo 在被调用时,并没有谁在调用 foo,默认为全局对象 Window 在调用,即 Window.foo(), 可以看到其中的 this 也是指向 Window 的,而 var 定义的变量 a,也是挂在全局对象 Window 上面的,所以 this.a 就被解析成了变量 a。
当然这是在非严格模式下,在严格模式下这个 this 可不是指向 Window 哦,而是 undefined
这里提一嘴,如果 a 是 let/const 定义的,那 a 就不是挂载在 Window 上了哦,而是在块级作用域中,那 this.a 就是 undefined 了。
规则二:隐式绑定
隐式绑定也很好理解,就是函数被某个对象调用了,那么 this 就是绑定到这个对象上。如果链式调用的话,那么只认最后调用的那个对象。
我们再来举个例子
例4:
function foo() {
console.log( this ); // {a: 2, foo: ƒ}
console.log( this.a ); // 2
}
var obj = {
a: 2,
foo: foo
};
obj.foo();
这里 foo 是被 obj 调用的,那么函数中的上下文就被绑定到了 obj 上了,即 this 指向了 obj 了。所以 this.a 就是 2 了。
我们再来看看另一个例子。
例5:
function foo() {
console.log( this ); // {a: 42, foo: ƒ}
console.log( this.a ); // 42
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo();
这就是一个链式调用,可以看到 foo 函数最后还是被 obj2 调用的,所以 foo 内部的 this 也指向了 obj2,谨记,this 只认最后调用它的那个对象。
我们来看一下一种迷惑性很强的情况
例6:
function foo() {
console.log( this.a ); // "oops, global"
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar();
这里 foo 函数是 obj 的一个属性,然后给了 foo 函数一个别名 bar,再直接调用 bar 函数,咋一看,这不是妥妥的隐式调用吗,为什么 a 输出的却是 oops, global 呢?
还记得我们之前说过的默认绑定吗?这里只是将 foo 函数的引用赋值给了 bar,但是并没有调用哦,在调用的那一刻,前面并没有任何对象在调用,所以这就是匹配到了隐式绑定的规则了, this 指向的是 Window。
规则三:显示绑定
通过 call 或者是 apply 等方法,可以强行的将 this 指向某个对象。
例7:
function foo() {
console.log( this.a ); // 2
}
var obj = { a:2 };
foo.call( obj );
在这个例子中,在通过 foo.call( obj )强行将 foo 中的 this 指向 obj,而无须像隐式绑定一样,obj 必须包含 foo 属性才行。
规则四:new 绑定
我们先来看看 new 的时候究竟干了些什么
- 创建一个全新的对象
- 新对象的原型对象指向构造函数的原型属性
- 将 this 指向这个新对象
- 如果构造函数返回了一个对象,就将该返回值返回,如果返回值不是对象,就将创建的新对象返回
这里也简单列一下 new 的实现方式
function _new() {
var o = new Object() // 创建一个新的对象
let [constructor, ...oArgs] = [...arguments] // 传入参数第一个是构造函数,后面的是构造函数的参数
o.__proto__ = constructor.prototype // 执行构造函数 将构造函数的原型赋给 实例对象的__proto__这样构造函数的属性就全都给实例了
let resultObject = constructor.apply(o,oArgs) // 将构造函数的this指向创建的对象
if (resultObject && typeof resultObject == Object || typeof resultObject == "function") {
// 如果构造函数的执行结果返回的是一个 对象 那么就返回这个对象
return resultObject
}
// 如果构造函数返回的是正常我们常见的那种(不是对象), 那么返回这个新创建的对象
return o
}
可以看出,其实 new 内部也是利用 call 或者是 apply 等方式,将 this 指向创建的新对象。
例8:
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
关于隐式绑定 this 丢失的问题
这里再详细讲一下例 6 吧,当时我第一次看到的时候也是困惑不已。
function foo() {
console.log( this.a ); // "oops, global"
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar();
再看一个变种
例 9:
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn();
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"
在例 6 中,给了 obj.foo 一个别名 bar ,然后再去执行 bar(),执行的结果还是 this 指向了全局变量 Window,从而输出的结果是 oops, global;
而在例 9 中,是将 obj.foo作为一个参数传入了 doFoo 中,再执行 doFoo( obj.foo ),但是结果也是一样的。因为参数的传递就是一种隐式的赋值,和例 6 中的 var bar = obj.foo是一个道理。
这其实就是出现了 this 丢失的问题,this 并没有指向包含这个函数 foo 的对象 obj,从而应用了默认规则,将 this 绑定到了全局对象 Window 或者是 undefined (严格模式) 上。
还不理解的话可以看一看 ruanyifeng 大佬的这篇 《JavaScript 的 this 原理》,这里我也大概讲述一下。
在这个示例中,将对象{foo: 5 } 赋值给了变量 obj,但是大家都知道, obj 只是一个地址,指向了这个{foo: 5 } 的内存地址。
原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象
属性描述对象长这样:
{
foo: {
[[value]]: 5
[[writable]]: true
[[enumerable]]: true
[[configurable]]: true
}
}
这是值为基本类型的情况,那如果 foo 的值是函数呢,那就会只这样的了。
var obj = { foo: function () {} };
function 是存在一个单独的地址里,所以它可以在不同的上下文执行。
回到例 6,执行var bar = obj.foo的时候,bar 其实也是一个指向 foo 函数的内存地址,bar() 是在全局的环境执行,所以输出的 this.a 也就是 oops, global。
这样理解是不是就清晰很多了。
参考资料
《你不知道的JavaScript》