你不知道的JS系列——全面解析this

1,871

前言

任何足够先进的技术都和魔法无异。this 关键字是 JavaScript 中最复杂的机制之一,搞懂它很重要。

一、this 是什么?

this 是 JavaScript 中的关键字,在常见的面向对象语言中都有 this 的身影,相较下 JavaScript 中的 this 比较特殊,特殊在它会在执行期间动态改变指向。this 一般定义在函数中,如果按英文解释,很容易产生误解,this 既不指向函数自身,也不指向函数的词法作用域。它在运行时进行绑定,它指向什么完全取决于函数在哪里被调用。

二、为什么要用 this ?

this 提供了一种更优雅的方式来隐式“传递”一个对象引用,让我们可以避免显示传递上下文对象引起的代码混乱,因此可以将 API 设计 得更加简洁并且易于复用。

三、4条绑定规则

我们普遍知道的规则是,谁调用 this , this 指向谁。但这只是下面要讲的规则中的一条。

1、默认绑定

我们在写原生 JS 时,会直接定义一个函数,然后直接进行独立的函数调用,此时应用默认绑定规则。
举例说明:

function run(){
    console.log(this.a);
}
var a = 1;
run();  // 1

浏览器环境下,在非严格模式中,var 在全局作用域中定义的变量会自动添加到 window 对象下,在全局环境下执行函数 run ,就理解为 run 在 window 中被调用,此时的 run 函数是定义在 window 对象中的。上面讲 this 指向什么完全取决于函数在哪里被调用,所以此时 this 指向 window, window.a 自然输出 1。
严格模式中,var 声明的变量不会自动绑定到 window 对象下,同理 run 函数也不是定义在 window 对象中的, run() 独立执行,执行 RHS 右查询 this ,查无此值,给到 undefined , undefined.a 自然报错,且报错类型为 TypeError ,类型错误,因为 undefined 不是一个对象。

默认绑定下,this 是 window 、undefined 二者其一,取决于是否是严格模式。

2、隐式绑定

这就是我们常说的,谁调用 this , this 指向谁。
举例说明:

function run(){
    console.log(this.a);
}
var obj = {
    a: 1,
    run
}
obj.run();  // 1

注意: 隐式丢失的发生,就是我们常说的 this 丢失,多发生在函数赋值。
举例说明:

function run() { 
   console.log( this.a ); 
}
var obj = { 
   a: 2, 
   run 
};
var fn = obj.run; // 函数别名!
var a = "welcome"; // a 是全局对象的属性 
fn();  // "welcome"

你产生的困惑发生于var fn = obj.run;赋值语句,很好理解,不要把问题复杂化。正如我们所知的,JS 中函数即对象,obj.run代表对run函数对象的引用,但此时执行的是赋值语句,赋值语句右侧一般执行 RHS 右查询,即查询具体的值,所以此时fn直接指向run函数本身,fn()的执行等同于直接执行run(),所以你的疑惑自然消除。
再看一个例子:

function run() { 
    console.log( this.a );
}
var obj = { 
    a: 2, 
    run 
};
var a = "welcome"; // a 是全局对象的属性 
setTimeout( obj.run, 100 ); // "welcome"

setTimeout函数第一个参数为函数,我们给它起个名字fn,参数obj.run的传入发生隐式赋值fn = obj.run,讲到这里,再结合上面,你该明白了。

3、显示绑定

call、apply、bind 实现的绑定我们称为实现显示绑定,这里不做过多解释。
解释一个我们常常忽略的可选参数,看下面一段代码:

function run(param) { 
    console.log( param, this.id ); 
}
var obj = { 
    id: "welcome"
}; 
[1, 2, 3].forEach( run, obj );  // 1 "welcome" 2 "welcome" 3 "welcome"

这里数组的forEach()方法,第二可选参数传递给函数的值一般用做 "this" 值。 如果这个参数为空, "undefined" 会传递给 "this" 值。

4、new绑定

我们都知道 new 一个函数,函数内部的 this 指向新生成的实例,但这是为什么呢?我们需要知道 new 的过程中发生了什么,当然我们这里不会讲开辟什么堆栈空间之类的,这不在我们的讨论范围。
new 就是一个可以调用普通函数的操作符,使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:

创建(或者说构造)一个全新的对象。

这个新对象会被执行 [[ 原型 ]] 连接。

这个新对象会绑定到函数调用的 this。

如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

四、优先级

new绑定 > 显示绑定 > 隐式绑定 > 默认绑定

所以我们见到 this ,该根据此优先级规则来判别。

五、被忽略的 this

一些情况下,我们会在显示绑定的时候传入 null,例如Math.max.apply(null,[1,2,3]),我们并不关心传入的 this ,只是传入一个占位置,此时应用默认绑定。 举个例子:

function run() { 
    console.log( this.a );
}
var a = 1; 
run.call(null); // 1

这里建议传入一个更安全的对象Object.create(null),Object.create(null)创建的对象与{}的区别在于,{}上面还会有Object原型上的toString()等方法,而Object.create(null)什么都没有。我喜欢用穷徒四壁来形容{},那么Object.create(null)对比之下就是连四壁也没有。

六、箭头函数下的 this

我们都知道箭头函数没有自己的 this,它的 this 来自于外层作用域,也因为箭头函数没有自己的 this 这一特性,它不能被用作构造函数。 箭头函数不使用上面 this 的四种标准规则,函数执行时,它会捕获外层作用域的 this ,一经绑定再也无法修改,并且高于 new 绑定。

说明

文章更多的内容是来自于《你不知道的JavaScript》上卷,当然加了自己的一些理解。没时间去看书的同学,不妨看看,查漏补缺。
学 React 的同学看完,可以再看下这篇 <理解:为什么React事件处理中要绑定this> ,巩固下对 this 的理解。