1. 为什么需要理解原型链
在JavaScript的世界里,原型链就像空气一样无处不在却又容易被忽视。我刚开始接触JS时,曾经天真地认为只要会用new关键字创建对象就够了,直到有一天遇到了这个诡异的场景:
function Person() {} Person.prototype.sayHello = function() { console.log('Hello') }; const p1 = new Person(); p1.sayHello(); // 正常输出 'Hello' const p2 = Object.create(null); p2.sayHello(); // TypeError: p2.sayHello is not a function为什么p1能调用sayHello而p2不行?这就是原型链在背后起作用。原型链是JavaScript实现继承的核心机制,理解它不仅能帮你避免这类诡异bug,更能让你真正掌握JS面向对象编程的精髓。
2. 原型链的基础结构
2.1 三个核心概念
原型链建立在三个关键概念上:
__proto__(现已标准化为Object.getPrototypeOf()):每个对象都有一个指向其原型的内部链接prototype属性:只有函数才有这个属性,它指向该函数作为构造函数时创建的实例的原型constructor属性:原型对象指向构造函数的反向引用
它们的关系可以用这个简单的代码表示:
function Foo() {} const foo = new Foo(); // 关系验证 console.log(foo.__proto__ === Foo.prototype); // true console.log(Foo.prototype.constructor === Foo); // true console.log(Object.getPrototypeOf(foo) === Foo.prototype); // true2.2 原型链的查找机制
当访问一个对象的属性时,JS引擎会按照以下顺序查找:
- 先在对象自身属性中查找
- 如果没找到,通过
__proto__向上查找原型对象 - 重复这个过程直到
Object.prototype(所有原型链的顶端) - 如果还没找到则返回
undefined
这个查找过程就是所谓的"原型链"。例如:
function Animal() {} Animal.prototype.eat = function() { console.log('Eating') }; function Dog() {} Dog.prototype = Object.create(Animal.prototype); Dog.prototype.bark = function() { console.log('Woof!') }; const myDog = new Dog(); myDog.bark(); // 先在myDog找,没找到就去Dog.prototype myDog.eat(); // 在Dog.prototype没找到,继续去Animal.prototype3. 原型链的实战应用
3.1 实现继承的几种方式
3.1.1 组合继承(最常用)
function Parent(name) { this.name = name; } Parent.prototype.sayName = function() { console.log(this.name); }; function Child(name, age) { Parent.call(this, name); // 继承实例属性 this.age = age; } Child.prototype = Object.create(Parent.prototype); // 继承方法 Child.prototype.constructor = Child; // 修复constructor const child = new Child('Alice', 5); child.sayName(); // "Alice"3.1.2 寄生组合式继承(最佳实践)
function inheritPrototype(child, parent) { const prototype = Object.create(parent.prototype); prototype.constructor = child; child.prototype = prototype; } function Parent(name) { this.name = name; } Parent.prototype.sayName = function() { console.log(this.name); }; function Child(name, age) { Parent.call(this, name); this.age = age; } inheritPrototype(Child, Parent); const child = new Child('Bob', 6); child.sayName(); // "Bob"3.2 原型链与性能优化
原型链虽然强大,但过长的原型链会影响性能。我曾经优化过一个项目,发现某个对象的原型链有7层之多,导致属性访问速度明显下降。解决方案:
- 扁平化原型链:尽量减少继承层级
- 缓存频繁访问的属性:
// 优化前 for (let i = 0; i < 10000; i++) { obj.deeply.nested.property; // 每次都要遍历原型链 } // 优化后 const prop = obj.deeply.nested.property; for (let i = 0; i < 10000; i++) { prop; // 直接访问缓存的值 }
4. 原型链的陷阱与解决方案
4.1 原型污染问题
原型污染是常见的安全问题,攻击者可以通过修改Object.prototype等基础原型来影响所有对象:
// 恶意代码 Object.prototype.isAdmin = true; // 你的代码 const user = { name: 'Alice' }; if (user.isAdmin) { // 本不该执行,但因为原型污染而执行了 grantAdminAccess(); }防御方案:
- 使用
Object.create(null)创建无原型的纯净对象 - 检查对象自身属性时使用
Object.hasOwnProperty() - 冻结基础原型(生产环境慎用):
Object.freeze(Object.prototype);
4.2 构造函数与原型的关系
一个常见的误区是混淆构造函数与其实例的原型关系:
function Foo() {} const foo = new Foo(); // 错误理解 console.log(foo.prototype); // undefined,只有函数有prototype属性 // 正确理解 console.log(Object.getPrototypeOf(foo) === Foo.prototype); // true4.3 原型链的终点
所有原型链最终都指向Object.prototype,而Object.prototype的原型是null:
console.log(Object.getPrototypeOf(Object.prototype)); // null理解这一点对深度操作对象很重要,比如实现深度拷贝时需要处理这个边界情况。
5. 现代JS中的原型链
5.1 class语法糖与原型链
ES6的class本质上是原型继承的语法糖:
class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a noise.`); } } class Dog extends Animal { constructor(name) { super(name); } speak() { console.log(`${this.name} barks.`); } } // 等价于 function Animal(name) { this.name = name; } Animal.prototype.speak = function() { console.log(`${this.name} makes a noise.`); }; function Dog(name) { Animal.call(this, name); } Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; Dog.prototype.speak = function() { console.log(`${this.name} barks.`); };5.2 原型链与模块化
在现代模块化开发中,原型链仍然扮演重要角色。比如React组件继承:
class MyComponent extends React.Component { // 这里就利用了原型链继承React.Component的方法 }5.3 原型方法与静态方法
理解原型链能帮你区分实例方法与静态方法:
class Foo { instanceMethod() {} // 存在于Foo.prototype static staticMethod() {} // 直接存在于Foo } // 等价于 function Foo() {} Foo.prototype.instanceMethod = function() {}; Foo.staticMethod = function() {};6. 原型链调试技巧
6.1 查看原型链
Chrome开发者工具中,可以展开对象的__proto__属性查看完整原型链:
const arr = []; // 在控制台输入arr,然后展开__proto__链 // arr -> Array.prototype -> Object.prototype -> null6.2 判断属性来源
function checkProperty(obj, prop) { if (obj.hasOwnProperty(prop)) { return 'own'; } else if (prop in obj) { return 'prototype'; } else { return 'not found'; } } const obj = { a: 1 }; Object.prototype.b = 2; console.log(checkProperty(obj, 'a')); // 'own' console.log(checkProperty(obj, 'b')); // 'prototype' console.log(checkProperty(obj, 'c')); // 'not found'6.3 原型链与instanceof
instanceof操作符就是通过检查原型链工作的:
function Foo() {} const foo = new Foo(); console.log(foo instanceof Foo); // true // 等价于 console.log(Foo.prototype.isPrototypeOf(foo)); // true7. 高级原型模式
7.1 混入模式(Mixin)
当需要多重继承时,可以使用混入模式:
const canEat = { eat() { console.log('Eating'); } }; const canWalk = { walk() { console.log('Walking'); } }; function Person() {} Object.assign(Person.prototype, canEat, canWalk); const person = new Person(); person.eat(); // "Eating" person.walk(); // "Walking"7.2 代理与原型链
Proxy可以拦截原型链操作:
const handler = { get(target, prop) { if (prop === 'secret') { return 'access denied'; } return Reflect.get(...arguments); } }; const obj = new Proxy({}, handler); obj.__proto__ = { secret: 'prototype secret' }; console.log(obj.secret); // "access denied" 而不是 "prototype secret"8. 原型链的最佳实践
根据多年经验,我总结了以下原型链使用原则:
- 继承层级不超过3层:过深的原型链会影响性能和可维护性
- 优先使用组合而非继承:多用对象组合,少用类继承
- 避免修改内置原型:除非开发polyfill,否则不要修改
Array.prototype等内置原型 - 明确属性来源:使用
hasOwnProperty区分自有属性和继承属性 - 考虑使用class语法:虽然本质是语法糖,但可读性更好
9. 常见面试题解析
9.1 实现new操作符
理解原型链后,可以手动实现new的功能:
function myNew(constructor, ...args) { // 1. 创建一个新对象,继承constructor的prototype const obj = Object.create(constructor.prototype); // 2. 执行构造函数,绑定this const result = constructor.apply(obj, args); // 3. 如果构造函数返回对象,则使用该对象 return result instanceof Object ? result : obj; } function Person(name) { this.name = name; } const p = myNew(Person, 'Alice'); console.log(p.name); // "Alice"9.2 原型链与闭包结合
一个有趣的模式是将原型方法与闭包结合:
function Counter() { let count = 0; // 私有变量 this.increment = function() { return ++count; }; } Counter.prototype.getCount = function() { // 这里无法访问count,因为它是构造函数内的局部变量 return 'Count is private'; }; const c = new Counter(); console.log(c.increment()); // 1 console.log(c.getCount()); // "Count is private"这个例子展示了原型方法与构造函数闭包的不同作用域。