# 原型链

每个实例对象都有一个 __proto__ 的私有属性指向它的构造函数的原型对象。该原型对象也有一个 __proto__ 的私有属性指向它的构造函数的原型对象,就这样层层向上,直到构造函数 Object 的原型对象 null,这就是原型链。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

下面是网络上传播很广的一张关于原型链的图:

如果你觉得上面的图很乱,可以看下面我整理的这张(点击放大):

记忆口诀:

  • 蓝色线:所有的对象(包括 .prototype 对象)最终都指向 Object.prototype,以继承对象共有的属性和方法 (valueOf、hasOwnProperty 等);
  • 红色线:所有的函数(包括构造函数),都要先指向 Function.prototype,以继承函数共有的方法 (apply、call、bind),然后再指向 Object.prototype。

# 基于原型链的继承

原型对象上的所有属性和方法,都能被对应的构造函数创建的实例对象共享。在读取对象的某个属性或方法时,JavaScript 引擎将首先检查对象本身是否存在该属性。 如果不存在,就会沿着__proto__到它的构造函数的原型对象上去找。如果找不到,就会沿着__proto__查找;如果直到最顶层的 Object.prototype 还是找不到,则返回 undefined。

需要注意的是,在原型链寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。

# 对象的创建

# 工厂模式

function createPerson(name, age, job) {
  let o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function() {
    console.log(this.name);
  };
  return o;
}
let person1 = createPerson("小明", 29, "Software Engineer");
let person2 = createPerson("小红", 27, "Doctor");

console.log(person1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# person1 在 chrome 控制台的原型链结构
age: 29
job: "Software Engineer"
name: "小明"
sayName: ƒ ()
# __proto__ 直接指向 Object 的原型,而不是 createPerson
[[Prototype]]: Object
  constructor: ƒ Object()
  hasOwnProperty: ƒ hasOwnProperty()
  ...
1
2
3
4
5
6
7
8
9
10

优点:可以解决创建多个类似对象的问题。
缺点:没有解决对象标识问题(无法使用 instanceof)。

# 构造函数模式

function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {
    console.log(this.name);
  };
}

let person1 = new Person("小明", 29, "Software Engineer");
let person2 = new Person("小红", 27, "Doctor");

person1.sayName(); // 小明
person2.sayName(); // 小红
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# person1 在 chrome 控制台的原型链结构
age: 29
job: "Software Engineer"
name: "小明"
sayName: ƒ () # 这个方法在每个实例上都有
[[Prototype]]: Object # __proto__ 指向了 Person 的原型
  constructor: ƒ Person(name, age, job)
  [[Prototype]]: Object
    constructor: ƒ Object()
    hasOwnProperty: ƒ hasOwnProperty()
1
2
3
4
5
6
7
8
9
10

Person() 内部的代码跟工厂模式 createPerson() 基本是一样的,只是有如下区别。

  • 没有显式地创建对象。
  • 属性和方法直接赋值给了 this。
  • 没有 return。

使用 new 操作符创建实例会执行以下操作:

  1. 在内存中创建一个新对象。
  2. 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。
  3. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
  4. 执行构造函数内部的代码(给新对象添加属性)。
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

优点:可以确定实例是什么类型(可以使用 instanceof)。
缺点:定义的方法会在每个实例上都创建一遍。

# 原型模式

function Person() {}
  Person.prototype.name = "小明";
  Person.prototype.age = 29;
  Person.prototype.job = "Software Engineer";
  Person.prototype.sayName = function() {
  console.log(this.name);
};

let person1 = new Person();
person1.sayName(); // "小明"
let person2 = new Person();
person2.sayName(); // "小明"

console.log(person1.sayName == person2.sayName); // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14

优点:原型上定义的属性和方法可以被对象实例共享。
缺点:对象实例添加一个属性,这个属性会遮蔽( shadow)原型对象上的同名属性。

# 构造函数 + 原型

使用最为广泛!

