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

写在前面

  • 书籍介绍:JavaScript这门语言简单易用,很容易上手,但其语言机制复杂微妙,即使是经验丰富的JavaScript开发人员,如果没有认真学习的话也无法真正理解。本套书直面当前JavaScript开发人员不求甚解的大趋势,深入理解语言内部的机制,全面介绍了JavaScript中常被人误解和忽视的重要知识点。
  • 我的简评:《你不知道的JavaScript》系列分上中下三卷,这里是上卷,主要讲解作用域、原型等核心概念相关的。该系列书籍本人觉得就上卷写的不错,中卷有些冗余,下卷讲ES6比较粗糙。这里推荐大家对上卷进行细读。
  • !!文末有pdf书籍、笔记思维导图、随书代码打包下载地址,需要请自取

第一章 作用域是什么

1.1.编译原理

  • 编译三个步骤:1、分词/词法分析;2、解析/语法分析;3、代码生成
  • 分词/词法分析(Tokenizing/Lexing):将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)
  • 解析/语法分析(Parsing):将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树
  • 代码生成:将 AST 转换为可执行代码的过程
  • JavaScript大部分情况下编译发生在代码执行前的几微秒
  • JavaScript用尽各种办法去保证性能最佳,比如JIT可以延迟编译设置重编译

1.2.理解作用域

  • 引擎:负责整个JavaScript程序的编译及执行过程
  • 编译器负责语法分析及代码生成等
  • 作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限
  • var a = 2; 引擎认为这里有两个完全不同的声明, 一个由编译器在编译时处理, 另一个则由引擎在运行时处理
  • 伪代码进行概括:“为一个变量分配内存, 将其命名为 a, 然后将值 2 保存进这个变量
  • 当变量出现在赋值操作的左侧时进行 LHS 查询, 出现在右侧时进行 RHS 查询
  • RHS 查询与简单地查找某个变量的值别无二致,而 LHS 查询则是试图找到变量的容器本身, 从而可以对其赋值
  • LHS查询,例如a=2,对变量赋值
  • RHS查询,例如console.log(a),获取变量的值
  • 加强理解,LHS赋值操作的目标是谁,RHS谁是赋值操作的源头
  • 在严格模式中LHS查询失败时,并不会创建并返回一个全局变量,引擎会抛出同RHS查询失败时类似的ReferenceError异常

1.3.作用域嵌套

  • 在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止

1.4.异常

  • 不成功的 RHS 引用会导致抛出 ReferenceError 异常。 不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下), 该变量使用 LHS 引用的目标作为标识符, 或者抛出 ReferenceError 异常(严格模式下)

第二章 词法作用域

2.1.词法阶段

  • 词法作用域就是定义在词法阶段的作用域。 换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)
  • 作用域查找会在找到第一个匹配的标识符时停止
  • 词法作用域:定义在词法阶段的作用域,动态作用域:作用域气泡,严格包含的,没有任何函数可以部分地出现在2个父级函数中
  • 无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定
  • 词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的
  • 词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用

2.2.欺骗词法

  • eval和with
  • JavaScript 中的 eval(…) 函数可以接受一个字符串为参数, 并将其中的内容视为好像在书写时就存在于程序中这个位置的代码
  • 在严格模式的程序中, eval(…) 在运行时有其自己的词法作用域, 意味着其中的声明无法修改所在的作用域
  • eval执行字符串,使其可以在运行期修改书写期的词法作用域,类似的还有setTimeout和SetInterval第一个参数传入字符串的情况
  • setTimeout(…) 和setInterval(…) 的第一个参数可以是字符串, 字符串的内容可以被解释为一段动态生成的函数代码
  • 在严格模式中,eval()在运行时有其自己的词法作用域,无法修改所在的作用域
  • new Function(..) 函数的行为也很类似, 最后一个参数可以接受代码字符串,并将其转化为动态生成的函数
  • with 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域, 因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符
  • 尽管 with 块可以将一个对象处理为词法作用域, 但是这个块内部正常的 var 声明并不会被限制在这个块的作用域中, 而是被添加到 with 所处的函数作用域中
  • with声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域,这两个机制(eval和with)的副作用是引擎无法在编译中对作用域查找进行优化
  • JavaScript 引擎会在编译阶段进行数项的性能优化。 其中有些优化依赖于能够根据代码的词法进行静态分析, 并预先确定所有变量和函数的定义位置, 才能在执行过程中快速找到标识符
  • 最悲观的情况是如果出现了 eval(…) 或 with, 所有的优化可能都是无意义的, 因此最简单的做法就是完全不做任何优化

