JavaScript 的继承机制是许多开发者初学时的“噩梦”,也是高级面试中的高频考点。它不像 Java 或 C# 那样通过传统的类(Class)实现继承,而是通过原型链(Prototype Chain)这一独特机制来完成。本文将带你彻底掌握原型继承的核心原理、实现方式及最佳实践。
一、为什么需要原型继承?
假设你正在开发一个电商系统,需要创建 Product
(商品)和 DiscountProduct
(打折商品)两个对象类型。直接复制代码显然不够优雅,而原型继承能让你:
-
复用代码:共享通用属性和方法
-
动态扩展:灵活添加新功能而不影响原有结构
-
内存优化:避免重复创建相同函数
二、原型继承四大核心概念
1.构造函数(Constructor)
构造函数是创建对象的模板,通过 new
关键字实例化对象:
function Product(name, price) {
this.name = name;
this.price = price;
}
const iphone = new Product("iPhone 15", 7999);
2. 原型对象(Prototype)
每个构造函数都有一个 prototype
属性,指向一个对象。该对象包含共享方法:
Product.prototype.showInfo = function() {
console.log(`${this.name} - ¥${this.price}`);
};
iphone.showInfo(); // iPhone 15 - ¥7999
3.实例对象(Instance)
通过 new
创建的对象会自动继承原型上的方法:
console.log(iphone.__proto__ === Product.prototype); // true
三、实现继承的三种方式
方式一:构造函数继承(属性继承)
function DiscountProduct(name, price, discount) {
Product.call(this, name, price); // 继承属性
this.discount = discount;
}
const discountIphone = new DiscountProduct("iPhone 15", 7999, 0.1);
特点:
-
✅ 子类实例拥有独立属性
-
❌ 无法继承父类原型方法
方式二:原型链继承(方法继承)
DiscountProduct.prototype = new Product(); // 方法继承
DiscountProduct.prototype.constructor = DiscountProduct; // 修复构造函数指向
DiscountProduct.prototype.showDiscount = function() {
console.log(`折扣价:¥${this.price * (1 - this.discount)}`);
};
特点:
-
✅ 继承父类原型方法
-
❌ 父类构造函数被调用两次(性能问题)
方式三:寄生组合继承(完美方案)
function inheritPrototype(child, parent) {
const prototype = Object.create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
inheritPrototype(DiscountProduct, Product); // 替换原型链继承步骤
优势:
-
只调用一次父类构造函数
-
保留完整的原型链结构
四、ES6 的 class
语法糖
现代 JavaScript 通过 class
关键字提供更清晰的继承语法:
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
showInfo() {
console.log(`${this.name} - ¥${this.price}`);
}
}
class DiscountProduct extends Product {
constructor(name, price, discount) {
super(name, price); // 调用父类构造函数
this.discount = discount;
}
showDiscount() {
console.log(`折扣价:¥${this.price * (1 - this.discount)}`);
}
}
注意:
-
class
本质仍是原型继承的语法糖 -
super
必须在构造函数第一行调用
五、原型继承的三大应用场景
场景1:框架开发
Vue 2.x 使用原型继承实现组件选项合并:
Vue.extend = function (extendOptions) {
const Super = this;
const Sub = function VueComponent() {
Super.call(this); // 继承属性
};
Sub.prototype = Object.create(Super.prototype); // 继承方法
return Sub;
};
场景2:工具库扩展
为内置对象添加自定义方法:
Array.prototype.last = function() {
return this[this.length - 1];
};
const arr = [1, 2, 3];
console.log(arr.last()); // 3(谨慎使用!可能引发命名冲突)
场景3:性能优化
共享大对象避免内存重复占用:
const heavyData = { /* 10MB 的数据 */ };
function DataWrapper() {}
DataWrapper.prototype.data = heavyData;
const wrapper1 = new DataWrapper();
const wrapper2 = new DataWrapper(); // 共享同一份数据
六、必须绕开的三大陷阱
陷阱1:意外修改原型
function Dog() {}
const dog1 = new Dog();
Dog.prototype = { bark() {} }; // 直接覆盖原型
const dog2 = new Dog();
console.log(dog1.bark); // undefined(原型链断裂)
解决方案:使用 Object.assign()
增量修改原型:
Object.assign(Dog.prototype, { bark() {} });
陷阱2:循环原型链
function A() {}
function B() {}
A.prototype = new B();
B.prototype = new A(); // 死循环!
陷阱3:方法遮蔽(Shadowing)
function Parent() {}
Parent.prototype.value = 1;
function Child() {}
Child.prototype = new Parent();
Child.prototype.value = 2; // 修改父类属性值
const obj = new Child();
console.log(obj.value); // 2(优先访问子类属性)
七、最佳实践清单
-
优先使用 ES6
class
语法 -
避免直接操作
__proto__
(用Object.getPrototypeOf()
替代) -
引用类型属性应定义在构造函数中
function BadExample() {
this.numbers = [1, 2, 3]; // 正确做法
}
4. 使用 TypeScript 增强类型安全
class Product {
constructor(public name: string, public price: number) {}
}
结语
原型继承是 JavaScript 的灵魂机制,理解它能让你:
-
写出更优雅的面向对象代码
-
深入理解主流框架的设计思想
-
在面试中轻松应对高阶问题
记住:Class 是语法糖,原型才是本质。掌握本文内容后,不妨尝试用原型继承实现一个简易版的 Vue 响应式系统,这将是对知识的绝佳实践!