再谈Javascript原型继承

真正意义上来讲Javascript并非一门面向对象的语言,没有提供传统的继承方式,可是它提供了一种原型继承的方式,利用自身提供的原型属性来实现继承。Javascript原型继承是一个被说烂掉了的话题,可是本身对于这个问题一直没有完全理解,今天花了点时间又看了一遍《Javascript模式》中关于原型实现继承的几种方法,下面来一一说明下,在最后我根据本身的理解提出了一个关于继承比较完整的实现,若是你们有不一样意见,欢迎建议。javascript

原型与原型链

说原型继承以前仍是要先说说原型和原型链,毕竟这是实现原型继承的基础。
在Javascript中,每一个函数都有一个原型属性prototype指向自身的原型,而由这个函数建立的对象也有一个__proto__属性指向这个原型,而函数的原型是一个对象,因此这个对象也会有一个__proto__指向本身的原型,这样逐层深刻直到Object对象的原型,这样就造成了原型链。下面这张图很好的解释了Javascript中的原型和原型链的关系。
图片描述java

每一个函数都是Function函数建立的对象,因此每一个函数也有一个__proto__属性指向Function函数的原型。这里须要指出的是,真正造成原型链的是每一个对象的__proto__属性,而不是函数的prototype属性,这是很重要的。app

原型继承

基本模式

var Parent = function(){
    this.name = 'parent' ;
} ;
Parent.prototype.getName = function(){
    return this.name ;
} ;
Parent.prototype.obj = {a : 1} ;

var Child = function(){
    this.name = 'child' ;
} ;
Child.prototype = new Parent() ;

var parent = new Parent() ;
var child = new Child() ;

console.log(parent.getName()) ; //parent
console.log(child.getName()) ; //child

这种是最简单实现原型继承的方法,直接把父类的对象赋值给子类构造函数的原型,这样子类的对象就能够访问到父类以及父类构造函数的prototype中的属性。 这种方法的原型继承图以下:
图片描述函数

这种方法的优势很明显,实现十分简单,不须要任何特殊的操做;同时缺点也很明显,若是子类须要作跟父类构造函数中相同的初始化动做,那么就得在子类构造函数中再重复一遍父类中的操做:this

var Parent = function(name){
    this.name = name || 'parent' ;
} ;
Parent.prototype.getName = function(){
    return this.name ;
} ;
Parent.prototype.obj = {a : 1} ;

var Child = function(name){
    this.name = name || 'child' ;
} ;
Child.prototype = new Parent() ;

var parent = new Parent('myParent') ;
var child = new Child('myChild') ;

console.log(parent.getName()) ; //myParent
console.log(child.getName()) ; //myChild

上面这种状况还只是须要初始化name属性,若是初始化工做不断增长,这种方式是很不方便的。所以就有了下面一种改进的方式。spa

借用构造函数

var Parent = function(name){
    this.name = name || 'parent' ;
} ;
Parent.prototype.getName = function(){
    return this.name ;
} ;
Parent.prototype.obj = {a : 1} ;

var Child = function(name){
    Parent.apply(this,arguments) ;
} ;
Child.prototype = new Parent() ;

var parent = new Parent('myParent') ;
var child = new Child('myChild') ;

console.log(parent.getName()) ; //myParent
console.log(child.getName()) ; //myChild

上面这种方法在子类构造函数中经过apply调用父类的构造函数来进行相同的初始化工做,这样无论父类中作了多少初始化工做,子类也能够执行一样的初始化工做。可是上面这种实现还存在一个问题,父类构造函数被执行了两次,一次是在子类构造函数中,一次在赋值子类原型时,这是不少余的,因此咱们还须要作一个改进:prototype

var Parent = function(name){
    this.name = name || 'parent' ;
} ;
Parent.prototype.getName = function(){
    return this.name ;
} ;
Parent.prototype.obj = {a : 1} ;

var Child = function(name){
    Parent.apply(this,arguments) ;
} ;
Child.prototype = Parent.prototype ;

var parent = new Parent('myParent') ;
var child = new Child('myChild') ;

console.log(parent.getName()) ; //myParent
console.log(child.getName()) ; //myChild

这样咱们就只须要在子类构造函数中执行一次父类的构造函数,同时又能够继承父类原型中的属性,这也比较符合原型的初衷,就是把须要复用的内容放在原型中,咱们也只是继承了原型中可复用的内容。上面这种方式的原型图以下:
图片描述code

临时构造函数模式(圣杯模式)

上面借用构造函数模式最后改进的版本仍是存在问题,它把父类的原型直接赋值给子类的原型,这就会形成一个问题,就是若是对子类的原型作了修改,那么这个修改同时也会影响到父类的原型,进而影响父类对象,这个确定不是你们所但愿看到的。为了解决这个问题就有了临时构造函数模式。对象

var Parent = function(name){
    this.name = name || 'parent' ;
} ;
Parent.prototype.getName = function(){
    return this.name ;
} ;
Parent.prototype.obj = {a : 1} ;

var Child = function(name){
    Parent.apply(this,arguments) ;
} ;
var F = function(){} ;
F.prototype = Parent.prototype ;
Child.prototype = new F() ;

var parent = new Parent('myParent') ;
var child = new Child('myChild') ;

console.log(parent.getName()) ; //myParent
console.log(child.getName()) ; //myChild

该方法的原型继承图以下:
图片描述
很容易能够看出,经过在父类原型和子类原型之间加入一个临时的构造函数F,切断了子类原型和父类原型之间的联系,这样当子类原型作修改时就不会影响到父类原型。继承

