Javascript继承机制

在介绍Javascript的继承原理之前,我们先来看下继承方法里需要使用的方法call、apply和new。

call和apply

call方法定义
语法:Function.call([thisObj[,arg1[, arg2[, [,.argN]]]]])
定义:调用一个对象的一个方法,以另一个对象替换当前对象。
说明:
call 方法可以用来代替另一个对象调用一个方法。call方法可将一个函数的对象上下文从初始的上下文改变为由thisObj指定的新对象。如果没有提供 thisObj 参数,那么 Global对象被用作thisObj。
apply方法定义
语法:Function.apply([thisObj[,argArray]])
定义:应用某一对象的一个方法,用另一个对象替换当前对象。
说明:如果没有提供 argArray 和 thisObj任何一个参数,那么 Global 对象将被用作 thisObj,并且无法被传递任何参数。
call方法跟apply方法并没有明显区别,只是传递参数的形式不一样。
call():一个一个的传递参数;
apply():以数组的形式传递。
call模拟方法的实现分为以下几个步骤
1.this参数可以传null,当为null的时候,视为指向window
2.将函数设为对象的属性
3.给定参数并执行该函数
4.删除该函数

1
2
3
4
5
6
7
8
9
10
11
12
Funtion.prototype.call = function(context) {
context = context || window;
context.fn = this;
var args = [];
//eval函数只接受原始字符串作为参数,如果参数不是字符串,那么该方法就会不做任何改变的返回
for (var i = 1, l = arguments.length; i < l; i++) {
args.push('arguments['+ i +']');
}
var result = eval('context.fn('+ args +')');
delete context.fn;
return result;
}

apply模拟方法的实现也是类似的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Function.prototype.apply = function(context, arr) {
context = context || window;
context.fn = this;
var result;
if(!arr) {
result = context.fn();
} else {
var args = [];
for(var i =0, l = arr.length; i < l; i++) {
args.push('arguments['+ i +']');
}
result = eval('context.fn('+ args +')');
}
delete context.fn;
return result;
}

new

new模拟方法的实现
当用new实例化一个构造函数后,我们可以访问到构造函数里的属性以及构造函数原型(prototype)上的属性,所以模拟new的实现分成以下几个步骤
1.创建一个空对象
2.将空对象的原型指向构造函数原型,这样空对象就可以访问到构造函数原型上的属性
3.使用apply,将构造函数this指向改变为新建的对象,这样新建的对象就能访问构造函数上属性
4.如果构造函数中返回的值是对象,则返回这个对象;如果不是,返回新建的对象

1
2
3
4
5
6
7
function newObject() {
var obj = {};
var constructor = Array.prototype.shift.call(arguments);
obj.__proto__ = constructor.prototype;
var result = constructor.apply(obj,arguments);
return typeof result === 'object'? result : obj;
}

寄生组合式继承

在《JavaScript高级程序设计》书中介绍了几种经典的继承方法,但都存在着一些问题。
1.原型链继承:当原型中属性值是个复杂数据类型时,所有的实例都会共享这个数据,其中一个实例修改这个数据其他实例都会受到影响。
2.构造函数继承:方法都在构造函数中定义,每次创建实例都会创建一遍方法。
3.组合继承:最大的缺点是会调用两次父构造函数。一次是设置子类型实例的原型的时候,一次在创建子类型实例的时候。
在高程书的最后介绍了寄生组合式继承,代码实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function object(o) {
function F(){};
F.prototype = o.prototype;
return new F();
}
function inheritPrototype(subType,superType) {
var prototype = object(superType.prototype);
prototype.consturctor = subType;
subType.prototype = prototype;
}
function SuperType(name){
this.name = name;
this.colors = {"red","blue","green"}
}
SuperType.prototype.sayName = function(){
//code
}
function SubType(name,age){
SuperType.call(this,name);
this.age = age;
}
inheritPrototype(SubType,SuperType);
var instance = new SubType();

simple-inheritance库的实现

