《你不知道的JavaScript(上卷)》读书笔记

你不知道的 JavaScript(上卷)

凯尔辛普森

81 个笔记

点评

underline

认为好看

非常好的一本书,作者把 JavaScript 中的一些难点讲的非常透彻,包括作用域、this 、对象、Prototype 和委托,值得仔细阅读理解!本书翻译作者也是鬼才,翻译的很接地气,不像有些技术书籍翻译的很晦涩,哈哈哈哈哈!

1.2 理解作用域

underline

引擎、编译器和作用域是干什么的

· 引擎从头到尾负责整个 JavaScript 程序的编译及执行过程。· 编译器引擎的好朋友之一,负责语法分析及代码生成等脏活累活(详见前一节的内容)。· 作用域引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

underline

变量赋值操作的两个动作

总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。

underline

LHS 查询和 RHS 查询

RHS 查询与简单地查找某个变量的值别无二致,而 LHS 查询则是试图找到变量的容器本身,从而可以对其赋值。从这个角度说,RHS 并不是真正意义上的“赋值操作的右侧”,更准确地说是“非左侧”。

underline

LHS 和 RHS 的含义是“赋值操作的左侧或右侧”并不一定意味着就是“=赋值操作符的左侧或右侧”。赋值操作还有其他几种形式,因此在概念上最好将其理解为“赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头(RHS)”。

underline

这段翻译的太接地气,太幽默了,哈哈哈哈哈!

引擎:我说作用域,我需要为 foo 进行 RHS 引用。你见过它吗?作用域:别说,我还真见过,编译器那小子刚刚声明了它。它是一个函数,给你。引擎:哥们太够意思了!好吧,我来执行一下 foo。引擎:作用域,还有个事儿。我需要为 a 进行 LHS 引用,这个你见过吗?作用域:这个也见过,编译器最近把它声名为 foo 的一个形式参数了,拿去吧。引擎:大恩不言谢,你总是这么棒。现在我要把 2 赋值给 a。引擎:哥们,不好意思又来打扰你。我要为 console 进行 RHS 引用,你见过它吗?作用域:咱俩谁跟谁啊,再说我就是干这个。这个我也有,console 是个内置对象。给你。引擎:么么哒。我得看看这里面是不是有 log(..)。太好了,找到了,是一个函数。引擎:哥们,能帮我再找一下对 a 的 RHS 引用吗?虽然我记得它,但想再确认一次。作用域:放心吧,这个变量没有变动过,拿走,不谢。引擎:真棒。我来把 a 的值,也就是 2,传递进 log(..)。……

1.4 异常

underline

ReferenceError 同作用域判别失败相关,而 TypeError 则代表作用域判别成功了,但是对结果的操作是非法或不合理的。

1.5 小结

underline

作用域小结

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。赋值操作符会导致 LHS 查询。=操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。JavaScript 引擎首先会在代码执行前对其进行编译,在这个过程中,像 var a = 2 这样的声明会被分解成两个独立的步骤:1.首先,var a 在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。2.接下来,a = 2 会查询(LHS 查询)变量 a 并对其进行赋值。LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层楼),最后抵达全局作用域(顶层),无论找到或没找到都将停止。不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛出 ReferenceError 异常(严格模式下)。

2.1 词法阶段

underline

作用域查找的过程

作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。

3.1 函数中的作用域

underline

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。这种设计方案是非常有用的,能充分利用 JavaScript 变量可以根据需要改变值类型的“动态”特性。

3.2 隐藏内部实现

underline

可以把变量和函数包裹在一个函数的作用域中,然后用这个作用域来“隐藏”它们。

实际的结果就是在这个代码片段的周围创建了一个作用域气泡,也就是说这段代码中的任何声明(变量或函数)都将绑定在这个新创建的包装函数的作用域中,而不是先前所在的作用域中。换句话说,可以把变量和函数包裹在一个函数的作用域中,然后用这个作用域来“隐藏”它们。

underline

最小授权或最小暴露原则

它们大都是从最小特权原则中引申出来的,也叫最小授权或最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的 API 设计。

underline

1.全局命名空间

3.3 函数作用域

