JavaScript继承
##概况 在《Object Oriented JavaScript》提及了12种javascript的继承方式的变化(12种,感觉有点多吧).
JavaScript中并没有类,function在JavaScript中的作用只是作为一个构造函数,不过我们后面都暂且把构造函数叫做类。我们认为一个实例的属性依赖于其构造函数提供的属性配置,以及构造函数的原型(prototype)的属性。
要做到继承就要先利用好这两个因素。
##从简单的例子开始
先声明一个Animal构造函数,用于创建一个动物的实例。
function Animal() {
this.name = "Animal";
this.color = "";
this.legsNumber = 4;
}
// 在原型链上声明一个shout方法
Animal.prototype.shout = function() {
this.name && alert("I am a " + this.name);
this.color && alert("My Color is " + this.color);
this.legsNumber && alert("I Have " + this.legsNumber + " legs");
};
然后我们从Animal类衍生出一类Cat的猫科动物类 通常我们会怎么写呢?由于JavaScript是原型继承的,我们会把新的构造函数的prototype指向由 父类 的创建的一个实例。
function WhiteCat() {
this.name = "Cat";
this.color = "white";
}
WhiteCat.prototype = new Animal();
我们生成一个WhiteCat实例看看,可以发现这时候他有了一个shout方法。
看了这一段代码可能会让人觉得很弱…不过至少这里WhiteCat继承了Animal类的shout方法。总之,这就是最基础的继承。
###注意点
WhiteCat.prototype = new Animal();
注意在这句代码中,我们已经将WhiteCat的原型完全重写了,原本WhiteCat.prototype.constructor是指向构造器本身的,经过重写这个链就断掉了,我们可以通过手写的方式补回这个链
WhiteCat.prototype.constructor = WhiteCat;
##再抽象一些 上一个例子比较具体地展示了一个类继承于另一个类的过程。
我们把它抽象一下,编写一个extend函数专门处理这个继承过程,这个函数接受两个参数,子类和父类。
var extend = function(Child, Parent) {
Child.prototype = new Parent();
Child.prototype.constructor = Child;
};
使用这个函数就可以实现类之间的继承。
function Animal() {
this.name = "Animal";
this.color = "";
this.legsNumber = 4;
}
Animal.prototype.shout = function() {…}
function WhiteCat() {
this.name = 'whitecat';
}
extend(WhiteCat,Animal);
##只继承prototype部分
前面的继承我们把父类的实例属性和原型属性都继承了过来。
如何只继承原型的部分,很简单。
var extendPrototype = fucntion(Child, Parent) {
Child.prototype = Parent.prototype;
Child.prototype.constructor = Child;
}
再试试Cat继承Animal的过程,可以发现Animal的legsNumber属性是没有继承过来的。
##使用new F() 不知道有没有看出来上面的继承过程有一个问题,我们把子类的原型指向父类的原型,他俩公用同一个原型对象,一旦我们更改了子类原型上面的某一方法,父类也会受到影响。
因此我们要做一些调整,使用一个空的构造器来隔离开他们两个。
var extendPrototype2 = function(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}
##如何从子类访问父类 为了在子类中读取父类的方法,我们要手动在子类上设定一个属性指向其父类。
var extendPrototype2 = function(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.super = Parent.prototype;
}
至此我们基本完成了一个最基本的继承。
##换一种方式继承 采用原型链的方式可以实现继承
除了子类继承父类的原型属性,我们还可以把父类原型的属性复制到子类的原型上面。
var extend2 = function(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}
这种方式不同于之前的继承在于,之前如果子类自身如果没有定义一些属性,对应的属性查找就会延伸到父类和父类的原型。
而这种继承直接复制了父类原型的属性(不过如果复制的属性是对象话还是会使用指针的方式,我们会在后面提到深度继承来解决这个问题)。
##继承自对象的对象
从上面的这种继承方式,我们可以衍生出一个简单的继承方式,其实Parent.prototype
和Child.prototype
本质都是对象。上面的继承方式可以直接改造为对象之间的继承。
var extendCopy = function(o) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
##深度属性拷贝 前面的继承存在一种问题,如果在父类的原型上面存在一个对象或者数组型的属性。那么在被子类的原型复制后,修改子类原型的同名属性,父类的原型可能会被修改。 所以我们要做一种原型的深度拷贝,直到拷贝的属性值是基本类型。
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
if (typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {};
deepCopy(p[i], c[i]);
} else {
c[i] = p[i];
}
}
return c;
}
##DC对属性拷贝的建议 我们之前通过拷贝对象属性来达到继承,著名的老道对于里面的子类原型部分的代码,对于对象的创建,他建议使用一个F函数的构造器来代替对象字面量(可见javascript语言精粹第三章),并把其实例作为结果返回。于是他写了这样的一个object函数,其参数作为新构造器的原型。
function object(o){
var F = function(){};
F.prototype = o;
return new F();
}
##使用原型继承和属性拷贝相结合 我们使用继承往往是先继承一个已有的对象,然后会在其基础上面再做一些修改。到代码的层面差不多是先做一次继承,然后再对实例添加一些额外的方法。 这时候把原型继承和属性拷贝结合起来就很有意义。
function objectPlus(o, stuff) {
var F = function(){};
F.prototype = o;
var c = new F();
c.uber = o;
for (var i in stuff) {
c[i] = stuff[i];
}
return c;
}
##多重的继承 我们还可以从多个父对象来继承我们的子对象,multi接受的参数是多个对象。
function multi() {
var c = {},// 或者使用new F()的方式
len = arguments.length,
stuff;
for (var i = 0; i < len; i++) {
stuff = arguments[i];
for (var k in sutff) {
c[k] = stuff[k];
}
}
return c;
}
这种多重的继承还可以用于对象的Mixin。
##借用父类的构造器来进行继承
除了继承父类的方法和属性我们还可以使用父类的构造器来完成子类的实例的构造。
在js中存在call
和apply
两种用于灵活调用函数的方法,借助他们我们就可以直接在子类的实例化过程中去调用父类的构造器,从而完成继承的过程。
我们先用具体的代码来实现这个过程,然后再进行抽象。
还是先声明一个Animal的类。
function Animal(config){
this.name = "Animal";
this.color = "";
this.legsNumber = 4;
}
// 在原型链上声明一个shout方法
Animal.prototype.shout = function() {
this.name && alert("I am a " + this.name);
this.color && alert("My Color is " + this.color);
this.legsNumber && alert("I Have " + this.legsNumber + " legs");
};
再声明我们的子类
function WhiteCat() {
Animal.apply(this);// 这里我们在子类直接调用Animal的构造器
this.name = "Cat";
this.color = "white";
}
WhiteCat.prototype = new Animal();
这样就简单的借用了父构造器来继承。
我们再把这个过程抽象一下
function extendCallParent(Child, Parent) {
Child.prototype = new Parent();
Child.prototype.contructor = Child;
Child.super = Parent;
}
function Animal(config){
this.name = "Animal";
this.color = "";
this.legsNumber = 4;
}
// 在原型链上声明一个shout方法
Animal.prototype.shout = function() {
// ...
};
function WhiteCat() {
WhiteCat.super.constructor.apply(this, arguments);
this.name = "Cat";
this.color = "white";
}
extendCallParent(WhiteCat, Animal);
##借用父构造器的一种改造 上面的继承过程中我们两次调用了父类的构造器。
如果我们只继承父类原型上面的属性的话,可以不做Child.prototype = new Parent()
这一步。
取而代之的是使用原型属性复制的方式。
function extendCallParent(Child, Parent) {
Child.prototype = Parent.prototype;
Child.prototype.contructor = Child;
Child.super = Parent.prototype;
}
##CoffeeScript中的继承 - JavaScript继承的实际应用 CoffeeScript是对JavaScript的语法的一个很好的约束的工具和语言,它定义了一套独立于JavaScript的语法,确保你能安全高效的编写js程序,不至于轻易的犯错。 你可以从CoffeeScript编译出对应的JavaScript语法。CoffeeScript关注的是你写代码的过程,让你编写更简洁明晰的代码。而让解析引擎编译出对应的js脚本。想详细的了解CoffeeScript你可以参考其官网和一些资料
CoffeeScript中的继承语法很简单,不过有一点要注意的是在CoffeeScript中缩进是有含义的,例如下面的这个例子
class Animal
constructor: ([[[[[[[[[[[[[[[[[[[@name](/user/name)](/user/name)](/user/name)](/user/name)](/user/name)](/user/name)](/user/name)](/user/name)](/user/name)](/user/name)](/user/name)](/user/name)](/user/name)](/user/name)](/user/name)](/user/name)](/user/name)](/user/name)](/user/name)) ->
alive: ->
false
class Parrot extends Animal
constructor: ->
super("Parrot")
dead: ->
not [[[[[[[[[[[[[[[[[[[[[@alive](/user/alive)](/user/alive)](/user/alive)](/user/alive)](/user/alive)](/user/alive)](/user/alive)](/user/alive)](/user/alive)](/user/alive)](/user/alive)](/user/alive)](/user/alive)](/user/alive)](/user/alive)](/user/alive)](/user/alive)](/user/alive)](/user/alive)](/user/alive)](/user/alive)()
这是一个从Animal扩展出Parrot类的例子。Animal类具有实例的构造函数和alive方法(返回为false)。然后我们定义了Parrot类并制定它继承自Animal类,我们使得Parrot类的构造函数直接调用父类的构造函数。Parrot实例的dead方法则直接调用的继承来的实例alive方法。
这是一个基本的例子。这一段CoffeeScript会被编译成什么样的JavaScript代码呢?
var Animal, Parrot,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) {
for (var key in parent) {
if (__hasProp.call(parent, key))
child[key] = parent[key];
}
function ctor() {
this.constructor = child;
}
ctor.prototype = parent.prototype; child.prototype = new ctor();
child.__super__ = parent.prototype;
return child;
};
Animal = (function() {
function Animal(name) {
this.name = name;
}
Animal.prototype.alive = function() {
return false;
};
return Animal;
})();
Parrot = (function(_super) {
__extends(Parrot, _super);
function Parrot() {
Parrot.__super__.constructor.call(this, "Parrot");
}
Parrot.prototype.dead = function() {
return !this.alive();
};
return Parrot;
})(Animal);
我们可以看到里面编译出来的__extend
函数。它做的事情就与我们之前说的属性复制加上new F()
的方式类似,不过可以看到子类所复制的属性都是来自于父类本身静态方法。
通常在CoffeeScript中,子类一旦继承于父类,它的实例初始化过程就会调用父类的构造器,当然你也可以重写其构造函数的过程。
上面的例子中我们就用CoffeeScript中的super
方法重写了对父类构造器的调用的过程。
##KISSY的继承方式 在KISSY库也有一个extend继承方法。
看官网的一个例子
var S = KISSY;
function Bird(name) {
this.name = name;
}
Bird.prototype.fly = function() {
alert(this.name + ' is flying now!');
};
function Chicken(name) {
Chicken.superclass.constructor.call(this, name);
}
S.extend(Chicken, Bird,{
fly:function(){
Chicken.superclass.fly.call(this)
alert("it's my turn");
}
});
new Chicken('kissy').fly();
可以直接看到KISSY使用的是调用父类构造器的继承方式。
再看其源码,extend的实现。
其接受4个参数,子类,父类,要覆盖的原型方法对象,要覆盖的静态方法对象
其extend方法中含有这个的一个create方法,它是用来从已有的父类创建一个新的对象,作为子类的原型对象。
var create = Object.create ? function (proto, c) {
return Object.create(proto, {
constructor:{
value:c
}
});
} : function (proto, c) {
function F() {}
F.prototype = proto;
var o = new F();
o.constructor = c;
return o;
}
可以看到,这里使用的方式和我们上面介绍的使用new F()
来继承对象的方式基本一样,并且还使用了Object.create方法做了对新版JavaScript规范的支持。
KISSY的extend方法中还调用了KISSY的mix方法,大家也可以阅读一下其实现。
总的来说,可以发现,KISSY使用的方式和CoffeeScript使用的继承方式还是基本一样的。
##总结
JavaScript的继承主要是源于对原型链的利用,我们可以看到最基本的继承的实现,子类的原型继承于父类的实例。 我们还可以在此基础上面做进一步的扩展,我们可以通过复制属性直接继承父类的原型,当然考虑到安全性,我们会使用深度的复制和使用一个对象来分隔子类和父类之间的联系。另外对于子类的构造器,我们也可以借助父类的构造器来完成其功能。最后,可以看到其实很多的现有库的方案都是对我们最基本的继承方式的一些包装。