# JavaScript原型链和继承机制详解
# 1. 原型链基础
# 1.1 构造函数与原型
在JavaScript中,每个函数都有一个prototype属性,这个属性指向一个对象,我们称之为原型对象。当我们使用构造函数创建实例时,实例会通过__proto__属性链接到构造函数的原型对象。
function Person () {
}
const person = new Person();
person.name = 'Liam';
console.log(person.name); // Liam
2
3
4
5
在这个例子中,Person是一个构造函数,我们使用new创建了一个实例对象person。每个实例对象都有一个__proto__属性,指向其构造函数的原型对象。
# 1.2 prototype属性
每个函数都有一个prototype属性,这个属性指向一个对象,即原型对象。这个原型对象包含了所有通过该构造函数创建的实例所共享的属性和方法。
function Person () {
}
// 虽然写在注释里,但是你要注意:
// prototype是函数才会有的属性
Person.prototype.name = 'Liam';
const person1 = new Person();
const person2 = new Person();
console.log(person1.name); // Liam
console.log(person2.name); // Liam
2
3
4
5
6
7
8
9
函数的prototype属性指向一个对象,这个对象是通过该构造函数创建的所有实例的原型。原型对象包含所有实例共享的属性和方法,每个实例都可以访问这些属性和方法。
# 1.3 __proto__属性
每个JavaScript对象(除了null)都有一个__proto__属性,这个属性指向该对象的原型。通过__proto__属性,对象可以访问其原型上的属性和方法,从而实现继承机制。
function Person () {
}
const person = new Person();
console.log(person.__proto__ === Person.prototype); // true
2
3
4
__proto__属性是连接对象与其原型的桥梁,它使得对象能够沿着原型链向上查找属性和方法。
# 1.4 constructor属性
每个原型对象都有一个constructor属性,指向关联的构造函数。这个属性使得我们能够识别对象是由哪个构造函数创建的。
function Person () {
}
const person = new Person();
console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
// 顺便学习一个ES5的方法,可以获得对象的原型
console.log(Object.getPrototypeOf(person) === Person.prototype); // true
2
3
4
5
6
7
8
9
constructor属性提供了一种从原型对象回溯到构造函数的方式,是原型链中的重要一环。
# 1.5 原型链结构图

