第一周02天 原型与原型链

两小时学习讲义:原型与原型链

配套《学习计划.md》使用,本讲义把每个知识点打碎到颗粒级 ,并逐点给出类比 + 例子 + 真实开发场景 。 学习方法建议:先猜后跑。看到任何一段代码,先写下输出或结论,再运行验证。 全程约 120 分钟,与学习计划的时间轴一一对应。


⏱️ 时间轴总览

时间段 环节 本讲义位置
20:00--20:10 🔁 回顾与预习 第 1 段
20:10--20:25 📚 知识块①:prototype 与原型链 第 2 段
20:25--20:40 📚 知识块②:__proto__/prototype/constructor 三兄弟 第 3 段
20:40--20:50 📚 知识块③:ES6 class 本质 + instanceof 第 4 段
20:50--21:00 📝 费曼复述 第 5 段
21:00--21:20 🛠️ 练习①:ES5 继承 + class 对照 第 6 段
21:20--21:40 🛠️ 练习②③:画关系图 + myInstanceof 第 7 段
21:40--21:50 💭 思考提高题 第 8 段
21:50--22:00 ✅ 测验 第 9 段

第 1 段(10min)回顾与预习:接住昨天的线

1.1 先接昨日(this 与 new)的线

昨天你学了四种 this 绑定,其中 new 绑定 讲了 new 的四步:

javascript 复制代码
new Foo() 四步:
① 创建空对象
② 把空对象的原型指向 Foo.prototype   ← 就是今天的主角!
③ this 指向空对象,执行函数体(this.name = name 写入自有属性)
④ 返回这个对象

今天的一切都从第 ② 步长出来foo.__proto__ === Foo.prototype 这个式子,昨天是"new 四步的副产品",今天要把它彻底讲透。

再回忆昨天的一句话:变量看定义处,this 看调用处 。今天补上第三句:属性看原型链。三句连起来:

变量 → 看定义处(闭包);this → 看调用处(绑定);属性 → 看原型链(上溯查找)。

1.2 先凭直觉回答 4 个小问题(不用对,先写下答案)

js 复制代码
// 问题 1:方法存在哪?每个实例一份还是共享一份?
function Dog(name) { this.name = name; }
Dog.prototype.speak = function () { return '汪汪'; };
const d1 = new Dog('旺财');
const d2 = new Dog('来福');
d1.speak === d2.speak;  // true 还是 false?为什么?

// 问题 2:自己身上没有的属性,从哪冒出来的?
const o = {};
o.toString;  // o 里没写 toString,为什么能访问到?

// 问题 3
function Foo() {}
const foo = new Foo();
foo.constructor === Foo;  // true 还是 false?

// 问题 4
const d = {};
d instanceof Object;      // true 还是 false?instanceof 到底在查什么?

学完第 2、3 段再回来对照。第 1 段只做一件事:把"对象会沿链向上问"这个画面装进脑子。


第 2 段(15min)知识块①:prototype 与原型链

2.1 为什么需要 prototype:为"共享"而生

知识点(细化)

  • 如果每个实例都自己带一份方法,1000 个实例就有 1000 份相同的函数------浪费内存。
  • 解法:把方法放进构造函数的 prototype(一个公共对象),所有实例共享同一份
  • 实例自己身上只放"独有"的数据(自有属性),方法是"借用"的。

例子:先猜后跑

js 复制代码
function Dog(name) {
  this.name = name;              // 自有属性:每个实例各有一份
}
Dog.prototype.speak = function () {   // 共享方法:放仓库里
  return `${this.name} 汪汪`;
};

const d1 = new Dog('旺财');
const d2 = new Dog('来福');
console.log(d1.speak());          // 旺财 汪汪
console.log(d1.speak === d2.speak); // true ------ 同一份函数!
console.log(d1.name === d2.name);   // false ------ 自有属性各一份

类比(共享仓库 / 祖传工具箱) :构造函数是"工厂",prototype 是工厂的公共仓库 。每个出厂的产品(实例)不各自买一套工具,而是领一张"仓库钥匙"(__proto__)。要用工具(方法)时,先翻自己口袋(自有属性),没有就去仓库借。仓库里永远只放一套工具。

