DAY_25 JavaScript 原型、原型链与值类型/引用类型 ── 深度全解(下)

场景二:利用原型实现继承(组合继承)

这是 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 委托 vs 复制的理解 ``` > **💡 代码解析** > > | 代码片段 | 含义 | > |--------------------------------------------|----------------------------------------------------------| > | `cat.hasOwnProperty('speak')` | 返回 `false`:`speak` 不在 `cat` 自身,而是在 `Animal.prototype` 上 | > | `cat.speak()` | cat 自身找不到,沿 `[[Prototype]]` 委托给 `Animal.prototype`,找到并执行 | > | `Animal.prototype.speak = function(){...}` | 运行时替换原型上的方法,所有已存在的实例立即受影响 | > | 对比类继承 | 类继承中方法被"复制"进实例,运行时修改类不会影响已有实例(静态) | > | 委托机制本质 | JavaScript 实例永远不复制方法,每次调用都实时向上"委托"查找 | *** ** * ** *** #### 8.2 `new` 运算符的内部执行步骤(规范级) `new Constructor(args)` 并不是魔法,ECMAScript 规范明确定义了它的 4 个步骤: 步骤 1:创建一个全新的空对象 {} 步骤 2:将新对象的 [[Prototype]] 设置为 Constructor.prototype 步骤 3:以新对象作为 this,执行构造函数体(Constructor.call(newObj, args)) 步骤 4: - 如果构造函数 return 了一个【对象类型】的值 → 返回该对象(覆盖新对象) - 否则(return 原始值或无 return)→ 返回步骤 1 创建的新对象 是(return 对象) 否(无 return 或 return 原始值) 步骤1: 创建空对象 obj = {} 步骤2: obj.__proto__ = Constructor.prototype 步骤3: Constructor.call(obj, ...args) 执行构造函数,this = obj 构造函数 return 了对象吗? 返回构造函数 return 的那个对象 返回步骤1创建的 obj **手动实现 `myNew`** ```html 手动实现 new 运算符 ``` > **💡 代码解析** > > | 代码片段 | 含义 | > |-----------------------------------------------------|-------------------------------------------------------------------| > | `var obj = {}` | 步骤1:在堆中创建一个全新的空对象 | > | `Object.setPrototypeOf(obj, Constructor.prototype)` | 步骤2:将新对象的 `[[Prototype]]` 指向构造函数的 `prototype`,建立原型链 | > | `Array.prototype.slice.call(arguments, 1)` | 从 `arguments` 中去掉第一个参数(Constructor),收集剩余参数 | > | `Constructor.apply(obj, args)` | 步骤3:以 `obj` 为 `this` 执行构造函数,把属性挂在 `obj` 上 | > | `typeof result === 'object' && result !== null` | 步骤4:判断构造函数是否 `return` 了对象(注意 `null` 的 `typeof` 也是 `'object'`,需排除) | > | `new Weird()` 返回 `{x:999}` | 构造函数 `return` 了对象 → `new` 的结果被这个对象覆盖,`this.x=1` 被丢弃 | > | `new Normal()` 返回 `{x:1}` | 构造函数 `return 42`(原始值)→ 被忽略,仍返回步骤1创建的 `obj` | > | `p instanceof Person` 为 `true` | 步骤2 正确设置了原型链,`instanceof` 能沿链找到 `Person.prototype` | > **面试高频** :`new` 的四个步骤是 JavaScript 面试的经典题目,能手写 `myNew` 是高级前端工程师的基本能力。 *** ** * ** *** #### 8.3 属性描述符(Property Descriptor) 每个对象属性背后都有一套**描述符**,控制属性的行为。这是原型和属性系统的底层机制。 ##### 两种描述符类型 **数据描述符(Data Descriptor):** | 特性 | 类型 | 默认值(defineProperty) | 说明 | |----------------|---------|---------------------|--------------------------------------| | `value` | any | `undefined` | 属性的值 | | `writable` | boolean | `false` | 是否可修改 value | | `enumerable` | boolean | `false` | 是否出现在 `for...in` / `Object.keys()` 中 | | `configurable` | boolean | `false` | 是否可删除 / 可再次修改描述符 | **访问器描述符(Accessor Descriptor):** | 特性 | 类型 | 默认值 | 说明 | |----------------|----------|-------------|---------| | `get` | function | `undefined` | 读取属性时调用 | | `set` | function | `undefined` | 写入属性时调用 | | `enumerable` | boolean | `false` | 同上 | | `configurable` | boolean | `false` | 同上 | > 两种描述符互斥:同一个属性不能同时有 `value/writable` 和 `get/set`。 ```html 属性描述符完整演示 ``` > **💡 代码解析** > > | 代码片段 | 含义 | > |-----------------------------------------------------------|------------------------------------------------------------------| > | `Object.defineProperty(obj, 'name', { writable: false })` | 数据描述符:将属性设为只读,任何赋值操作静默失败(严格模式下报错) | > | `configurable: false` | 不可配置:禁止删除该属性,也禁止再次修改描述符(不可逆操作,慎用) | > | `Object.defineProperty(person, 'age', { get, set })` | 访问器描述符:读取时触发 `get`,写入时触发 `set`,是 Vue 2 双向绑定的核心机制 | > | `set(val)` 中的范围验证 | 利用 setter 拦截赋值,在写入前做业务校验,防止非法数据进入对象 | > | `Object.getOwnPropertyDescriptor(arr, 'length')` | 查看内置属性的描述符:数组 `length` 可写(可以直接赋值截断)但不可枚举(`for...in` 看不到) | > | `Object.defineProperties(point, {...})` | 批量定义多个属性描述符,避免多次调用 `defineProperty` | > | `toString` 设置 `enumerable: false` | 自定义的 `toString` 不会出现在 `for...in` 或 `Object.keys()` 中,符合内置方法的行为规范 | > | `Object.freeze(config)` | 冻结对象:所有属性变为 `writable: false`、`configurable: false`,同时阻止添加新属性 | ##### 属性描述符与原型的交互 ```html 属性描述符与原型链的交互 ``` > **💡 代码解析** > > | 代码片段 | 含义 | > |----------------------------------------------------------------------|------------------------------------------------------------| > | `Object.defineProperty(Base.prototype, 'type', { writable: false })` | 在原型上定义只读属性,所有实例通过原型链继承该值,且无法通过普通赋值覆盖 | > | `b.type = 'Override'`(静默失败) | 当原型属性为 `writable: false` 时,赋值不会创建实例自有属性,也不修改原型值,非严格模式下静默失败 | > | `b.hasOwnProperty('type')` 返回 `false` | 确认实例上没有创建 `type` 的自有属性,证明了 `writable: false` 的拦截效果 | > | `Object.defineProperty(b, 'type', { ... })` | 唯一能在实例上覆盖 `writable: false` 原型属性的方法:显式在实例上重新定义描述符 | > | 覆盖后 `hasOwnProperty('type')` 返回 `true` | 现在 `b` 自身有了 `type` 属性,实现了对原型属性的遮蔽 | > **关键规律** :当原型上的属性被标记为 `writable: false` 时,无法通过赋值在实例上创建同名自有属性(即使是普通赋值)。这是属性描述符与原型链协同工作的微妙之处。 *** ** * ** *** #### 8.4 `[[Get]]` 操作与原型链查找算法 ECMAScript 规范将访问对象属性 `obj.prop` 的过程定义为 **`[[Get]]`** 内部操作,其完整算法如下: 有且是数据描述符 有且是访问器描述符 (有 get) 有访问器但无 get 没有 是 否 obj.prop 被访问 obj 自身有 prop 的 属性描述符? 返回 descriptor.value 调用 get() 返回结果 返回 undefined p = obj.\[\[Prototype\]

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(无效) 此时实例上已无 shareddelete 作用于原型属性时无效(不报错,静默忽略)

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, '你好', '!') 立即调用 introducethis 绑定为 alice,参数逐个传入
introduce.apply(bob, ['Hi', '.']) 立即调用,this 绑定为 bob,参数以数组形式传入(适用于参数列表动态变化的场景)
introduce.bind(alice, '嗨') 返回新函数,this 永久绑定 alice,第一个参数预设为 '嗨'(柯里化效果)
aliceIntro('~') 后续调用 每次调用只需传最后一个参数,前面的参数已通过 bind 预置
new BoundGreeter() 的结果 new 的优先级高于 bind 的显式绑定,this 是新创建的对象,而非 {x:999}
g.xundefined 证明 new 调用时完全忽略 bind 指定的 this,绑定的 {x:999} 对象不起作用
Function.prototype.myBind 手写实现 通过闭包保存 fnctxargs,返回新函数;调用时合并预置参数与运行时参数,再用 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 会直接抛出 TypeError
structuredClone(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 就无法被 GC
leak = null 解除引用后,bigObj 变为不可达对象,标记清除算法在下一轮 GC 时将其回收
btn.addEventListener('click', handleClick) 事件监听器持有 handleClick 的引用;如果 btn 被移出 DOM 但未移除监听,handleClick 及其闭包中的对象无法被 GC
new 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; } } classconstructor 等价于 ES5 的构造函数体,this.name 在实例上设置自有属性
super(name) 等价于 AnimalES6.call(this, name),必须在使用 this 前调用,否则报 ReferenceError
static create(name) 静态方法挂在类本身(AnimalES6.create),而非 prototype 上,等价于 AnimalES6.create = function(){}
DogES5.prototype = Object.create(AnimalES5.prototype) 建立原型链:DogES5 实例 → DogES5.prototypeAnimalES5.prototypeObject.prototype
DogES5.prototype.constructor = DogES5 修复因替换 prototype 导致的 constructor 丢失(替换后 constructor 指向 AnimalES5,需手动补回)
typeof AnimalES6 === 'function' 证明 class 的本质仍是函数,只是有更多限制和语法糖
DogES6.prototype.barkenumerable: false class 内定义的方法通过 Object.definePropertyenumerable: false 挂载,不会出现在 for...in
DogES5.prototype.barkenumerable: 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 = 2q2.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.from
return { next: function() { ... } } 返回符合迭代器协议的对象:next() 方法每次返回 { value, done } 格式
Object.defineProperty(EvenNumber, Symbol.hasInstance, { value: fn }) 在构造函数上定义 Symbol.hasInstance,完全接管 instanceof 的判断逻辑
2 instanceof EvenNumber 返回 true instanceof 不再检查原型链,直接调用我们定义的函数(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 操作,在写入前校验数据合法性,不合法则抛出 TypeError
createReadonly 中的 setdeleteProperty 陷阱 只读代理:拦截所有写入和删除,抛出错误;不像 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 的动态绑定:调用时 thisUser 实例,把实例本身序列化为 JSON 字符串
Validatable.validate 中的 .bind(this) forEach 回调中的 this 会丢失,显式绑定当前 this(即 User 实例),以访问 this.rulesthis[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 的原型是 AnimalProto
DogProto.setup = function(name, breed) { this.init(name); ... } setup 是初始化方法(代替构造函数),调用时委托给 AnimalProto.init 初始化父级属性
Object.create(DogProto).setup('旺财', '柴犬') 链式调用:创建以 DogProto 为原型的空对象,然后立即初始化,返回 this(即刚创建的对象)
dog1.speak() 委托给 AnimalProto dog1 自身没有 speak,委托给 DogProtoDogProto 也没有,继续委托给 AnimalProto,找到并执行
Object.getPrototypeOf(dog1) === DogProto 原型链清晰可验:dog1 → DogProto → AnimalProto → Object.prototype → null
new、无 prototype、无 constructor OLOO 只使用 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 原型引用类型属性共享陷阱 ``` > **💡 代码解析** > > | 代码片段 | 含义 | > |---------------------------------------|---------------------------------------------------------------------| > | `Student.prototype.scores = []` | 将数组放在原型上,所有实例通过原型链共享同一个数组对象,这是经典陷阱 | > | `s1.scores.push(95)` 污染 `s2.scores` | `push` 操作修改的是原型上共享数组的内容,`s2.scores` 和 `s1.scores` 指向同一个数组 | > | `StudentFixed` 中 `this.scores = []` | 正确写法:在构造函数中为每个实例创建独立的数组,各实例互不影响 | > | `StudentFixed.prototype.addScore` 放原型 | 方法是函数,所有实例共享一份即可,不需要每个实例都有副本,节省内存 | > | `t1.addScore(95)` 不影响 `t2.scores` | 每个实例有自己的 `scores` 数组,`addScore` 中 `this.scores.push(s)` 操作的是各自的独立数组 | *** ** * ** *** #### 9.7 面试高频考点清单 ##### ⭐⭐⭐ 三星必考 | 考点 | 核心答案要点 | |-------------------------------|-------------------------------------------------------| | 原型链是什么? | 对象的 `[[Prototype]]` 串联的链,用于属性查找,终点为 `null` | | `__proto__` 与 `prototype` 的区别 | `__proto__` 是实例访问原型的方式;`prototype` 是构造函数上用来设置新实例原型的属性 | | `instanceof` 的原理 | 沿左操作数原型链,查找右操作数 `.prototype` 是否存在于链上 | | `new` 做了什么? | 4步:创建空对象 → 设置原型 → 执行构造函数 → 返回对象 | | 值类型和引用类型的区别 | 存储(栈 vs 栈+堆)、赋值(复制值 vs 复制引用)、比较(按值 vs 按地址)、可变性 | | `this` 指向规则 | new \> 显式绑定 \> 方法调用 \> 默认绑定;箭头函数继承外层 | ##### ⭐⭐ 二星进阶 | 考点 | 核心答案要点 | |---------------------------|-----------------------------------------------------| | `call/apply/bind` 区别 | 都改变 this;call/apply 立即调用;bind 返回新函数延迟调用;apply 用数组传参 | | 深拷贝 vs 浅拷贝 | 浅:只复制第一层引用;深:完全独立复制所有层级 | | 如何手写深拷贝 | 递归 + 类型判断 + WeakMap 处理循环引用 | | 属性描述符 | value/writable/enumerable/configurable vs get/set | | `Object.create(null)` 的用途 | 创建无原型的纯净对象,用作安全字典 | | `constructor` 属性的陷阱 | 替换整个 prototype 会导致 constructor 丢失,需手动补回 | ##### ⭐ 一星拔高 | 考点 | 核心答案要点 | |---------------------------------------------|-----------------------------------------------------------------------------------------------| | `Function.__proto__ === Function.prototype` | Function 是自己的实例,是 JS 中唯一一个这样的构造函数 | | `typeof null === 'object'` 为什么? | JS 历史遗留 Bug,null 的二进制表示以 000 开头,与对象相同 | | 原型污染是什么?怎么防? | 修改 `Object.prototype` 危害全局;防御:`Object.create(null)` / `hasOwnProperty` 检查 / `Map` 代替 `Object` | | `class` 和 ES5 构造函数的差异 | 不能不用 new 调用;方法默认不可枚举;内部严格模式;不提升(TDZ) | | 垃圾回收算法 | 标记清除(主流);引用计数(已淘汰,无法处理循环引用) | *** ** * ** *** ### 知识脑图总结 #### 整体知识架构 **渲染错误:** Mermaid 渲染失败: Parse error on line 28: ... \[\[Prototype\]\] 链式结构 终点为 n -----------------------\^ Expecting 'SPACELINE', 'NL', 'EOF', got 'NODE_ID' #### 原型链核心关系图(完整版) 内置构造函数 原型对象 实例 __proto__ __proto__ __proto__ __proto__ __proto__ __proto__ __proto__ __proto__ __proto__ __proto__ __proto__ .prototype .prototype .prototype .prototype .constructor .constructor .constructor .constructor arr = \[

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' }