个人方法

《Javascript模式》中到圣杯模式就结束了,但是无论上面哪种方法都有一个不容易被发现的问题。你们能够看到我在'Parent'的prototype属性中加入了一个obj对象字面量属性,可是一直都没有用。咱们在圣杯模式的基础上来看看下面这种状况:

var Parent = function(name){
    this.name = name || 'parent' ;
} ;
Parent.prototype.getName = function(){
    return this.name ;
} ;
Parent.prototype.obj = {a : 1} ;

var Child = function(name){
    Parent.apply(this,arguments) ;
} ;
var F = function(){} ;
F.prototype = Parent.prototype ;
Child.prototype = new F() ;

var parent = new Parent('myParent') ;
var child = new Child('myChild') ;

console.log(child.obj.a) ; //1
console.log(parent.obj.a) ; //1
child.obj.a = 2 ;
console.log(child.obj.a) ; //2
console.log(parent.obj.a) ; //2

在上面这种状况中,当我修改child对象obj.a的时候,同时父类的原型中的obj.a也会被修改,这就发生了和共享原型一样的问题。出现这个状况是由于当访问child.obj.a的时候,咱们会沿着原型链一直找到父类的prototype中,而后找到了obj属性,而后对obj.a进行修改。再看看下面这种状况:

var Parent = function(name){
    this.name = name || 'parent' ;
} ;
Parent.prototype.getName = function(){
    return this.name ;
} ;
Parent.prototype.obj = {a : 1} ;

var Child = function(name){
    Parent.apply(this,arguments) ;
} ;
var F = function(){} ;
F.prototype = Parent.prototype ;
Child.prototype = new F() ;

var parent = new Parent('myParent') ;
var child = new Child('myChild') ;

console.log(child.obj.a) ; //1
console.log(parent.obj.a) ; //1
child.obj.a = 2 ;
console.log(child.obj.a) ; //2
console.log(parent.obj.a) ; //2

这里有一个关键的问题,当对象访问原型中的属性时,原型中的属性对于对象来讲是只读的,也就是说child对象能够读取obj对象,可是没法修改原型中obj对象引用,因此当child修改obj的时候并不会对原型中的obj产生影响,它只是在自身对象添加了一个obj属性,覆盖了父类原型中的obj属性。而当child对象修改obj.a时,它先读取了原型中obj的引用,这时候child.objParent.prototype.obj是指向同一个对象的,因此childobj.a的修改会影响到Parent.prototype.obj.a的值,进而影响父类的对象。AngularJS中关于$scope嵌套的继承方式就是模范Javasript中的原型继承来实现的。
根据上面的描述,只要子类对象中访问到的原型跟父类原型是同一个对象,那么就会出现上面这种状况,因此咱们能够对父类原型进行拷贝而后再赋值给子类原型,这样当子类修改原型中的属性时就只是修改父类原型的一个拷贝,并不会影响到父类原型。具体实现以下:

var deepClone = function(source,target){
    source = source || {} ;
    target = target || {};
    var toStr = Object.prototype.toString ,
        arrStr = '[object array]' ;
    for(var i in source){
        if(source.hasOwnProperty(i)){
            var item = source[i] ;
            if(typeof item === 'object'){
                target[i] = (toStr.apply(item).toLowerCase() === arrStr) ? [] : {} ;
                deepClone(item,target[i]) ;    
            }else{
                target[i] = item;
            }
        }
    }
    return target ;
} ;
var Parent = function(name){
    this.name = name || 'parent' ;
} ;
Parent.prototype.getName = function(){
    return this.name ;
} ;
Parent.prototype.obj = {a : '1'} ;

var Child = function(name){
    Parent.apply(this,arguments) ;
} ;
Child.prototype = deepClone(Parent.prototype) ;

var child = new Child('child') ;
var parent = new Parent('parent') ;

console.log(child.obj.a) ; //1
console.log(parent.obj.a) ; //1
child.obj.a = '2' ;
console.log(child.obj.a) ; //2
console.log(parent.obj.a) ; //1

综合上面全部的考虑,Javascript继承的具体实现以下,这里只考虑了Child和Parent都是函数的状况下:

var deepClone = function(source,target){
    source = source || {} ;
    target = target || {};
    var toStr = Object.prototype.toString ,
        arrStr = '[object array]' ;
    for(var i in source){
        if(source.hasOwnProperty(i)){
            var item = source[i] ;
            if(typeof item === 'object'){
                target[i] = (toStr.apply(item).toLowerCase() === arrStr) ? [] : {} ;
                deepClone(item,target[i]) ;    
            }else{
                target[i] = item;
            }
        }
    }
    return target ;
} ;

var extend = function(Parent,Child){
    Child = Child || function(){} ;
    if(Parent === undefined)
        return Child ;
    //借用父类构造函数
    Child = function(){
        Parent.apply(this,argument) ;
    } ;
    //经过深拷贝继承父类原型    
    Child.prototype = deepClone(Parent.prototype) ;
    //重置constructor属性
    Child.prototype.constructor = Child ;
} ;

总结

说了这么多,其实Javascript中实现继承是十分灵活多样的,并无一种最好的方法,须要根据不一样的需求实现不一样方式的继承,最重要的是要理解Javascript中实现继承的原理,也就是原型和原型链的问题,只要理解了这些,本身实现继承就能够游刃有余。

最后,安利下个人我的博客,欢迎访问: http://bin-playground.top

相关文章
相关标签/搜索