原型、原型链以及继承(二)

本文主要是对 【小红书】 中原型和继承进行了归纳总结。

原型链

许多语言都支持两种继承方式:接口继承和实现继承;而 JavaScript 只支持 实现继承,且其主要是依靠 原型链 来实现的。

利用原型链实现继承的主要思想:
让一个引用类型继承另一个引用类型的属性和方法;

什么是原型链?

构造函数、原型和实例的关系:
每一个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

如果让原型对象 A Prototype 等于另一个类型的实例 b(也即重写原型对象 A Prototype),则该原型对象不再包含指向对应的构造函数 A 的指针,而是包含了一个指向另一个原型对象 B Prototype 的指针;相应的,如果再让原型对象 B Prototype 等于另一个类型的实例 c,则原型对象 B Prototype 包含了一个指向原型对象 C Prototype 的指针;如此层层递进,就构成了实例与原型对象之间的链条。

原型链

1
2
3
4
5
6
7
8
function C(){}
function B(){}
B.prototype = new C(); // 让 B 的原型对象等于 C 的实例
function A(){}
A.prototype = new B(); // 让 A 的原型对象等于 B 的实例
var a = new A();

因此,利用原型链实现继承的本质就是重写原型对象,也就是说上面例子中,原来存在于 BC 的实例中的属性和方法,现在也存在于 A Prototype 中。

在通过原型链实继承的情况下,当以读取模式访问一个属性的时候,搜索过程就得以沿着原型链继续向上。

默认的原型

所有引用类型默认都继承了 Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype
默认的原型

确定原型对象和实例的关系

  • instanceof()
    只要用这个操作符来测试实例与原型链中出现过的构造函数,结果都会返回 true

    1
    alert(a instanceof C); // true
  • isPrototypeOf()
    只要用这个操作符来测试实例与原型链中出现过的原型对象,结果都会返回 true

    1
    alert(C.prototype.isPrototypeOf(a)); // true

定义方法

  • 给原型对象添加方法或者是重写超类型中的方法的代码一定要放在替换原型(重写原型对象)的语句之后;
  • 在通过原型链实现继承时,不能使用对象字面量创建原型方法;

原型链存在的问题

  • 最主要的问题来自包含引用类型值的原型
    在通过原型链实现继承时,原型对象实际上会变成另一个类型的实例;原先的实例属性(可能会有包含引用类型值的属性)也就顺理成章的变成了现在的原型属性了;而包含引用类型值的原型属性会被所有实例共享。

  • 在创建子类型的实例时,没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数

基于以上两点,原型链在继承中很少单独使用。

继承的方法(六种)

使用原型链(很少单独使用)

见上文 ↑

借用构造函数(很少单独使用)

别名: 伪造对象或经典继承
基本思路: 通过使用 apply()call() 方法,在子类型构造函数的内部调用超类型构造函数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}
function SubType(){
// 继承了 SuperType,同时还传递了参数
SuperType.call(this,"Bob");
// 为避免 SuperType 构造函数不会重写子类型的属性,应该在调用超类型构造函数之后,再添加应该在子类型中定义的属性(即子类型的实例属性)
this.age = 28;
}
var instance1 = new SuperType("Alice");
instance1.colors.push("white");
alert(instance1.colors); // "red,blue,green,white"
alert(instance1.name); // Alice
var instance2 = new SubType();
alert(instance2.colors); // "red,blue,green"
alert(instance2.name); // Bob
alert(instance2.age); // 28

优点: 解决了原型中包含引用类型值所带来的问题;可以在子类型构造函数中向超类型构造函数传递参数;
缺点: 子类型只能继承超类型构造函数中的属性和方法。如果方法都在构造函数中定义,函数无法复用;如果方法在超类型中的原型中定义,则对子类型而言是不可见的;

组合继承(最常用)

别名: 伪经典继承
基本思路: 使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承;

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
function SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function{
alert(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(){
alert(this.age);
};
var instance1 = new SubType("Bob",27);
instance1.colors.push("white");
alert(instance1.colors); // "red,blue,green,white"
instance1.sayName(); // "Bob"
instance1.sayAge(); // 27
var instance2 = new SubType("Alice",29);
alert(instance2.colors); // "red,blue,green"
instance2.sayName(); // "Alice"
instance2.sayAge(); // 29

优点: 避免了单独使用原型链和构造函数的缺陷,融合了它们的优点;instanceofisPrototypeOf 也能用于识别基于组合继承创建的对象;
缺点: 无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

原型式继承

基本思想: 借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型;
前提条件: 必须有一个对象可以作为另一个对象的基础;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var person = {
name: "Bob",
friends: ["Ann", "Lucy", "Petter"]
};
var person1 = Object.create(person,{
name: {
value: "Alice"
}
});
person1.friens.push("David");
alert(peron1.name); // "Alice"
alert(person1.friends); // "Ann,Lucy,Petter,David"
alert(person.name); // "Bob"
alert(person.friends); // "Ann,Lucy,Petter,David"

Object.create(para1,para2)方法

  • 第一个参数(必需):一个用作新对象原型的对象;
  • 第二个参数(可选):一个为新对象定义额外属性的对象,其格式为:每个属性都是通过自己的描述符定义的。(以这种方式指定的任何属性都会覆盖原型对象上的同名属性)
    使用场景: 在没有必要创建构造函数,而只是想让一个对象与另一个对象保持类似的情况下,可以使用原型式继承;
    缺点: 包含引用类型值的属性会被所有实例所共享;

寄生式继承

基本思路: 创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function createAnother(original){
// 参数 original 即为将要作为新对象基础的对象
var clone = object(original);
// 添加新方法
clone.sayHi = function(){
alert("Hi!");
}
return clone;
}
function object(o){
function F(){}
F.prototype = o;
return new F();
}
var person = {
name: "Bob",
friends: ["Ann", "Lucy", "Petter"]
};
var person1 = createAnother(person);
person1.sayHi(); // "Hi!"

使用场景: 在主要考虑对象而不是自定义类型和构造函数的情况下,可以使用寄生式继承;
注意: 上面代码中使用的 object() 函数不是必需的,任何能返回新对象的函数都适用于此模式;

寄生组合式继承(引用类型最理想的继承范式)

基本思路: 通过借用构造函数来继承属性,通过原型链的混成形式来继承方法;(使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型)

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
function inheritPrototype(subType,superType){
// 创建超类型原型的一个副本
var prototype = Object(superType.prototype);
// 为创建的副本添加 constructor 属性
prototype.constructor = subType;
// 将新创建的对象(副本)赋给子类型的原型
subType.prototype = prototype;
}
function SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name,age){
SuperType.call(this,name);
this.age = age;
}
inheritPrototype(SubType, SuperType); // 为子类型原型赋值
SubType.prototype.sayAge = function(){
alert(this.age);
}
var instance = new SubType("Bob",23);
instance.sayName(); // "Bob"
instance.sayAge(); // 23

优点: 高效率(只调用了一次超类型的构造函数);避免了在子类型原型上创建不必要的、多余的属性;原型链保持不变, instanceofisPrototypeOf 能正常使用;

寄生组合式继承,集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效方式。

您的支持将鼓励我继续创作!