原型与原型链: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 方法只存了一份,但所有实例都能用。我在项目中经常用这个特性来优化内存——尤其是那些工具方法,挂到原型上比每个实例都复制一份要省得多。

核心要点:prototype 是函数特有的属性,用来定义所有实例共享的属性和方法。

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

但有个坑:如果你手动覆盖了 prototypeconstructor 就丢了。

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');
  }
};
注意:覆盖 prototype 时,记得手动修复 constructor 指向,否则会导致类型判断出错。

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__ 一层层看,非常直观。

原型链的本质:对象通过 __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())。

我的建议:如果你还在用 ES5 写代码,组合继承是最稳妥的选择。如果可以用 ES6,直接用 class 和 extends,底层原理是一样的,但写法更优雅。

总结一下

概念 作用 注意点
prototype 函数上的原型对象,定义共享属性和方法 只有函数才有
__proto__ 实例指向原型的内部链接 建议用 Object.getPrototypeOf()
constructor 原型指向构造函数的引用 覆盖 prototype 后要手动修复
原型链继承 子类 prototype 指向父类实例 引用共享,无法传参
组合继承 构造函数 + 原型链 最常用,父类构造函数调用两次

原型和原型链,说白了就是 JavaScript 实现「类」和「继承」的底层机制。你只要记住一句话:对象通过 __proto__ 连接成链,方法通过 prototype 共享。搞懂这个,后面学 class、extends 就轻松多了。

我个人习惯在项目里用 class 语法,但遇到需要深度定制继承逻辑的场景,还是会回到原型链上来。毕竟,理解底层原理,才能写出更健壮的代码。