underline

[插图][插图]

underline

区分函数声明和表达式的方法

区分函数声明和表达式最简单的方法是看 function 关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果 function 是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

underline

匿名函数表达式的缺点

匿名函数表达式书写起来简单快捷,很多库和工具也倾向鼓励使用这种风格的代码。但是它也有几个缺点需要考虑。1.匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。2.如果没有函数名,当函数需要引用自身时只能使用已经过期的 arguments.callee 引用,比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑自身。3.匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。

underline

3.3.2 立即执行函数表达式[插图]由于函数被包含在一对()括号内部,因此成为了一个表达式,通过在末尾加上另外一个()可以立即执行这个函数,比如(function foo(){ .. })()。第一个()将函数变成表达式,第二个()执行了这个函数。

underline

这种模式很常见,几年前社区给它规定了一个术语:IIFE,代表立即执行函数表达式(Immediately Invoked Function Expression);

underline

相较于传统的 IIFE 形式,很多人都更喜欢另一个改进的形式:(function(){ .. }())。仔细观察其中的区别。第一种形式中函数表达式被包含在()中,然后在后面用另一个()括号来调用。第二种形式中用来调用的()括号被移进了用来包装的()括号中。这两种形式在功能上是一致的。选择哪个全凭个人喜好。

3.4 块作用域

underline

let 关键字可以将变量绑定到所在的任意作用域中(通常是{ .. }内部)。换句话说,let 为其声明的变量隐式地劫持了所在的块作用域。

underline

提升是指声明会被视为存在于其所出现的作用域的整个范围内。

underline

但是使用 let 进行的声明不会在块作用域中进行提升。声明的代码被运行之前,声明并不“存在”。[插图]

4.2 编译器再度来袭

underline

因此,打个比方,这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动”到了最上面。这个过程就叫作提升。换句话说,先有蛋(声明)后有鸡(赋值)。[插图] 只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。如果提升改变了代码执行的顺序,会造成非常严重的破坏。

4.4 小结

underline

这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。

underline

声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。要注意避免重复声明,特别是当普通的 var 声明和函数声明混合在一起的时候,否则会引起很多危险的问题!

5.1 启示

underline

我现在就是这种感觉,好期待作者可以教我掌握闭包的概念!!!

对于那些有一点 JavaScript 使用经验但从未真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生,但是需要付出非常多的努力和牺牲才能理解这个概念。回忆我前几年的时光,大量使用 JavaScript 但却完全不理解闭包是什么。总是感觉这门语言有其隐蔽的一面,如果能够掌握将会功力大涨,但讽刺的是我始终无法掌握其中的门道。

5.2 实质问题

underline

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

underline

这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。

5.3 现在我懂了

underline

给译者点个赞,翻译的真好!

尽管技术上来讲,闭包是发生在定义时的,但并不非常明显,就好像六祖慧能所说:“既非风动,亦非幡动,仁者心动耳。”[插图]

5.4 循环和闭包

underline

这里引伸出一个更深入的问题,代码中到底有什么缺陷导致它的行为同语义所暗示的不一致呢?缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。这样说的话,当然所有函数共享一个 i 的引用。循环结构让我们误以为背后还有更复杂的机制在起作用,但实际上没有。如果将延迟函数的回调重复定义五次,完全不使用循环,那它同这段代码是完全等价的。下面回到正题。缺陷是什么?我们需要更多的闭包作用域,特别是在循环的过程中每个迭代都需要一个闭包作用域。

underline

在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

5.5 模块

underline

如果要更简单的描述,模块模式需要具备两个必要条件。1.必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。2.封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

5.6 小结

underline

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

underline

模块有两个主要特征:(1)为创建内部作用域而调用了一个包装函数;(2)包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。

附录 A 动态作用域

underline

