JavaScript 基础 - 继承

一,基础知识

回顾 构造函数、原型对象、实例对象三者之间的关系:

构造函数prototype 属性 值为 原型对象。

原型对象constructor 属性 值为 构造函数。

实例对象[[prototype]] 内部属性(__proto__ 属性) 值为 其构造函数的 prototype 属性值。

获取对象(包括了函数和数组)的 [[prototype]] 内部属性值有两种方式:

1,Object.getPrototypeOf() 方法

2,__proto__ 属性

构造函数的 prototype 属性值有两种情况:

1,原型对象constructor 属性,值为 构造函数。

2,实例对象[[prototype]] 内部属性,值为 该实例对象的构造函数的 prototype 属性值。

我理解的继承:

把一个实例对象 赋值给 一个函数的原型,这个函数就继承了这个实例对象的构造函数。

如何去判断继承:

1,子类 vs 父类

如果 SubA 继承自某构造函数,那么 SubAprototype 属性指向这个构造函数的实例对象。

方法一:

通过这个实例的 constructor 属性找到这个构造函数,就能确定其父类。

方法二:

constructor 丢失的情况下,

通过这个实例的 [[prototype]] 内部属性或者 __proto__ 属性,就能找到其父类的原型对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function A () {}
function SubA () {}
SubA.prototype = new A();

function B () {}
function SubB () {}
SubB.prototype = new B();

function C () {}

// 方法一,获取子类原型对象的构造器属性(从其原型对象上继承而来)
SubA.prototype.constructor === A // true
SubB.prototype.constructor === B // true
C.prototype.constructor === C // true

// 方法二,获取子类原型对象的原型对象
SubA.prototype.__proto__ === A.prototype // true
Object.getPrototypeOf(SubA.prototype) === A.prototype // true
SubB.prototype.__proto__ === B.prototype // true
C.prototype.__proto__ === Object.prototype // true
2,子类实例 vs 父类

子类实例的 [[prototype]] 内部属性指向父类的实例。

1
2
3
4
5
6
7
8
9
10
11
12
function A () {}
var objA = Object.create(new A());


/*
父类实例和子类实例的 [[prototype]] 属性的 constructor 属性都等于 父类构造函数,
objA.__proto__.constructor === A // true
Object.getPrototypeOf(objA).constructor === A // true
*/

objA.__proto__.__proto__ === A.prototype // true
Object.getPrototypeOf(Object.getPrototypeOf(objA)) === A.prototype // true

二,ES5中的继承模式

1、原型链继承

把 父类的实例对象 作为 子类的 prototype 属性的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 firstInst = new SubType();

var secondInst = new SubType();

console.log(secondInst.getSuperValue());
console.log(Object.getPrototypeOf(firstInst) === Object.getPrototypeOf(secondInst));

原型链的问题:

在通过原型来实现继承时,原型实际上会变成另一个类型的实例,
于是,另一个类型上原先的实例属性也就变成了现在的原型属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}

function SubType() {
}

// 继承了 SuperType
SubType.prototype = new SuperType();

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

var instance2 = new SubType();
// instance1.colors 和 instance2.colors 中存放的是同一个组数对象的引用。
console.log(instance2.colors); // "red,blue,green,black"

2、借用构造函数继承

在子类构造函数的内部调用父类构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function SuperType (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.getSuperColors = function () {
return this.colors;
};

function SubType (name) {
SuperType.call(this, name);
}
SubType.prototype.getSubColors = function () {
return this.colors;
};

var instance1 = new SubType('Nicholas');
instance1.colors.push('black');
console.log(instance1.getSuperColors()); // TypeError
console.log(instance1.name);
console.log(instance1.getSubColors());

var instance2 = new SubType('Greg');
console.log(instance2.name);
console.log(instance2.getSubColors());

优点:

相对于原型链而言,这种方式可以在子类构造函数中向父类构造函数传递参数。

缺点:

方法都在构造函数中定义,无法复用,父类定义在原型中的方法,对于子类而言不可见。

3、组合继承

使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。

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
function SuperType (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
};

function SubType (name, age) {
SuperType.call(this, name);
this.age = age;
}
// SubType.prototype = new SuperType();
// 使用 Object.create(SuperType.prototype) 可以解决下面的缺点,是寄生组合式继承的简化版。
SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
console.log(this.age);
};

var instance1 = new SubType('Nicholas', 29);
instance1.colors.push('black');
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // Nicholas
instance1.sayAge(); // 29