真实场景 :电商商品列表 1 万件商品,每件商品实例都共享 getPrice/format 这些方法------放 prototype 上只存一份;若放进构造函数里 this.getPrice = ...,就是 1 万份。这就是 prototype 存在的最大意义。

2.2 属性查找规则:先自己,再沿链上溯

知识点(细化)

  • 访问 obj.xxx 时引擎做的事:
    1. 先查 obj自有属性Object.hasOwn(obj, 'xxx'));
    2. 没有 → 沿 obj.__proto__(即 [[Prototype]])找;
    3. 还没有 → 再沿那层的 __proto__ 继续上溯;
    4. 直到 Object.prototype 也没有 → 返回 undefined
  • 这条逐级上溯的路径就是原型链(prototype chain)
  • 链的终点Object.prototype.__proto__ === null,走到 null 就停下。

例子:完整走一遍链

js 复制代码
function Dog(name) { this.name = name; }
Dog.prototype.speak = function () { return '汪汪'; };

const d = new Dog('旺财');
console.log(d.name);    // 旺财      ← 第 1 步:自有属性,命中
console.log(d.speak()); // 汪汪      ← 第 2 步:自己/狗名上没有 → 沿链到 Dog.prototype 命中
console.log(d.toString); // [Function] ← 第 3 步:Dog.prototype 没有 → 沿链到 Object.prototype 命中
console.log(d.nothing);  // undefined ← 第 4 步:Object.prototype 也没有 → undefined

// 验证链条
console.log(d.__proto__ === Dog.prototype);           // true
console.log(Dog.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__);              // null ------ 终点

类比(家族问话 / 祖传手艺) :孙子不会某门手艺,先问爸爸;爸爸不会,去问爷爷;爷爷再不会,到祖宗那里还是没有,那就"没有"。对象查属性同理------一层一层往上问,问到头就说没有(undefined)

真实场景 :你每天写的 [1,2].map(...)'abc'.toUpperCase(){}.hasOwnProperty(),全都是沿原型链借来的 ------数组实例自己没有 map,是 Array.prototype.map;字符串对象自己没有 toUpperCase,是 String.prototype 上的。你能直接调用它们,靠的就是这条链。

2.3 关键分界:[[Prototype]] / __proto__ / getPrototypeOf

知识点(细化)

  • [[Prototype]]:规范内部属性名,真正存"原型是谁"的地方(不在代码里直接写)。
  • __proto__ :浏览器提供的便捷读写器(本质是 Object.prototype 上的 getter/setter),不是规范推荐的正规访问方式
  • 正规方式 :读用 Object.getPrototypeOf(obj),写用 Object.setPrototypeOf(obj, proto)
  • 三者的值指向同一个东西:d.__proto__ === Object.getPrototypeOf(d) === Dog.prototype

例子

js 复制代码
console.log(Object.getPrototypeOf(d) === d.__proto__);       // true
console.log(Object.getPrototypeOf(d) === Dog.prototype);     // true

为什么规范不推荐 __proto__?因为它只是历史遗留的便捷写法,且直接改 __proto__ 可能引发原型污染(第 3 段讲)。面试说"我用 getPrototypeOf/setPrototypeOf"是加分项。

2.4 原型链上的"隐藏机制":new 第②步在做什么

知识点(细化) :昨天 new 四步里的第②步"把空对象原型指向 Foo.prototype",代码上就等于:

js 复制代码
function Foo() {}
const foo = new Foo();
// new 的内部等价于:创建一个对象,并把它的 [[Prototype]] 指向 Foo.prototype
const manual = {};
Object.setPrototypeOf(manual, Foo.prototype);  // 手动复刻第②步
console.log(manual instanceof Foo);            // true ------ 效果一样

意义instanceof 也好、方法共享也好,全都建立在"new 会把实例和构造函数的 prototype 拴在一起"这件事上。


第 3 段(15min)知识块②:__proto__ / prototype / constructor 三兄弟