第三章 函数作用域和块作用域

3.1.函数中的作用域

  • 函数作用域的含义是指属于这个函数的全部变量都可以在整个函数的范围内使用及复用(包括嵌套的作用域)

3.2.隐藏内部实现

  • 最小授权或最小暴露原则:是指在软件设计中, 应该最小限度地暴露必要内容,而将其他内容都“隐藏” 起来,比如某个模块或对象的 API 设计
  • 变量冲突的一个典型例子存在于全局作用域中。当程序中加载了多个第三方库时,如果它们没有妥善地将内部私有的函数或变量隐藏起来,就会很容易引发冲突

3.3.函数作用域

  • 区分函数声明和函数表达式最简单方法看function关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置) 5
  • 函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处
  • (function foo(){ .. }) 作为函数表达式意味着 foo 只能在 .. 所代表的位置中被访问,外部作用域则不行
  • 匿名函数表达式几个缺点需要考虑:1.匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难;2.如果没有函数名,当函数需要引用自身时只能使用已经过期的 arguments.callee 引用,比如在递归中;3.匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可以让代码不言自明

3.4.块作用域

  • 变量的声明应该距离使用的地方越近越好,并最大限度地本地化
  • 块作用域是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息扩展为在块中隐藏信息
  • 用with从对象中创建出的作用域仅在with声明中而非外部作用域中有效
  • try/catch的catch分句会创建一个块作用域,其中声明的变量仅在catch内部有效
  • let 关键字可以将变量绑定到所在的任意作用域中(通常是 { .. } 内部)
  • for 循环头部的 let 不仅将 i 绑定到了 for 循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值
  • Tracer,Google维护的项目,正是用来将ES6代码转换成兼容ES6之前的环境
  • IIFE和try/catch都可以用来实现let块作用域,但try/catch性能的确很糟糕
  • var a=2;JavaScript实际上会将其看成两个声明。var a;a=2;第一个定义声明是在编译阶段进行的,第二个赋值会留在原地等待执行阶段

第四章 提升

4.1.先有鸡还是先有蛋

  • 函数作用域和块作用域的行为是一样的,可以总结为:任何声明在某个作用域内的变量,都将附属于这个作用域

4.2.编译器再度来袭

  • 引擎会在解释 JavaScript 代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来
  • 正确的思考思路是,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理
  • 变量和函数声明从它们在代码中出现的位置被“移动”到了最上面。这个过程就叫作提升
  • 只有声明本身会被提升,但函数会首先被提升,然后才是变量
  • 函数声明后面同名的var声明会被忽略掉

4.3.函数优先

  • 一个值得注意的细节(这个细节可以出现在有多个“重复”声明的代码中)是函数会首先被提升, 然后才是变量
  • 一个普通块内部的函数声明通常会被提升到所在作用域的顶部

第五章 作用域闭包

5.1.启示

  • 闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包

5.2.实质问题

  • 函数在定义时的词法作用域以外的地方被调用,闭包使得函数可以继续访问定义时的词法作用域
  • 如果将(访问它们各自词法作用域的)函数当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用,在定时器,事件监听器,Ajax请求,跨窗口通信,Web workers或者任何其他的异步(或者同步)任务中,只要有用了回调函数,实际上就是在使用闭包
  • 循环和闭包:延迟函数的回调会在循环结束时才执行
  • for/let行为指出变量在循环过程中不止被声明一次,每次迭代都会声明
  • 模块模式需具备的两个必要条件:1、必须有外部的封闭函数,该函数必须至少被调用一次;2、封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或修改私有的状态

5.3.现在我懂了

  • 在定时器、事件监听器、Ajax 请求、 跨窗口通信、 Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数, 实际上就是在使用闭包

5.4.循环和闭包

  • 我们使用 IIFE 在每次迭代时都创建一个新的作用域。换句话说,每次迭代我们都需要一个块作用域

5.5.模块

  • 模块模式需要具备两个必要条件:必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例);封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态;
  • 基于函数的模块并不是一个能被稳定识别的模式(编译器无法识别),它们的 API 语义只有在运行时才会被考虑进来。因此可以在运行时修改一个模块的API
  • ES6 模块 API 更加稳定(API 不会在运行时改变)

附录

  • 动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用
  • 换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套
  • 主要区别:词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的

