两小时学习讲义:原型与原型链
配套《学习计划.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时引擎做的事:- 先查
obj的自有属性 (Object.hasOwn(obj, 'xxx')); - 没有 → 沿
obj.__proto__(即[[Prototype]])找; - 还没有 → 再沿那层的
__proto__继续上溯; - 直到
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/hasOwnProperty,Object.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__、constructor、prototype,赋值会污染原型链(原型污染攻击)。用 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 复述提纲(出声讲一遍,卡壳处回去重看)
- 为什么要有原型? ------ 方法放公共仓库(prototype),所有实例共享一份,省内存。
- 属性怎么找到? ------ 先翻自己口袋(自有属性),没有就沿绳子(
__proto__)一层层往上问,问到Object.prototype还没有就undefined。 - 三个词谁是谁? ------
prototype是构造函数身上的仓库;__proto__是实例身上的钥匙/绳子;constructor是仓库门上的铭牌。等式:foo.__proto__ === Foo.prototype,Foo.prototype.constructor === Foo。 - instanceof 在干嘛? ------ 翻族谱:右操作数的 prototype 在不在左操作数的链上。
- class 是什么? ------ 语法糖礼盒:
extends= 把子类仓库挂到父类仓库下,super()= 调父构造器;盒子上还印了"必须 new、方法不可枚举"等条款。
5.2 自测清单(每条能写出例子才算过)
- 能画出
new Foo()后foo、Foo.prototype、Object.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),但现代标准提供
Map、Object.hasOwn、Symbol等更干净的替代。给内置原型"打补丁"是 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 的前提是?(一行代码) |
📖 参考答案(先做完再展开)
Foo.prototype------ 实例 → 仓库的绳子(2.2/3.1)- 错 。
prototype挂在构造函数 身上;实例身上的是__proto__(3.1) null------ 原型链终点(2.2)1------ 老实例a的钥匙还指着旧仓库,新方法() => 2在新仓库里,老实例看不见(3.2 断链)false------ 它没有原型,不是Object的实例(3.3)null------ 无原型对象,getPrototypeOf返回 null(3.3)constructor------ 补Foo.prototype.constructor = Foo(3.2 / 6.1)super.speak()------ 调父类同名方法(4.1)Dog.prototype = Object.create(Animal.prototype)------ 让Animal.prototype出现在链上(4.3 / 6.1)