var instance2 = new SubType('Greg', 27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // Greg
instance2.sayAge(); // 27

缺点:

1,在创建子类原型时,调用了父类的构造函数,写入所有父类的实例属性。

2,在调用子类构造函数时,重写这些属性。

4、原型式继承 和 Object.create()

原型式继承没有使用严格意义上的构造函数,借助原型可以基于已有对象创建新对象。

ECMAScript 5 通过新增 Object.create() 方法规范了原型式继承。

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
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
};

var anotherPerson = Object.create(person, {
// foo会成为所创建对象的数据属性
foo: { writable:true, configurable:true, value: "hello" }
});
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');

var yetAnotherPerson = Object.create(person, {
// bar会成为所创建对象的访问器属性
bar: {
configurable: false,
get: function() { return 10 },
set: function(value) { console.log("Setting `o.bar` to", value) }
}
});
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');

console.log(person.friends); // Shelby,Court,Van,Rob,Barbie
console.log(anotherPerson.friends); // Shelby,Court,Van,Rob,Barbie
console.log(yetAnotherPerson.friends); // Shelby,Court,Van,Rob,Barbie

console.log(anotherPerson.foo); // hello
console.log(yetAnotherPerson.foo); // undefined

console.log(anotherPerson.bar); // undefined
console.log(yetAnotherPerson.bar); // 10
对象关联:

要创建一个合适的关联对象,建议使用 Object.create() 而不是使用具有副作用的 new SuperType()

比较一下两种关联对象的方法:

1
2
3
4
5
// ES6 之前需要抛弃默认的 SubType.prototype
SubType.prototype = Object.create(SuperType.prototype);

// ES6 开始可以直接修改现有的 SubType.prototype
Object.setPrototypeOf(SubType.prototype, SuperType.prototype);
ES5 之前 Object.create()polyfill
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
if (typeof Object.create != 'function') {
// Production steps of ECMA-262, Edition 5, 15.2.3.5
// Reference: http://es5.github.io/#x15.2.3.5
Object.create = (function() {
//为了节省内存,使用一个共享的构造器
function Temp() {}

// 使用 Object.prototype.hasOwnProperty 更安全的引用
var hasOwn = Object.prototype.hasOwnProperty;

return function (O) {
// 1. 如果 O 不是 Object 或 null,抛出一个 TypeError 异常。
if (typeof O != 'object') {
throw TypeError('Object prototype may only be an Object or null');
}

// 2. 使创建的一个新的对象为 obj ,就和通过
// new Object() 表达式创建一个新对象一样,
// Object是标准内置的构造器名
// 3. 设置 obj 的内部属性 [[Prototype]] 为 O。
Temp.prototype = O;
var obj = new Temp();
Temp.prototype = null; // 不要保持一个 O 的杂散引用(a stray reference)...

// 4. 如果存在参数 Properties ,而不是 undefined ,
// 那么就把参数的自身属性添加到 obj 上,就像调用
// 携带obj ,Properties两个参数的标准内置函数
// Object.defineProperties() 一样。
if (arguments.length > 1) {
// Object.defineProperties does ToObject on its first argument.
var Properties = Object(arguments[1]);
for (var prop in Properties) {
if (hasOwn.call(Properties, prop)) {
obj[prop] = Properties[prop];
}
}
}

// 5. 返回 obj
return obj;
};
})();
}

5、寄生式继承

寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回该对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createAnother (original) {
// 任何能返回新对象的函数都适合用于此模式
var clone = Object.create(original);
clone.sayHi = function () {
console.log("Hi");
};

return clone;
}

var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi();
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
// 传统的 JavaScript 类 Vehicle
function Vehicle() {
this.engines = 1;
}
Vehicle.prototype.ignition = function () {
console.log('Turning on my engine.');
};
Vehicle.prototype.drive = function () {
this.ignition();
console.log('Steering and moving forward!');
};

// 寄生类 Car
function Car() {
var car = new Vehicle();
car.wheels = 4;

var vehDrive = car.drive; // 把父类的方法存一个副本

car.drive = function () {
vehDrive.call(this); // 显式调用父类方法,实现显式多态
console.log('Rolling on all ' + this.wheels + ' wheels');
};

return car;
}

var myCar = Car(); // 这里没有必要使用 new Car() 来创建一个新对象。
myCar.drive();

6、寄生组合式继承

组合式继承中两次调用父类构造函数的不足

1,父类中的所有属性都会成为原型属性继承给子类。