主要区别:词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。(this 也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。

附录 B 块作用域的替代方案

underline

哈哈哈哈哈,本文翻译真是鬼才~~~

“但是,”你可能会说,“鬼才要写这么丑陋的代码!”没错,没人写的代码像 CoffeeScript 编译器输出的代码,但这不是重点。

第 1 章 关于 this

underline

经典!

任何足够先进的技术都和魔法无异。

1.2 误解

underline

说的不就是我嘛……(╯﹏╰)b

遇到这样的问题时,许多开发者并不会深入思考为什么 this 的行为和预期的不一致,也不会试图回答那些很难解决但却非常重要的问题。他们只会回避这个问题并使用其他方法来达到目的,比如创建另一个带有 count 属性的对象。

underline

2023 跳出舒适区!!!

从某种角度来说这个方法确实“解决”了问题,但可惜它忽略了真正的问题——无法理解 this 的含义和工作原理——而是返回舒适区,使用了一种更熟悉的技术:词法作用域。

underline

如果要从函数对象内部引用它自身,那只使用 this 是不够的。一般来说你需要通过一个指向函数对象的词法标识符(变量)来引用它。

underline

第一个函数被称为具名函数,在它内部可以使用 foo 来引用自身。但是在第二个例子中,传入 setTimeout(..)的回调函数没有名称标识符(这种函数被称为匿名函数),因此无法从函数内部引用自身。[插图] 还有一种传统的但是现在已经被弃用和批判的用法,是使用 arguments. callee 来引用当前正在运行的函数对象。这是唯一一种可以从匿名函数对象内部引用自身的方法。然而,更好的方式是避免使用匿名函数,至少在需要自引用时使用具名函数(表达式)。arguments.callee 已经被弃用,不应该再使用它。

1.4 小结

underline

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

2.1 调用位置

underline

亲测谷歌浏览器调试工具最右边中间有个 Call Stack 可以显示调用栈!

你可以把调用栈想象成一个函数调用链,就像我们在前面代码段的注释中所写的一样。但是这种方法非常麻烦并且容易出错。另一个查看调用栈的方法是使用浏览器的调试工具。绝大多数现代桌面浏览器都内置了开发者工具,其中包含 JavaScript 调试器。就本例来说,你可以在工具中给 foo()函数的第一行代码设置一个断点,或者直接在第一行代码之前插入一条 debugger;语句。运行代码时,调试器会在那个位置暂停,同时会展示当前位置的函数调用列表,这就是你的调用栈。因此,如果你想要分析 this 的绑定,使用开发者工具得到调用栈,然后找到栈中第二个元素,这就是真正的调用位置。

2.2 绑定规则

underline

对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。

underline

隐式丢失一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。

underline

JavaScript 中的“所有”函数都有一些有用的特性(这和它们的[[Prototype]]有关——之后我们会详细介绍原型),可以用来解决这个问题。具体点说,可以使用函数的 call(..)和 apply(..)方法。严格来说,JavaScript 的宿主环境有时会提供一些非常特殊的函数,它们并没有这两个方法。但是这样的函数非常罕见,JavaScript 提供的绝大多数函数以及你自己创建的所有函数都可以使用 call(..)和 apply(..)方法。这两个方法是如何工作的呢?它们的第一个参数是一个对象,是给 this 准备的,接着在调用函数时将其绑定到 this。因为你可以直接指定 this 的绑定对象,因此我们称之为显式绑定。

underline

如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new String(..)、new Boolean(..)或者 new Number(..))。这通常被称为“装箱”。

underline

硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值:

underline

另一种使用方法是创建一个可以重复使用的辅助函数:

underline

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。1.创建(或者说构造)一个全新的对象。2.这个新对象会被执行[[Prototype]]连接。3.这个新对象会绑定到函数调用的 this。4.如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

2.3 优先级

underline

这种 bind(..)是一种 polyfill 代码(polyfill 就是我们常说的刮墙用的腻子,polyfill 代码主要用于旧浏览器的兼容,比如说在旧的浏览器中并没有内置 bind 函数,因此可以使用 polyfill 代码在旧浏览器中实现新的功能),对于 new 使用的硬绑定函数来说,这段 polyfill 代码和 ES5 内置的 bind(..)函数并不完全相同(后面会介绍为什么要在 new 中使用硬绑定函数)。由于 polyfill 并不是内置函数,所以无法创建一个不包含.prototype 的函数,因此会具有一些副作用。如果你要在 new 中使用硬绑定函数并且依赖 polyfill 代码的话,一定要非常小心。

