this 陷阱与原理

前言

this 在很多地方都有解释。听得最多的说法就是 this 是由调用的位置来决定的。话虽然没有错,但是什么是调用的位置,还是非常含糊不清。

另外,this 绑定中有

  • 显式绑定

  • 隐式绑定

  • 默认绑定

又分别是什么?看定义的名称都非常的抽象。如果我们仅仅是面试前记住对应的概念,而不知道其中的原理,这种知识会非常容易遗忘。

陷阱

const obj = {
	foo: function () {
		console.log(this)
	}
}

const foo = obj.foo

// 写法一
obj.foo()

// 写法二
foo()

答案:

写法一的 this 指向 obj 对象,但是写法二的obj指向 window 对象 / undefined

很多教科书会说,this指的是函数运行时所在的环境。对于obj.foo()来说,foo运行在obj环境,所以this指向obj;对于foo()来说,foo运行在全局环境,所以this指向全局环境。所以,两者的运行结果不一样。

这种解释没错,但是教科书往往不告诉你,为什么会这样?也就是说,函数的运行环境到底是怎么决定的?举例来说,为什么obj.foo()就是在obj环境执行,而一旦var foo = obj.foofoo()就变成在全局环境执行?

原理

定义的对象的 JS 过程

JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj

变量obj是一个地址(reference)。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。

原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo属性,实际上是以下面的形式保存的。

在 ES5 中模拟实现 const,也即通过 Object.defineProperty 来改变 writable 属性

上面的例子中,value 一个数值,但是,value也可以是一个函数。

实际示例

单独运行

对象运行

总结

回到本文开头提出的问题,obj.foo()是通过obj找到foo,所以就是在obj环境执行。一旦var foo = obj.foo,变量foo就直接指向函数的地址,所以foo()就变成在全局环境执行。

延伸1 - this 的优先级

new > call / apply / bind (显示绑定) > obj call (隐式绑定)> function call (默认绑定)

下面将阐述为什么有这样的优先级。

call / apply

apply和call所做的事情都相同,那就是改变函数内部的this指向并调用它。

模拟实现 call

new

new运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new 关键字会进行如下的操作:

  1. 创建一个空的简单JavaScript对象(即{}

  2. 链接该对象(即设置该对象的构造函数)到另一个对象

  3. 将步骤1新创建的对象作为this的上下文

  4. 如果该函数没有返回对象,则返回this。

延伸2 - class this 调用陷阱

😒 问题:对象解构后丢失 this 作用域

😁 因此有这样的改写

延伸3 - 回调函数的 this 陷阱

箭头函数

箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this,并且形成绑定关系

箭头函数中的this是在定义函数的时候绑定(词法作用域),而不是在执行函数的时候绑定

于是 class 中的 this 调用改写

对象方法中的 this 改写

再次总结

  1. this 指向当前运行环境下调用的对象

  2. this 的执行可以被 call/apply/bind 改写

  3. 箭头函数从句法上定义 this 指向,不能被 call/apply/bind 修改

最后更新于

这有帮助吗?