2,在子类不得不调用父类构造函数,利用屏蔽属性的原理来重写非原型属性。

寄生组合式继承解决了上述问题。

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
function inheritsPrototype (subType, superType) {
var prototype = Object.create(superType.prototype, {
'constructor': {
value: subType,
writable: true,
configurable: true,
enumerable: false
}
});

subType.prototype = prototype;
}

function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
};

function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritsPrototype(SubType, SuperType);

SubType.prototype.sayAge = function () {
console.log(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.sayAge(); // 29
instance1.colors.push('black');
console.log(instance1.colors); // ["red", "blue", "green", "black"]

var instance2 = new SubType("Greg", 27);
instance2.sayAge(); // 27
console.log(instance2.colors); // ["red", "blue", "green"]

JavaScript 中的差异化继承

差异化继承是基于原型编程的一个常见模型,它讲的是大部分的对象是从其他更一般的对象中派生而来的的,只是在一些很小的地方进行了修改。每个对象维护一个指向它们的 prototype 的引用和一个差异化属性列表

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
Object.prototype.clone = function(){
// 创建一个新的对象,并用 this 作为它的原型
var p = Object.create(this);

/* 并不是必须的:
// 在新对象上应用构造函数
this.constructor.apply(p, arguments);
*/

return p;
};

var root = {}; // 可以是拥有任意原型的任意对象

var record = root.clone();
record.toString = function(){ return "a Record"; };
console.log(record.toString()); // a Record


var person = root.clone();
person.firstName = false;
person.lastName = false;
person.toString = function(){
return this.firstName ?
(this.lastName ?
this.firstName + " " +this.lastName :
this.firstName) :
(this.lastName ?
this.lastName :
"a Person");
};
console.log(person.toString()); // a Person

JoePerson = person.clone();
JoePerson.firstName = "Joe";
console.log(JoePerson.toString()); // Joe

三、多态

相对多态与显式多态

相对多态是不指定想要访问的绝对继承层次,而是使用相对的“查找上一层”,参考 ES6 中的 super

ES6 之前,并没有相对多态的机制,需要通过名称显式指定父类并调用它的方法,称之为显式多态

关联性

多态并不表示子类和父类有关联,子类得到的只是父类的一个实例,子类对继承到的一个方法进行重写,并不会影响到父类中的方法。

四、混入(多重继承)

混入是 JavaScript 中实现多重继承的一种方式。

显式混入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function mixin (sourceObj, targetObj) {
for(var key in sourceObj) {
if(!(key in targetObj)) { // 只会在不存在的情况下才会复制
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}

function MyClass() {
SuperClass.call(this);
OtherSuperClass.call(this);
}

MyClass.prototype = Object.create(SuperClass.prototype); //inherit
mixin(MyClass.prototype, OtherSuperClass.prototype); //mixin

MyClass.prototype.myMethod = function() {
// do a thing
};

实际上,在显式混入完成之后,两者之间仍然会有一些方法能影响到对方,例如引用同一个数组。

JavaScript 中只能复制对共享对象(包括了函数,数组)的引用,修改了父类的共享对象,会影响子类。

隐式混入

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
var Something = {
cool: function () {
this.greeting = 'Hello World';
this.count = this.count ? this.count+1 : 1;
this.colors = ['red', 'blue', 'green'];
}
};
Something.cool();
console.log(Something.greeting); // "Hello World"
console.log(Something.count); // 1
console.log(Something.colors); //

var Another1 = {
cool: function () {
Something.cool.call(this); // 隐式把 Something 混入 Another1
}
};
Another1.cool();
console.log(Another1.greeting); // "Hello World"
console.log(Another1.count); // 1
Another1.colors.push('black');
console.log(Another1.colors); // ["red", "blue", "green", "black"]

var Another2 = {
cool: function () {
Something.cool.call(this); // 隐式把 Something 混入 Another2
}
};
Another2.cool();
console.log(Another2.greeting); // "Hello World"
console.log(Another2.count); // 1
console.log(Another2.colors); // ["red", "blue", "green"]

隐式混入是在子类的构造函数或者方法中调用了父类的方法,实现了把父类中的行为混入到子类中。

最后

文中提到的,只是便于理解和区别与其他函数。
它不同于面向对象中的类,它在实例化和继承中不能被完全复制。

多态看起来似乎是从子类引用父类,但本质上引用的其实是复制的结果。

参考

An easy way to understand JavaScript’s prototypal inheritance

Prototypes as classes – an introduction to JavaScript inheritance