underline

判断 this 现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的顺序来进行判断:1.函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。[插图]2.函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是指定的对象。[插图]3.函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。[插图]4.如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象。[插图]就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白 this 的绑定原理了。不过……凡事总有例外。

2.4 绑定例外

underline

注意:对于默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到 undefined,否则 this 会被绑定到全局对象。

2.5 this 词法

underline

箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。

2.6 小结

underline

如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。1.由 new 调用?绑定到新创建的对象。2.由 call 或者 apply(或者 bind)调用?绑定到指定的对象。3.由上下文对象调用?绑定到那个上下文对象。4.默认:在严格模式下绑定到 undefined,否则绑定到全局对象。一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑定,你可以使用一个 DMZ 对象,比如 ø = Object.create(null),以保护全局对象。ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。

3.3 内容

underline

在对象中,属性名永远都是字符串。如果你使用 string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串。即使是数字也不例外,虽然在数组下标中使用的的确是数字,但是在对象属性名中数字会被转换成字符串,所以当心不要搞混对象和数组中数字的用法:

underline

给数组添加属性,数组的 length 值不会发生改变

可以看到虽然添加了命名属性(无论是通过.语法还是[]语法),数组的 length 值并未发生变化。

underline

你完全可以把数组当作一个普通的键/值对象来使用,并且不添加任何数值索引,但是这并不是一个好主意。数组和普通的对象都根据其对应的行为和用途进行了优化,所以最好只用对象来存储键/值对,只用数组来存储数值下标/值对

underline

注意:如果你试图向数组添加一个属性,但是属性名“看起来”像一个数字,那它会变成一个数值下标(因此会修改数组的内容而不是添加一个属性):

underline

如你所见,这个普通的对象属性对应的属性描述符(也被称为“数据描述符”,因为它只保存一个数据值)可不仅仅只是一个 2。它还包含另外三个特性:writable(可写)、enumerable(可枚举)和 configurable(可配置)。

underline

在创建普通属性时属性描述符会使用默认值,我们也可以使用 Object.defineProperty(..)来添加一个新属性或者修改一个已有属性(如果它是 configurable)并对特性进行设置。

underline

1.Writablewritable 决定是否可以修改属性的值。

underline

2.Configurable 只要属性是可配置的,就可以使用 defineProperty(..)方法来修改属性描述符:

underline

把 configurable 修改成 false 是单向操作,无法撤销!

underline

3.Enumerable 这里我们要介绍的最后一个属性描述符(还有两个,我们会在介绍 getter 和 setter 时提到)是 enumerable。从名字就可以看出,这个描述符控制的是属性是否会出现在对象的属性枚举中,比如说 for..in 循环。如果把 enumerable 设置成 false,这个属性就不会出现在枚举中,虽然仍然可以正常访问它。相对地,设置成 true 就会让它出现在枚举中。

underline

1.对象常量结合 writable:false 和 configurable:false 就可以创建一个真正的常量属性(不可修改、重定义或者删除):

underline

2.禁止扩展如果你想禁止一个对象添加新属性并且保留已有属性,可以使用 Object.prevent Extensions(..):

underline

3.密封 Object.seal(..)会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions(..)并把所有现有属性标记为 configurable:false。所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。

underline

4.冻结 Object.freeze(..)会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal(..)并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值。

underline

in 操作符会检查属性是否在对象及其[[Prototype]]原型链中(参见第 5 章)。相比之下,hasOwnProperty(..)只会检查属性是否在 myObject 对象中,不会检查[[Prototype]]链。在第 5 章讲解[[Prototype]]时我们会详细介绍这两者的区别。

underline

原因是“可枚举”就相当于“可以出现在对象属性的遍历中”。

underline

在数组上应用 for..in 循环有时会产生出人意料的结果,因为这种枚举不仅会包含所有数值索引,还会包含所有可枚举属性。最好只在对象上应用 for..in 循环,如果要遍历数组就使用传统的 for 循环来遍历数值索引。

underline

