2022 年 9 月 21 日

引用类型

深入语言功能

本文涵盖了一个高级主题,以便更好地理解某些极端情况。

这并不重要。许多经验丰富的开发人员在不知道的情况下也能很好地生活。如果您想了解事物的内部运作方式,请继续阅读。

动态求值的函数调用可能会丢失 this

例如

let user = {
  name: "John",
  hi() { alert(this.name); },
  bye() { alert("Bye"); }
};

user.hi(); // works

// now let's call user.hi or user.bye depending on the name
(user.name == "John" ? user.hi : user.bye)(); // Error!

在最后一行有一个条件运算符,它选择 user.hiuser.bye。在这种情况下,结果是 user.hi

然后使用括号 () 立即调用该方法。但它不能正常工作!

如你所见,调用导致错误,因为调用内部的 "this" 值变为 undefined

此方法有效(对象点方法)

user.hi();

此方法无效(已评估方法)

(user.name == "John" ? user.hi : user.bye)(); // Error!

为什么?如果我们想要了解为什么发生这种情况,让我们深入了解 obj.method() 调用如何工作的内部原理。

引用类型说明

仔细观察,我们可能会注意到 obj.method() 语句中有两个操作

  1. 首先,点 '.' 检索属性 obj.method
  2. 然后,括号 () 执行它。

那么,有关 this 的信息如何从第一部分传递到第二部分?

如果我们将这些操作放在单独的行上,那么 this 肯定会丢失

let user = {
  name: "John",
  hi() { alert(this.name); }
};

// split getting and calling the method in two lines
let hi = user.hi;
hi(); // Error, because this is undefined

此处 hi = user.hi 将函数放入变量中,然后在最后一行中它完全独立,因此没有 this

为了使 user.hi() 调用起作用,JavaScript 使用了一个技巧——点 '.' 返回的不是函数,而是特殊 引用类型 的值。

引用类型是一种“规范类型”。我们无法显式使用它,但语言内部使用它。

引用类型的值是三值组合 (base, name, strict),其中

  • base 是对象。
  • name 是属性名称。
  • 如果 use strict 生效,则 strict 为 true。

属性访问 user.hi 的结果不是函数,而是引用类型的值。对于严格模式中的 user.hi,它为

// Reference Type value
(user, "hi", true)

当在引用类型上调用括号 () 时,它们会收到有关对象及其方法的完整信息,并且可以设置正确的 this(在本例中为 user)。

引用类型是一种特殊的“中间”内部类型,其目的是将信息从点 . 传递到调用括号 ()

任何其他操作(如赋值 hi = user.hi)都会整体丢弃引用类型,获取 user.hi 的值(函数)并将其传递。因此,任何进一步的操作都会“丢失”this

因此,结果是,只有在使用点 obj.method() 或方括号 obj['method']() 语法直接调用函数时,才会正确传递 this 的值(它们在此处执行相同操作)。有多种方法可以解决此问题,例如 func.bind()

摘要

引用类型是语言的内部类型。

读取属性(例如使用点 .obj.method() 中)不会返回属性值,而是返回一个特殊的“引用类型”值,该值存储属性值和从中获取该值的属性。

这是为了后续方法调用 () 获取对象并将 this 设置为该对象。

对于所有其他操作,引用类型会自动变为属性值(在本例中为函数)。

整个机制对我们来说是隐藏的。它只在一些微妙的情况下很重要,例如使用表达式从对象动态获取方法时。

任务

重要性:2

这段代码的结果是什么?

let user = {
  name: "John",
  go: function() { alert(this.name) }
}

(user.go)()

P.S. 有个陷阱 :)

错误!

尝试一下

let user = {
  name: "John",
  go: function() { alert(this.name) }
}

(user.go)() // error!

大多数浏览器中的错误消息并未给我们提供太多关于出错原因的线索。

错误出现是因为在 user = {...} 之后缺少分号。

JavaScript 不会在括号 (user.go)() 之前自动插入分号,因此它将代码解读为

let user = { go:... }(user.go)()

然后我们还可以看到,这样的联合表达式在语法上是将对象 { go: ... } 作为函数调用,参数为 (user.go)。而且这也发生在与 let user 相同的行上,因此 user 对象甚至尚未定义,因此出现错误。

如果我们插入分号,一切都会好起来

let user = {
  name: "John",
  go: function() { alert(this.name) }
};

(user.go)() // John

请注意,括号 (user.go) 在这里不起作用。通常,它们设置运算顺序,但这里点 . 首先起作用,因此没有效果。只有分号很重要。

重要性:3

在下面的代码中,我们打算连续 4 次调用 obj.go() 方法。

但是调用 (1)(2)(3)(4) 的工作方式不同。为什么?

let obj, method;

obj = {
  go: function() { alert(this); }
};

obj.go();               // (1) [object Object]

(obj.go)();             // (2) [object Object]

(method = obj.go)();    // (3) undefined

(obj.go || obj.stop)(); // (4) undefined

以下是解释。

  1. 这是一个常规的对象方法调用。

  2. 同样,括号不会改变这里的运算顺序,无论如何,点始终是优先的。

  3. 这里我们有一个更复杂的调用 (expression)()。调用就像将其拆分为两行一样

    f = obj.go; // calculate the expression
    f();        // call what we have

    这里 f() 作为一个函数执行,没有 this

  4. (3) 类似,在括号 () 的左侧,我们有一个表达式。

要解释 (3)(4) 的行为,我们需要回想一下属性访问器(点或方括号)返回引用类型的值。

除了方法调用(如赋值 =||)之外,对它的任何操作都会将其变为普通值,该值不携带允许设置 this 的信息。

教程地图

评论

评论前请阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容 - 请详细说明。
  • 要插入几个单词的代码,请使用 <code> 标记,对于多行 - 将它们包装在 <pre> 标记中,对于超过 10 行 - 使用沙箱(plnkrjsbincodepen…)