// 属性放在构造函数中
function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ["Shelby", "Court"];
}
// 方法挂载原型上
Person.prototype = {
  constructor : Person,
  sayName : function(){
    alert(this.name);
  }
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.friends.push("Van");
alert(person1.friends); // "Shelby,Count,Van"
alert(person2.friends); // "Shelby,Count"
alert(person1.friends === person2.friends); // false
alert(person1.sayName === person2.sayName); // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

优点:每个实例有自己的属性,并共享着原型上的方法。
缺点:暂未发现。

# 继承方式

# 基本模式

function Pet () {
  this.cute = true;
}
Pet.prototype.isCute = function () {
  return this.cute;
};
function Cat () {
  this.name = false;
}

// Cat 的原型对象被修改为 Pet 的实例,Cat 继承了 Pet
Cat.prototype = new Pet();
Cat.prototype.getName = function () {
  return this.name;
};

var instance = new Cat();
alert(instance.isCute()); // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

缺点:Pet 实例上的属性,被添加到了 Cat.prototype 上,我们希望的是 prototype 上只有方法。

WARNING

在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做会重写原型链。

// = 右侧的对象是 Object 构造函数的实例,而不再是 Pet 的实例
Cat.prototype = {
  getName : function () {
    return this.name;
  }
};
1
2
3
4
5
6

# 经典继承

也叫借用构造函数模式。

function Pet () {
  this.colors = ["red", "blue", "green"];
}
function Cat () {
  // 借用了 Pet 构造函数
  Pet.call(this)
}

var cat1 = new Cat();
cat1.colors.push("black");
alert(cat1.colors); // "red,blue,green,black"

var cat2 = new Cat();
alert(cat2.colors); // "red,blue,green"
1
2
3
4
5
6
7
8
9
10
11
12
13
14

优点:可以在借用父构造函数时传参。 缺点:函数无法复用。

# 组合式继承

也叫伪经典继承,就是 基本模式+经典继承。

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且,instanceof 和 isPrototypeOf() 也能够用于识别基于组合继承创建的对象。

function Pet (cute) {
  this.cute = cute;
  this.colors = ["red", "blue", "green"];
}
Pet.prototype.isCute = function () {
  alert(this.cute);
};
function Cat (cute, name) {
  // 继承属性
  Pet.call(this, cute)
  this.name = name;
}

// 继承方法
Cat.prototype = new Pet()
Cat.prototype.constructor = Cat;
Cat.prototype.sayName = function () {
  alert(this.name)
}

var cat1 = new Cat(true"小明");
cat1.colors.push("black");
alert(cat1.colors); // "red,blue,green,black"
cat1.isCute();      // true
cat1.sayName();     // "小明";

var cat2 = new Cat(false, "小红");
alert(cat2.colors); // "red,blue,green"
cat2.isCute();      // false
cat2.sayName();     // "小红";
1
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

缺点:无论什么情况下,都会调用两次 Pet 构造函数:一次是在创建 Cat 原型的时候,另一次是在 Cat 构造函数内部。

# 原型式继承

基于现有对象快速的创建新对象,可以使用现有对象的属性和方法。

// 从本质上讲, object() 对传入其中的对象执行了一次浅复制
function object(o){
  function F(){}    // 创建构造函数
  F.prototype = o;  // 修改原型对象
  return new F();   // 返回实例
}
1
2
3
4
5
6

EC5 通过新增 Object.create() 方法规范化了原型式继承。这个方法接收两个参数:

  • 一个用作新对象原型的对象
  • 一个为新对象定义额外属性的对象(可选的)。
    • 是一个属性描述对象
    • 第二个参数不传时和原型继承的 object() 方法一样
const cat = {
  name: "小明",
  sayName: function () {
    alert(this.name)
  }
}

const cat1 = Object.create(cat, {
  name: {
    value: "小红"
  }
})

console.log(cat1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# cat1 在 chrome 控制台显示的原型链结构

name: "小红"
[[Prototype]]: Object
  name: "小明"
  sayName: ƒ ()
  [[Prototype]]: Object
    constructor: ƒ Object()
    hasOwnProperty: ƒ hasOwnProperty()
    isPrototypeOf: ƒ isPrototypeOf()
    toString: ƒ toString()
    valueOf: ƒ valueOf()
    ...
1
2
3
4
5
6
7
8
9
10
11
12
13

优点:快速,方便。 缺点:由于是浅拷贝,所以引用类型的属性共享一个值,创建多个对象时,修改一个属性影响所有对象。

# 寄生式继承

大白话:把原型式继承包了一层,在新对象上加一些属性和方法,返回新对象。

虽然其优缺点和原型式继承一样,但相比于原型式继承,还是在父类基础上添加了更多的方法。

function createAnother(obj){
  var clone = Object.create(obj); // 原型式继承
  clone.sayHi = function(){ // 增强这个对象
    alert("hi");
  };
  return clone; // 返回这个对象
}

var person = {
  name: "小明",
  friends: ["小红", "小花"]
};
var anotherPerson = createAnother(person);

console.log(anotherPerson)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 寄生组合式继承

组合继承 + 寄生式继承,通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

基本思路是:修改 Cat 的原型时不再生成 Pet 的实例,而是使用 Object.create() 创建一个新对象,这个新对象的 __proto__属性指向 Pet.prototype。

// 基本模式
function inheritPrototype(Cat, Pet){
  const proto = Object.create(Pet.prototype); // 创建对象
  proto.constructor = Cat; // 修改指针
  Cat.prototype = proto; // 修改原型
}
1
2
3
4
5
6
function Pet(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
Pet.prototype.sayName = function(){
  alert(this.name);
};
function Cat(name, age){
  // 这里依然调用 Pet 来继承属性
  Pet.call(this, name);
  this.age = age;
}

// 继承方法时不再使用 Pet 的实例
inheritPrototype(Cat, Pet);
Cat.prototype.sayAge = function(){};

const cat = new Cat("小明", 3)
console.dir(cat)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# cat 实例在 chrome 控制台显示的原型链结构

Cat
age: 3
colors: (3) ['red', 'blue', 'green']
name: "小明"
# 此对象由 Object.create 创建,__proto__ 指向 Pet.prototype 对象
[[Prototype]]: Pet
  constructor: ƒ Cat(name, age)
  sayAge: ƒ ()
  # 这个是 Pet.prototype 对象
  [[Prototype]]: Object
    constructor: ƒ Pet(name)
    sayName: ƒ ()
    [[Prototype]]: Object
      constructor: ƒ Object()
      hasOwnProperty: ƒ hasOwnProperty()
      toString: ƒ toString()
      valueOf: ƒ valueOf()
      ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# ES6 extends

ES6 类支持单继承。使用 extends 关键字,就可以继承任何拥有[[Construct]]和原型的对象。很大程度上,这意味着不仅可以继承一个类,也可以继承普通的构造函数(保持向后兼容)。

因为浏览器的兼容性问题,如果遇到不支持 ES6 的浏览器,那么就得利用 babel 这个编译工具,将 ES6 的代码编译成 ES5,让一些不支持新语法的浏览器也能运行。

class Pet {}
class Cat extends Pet {}

const cat = new Cat()
console.dir(cat)
1
2
3
4
5

请注意图中的绿色线,是 extends 继承与寄生组合式继承的区别!

区别:在 ES6 中,父类的静态方法,可以被子类继承。这是因为 Class 作为构造函数的语法糖,同时有 prototype 属性和 __proto__ 属性,因此同时存在两条继承链。

  • 子类的 __proto__ 属性,表示构造函数的继承,总是指向父类。
  • 子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的 prototype 属性。
class Parent {}
class Child extends Parent {}

console.log(Child.__proto__ === Parent) // true
console.log(Child.prototype.__proto__ === Parent.prototype) // true
1
2
3
4
5

# 参考

上次更新: 4/17/2022, 10:59:22 AM