# 原型与继承

作者: 刘周玮 日期: 2020-07-30

# 1. 原型

# 1.1 原型是什么?

原型 是创建自定义对象的工具。原型兄弟众多,比如 Object、工厂模式、构造函数模式、构造函数模式与原型模式的组合,动态原型模式、寄生构造函数模式、稳妥构造函数模式。

原型的特点 是能复用代码,能将创建的实例标识为一种特定的类型,不污染全局作用域; 从不初始化参数,且过于乐于共享。



# 1.2 原型到底是什么?(栗子版)

function Person() {}

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

var person1 = new Person();
person1.sayName();

var person2 = new Person();
person2.sayName();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 理解原型对象分三步
  1. 创建自定义的构造函数阶段
      如例子所示,创建 Person 函数的时候,会根据一定规则为 Person 函数创建一个 prototype 属性,prototype 属性指向 Person 函数的原型对象。Person.prototype 可以为 Person 原型对象添加属性和方法,比如 name、age、job、sayName。
      Person 原型对象在默认情况下自动获得 constructor 属性,指向 Person 函数。Person 原型对象只会默认获得 constructor 属性,其他方法则是从 Object 继承而来。

  2. 创建实例阶段
      如例子所示,person1 与 person2 是 Person 函数的实例,在创建 person1 后,person1 内部将包含属性 [ [Prototype] ] ,指向 Person 原型对象。

  3. 查找对象属性阶段
      如例子所示,person1.sayName( )调用了 sayName 属性,其查找属性的流程是先在 person1 实例中查找 sayName 属性;如果没找到,则顺着 [ [Prototype] ] 去 Person 原型里找;如果原型里也没找到,则顺着原型对象里的 [ [Prototype] ] ,去 Person 原型继承的 Object 构造函数的原型对象里找;如果最终还是没找到,则返回 null。



# 1.3 原型相关属性和方法(栗子版)

  1. prototype :

    函数的属性,函数创建时必然根据一定规则为该函数创建 prototype 属性,它指向函数的原型对象。

  2. constructor :

    函数原型对象的属性,是一个指向 prototype 属性所在函数的指针。
    (例如: Person.prototype.constructor 指向 Person)

  3. Object.hasOwnProperty( ) :

    检测属性是存在于实例中(true),还是存在于原型中(false)。

function Person () { }
Person.prototype.name = “小红”;
var p1 = new Person();
p1.sex = "女";
console.log(p1.hasOwnProperty("sex"));  //sex属性是直接在p1属性中添加,所以是true
console.log(p1.hasOwnProperty("name")); // name属性是在原型中添加的,所以是false
console.log(p1.hasOwnProperty("age"));// age 属性不存在,所以也是false
1
2
3
4
5
6
7
  1. prototypeObj.isPrototypeOf( ) :

    判断实例有没有 [ [Prototype] ] ,指向原型。

function Person () { }
Person.prototype.name = “小红”;
var p1 = new Person();
p1.sex = "女";
console.log(Person.prototype.isPrototypeOf(p1)); //true
1
2
3
4
5
  1. Object.getPrototypeof( ) :

    返回[[Prototype]]的值,即返回原型对象。

function Person () { }
Person.prototype.name = “小红”;
var p1 = new Person();
p1.sex = "女";
console.log(Object.getPrototypeOf(p1)==Person.prototype); //true
console.log(Object.getPrototypeOf(p1).name); //小红
1
2
3
4
5
6
  1. in 操作符

    用来判断一个属性是否存在于这个对象中。在查找这个属性时候,先在对象本身中找,如果对象找不到再去原型中找。换句话说,只要对象和原型中有一个地方存在这个属性,就返回 true。如果一个属性存在,但是没有在对象本身中,则一定存在于原型中。

function Person () {}
Person.prototype.name = "小红";
var p1 = new Person();
p1.sex = "女";