3.1 三个词,三个角色(先建立"三个角色"画面)

知识点(细化)

名称 挂在谁身上 是什么 一句话
prototype 构造函数身上 一个普通对象,放共享方法 工厂的"公共仓库"
__proto__ 实例对象身上 指向其构造函数 prototype 的绳子([[Prototype]] 领到的"仓库钥匙"
constructor prototype 对象身上 默认指回构造函数 仓库门上的"铭牌"

核心等式(背下来,三兄弟全靠它)

js 复制代码
function Foo() {}
const foo = new Foo();

foo.__proto__ === Foo.prototype;          // true  实例 → 仓库
Foo.prototype.constructor === Foo;        // true  铭牌 → 工厂
foo.constructor === Foo;                  // true  实例沿链找到铭牌

foo.constructor 为什么成立?foo 自己没有 constructor → 沿链找到 Foo.prototype.constructor → 是 Foo这是"链式查找"的又一次实践。

类比(仓库三件套)prototype 是公共仓库(放工具/方法);__proto__ 是发给每个工人的仓库钥匙(指向仓库);constructor 是仓库门上的铭牌("本仓库归 Foo 工厂所有")。工人要找人算账,看一眼铭牌就知道自己是哪个工厂的人。

3.2 易错点①:重写 prototype 会"断链"

知识点(细化)

  • Foo.prototype.speak = ... 追加属性:不断链,老实例立即可见。
  • Foo.prototype = { ... } 整体替换 :已创建的实例的 __proto__ 仍指向旧仓库 ,新方法在新仓库里 → 老实例看不见新方法。
  • 整体替换后,Foo.prototype.constructor 也丢了(新对象没有铭牌),要手动补回。

例子:先猜后跑

js 复制代码
function Foo() {}
const foo = new Foo();           // 先把实例创建出来
Foo.prototype.say = function () { return 'hi'; };   // 追加 → 老实例能看到
console.log(foo.say());          // 'hi' ✅

Foo.prototype = { other() { return 'new' }; };      // 整体替换 → 断链!
console.log(foo.other);          // undefined ------ foo 的钥匙还指着旧仓库
const foo2 = new Foo();          // 新实例才拿新钥匙
console.log(foo2.other());       // 'new' ✅
console.log(foo2.constructor === Foo);  // false ------ 新仓库没铭牌,需要手动补

类比:工厂搬家换了新仓库,但老工人的钥匙还是旧的------他进不了新仓库。而且新仓库门口没挂铭牌,得重新挂。

真实场景 :这是"猴子补丁 / 重写原型"类库踩坑点;而 class 语法天然规避了这个问题(方法就是方法,不会出现"整体替换"写法)------这也是为什么工程上推荐 class。

3.3 Object.create:自己指定原型造对象

知识点(细化)

  • Object.create(proto)proto 作为原型,创建一个新对象。
  • 等价于手动:const o = {}; Object.setPrototypeOf(o, proto),但 Object.create 更干净。
  • Object.create(null):创建完全没有原型 的对象------不是 Object 的实例,没有 toString/hasOwnPropertyObject.getPrototypeOf 返回 null

例子

js 复制代码
const base = { greet() { return 'hi'; } };
const child = Object.create(base);
console.log(child.greet());      // 'hi' ------ 沿链借到 base 的方法
console.log(child.__proto__ === base); // true

// 纯字典:没有任何继承来的东西
const dict = Object.create(null);
console.log(Object.getPrototypeOf(dict));   // null
console.log(dict.toString);                 // undefined ------ 连 toString 都没有
console.log(dict instanceof Object);        // false ------ 不是 Object 的实例

类比Object.create(base) = "照着 base 的模板生一个孩子,孩子不会的问他爹 base";Object.create(null) = "孤儿,没有爹,什么都问不到"。

真实场景(安全) :存用户可控的 key (表单字段、URL 参数)时,若用普通 {},恶意 key 可能是 __proto__constructorprototype,赋值会污染原型链(原型污染攻击)。用 Object.create(null) 造"无原型纯字典",这些 key 就只是普通键。事件总线、白名单配置、key-value 映射表 常用这个招。更现代的做法是直接 Map(键完全独立、不受原型链影响)。

3.4 易错点②:原型污染(安全)

知识点(细化)

  • 原理:所有对象都沿链访问 Object.prototype。如果攻击者往 Object.prototype 塞了一个属性,所有对象都能看到它
  • 攻击路径:obj.__proto__.isAdmin = true(直接改原型);Object.assign({}, JSON.parse('{"__proto__": {...}}'))(合并不可信数据时触发 __proto__ setter)。
  • 防御:Object.create(null) 纯字典;用 Map;对 key 做白名单校验(['__proto__','prototype','constructor'].includes(key) 就拒绝)。

例子

js 复制代码
const polluted = {};
polluted.__proto__.isAdmin = true;   // 攻击:改 Object.prototype
console.log({}.isAdmin);             // true ------ 所有对象都"中招"了!

类比Object.prototype 是所有对象的"公共水源"。有人往水源里下毒,所有人喝水都中毒。防御就是"别让不可信的人往公共水源里倒东西"。


第 4 段(10min)知识块③:ES6 class 的本质 + instanceof 原理

4.1 class 只是语法糖:底层仍是原型链

知识点(细化)

  • class 没有引入新的继承机制,底层就是 ES5 那套原型链。
  • class Dog extends Animal 做的事 ≈ Dog.prototype.__proto__ = Animal.prototype(把子类仓库挂到父类仓库下面)。
  • super() ≈ 在子构造函数里调用父构造函数(Animal.call(this, ...))。
  • super.method()Animal.prototype.method.call(this, ...)

例子:class 与 ES5 一一对照

js 复制代码
// ------ ES5 原型继承 ------
function Animal(name) { this.name = name; }
Animal.prototype.speak = function () { return `${this.name} speaks`; };

function Dog(name) {
  Animal.call(this, name);                 // ≈ super(name)
}
Dog.prototype = Object.create(Animal.prototype);  // ≈ extends(把链接上)
Dog.prototype.constructor = Dog;           // 补回铭牌
Dog.prototype.speak = function () {        // 覆盖方法
  return Animal.prototype.speak.call(this) + ' 汪汪';  // ≈ super.speak()
};

// ------ 同样的东西用 class ------
class Animal2 { constructor(name) { this.name = name; } speak() { return `${this.name} speaks`; } }
class Dog2 extends Animal2 {
  constructor(name) { super(name); }               // super() ≈ Animal.call(this, name)
  speak() { return super.speak() + ' 汪汪'; }       // super.speak() ≈ Animal.prototype.speak.call(this)
}

验证两张图是同一张图

js 复制代码
const d = new Dog2('旺财');
console.log(d.speak());                  // 旺财 speaks 汪汪
console.log(d instanceof Dog2);          // true
console.log(d instanceof Animal2);       // true ------ Dog2.prototype.__proto__ === Animal2.prototype
console.log(Dog2.prototype.__proto__ === Animal2.prototype); // true

类比(礼盒包装)class 是精致的礼盒,里面的东西(原型链)没变,但盒子上写了"使用须知"(见下),比 ES5 那套更不容易用错。面试被问"class 和原型继承有什么关系",答"语法糖 + 背后的链",再加这张对照图,就满分了。

4.2 class 带来的"约束"(它比手写原型严谨在哪)

知识点(细化)

  • 方法不可枚举Object.keys(Dog2.prototype) 是空数组)------手写 Dog.prototype.speak = ... 是可枚举的,会被 for...in 遍历到。
  • class 必须用 new 调用 (当普通函数调 Dog2() 会抛 TypeError)。
  • class 内部默认严格模式this 不会悄悄变成全局)。
  • class 方法不可作为构造函数new Dog2.prototype.speak() 报错)。
  • constructor 时,this 初始化为父类构造结果;派生类里super() 才能用 this

