原型(prototype)是JavaScript这门语言最核心、也是最难啃的骨头之一。它不仅是实现对象继承的基础,更是理解JS内部机制的关键。本文将带你从零开始,全面剖析原型、原型链、各种继承模式以及ES6类语法,配合大量代码和示意图,让你彻底掌握“原型与继承”。无论你是刚入门还是想加深理解,这篇文章都能提供足够的深度。
原型(Prototype)基础
每个JavaScript函数都有一个特殊的prototype属性,指向一个对象。当这个函数作为构造函数(使用new)调用时,新创建的对象会内部链接到该原型对象,这个链接称为[[Prototype]](可通过__proto__或Object.getPrototypeOf()访问)。
function Person(name) {
this.name = name;
}
// 向原型添加方法
Person.prototype.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
// 创建实例
const p1 = new Person('Alice');
p1.sayHello(); // 你好,我是Alice
// 查看原型关系
console.log(p1.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
{ sayHello, constructor }
name: "Alice"
p1.__proto__ → Person.prototype → Person.prototype.__proto__ → Object.prototype
关键点:所有实例共享原型上的方法和属性,但实例属性(如name)是独立的。原型链查找:访问对象属性时,先找自身,再沿__proto__向上查找。
原型链(Prototype Chain)
原型对象本身也是一个对象,它也有自己的原型,这样就构成了原型链。最终链顶是Object.prototype,其原型为null。
function Animal(type) {
this.type = type;
}
Animal.prototype.getType = function() {
return this.type;
};
function Dog(name) {
this.name = name;
}
// 关键:让Dog.prototype 继承 Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复constructor
Dog.prototype.bark = function() {
console.log('汪汪!');
};
const d1 = new Dog('Buddy');
d1.bark(); // 汪汪!
// d1本身没有type,但可以从原型链获取
console.log(d1.type); // undefined (因为Animal构造函数未执行)
继承模式深度剖析
JavaScript中实现继承有多种方式,每种都有其优缺点。我们逐一分析。
1. 原型链继承
function Parent() {
this.colors = ['red','blue'];
}
Parent.prototype.getColors = function() {
return this.colors;
};
// 子类
function Child() {}
Child.prototype = new Parent(); // 原型指向父实例
const c1 = new Child();
c1.colors.push('green');
const c2 = new Child();
console.log(c2.colors); // ['red','blue','green'] 引用共享!
2. 盗用构造函数 (Constructor Stealing)
this.name = name;
this.colors = ['red','blue'];
}
function Child(name) {
Parent.call(this, name); // 借用构造函数
}
const c1 = new Child('Tom');
c1.colors.push('green');
const c2 = new Child('Jerry');
console.log(c2.colors); // ['red','blue'] 独立
❌ 缺点:方法必须在构造函数内定义,无法复用。
3. 组合继承 (最常用)
this.name = name;
this.colors = ['red','blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name); // 第二次调用Parent
this.age = age;
}
Child.prototype = new Parent(); // 第一次调用Parent
Child.prototype.constructor = Child;
const c = new Child('Lisa',12);
c.sayName(); // Lisa
⚠️ 缺点:调用了两次父构造函数,子原型上有多余的父实例属性。
4. 寄生组合继承 (完美方案)
const proto = Object.create(Parent.prototype);
proto.constructor = Child;
Child.prototype = proto;
}
function Parent(name) {
this.name = name;
}
Parent.prototype.say = function() {};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
inheritPrototype(Child, Parent);
// 只调用一次Parent,且原型链干净
ES6 Class 与 extends
ES6引入的class语法让原型继承更清晰,但其本质依然是基于原型的。
constructor(type) {
this.type = type;
}
getType() { return this.type; }
}
class Dog extends Animal {
constructor(name, type) {
super(type); // 调用父构造函数
this.name = name;
}
bark() { console.log('汪汪'); }
}
const d = new Dog('Buddy','犬科');
console.log(d instanceof Dog); // true
console.log(d instanceof Animal); // true
super()必须在使用this之前调用,且只能用在constructor中。extends 会设置原型链:Object.setPrototypeOf(Dog, Animal) 以及 Dog.prototype = Object.create(Animal.prototype)。
原型方法 vs 实例方法
this.value = value; // 实例属性
this.instanceMethod = function() { // 实例方法
console.log('实例方法', this.value);
};
}
MyClass.prototype.protoMethod = function() { // 原型方法
console.log('原型方法', this.value);
};
const a = new MyClass(1);
const b = new MyClass(2);
console.log(a.instanceMethod === b.instanceMethod); // false
console.log(a.protoMethod === b.protoMethod); // true
✨ 原型方法 共享同一函数,节省内存。
🔧 实例方法 每个实例独立拷贝,适合需要闭包或动态生成的场景。
🔄 一般将可复用的方法定义在原型上,实例属性放在构造函数内。
内置对象原型与扩展
所有内置对象(Array, Function, Object等)都有自己的原型,我们可以修改它们,但不推荐。
Array.prototype.first = function() { return this[0]; };
const arr = [1,2,3];
console.log(arr.first()); // 1
// 但是可能引发冲突或意外行为,尤其在不同库之间。
// 更好的做法是使用静态方法或继承。
性能与最佳实践
- 原型链不宜过长:每层查找都有性能损耗,保持原型链扁平。
- 使用 hasOwnProperty 过滤原型属性:遍历对象时配合
Object.hasOwn()或hasOwnProperty。 - 善用 Object.create(null):创建无原型的纯净对象(适合作为字典)。
- 避免动态修改原型:性能差且不利于优化。
- ES6 class 更清晰:推荐用于新项目,但理解原型有助于调试。
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key);
}
}
// 或使用 Object.keys(obj) 只返回自有属性
调试技巧
const obj = {a:1};
console.dir(obj); // 展开__proto__
console.log(obj.__proto__);
console.log(Object.getPrototypeOf(obj));
// 检查原型链
console.log(obj instanceof Object);
console.log(Object.prototype.isPrototypeOf(obj));
a: 1
▶ __proto__: Object
▶ constructor: ƒ Object()
▶ hasOwnProperty: ƒ hasOwnProperty()
...