原型链的顶层是Object.prototype,它的__proto__指向null。所有对象都通过__proto__属性链接到其原型对象,形成原型链。
关键关系:
Object是所有对象的基类,function Object(){ [ native code ] }Function是所有函数的基类,function Function(){ [ native code ] }- 所有构造函数的
__proto__(包括Function和Object)都指向Function.prototype - 所有原型对象的
__proto__都指向Object.prototype Object.prototype.__proto__指向null- 所有对象(包括函数)都有
__proto__属性,指向其构造函数的原型 - 只有函数具有
prototype属性,指向原型对象。原型对象的constructor属性指回构造函数
# 1.6 原型链示例分析
通过代码示例来理解原型链的工作原理:
// 原型链的顶端
Object.prototype.__proto__; // null
// Function.prototype的原型是Object.prototype
Function.prototype.__proto__; // Object.prototype
// Object构造函数本身是Function的实例
Object.__proto__; // Function.prototype
2
3
4
5
6
7
8
属性查找示例:
Function.prototype.a = () => {
console.log(1);
};
Object.prototype.b = () => {
console.log(2);
};
function A() {}
const a = new A();
// a实例本身没有a方法,沿着原型链查找到Function.prototype.a
a.a(); // 1
// a实例本身没有b方法,沿着原型链查找到Object.prototype.b
a.b(); // 2
// A函数本身没有a方法,沿着原型链查找到Function.prototype.a
A.a(); // 1
// A函数本身没有b方法,沿着原型链查找到Object.prototype.b
A.b(); // 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
属性赋值与查找机制:
function A () {}
A.prototype.n = 0;
A.prototype.add = function () {
this.n += 1;
};
a = new A();
b = new A();
a.add();
console.log(b.n); // 0
console.log(a.n); // 1
2
3
4
5
6
7
8
9
10
在上述示例中,当调用a.add()时,this指向实例a。首先在a实例上查找n属性,未找到,然后在原型链上找到A.prototype.n值为0。执行this.n += 1后,会在a实例上创建n属性并赋值为1。而b实例仍然使用原型上的n值0。
# 2. 继承方式详解
# 2.1 原型链继承
通过将子类的原型指向父类的实例来实现继承:
function Father () {
this.property = ['red', 'blue'];
}
function Son () {
this.sonProperty = false;
}
// Student.prototype.sayHello = function () { }
// 在这里写子类的原型方法和属性是无效的,因为会改变原型的指向,所以应该放到重新指定之后
Son.prototype = new Father();
Son.prototype.sayHello = function () { };
const liam = new Son();
const tom = new Son();
console.log(liam.property); // ["red", "blue"]
liam.property.push('yellow');
console.log(tom.property); // ["red", "blue", "yellow"]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
多个实例对引用类型的操作会被篡改。
可以把Son.prototype = new Father(); 拆开来看: let obj = new Father(); Son.prototype = obj;。
也就是说Son的原型指向一个已经创建好的对象实例。liam._proto_ = Son.prototype = obj;,所以所有Son公用同一个对象实例。
优点:
- 父类新增原型方法/原型属性,子类都能访问到
- 简单,易于实现
缺点:
- 无法实现多继承
- 来自原型对象的所有属性被所有实例共享
- 创建子类实例时,无法向父类构造函数传参
- 要想为子类新增属性和方法,必须要在Student.prototype = new Person() 之后执行,不能放到构造器中
# 2.2 构造函数继承(借用构造函数)
在子类构造函数中调用父类构造函数,通过call或apply方法实现继承:
function Father (name, age) {
this.name = name;
this.age = age;
}
Father.prototype.setAge = function () {};
function Son (name, age) {
Father.call(this, name, age);
}
const liam = new Son('liam', '25');
console.log(liam.setAge()); // Uncaught TypeError: liam.setAge is not a function
2
3
4
5
6
7
8
9
10
11
优点:
- 解决了原型链继承中子类实例共享父类引用属性的问题
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(call多个父类对象)
缺点:
- 实例并不是父类的实例,只是子类的实例
- 只能继承父类的实例属性和方法,不能继承原型属性和方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
# 2.3 组合继承(伪经典继承)
结合原型链继承和构造函数继承的优点,通过调用父类构造函数继承实例属性,通过原型链继承原型属性:
function Father (name, age) {
this.name = name;
this.age = age;
}
Father.prototype.setAge = function () {};
function Son (name, age) {
Father.call(this, name, age);
}
Son.prototype = new Father();
Son.prototype.constructor = Son; // 组合继承也是需要修复构造函数指向的
const liam = new Son('liam', '25');
console.log(liam);
/**
{
age: "25", // 通过调用new Son执行构造函数Son中Father.call(this, name, age)代码创建的
name: "liam",
__proto__: {
age: undefined, // liam_proto_指向一个Father实例(obj对象,其愿项链指向Father)
constructor: ƒ Son(name, age),
name: undefined,
__proto__: {
setAge: ƒ (),
constructor: ƒ Father(name, age),
__proto__: Object
}
}
}
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
优点:
- 可以继承实例属性/方法,也可以继承原型属性/方法
- 不存在引用属性共享问题
- 可传参
- 函数可复用
缺点:
- 调用了两次父类构造函数,生成了两份实例
# 2.4 原型式继承
借助原型可以基于已有的对象创建新对象,同时不必因此创建自定义类型:
function object (o) {
function F () {}
F.prototype = o;
return new F();
}
const person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van'],
};
const anotherPerson = object(person);
// let anotherPerson = Object.create(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');
const yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与 object()方法的行为相同。——《JavaScript高级编程》
优点:父类方法可以复用
缺点:
- 父类的引用属性会被所有子类实例共享
- 子类构建实例时不能向父类传递参数
# 2.5 寄生式继承
创建一个仅用于封装继承过程的函数,该函数在内部以某种方式增强对象,最后再像真的是它做了所有工作一样返回对象:
function createAnother (original) {
const clone = object(original); // 通过调用函数创建一个新对象
clone.getName = function () { // 以某种方式来增强这个对象
console.log('liam');
};
return clone; // 返回这个对象
}
const person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van'],
};
const anotherPerson = createAnother(person);
anotherPerson.getName(); // "liam"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
缺点:
- 使用寄生式继承为对象添加函数,会由于不能做到函数复用而降低效率
# 2.6 寄生组合继承
通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型:
function inheritPrototype(Son, Father) {
// eslint-disable-next-line no-new-object
const prototype = new Object(Father.prototype); // 创建了父类原型的浅复制
prototype.constructor = Son; // 修正原型的构造函数
Son.prototype = prototype; // 将子类的原型替换为这个原型
}
function Father (name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Father.prototype.sayName = function () {
console.log(this.name);
};
function Son (name, age) {
Father.call(this, name);
this.age = age;
}
// 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费
inheritPrototype(Son, Father);
Son.prototype.sayAge = function () {
console.log(this.age);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
优点:
- 只调用一次父类构造函数
- 避免在子类原型上创建不必要的、多余的属性
- 原型链保持不变
# 3. 参考文章
← Js中的new操作符 Js防抖节流 →