propertyIsEnumerable(..)会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足 enumerable:true。Object.keys(..)会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(..)会返回一个数组,包含所有属性,无论它们是否可枚举。in 和 hasOwnProperty(..)的区别在于是否查找[[Prototype]]链,然而,Object.keys(..)和 Object.getOwnPropertyNames(..)都只会查找对象直接包含的属性。(目前)并没有内置的方法可以获取 in 操作符使用的属性列表(对象本身的属性以及[[Prototype]]链中的所有属性,参见第 5 章)。不过你可以递归遍历某个对象的整条[[Prototype]]链并保存每一层中使用 Object.keys(..)得到的属性列表——只包含可枚举属性。

3.4 遍历

underline

ES5 中增加了一些数组的辅助迭代器,包括 forEach(..)、every(..)和 some(..)。每种辅助迭代器都可以接受一个回调函数并把它应用到数组的每个元素上,唯一的区别就是它们对于回调函数返回值的处理方式不同。forEach(..)会遍历数组中的所有值并忽略回调函数的返回值。every(..)会一直运行直到回调函数返回 false(或者“假”值), some(..)会一直运行直到回调函数返回 true(或者“真”值)。every(..)和 some(..)中特殊的返回值和普通 for 循环中的 break 语句类似,它们会提前终止遍历。使用 for..in 遍历对象是无法直接获取属性值的,因为它实际上遍历的是对象中的所有可枚举属性,你需要手动获取属性值。

4.4 混入

underline

mixin() 函数

回顾一下之前提到的 mixin(..)函数:[插图][插图]现在我们来分析一下 mixin(..)的工作原理。它会遍历 sourceObj(本例中是 Vehicle)的属性,如果在 targetObj(本例中是 Car)没有这个属性就会进行复制。由于我们是在目标对象初始化之后才进行复制,因此一定要小心不要覆盖目标对象的原有属性。

underline

一定要注意,只在能够提高代码可读性的前提下使用显式混入,避免使用增加代码理解难度或者让对象关系更加复杂的模式。

underline

如果使用混入时感觉越来越困难,那或许你应该停止使用它了。实际上,如果你必须使用一个复杂的库或者函数来实现这些细节,那就标志着你的方法是有问题的或者是不必要的

5.3 (原型)继承

underline

下面是第二种判断[[Prototype]]反射的方法,它更加简洁:[插图]

underline

我们也可以直接获取一个对象的[[Prototype]]链。在 ES5 中,标准的方法是:[插图]

5.4 对象关联

underline

原型链

现在我们知道了,[[Prototype]]机制就是存在于对象中的一个内部链接,它会引用其他对象。通常来说,这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就会继续在[[Prototype]]关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的[[Prototype]],以此类推。这一系列对象的链接被称为“原型链”。

5.5 小结

underline

如果要访问对象中并不存在的一个属性,[[Get]]操作(参见第 3 章)就会查找对象内部[[Prototype]]关联的对象。这个关联关系实际上定义了一条“原型链”(有点像嵌套的作用域链),在查找属性时会对它进行遍历。所有普通对象都有内置的 Object.prototype,指向原型链的顶端(比如说全局作用域),如果在原型链中找不到指定的属性就会停止。toString()、valueOf()和其他一些通用的功能都存在于 Object.prototype 对象上,因此语言中所有的对象都可以使用它们。关联两个对象最常用的方法是使用 new 关键词进行函数调用,在调用的 4 个步骤(第 2 章)中会创建一个关联其他对象的新对象。使用 new 调用函数时会把新对象的.prototype 属性关联到“其他对象”。带 new 的函数调用通常被称为“构造函数调用”,尽管它们实际上和传统面向类语言中的类构造函数不一样。虽然这些 JavaScript 机制和传统面向类语言中的“类初始化”和“类继承”很相似,但是 JavaScript 中的机制有一个核心区别,那就是不会进行复制,对象之间是通过内部的[[Prototype]]链关联的。出于各种原因,以“继承”结尾的术语(包括“原型继承”)和其他面向对象的术语都无法帮助你理解 JavaScript 的真实机制(不仅仅是限制我们的思维模式)。相比之下,“委托”是一个更合适的术语,因为对象之间的关系不是复制而是委托。

微信读书