参考资料


作者按 :原型与原型链是 JavaScript 语言区别于其他面向对象语言的核心所在。理解了这套机制,你就能彻底搞清 classextendssuper 的底层逻辑,以及各种框架源码中的继承实现。多画图、多运行代码是掌握这部分知识最有效的方式。

相关推荐
段ヤシ.1 小时前
回顾Java知识点,面试题汇总Day7(持续更新)
java·开发语言
努力努力再努力wz1 小时前
【Qt入门系列】深入理解信号与槽:从事件响应到自定义信号机制
c语言·开发语言·数据结构·数据库·c++·qt·mysql
在角落发呆1 小时前
DTU 数据转发服务器:工业物联网的隐形桥梁
开发语言·php
Sakuyu434681 小时前
C语言基础--基本数据类型
c语言·开发语言
在坚持一下我可没意见1 小时前
Python 修仙修炼录 05:循环神通,省去无用苦修
开发语言·python·面试·入门·循环·复习
techdashen1 小时前
Rust 社区在 4 月做了什么:项目管理月报解读
开发语言·rust·mfc
十五年专注C++开发1 小时前
QFluentKit: 一个基于 Qt Widgets 的 Fluent Design 风格 UI 组件库
开发语言·c++·qt·ui·qfluentkit
咪饭只吃一小碗1 小时前
从变量提升到 V8 预编译,彻底搞懂 JS 执行机制
javascript
lly2024061 小时前
PHP JSON 使用指南
开发语言