例子

js 复制代码
class Animal { constructor(name) { this.name = name; } speak() {} }
console.log(Object.keys(Animal.prototype));  // [] ------ 方法不可枚举
Animal();      // TypeError: Class constructor cannot be invoked without 'new'

类比(保质期条款):礼盒上印着的"必须冷藏、开封后 3 天内食用"------这些条款(不可枚举/必须 new/严格模式)解决了 ES5 时代"手写原型容易出错、方法被 for...in 枚举出来、忘记 new 导致 this 污染全局"的老问题。

4.3 instanceof:不是在问"是不是你生的",而是在查"血统"

知识点(细化)

  • a instanceof B 的逻辑:B.prototype 是否出现在 a 的原型链上 (沿 a.__proto__ 逐级上溯查找)。
  • 所以:d instanceof Animal 为 true,不是因为 d 直接由 Animal 创建 ,而是因为 Animal.prototype 在 d 的链上(通过 Dog 继承来的)。
  • 边界:左操作数是原始值(1 instanceof Number)→ false(不装箱);右操作数不是函数 → 抛 TypeError
  • 进阶(了解):Symbol.hasInstance 可以自定义 instanceof 行为。

例子:先猜后跑

js 复制代码
class Animal {}
class Dog extends Animal {}
const d = new Dog();