第二部分 this和对象原型

第一章 关于this

1.1.为什么要用this

  • 箭头函数:1、容易让人混淆了this绑定规则和词法作用域规则,2、另外箭头函数是匿名而非具名的

1.2.误解

  • this误解:1、指向自身,误理解成指向函数自身;2、指向函数作用域,误理解成指向函数的作用域
  • 为什么需要从函数内部引用函数自身:常见的原因是递归(从函数内部调用这个函数)或者可以写一个在第一次被调用后自己解除绑定的事件处理器
  • 一种传统的但是现在已经被弃用和批判的用法,是使用 arguments.callee 来引用当前正在运行的函数对象。这是唯一一种可以从匿名函数对象内部引用自身的方法

1.3.this到底是什么

  • this是在运行时进行绑定的,并不是在编写时绑定。它的上下文取决于函数调用时的各种条件
  • this的绑定和函数声明的位置没有关系,只取决于函数的调用方式

第二章 this全面解析

2.1.调用位置

  • 调用位置:寻找函数在代码中被调用的地方(而不是声明的位置)
  • 调用栈:为了到达当前执行位置所调用的所有函数,在JavaScript调试器中可以很方便查看
  • 另一个查看调用栈的方法是使用浏览器的调试工具。 绝大多数现代桌面浏览器都内置了开发者工具,其中包含 JavaScript 调试器

2.2.绑定规则

  • 默认绑定、隐式绑定、显式绑定、new绑定
  • 默认绑定:常见的独立函数调用,无法应用其他规则时默认规则。但使用严格模式(strict mode)时,不能将全局对象用于默认绑定,因此this会绑定到undefined
  • 隐式绑定:需要考虑调用位置是否有上下文对象,或者说是否被某个对象拥有或包含。当函数引用有上下文对象时,该规则会把函数调用中的this绑定到这个上下文对象
  • 一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式
  • 隐式丢失:1、var bar = obj.foo; bar();//会应用默认绑定;2、doFoo(obj.foo);//函数作为参数传递,隐式赋值;3、setTimeout(obj.foo, 100);//函数传入语言内置函数,同样
  • 显式绑定:想在某个对象上强调调用函数,可以使用函数call和apply方法。foo.call(obj)调用foo时强制把this绑定到obj上,如果传入原始值,会转换成它的对象形式,如new String(…),new Boolean(…)通常称装箱
  • 也会出现绑定丢失问题,硬绑定:不可能再修改它的this。bind(…)会返回一个硬编码的新函数
  • 由于硬绑定是一种非常常用的模式 所以在 ES5 中提供了内置的方法Function.prototype.bind
  • new 绑定:JavaScript中new的机制实际上和面向类的语言完全不同,使用new来调用函数时,通常会自动执行下面的操作:1、创建(或者说构造)一个全新的对象;2、这个新对象会被执行[[prototype]]连接;3、新对象会绑定到函数调用的this;4、如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

2.3.优先级

  • 四条规则优先级:1、显式绑定比隐式绑定更高;2、new绑定比隐式绑定更高;3、new修改了硬绑定调用中的this
  • new 和 call/apply 无法一起使用, 因此无法通过 new foo.call(obj1) 来直接进行测试
  • MDN 提供的一种bind(…) 实现
  • 之所以要在 new 中使用硬绑定函数,主要目的是预先设置函数的一些参数,这样在使用new 进行初始化时就可以只传入其余的参数。 bind(..) 的功能之一就是可以把除了第一个参数(第一个参数用于绑定 this)之外的其他参数都传给下层的函数(这种技术称为“部分应用”, 是“柯里化” 的一种)
  • 可以按照下面的顺序来进行判断this:1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象;2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是指定的对象;3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象;4. 如果都不是的话,使用默认绑定。如果在严格模式下, 就绑定到 undefined,否则绑定到全局对象;

2.4.绑定例外

  • 如果把null或者undefined作为this的绑定对象传入call,apply或者bind会被忽略,实际应用的是默认绑定规则
  • Object.create(null)和{}很像,但不会创建Object.prototype,所以比{}更空
  • 注意:对于默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式

2.5.this词法

  • ES6 中介绍了一种无法使用这些规则的特殊函数类型:箭头函数
  • 箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this

第三章 对象

3.1.语法

  • 对象可以两种形式定义:构造形式和声明形式,即new Object()和{}
  • 唯一的区别是,在文字声明中你可以添加多个键/值对,但是在构造形式中你必须逐个添加属性

3.2.类型

  • JavaScript共六种主要类型(语言类型),string、number、boolean、null、undefined、object,前五种是基本类型
  • JavaScript中万物皆是对象,显然是错误的
  • 有些内置对象的名字看起来和简单基础类型一样,不过实际上它们的关系更复杂
  • 原始值 “I am a string” 并不是一个对象, 它只是一个字面量,并且是一个不可变的值。如果要在这个字面量上执行一些操作,比如获取长度、访问其中某个字符等,那需要将其转换为 String 对象
  • 在JavaScript中二进制前三位都为0的会被判断为object类型,null的二进制全部是0,故被判定为Object类型
  • null和undefined没有对应的构造形式,只有文字形式,而Date只有构造形式
  • 对于 Object、 Array、 Function 和 RegExp(正则表达式) 来说, 无论使用文字形式还是构造形式,它们都是对象,不是字面量
  • Error 对象很少在代码中显式创建,一般是在抛出异常时被自动创建。也可以使用 new Error(…) 这种构造形式来创建,不过一般来说用不着
  • 数组和普通的对象都根据其对应的行为和用途进行了优化,所以最好只用对象来存储键/值对,只用数组来存储数值下标/值对 14

3.3.内容

  • .a 语法通常被称为“属性访问”, ["a"] 语法通常被称为“键访问”
  • 这两种语法的主要区别在于 . 操作符要求属性名满足标识符的命名规范,而 [".."] 语法 可以接受任意 UTF-8/Unicode 字符串作为属性名
  • 在对象中,属性名永远都是字符串。如果你使用 string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串
  • 数组也是对象,所以虽然每个下标都是整数,你仍然可以给数组添加属性。虽然添加了命名属性(无论是通过 . 语法还是 [] 语法), 数组的 length 值并未发生变化
  • 我们还不确定“复制” 一个函数意味着什么。有些人会通过 toString() 来序列化一个函数的源代码(但是结果取决于 JavaScript 的具体实现, 而且不同的引擎对于不同类型的函数处理方式并不完全相同)
  • 对象属性描述符:value(值)、writable(是否可修改)、enumrable(是否可枚举)、configurable(是否可配置)、get、set
  • 不管是不是处于严格模式, 尝试修改一个不可配置的属性描述符都会出错。注意:如你所见,把 configurable 修改成false 是单向操作, 无法撤销!
  • 除了无法修改, configurable:false 还会禁止删除这个属性
  • 不变性:1、对象常量 writable: false和configurable: false;2、禁止扩展:Object.preventExtensions(obj);3、密封:Object.seal在现有对象上调用Object.preventExtensions,并把现有属性标记configuable: false;4、冻结Object.freeze在现有对象上调用Object.seal并把数据访问属性标记writable: false
  • 存在性:in操作符会检查属性是否在对象及其[[prototype]]原型链中,hasOwnProperty只会检查属性是否在对象中,不会检查[[prototype]]链,object.keys和object.getOwnPropertyNames都只会查找对象直接包含的属性

3.4.遍历

  • 遍历:forEach遍历并忽略回调函数返回值,无法break跳出;every一直运行到回调函数返回false,some一直运行到回调函数返回true;for…of遍历值而不是属性
  • every(…) 和 some(…) 中特殊的返回值和普通 for 循环中的 break 语句类似,它们会提前终止遍历

第四章 混合对象“类”

4.1.类理论

  • 面向对象编程强调的是数据和操作数据的行为本质上是互相关联的(当然,不同的数据有不同的行为),因此好的设计就是把数据以及和它相关的行为打包(或者说封装)起来
  • 类的另一个核心概念是多态,这个概念是说父类的通用行为可以被子类用更特殊的行为重写
  • 面向类的设计模式:实例化(instantiation) 继承(inheritance)(相对)多态(polymophism)

4.2.类的机制

  • 类的机制:许多面向类的语言中,“标准库”会提供stack类,是一种"栈"数据结构
  • Stack类内部会有一些变量来存储数据,同时提供一些公有的可访问行为(方法),从而让你的代码可以和(隐藏的)数据进行交互(比如添加、删除数据)
  • 蓝图—建筑,类比于 类—实例

4.3.类的继承

  • 对于真正的类来说,构造函数是属于类的,而JavaScript是相反的,实际上类是属于构造函数的,类的继承其实就是复制
  • 多态是一个非常广泛的话题,我们现在所说的“相对” 只是多态的一个方面:任何方法都可以引用继承层次中高层的方法(无论高层的方法名和当前方法名是否相同)
  • 多态的另一个方面是,在继承链的不同层次中一个方法名可以被多次定义,当调用方法时会自动选择合适的定义
  • 需要注意,子类得到的仅仅是继承自父类行为的一份副本。子类对继承到的一个方法进行“重写”,不会影响父类中的方法,这两个方法互不影响,因此才能使用相对多态引用访问父类中的方法
  • 多重继承意味着所有父类的定义都会被复制到子类中

4.4.混入

  • 在继承或者实例化时, JavaScript 的对象机制并不会自动执行复制行为。简单来说, JavaScript 中只有对象,并不存在可以被实例化的“类”
  • 显式混入mixin,无法(用标准、可靠的方法)真正的复制,只能复制对共享函数对象的引用
  • 隐式混入call,通过this绑定实现
  • 混入模式(无论显式还是隐式) 可以用来模拟类的复制行为,但是通常会产生丑陋并且脆弱的语法,比如显式伪多态(OtherObj.methodName.call(this, ...)), 这会让代码更加难懂并且难以维护

第五章 原型

5.1.[[Prototype]]

  • Object的原理:所有普通的[prototype]链最终都会指向内置的Object.prototype
  • constructor是一个非常不可靠并且不安全的引用,尽量避免使用这些引用
  • 调用Object.create会凭空创建一个“新”对象并把新对象内部的[prototype]关联到你指定的对象

5.2.“类”

  • a instanceof Foo,在a的整条[prototype]链中是否有Foo.prototype指向的对象
  • JavaScript和面向类的语言不同,它并没有类来作为对象的抽象模式或者说蓝图,JavaScript中只有对象
  • 继承意味着复制操作,JavaScript(默认)并不会复制对象属性,相反,JavaScript会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数

5.3.技术

  • 在JavaScript中,对于“构造函数”最准确的解释是所有带new的函数调用
  • 实际上,构造函数和你程序中其他函数没有任何区别,当在普通函数调用前加上new关键词之后,就会把这个函数调用变成一个“构造函数调用”
  • new会劫持所有普通函数并用构造函数的形式来调用它
  • 奇怪的__prototype__(在ES6之前并不是标准),属性引用了内部(prototype)对象

5.4.对象关联

  • 原型链:如果在对象上没有找到需要的属性或者方法引用,引擎就会继续在[prototype]关联的对象上进行查找,同理,如果在后者中也没找到,就到需要的引用就会继续查找它的prototype,以此类推
  • Object.create会创建一个新对象(bar)并把它关联到我们指定的对象(foo)
  • 可以充分发挥[prototype]机制的威力(委托)并且避免不必要的麻烦(比如使用new的构造函数调用会生成.prototype和.constructor引用)
  • Object.create(null)会创建一个拥有空[prototype]链接的对象,这个对象无法委托

第六章 行为委托

6.1.面向委托的设计

  • 回顾:[Prototype]机制就是指对象中的一个内部链接引用另一个对象
  • JavaScript中这个机制的本质就是对象之间的关联关系
  • 把思路从类和继承的设计模式转换到委托行为的设计模式

6.2.类与对象

  • 委托行为意味着某些对象(XYZ)在找不到属性或者方法引用时会把这个请求委托给另一个对象(Task),这是一种极其强大的设计模式和父类、子类、继承、多态完全不同
  • 对象关联风格的代码相较于对象与类风格更加简洁,因为只关注一件事,对象之间的关系

6.4.更好的语法

  • ES6中的class仍然是通过[prototype]机制实现的
  • 匿名函数没有name标识符,会导致:1、自我引用(递归,事件绑定等)更难;2、调用栈更难追踪;3、代码(稍微)更难理解
  • 鸭子类型:辨别特性,很脆弱的设计

6.5.内省

  • 内省:检查实例的类型,主要目的是通过创建方式来判断对象的结构和功能
  • instanceof:因为Foo.prototype在a1的[prototype]链上,所以instanceof操作告诉我们a1是Foo类的一个实例。从语法角度上说:instanceof似乎是检查a1和Foo的关系,但实际上它想说的是a1和Foo.prototype(引用的对象)是互相关联的
  • 行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。我们可以选择在JavaScript中努力实现类机制,也可以拥抱更自然的[prototype]委托机制

写在后面