JavaScript 原型系统深度解析(下卷):原型链、继承与 ES6 class 本质

67 阅读4分钟

前言

在上卷中,我们厘清了构造函数、prototype__proto__constructor 的基本关系,理解了“实例通过 __proto__ 链接到构造函数的 prototype”这一核心机制。然而,JavaScript 的原型系统远不止于此——它支持多层链接,形成一条从实例到 Object.prototype 的完整原型链(Prototype Chain) ,并以此实现继承

本卷将深入探讨:

  • 原型链如何逐级向上查找属性?
  • 如何通过 new Animal() 实现“类式继承”?
  • Object.prototype 为何是链的终点?
  • ES6 class 到底是语法糖还是新机制?
  • 大厂面试必考的 instanceofisPrototypeOf 原理

掌握这些,你将真正打通 JavaScript 面向对象的任督二脉。


一、原型链:属性查找的完整路径

当你访问一个对象的属性时,JavaScript 引擎会按以下顺序查找:

  1. 自身属性:先在对象自身查找
  2. 原型属性:若未找到,则沿 __proto__ 向上查找
  3. 逐级递归:直到 Object.prototype
  4. 终止条件:若到达 null(即 Object.prototype.__proto__ === null)仍未找到,返回 undefined

示例验证:

js
编辑
function Person() {}
Person.prototype.speci = '人类';

const su = new Person();
console.log(su.speci); // '人类' → 来自 Person.prototype

// 继续向上
console.log(su.toString()); // '[object Object]' → 来自 Object.prototype
console.log(su.__proto__.__proto__ === Object.prototype); // true
console.log(su.__proto__.__proto__.__proto__); // null

原型链结构
suPerson.prototypeObject.prototypenull


二、模拟“类继承”:通过原型链实现多层共享

你的代码中有一段关键实践:

js
编辑
var obj = new Object();
obj.spec = '动物';

function Animal() {}
Animal.prototype = obj;

function Person() {}
Person.prototype = new Animal(); // 👈 关键!

var su = new Person();
console.log(su.spec); // '动物'

这是如何工作的?

  1. new Animal() 创建了一个空对象,其 __proto__ 指向 Animal.prototype(即 obj

  2. 将这个对象赋值给 Person.prototype

  3. 因此,Person 实例的原型链变为:

    text
    编辑
    su → (new Animal()) → obj → Object.prototypenull
    

效果:

  • su.spec 在 su 自身找不到 → 查找 (new Animal())(无)→ 查找 obj → 找到 '动物'
  • 这模拟了  “Person 继承 Animal”  的效果

💡 这正是 ES5 时代实现“继承”的经典模式之一(组合寄生式继承的雏形)。


三、Object.prototype:原型链的终极根节点

所有对象最终都继承自 Object.prototype,除非显式切断:

js
编辑
console.log({}.__proto__ === Object.prototype); // true
console.log([].__proto__.__proto__ === Object.prototype); // true
console.log((function(){}).__proto__.__proto__ === Object.prototype); // true

Object.prototype 提供了通用方法:

  • toString()
  • valueOf()
  • hasOwnProperty()
  • isPrototypeOf()

而它的 __proto__null,标志着原型链的终结:

js
编辑
console.log(Object.prototype.__proto__); // null

📌 设计哲学
JavaScript 不预设“万物皆对象”的强制血缘,而是通过可链接的对象实现灵活复用——这正是“原型式面向对象”的精髓。


四、属性遮蔽(Shadowing):实例优先原则

当实例自身定义了与原型同名的属性时,实例属性会“遮蔽”原型属性

js
编辑
function Person() {}
Person.prototype.speci = '人类';

const su = new Person();
su.speci = 'LOL达人'; // 在实例上创建新属性

console.log(su.speci);        // 'LOL达人'(实例属性)
console.log(su.__proto__.speci); // '人类'(原型未被修改)

关键点:

  • 赋值操作(=)总是在实例上创建/修改属性
  • 读取操作才会触发原型链查找

这保证了数据隔离:每个实例可拥有个性化状态,同时共享公共行为。


五、ES6 class:语法糖下的原型本质

你对比了 ES5 与 ES6 写法:

js
编辑
// ES6
class Person {
  constructor(name) { this.name = name; }
  sayHi() { ... }
}

// ES5 等价写法
function Person(name) { this.name = name; }
Person.prototype.sayHi = function() { ... };

事实是:

  • class 完全是语法糖,底层仍基于原型
  • constructor 对应构造函数
  • 类方法自动挂载到 Person.prototype 上
  • static 方法挂载到 Person 函数本身

验证:

js
编辑
class Person {}
console.log(typeof Person); // "function"
console.log(Person.prototype.constructor === Person); // true

结论
学习原型机制,永远比死记 class 语法更重要。因为 React、Vue、Lodash 等源码,依然大量使用原型思想。


六、大厂面试高频考点精析

Q1:instanceof 的原理是什么?

A:检查构造函数的 prototype 是否出现在实例的原型链上。

js
编辑
su instanceof Person
// 等价于:Person.prototype in su's __proto__ chain

Q2:Object.getPrototypeOf(obj) 和 obj.__proto__ 有何区别?

A:前者是标准 API,后者是非标准属性。功能相同,但推荐使用前者。

Q3:如何判断一个属性是实例自有还是继承的?

A:使用 hasOwnProperty

js
编辑
su.hasOwnProperty('speci'); // false(来自原型)
su.hasOwnProperty('name');  // true(实例自有)

Q4:能否修改原型链?

A:可以,但不推荐。例如:

js
编辑
Object.setPrototypeOf(su, anotherProto);

会破坏引擎优化,影响性能。


结语:回到哲学——原型 vs 类

传统 OOP(如 Java)强调“抽象模板 → 具体实例”的静态血缘;
而 JavaScript 选择“对象链接 → 动态委托”的灵活哲学。

正如道格拉斯·克罗克福德所言:

“JavaScript 的原型机制,是一种更贴近现实世界的建模方式——我们不是从‘人类’这个抽象概念造人,而是从已有的人身上学习行为。”

理解原型链,不仅是掌握一门语言的特性,更是拥抱一种去中心化、可组合、动态演化的编程思维。

最后提醒
下次当你写下 class 时,请记住——
背后仍是那条从实例出发,穿越层层 __proto__,最终抵达 Object.prototype 的古老链条。