以下是摘自别人对simple-inheritance的注释。

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
51
(function() {
//initializing用于控制类的初始化,非常巧妙,请留意下文中使用技巧
//fnTest返回一个正则比表达式,用于检测函数中是否含有_super,这样就可以按需重写,提高效率。当然浏览器如果不支持的话就返回一个通用正则表达式,
// /xyz/.test(function() {xyz;})等价于
// /xyz/.test((function() {xyz;}).toString())
var initializing = false,
fnTest = /xyz/.test(function() {
xyz;
}) ? /\b_super\b/ : /.*/;
//所有类的基类Class,这里的this一般是window对象
this.Class = function() {};
//对基类添加extend方法,用于从基类继承
Class.extend = function(prop) {
//保存当前类的原型
var _super = this.prototype;
//创建当前类的对象,用于赋值给子类的prototype,这里非常巧妙的使用父类实例作为子类的原型,而且避免了父类的初始化(通过闭包作用域的initializing控制);
initializing = true;
var prototype = new this();
initializing = false;
//将参数prop中赋值到prototype中,这里的prop中一般是包括init函数和其他函数的对象
for (var name in prop) {
//对应重名函数,需要特殊处理,处理后可以在子函数中使用this._super()调用父类同名构造函数, 这里的fnTest很巧妙:只有子类中含有_super字样时才处理从而提高效率
prototype[name] = typeof prop[name] == "function" && typeof _super[name] == "function" && fnTest.test(prop[name]) ?
(function(name, fn) {
return function() {
//_super在这里是我们的关键字,需要暂时存储一下
var tmp = this._super;
//这里就可以通过this._super调用父类的构造函数了
this._super = _super[name];
//调用子类函数
fn.apply(this, arguments);
//复原_super,如果tmp为空就不需要复原了
tmp && (this._super = tmp);
}
})(name, prop[name]) : prop[name];
}
//当new一个对象时,实际上是调用该类原型上的init方法,注意通过new调用时传递的参数必须和init函数的参数一一对应
function Class() {
if (!initializing && this.init) {
this.init.apply(this, arguments);
}
}
//给子类设置原型
Class.prototype = prototype;
//给子类设置构造函数
Class.prototype.constructor = Class;
//设置子类的extend方法,使得子类也可以通过extend方法被继承
Class.extend = arguments.callee;
return Class;
}
})();

ES6 Class继承

ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。Class的基本语法如下

1
2
3
4
5
6
7
8
9
10
11
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}

类的所有方法都是定义在类的prototype属性上面,类的内部所有定义的方法,都是不可枚举的。Class 可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。

1
2
3
4
5
6
7
8
9
10
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}

定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。在constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this

Javascript实现多继承

如果有火柴人的粉丝相信对dojo不会陌生,在这个库里使用dojo.declare实现里类的定义机制,不仅可以实现如前几节介绍的单继承,还能实现多继承方式。

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
dojo.declare('classX', null,
{
sayX: function(){
console.log("hi, this is X");
},
say: function() {
console.log("hi, welcome!");
}
});
dojo.declare('classY', null,
{
sayY: function(){
console.log("hi, this is Y");
},
say: function() {
console.log("hi, goodbye!");
}
});
dojo.declare('classZ', [classX,classY]);
var objZ = new classZ();
objZ.sayX(); //hi, this is X
objZ.sayY(); //hi, this is Y
objZ.say(); //hi, goodbye!
objZ instanceof classX //true
objZ instanceof Object //true
objZ instanceof classY //flase

从上面代码看到,classZ拥有classX和classY类的全部方法,一般我们会认为classZ继承了classX和classY,那么classZ instanceof classX、classZ instanceof classY应该都是true,然而在JavaScript中只有一个prototype,实际上classZ的父类是classX,classY类似于聚合类,他的属性和方法mixin到classX,达到实现继承的效果。使用ES6 Class简单模拟多继承的实现。

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
const mixinClass = (base, ...mixins) => {
const mixinProps = (dst, src) => {
Object.getOwnPropertyNames(src).forEach(prop => {
if (/^constructor$/.test(prop)) { return; }
Object.defineProperty(dst, prop, Object.getOwnPropertyDescriptor(src, prop));
});
};
let ctor;
if (base && typeof base === 'function') {
ctor = class extends base {
constructor(...props) {
super(...props);
}
};
mixins.forEach(src => {
mixinProps(ctor.prototype, src.prototype);
});
} else {
ctor = class {};
}
return ctor;
}
class A {
methodA() {
console.log('methodA');
}
}
class B {
methodB() {
console.log('methodB');
}
}
class C extends mixinClass(A, B) {
methodA() {
console.log('methodA in C');
}
methodC() {
console.log('methodC');
}
}
let classC = new C();
classC.methodA(); //methodA in C
classC.methodB(); //methodB
classC instanceof C; //true
classC instanceof A; //true
classC instanceof B; //false