console.log(d instanceof Dog);      // true   Dog.prototype 在链上
console.log(d instanceof Animal);   // true   Animal.prototype 也在链上(隔代继承)
console.log(1 instanceof Number);   // false  原始值不参与
Dog instanceof Function;            // true   类本身是函数,Function.prototype 在它的链上

判定流程(背下来)

javascript 复制代码
a instanceof B:
① a 是原始值? → false
② B 不是函数? → 抛 TypeError
③ 沿 a.__proto__ 逐级上溯,能找到 === B.prototype 的吗? → 找到 true,到 null 则 false

类比(血统/族谱)instanceof 不是在问"你是不是我亲爹生的",而是在翻你的族谱 :你家族谱(原型链)上有没有 XX 这个人。d instanceof Animal 翻族谱时看到"狗的先祖是动物",所以是 true。

真实场景 :框架底层大量用 instanceof/constructor类型识别 ------判断一个值是不是数组(arr instanceof Array)、判断组件实例、判断某个对象属于哪个类。工具库(如 lodash 的 isArray)内部就用 Object.prototype.toString 等原型链技术。


第 5 段(10min)费曼复述:讲给"零基础同学"听

5.1 复述提纲(出声讲一遍,卡壳处回去重看)

  1. 为什么要有原型? ------ 方法放公共仓库(prototype),所有实例共享一份,省内存。
  2. 属性怎么找到? ------ 先翻自己口袋(自有属性),没有就沿绳子(__proto__)一层层往上问,问到 Object.prototype 还没有就 undefined
  3. 三个词谁是谁? ------ prototype 是构造函数身上的仓库;__proto__ 是实例身上的钥匙/绳子;constructor 是仓库门上的铭牌。等式:foo.__proto__ === Foo.prototypeFoo.prototype.constructor === Foo
  4. instanceof 在干嘛? ------ 翻族谱:右操作数的 prototype 在不在左操作数的链上。
  5. class 是什么? ------ 语法糖礼盒:extends = 把子类仓库挂到父类仓库下,super() = 调父构造器;盒子上还印了"必须 new、方法不可枚举"等条款。

5.2 自测清单(每条能写出例子才算过)

  • 能画出 new Foo()fooFoo.prototypeObject.prototype 的完整链
  • 能说清 prototype/__proto__/constructor 分别挂在谁身上、指向谁
  • 能解释"重写 prototype 为什么断链",以及如何补救
  • 能说出 Object.create(null) 的用途与防污染原理
  • 能把一段 ES5 继承逐行对应到 class 的 constructor/extends/super()
  • 能讲清 instanceof 的底层判断步骤

第 6 段(20min)练习①:ES5 继承 + class 对照(code\es5-inheritance.js)

目标:先自己写 ,卡住再看提示。写完用验证清单测。 📄 参考实现已放好:code\es5-inheritance.js(含 ES5 版、class 版、逐行对照注释、自测)。写完自己的版本再打开对照。

