原型与原型链:prototype、__proto__、constructor、原型链继承、组合继承
说实话,原型和原型链这块,是很多 JavaScript 开发者绕不过去的坎。我记得刚入行那会儿,面试官问我「说说原型链」,我背得滚瓜烂熟,但真到项目里用的时候,还是一脸懵。后来踩了不少坑,才慢慢摸清楚这里面的门道。
今天咱们就把这块彻底掰扯清楚。你想想看,JavaScript 没有传统的「类」的概念,它是怎么实现继承的?答案就是原型。
1. prototype:函数的「遗产」
每个函数都有一个 prototype 属性。这个属性指向一个对象,叫「原型对象」。说白了,就是给将来通过 new 创建出来的实例准备的「共享遗产」。
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log('Hi, I am ' + this.name);
};
const p1 = new Person('张三');
const p2 = new Person('李四');
p1.sayHi(); // Hi, I am 张三
p2.sayHi(); // Hi, I am 李四
console.log(p1.sayHi === p2.sayHi); // true
看到了吗?sayHi 方法只存了一份,但所有实例都能用。我在项目中经常用这个特性来优化内存——尤其是那些工具方法,挂到原型上比每个实例都复制一份要省得多。
2. __proto__:实例的「秘密通道」
每个对象都有一个 __proto__ 属性(现在标准里叫 [[Prototype]])。它指向创建这个对象的构造函数的 prototype。
function Person(name) {
this.name = name;
}
const p = new Person('王五');
console.log(p.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
嗯,这里要注意:__proto__ 是浏览器实现的,虽然好用,但生产环境我建议用 Object.getPrototypeOf() 来获取原型。我曾经在老旧浏览器上因为 __proto__ 踩过兼容性的坑,后来就养成了用标准 API 的习惯。
Object.getPrototypeOf(obj) 代替 obj.__proto__,更安全也更规范。
3. constructor:回家的「门牌号」
原型对象上默认有一个 constructor 属性,指向构造函数本身。你可以把它理解成「门牌号」——通过实例能找到它的「家」是哪个构造函数。
function Person(name) {
this.name = name;
}
const p = new Person('赵六');
console.log(Person.prototype.constructor === Person); // true
console.log(p.constructor === Person); // true
但有个坑:如果你手动覆盖了 prototype,constructor 就丢了。
function Person(name) {
this.name = name;
}
Person.prototype = {
sayHi: function() {
console.log('Hi');
}
};
const p = new Person('测试');
console.log(p.constructor === Person); // false
console.log(p.constructor === Object); // true
为什么会这样?因为新赋值的对象字面量没有 constructor 属性,它继承自 Object.prototype。我建议你手动补上:
Person.prototype = {
constructor: Person,
sayHi: function() {
console.log('Hi');
}
};
4. 原型链:一层一层往上找
当你访问一个对象的属性时,JavaScript 会先找对象本身。找不到,就顺着 __proto__ 往上找。再找不到,就继续往上。直到 Object.prototype.__proto__ 为 null 为止。这条链,就是原型链。
function Animal(type) {
this.type = type;
}
Animal.prototype.eat = function() {
console.log('吃吃吃');
};
function Dog(name) {
this.name = name;
}
// 让 Dog 继承 Animal
Dog.prototype = new Animal('哺乳动物');
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log('汪汪汪');
};
const d = new Dog('旺财');
console.log(d.name); // 旺财 —— 来自实例
console.log(d.type); // 哺乳动物 —— 来自原型链
console.log(d.eat()); // 吃吃吃 —— 来自原型链
console.log(d.bark()); // 汪汪汪 —— 来自原型链
console.log(d.toString());// [object Object] —— 来自 Object.prototype
你想想看,这个查找过程就像「向上追溯家谱」。我调试原型链问题时,经常在浏览器控制台里展开 __proto__ 一层层看,非常直观。
5. 原型链继承:最基础的继承方式
原型链继承,就是把父类的实例赋值给子类的 prototype。
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
this.age = age;
}
// 继承
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const c1 = new Child('小明', 10);
const c2 = new Child('小红', 12);
c1.colors.push('green');
console.log(c1.colors); // ['red', 'blue', 'green']
console.log(c2.colors); // ['red', 'blue', 'green'] —— 被影响了!
看到了吗?引用类型属性被所有实例共享了。我在项目中遇到过这个坑——一个实例改了 colors,其他实例也跟着变。这显然不是我们想要的。
6. 组合继承:取长补短
组合继承,就是把原型链继承和构造函数继承结合起来。说白了,用构造函数解决属性复制问题,用原型链解决方法共享问题。
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
// 借用构造函数继承属性
Parent.call(this, name);
this.age = age;
}
// 原型链继承方法
Child.prototype = new Parent();
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(this.age);
};
const c1 = new Child('小明', 10);
const c2 = new Child('小红', 12);
c1.colors.push('green');
console.log(c1.colors); // ['red', 'blue', 'green']
console.log(c2.colors); // ['red', 'blue'] —— 互不影响
c1.sayName(); // 小明
c2.sayName(); // 小红
这个模式我用了好多年,直到 ES6 的 class 出现。它解决了原型链继承的两个核心问题:引用共享和参数传递。不过它也有个小毛病——父类构造函数被调用了两次(一次在 Parent.call,一次在 new Parent())。
总结一下
| 概念 | 作用 | 注意点 |
|---|---|---|
| prototype | 函数上的原型对象,定义共享属性和方法 | 只有函数才有 |
| __proto__ | 实例指向原型的内部链接 | 建议用 Object.getPrototypeOf() |
| constructor | 原型指向构造函数的引用 | 覆盖 prototype 后要手动修复 |
| 原型链继承 | 子类 prototype 指向父类实例 | 引用共享,无法传参 |
| 组合继承 | 构造函数 + 原型链 | 最常用,父类构造函数调用两次 |
原型和原型链,说白了就是 JavaScript 实现「类」和「继承」的底层机制。你只要记住一句话:对象通过 __proto__ 连接成链,方法通过 prototype 共享。搞懂这个,后面学 class、extends 就轻松多了。
我个人习惯在项目里用 class 语法,但遇到需要深度定制继承逻辑的场景,还是会回到原型链上来。毕竟,理解底层原理,才能写出更健壮的代码。