为了理解某种特定的边界情况,这篇文章涉及到了一个高级主题。
没关系,好多有经验的开发者即使不知道这部分知识,也能做好工作。但如果你想了解它们是如何运行的,就读下去吧。
动态求值的方法调用可能丢失 this
。
例如:
let user = {
name: "John",
hi() { alert(this.name); },
bye() { alert("Bye"); }
};
user.hi(); // 正常
// 现在,让我们根据 name 的值来调用 user.hi 或者 user.bye
*!*
(user.name == "John" ? user.hi : user.bye)(); // Error!
*/!*
在最后一行,有一个三元运算符来选择 user.hi
或者 user.bye
的值。在这种情况下,结果是 user.hi
。
之后,这个方法被括号 ()
立即调用。但它并没有正确运行!
正如你所看到的,调用的结果是一个 error ,因为在调用中 this
的值变成了 undefined
。
第七行代码运行正常 (以对象名加点的形式调用):
user.hi();
而这一行就不行 (被赋值的方法):
(user.name == "John" ? user.hi : user.bye)(); // Error!
为什么? 如果我们想了解到底发生了什么, 就让我们深入了解一下 obj.method()
调用的工作原理.
仔细观察,我们观察到在 obj.method()
声明中有两步运算:
- 首先,点运算符
'.'
检索到对象属性obj.method
。 - 然后,括号运算符
()
执行它。
所以,有关 this
的信息如何从第一步传递到第二步的呢?
假如我们把这些运算分成两行进行,那么 this
就会丢失:
let user = {
name: "John",
hi() { alert(this.name); }
}
*!*
// 把获取和调用方法分成两行
let hi = user.hi;
hi(); // Error, 因为 this 值为 undefined
*/!*
这里的 hi = user.hi
把函数传递给变量,而后,最后一行的执行是完全独立的,所以这里没有 this
值。
为了让 user.hi()
工作正常,JavaScript 使用了一种技巧——点运算符 '.'
。它的返回值不是一个函数,而是一种特殊的 引用类型。
引用类型是一种 "规范类型"。我们不能显式地使用它, 它被使用在语言内部。
引用类型的值是一种三个值的组合 (base, name, strict)
:
base
是一个对象name
表示一个属性名(String 或者 Symbol 类型)strict
标识符,如果它的值为真,则代表使用严格模式(use strict
)。
访问一个属性 user.hi
的结果不是一个函数,而是引用类型的值. For user.hi
in strict mode it is:
// 引用类型的值如下
(user, "hi", true)
当括号运算符 ()
在引用类型后被使用, 他就接收到了对象及其方法得完整信息,并且设置正确的 this
(在这种情况下是 =user
)。
引用类型是一种特殊的「中介」内部类型,它的作用是从点运算符 .
到括号调用 ()
传递信息。
任何其他的运算,例如赋值运算 hi = user.hi
,会整体丢弃引用类型,而只取属性的值 user.hi
(在这里是一个函数) 并传递下去,所以,在这之后,任何进一步的操作都会「丢失」this
.
因此,this
的值只会在函数被点运算符 obj.method()
或中括号语法 obj['method']()
直接调用时正确传递(在这里它们做了相同的事)。在本教程的后面,我们会学习用多种方法解决这个问题,例如 func.bind().
引用类型 Referance Type
是一种语言的内部类型。
当读取一个属性的时候,例如 obj.method()
这个语句中的点运算符 .
,返回的并不完全是属性值,而是一个特殊的「引用类型」值,这个值存储了属性值和属性值所属的对象。
这是为了接下来的方法调用 ()
能够得到对象并正确的设置 this
值。
对于其他所有的操作,引用类型会自动转换为属性值(在我们的例子中是一个函数)。
这一套原理我们肉眼不可见。它只在一些微妙的情况下起作用,比如使用表达式从对象动态获取方法时。
点运算符 .
的结果事实上并不是一个方法,而是一种 需要某种方法去传递 obj 的信息
的值。