6.1 分步提示(不要一上来就看)

  • 第 1 步 :写父类 Animal:构造函数里 this.name = name(自有属性),Animal.prototype.speak 返回 '${this.name} speaks'
  • 第 2 步 :写子类 Dog
    • 构造函数里 Animal.call(this, name)(≈ super);
    • Dog.prototype = Object.create(Animal.prototype)(≈ extends,把链接上);
    • Dog.prototype.constructor = Dog(补铭牌);
    • 覆盖 Dog.prototype.speak,内部用 Animal.prototype.speak.call(this)(≈ super.speak())。
  • 第 3 步:写等价的 class 版,跑出相同结果。
  • 第 4 步:在注释里逐行标注"这行 ≈ class 的哪个语法"。

6.2 验证清单(每一条都应通过)

js 复制代码
new Dog('旺财').speak();        // '旺财 speaks 汪汪'
const d = new Dog('来福');
d instanceof Dog;               // true
d instanceof Animal;            // true ------ 证明链接对了
Dog.prototype.constructor === Dog;  // true ------ 铭牌补回来了

6.3 常见报错排查

报错/现象 原因 修法
Animal is not defined 子类构造里忘了 Animal.call(this, ...) 加上父类调用
d instanceof Animal 是 false 忘了 Dog.prototype = Object.create(Animal.prototype),链没接上 加上并确认写在 new 之前
d.constructor 变成了 Animal 重写 prototype 后没补铭牌 Dog.prototype.constructor = Dog
输出是 undefined speaks this.name 没写进实例(父类构造没执行) 检查子类构造是否 Animal.call(this, name)

第 7 段(20min)练习②③:画关系图 + myInstanceof

7.1 练习②:画 new Foo() 的关系图(code\prototype-chain.md)

先用纸笔或文本画,画完对照参考。要点:四个节点(foo、Foo.prototype、Object.prototype、null)+ 三条连线 + 标注 constructor 归属。

参考图(文本版)

text 复制代码
                 ┌──────────────────────────────┐
                 │      Object.prototype        │
                 │  (toString / hasOwnProperty) │
                 └───────────────┬──────────────┘
                     __proto__ ▲ │ (再往上没有了)
                     (指向这里)│
                 ┌───────────────┴──────────────┐
                 │         Foo.prototype         │
                 │  constructor ─────► Foo       │  ← 铭牌指向构造函数
                 └───────────────┬──────────────┘
                     __proto__ ▲ │
                 ┌───────────────┴──────────────┐
                 │      foo(实例对象)           │
                 │  name: 'xxx'(自有属性)       │
                 └─────────────────────────────┘

关键等式:
  foo.__proto__ === Foo.prototype
  Foo.prototype.constructor === Foo
  Foo.prototype.__proto__ === Object.prototype
  Object.prototype.__proto__ === null   (链的终点)

自检 :图里能不能回答"foo.toString 从哪来"?(答:自己没有 → Foo.prototype 没有 → Object.prototype 上借来的。)

7.2 练习③:myInstanceof(code\myInstanceof.js)

分步提示

  • 第 1 步 :取 obj 的原型:let proto = Object.getPrototypeOf(obj)(比 obj.__proto__ 规范)。
  • 第 2 步while 循环,proto !== null 就继续;每次比较 proto === Ctor.prototype,命中返回 true。
  • 第 3 步:循环结束返回 false。
  • 第 4 步(边界)obj 是原始值 → 返回 false;Ctor 不是函数 → 抛 TypeError
  • 第 5 步 :对照参考实现 code\myInstanceof.js,重点看边界处理。

验证清单

js 复制代码
myInstanceof([], Array);        // true
myInstanceof([], Object);       // true(数组链上有 Object.prototype)
myInstanceof(Object.create(null), Object); // false(无原型)
myInstanceof(1, Number);        // false(原始值)
myInstanceof({}, 123);          // TypeError

第 8 段(10min)思考提高题:先独立想,再看提示

思考题①:给 Object.prototype 加属性,所有对象都能看到,对工程意味着什么?为什么规范不建议?

提示

  • 先跑一下 Object.prototype.polluted = 1; console.log({}.polluted, [].polluted, function(){}.polluted)------你会发现万物皆中招。这就是共享的代价
  • 三点坏处:① 命名冲突(某个库加的 contains 可能和你的冲突);② 被 for...in 遍历到(如果可枚举,所有对象的遍历都多出一个键);③ 安全(原型污染攻击面)。
  • 规范角度:早期 JS 确实有人这么干(老 polyfill),但现代标准提供 MapObject.hasOwnSymbol 等更干净的替代。给内置原型"打补丁"是 ES5 时代的妥协,现在是反模式。

思考题②:class 相比手写原型链多了哪些约束?解决了 ES5 的什么问题?

提示

  • 回到 4.2 的"保质期条款"列表:不可枚举、必须 new、内部严格模式、方法不可构造。
  • 每个约束对应 ES5 的一个真实痛点:方法被 for...in 枚举出来(Object.keys(Foo.prototype) 全是方法)→ 不可枚举;忘记 new 导致 this 污染全局(Foo() 把 name 写到 window)→ 必须 new + 严格模式;new Foo.prototype.method() 被当构造器乱用 → 不可构造。
  • 结论:class 是用"更多约束"换取"更少意外"------约束不是限制,是保护。

思考题③:Vue3 / React 几乎不用手写原型继承,但底层到处是原型链。为什么框架倾向"组合/闭包"而非"继承"?

提示

  • 原型链继承的缺点:层级越深越难维护(改父类波及所有子类)、无法复用"横向"逻辑(一个类只能 extends 一个父类)、继承关系是静态的(运行时不好换)。
  • 组合/闭包的优点:逻辑以函数为单位 自由拼接(useCount()useFetch() 想装几个装几个)、依赖通过参数显式传入(好测试、好替换)、没有"家族图谱"的心智负担。
  • 思考一下 Vue3 的 setup() 里为什么"像写普通函数"就能拥有响应式------因为那是闭包 + 依赖跟踪,根本不依赖 this 和原型链。理解原型链,是为了读懂框架底层(比如 Vue 的响应式对象代理、组件继承),而不是为了鼓励写继承。

第 9 段(10min)测验与收尾

9.1 测验

  • 打开 test\测验.md,限时 15 分钟完成 10 题(巩固 5 + 提升 3 + 横向 2)。
  • 做完再翻答案,把错题按"概念错误 / 表达不清 / 纯粗心"分类标记,回到讲义对应章节重读。

9.2 今日能力自检(打分 1--5,低于 3 分明天补)

能力 自评
能徒手画出 new Foo() 的原型链关系图并解释每条连线
能分清 prototype / __proto__ / constructor 且说清等式
能解释"重写 prototype 断链"和 Object.create(null) 防污染
能把 ES5 继承逐行对照到 class,并说出 class 的约束
能手写 myInstanceof 并处理边界

📌 今日核心速记卡(可截图/摘抄带走)

javascript 复制代码
原型链 = 对象沿 __proto__ 逐级上溯找属性的路径(先自有属性 → 链 → Object.prototype → null)

prototype  = 构造函数身上的共享仓库(放方法)
__proto__  = 实例身上的绳子,指向构造函数的 prototype([[Prototype]])
constructor = 仓库门牌:Foo.prototype.constructor === Foo

核心等式:foo.__proto__ === Foo.prototype
          Foo.prototype.__proto__ === Object.prototype
          Object.prototype.__proto__ === null

instanceof = 查血统:B.prototype 在不在 a 的原型链上(原始值→false,非函数RHS→TypeError)

class = 语法糖礼盒:
  extends    ≈ Dog.prototype = Object.create(Animal.prototype)
  super()    ≈ Animal.call(this, ...)
  super.x()  ≈ Animal.prototype.x.call(this)
  约束:必须 new、方法不可枚举、内部严格模式

Object.create(null) = 无原型纯字典 → 防原型污染
口诀:方法放 prototype,属性放构造函数;给原型打补丁是旧时代的妥协

❌ 常见误区清单(学完自查,哪个说过/想过就划掉)

  • "proto 和 prototype 是同一个东西" ------ ❌ 一个是实例的绳子,一个是构造函数的仓库
  • "实例身上有 prototype 属性" ------ ❌ prototype 挂在构造函数身上
  • "instanceof 判断对象是不是构造函数创建的" ------ ❌ 判断的是 prototype 在不在链上
  • "class 引入了全新的继承机制" ------ ❌ 底层仍是原型链,只是语法糖 + 约束
  • "Object.create(null) 也是 Object 的实例" ------ ❌ 它没有原型,instanceof Object 是 false
  • "重写 prototype 后老实例也能看到新方法" ------ ❌ 老实例的钥匙指向旧仓库,断链
  • "所有函数都有 prototype" ------ ❌ 箭头函数、方法简写没有(不可构造)
  • "构造函数里 this.name = ... 也是共享的" ------ ❌ 那是每个实例的自有属性,各一份

🎯 附加自测:原型链九宫格冲刺(可选,学完当天 / 次日复习用)

玩法:九宫格里每格一道 15 秒快题。先全部填完再对答案,全对 = 原型链通关。 每题都对应本讲义某一段,错了就回去补那一段。

① 填空 ② 判断 ③ 填空
const foo = new Foo(); foo.__proto__ === ___ "prototype 是实例对象的属性。" 对 / 错? Object.prototype.__proto__ === ___
④ 问结果 ⑤ 问结果 ⑥ 问结果
function F(){} F.prototype.say = () => 1; const a = new F(); F.prototype = { say: () => 2 }; a.say(); // ? Object.create(null) instanceof Object // ? const b = Object.create(null); Object.getPrototypeOf(b) // ?
⑦ 补缺 ⑧ 连线 ⑨ 实战判断
整体替换 prototype 后 必须补回哪个属性? Animal.prototype.speak.call(this) 对应 class 里的哪个关键字? new Dog('x') instanceof Animal 为 true 的前提是?(一行代码)

📖 参考答案(先做完再展开)

  1. Foo.prototype ------ 实例 → 仓库的绳子(2.2/3.1)
  2. prototype 挂在构造函数 身上;实例身上的是 __proto__(3.1)
  3. null ------ 原型链终点(2.2)
  4. 1 ------ 老实例 a 的钥匙还指着旧仓库,新方法 () => 2 在新仓库里,老实例看不见(3.2 断链)
  5. false ------ 它没有原型,不是 Object 的实例(3.3)
  6. null ------ 无原型对象,getPrototypeOf 返回 null(3.3)
  7. constructor ------ 补 Foo.prototype.constructor = Foo(3.2 / 6.1)
  8. super.speak() ------ 调父类同名方法(4.1)
  9. Dog.prototype = Object.create(Animal.prototype) ------ 让 Animal.prototype 出现在链上(4.3 / 6.1)
相关推荐
半仙er1 小时前
第一周04天Promise 深入与 async / await
前端
fail_to_code1 小时前
从 Lighthouse 83 到 100:一次 Vue 项目的性能排查实录
前端·人工智能
用户69371750013841 小时前
DeepSeek 调价正式生效:一夜涨 11 倍,靠低价薅羊毛的日子结束了
前端·人工智能·后端
半仙er1 小时前
第一周01天:this 指向与 call / apply / bind
前端
小帅不太帅1 小时前
给大家推荐一个特别好用的专为 AI Agent 打造的最快浏览器
前端·agent·浏览器
半仙er1 小时前
第一周03天 事件循环与宏任务 / 微任务
前端
做前端的娜娜子1 小时前
#浏览器存储方案:localStorage、sessionStorage 与 Cookie
前端·面试·掘金·金石计划
用户921080262861 小时前
1. Ant Design X Vue 项目介绍:结构、组件和启动方式
前端
摸鱼研究员1 小时前
neverthrow,ts 中优雅的异常处理方案
前端·javascript