想真正理解JavaScript的对象模型,绕不开原型与原型链。即便你每天都在用class和extends,如果说不清一个对象为什么能访问自己没有的方法,遇到继承相关的性能问题和调试难题时就会非常被动。下面从函数与对象之间的隐藏联系开始,逐步拆解原型链的工作方式。

一、原型对象与三个关键属性
在JavaScript中,每一个函数被创建时都会自动获得一个prototype属性,这个属性指向一个对象,通常称为原型对象。默认情况下,原型对象内部只有一个constructor属性,并且该属性指回构造函数本身。通过构造函数创建实例时,实例的内部属性[[Prototype]]会指向构造函数的prototype。浏览器环境提供了__proto__访问器来暴露这个内部指针,不过更规范的做法是使用Object.getPrototypeOf。
下面这段代码演示了prototype、constructor和__proto__之间的关系。理解这个三角形结构是掌握原型链的第一步。
function Person(name) {
this.name = name;
}
console.log(Person.prototype.constructor === Person); // true
var p = new Person('Alice');
console.log(Object.getPrototypeOf(p) === Person.prototype); // true
console.log(p.__proto__ === Person.prototype); // true(浏览器环境)
console.log(p.constructor === Person); // true
需要注意的是,constructor属性并不是实例自身的属性,而是通过原型链找到的。如果我们手动重写了构造函数的prototype对象,但没有重新指定constructor,那么实例的constructor可能指向Object而不是原构造函数。这也是很多代码中会出现SubType.prototype.constructor = SubType的原因。
二、属性查找:原型链如何决定取值与赋值
当读取一个对象的属性时,JavaScript引擎会先检查对象自身的可枚举属性,如果没找到,就沿着[[Prototype]]向上查找,直到找到属性或者到达原型链的末端null。这条由对象、原型对象、原型的原型串联起来的路径,就是原型链。所有普通对象最终都会指向Object.prototype,而Object.prototype的原型是null,因此像toString、hasOwnProperty这些方法能被任何对象调用。
赋值操作的行为与读取不同。如果给一个对象设置一个它自身没有的属性,JavaScript会直接在对象上创建该属性,而不会去修改原型链上的同名属性。但如果原型上的属性是一个引用类型,并且实例直接修改了这个引用类型的内部结构,那么所有共享该原型的实例都会受到影响,这是早期原型链继承中非常典型的坑。
function Animal() {}
Animal.prototype.type = 'mammal';
var a1 = new Animal();
var a2 = new Animal();
console.log(a1.type); // 'mammal'
a1.type = 'bird';
console.log(a1.type, a2.type); // 'bird' 'mammal'
delete a1.type;
console.log(a1.type); // 'mammal' 再次沿原型链查找
区分属性是实例自身的还是从原型链继承的,可以使用hasOwnProperty方法。它只会检查对象自身,不会沿原型链查找。而in操作符则不同,只要属性在原型链上存在就会返回true。这种差异在遍历对象属性时尤为重要,配合Object.keys和for...in可以避免误操作原型成员。
三、六种继承方案对比与实现
理解了原型链的查找机制后,再来分析继承的几种实现方式。原型链继承是最直接的一种:把子类的prototype设置为父类实例。这样做能让子类实例访问父类原型上的方法,但父类实例自身的引用类型属性会被所有子类实例共享,而且子类构造函数无法向父类传递参数。
function SuperType() {
this.colors = ['red', 'blue'];
}
function SubType() {}
SubType.prototype = new SuperType();
var s1 = new SubType();
s1.colors.push('green');
var s2 = new SubType();
console.log(s2.colors); // ['red', 'blue', 'green']
为了解决共享和传参问题,出现了盗用构造函数继承。在子类构造函数内部通过SuperType.call(this)调用父类构造函数,这样每个实例都会拥有独立的属性副本。但缺点也很明显:方法只能在构造函数中定义,无法通过原型复用,每次创建实例都会重新创建一遍方法。
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
function SubType(name) {
SuperType.call(this, name);
}
var s1 = new SubType('Alice');
console.log(s1.name); // 'Alice'
组合继承结合了原型链继承和盗用构造函数继承的优点:使用原型链继承方法,使用盗用构造函数继承实例属性。不过它有一个明显的效率问题,父类构造函数会被调用两次,一次是在设置子类原型时,另一次是在子类构造函数内部。子类原型上因此存在多余的父类实例属性。
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
};
更优雅的寄生组合继承通过Object.create创建父类原型的一个副本,避免调用父类构造函数,同时还能正确维护原型链。它被认为是ES5环境下最理想的继承方案,也是很多类库内部实现继承的基础。
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
};
原型式继承利用Object.create直接以某个对象为原型创建新对象,适合不需要构造函数的场景。寄生式继承则在原型式继承的基础上对返回的对象进行增强。这两种方式常见于临时对象或轻量级继承需求中。
四、class语法与原型继承的关系
ES6引入的class和extends让继承写法更加清晰,但底层仍然使用原型链。一个类本质上是构造函数加原型对象的组合,extends背后做了两件事:让子类构造函数的[[Prototype]]指向父类构造函数以实现静态成员继承,同时让子类prototype的[[Prototype]]指向父类prototype以实现实例方法继承。
class Parent {
constructor(name) {
this.name = name;
}
say() {
return 'I am ' + this.name;
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
}
var c = new Child('Bob', 10);
console.log(c.say());
console.log(Object.getPrototypeOf(Child) === Parent); // true
console.log(Object.getPrototypeOf(Child.prototype) === Parent.prototype); // true
因此,class继承本质上就是经过语法封装的寄生组合继承。理解这一点后,就不会把JavaScript的类继承与Java、C++等静态语言的类继承混淆。JavaScript没有真正的类复制行为,继承的本质是对象之间的委托,实例通过原型链找到共享的方法。
掌握原型与原型链不仅能帮你写出更健壮的继承结构,也能在阅读框架源码、排查内存泄漏、理解instanceof原理时提供清晰的思维模型。原型链是JavaScript语言的核心支柱,值得花时间彻底搞懂。
JavaScript原型原型链继承构造函数修改时间:2026-09-27 23:26:01