【译】this 是什么?JavaScript 对象的内部工作原理
- 原文链接:What is
this
? The Inner Workings of JavaScript Objects (需要梯子) - 原文作者:Eric Elliott
- 译文永久链接:【译】什么是 this?JavaScript 对象的内部工作原理
- 译者:士心
- 翻译目的:函数动态绑定 this 的特性,经常让开发者感到头疼,这篇文章能帮助梳理概念
JavaScript 是一种支持面向对象编程和动态绑定的多范式语言。动态绑定是其一个强大的特性,它允许 JavaScript 代码在运行时更改 this
,但是这一强大而且灵活的特性却会给开发者带来一些困惑,这些困惑集中在 JavaScript 代码运行时的表现上。
动态绑定(Dynamic Binding)
动态绑定指的是在运行时才确定调用函数的方式,而不是更早的编译阶段。JavaScript 通过 this
和原型链来体现动态绑定。也就是说,函数内部的 this
是在运行时确定,并且定义函数的方式不同,确定 this
的规则也不同。
先来玩个游戏。这个游戏叫 “this
是什么?”
const a = { a: 'a' }; const obj = { getThis: () => this, getThis2 () { return this; } }; obj.getThis3 = obj.getThis.bind(obj); obj.getThis4 = obj.getThis2.bind(obj); const answers = [ obj.getThis(), obj.getThis.call(a), obj.getThis2(), obj.getThis2.call(a), obj.getThis3(), obj.getThis3.call(a), obj.getThis4(), obj.getThis4.call(a) ];
在继续之前,请先写下你的答案。完成后,console.log()
你的答案,你答对了吗?
<scirpt type="module">...</script>
让代码运行在 ES6 Module 下。如果你将代码直接复制到浏览器控制台运行,下文说的 undefined
实际上是 window
。让我们从第一个结果开始。obj.getThis()
返回 undefined
,但为什么呢?箭头函数永远不会绑定属于自己的 this
,它们的 this
总是绑定在定义时所在的作用域上。本例中的定义时所在的作用域,就是 ES6 模块的根作用域,这里的 this
是 undefined
。因为相同的原因,obj.getThis.call(a)
同样也是 undefined
。对于箭头函数,它的 this
不能被重新分配赋值,即使使用 .call()
或 .bind()
,它的 this
总是绑定在定义时所在的作用域上,而不会指向运行时所在的作用域。
obj.getThis2()
通过一般的函数调用获取其绑定。如果函数之前没有绑定 this
(就是说,它不是箭头函数),那该函数就可以拥有自己的 this
绑定,具体绑定到使用 .
或 []
调用该方法的对象上。
obj.getThis2.call(a)
做了点小动作,call()
方法提供给定的 this
值和可选参数调用函数。换句话说,函数从 .call()
参数获取 this
的绑定,因此 obj.getThis2.call(a)
返回 a
对象。
使用 obj.getThis3 = obj.getThis.bind(obj);
,我们尝试绑定一个箭头函数,前面我们已经讨论过绑定箭头函数是不起作用的,所以 obj.getThis3()
和 obj.getThis3.call(a)
都得到 undefined
。
我们可以绑定一般的函数,所以 obj.getThis4()
按预期返回 obj
,因为它已经使用 obj.getThis4 = obj.getThis2.bind(obj);
绑定了。而 obj.getThis4.call(a)
遵从第一个的绑定,所以返回 obj
而不是 a
。
加大难度(Curve Ball)
同样的挑战,不过这一次,使用到了 class
的公共字段语法(public fields syntax) (写这篇文章的时候,该语法提案处于 Stage3 阶段。Chrome 和 @babel/plugin-proposal-class-properties
已经支持):
class Obj { getThis = () => this getThis2 () { return this; } } const obj2 = new Obj(); obj2.getThis3 = obj2.getThis.bind(obj2); obj2.getThis4 = obj2.getThis2.bind(obj2); const answers2 = [ obj2.getThis(), obj2.getThis.call(a), obj2.getThis2(), obj2.getThis2.call(a), obj2.getThis3(), obj2.getThis3.call(a), obj2.getThis4(), obj2.getThis4.call(a) ];
在继续之前写下你的答案。
准备好了?
除了 obj2.getThis2.call(a)
返回 a
对象外,其它都返回对象实例。箭头函数的 this
仍然绑定在定义时所在的作用域上,区别在于定义时所在作用域的 this
已然不是 undefined
。这段代码的底层,会将类的属性赋值编译成:
class Obj { constructor() { this.getThis = () => this; } ...
也就是说,箭头函数是在构造函数(constructor)的上下文中定义的。由于它是一个类,创建实例的唯一方法是使用 new
关键字(省略 new
会抛出错误)。
new
关键字会实例化一个新的对象实例,并在执行构造函数时将 this
指向该实例。这种行为,加上我们上面已经提到的其他行为,就能解释清楚结果。
总结
你做得怎样?有没有做对呢?理解了 this
的表现行为,在调试棘手的问题时能节省大量时间。如果你做错了任意一道,那你需要多加练习。研究上面这些例子,然后回来再做一次,直到你都能做对,并向其他人解释为什么这些方法会返回这些值。
如果这些题比你想象的要难,你并不是一个人。针对这个主题,我已经问过了不少开发者,我认为到目前为止只有一位开发人员掌握了。
加上类和箭头函数的行为,会使 .call()
,. bind()
或 .apply()
的动态绑定开始变得复杂。请记住,箭头函数总是将 this
绑定在定义时所在的作用域上,第二个例子 class
中的 this
实际绑定在执行构造函数时的作用域上。如果你还有疑问,请记住使用 debugger 工具来验证是否符合你的想法。
还要记住,在JavaScript中,即使不用 this
,你也可以做很多事情。根据我的经验,几乎任何东西都可以使用纯函数重新实现,纯函数接收所有传递给它们的显式参数(你可以将 this
看成是可变的隐式参数)。封装在纯函数中的逻辑具有确定性,这使得它更易于测试,并且没有副作用。这意味着与操作 this
不同,你不可能破坏其他任何东西。而每当你修改 this
时,依赖于 this
的行为就可能被破坏。
也就是说,this
有时很有用,例如:在大量对象之间共享方法。即使在函数式编程中,this
对于访问其他对象上的函数,以实现在现有函数之上构建新函数是很有用的,例如:.flatMap()
可以通过组合 this.map()
和 this.constructor.of()
来实现。
如果你喜欢这篇文章,请关注我,我会持续输出更多原创且高质量的内容。