原型链是 JavaScript 最核心也是最容易被误解的概念之一。理解原型链不仅是面试通关的必修课,更是写出优雅、高效 JavaScript 代码的基础。本文将从底层原理出发,系统剖析原型链与继承机制。
一切皆对象?先搞清楚 __proto__ 和 prototype
在 JavaScript 中,每个对象都有一个内部属性 [[Prototype]],在大多数浏览器中通过 __proto__ 访问。而 只有函数才有 prototype 属性——这个 prototype 是一个对象,当该函数作为构造函数调用时,新创建的对象的 __proto__ 会指向这个 prototype。
function Person(name) { this.name = name; }\nPerson.prototype.sayHello = function() { return 'Hello, ' + this.name; };\nconst p = new Person('小明');\n\n// 验证原型关系\nconsole.log(p.__proto__ === Person.prototype); // true\nconsole.log(Person.prototype.constructor === Person); // true\nconsole.log(Object.getPrototypeOf(p) === Person.prototype); // true这段代码揭示了经典的原型三角关系:实例的 __proto__ → 构造函数的 prototype → 构造函数本身。理解这个三角关系是掌握原型链的第一步。
原型链的本质:一条委托链
当访问一个对象的属性时,JavaScript 引擎会遵循以下查找路径:先在对象自身属性中查找,如果找不到则沿着 __proto__ 向上查找,重复直到找到属性或到达 null(原型链终点)。
const arr = [1, 2, 3];\nconsole.log(arr.hasOwnProperty('push')); // false——arr 自身没有 push 方法\narr.push(4); // 但可以调用——它来自 Array.prototype\nconsole.log(arr.__proto__ === Array.prototype); // true\nconsole.log(arr.__proto__.__proto__ === Object.prototype); // true\nconsole.log(arr.__proto__.__proto__.__proto__ === null); // true这就是原型链的完整路径:实例 → 构造函数.prototype → Object.prototype → null。JavaScript 正是通过这条链实现了属性和方法的"继承"——本质上是一种委托机制。
ES5 的六种继承方式
1. 原型链继承
function Parent() { this.colors = ['red', 'blue']; }\nfunction Child() {}\nChild.prototype = new Parent(); // 关键:将父类实例作为子类原型\nconst c1 = new Child(); const c2 = new Child();\nc1.colors.push('green');\nconsole.log(c2.colors); // ['red', 'blue', 'green'] —— 引用类型被共享!致命缺陷:所有子实例共享父类的引用类型属性。
2. 构造函数继承
function Child() { Parent.call(this); }解决了属性共享问题,但无法继承父类原型上的方法。
3. 组合继承(最常用)
function Child() { Parent.call(this); }\nChild.prototype = new Parent();\nChild.prototype.constructor = Child;结合了前两种的优点,但调用了两次父类构造函数。
4. 原型式继承
function createObject(o) { function F() {} F.prototype = o; return new F(); }\n// ES5 标准化为 Object.create()5. 寄生式继承
在原型式继承的基础上增强对象,返回增强后的对象。
6. 寄生组合继承(最优方案)
function Child() { Parent.call(this); }\nChild.prototype = Object.create(Parent.prototype);\nChild.prototype.constructor = Child;这是 ES5 下最理想的继承方案,也是 Babel 转译 ES6 class 继承时使用的模式。
ES6 Class:语法糖还是新机制?
class Animal {\n constructor(name) { this.name = name; }\n speak() { console.log(this.name + ' makes a sound.'); }\n}\nclass Dog extends Animal {\n constructor(name, breed) {\n super(name); // 必须在使用 this 前调用 super\n this.breed = breed;\n }\n speak() { super.speak(); console.log(this.name + ' barks.'); }\n}ES6 class 本质上是原型链继承的语法糖,但引入了一些重要语义:super 关键字必须在子类构造函数中使用 this 之前调用;不可枚举——class 中定义的方法默认不可枚举;暂时性死区——class 声明不会被提升;严格模式——class 体内默认使用严格模式。
实战中的原型链应用
判断数据类型:Object.prototype.toString.call(value) 是判断类型最可靠的方法。方法借用(Method Borrowing):Array.prototype.slice.call(arguments)。避免原型污染:使用 Object.create(null) 创建没有原型的纯字典对象。
总结
原型链不是魔法——它只是一条由 __proto__ 串联起来的对象链。掌握它最有效的方式是:在控制台中打印对象的 __proto__,一层层向上追溯,直到 null。当你能够画出任何对象的完整原型链时,你就真正理解了 JavaScript 的继承机制。
评论 (0)