场景二:利用原型实现继承(组合继承)
这是 ES6 class 语法糖的底层实现原理。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>利用原型链实现继承</title>
</head>
<body>
<script>
// ===== 父类 =====
function Shape(color) {
this.color = color;
}
Shape.prototype.getColor = function () {
return '颜色: ' + this.color;
};
Shape.prototype.toString = function () {
return '[Shape color=' + this.color + ']';
};
// ===== 子类 Circle =====
function Circle(color, radius) {
Shape.call(this, color); // 借用父构造函数,继承实例属性
this.radius = radius;
}
// 设置原型链:Circle.prototype → Shape.prototype → Object.prototype
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle; // 补回 constructor
// 子类特有方法
Circle.prototype.getArea = function () {
return '面积: ' + (Math.PI * this.radius * this.radius).toFixed(2);
};
// 方法重写(覆盖父类方法)
Circle.prototype.toString = function () {
return '[Circle color=' + this.color + ' radius=' + this.radius + ']';
};
// ===== 子类 Rectangle =====
function Rectangle(color, width, height) {
Shape.call(this, color);
this.width = width;
this.height = height;
}
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
Rectangle.prototype.getArea = function () {
return '面积: ' + (this.width * this.height);
};
// ===== 测试 =====
var c = new Circle('红色', 5);
var r = new Rectangle('蓝色', 4, 6);
console.log(c.getColor()); // 颜色: 红色(继承自 Shape)
console.log(c.getArea()); // 面积: 78.54(Circle 自有方法)
console.log(String(c)); // [Circle color=红色 radius=5]
console.log(r.getColor()); // 颜色: 蓝色
console.log(r.getArea()); // 面积: 24
// instanceof 验证继承关系
console.log(c instanceof Circle); // true
console.log(c instanceof Shape); // true
console.log(c instanceof Object); // true
console.log(r instanceof Circle); // false(不是 Circle 的实例)
console.log(r instanceof Rectangle); // true
</script>
</body>
</html>
📋 代码逐行解析(组合继承)
js
// ===== 组合继承 = 构造函数继承 + 原型链继承 =====
// 步骤1:父构造函数借用(构造函数继承)
function Circle(color, radius) {
Shape.call(this, color); // 关键:借用父构造函数初始化实例属性
// 等价于在 Circle 构造函数内执行:this.color = color
// 这样每个 Circle 实例都有独立的 color 属性(不共享)
this.radius = radius;
}
// 步骤2:建立原型链(原型链继承)
Circle.prototype = Object.create(Shape.prototype);
// 等价效果:Circle.prototype.__proto__ = Shape.prototype
// 这样 Circle 实例可以访问 Shape.prototype 上的方法(getColor 等)
// 步骤3:修复 constructor(必须!)
Circle.prototype.constructor = Circle;
// 替换整个 prototype 导致 constructor 丢失,手动补回
// 验证继承关系:
// c 的原型链:c → Circle.prototype → Shape.prototype → Object.prototype
// c instanceof Shape → Shape.prototype 在链上 → true
// c instanceof Circle → Circle.prototype 在链上 → true
组合继承 vs 其他继承方式
| 继承方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 原型链继承 | Sub.prototype = new Super() |
简单 | 引用属性共享;无法传参 |
| 构造函数继承 | Super.call(this) |
属性独立;可传参 | 方法无法复用(每次 new 都创建新方法) |
| 组合继承 | 两者结合 | 属性独立 + 方法共享 | 父构造函数执行两次 |
| 寄生组合继承 | Object.create + call |
最优解,父构造函数只执行一次 | 写法略复杂 |
| ES6 class extends | 语法糖(本质是寄生组合) | 最简洁,最推荐 | 需要 ES6 环境 |
🌐 经典使用场景
场景一:UI 组件体系
BaseComponent(基类): render(), destroy(), on(), off()
├─ InputComponent extends BaseComponent: validate(), focus()
├─ ButtonComponent extends BaseComponent: click(), disable()
└─ ModalComponent extends BaseComponent: open(), close()
每种组件共享基类方法,各自扩展专有行为
场景二:数据模型体系
BaseModel: save(), delete(), toJSON()
├─ UserModel extends BaseModel: login(), logout()
├─ ProductModel extends BaseModel: addToCart(), getReviews()
→ 公共 CRUD 逻辑复用,业务逻辑各自扩展
场景三:错误体系
class AppError extends Error {
constructor(code, message) {
super(message);
this.code = code;
}
}
class NetworkError extends AppError { ... }
class AuthError extends AppError { ... }
→ 业务错误分类,统一处理 + 精确捕获
💼 业务价值
- 代码复用率:继承体系将公共逻辑下沉到基类,子类只关注差异点,可使代码量减少 30%~60%
- 一致性保证:基类接口统一,所有子类行为可预期,降低团队协作成本
- 扩展性:新增子类只需继承基类,不需要修改现有代码(开闭原则),是大型项目长期维护的基础
场景三:防止原型污染
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>原型污染与防护</title>
</head>
<body>
<script>
// ❌ 危险:原型污染
// 假设来自不可信数据源的 JSON
// JSON.parse('{"__proto__": {"polluted": "危险!"}}') 在某些场景下会污染原型
// ✅ 安全:使用 Object.create(null) 创建无原型的纯净 Map 对象
var safeMap = Object.create(null);
safeMap['key1'] = 'value1';
safeMap['key2'] = 'value2';
// safeMap 没有 toString、hasOwnProperty 等原型方法
console.log(safeMap.toString); // undefined(无原型链!)
console.log(Object.prototype.isPrototypeOf(safeMap)); // false
// 遍历时无需 hasOwnProperty 过滤
for (var k in safeMap) {
console.log(k, ':', safeMap[k]);
}
// ✅ 现代做法:使用 Map 数据结构代替 Object 作为键值存储
var map = new Map();
map.set('name', 'Alice');
map.set('age', 28);
console.log(map.get('name')); // 'Alice'
console.log(map.size); // 2
</script>
</body>
</html>
📋 代码逐行解析(原型污染防御)
js
// 原型污染(Prototype Pollution):
// 攻击者通过操控 __proto__ 或 constructor.prototype,
// 修改 Object.prototype,使所有对象都受影响。
// 攻击示例(了解原理,不要在生产代码中执行):
// var evil = JSON.parse('{"__proto__": {"isAdmin": true}}');
// 在有漏洞的合并函数中,evil.__proto__ 的属性会被复制到 Object.prototype
// 结果:所有对象都有 isAdmin = true,严重的安全漏洞!
// 防御方式一:Object.create(null) 作为数据容器
var safeMap = Object.create(null);
// safeMap 没有任何原型,攻击者无法通过 __proto__ 触及 Object.prototype
// 所有 key 都是纯粹的数据,不存在方法名冲突(toString/valueOf 等)
// 防御方式二:访问前用 hasOwnProperty 校验
function safeGet(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : undefined;
// 注意:用 Object.prototype.hasOwnProperty.call 而不是 obj.hasOwnProperty
// 因为 obj 可能是 Object.create(null) 创建的,没有 hasOwnProperty 方法
}
// 防御方式三:现代代码推荐用 Map 代替 Object 存储动态 key
var map = new Map();
// Map 的 key 存储在内部的哈希表中,与原型链完全隔离,天然安全
🌐 经典使用场景
场景一:接口参数合并(高危场景)
// ❌ 危险:直接合并用户输入的对象
function mergeConfig(target, source) {
for (var key in source) target[key] = source[key]; // 可能污染原型!
}
// ✅ 安全:只合并自有属性
function mergeConfigSafe(target, source) {
Object.keys(source).forEach(key => target[key] = source[key]);
// Object.keys 只返回自有可枚举属性,不遍历原型链
}
场景二:路由/权限表
var routes = Object.create(null);
routes['/home'] = HomeHandler;
routes['/admin'] = AdminHandler;
// 不会与 toString/valueOf 等方法名冲突,安全可靠
场景三:缓存系统
var cache = new Map(); // 用 Map 而不是 {}
// Map 支持任意类型的 key(包括对象),且没有原型污染风险
💼 业务价值
- 安全性:原型污染是 Node.js 生态中一类真实存在的 CVE 漏洞(如 lodash merge 曾被爆出高危漏洞),防御意识能直接避免安全事故
- 稳定性 :污染
Object.prototype会影响所有对象的行为,可能导致系统级崩溃,防御成本远低于修复成本 - 可信度:在代码审查时能识别并修复原型污染风险,是资深工程师的重要能力标志
场景四:事件委托中的 this 应用
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>事件处理中的 this</title>
<style>
.btn-group { display: flex; gap: 8px; margin: 20px; }
button { padding: 8px 16px; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; }
#output { margin: 20px; padding: 12px; background: #f5f5f5; border-radius: 4px; min-height: 40px; }
</style>
</head>
<body>
<div class="btn-group" id="btnGroup">
<button data-action="save">保存</button>
<button data-action="delete">删除</button>
<button data-action="export">导出</button>
</div>
<div id="output">点击上方按钮查看 this 指向</div>
<script>
var output = document.getElementById('output');
var group = document.getElementById('btnGroup');
// 事件委托:this 指向触发事件的元素(事件绑定的元素)
group.addEventListener('click', function (e) {
// this === group(绑定事件的元素)
// e.target === 被点击的 button
if (e.target.tagName === 'BUTTON') {
var action = e.target.dataset.action;
output.textContent =
'this 是: ' + this.id +
',e.target 是: ' + e.target.textContent +
',action: ' + action;
}
});
// 构造函数 + 原型方法 + this
function Counter(el) {
this.count = 0;
this.el = el;
}
Counter.prototype.increment = function () {
this.count++;
this.el.textContent = '计数: ' + this.count;
};
Counter.prototype.reset = function () {
this.count = 0;
this.el.textContent = '计数: 0';
};
var display = document.createElement('div');
display.style.margin = '20px';
display.textContent = '计数: 0';
document.body.appendChild(display);
var btn1 = document.createElement('button');
btn1.textContent = '+1';
btn1.style.margin = '0 20px';
document.body.appendChild(btn1);
var btn2 = document.createElement('button');
btn2.textContent = '重置';
document.body.appendChild(btn2);
var counter = new Counter(display);
btn1.addEventListener('click', function () { counter.increment(); });
btn2.addEventListener('click', function () { counter.reset(); });
</script>
</body>
</html>
📋 代码逐行解析(事件委托 + 计数器)
js
// ===== 事件委托部分 =====
group.addEventListener('click', function (e) {
// this === group(绑定事件的父元素)
// e.target === 实际被点击的子元素(button)
// 事件委托原理:点击冒泡到 group,group 的监听器检查 e.target
if (e.target.tagName === 'BUTTON') {
// 通过 dataset.action 读取按钮的自定义数据属性
var action = e.target.dataset.action; // 'save'、'delete' 或 'export'
// this.id 是 'btnGroup'(this === group)
// e.target.textContent 是被点击按钮的文字
}
});
// 优势:只需1个监听器处理所有按钮,动态添加的按钮也自动生效
// ===== 计数器部分(构造函数 + 原型方法)=====
function Counter(el) {
this.count = 0; // 计数器状态:每个实例独立
this.el = el; // 关联的 DOM 元素:每个实例独立
}
// 方法放原型:所有计数器实例共享同一份逻辑
Counter.prototype.increment = function () {
this.count++; // 修改当前实例的 count
this.el.textContent = '计数: ' + this.count; // 更新当前实例关联的 DOM
};
// 注意:这里不能用箭头函数替换 function,因为需要 this 动态绑定到各自的实例
// 如果有多个计数器,各自维护各自的 count 和 el,互不干扰
// 为什么用匿名函数包一层传给 addEventListener?
btn1.addEventListener('click', function () { counter.increment(); });
// 等价于:btn1.onclick = function() { counter.increment(); }
// 如果直接传 counter.increment,事件触发时 this 会变成 btn1,导致 this.count/el 出错
// 用匿名函数包一层,通过闭包持有 counter 引用,确保调用正确的对象
🌐 经典使用场景
场景一:事件委托在列表中的应用(最常见)
<ul id="list">
<li data-id="1">商品A</li>
<li data-id="2">商品B</li>
<!-- 可能动态追加数百个 li -->
</ul>
document.getElementById('list').addEventListener('click', function(e) {
if (e.target.tagName === 'LI') {
loadDetail(e.target.dataset.id); // 只需1个监听器,动态 li 也生效
}
});
→ 性能:1个监听器 vs N个监听器(N=列表项数量)
场景二:计数器组件(倒计时、点赞数、购物车数量)
var likeBtn = document.getElementById('like');
var counter = new Counter(likeBtn.querySelector('.count'));
likeBtn.addEventListener('click', function() { counter.increment(); });
→ 每个点赞按钮对应一个 Counter 实例,互不干扰
场景三:表单步骤控制器
function StepController(totalSteps, displayEl) {
this.current = 1;
this.total = totalSteps;
this.el = displayEl;
}
StepController.prototype.next = function() { this.current = Math.min(this.current+1, this.total); this.render(); };
StepController.prototype.prev = function() { this.current = Math.max(this.current-1, 1); this.render(); };
StepController.prototype.render = function() { this.el.textContent = this.current + '/' + this.total; };
💼 业务价值
- 性能:事件委托将 N 个监听器减为 1 个,在长列表(如商品列表、消息流)中内存消耗减少显著,且支持动态内容
- 组件设计:构造函数 + 原型方法的计数器模式,是 Vue/React 组件思想的原始形态,理解它有助于深刻理解前端组件化的本质
- 可扩展性:基于原型的组件可以随时添加新方法(热补丁),无需重构现有实例
深入理论扩展
8.1 设计哲学:基于原型 vs 基于类
JavaScript 的"基因"
JavaScript 诞生于 1995 年,设计者 Brendan Eich 在短短 10 天内完成了语言原型。他参考了 Self 语言 (基于原型的 OOP 先驱)和 Scheme(函数式),放弃了传统的"类"模型,选择了"原型委托"作为对象复用机制。
"我本打算让 JavaScript 拥有 Self 语言的对象模型,但为了让它看起来更像 Java,我加上了构造函数和
new关键字。" ------ Brendan Eich
基于原型 vs 基于类的本质差异
基于原型 Prototype-based(JavaScript)
Object.create 或 new
\[Prototype\]\] 链 可继续链接 原型对象 (普通对象) 新对象 (委托原型) 更高层原型 基于类 Class-based(Java / C++) 实例化 new 继承 extends 实例化 new 类 Class (蓝图/模板) 对象实例 Instance (具体对象) 子类 Subclass 子类实例 | 维度 | 基于类(Class-based) | 基于原型(Prototype-based) | |------|-------------------------|-------------------------------------| | 模板 | 类(抽象描述,不是对象) | 原型(本身就是普通对象) | | 实例化 | `new ClassName()` 按蓝图复制 | `new Fn()` 或 `Object.create()` 委托原型 | | 继承 | 类继承(静态,编译期确定) | 原型链(动态,运行时可修改) | | 代码复用 | 复制成员到实例 | 委托(实例访问原型,不复制) | | 灵活性 | 较低(类结构固定) | 极高(运行时可动态添加/修改原型) | | 代表语言 | Java、C++、C# | JavaScript、Self、Lua | ##### 委托(Delegation)是核心理念 在 JavaScript 中,实例并不"拥有"方法------它只是**委托**给原型去查找。这是与类继承的本质区别: 类继承:复制(Copy) → 实例自己拥有所有方法 原型委托:链接(Link) → 实例查找时"询问"原型 ```html
p === null?
返回 undefined
对 p 重复 [[Get]] 操作
(即递归沿链向上)
[[Set]] 操作(赋值)的完整规则:
有且 writable:true
有且 writable:false
有访问器描述符
没有
没有
有且 writable:true
有且 writable:false
有访问器描述符
obj.prop = value
obj 自身有 prop?
直接设置自有属性的 value
严格模式报错
非严格模式静默失败
调用 set(value)
原型链上有 prop?
在 obj 自身创建新属性
在 obj 自身创建新属性
(遮蔽原型属性)
严格模式报错
非严格模式静默失败
调用原型上的 set(value)
重要 :只有在原型链上找到的属性是可写的(writable: true)数据属性时,赋值才会在实例自身上创建新属性(属性遮蔽)。否则要么报错,要么静默失败,要么调用 setter。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>[[Get]] 与 [[Set]] 操作演示</title>
</head>
<body>
<script>
// 演示 delete 只删除自有属性
function Demo() {}
Demo.prototype.shared = '来自原型';
var d = new Demo();
// 在实例上创建同名属性(遮蔽原型)
d.shared = '实例自有';
console.log(d.shared); // '实例自有'
console.log(d.hasOwnProperty('shared')); // true
// delete 只删除实例自有属性
delete d.shared;
console.log(d.shared); // '来自原型'(回到原型属性)
console.log(d.hasOwnProperty('shared')); // false
// delete 不能删除原型上的属性(通过实例操作)
delete d.shared; // 什么都没发生
console.log(d.shared); // '来自原型'(原型属性仍在)
</script>
</body>
</html>
💡 代码解析
代码片段 含义 d.shared = '实例自有'[[Set]]操作:原型上有shared且可写,所以在实例d上创建了同名自有属性(属性遮蔽)d.hasOwnProperty('shared')返回true确认实例自身有 shared,原型的同名属性被遮蔽但仍然存在delete d.shared[[Delete]]操作只删除实例自有属性,不影响原型链上的属性删除后 d.shared返回'来自原型'自有属性被删除后, [[Get]]沿原型链继续查找,找到Demo.prototype.shared并返回再次 delete d.shared(无效)此时实例上已无 shared,delete作用于原型属性时无效(不报错,静默忽略)
8.5 this 绑定规则优先级与 call/apply/bind
四种绑定规则的优先级(从高到低)
优先级 1(最高):new 绑定
优先级 2:显式绑定(call / apply / bind)
优先级 3:隐式绑定(方法调用,对象.方法())
优先级 4(最低):默认绑定(普通函数调用)
是
否
是
否
是
否
调用函数
用 new 调用?
✅ 优先级1: this = 新对象
call/apply/bind 绑定?
✅ 优先级2: this = 指定对象
作为对象方法调用?
✅ 优先级3: this = 调用对象
✅ 优先级4: this = window/undefined
call / apply / bind 的区别
| 方法 | 语法 | 调用时机 | 参数传递 |
|---|---|---|---|
call |
fn.call(ctx, arg1, arg2) |
立即调用 | 逐个传入 |
apply |
fn.apply(ctx, [arg1, arg2]) |
立即调用 | 数组传入 |
bind |
fn.bind(ctx, arg1) |
返回新函数,延迟调用 | 逐个传入(可柯里化) |
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>call / apply / bind 完整演示</title>
</head>
<body>
<script>
function introduce(greeting, punctuation) {
return greeting + ',我是 ' + this.name + ',年龄 ' + this.age + punctuation;
}
var alice = { name: 'Alice', age: 28 };
var bob = { name: 'Bob', age: 32 };
// call:立即调用,参数逐个传
console.log(introduce.call(alice, '你好', '!'));
// 你好,我是 Alice,年龄 28!
// apply:立即调用,参数用数组
console.log(introduce.apply(bob, ['Hi', '.']));
// Hi,我是 Bob,年龄 32.
// bind:返回新函数,this 永久绑定 alice
var aliceIntro = introduce.bind(alice, '嗨');
console.log(aliceIntro('~')); // 嗨,我是 Alice,年龄 28~
console.log(aliceIntro('!!!')); // 嗨,我是 Alice,年龄 28!!!
// 优先级验证:bind 后再 new,new 优先级更高
function Greeter(greeting) {
this.greeting = greeting;
}
var BoundGreeter = Greeter.bind({ x: 999 }, 'Hello');
var g = new BoundGreeter(); // new 优先,this 是新对象,不是 {x:999}
console.log(g.greeting); // 'Hello'
console.log(g.x); // undefined(不是 999,this 不是绑定的对象)
// 手写 bind
Function.prototype.myBind = function (ctx) {
var fn = this;
var args = Array.prototype.slice.call(arguments, 1);
return function () {
var callArgs = args.concat(Array.prototype.slice.call(arguments));
return fn.apply(ctx, callArgs);
};
};
var aliceIntro2 = introduce.myBind(alice, '大家好');
console.log(aliceIntro2('?')); // 大家好,我是 Alice,年龄 28?
</script>
</body>
</html>
💡 代码解析
代码片段 含义 introduce.call(alice, '你好', '!')立即调用 introduce,this绑定为alice,参数逐个传入introduce.apply(bob, ['Hi', '.'])立即调用, this绑定为bob,参数以数组形式传入(适用于参数列表动态变化的场景)introduce.bind(alice, '嗨')返回新函数, this永久绑定alice,第一个参数预设为'嗨'(柯里化效果)aliceIntro('~')后续调用每次调用只需传最后一个参数,前面的参数已通过 bind预置new BoundGreeter()的结果new的优先级高于bind的显式绑定,this是新创建的对象,而非{x:999}g.x为undefined证明 new调用时完全忽略bind指定的this,绑定的{x:999}对象不起作用Function.prototype.myBind手写实现通过闭包保存 fn、ctx、args,返回新函数;调用时合并预置参数与运行时参数,再用apply调用原函数
8.6 深拷贝与浅拷贝(引用类型核心应用)
引用类型"赋值复制引用"的特性,直接催生了深浅拷贝这一核心话题。
浅拷贝(Shallow Copy)
只复制对象的第一层属性,嵌套的对象仍然共享引用。
堆内存
浅拷贝 copy
原始对象 original
指向
指向同一个
name: 'Alice'
score: [95, 87]
name: 'Alice'(独立)
score: 引用 →
95, 87
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>浅拷贝 vs 深拷贝</title>
</head>
<body>
<script>
var original = {
name: 'Alice',
address: { city: '深圳', zip: '518000' },
scores: [95, 87, 92]
};
// ===== 浅拷贝方式 =====
// 方式一:Object.assign
var shallow1 = Object.assign({}, original);
// 方式二:扩展运算符(ES6)
var shallow2 = { ...original };
// 方式三:手写浅拷贝
function shallowCopy(obj) {
var copy = {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) copy[key] = obj[key];
}
return copy;
}
var shallow3 = shallowCopy(original);
// 验证:修改第一层属性互不影响
shallow1.name = 'Bob';
console.log(original.name); // 'Alice'(✅ 独立)
// 验证:修改嵌套对象相互影响
shallow1.address.city = '上海';
console.log(original.address.city); // '上海'(❌ 共享引用!)
console.log('---深拷贝---');
// ===== 深拷贝方式 =====
// 方式一:JSON 序列化(简单但有缺陷)
var deep1 = JSON.parse(JSON.stringify(original));
deep1.address.city = '成都';
console.log(original.address.city); // '上海'(✅ 独立,JSON 深拷贝生效)
// JSON 序列化的缺陷演示
var tricky = {
fn: function () {}, // 函数会丢失
undef: undefined, // undefined 会丢失
date: new Date(), // Date 变成字符串
reg: /hello/g, // 正则变成 {}
circular: null
};
tricky.circular = tricky; // 循环引用 → JSON.stringify 报错
// 方式二:structuredClone(现代浏览器内置,推荐)
var original2 = { name: 'Alice', scores: [95, 87], address: { city: '深圳' } };
var deep2 = structuredClone(original2);
deep2.address.city = '北京';
console.log(original2.address.city); // '深圳'(✅ 完全独立)
// 方式三:递归手写深拷贝(支持循环引用)
function deepCopy(obj, seen) {
if (obj === null || typeof obj !== 'object') return obj;
seen = seen || new WeakMap();
if (seen.has(obj)) return seen.get(obj); // 处理循环引用
var copy = Array.isArray(obj) ? [] : {};
seen.set(obj, copy);
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key], seen);
}
}
return copy;
}
var src = { a: 1, b: { c: [1, 2, 3] } };
src.self = src; // 循环引用
var dst = deepCopy(src);
dst.b.c.push(4);
console.log(src.b.c); // [1, 2, 3](✅ 独立)
console.log(dst.b.c); // [1, 2, 3, 4]
</script>
</body>
</html>
💡 代码解析
代码片段 含义 Object.assign({}, original)浅拷贝:创建新对象,第一层属性复制值,嵌套对象复制引用地址 { ...original }ES6 扩展运算符浅拷贝,效果与 Object.assign相同shallow1.name = 'Bob'不影响original.name第一层的字符串属性是值类型,赋值是独立副本 shallow1.address.city = '上海'污染original嵌套对象共享同一引用,修改任一变量都会影响另一个 JSON.parse(JSON.stringify(original))深拷贝:序列化为字符串后再解析,简单但有缺陷(函数/undefined/Date/循环引用无法正确处理) tricky.circular = tricky创建循环引用, JSON.stringify会直接抛出TypeErrorstructuredClone(original2)现代浏览器内置的深拷贝 API,支持 Date、RegExp、循环引用,推荐使用 `seen = seen Array.isArray(obj) ? [] : {}区分数组和对象,保证深拷贝结果的类型正确
深浅拷贝方法对比
| 方法 | 类型 | 函数 | 循环引用 | Date/RegExp | 推荐场景 |
|---|---|---|---|---|---|
Object.assign |
浅 | ❌ 丢失 | ✅ | ✅ | 简单对象合并 |
扩展运算符 {...} |
浅 | ❌ 丢失 | ✅ | ✅ | 简单对象合并 |
JSON.parse/stringify |
深 | ❌ 丢失 | ❌ 报错 | ❌ 变字符串 | 简单纯数据对象 |
structuredClone |
深 | ❌ 丢失 | ✅ | ✅ | 现代浏览器推荐 |
| 递归手写 | 深 | 可支持 | ✅ | 需特殊处理 | 自定义需求 |
8.7 垃圾回收机制与内存管理
JavaScript 引擎(V8 等)使用自动垃圾回收(Garbage Collection,GC),开发者无需手动释放内存。但理解其原理有助于避免内存泄漏。
标记清除算法(Mark-and-Sweep)
现代 JavaScript 引擎采用的主要 GC 算法:
阶段1:标记
从根对象(window/全局)出发
可达的对象打上标记
阶段2:清除
未被标记的对象
= 不可达 = 垃圾
释放其内存
阶段3:整理(可选)
压缩内存碎片
提升分配效率
根对象(Roots) 包括:全局变量、调用栈中的局部变量、活跃的定时器回调等。
引用类型常见内存泄漏场景
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>内存泄漏场景演示</title>
</head>
<body>
<script>
// ===== 场景一:意外的全局变量 =====
function leakGlobal() {
// 忘写 var/let/const,意外创建全局变量
leakedVar = { bigData: new Array(100000).fill('x') };
}
// leakGlobal(); // leakedVar 挂在 window 上,永不被 GC
// ===== 场景二:闭包持有大对象引用 =====
function createLeak() {
var bigObj = { data: new Array(100000).fill(0) };
return function () {
// 闭包持有 bigObj 的引用,即使外部不再需要
console.log(bigObj.data.length);
};
}
var leak = createLeak(); // bigObj 无法被 GC,因为 leak 持有其引用
// ✅ 修复:不再需要时手动解除引用
// leak = null; // bigObj 可以被 GC 了
// ===== 场景三:忘记移除事件监听器 =====
var btn = document.createElement('button');
btn.textContent = '测试按钮';
document.body.appendChild(btn);
function handleClick() { console.log('clicked'); }
btn.addEventListener('click', handleClick);
// ❌ 如果 btn 被移出 DOM,但未移除事件,handleClick 和相关闭包仍被持有
// ✅ 正确做法:移除时同时移除监听
// btn.removeEventListener('click', handleClick);
// document.body.removeChild(btn);
// ===== 场景四:WeakMap 解决引用类型键的内存问题 =====
var cache = new WeakMap(); // 键是弱引用,不阻止 GC
function processNode(node) {
if (!cache.has(node)) {
cache.set(node, { computed: node.textContent.length });
}
return cache.get(node).computed;
}
// node 被 DOM 移除后,cache 中对应的条目自动被 GC
// 若用普通 Map,则 node 即使移除也不会被 GC(Map 持有强引用)
console.log('内存泄漏场景演示完成');
</script>
</body>
</html>
💡 代码解析
代码片段 含义 leakedVar = { bigData: ... }忘写 var/let/const,意外将变量挂到window上,window.leakedVar永远可达,GC 无法回收createLeak()返回闭包闭包内的 bigObj被返回的函数引用,只要leak变量存在,bigObj就无法被 GCleak = null解除引用后, bigObj变为不可达对象,标记清除算法在下一轮 GC 时将其回收btn.addEventListener('click', handleClick)事件监听器持有 handleClick的引用;如果btn被移出 DOM 但未移除监听,handleClick及其闭包中的对象无法被 GCnew WeakMap()WeakMap的键是弱引用,不阻止键对象被 GC。当 DOM 节点被移除后,对应的缓存条目也会自动消失普通 Map的问题普通 Map持有键的强引用,即使 DOM 节点已移除,Map中的node → data映射仍然阻止该节点被 GC
引用计数算法(已淘汰,了解即可)
早期浏览器(IE 6/7 时代)使用引用计数,存在循环引用无法回收的致命缺陷:
js
// 循环引用导致引用计数永远不为 0
var a = {};
var b = {};
a.ref = b; // b 的引用计数 +1
b.ref = a; // a 的引用计数 +1
// 即使 a 和 b 从全局移除,双方互相持有引用,引用计数永远 ≥ 1,无法 GC
a = null;
b = null;
// 现代 JS 引擎用标记清除(从根出发),可正确处理循环引用
8.8 ES6 class 语法与原型链的对应关系
ES6 的 class 是纯粹的语法糖,其底层完全基于原型链。理解这一点,能让你读懂任何 ES5 风格的框架源码。
等价的 ES5 原型写法
ES6 class 语法
编译为
编译为
class Animal {
constructor(name) {}
speak() {}
}
class Dog extends Animal {
constructor(name) { super(name); }
bark() {}
}
function Animal(name) {}
Animal.prototype.speak = function() {}
function Dog(name) {
Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {}
完整对比示例
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>class 与原型的等价关系</title>
</head>
<body>
<script>
// ===== ES6 class 写法 =====
class AnimalES6 {
constructor(name) {
this.name = name;
}
speak() {
return this.name + ' 发出声音';
}
static create(name) {
return new AnimalES6(name);
}
}
class DogES6 extends AnimalES6 {
constructor(name, breed) {
super(name); // 等价于 AnimalES6.call(this, name)
this.breed = breed;
}
bark() {
return this.name + '(' + this.breed + ')汪汪!';
}
}
// ===== 等价的 ES5 原型写法 =====
function AnimalES5(name) {
this.name = name;
}
AnimalES5.prototype.speak = function () {
return this.name + ' 发出声音';
};
AnimalES5.create = function (name) { // 静态方法
return new AnimalES5(name);
};
function DogES5(name, breed) {
AnimalES5.call(this, name); // super(name)
this.breed = breed;
}
DogES5.prototype = Object.create(AnimalES5.prototype);
DogES5.prototype.constructor = DogES5;
DogES5.prototype.bark = function () {
return this.name + '(' + this.breed + ')汪汪!';
};
// 两者行为完全一致
var d1 = new DogES6('旺财', '柴犬');
var d2 = new DogES5('小黑', '拉布拉多');
console.log(d1.speak()); // '旺财 发出声音'(继承自 AnimalES6)
console.log(d1.bark()); // '旺财(柴犬)汪汪!'
console.log(d1 instanceof DogES6); // true
console.log(d1 instanceof AnimalES6); // true
console.log(d2.speak()); // '小黑 发出声音'
console.log(d2.bark()); // '小黑(拉布拉多)汪汪!'
// 验证 class 的本质是函数
console.log(typeof AnimalES6); // 'function'
console.log(AnimalES6.prototype.constructor === AnimalES6); // true
// class 与 function 的差异(class 更严格)
// 1. class 不能不用 new 调用(会报错)
// 2. class 方法默认不可枚举(enumerable: false)
// 3. class 内部默认严格模式
// 4. class 不存在变量提升(Temporal Dead Zone)
console.log(Object.getOwnPropertyDescriptor(DogES6.prototype, 'bark').enumerable);
// false(class 方法默认不可枚举)
console.log(Object.getOwnPropertyDescriptor(DogES5.prototype, 'bark').enumerable);
// true(普通赋值默认可枚举)
</script>
</body>
</html>
💡 代码解析
代码片段 含义 class AnimalES6 { constructor(name) { this.name = name; } }class的constructor等价于 ES5 的构造函数体,this.name在实例上设置自有属性super(name)等价于 AnimalES6.call(this, name),必须在使用this前调用,否则报ReferenceErrorstatic create(name)静态方法挂在类本身( AnimalES6.create),而非prototype上,等价于AnimalES6.create = function(){}DogES5.prototype = Object.create(AnimalES5.prototype)建立原型链: DogES5实例 →DogES5.prototype→AnimalES5.prototype→Object.prototypeDogES5.prototype.constructor = DogES5修复因替换 prototype导致的constructor丢失(替换后constructor指向AnimalES5,需手动补回)typeof AnimalES6 === 'function'证明 class的本质仍是函数,只是有更多限制和语法糖DogES6.prototype.bark的enumerable: falseclass内定义的方法通过Object.defineProperty以enumerable: false挂载,不会出现在for...inDogES5.prototype.bark的enumerable: true普通赋值方式默认 enumerable: true,这是class与 ES5 写法的关键差异之一
📋 代码逐行解析(class 与 ES5 原型等价关系)
js
// ES6 class DogES6 extends AnimalES6 的底层等价操作:
// 1. super(name) → AnimalES6.call(this, name)
// 在子类构造函数里必须先调用 super,才能使用 this
// 本质是借用父构造函数初始化父类定义的实例属性
// 2. class 方法定义 → 等价于 Object.defineProperty(proto, 'bark', { enumerable: false, ... })
// class 内部定义的方法,enumerable 默认为 false,不会出现在 for...in 中
// 而 DogES5.prototype.bark = function(){} 的赋值方式,enumerable 默认为 true
// 3. extends 建立原型链 →
// Object.setPrototypeOf(DogES6.prototype, AnimalES6.prototype)
// Object.setPrototypeOf(DogES6, AnimalES6) // 静态方法继承
// 注意:ES5 组合继承只继承实例链,不继承静态属性链
// 4. class 内部严格模式:
// class 体内所有代码自动运行在 'use strict' 下
// 所以 class 方法内的 this 在未绑定时为 undefined(而非 window)
// 5. typeof AnimalES6 === 'function'
// class 的本质还是函数,只是有更多限制(不能不用 new 调用)
🌐 经典使用场景
场景一:阅读框架源码
React Component 类、Vue 构造函数等框架源码大量使用 ES5 原型写法(兼容旧浏览器)
理解 class 与原型的等价关系,能无障碍阅读这些源码
场景二:Babel 编译输出
将 ES6 class 代码通过 Babel 编译为 ES5,输出的正是 Object.create + call 的组合
理解等价关系,能解释为什么 Babel 编译的代码要这么写
场景三:TypeScript 类的编译
TypeScript class → 编译 → JavaScript(ES5 或 ES6)
理解编译结果,能更好地理解 TypeScript 的运行时行为和继承机制
场景四:面试手写题
"请用 ES5 实现 class 语法"是常见面试题,
答案正是 Object.create + call 的组合模式
💼 业务价值
- 跨版本兼容:掌握 ES5 写法是在必须支持旧浏览器环境时的核心能力(如企业内部系统)
- 框架贡献:理解底层实现才能为 React/Vue 等框架提交高质量 PR,而不只是表面用户
- 调试能力:生产环境报错的 stacktrace 常指向编译后的 ES5 代码,理解原型写法能快速定位问题
class 与 ES5 原型写法的关键差异
| 特性 | ES5 原型写法 | ES6 class |
|---|---|---|
| 语法 | 函数 + 手动设置 prototype | 关键字 class,声明式 |
| 调用限制 | 普通函数可不用 new | 必须用 new,否则报 TypeError |
| 方法可枚举性 | 默认 enumerable: true |
默认 enumerable: false |
| 严格模式 | 需手动开启 | 类体内自动严格模式 |
| 变量提升 | 函数声明提升 | 不提升(TDZ 暂时死区) |
| 继承 | 手动 Object.create + call |
extends + super |
| 静态方法 | 直接赋给构造函数 | static 关键字 |
| getter/setter | Object.defineProperty |
直接用 get/set 关键字 |
8.9 V8 引擎的对象内部表示与性能优化
为什么要了解 V8 内部? 原型链的属性查找发生在引擎层,了解 V8 的优化策略,才能写出真正高性能的 JavaScript。
Hidden Class(隐藏类 / Shape)
V8 并不直接用哈希表存储对象属性(那样太慢)。它会为每个具有相同属性布局 的对象分配同一个 Hidden Class(也叫 Shape / Map),用固定偏移量存储属性值,访问速度接近 C++ 结构体。
❌ 不同 Hidden Class(性能差)
使用
使用
var q1 = {}; q1.x=1; q1.y=2;
Hidden Class D2
x @ offset 0
y @ offset 1
var q2 = {}; q2.y=1; q2.x=2;
Hidden Class D1
y @ offset 0
x @ offset 1
✅ 相同 Hidden Class(性能好)
共享
共享
var p1 = {}; p1.x=1; p1.y=2;
Hidden Class C2
x @ offset 0
y @ offset 1
var p2 = {}; p2.x=3; p2.y=4;
关键规律:属性添加顺序决定 Hidden Class
html
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>Hidden Class 演示</title></head>
<body>
<script>
// ✅ 好的写法:以相同顺序初始化属性,共享 Hidden Class
function Point(x, y) {
this.x = x; // 始终先添加 x
this.y = y; // 再添加 y
}
var p1 = new Point(1, 2);
var p2 = new Point(3, 4);
// p1 和 p2 共享同一 Hidden Class → V8 可以用固定偏移量高速访问属性
// ❌ 坏的写法:属性添加顺序不一致,导致不同 Hidden Class
var q1 = {};
var q2 = {};
q1.x = 1; q1.y = 2; // Hidden Class: x→0, y→1
q2.y = 1; q2.x = 2; // Hidden Class: y→0, x→1 (不同!)
// V8 无法对 q1/q2 做同等优化
// ❌ 动态添加/删除属性会"降级"对象
var obj = { a: 1, b: 2 };
delete obj.a; // 删除属性 → 对象变为 slow mode(哈希表存储)
obj.c = 3; // 再添加属性,无法回到 fast mode
// 生产代码中应避免 delete,用 obj.a = undefined 代替(值为 undefined 但属性仍在 Hidden Class 中)
console.log('Hidden Class 演示完成');
</script>
</body>
</html>
💡 代码解析
代码片段 含义 function Point(x, y) { this.x = x; this.y = y; }始终以相同顺序添加属性,所有 Point实例共享同一 Hidden Class,V8 可用固定偏移量快速访问q1.x = 1; q1.y = 2与q2.y = 1; q2.x = 2属性添加顺序不同 → 产生两个不同 Hidden Class → V8 无法对它们做同等优化 delete obj.a删除属性会破坏 Hidden Class 的结构,对象从 "fast mode"(数组偏移)降级为 "slow mode"(哈希表),访问速度下降 obj.a = undefined替代delete保留属性槽位但将值设为 undefined,维持 Hidden Class 结构,性能更好
Inline Cache(内联缓存,IC)
V8 会缓存每次属性访问的结果(对象类型 + 属性偏移量),下次访问同类型对象时直接用缓存,无需重新查找。
第 1 次:p.x
→ V8 查找 p 的 Hidden Class,找到 x 在 offset 0
→ 缓存:「该 Hidden Class 的 x 在 offset 0」
第 2 次:p.x(同 Hidden Class)
→ 命中缓存,直接读 offset 0
→ 速度接近静态语言
多态(Polymorphic IC):同一行代码多次访问不同类型
→ IC 最多缓存 4 种类型(megamorphic 后退化为全量查找)
Fast properties vs Slow properties
| 属性类型 | 存储方式 | 访问速度 | 触发条件 |
|---|---|---|---|
| Fast(命名属性) | 固定偏移量数组 | 极快(O(1)常数时间) | 对象创建时按序添加的属性 |
| Fast(数组元素) | 连续内存 elements |
极快 | 小整数索引 obj[0]、obj[1] |
| Slow(字典模式) | 哈希表 | 较慢 | delete 属性后;属性数量过多;添加不规则属性 |
对原型链的影响:原型越短,IC 越容易命中,性能越好。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>原型链性能对比</title></head>
<body>
<script>
// ✅ 性能友好:浅原型链(常用方法放近处)
function FastUser(name) { this.name = name; }
FastUser.prototype.getName = function() { return this.name; }; // 1 跳
var fu = new FastUser('Alice');
fu.getName(); // 1 跳找到方法,IC 容易缓存
// ⚠️ 深原型链(6+ 层)访问深层属性时 IC 更难命中
// A → B → C → D → E → F → Object.prototype
// 每访问一个深层属性,引擎需沿链依次检查 6 个对象
// 最佳实践:
// 1. 保持原型链深度在 3 层以内(实例 → 构造原型 → Object.prototype)
// 2. 使用构造函数初始化所有属性(不要在外部随意添加)
// 3. 避免 delete,用赋值 null/undefined 代替
// 4. 避免在热点代码中动态修改原型(会使所有依赖该原型的 IC 失效)
Animal.prototype.newMethod = function() {}; // ⚠️ 在循环外执行才安全
console.log('V8 性能演示完成');
</script>
</body>
</html>
💡 代码解析
代码片段 含义 FastUser.prototype.getName在实例访问时 "1跳"实例 → FastUser.prototype→ 找到getName,只需 1 次原型跳转,IC 容易缓存此路径A → B → C → D → E → F → Object.prototype深度为 6 层的原型链,每次访问深层方法需爬升 6 步,IC 更难稳定命中,性能下降 在循环外修改原型 循环内修改 prototype会使所有依赖该原型的 Inline Cache 失效(deoptimization),导致 V8 回退到慢路径
性能优化实践总结
| 实践 | 原理 | 收益 |
|---|---|---|
| 构造函数中初始化所有属性 | 统一 Hidden Class,利于 IC | 属性访问提速 2~10 倍 |
| 以相同顺序添加属性 | 共享 Hidden Class | 减少内存分配 |
避免 delete |
防止对象降级为 slow mode | 避免哈希表访问开销 |
| 不在循环内修改原型 | 避免 IC 失效(deoptimization) | 防止热点代码性能崩溃 |
| 保持原型链浅层(≤3层) | 减少原型链爬升次数 | 属性查找线性加速 |
8.10 Well-known Symbols 与原型链的元编程扩展
ES6 引入了 Well-known Symbols (知名 Symbol),允许开发者通过在对象原型上挂载这些特殊 Symbol 属性,定制 JavaScript 语言内置行为。这是原型系统和元编程能力的深度融合。
Symbol.iterator
Symbol.hasInstance
Symbol.toPrimitive
Symbol.toStringTag
Symbol.species
Well-known Symbols
(挂在原型或对象上)
使对象支持 for...of / 展开运算符
自定义 instanceof 的判断逻辑
自定义类型转换(+、==、模板字符串)
自定义 Object.prototype.toString 的输出
自定义派生类返回的实例类型
html
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>Well-known Symbols 完整演示</title></head>
<body>
<script>
// ===== 1. Symbol.iterator --- 让自定义对象可迭代 =====
function Range(start, end) {
this.start = start;
this.end = end;
}
// 在原型上挂载 Symbol.iterator,使所有 Range 实例支持 for...of
Range.prototype[Symbol.iterator] = function() {
var current = this.start;
var end = this.end;
return {
next: function() {
return current <= end
? { value: current++, done: false }
: { value: undefined, done: true };
}
};
};
var r = new Range(1, 5);
for (var n of r) process.stdout ? process.stdout.write(n + ' ') : null;
console.log([...r]); // [1, 2, 3, 4, 5](展开运算符也能用)
console.log(Array.from(r)); // [1, 2, 3, 4, 5](Array.from 也能用)
// ===== 2. Symbol.hasInstance --- 自定义 instanceof 行为 =====
function EvenNumber() {}
Object.defineProperty(EvenNumber, Symbol.hasInstance, {
value: function(instance) {
return typeof instance === 'number' && instance % 2 === 0;
}
});
console.log(2 instanceof EvenNumber); // true (偶数)
console.log(3 instanceof EvenNumber); // false (奇数)
console.log(10 instanceof EvenNumber); // true (偶数)
// instanceof 不再检查原型链,而是调用我们定义的函数!
// ===== 3. Symbol.toPrimitive --- 自定义类型转换 =====
function Money(amount, currency) {
this.amount = amount;
this.currency = currency;
}
Money.prototype[Symbol.toPrimitive] = function(hint) {
// hint 可以是 'number'、'string' 或 'default'
if (hint === 'number') return this.amount;
if (hint === 'string') return this.amount + ' ' + this.currency;
return this.amount; // 'default'
};
var price = new Money(99.9, 'CNY');
console.log(+price); // 99.9 (hint: 'number')
console.log(`${price}`); // '99.9 CNY'(hint: 'string')
console.log(price + 0.1); // 100 (hint: 'default')
console.log(price > 50); // true (hint: 'number')
// ===== 4. Symbol.toStringTag --- 自定义类型标签 =====
function Queue() {
this._data = [];
}
Queue.prototype[Symbol.toStringTag] = 'Queue';
Queue.prototype.enqueue = function(val) { this._data.push(val); };
Queue.prototype.dequeue = function() { return this._data.shift(); };
var q = new Queue();
console.log(Object.prototype.toString.call(q)); // '[object Queue]'
// 不再是默认的 '[object Object]',调试信息更清晰
// ===== 5. Symbol.species --- 控制派生类的实例类型 =====
class PowerArray extends Array {
isEmpty() { return this.length === 0; }
// 如果不设置 species,map/filter 返回的仍是 PowerArray 实例
static get [Symbol.species]() { return Array; }
// 设置后,map/filter 返回普通 Array,不带 isEmpty 方法(避免意外继承)
}
var pa = new PowerArray(1, 2, 3);
console.log(pa.isEmpty()); // false
var mapped = pa.map(x => x * 2);
console.log(mapped instanceof PowerArray); // false(因为 species = Array)
console.log(mapped instanceof Array); // true
</script>
</body>
</html>
💡 代码解析
代码片段 含义 Range.prototype[Symbol.iterator] = function() { ... }在原型上挂载迭代器方法,所有 Range实例自动支持for...of、展开运算符和Array.fromreturn { next: function() { ... } }返回符合迭代器协议的对象: next()方法每次返回{ value, done }格式Object.defineProperty(EvenNumber, Symbol.hasInstance, { value: fn })在构造函数上定义 Symbol.hasInstance,完全接管instanceof的判断逻辑2 instanceof EvenNumber返回trueinstanceof不再检查原型链,直接调用我们定义的函数(instance % 2 === 0)Money.prototype[Symbol.toPrimitive] = function(hint)hint告知引擎当前期望的转换类型:'number'(如+price)、'string'(如模板字符串)、'default'(如+运算符)Queue.prototype[Symbol.toStringTag] = 'Queue'自定义 Object.prototype.toString.call(q)的输出,让调试信息更直观(默认是[object Object])static get [Symbol.species]() { return Array; }控制 map/filter等派生方法返回的实例类型,设为Array后返回的是普通数组而非PowerArray
Well-known Symbols 实用速查表
| Symbol | 作用 | 典型应用 |
|---|---|---|
Symbol.iterator |
定义迭代器协议 | 自定义可迭代数据结构 |
Symbol.asyncIterator |
定义异步迭代器 | 异步数据流(Node.js Readable Stream) |
Symbol.hasInstance |
自定义 instanceof |
类型判断框架 |
Symbol.toPrimitive |
自定义类型转换 | 金融数字类、日期类的运算 |
Symbol.toStringTag |
自定义 toString 标签 | 调试工具、日志系统 |
Symbol.species |
指定派生类构造函数 | 继承内置类时控制返回类型 |
Symbol.isConcatSpreadable |
控制 Array.concat 展开行为 |
自定义集合类型 |
8.11 Proxy 与 Reflect --- 拦截和反射原型操作
Proxy 是 ES6 引入的元编程利器,允许在对象的底层操作(包括原型链查找、属性访问、赋值)上设置陷阱(Trap) ,完全自定义对象的行为。Vue 3 的响应式系统正是基于 Proxy 实现的。
Proxy 陷阱与对应的内部方法
常用陷阱
get --- 读取属性
set --- 写入属性
has --- in 运算符
deleteProperty --- delete
getPrototypeOf --- Object.getPrototypeOf
setPrototypeOf --- Object.setPrototypeOf
ownKeys --- Object.keys 等
apply --- 函数调用
construct --- new 运算符
对象操作
Proxy 陷阱 (Trap)
Reflect API
(执行默认行为)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>Proxy 与 Reflect 完整演示</title></head>
<body>
<script>
// ===== 1. 基础 get/set 陷阱 --- 属性访问日志 =====
function createLoggingProxy(target, label) {
return new Proxy(target, {
get(target, prop, receiver) {
console.log('[GET]', label, '.', String(prop));
return Reflect.get(target, prop, receiver); // 执行默认行为
},
set(target, prop, value, receiver) {
console.log('[SET]', label, '.', String(prop), '=', value);
return Reflect.set(target, prop, value, receiver);
}
});
}
var user = createLoggingProxy({ name: 'Alice', age: 28 }, 'user');
user.name; // [GET] user . name
user.age = 29; // [SET] user . age = 29
// ===== 2. 数据验证 --- 拦截 set =====
function createValidator(target, validators) {
return new Proxy(target, {
set(target, prop, value) {
if (validators[prop]) {
var result = validators[prop](value);
if (!result.valid) throw new TypeError(prop + ': ' + result.message);
}
return Reflect.set(target, prop, value);
}
});
}
var person = createValidator({}, {
age: v => ({
valid: typeof v === 'number' && v >= 0 && v <= 150,
message: '年龄必须是 0~150 之间的数字'
}),
name: v => ({
valid: typeof v === 'string' && v.trim().length > 0,
message: '姓名不能为空'
})
});
person.name = 'Bob';
person.age = 30;
console.log(person.name, person.age); // Bob 30
try { person.age = -1; }
catch(e) { console.log('验证失败:', e.message); } // 年龄必须是 0~150 之间的数字
// ===== 3. 只读保护 --- 冻结但可读 =====
function createReadonly(target) {
return new Proxy(target, {
set(target, prop) {
throw new Error('只读对象不可修改属性: ' + String(prop));
},
deleteProperty(target, prop) {
throw new Error('只读对象不可删除属性: ' + String(prop));
}
});
}
var config = createReadonly({ apiUrl: 'https://api.example.com', timeout: 5000 });
console.log(config.apiUrl); // 'https://api.example.com' ✅ 读取正常
try { config.apiUrl = 'xxx'; }
catch(e) { console.log(e.message); } // 只读对象不可修改属性: apiUrl
// ===== 4. Vue 3 响应式核心原理(简化版)=====
function reactive(raw) {
var effectMap = {}; // 存储 prop → 依赖列表
return new Proxy(raw, {
get(target, prop, receiver) {
// 依赖收集:记录哪个"计算属性/渲染函数"访问了该属性
if (currentEffect) {
(effectMap[prop] = effectMap[prop] || []).push(currentEffect);
}
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
var result = Reflect.set(target, prop, value, receiver);
// 派发更新:通知所有依赖该属性的 effect 重新执行
if (effectMap[prop]) {
effectMap[prop].forEach(fn => fn());
}
return result;
}
});
}
var currentEffect = null;
function watchEffect(fn) {
currentEffect = fn;
fn(); // 首次执行,触发 get,完成依赖收集
currentEffect = null;
}
var state = reactive({ count: 0, message: 'Hello' });
watchEffect(function renderCount() {
console.log('count =', state.count); // 初始执行:count = 0
});
state.count = 1; // → 触发 set → 通知 renderCount 重新执行 → 输出: count = 1
state.count = 2; // → 输出: count = 2
state.message = 'World'; // → 没有 effect 依赖 message,无输出
</script>
</body>
</html>
💡 代码解析
代码片段 含义 new Proxy(target, handler)创建代理对象,所有对 target的操作都会经过handler中对应的陷阱函数拦截get(target, prop, receiver)中的Reflect.get(target, prop, receiver)执行默认的读取操作并保持正确的 receiver(对 getter 属性至关重要),避免直接return target[prop]导致的this绑定问题createLoggingProxy日志代理:每次读写属性都自动打印日志,常用于调试和性能监控 validators[prop](value)返回{ valid, message }数据验证代理:拦截 set操作,在写入前校验数据合法性,不合法则抛出TypeErrorcreateReadonly中的set和deleteProperty陷阱只读代理:拦截所有写入和删除,抛出错误;不像 Object.freeze,读取仍走正常路径effectMap[prop]存储依赖Vue 3 响应式核心: get陷阱中收集当前正在执行的effect到属性的依赖列表effectMap[prop].forEach(fn => fn())set陷阱中触发依赖更新:通知所有依赖该属性的effect重新执行(视图重渲染)watchEffect首次执行fn()首次执行时触发 get陷阱,完成依赖收集(自动知道哪个 effect 依赖了哪些属性)state.message = 'World'无输出没有任何 effect在运行时访问过message,所以effectMap['message']为空,无需通知
Reflect API 的设计价值
Reflect 将所有对象内部操作暴露为函数,与 Proxy 陷阱一一对应:
| Proxy 陷阱 | Reflect 对应方法 | 传统等价写法 |
|---|---|---|
get |
Reflect.get(t, p, r) |
t[p] |
set |
Reflect.set(t, p, v, r) |
t[p] = v |
has |
Reflect.has(t, p) |
p in t |
deleteProperty |
Reflect.deleteProperty(t, p) |
delete t[p] |
getPrototypeOf |
Reflect.getPrototypeOf(t) |
Object.getPrototypeOf(t) |
setPrototypeOf |
Reflect.setPrototypeOf(t, p) |
Object.setPrototypeOf(t, p) |
ownKeys |
Reflect.ownKeys(t) |
Object.getOwnPropertyNames(t) |
construct |
Reflect.construct(T, args) |
new T(...args) |
apply |
Reflect.apply(fn, ctx, args) |
fn.apply(ctx, args) |
为什么在 Proxy 陷阱中用
Reflect.xxx而不直接操作target?因为
Reflect会正确处理receiver(原型链上的访问器属性 getter/setter 需要正确的 this),直接操作target可能导致 this 绑定错误。
8.12 原型链的设计模式
模式一:Mixin 混入(解决单继承的局限性)
JavaScript 的原型链是单链结构(一个对象只能有一个直接原型),无法原生多继承。Mixin 模式通过将多个对象的属性复制到目标原型上,实现"行为组合"。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>Mixin 模式演示</title></head>
<body>
<script>
// ===== Mixin 工厂 =====
function mixin(target) {
var sources = Array.prototype.slice.call(arguments, 1);
sources.forEach(function(source) {
Object.keys(source).forEach(function(key) {
if (!target[key]) { // 不覆盖已有方法
target[key] = source[key];
}
});
});
return target;
}
// 能力模块(纯行为,不关心 this 的具体类型)
var Serializable = {
serialize: function() { return JSON.stringify(this); },
deserialize: function(json) { return Object.assign(this, JSON.parse(json)); }
};
var Validatable = {
validate: function() {
return Object.keys(this.rules || {}).every(function(key) {
return this.rules[key](this[key]);
}.bind(this));
}
};
var EventEmittable = {
_events: null,
on: function(event, fn) {
if (!this._events) this._events = {};
(this._events[event] = this._events[event] || []).push(fn);
return this;
},
emit: function(event) {
if (this._events && this._events[event]) {
var args = Array.prototype.slice.call(arguments, 1);
this._events[event].forEach(function(fn) { fn.apply(this, args); }.bind(this));
}
return this;
}
};
// 主类:User 只关注核心业务逻辑
function User(name, email) {
this.name = name;
this.email = email;
this._events = {};
this.rules = {
name: function(v) { return typeof v === 'string' && v.length > 0; },
email: function(v) { return /\S+@\S+\.\S+/.test(v); }
};
}
// 通过 Mixin 混入多种能力
mixin(User.prototype, Serializable, Validatable, EventEmittable);
var u = new User('Alice', 'alice@example.com');
console.log(u.validate()); // true(验证通过)
console.log(u.serialize()); // JSON 字符串
u.on('login', function(ip) { console.log(u.name + ' 从 ' + ip + ' 登录'); });
u.emit('login', '192.168.1.1'); // Alice 从 192.168.1.1 登录
var badUser = new User('', 'not-an-email');
console.log(badUser.validate()); // false(验证失败)
</script>
</body>
</html>
💡 代码解析
代码片段 含义 mixin(target, ...sources)Mixin 工厂函数:将多个 source 对象的属性复制到 target原型上,实现"行为组合"if (!target[key])不覆盖已有方法:保证主类已有的同名方法优先级更高(防止 Mixin 意外覆盖业务方法) Serializable.serialize中的JSON.stringify(this)利用 this的动态绑定:调用时this是User实例,把实例本身序列化为 JSON 字符串Validatable.validate中的.bind(this)因 forEach回调中的this会丢失,显式绑定当前this(即User实例),以访问this.rules和this[key]mixin(User.prototype, Serializable, Validatable, EventEmittable)将三种能力模块一次性混入, User.prototype现在拥有序列化、验证、事件三种能力u.on('login', fn)/u.emit('login', ...)通过混入的 EventEmittable能力,User实例拥有了事件发布/订阅功能,无需继承任何父类
模式二:组合优于继承(Composition over Inheritance)
这是面向对象设计的核心原则之一(GoF 设计模式书中的第一原则)。
继承的问题:
① 紧耦合:子类依赖父类的内部实现(脆基类问题)
② 层级膨胀:随着业务发展,继承树越来越深,越来越难维护
③ 无法灵活组合:一个对象只能从一条继承链获取能力
组合的优势:
① 松耦合:功能模块互相独立,按需组装
② 扁平结构:不需要深层继承树
③ 灵活复用:任意组合多个能力模块
html
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>组合 vs 继承</title></head>
<body>
<script>
// ===== 继承写法(有问题)=====
// 假设需求:会飞的鸭子、不会飞的橡皮鸭、会飞不会叫的遥控鸭
// 用继承很难同时满足,容易出现"会飞的橡皮鸭"这种荒谬情况
// ===== 组合写法(推荐)=====
// 将行为拆成独立的"能力函数"
var canFly = function(state) ({
fly: function() { console.log(state.name + ' 在飞!'); }
});
var canQuack = function(state) ({
quack: function() { console.log(state.name + ':嘎嘎嘎!'); }
});
var canSwim = function(state) ({
swim: function() { console.log(state.name + ' 在游泳!'); }
});
// 按需组合,灵活创建不同"品种"的鸭子
function createMallard(name) { // 绿头鸭:能飞、能叫、能游
var state = { name: name };
return Object.assign(state, canFly(state), canQuack(state), canSwim(state));
}
function createRubberDuck(name) { // 橡皮鸭:只能叫、能游,不会飞
var state = { name: name };
return Object.assign(state, canQuack(state), canSwim(state));
}
function createDecoyDuck(name) { // 诱饵鸭:只能游,不会飞不会叫
var state = { name: name };
return Object.assign(state, canSwim(state));
}
</script>
</body>
</html>
💡 代码解析
代码片段 含义 canFly(state)返回{ fly: function(){} }每个能力是一个函数,接收共享的 state对象,返回包含该能力方法的对象Object.assign(state, canFly(state), canQuack(state), canSwim(state))组合模式:将多个能力对象合并到 state上,按需选取,绿头鸭有飞/叫/游三种能力createRubberDuck只混入canQuack + canSwim橡皮鸭不会飞,只需混入相应能力,不会出现继承体系中"橡皮鸭意外会飞"的荒谬情况 无 class、无new、无继承树组合模式完全摆脱类层级,每个能力模块独立,可以任意组合,是函数式面向对象的典型写法
模式三:OLOO(Objects Linking to Other Objects)
由 Kyle Simpson(《You Don't Know JS》作者)提出,认为直接使用 Object.create 建立对象关联,比构造函数 + new 更能体现 JavaScript 的"原型委托"本质。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>OLOO 模式演示</title></head>
<body>
<script>
// ===== 传统构造函数写法 =====
function Animal(name) { this.name = name; }
Animal.prototype.speak = function() { return this.name + ' 发出声音'; };
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() { return this.name + ' 汪汪!'; };
// ===== OLOO 写法(直接对象关联,更贴近原型本质)=====
var AnimalProto = {
init: function(name) { this.name = name; return this; },
speak: function() { return this.name + ' 发出声音'; }
};
var DogProto = Object.create(AnimalProto); // 建立委托关系
DogProto.setup = function(name, breed) {
this.init(name); // 委托给 AnimalProto.init
this.breed = breed;
return this;
};
DogProto.bark = function() { return this.name + ' 汪汪!'; };
// 创建实例:不需要 new,直接 create + 初始化
var dog1 = Object.create(DogProto).setup('旺财', '柴犬');
var dog2 = Object.create(DogProto).setup('小黑', '拉布拉多');
console.log(dog1.speak()); // '旺财 发出声音'(委托给 AnimalProto)
console.log(dog1.bark()); // '旺财 汪汪!'
console.log(dog2.bark()); // '小黑 汪汪!'
// OLOO 原型链:dog1 → DogProto → AnimalProto → Object.prototype
console.log(Object.getPrototypeOf(dog1) === DogProto); // true
console.log(Object.getPrototypeOf(DogProto) === AnimalProto); // true
// 对比:OLOO 写法没有 .prototype、没有 new、没有 constructor
// 代码语义更清晰:「dog1 委托给 DogProto,DogProto 委托给 AnimalProto」
</script>
</body>
</html>
💡 代码解析
代码片段 含义 var DogProto = Object.create(AnimalProto)OLOO 核心:直接用 Object.create建立委托关系,DogProto的原型是AnimalProtoDogProto.setup = function(name, breed) { this.init(name); ... }setup是初始化方法(代替构造函数),调用时委托给AnimalProto.init初始化父级属性Object.create(DogProto).setup('旺财', '柴犬')链式调用:创建以 DogProto为原型的空对象,然后立即初始化,返回this(即刚创建的对象)dog1.speak()委托给AnimalProtodog1自身没有speak,委托给DogProto,DogProto也没有,继续委托给AnimalProto,找到并执行Object.getPrototypeOf(dog1) === DogProto原型链清晰可验: dog1 → DogProto → AnimalProto → Object.prototype → null无 new、无prototype、无constructorOLOO 只使用 Object.create和普通对象,代码意图是"对象之间的委托关系",而非模拟类继承
三种设计模式对比
| 模式 | 核心思想 | 适用场景 | 优缺点 |
|---|---|---|---|
| 传统继承(原型链) | 建立 is-a 关系 | 明确的分类层级(动物→哺乳动物→狗) | 直观但层级深时难维护 |
| Mixin 混入 | 将行为复制到原型 | 多行为组合、跨类复用 | 灵活但可能命名冲突 |
| 组合(Composition) | 按需组装功能函数 | 行为需要灵活组合的场景 | 最灵活,函数式风格 |
| OLOO | 纯原型委托,无 new | 追求语义纯粹、原型本质 | 代码意图清晰,但社区认知度低 |
8.13 ECMAScript 规范中的对象内部方法(Internal Methods)
每个 JavaScript 对象在规范层面都有一组内部方法(Internal Methods) ,用双方括号 [[MethodName]] 标记。这些是引擎实现的底层接口,Proxy 的陷阱正是对这些内部方法的拦截。
普通对象(Ordinary Object)的内部方法
| 内部方法 | 对应操作 | Proxy 陷阱 |
|---|---|---|
[[GetPrototypeOf]] |
Object.getPrototypeOf(obj) |
getPrototypeOf |
[[SetPrototypeOf]] |
Object.setPrototypeOf(obj, p) |
setPrototypeOf |
[[IsExtensible]] |
Object.isExtensible(obj) |
isExtensible |
[[PreventExtensions]] |
Object.preventExtensions(obj) |
preventExtensions |
[[GetOwnProperty]] |
Object.getOwnPropertyDescriptor(obj, p) |
getOwnPropertyDescriptor |
[[DefineOwnProperty]] |
Object.defineProperty(obj, p, desc) |
defineProperty |
[[HasProperty]] |
p in obj |
has |
[[Get]] |
obj[p] |
get |
[[Set]] |
obj[p] = v |
set |
[[Delete]] |
delete obj[p] |
deleteProperty |
[[OwnPropertyKeys]] |
Object.keys(obj) 等 |
ownKeys |
函数对象额外拥有的内部方法
| 内部方法 | 触发时机 | 说明 |
|---|---|---|
[[Call]] |
fn(args) 普通调用 |
所有函数对象都有 |
[[Construct]] |
new fn(args) |
只有构造函数(非箭头函数)才有 |
关键洞见 :箭头函数没有
[[Construct]]内部方法,所以new (() => {})会抛出 TypeError。这是语言规范层面的决定,不仅仅是"语法限制"。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>内部方法可观测性演示</title></head>
<body>
<script>
// 通过 Proxy 让内部方法"可见"
var handler = {
getPrototypeOf(target) { console.log('[[GetPrototypeOf]] 被调用'); return Reflect.getPrototypeOf(target); },
has(target, prop) { console.log('[[HasProperty]]', prop, '被检查'); return Reflect.has(target, prop); },
get(target, prop, receiver) { console.log('[[Get]]', prop, '被读取'); return Reflect.get(target, prop, receiver); },
set(target, prop, val, recv) { console.log('[[Set]]', prop, '=', val); return Reflect.set(target, prop, val, recv); },
deleteProperty(target, prop) { console.log('[[Delete]]', prop); return Reflect.deleteProperty(target, prop); }
};
var observed = new Proxy({ x: 1 }, handler);
Object.getPrototypeOf(observed); // [[GetPrototypeOf]] 被调用
'x' in observed; // [[HasProperty]] x 被检查
var _ = observed.x; // [[Get]] x 被读取
observed.y = 2; // [[Set]] y = 2
delete observed.y; // [[Delete]] y
// 验证箭头函数没有 [[Construct]]
var ArrowFn = () => {};
try {
new ArrowFn();
} catch(e) {
console.log('箭头函数无 [[Construct]]:', e.message);
// TypeError: ArrowFn is not a constructor
}
// 普通函数有 [[Call]] 和 [[Construct]]
function NormalFn() {}
NormalFn(); // 触发 [[Call]]
new NormalFn(); // 触发 [[Construct]]
</script>
</body>
</html>
💡 代码解析
代码片段 含义 getPrototypeOf(target)陷阱拦截 Object.getPrototypeOf(observed)调用,触发[[GetPrototypeOf]]内部方法has(target, prop)陷阱拦截 'x' in observed,触发[[HasProperty]]内部方法get(target, prop, receiver)陷阱拦截 observed.x的读取,触发[[Get]]内部方法set(target, prop, val, recv)陷阱拦截 observed.y = 2的赋值,触发[[Set]]内部方法deleteProperty(target, prop)陷阱拦截 delete observed.y,触发[[Delete]]内部方法new ArrowFn()抛出TypeError箭头函数没有 [[Construct]]内部方法(规范层面限制),new操作无法对箭头函数生效NormalFn()触发[[Call]],new NormalFn()触发[[Construct]]普通函数同时拥有两个内部方法,既可普通调用也可 new调用
9.1 this 的核心特点
一句话 :
this是运行时动态绑定 的,由调用方式决定,而非定义位置。
特点一:动态性(Dynamic Binding)
this 的值在调用时才确定,同一个函数在不同调用上下文中 this 不同。
var fn = function() { console.log(this.x); };
var a = { x: 1, f: fn };
var b = { x: 2, f: fn };
a.f(); // 1
b.f(); // 2 ← 同一个函数,this 完全不同
特点二:不可修改性(Read-only Variable)
this 是系统自动创建的只读变量,无法通过赋值改变其值。
this = something; // SyntaxError
特点三:传递性(Propagation in Methods)
方法调用中,this 沿调用链传递:obj.method() 中 this = obj。
但赋值给变量后调用(var f = obj.method; f();),传递中断,this 回退到 window。
特点四:箭头函数例外(Lexical this)
箭头函数没有自己的 this,继承定义时外层函数的 this(词法作用域)。
这使得箭头函数非常适合作为回调,避免 this 丢失。
特点五:严格模式差异
严格模式('use strict')下,默认绑定的 this = undefined(而非 window)。
这是一个重要的安全设计:防止意外污染全局。
this 易错点清单
| 易错场景 | 错误预期 | 实际结果 | 原因 |
|---|---|---|---|
var fn = obj.method; fn() |
this = obj |
this = window |
赋值断开了方法与对象的绑定 |
回调函数中的 this |
this = 外层对象 |
this = window |
传入回调时同样断开绑定 |
嵌套函数中的 this |
this = 外层 this |
this = window |
每个普通函数都有自己的 this |
setTimeout(obj.fn, 0) |
this = obj |
this = window |
定时器作为普通函数调用 |
new fn() |
--- | this = 新对象 |
new 改变了 this 的绑定 |
9.2 原型的核心特点
一句话 :原型是 JavaScript 实现对象共享与继承的唯一原生机制,其核心是"委托查找"。
特点一:隐式共享(Implicit Sharing)
不需要在每个对象上显式声明,只要挂在原型上,所有实例自动继承。
这是原型系统最强大的地方,也是方法应该放在原型上的根本原因。
特点二:动态性(Dynamic Prototype)
原型对象可以在运行时被修改,且修改立即对所有实例生效。
Animal.prototype.newMethod = function() {};
// 之前创建的所有 Animal 实例立即拥有 newMethod
特点三:单一性(Single Prototype)
每个对象只能有且只有一个直接原型([[Prototype]] 只有一个值)。
JavaScript 不支持多继承(但可以通过 Mixin 模式实现类似效果)。
特点四:对象性(Prototype is Object)
原型本身也是普通对象,具有普通对象的一切特性。
原型上可以有属性、方法、getter/setter,甚至可以再有原型(形成链)。
特点五:非侵入性(Non-invasive)
通过原型给对象增加能力,不会修改对象自身的结构。
对象自身的属性(自有属性)和继承的属性(原型属性)是分开的。
原型的使用场景总结
| 场景 | 推荐操作 | 原因 |
|---|---|---|
| 多实例共用的方法 | 放在 Constructor.prototype |
节省内存,共享一份 |
| 实例独有的状态(数据) | 放在构造函数 this.xxx |
每个实例有独立副本 |
| 扩展内置对象 | 谨慎操作 Array.prototype 等 |
可能引发原型污染,影响全局 |
| 无原型的纯净字典 | Object.create(null) |
不受 Object.prototype 影响 |
9.3 原型链的核心特点
一句话 :原型链是 JavaScript 属性查找的传送带,单向、动态、可终止。
特点一:单向性(Unidirectional)
只能沿 [[Prototype]] 向上(父级方向)查找,不能向下(子级)查找。
父原型无法"知道"有哪些子对象在引用自己。
特点二:终止性(Terminatable)
原型链必然终止于 null(Object.prototype.__proto__ === null)。
到达 null 后,继续访问属性只会得到 undefined,不会报错(除非调用)。
特点三:遮蔽性(Shadowing)
就近原则:自有属性优先于继承属性。
对象自身的属性会"遮蔽"原型链上同名属性,但不影响原型上的值。
特点四:动态查找(Dynamic Lookup)
每次访问属性,引擎都实时沿链查找,不是静态复制到对象上。
因此原型的修改能立即反映到所有实例(双刃剑:强大但需谨慎)。
特点五:共享性(Shared Prototype)
相同构造函数的所有实例共享同一个原型对象。
修改原型(增删改原型属性)会同时影响所有实例。
特点六:性能影响(Performance)
原型链越长,属性查找越慢。
V8 引擎针对常见访问模式做了内联缓存(Inline Cache)优化,实际开销极小。
但极深的原型链(10+ 层)在性能敏感场景中需要注意。
原型链深度与性能
1跳
2跳
3跳
实例 instance
Constructor.prototype
Object.prototype
null
最佳实践:将最常用的方法放在距离实例最近的原型上(1-2跳以内),避免不必要的深层链查找。
9.4 值类型的核心特点
一句话 :值类型是完全独立、不可变的,操作的是值本身的副本。
特点一:不可变性(Immutability)
原始值本身不可改变。string 的方法(toUpperCase 等)都返回新字符串,不修改原值。
这是函数式编程"纯函数"的天然基础。
特点二:独立性(Independence)
赋值或传参时,接收方得到的是完全独立的副本。
修改副本不会影响原值,完全隔离。
特点三:按值比较(Value Equality)
相等比较(=== / ==)比较的是值本身,而不是内存地址。
10 === 10 → true(直接比较值)
特点四:自动装箱(Auto-boxing)
访问字符串、数字等原始值的属性时,JS 引擎会临时创建包装对象(String/Number/Boolean)。
'hello'.length → 临时创建 new String('hello'),读取 length,随即销毁。
这解释了为什么原始值能调用方法,却无法给原始值设置自定义属性。
特点五:null 的特殊性
null 是值类型,但 typeof null === 'object'(JS 历史遗留的著名 Bug)。
null 表示"有意设置为空的引用",undefined 表示"未初始化/不存在"。
自动装箱演示
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>自动装箱(Auto-boxing)演示</title>
</head>
<body>
<script>
var str = 'hello';
// 访问 length 属性时,引擎临时创建 new String('hello')
console.log(str.length); // 5
console.log(str.toUpperCase()); // 'HELLO'
// 但无法在原始值上设置属性(装箱对象是临时的,立即销毁)
str.custom = '自定义';
console.log(str.custom); // undefined(装箱对象已销毁)
// 如果用 new String() 创建包装对象,则可以设置属性(但不推荐)
var strObj = new String('hello');
strObj.custom = '自定义';
console.log(strObj.custom); // '自定义'
console.log(typeof str); // 'string'(原始值)
console.log(typeof strObj); // 'object'(包装对象)
// typeof 检测各原始类型
console.log(typeof 42); // 'number'
console.log(typeof 'hello'); // 'string'
console.log(typeof true); // 'boolean'
console.log(typeof undefined); // 'undefined'
console.log(typeof null); // 'object' ← JS历史遗留Bug!
console.log(typeof Symbol()); // 'symbol'
console.log(typeof 42n); // 'bigint'
</script>
</body>
</html>
💡 代码解析
代码片段 含义 str.length/str.toUpperCase()访问字符串原始值的属性时,JS 引擎自动创建临时 String包装对象,调用完毕立即销毁str.custom = '自定义'后读取为undefined赋值的是临时包装对象的属性,包装对象立即销毁,下次访问时重新创建新的包装对象,之前设置的属性消失 new String('hello')创建的strObj显式包装对象是持久对象,可以设置自定义属性,但类型变为 'object',使用时需注意typeof检测差异typeof null === 'object'JS 历史遗留 Bug: null的二进制表示前三位是000(与对象相同),导致typeof null错误返回'object'typeof Symbol()返回'symbol'ES6 新增的基本类型,有专用的 typeof标识,不会与其他类型混淆typeof 42n返回'bigint'ES2020 新增类型,用于表示任意精度整数, typeof正确识别
9.5 引用类型的核心特点
一句话 :引用类型是可变、共享的,操作的是指向堆内存的引用地址。
特点一:可变性(Mutability)
对象、数组等引用类型可以随时增删改属性/元素,内存地址不变,但内容可变。
特点二:共享性(Shared Reference)
多个变量可以同时引用同一个对象。通过任一变量修改对象内容,其他变量立即可见。
这既是"引用类型"强大的协作能力,也是 Bug 的常见来源。
特点三:按引用传递(Pass by Reference)
函数传参传入的是引用地址的副本(值传递引用),而不是对象本身的副本。
函数内修改对象属性 → 外部可见;函数内重新赋值参数 → 外部不可见。
特点四:身份比较(Identity Equality)
=== 比较的是内存地址是否相同(是否是同一个对象),不比较内容。
[1,2,3] === [1,2,3] → false(两个不同对象)
var a = []; var b = a; a === b → true(同一个对象)
特点五:原型链归属
所有引用类型对象的原型链最终都指向 Object.prototype。
这意味着 toString()、hasOwnProperty() 等方法对所有对象都可用。
特点六:垃圾回收依赖(GC Dependency)
引用类型的内存由 GC 管理。只要有任何变量持有对象的引用,该对象就不会被回收。
当所有引用都解除(赋 null 或超出作用域),对象才成为 GC 候选。
9.6 高频易错点汇总
值/引用类型易错
浅拷贝后修改嵌套对象
影响原对象
函数内重新赋值引用参数
外部不受影响
=== 比较对象
比的是地址不是内容
原型链易错
\] instanceof Object → true (不只是直接构造函数) Function.__proto__ === Function.prototype(自己是自己实例) 原型链终点是 null 不是 Object.prototype 原型易错 替换 prototype 后 constructor 丢失 修改原型属性 影响所有已有实例 原型上的引用类型属性 被所有实例共享(陷阱) this 易错 赋值后调用:var f = obj.fn; f() this 变 window 回调中 this 丢失 setTimeout(obj.fn, 0) new o.getInfo() this 是新对象不是 o **原型上引用类型属性的共享陷阱(必须掌握)** ```html
fn_inst = new fn()
obj = {}
Array.prototype
fn.prototype
Object.prototype
原型链终点
Function.prototype
Array(构造函数)
Object(构造函数)
Function(构造函数)
fn(自定义函数)
null ❌ 终点
值类型与引用类型内存模型
堆内存(Heap)
栈内存(Stack)
指针
指针
a = 10
b = 20
str = 'hello'
refArr → 0xAB01
refObj → 0xCD02
0xAB01
1, 2, 3
0xCD02
{ name: 'Alice' }
参考资料
- MDN --- Inheritance and the prototype chain
- MDN --- Object.getPrototypeOf()
- MDN --- Object.create()
- MDN --- instanceof
- MDN --- hasOwnProperty()
- MDN --- JavaScript data types and data structures
- ECMAScript 2025 Language Specification
- Exploring JS --- Values chapter
作者按 :原型与原型链是 JavaScript 语言区别于其他面向对象语言的核心所在。理解了这套机制,你就能彻底搞清
class、extends、super的底层逻辑,以及各种框架源码中的继承实现。多画图、多运行代码是掌握这部分知识最有效的方式。