前言
在上卷中,我们厘清了构造函数、prototype、__proto__ 与 constructor 的基本关系,理解了“实例通过 __proto__ 链接到构造函数的 prototype”这一核心机制。然而,JavaScript 的原型系统远不止于此——它支持多层链接,形成一条从实例到 Object.prototype 的完整原型链(Prototype Chain) ,并以此实现继承。
本卷将深入探讨:
- 原型链如何逐级向上查找属性?
- 如何通过
new Animal()实现“类式继承”? Object.prototype为何是链的终点?- ES6
class到底是语法糖还是新机制? - 大厂面试必考的
instanceof、isPrototypeOf原理
掌握这些,你将真正打通 JavaScript 面向对象的任督二脉。
一、原型链:属性查找的完整路径
当你访问一个对象的属性时,JavaScript 引擎会按以下顺序查找:
- 自身属性:先在对象自身查找
- 原型属性:若未找到,则沿
__proto__向上查找 - 逐级递归:直到
Object.prototype - 终止条件:若到达
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
✅ 原型链结构:
su→Person.prototype→Object.prototype→null
二、模拟“类继承”:通过原型链实现多层共享
你的代码中有一段关键实践:
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); // '动物'
这是如何工作的?
-
new Animal()创建了一个空对象,其__proto__指向Animal.prototype(即obj) -
将这个对象赋值给
Person.prototype -
因此,
Person实例的原型链变为:text 编辑 su → (new Animal()) → obj → Object.prototype → null
效果:
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的古老链条。