//定义一个函数去判断原型所在的位置
function propertyLocation(obj, prop){
  if(!(prop in obj)){
     console.log(prop + "属性不存在”);
  }else if(obj.hasOwnProperty(prop)){
     console.log(prop + "属性存在于对象中");
   }else {
      console.log(prop + "对象存在于原型中");
   }
}
propertyLocation(p1, "age"); //age属性不存在
propertyLocation(p1, "name"); //name对象存在于原型中
propertyLocation(p1, "sex"); //sex属性存在于对象中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  1. Object.create(prototype)

    创建以 prototype 为原型的新对象
    newObj.__proto__ = prototype



# 2 继承

# 2.1 继承怎么实现?原型链是什么?

(图片转载自JS 继承的八种写法)

继承主要依赖原型链实现。继承的方法有原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承

原型链的基本思想是利用原型让一个引用类型继承另一个引用的属性和方法。每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。假如让一个原型对象等于另一个类型的实例,那么此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条,这就是我们说的原型链。

# 2.2 原型链的基本实现(栗子版)

function SuperType() {
  this.property = true;
}

SuperType.prototype.getSuperValue = function () {
  return this.property;
};

function SubType() {
  this.subproperty = false;
}

SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function () {
  return this.subproperty;
};

var instance = new SubType();
console.log(instance.getSuperValue());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

  如例子所示,SubType 通过创建 SuperType 实例,并重写 SubType 的原型对象将该值赋予 SubType.prototype,继承了 SuperType,而 SuperType 同理继承了 Object。(所有函数的默认原型都是 Object 实例,都继承 toStirng( )、valueOf( )等默认方法)

# 2.3 创建对象和原型链的几种方法

(以下转载自JS 继承的八种写法

  1. 使用语法结构创建
// 对象都继承成于Object.prototype(包含hasOwnProperty、toString等方法)
var obj = { a: 1 }; // 原型链如: obj ---> Object.prototype ---> null
var obj2 = Object.create(obj);
obj2.b = 2;

// 数组都继承于 Array.prototype (包含 indexOf, forEach 等方法)
var a = [1, 2, 3]; // 原型链如: a ---> Array.prototype ---> Object.prototype ---> null

// 函数都继承于 Function.prototype (包含 call, bind等方法)
function f() {
  return 2;
} // 原型链如: f ---> Function.prototype ---> Object.prototype ---> null
1
2
3
4
5
6
7
8
9
10
11
12
  1. 使用构造器创建
    在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作符 来作用这个函数时,它就可以被称为构造方法(构造函数)。
// 创建Person构造函数(其原型指向Object.prtototype)
function Person(){};
// 创建Stuendt构造函数(此时Student构造函数的原型和Person构造函数的原型一样)
function Student(){};
// 创建Person构造函数的实例对象person
let person=new Person();
// 重写Student构造函数的原型对象,将其指向Person构造函数的实例对象person
Student.prototype=person;
// 创建Student构造函数的实例对象stu
let stu=new Student();
1
2
3
4
5
6
7
8
9
10
  1. 使用 Object.create 创建的对象
var a = { a: 1 }; // a ---> Object.prototype ---> null
var b = Object.create(a); // b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (继承而来)
1
2
3
  1. 使用 class 关键字创建的对象
    ECMAScript6 引入了一套新的关键字用来实现 class。主要有:class、extends、constructor、super、static。
class Graphic {
  constructor(l, width, height) {
    this.l = l;
    this.width = width;
    this.height = height;
  }
  V1() {
    return this.l * this.width * this.height;
  }
  static V2(l, width, height) {
    return l * height * width;
  }
}

// 构建一个长方形对象,求面积
class Rectangle extends Graphic {
  constructor(width, height) {
    super(width, height);
  }
  get area() {
    return this.l * this.width;
  }
}

let rec = new Rectangle(4, 5);
console.log(rec.l); //4
console.log(rec.width); //5
console.log(rec.height); //undefined
console.log(rec.area); //20

// 构建一个长方体对象,求体积
class Cuboid extends Graphic {
  constructor(l, width, height, unit) {
    super(l, width, height);
    this.unit = unit;
  }
  // 只有在子类的静态函数中才能调用父类的静态函数
  static v2(l, width, height) {
    return super.V2(l, width, height);
  }
  v3() {
    return this.l * this.height * this.width;
  }
}

let cub = new Cuboid(2, 2, 3, "立方米");
console.log(cub.V1()); //6
console.log(cub.unit); //“立方米”
console.log(Cuboid.v2(2, 3, 4)); //24
console.log(cub.v3()); //6
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# 2.4 继承的几种方法比较

  1. 原型链
function SuperType() {
  this.colors = ["red", "blue", "green"];
}
function SubType() {}

SubType.prototype = new SuperType();

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

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

特点 :可以复用父函数方法;过分乐于共享,一个实例改变父属性后,其他实例引用的父属性也会改变。

  1. 构造函数继承
function SuperType() {
  this.color = ["red", "green"];
}

// 构造函数继承
// 使得每个实例都会复制得到自己独有的一份属性
function SubType() {
  // 将父对象的构造函数绑定在子对象上
  SuperType.call(this);
  // 创建子类实例时调用SuperType构造函数,
  // 于是SubType的每个实例都会将SuperType中的属性复制一份
  // 解决了原型链继承中多实例相互影响的问题。
}

let inst1 = new SubType();

console.log(inst1); // SubType {color: Array(2)}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

特点 :父类的引用属性不会被共享,子类构建实例时可以向父类传参;无法实现复用,每个子类都有父类实例函数的副本,影响性能。同时,只能继承父类的实例属性和方法,不能继承原型属性或方法。

  1. 组合继承
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.getName = function () {
  return this.name;
};

function SubType(name, age) {
  // 1、构造函数来复制父类的属性给SubType实例
  // *** 第二次调用SuperType(),给instance1写入两个属性name,color。
  SuperType.call(this, name);
  this.age = age;
}

SubType.prototype.getAge = function () {
  return this.age;
};

// 2、原型继承
// *** 第一次调用SuperType(),给SubType.prototype写入两个属性name,color。
SubType.prototype = new SuperType();
// 手动挂上构造器,指向自己的构造函数 SubType
SubType.prototype.constructor = SubType;
SubType.prototype.getAge = function () {
  return this.age;
};

let inst1 = new SubType("Asuna", 20);

console.log("inst1", inst1);
console.log(inst1.getName(), inst1.getAge());
console.log(inst1 instanceof SubType, inst1 instanceof SuperType);

// inst1 SubType {name: "Asuna", colors: Array(3), age: 20}
// Asuna 20
// true true
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
31
32
33
34
35
36
37
38

特点 :结合了原型链和构造函数的优点;而在使用子类创建实例对象时,其原型中会存在两份相同的父类实例的属性或方法。这种被覆盖的情况造成了性能上的浪费。