嗨,this

247 阅读9分钟

本文原创:liruifang

你是否被this困扰过? 求职时你是否在笔试或者面试的时候被问起过? 你是否在代码中写过self = this?

this,到底是何方神圣?

this是一个很特别的关键字,被自动定义在 所有函数的作用域中 this 关键字是 JavaScript 中最复杂的机制之一

科幻小说家亚瑟·查理斯·克拉克说过一句话:任何足够先进的技术都和魔法无异。

在缺乏清晰认识的情况下,this 完全就是一种魔法

欢迎来到this的魔法世界~

先看几个例子~

1.png

2.png

3.png

4.png

  • 如果你能很快地得到确定的答案,那么恭喜你,你已经是this魔法世界的魔法师了~
  • 如果你不确定或是无从下手,那么请跟随我来~

如何准确判断this指向的是什么?

正经解释:

  1. this 就是一个指针,指向调用函数的对象
  2. this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件
  3. 当一个函数被调用时,会创建一个活动记录(执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的其中一个属性,会在函数执行的过程中用到。
  4. 每个函数的 this 是在调用时被绑定的,完全取决于函数的调用位置
  5. 在函数执行过程中调用位置决定 this 的绑定对象。

听过很多道理 却依然……

不!融于意识、付诸实践的道理,才是你的道理。

透过现象看本质

一、调用位置

二、绑定规则

三、绑定优先级

四、凡事都有例外

一、调用位置

  • 调用位置就是函数在代码中被调用的位置(而不是声明的位置)。
  • 最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。调用位置就在当前正在执行的函数的前一个调用中。
    1-1.png

this 的绑定,使用开发者工具得到调用栈,然后找到栈中第二个元素,这就是真正的调用位置。

1-2.png

  • this 在任何情况下都不指向函数的词法作用域。
  • 本例试图用this 联通 foo() 和 bar() 的词法作用域,从而让 bar() 可以访问 foo() 作用域里的变量 a。这是不可能实现的,你不能使用 this 来引用一 个词法作用域内部的东西。
  • 刚刚分析的只是调用位置,调用方式会决定this的指向,这需要通过具体的规则来判断。

二、绑定规则

(一)默认绑定

2-1.png

  • foo() 是直接使用不带任何修饰的函数引用进行调用的
  • 最常用的函数调用类型:独立函数调用
  • 默认绑定
  • this指向全局对象(非严格模式下)
  • this指向undefined(严格模式下), undefined上没有this对象,会抛出错误
  • 非严格模式下,在浏览器环境中运行,结果是 Hello,2019

(二)隐式绑定

2-2.png

  • 函数的调用是在某个对象上触发的,即调用位置上存在上下文对象
  • 典型的形式为 XXX.fun()
  • 调用位置会使用 obj 上下文来引用函数
  • 隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象
  • 此例foo函数中的this绑定到obj,因此 this.a 和 obj.a 是一样的

2-3.png

Bob.callPerson(John);
Bob called a person named John”
callPerson() 是 Bob 发起的,this 就指向 Bob。

2-4.png

- 答案:16
- 对象属性引用链中只有最顶层或者说最后一层会影响调用位置。 
- 不管有多少层,在判断this的时候,我们只关注最后一层。

2-5.png

- 虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身
- bar() 其实是一个不带任何修饰的函数调用 
- 重复强调:隐式绑定的形式 obj.fun()

(三)显式绑定

通过call、apply、bind的方式,显式地指定this所指向的对象。

2-6.png

  • call,apply和bind的第一个参数,就是对应函数的this所指向的对象。
  • 从 this 绑定的角度来说,call和apply的作用一样,只是传参方式不同。
  • call和apply都会执行对应的函数,bind不会。
    2-7.png
- 答案:number is  2
- 执行fn的时候,相当于直接调用了foo方法(记住: obj.foo已经被赋值给fn了,隐式绑定也丢了),没有指定this的值,对应的是默认绑定。
- 这显然不是我们想要的,怎么办?

调用fn显式的强制绑定,硬绑定

2-8.png

我们创建了函数 Hi(),并在它的内部手动调用 了 fn.call(obj),因此强制把 fn 的 this 绑定到了 obj。无论之后如何调用函数 Hi,它总会手动在 obj 上调用 fn。 

(四)new绑定

思考: new 做了什么?

1. 创建一个新对象
2. 将构造函数的作用域赋值给新对象,即this指向这个新对象
3. 执行构造函数中的代码
4. 返回新对象

2-9.png

foo的this指向bar对象上

绑定规则

(一)默认绑定 var bar = foo()

严格模式下,绑定到undefined,否则绑定到 全局对象。 

(二)隐式绑定 var bar = obj1.foo()

在某个上下文对象中调用,this 绑定的是那个上下文对象。 

(三)显式绑定 var bar = foo.call(obj2)

this绑定的是 指定的对象。 

(四)new绑定 var bar = new foo()

this绑定的是新创建的对象。

三、优先级

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

3-1.png

这样就结束了吗?

世界没你想象的那么简单~~

四、凡事都有例外

将null或undefined作为this的绑定对象传入call、apply或者是bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。

3-2.png

箭头函数

  • ES6 中介绍了一种无法使用 这些规则的特殊函数类型:箭头函数。
  • 箭头函数不使用this 的四种标准规则,而是根据外层(函数或者全局)作用域来决 定 this。
  • 箭头函数按词法作用域来绑定它的上下文,所以 this 实际上会引用到原来的上下文。箭头函数从包含它的词法作用域中继承到了 this 的值。
  • 箭头函数没有自己的this,所以不能用call()、apply()、bind()这些方法去改变this的指向。
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

3-3.png

- 答案:2
- foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1, bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。 

总结

(一)默认绑定 var bar = foo()

严格模式下,绑定到undefined,否则绑定到 全局对象。 

(二)隐式绑定 var bar = obj1.foo()

在某个上下文对象中调用,this 绑定的是那个上下文对象。 

(三)显式绑定 var bar = foo.call(obj2)

this绑定的是 指定的对象。 

(四)new绑定 var bar = new foo()

this绑定的是新创建的对象。

(五)箭头函数

没有自己的this,当前的词法作用域覆盖了 this 本来的值,this继承于外层代码库中的this,且不可被修改。

欢迎再次来到this的魔法世界,让我们一起升级打怪吧~

问题剖析,真相只有一个!

4-1.png

- 答案:
    1 2
- 分析:
    隐式绑定,this指向obj,num是1;
    默认绑定:foo直接指向了foo的引用,和obj无关,是不带任何修饰的函数调用 。

4-2.png

- 答案:hi,jadfe

4-2-1.png

- 分析:
    setTimeout(fn,delay){ fn(); },相当于是将obj.foo赋值给了一个变量,最后执行了变量, foo是不带任何修饰的函数调用这个时候,foo的this显然和obj就没有关系了。

4-3.png

- 答案:
    Hello, Wiliam
    Hello, Wiliam
    Hello, Christina
- 分析:
    ① setTimeout的回调函数中,this使用的是默认绑定,非严格模式下,执行的是全局对象
    ② 上个例子刚分析了,跟setTimeout实现机制有关。
    ③ 第三条虽然也是在setTimeout的回调中,但是我们可以看出,这是执行的是person2.sayHi()使用的是隐式绑定,因此这是this指向的是person2。

4-4.png

- 答案:
    2 2
- 分析:
    箭头函数,this继承于外层的this
    默认绑定:foo是不带任何修饰的函数调用 

4-5.png

- 答案:
    2
- 分析:
    箭头函数常用于回调函数中,this继承于外层foo的this,foo中的this显示绑定到obj上,因此相当于obj.a = a4-5

4-6.png

- 答案:
    Obj对象
    Obj对象
    Window对象
    Window对象
    Window对象
- 分析:
    ① obj.hi(); 隐式绑定,this绑定在obj上,输出obj。
    ② hi(); 执行箭头函数,继承外层作用域的this。
    ③ sayHi(); 隐式绑定丢失的情况,this执行的是默认绑定,指向全局对象window。
    ④ fun1(); 执行的是箭头函数,this是继承于外层代码库的this。外层代码库我们刚刚分析了,this指向的是window,因此这儿的输出结果是window。
    ⑤ obj.say(); 执行的是箭头函数,当前的代码块obj中是不存在this的,只能往上找,就找到了全局的this,指向的是window。

4-7.png

- 答案:
    0 NaN
- 分析:
    看调用位置,显示绑定,
    无意中创建了一个全局变量 count
    这个count值是什么?
    有些地方称值为NaN是错误的,值为undefined,++运算符,类型转换NaN

4-8.png

- 答案:
    5
- 分析:
    将null或undefined作为this的绑定对象传入call、apply或者是bind,
    这些值在调用时会被忽略,实际应用的是默认绑定规则

如何准确判断this指向的是什么?

1. 函数是否是new绑定,如果是,那么this绑定的是新创建的对象。
2. 函数是否通过call,apply,bind调用显式绑定,如果是,那么this绑定的就是指定的对象。
3. 函数是否在某个上下文对象中调用(隐式绑定),一般是obj.foo(),如果是的话,this绑定的是那个上下文对象[注意隐式丢失的情况]。
4. 如果以上都不是,那么使用默认绑定。在严格模式下,则绑定到undefined,否则绑定到全局对象。
5. 如果把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
6. 如果是箭头函数,箭头函数的this继承的是外层代码块的this。

如果你已经非常清楚的知道怎么判断this的指向,那就试试这道题吧~

5-1.png

答案: 
    10 
    9 
    3 
    27 
    20

6.png

参考文献: