跳到主要内容

对象的秘密一生 — 原型、类与多态

这一章讲三件事: JS 的对象如何靠原型链互相借属性; class 语法在这套机制上加了什么糖、thisnew 到底是什么; 以及面向对象三件套(封装、多态、继承)里,作者为什么唯独对继承有保留。 读完你能解释「这个对象明明没这个属性,为什么能取到」。

1. 先看现象:两个兔子,同一段说话代码

function speak(line) {
console.log(`The ${this.type} rabbit says '${line}'`);
}
let whiteRabbit = {type: "white", speak};
let hungryRabbit = {type: "hungry", speak};

whiteRabbit.speak("Oh my fur and whiskers");
// → The white rabbit says 'Oh my fur and whiskers'
hungryRabbit.speak("Got any carrots?");
// → The hungry rabbit says 'Got any carrots?'

同一个函数,报出的 type 却不同。玄机在 this:函数被「当方法调用」——先从对象身上取出来、紧接着调用(形如 对象.方法())——时,它体内的 this 自动指向被调用时的那个对象。书里给的心智图像很准:this 是一个额外参数,只是用一种特殊方式递进来;想显式递,用 speak.call(whiteRabbit, "Hurry"),第一个参数就是 this1

两条补充规则:

  • 箭头函数不绑自己的 this,看到的是外围作用域的 this。把回调换成箭头,this.value 就还指着对象;换成 function,回调自己那份 this 是空的,立刻出错2;
  • find(array) {…} 这种写在对象字面量里的,是方法的简写3

2. 主走查(上):原型 — 自己没有,就问「爹」

十只兔子共享同一个 speak,把方法存在每个对象身上太浪费。原型(prototype) 就是 JS 的答案:对象可以链到另一个对象,自己没有的属性,就沿着链向上找4

你写的 blackRabbit.speak("I am fear and darkness")
第 1 步 blackRabbit 自己有 speak 吗?没有
第 2 步 去原型 protoRabbit 找 → 有!调用,
this = blackRabbit(是「点号前那个」决定 this)
输出 The black rabbit says 'I am fear and darkness'
第 3 步 blackRabbit.type = "black" —— 自己身上就有,不用问爹

blackRabbit ──原型──> protoRabbit ──原型──> Object.prototype ──> null

图说:链的尽头是 Object.prototype(它自己的原型是 null)。

用代码复现这条链:Object.create(protoRabbit) 造出链上去;Object.getPrototypeOf({}) 返回 Object.prototype;连你从没定义过的 empty.toString 都能用,因为它存在 Object.prototype 上5

不同家族有不同祖先:函数派生自 Function.prototype,数组派生自 Array.prototype;而这些原型自己又链回 Object.prototype,所以数组照样有 toString6

3. 主走查(下):class — 把「造有原型的对象」打包

原型只是「共享属性」;一个类型还需要「每个实例不同的部分 + 一套创建流程」。书里先手写一个构造函数造兔,再给出等价的 class 写法7:

class Rabbit {
constructor(type) { this.type = type; }
speak(line) { console.log(`The ${this.type} rabbit says '${line}'`); }
}
let killerRabbit = new Rabbit("killer");

new Rabbit("killer") 干了三件事,合在主走查里:造一个空对象,它的原型是 Rabbit.prototype(所以能拿到 speak);以这个新对象为 this 跑一遍 constructor(this.type = "killer" 落在自己身上);把这个对象交出来8

三个容易缠住人的事实:

  • class 是 2015 年才有的记法。之前,大家写普通函数再手工往 prototype 属性上挂方法——所以所有非箭头函数天生带一个空的原型属性,这纯属历史包袱9;
  • 构造函数名大写是约定(区别于普通函数);属性也能直接写在 class 体里(speed = 0),它落到每个实例身上,不进原型10;
  • 最容易混的一对:构造函数的 prototype 属性(它造的实例的原型)≠ 构造函数自己的原型(那是 Function.prototype)。Object.getPrototypeOf(killerRabbit) == Rabbit.prototype 为 true,而 Object.getPrototypeOf(Rabbit) == Function.prototype 也为 true——两个问句,两个答案11

覆盖(override):往对象身上写一个与原型同名的属性,它就「挡住」了原型那份。书里的牙口演示:Rabbit.prototype.teeth = "small" 时,killerRabbit 读 teeth 得 "small";一旦 killerRabbit.teeth = "long, sharp, and bloody",读自己的,别的新兔子仍是 "small"——原型是背景板,例外写在个体身上12

4. 别拿普通对象当映射:toString 会在夜里潜入

想存「名字 → 年龄」,直接用对象?试看:

let ages = { Boris: 39, Liang: 22, "Júlia": 62 };
"toString" in ages; // → true !!

没人存过 toString,它顺着原型链漏了进来。三条出路13:

  1. Object.create(null) 造无原型对象,干净但失去一切内建方法;
  2. 用标准的 Map:任何类型的键都能放,set/get/has 三个动词搞定——快速查找的实现难点(哈希(按钥匙直接算出存放位置)表那一套)有人替你做完了;
  3. 判断时用 Object.hasOwn(obj, name)Object.keys(都只看自己身上的,不问原型)。

5. 多态:面向接口写的代码,谁来都接

给 Rabbit 挂一个自己的 toString 后,String(killerRabbit) 输出 "a killer rabbit"——String 并不知道 Rabbit 存在,它只管「你有 toString 我就调」14

这就是多态(polymorphism):代码按接口写,任何碰巧实现该接口的对象都能插进来用。一个应用极广的接口是「array-like」:有 length、有数字下标——数组满足,字符串也满足,所以你可以把 forEach 借给一个裸对象用:Array.prototype.forEach.call({length: 2, 0: "A", 1: "B"}, …) 照样打出 A、B15

配套的糖:getter/setter 让「读属性」暗中变成一次方法调用(Temperature 类只存摄氏,temp.fahrenheit 读时换算、写时反算);static 把方法挂在构造函数本体上,比如 Temperature.fromFahrenheit(212) 直接造出 100°C 的实例16

6. Symbol 与迭代器:for-of 的接口长这样

先解释标题里的那个词:迭代,就是挨个过一遍、逐个取出。属性名也可以不是字符串而是 Symbol——2015 年加入,每次 Symbol("name") 造出的值独一无二,适合做「绝不会和别的属性撞名」的接口槽位(预留的固定名字)17

for-of 背后正是这样一个槽位——这个机制叫迭代(挨个过一遍取值):对象若有 Symbol.iterator 方法,调用它应返回一个带 next() 的迭代器,每次 next 交出 {value, done}18。书里手动玩了一次:

let okIterator = "OK"[Symbol.iterator] ();
okIterator.next(); // → {value: "O", done: false}
okIterator.next(); // → {value: "K", done: false}
okIterator.next(); // → {value: undefined, done: true}

然后给一个链式 List 类装上迭代器(独立 ListIterator 类记进度),从此 for (let el of list) 和展开 [..."PCI"] 全都通吃19

7. 继承:三件套里最有争议的那件

继承(inheritance) = 新类的原型链在旧类的原型上,再补几个自己的定义:class LengthList extends List,constructor 里用 super(...) 先完成父类那份初始化。书里的例子把 length 预先存进私有属性 #length(省去每次爬链),细节里有个精彩的坑:初始化时要用 super.length 而不是 this.length,否则会调到自己的 getter、读到还没填好的 #length20

私有属性 # 值得单记一句:只能在 class 体内部碰;而且私有属性必须先声明,普通属性却可以随手赋出来21

然后是作者的态度,原话锋利:封装与多态被普遍认为是好主意;继承更有争议——它从根本上把类与类捆在一起,制造新的纠缠。继承别的类,你往往得知道它内部怎么运转;它是让某些程序变简短的工具,但不应是你第一个伸手去拿的工具,更不该主动找机会搭建类的族谱22

8. 作者的判断与证据

  • 「抽象数据类型=家电」:搅拌机把材料学与电学封进塑料壳,用户只需要几个旋钮——接口之外的细节全部封装。这是本章的理论底座23;
  • 「把单个对象当组织单位有点不幸」:作者明确表达对经典 OOP 的保留——有用的功能常常是一组类协作完成的。立场,非定理24;
  • 「数字码不像 JavaScript」:DOM(第 12 章)用数字常量表示节点类型,作者归因于它是语言中立接口、还要伺候 XML;他的判句值得背:「与所用语言深度整合的接口,省下的时间比跨语言一致的接口多」25;
  • instanceof 能穿透继承层级认亲(LengthList 的实例也 instanceof List)26

判断(我们的,不是书里的): 本章标题里的「秘密」一词是自嘲——多数人天天用 class, 却不知道底下是原型链。这个分层(JS 机制层 / class 语法层)解释了大量怪现象: 比如 class 之间可以事后往 prototype 上补方法、比如没有类的对象字面量照样能享用原型。 先懂原型再学 class,顺序反过来会很痛苦。 如果错,会错在: 如果读者长期只写 class、从不用 Object.create,「先懂原型」的收益确实有限——这条判断的强度取决于你往多深处挖。

9. 边界与局限

  • 书没有讲 class fields 的完整提案细节与私有方法的兼容性年代问题;
  • Mixin、组合优于继承的现代替代表达,书只在立场句里暗示,没有展开;
  • 生成器能大幅简化迭代器(第 11 章),本章的 ListIterator 写法到时会被一行 function* 取代;
  • WeakMap、Proxy 等元编程能力完全不在本书范围。

10. 可带走的

  1. 方法=属性里的函数;this 由「怎么调用」决定;fn.call(obj, …) 显式给;
  2. 箭头函数不绑 this——回调里要用外面的 this,就选箭头;
  3. 取属性走原型链,链底是 Object.prototype,再往上是 null;
  4. new 三步:造空对象挂原型 → 跑 constructor → 交出对象;
  5. class 是语法糖,2015 年前的写法是函数+手挂 prototype;
  6. 实例的 prototype 属性 ≠ 构造函数自己的原型(Function.prototype);
  7. 自己的属性挡住原型——例外写在个体,共性放原型;
  8. 映射用 Map,不用普通对象(toString 会漏进来);判断归属用 Object.hasOwn;
  9. 多态=面向接口;getter/setter 是「读 写时 secretly 调方法」;
  10. for-of 的本体是 Symbol.iterator{value, done};
  11. 继承制造纠缠:封装、多态先想,继承最后想

11. 原文地图

主题原书章原文位置
抽象数据类型与家电The Secret Life of Objectstext/09-fm-the-secret-life-of-objects.txt:15(搜「electric mixer」) · :21(搜「interface」)
「单个对象不幸」同上text/09-fm-the-secret-life-of-objects.txt:23(搜「somewhat unfortunate」)
方法与 this、call同上text/09-fm-the-secret-life-of-objects.txt:40(搜「this」) · :42(搜「call」)
箭头函数与 this同上text/09-fm-the-secret-life-of-objects.txt:49(搜「Arrow functions」)
原型链与 Object.prototype同上text/09-fm-the-secret-life-of-objects.txt:70(搜「prototypes」) · :80(搜「prototype」)
Function/Array.prototype同上text/09-fm-the-secret-life-of-objects.txt:89(搜「derive from」)
Object.create 的 proto 兔同上text/09-fm-the-secret-life-of-objects.txt:99(搜「Object.create」) · :108(搜「fear and darkness」)
class 与 new 的机制同上text/09-fm-the-secret-life-of-objects.txt:129(搜「class Rabbit」) · :140(搜「new」)
2015 之前的手工原型同上text/09-fm-the-secret-life-of-objects.txt:146(搜「ArchaicRabbit」) · :154(搜「prototype property」)
两种 prototype 的区分同上text/09-fm-the-secret-life-of-objects.txt:158(搜「Function.prototype」)
teeth 覆盖同上text/09-fm-the-secret-life-of-objects.txt:220(搜「teeth」) · :233(搜「backdrop」)
数组的 toString 之争同上text/09-fm-the-secret-life-of-objects.txt:240(搜「1,2」)
普通对象当映射的危险同上text/09-fm-the-secret-life-of-objects.txt:264(搜「toString」) · :276(搜「Map」)
Object.hasOwn同上text/09-fm-the-secret-life-of-objects.txt:294(搜「hasOwn」)
多态与 array-like同上text/09-fm-the-secret-life-of-objects.txt:299(搜「polymorphism」) · :316(搜「forEach.call」)
getter/setter/static同上text/09-fm-the-secret-life-of-objects.txt:328(搜「getters」) · :343(搜「Temperature」)
Symbol同上text/09-fm-the-secret-life-of-objects.txt:382(搜「symbols」) · :386(搜「Symbol(」)
迭代器协议与 OK 演示同上text/09-fm-the-secret-life-of-objects.txt:418(搜「iterable」) · :426(搜「okIterator」)
List 迭代器实现同上text/09-fm-the-secret-life-of-objects.txt:436(搜「class List」) · :459(搜「ListIterator」)
继承与 super 的坑同上text/09-fm-the-secret-life-of-objects.txt:505(搜「extends List」) · :525(搜「super」)
继承的争议立场同上text/09-fm-the-secret-life-of-objects.txt:527(搜「controversial」) · :529(搜「family trees」)
instanceof 穿透同上text/09-fm-the-secret-life-of-objects.txt:545(搜「see through」)
私有属性 #同上text/09-fm-the-secret-life-of-objects.txt:186(搜「private」) · :206(搜「RandomSource」)
DOM 是语言中立接口同上text/17-fm-the-document-object-model.txt:56(搜「language-neutral」)

Footnotes

  1. 出处:「The Secret Life of Objects」第 40 段(text/09-fm-the-secret-life-of-objects.txt:40,搜「this」)与第 42 段(text/09-fm-the-secret-life-of-objects.txt:42,搜「call」)。

  2. 出处:「The Secret Life of Objects」第 49 段(text/09-fm-the-secret-life-of-objects.txt:49,搜「Arrow functions」)。

  3. 出处:「The Secret Life of Objects」第 60 段(text/09-fm-the-secret-life-of-objects.txt:60,搜「shorthand」)。

  4. 出处:「The Secret Life of Objects」第 70 段(text/09-fm-the-secret-life-of-objects.txt:70,搜「prototypes」)。

  5. 出处:「The Secret Life of Objects」第 78 段(text/09-fm-the-secret-life-of-objects.txt:78,搜「Object.prototype」)与第 82 段(text/09-fm-the-secret-life-of-objects.txt:82,搜「getPrototypeOf」)。

  6. 出处:「The Secret Life of Objects」第 89 段(text/09-fm-the-secret-life-of-objects.txt:89,搜「derive from」)。

  7. 出处:「The Secret Life of Objects」第 119 段(text/09-fm-the-secret-life-of-objects.txt:119,搜「constructor」)与第 129 段(text/09-fm-the-secret-life-of-objects.txt:129,搜「class Rabbit」)。

  8. 出处:「The Secret Life of Objects」第 140 段(text/09-fm-the-secret-life-of-objects.txt:140,搜「fresh instance」)。

  9. 出处:「The Secret Life of Objects」第 144 段(text/09-fm-the-secret-life-of-objects.txt:144,搜「2015 edition」)与第 154 段(text/09-fm-the-secret-life-of-objects.txt:154,搜「prototype property」)。

  10. 出处:「The Secret Life of Objects」第 156 段(text/09-fm-the-secret-life-of-objects.txt:156,搜「capitalized」)与第 167 段(text/09-fm-the-secret-life-of-objects.txt:167,搜「instance objects」)。

  11. 出处:「The Secret Life of Objects」第 158 段(text/09-fm-the-secret-life-of-objects.txt:158,搜「Function.prototype」)与第 142 段(text/09-fm-the-secret-life-of-objects.txt:142,搜「killerRabbit」)。

  12. 出处:「The Secret Life of Objects」第 218 段(text/09-fm-the-secret-life-of-objects.txt:218,搜「hidden」)与第 220 段(text/09-fm-the-secret-life-of-objects.txt:220,搜「teeth」)。

  13. 出处:「The Secret Life of Objects」第 269 段(text/09-fm-the-secret-life-of-objects.txt:269,搜「dangerous」)与第 292 段(text/09-fm-the-secret-life-of-objects.txt:292,搜「hasOwn」)。

  14. 出处:「The Secret Life of Objects」第 301 段(text/09-fm-the-secret-life-of-objects.txt:301,搜「toString」)与第 307 段(text/09-fm-the-secret-life-of-objects.txt:307,搜「killer rabbit」)。

  15. 出处:「The Secret Life of Objects」第 299 段(text/09-fm-the-secret-life-of-objects.txt:299,搜「polymorphism」)与第 314 段(text/09-fm-the-secret-life-of-objects.txt:314,搜「array-like」)。

  16. 出处:「The Secret Life of Objects」第 328 段(text/09-fm-the-secret-life-of-objects.txt:328,搜「getters」)与第 343 段(text/09-fm-the-secret-life-of-objects.txt:343,搜「Temperature」)、第 354 段(text/09-fm-the-secret-life-of-objects.txt:354,搜「fromFahrenheit」)。

  17. 出处:「The Secret Life of Objects」第 382 段(text/09-fm-the-secret-life-of-objects.txt:382,搜「2015」)与第 384 段(text/09-fm-the-secret-life-of-objects.txt:384,搜「unique」)。

  18. 出处:「The Secret Life of Objects」第 418 段(text/09-fm-the-secret-life-of-objects.txt:418,搜「iterable」)与第 420 段(text/09-fm-the-secret-life-of-objects.txt:420,搜「next」)。

  19. 出处:「The Secret Life of Objects」第 426 段(text/09-fm-the-secret-life-of-objects.txt:426,搜「okIterator」)与第 484 段(text/09-fm-the-secret-life-of-objects.txt:484,搜「List.fromArray」)。

  20. 出处:「The Secret Life of Objects」第 505 段(text/09-fm-the-secret-life-of-objects.txt:505,搜「extends List」)与第 525 段(text/09-fm-the-secret-life-of-objects.txt:525,搜「super」)。

  21. 出处:「The Secret Life of Objects」第 186 段(text/09-fm-the-secret-life-of-objects.txt:186,搜「private」)与第 202 段(text/09-fm-the-secret-life-of-objects.txt:202,搜「declare」)。

  22. 出处:「The Secret Life of Objects」第 527 段(text/09-fm-the-secret-life-of-objects.txt:527,搜「controversial」)与第 529 段(text/09-fm-the-secret-life-of-objects.txt:529,搜「first tool」)。

  23. 出处:「The Secret Life of Objects」第 15 段(text/09-fm-the-secret-life-of-objects.txt:15,搜「electric mixer」)。

  24. 出处:「The Secret Life of Objects」第 23 段(text/09-fm-the-secret-life-of-objects.txt:23,搜「somewhat unfortunate」)。

  25. 出处:「The Secret Life of Objects」第 56 段(text/17-fm-the-document-object-model.txt:56,搜「language-neutral」)与第 58 段(text/17-fm-the-document-object-model.txt:58,搜「properly integrated」)。

  26. 出处:「The Secret Life of Objects」第 545 段(text/09-fm-the-secret-life-of-objects.txt:545,搜「see through」)。