關於原型繼承的重要性


http://aaditmshah.github.io/why-prototypal-inheritance-matters/

https://segmentfault.com/a/1190000002596600

看到一篇外國博客,講關於原型繼承的重要性。

類繼承的問題

文中說使用new關鍵字和構造函數的方式來實現對象實例化的缺點就是破壞了js的函數式特點,因為new是一個關鍵字而不是一個函數,這樣使得函數式無法與對象實例化一起使用。

但是可以自己實現一個函數new。

function Person(firstname,lastname){
    this.firstname = firstname ;
    this.lastname = lastname ;
}

var author = new Person('Aadit','Shah') ;

這是普通方式,使用new關鍵字調用Person構造函數來創建一個實例。

但是沒有方法可以使用apply來指定參數列表:

var author = new Person.apply(null,['Aadit','Shah']);//error

但是如果自己實現一個函數new就可以達到想要的效果:

function Person(firstname,lastname){
    this.firstname = firstname ;
    this.lastname = lastname ;
}

Function.prototype.new = function () {
    function functor() { return constructor.apply(this, args); }
    var args = Array.prototype.slice.call(arguments);
    functor.prototype = this.prototype;
    var constructor = this;
    return new functor;
};

var author = Person.new.apply(Person,['Aadit','Shah']) ;

這樣子就達到了使用函數式來實現了對象的實例化。

首先在Function的原型上添加了這個new方法,因此Person構造函數就可以調用new方法了。在最后一句,var author = Person.new.apply(Person, ['Aadit', 'Shah']),調用new方法的時候從Person身上調用,Person本身沒有new屬性,於是通過原型鏈找到了原生的Function上的new方法,就是之前定義好的。在調用new的時候,使用apply指定了new方法的this,將其指定為Person構造函數,並且傳了一個參數數組。

接下來進入new內部,先定義一個functor構造函數供后面調用。接着,args用來存傳進來的數組參數的副本,Array.prototype.slice.call(arguments)的寫法可以復制函數的參數列表將其變成一個新的數組。然后將functor的prototype指定為Person.prototype,因為此時new中的this被指定為Person了。將this指向的Person存在constructor變量里。最后返回了functor的實例化。這時候就去看functor的定義,它指定了return。它其實就等價於這樣:

function functor() { return Person.apply(this, args); }

return new funcor ;做了這樣幾件事:

創建一個對象,使this指向這個對象,然后使用apply在剛才新創建的對象上調用Person構造函數,也就是指定調用Person時的this為剛才的新對象,並且指定參數數組,這個數組會因為apply的作用自動變成一個一個的參數。

一句話解釋自定義的new就是新建一個functor ,讓functor的prototype和Person的prototype一樣,然后在它內部apply調用Person以傳遞數組參數,作用是添加屬性,相當於functor是Person的一個副本。

也就是說先apply調用自定義new方法,接着在里面創建Person 的副本functor ,然后在它里面再apply調用Person。

最后的結果就是生成了Person的實例化對象,使用了函數式的方法,可以直接給構造函數傳遞數組而不是一個一個的參數。

理解原型繼承

創建一個對象有兩種方法:無中生有法和克隆現有對象法

var object = Object.create(null) ;//無中生有

var rectangle = {
    area : function(){
        return this.width * this.height ;
    }
} ;
var rect = Object.create(rectangle) ;//克隆現有對象

其中無中生有Object.create(null)這樣創建出的對象是沒有屬性的。

而克隆現有對象的寫法,其實現有對象就是提供作為新對象的__proto__,也就是prototype。所以rect繼承自rectangle。

而rectangle使用了對象字面量的寫法,這樣的寫法其實是繼承自原生Object。其實對象字面量等價於下面:

var rectangle = Object.create(Object.prototype) ;
rectangle.area = function(){
    return this.width * this.height ;
} ;

因此這條原型鏈就是

rect.__proto__ => rectangle
rectangle.__proto__ => Object.prototype

原型鏈就是用內部指針__proto__連起來的。

構造函數vs原型

下面是正常構造模式和原型配合使用:

function Rectangle(width, height) {
    this.height = height;
    this.width = width;
} ;

Rectangle.prototype.area = function () {
    return this.width * this.height;
};

var rect = new Rectangle(5, 10);
 
alert(rect.area());

下面使用原型模式:

var rectangle = {
    create : function(width,height){
        var self = Object.create(this) ;
        self.height = height ;
        self.width = width ;
        return self ;
    } ,
    area : function(){
        return this.width * this.height ;
    }
} ;
var rect = rectangle.create(5,10) ;
alert(rect.area()) ;

上面兩種寫法最終使用的時候是等價的。但是原型模式的寫法不用使用new關鍵字,完全是函數式寫法,不會出現忘記使用new關鍵字后污染全局變量的問題。因為它使用了Object.create()來完成實例化。

如果分別用兩種方式實現繼承,寫法也是不一樣的:

下面是原型模式來實現繼承:

var rectangle = {
    create : function(width,height){
        var self = Object.create(this) ;
        self.height = height ;
        self.width = width ;
        return self ;
    } ,
    area : function(){
        return this.width * this.height ;
    }
} ;

var square = Object.create(rectangle); //創建rectangle的實例存為square
square.create = function (side) {
    return rectangle.create.call(this, side, side); //創建square的實例
} ;
var sq = square.create(5) ;
alert(sq.area()) ;

這樣的原型鏈就是

sq.__proto__ => square
square.__proto__ => rectangle

下面是構造函數模式的寫法實現繼承:

function Rectangle(width, height) {
    this.height = height;
    this.width = width;
} ;

Rectangle.prototype.area = function () {
    return this.width * this.height;
};

function Square(side){
    Rectangle.call(this,side,side) ;
} ;

Square.prototype = Object.create(Rectangle.prototype) ;

Square.prototype.constructor = Square ;

var sq = new Square(5) ;

alert(sq.area()) ;

可以看出,這就是經典的組合繼承寫法。Square的prototype手動設置為Rectangle的一個實例,並且手動添加constructor指針指向對應的構造函數。

 然而這樣的組合繼承寫法還是和經典的寫法有些區別,因為經典寫法在設置Square.prototype的時候是new了一個Rectangle,這使得Square.prototype是Rectangle的一個實例,然而其身上還有了height和width屬性,而Object.create(Rectangle.Prototype)所創建的實例是不會執行Rectangle構造函數中的初始化代碼的

看下面這個例子,就可以看出new和Object.create()的區別:

function Constructor(){}
o = new Constructor();
// 上面的一句就相當於:
o = Object.create(Constructor.prototype);
// 當然,如果在Constructor函數中有一些初始化代碼,Object.create不能執行那些代碼

Object.create()不會執行構造函數中的初始化代碼,也就不會給原型鏈中的Square.prototype上添加額外的無用的屬性。

創建對象和擴展相結合

上面的例子中,先用Object.create()創建rectangle的克隆也就是square,然后再在square的身上重寫create方法來覆蓋rectangle上的同名方法,如果把這兩個操作合並成一個方法,寫起來就更加簡潔。

就像使用對象字面量是用來創建Object.prototype的克隆然后用新的屬性擴展它。這個操作是extend。

Object.prototype.extend = function(extension){
    var hasOwnProperty = Object.hasOwnProperty ;
    var object = Object.create(this) ;
    
    for(var property in extension){
        if(hasOwnProperty.call(extension,property) ||
            typeof object[property] === 'undefined')
            //這段代碼有問題,按照文章意思,這里應該使用深復制,而不是簡單的淺復制,deepClone(extension[property],object[property])
            object[property] = extension[property] ;
    }
    return object ;
} ;

這其中if判斷里的代碼,首先判斷extension屬性是否是對象自身的,如果是就直接復制到object上,否則再判斷object上是否有這個屬性,如果沒有那么也會把屬性復制到object上,這種實現的結果就使得被擴展的對象不僅僅只擴展了extension中的屬性,還包括了extension原型中的屬性。不難理解,extension原型中的屬性會在extension中表現出來,所以它們也應該作為extension所具有的特性而被用來擴展object。

注意:in操作符可以訪問到對象自身的屬性,也可以訪問到對象原型上的屬性。比如給上面的extension的__proto__上添加的屬性就會被in遍歷出來。

如果按照譯者的意思,正常extend的實現應該是可以配置當被擴展對象和用來擴展的對象屬性重復時是否覆蓋原有屬性,使用深復制來改寫的話,就如下:

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 ;
} ;


 Object.prototype.extend = function(extension,override){
    var hasOwnProperty = Object.hasOwnProperty ;
    var object = Object.create(this) ;
    for(var property in extension){
        if(hasOwnProperty.call(extension,property) || 
            typeof object[property] === 'undefined'){
            if(object[property] !== 'undefined'){
                if(override){
                    deepClone(extension[property],object[property]) ;
                }
            }else{
                deepClone(extension[property],object[property]) ;
            }    
        }
    }
}; 

可以看到,譯者實現的就多加了一個判斷,如果extension的屬性和object從原型繼承來的屬性重復了的話,通過傳入的第二個參數來判定是否需要覆蓋掉原型上的這個屬性。淺復制復制也改成了深復制。

使用此extend方法,重寫square:

Object.prototype.extend = function(extension){
    var hasOwnProperty = Object.hasOwnProperty ;
    var object = Object.create(this) ;
    
    for(var property in extension){
        if(hasOwnProperty.call(extension,property) ||
            typeof object[property] === 'undefined')
            //這段代碼有問題,按照文章意思,這里應該使用深復制,而不是簡單的淺復制,deepClone(extension[property],object[property])
            object[property] = extension[property] ;
    }
    return object ;
} ;

var rectangle = {
    create : function(width,height){
        var self = Object.create(this) ;
        self.height = height ;
        self.width = width ;
        return self ;
    } ,
    area : function(){
        return this.width * this.height ;
    }
} ;

var square = rectangle.extend({
    create : function(side){
        return rectangle.create.call(this,side,side) ;
    }
}) ;

var sq = square.create(5) ;
alert(sq.area()) ;

然后也可以用extend將rectangle重寫:

Object.prototype.extend = function(extension){
    var hasOwnProperty = Object.hasOwnProperty ;
    var object = Object.create(this) ;
    
    for(var property in extension){
        if(hasOwnProperty.call(extension,property) ||
            typeof object[property] === 'undefined')
            //這段代碼有問題,按照文章意思,這里應該使用深復制,而不是簡單的淺復制
            object[property] = extension[property] ;
    }
    return object ;
} ;

var rectangle = {
    create : function(width,height){
        return this.extend({
            height : height ,
            width : width
        }) ;
    },
    area : function(){
        return this.width * this.height ;
    }
} ;

var square = rectangle.create(5, 5);

var sq = square.create(5, 5);

alert(sq.area())

最后看一下原型鏈是怎么連起來的:

sq.__proto__ == square
//true
square.__proto__ == rectangle
//true

extend方法是原型繼承中唯一需要的操作。它是Object.create函數的超集,因此它可以用在對象的創建和擴展上。

原型繼承的兩種方法

上面自己定義的extend方法將Object.create()創建克隆和重寫原型上的方法兩個操作結合到了一起,其實extend方法創建出來的對象其實是繼承了兩個對象的屬性。第一個就是被擴展的對象,也就是原型上的屬性,上面的例子也就是作為square原型的rectangle;第二個就是用來擴展的對象,也就是給extend方法傳進去用來重寫create方法的對象,參數extension。

第一種情況下是通過委派來繼承屬性(也就是使用Object.create()來繼承屬性),第二種情況下使用合並屬性的方式來繼承屬性。

委派(差異化繼承)

Javascript中的原型繼承是基於差異化繼承的。每個對象都有個內部指針叫做[[proto]] (在大部分瀏覽器中可以通過__proto__屬性訪問),這個指針指向對象的原型。多個對象之間通過內部[[proto]]屬性鏈接起來形成了原型鏈,鏈的最后指向null。

當你試圖獲取一個對象的屬性時Javascript引擎會首先查找對象自身的屬性。如果在對象上沒找到該屬性,那么它就會去對象的原型中去查找。以此類推,它會沿着原型鏈一直查找知道找到或者到原型鏈的末尾。

function get(object,property){
    if(!Object.hasOwnProperty.call(object,property)){
        var prototype = Object.getPrototypeOf(object) ;
        if(prototype) return get(prototype,property) ;
    }else{
        return object[property] ;
    }
} ;

Javascript中屬性查找的過程就像上面的程序那樣。

克隆(合並式繼承)

克隆其實就是復制一個對象的屬性到另一個對象上。

從多個原型中繼承

上面的合並式繼承使得原型繼承比Java中的類繼承更強大並且與C++中的類繼承一樣強大。為了實現多重繼承,你只需要修改extend方法來從多個原型中復制屬性。

Object.prototype.extend = function(){
    var hasOwnProperty = Object.hasOwnProperty ;
    var object = Object.create(this) ;
    var length = arguments.length ;
    var index = length ;
    
    while(index){
        var extension = arguments[length - (index--)] ;
        for(var property in extension){
            if(hasOwnProperty.call(extension,property)||
                typeof object[property] === 'undefined'){
                //這里同樣應該使用深復制
                object[property] = extension[property] ;
            }
        }
    }
    return object;
} ;

多重繼承是非常有用的因為它提高了代碼的可重用性和模塊化。對象通過委派繼承一個原型對象然后通過合並繼承其他屬性。比如說你有一個事件發射器的原型,像下面這樣:

var eventEmitter = {
    on : function(event,listener){
        if(typeof this[event] !== 'undefined')
            this[event].push(listener) ;
        else
            this[event] = [listener] ;
    } ,
    emit : function(event){
        if(typeof this[event] !== 'undefined'){
            var listeners = this[event] ;
            var length = listeners.length,index = length ;
            var args = Array.prototype.slice.call(arguments,1) ;
            
            while(index){
                var listener = listeners[length - (index--)] ;
                listener.apply(this,args) ;
            }
        }
    }
} ;

現在你希望square表現得像一個事件發射器。因為square已經通過委派的方式繼承了rectangle,所以它必須通過合並的方式繼承eventEmitter。這個修改可以很容易地通過使用extend方法實現:

var rectangle = {
    create : function(width,height){
        var self = Object.create(this) ;
        self.height = height ;
        self.width = width ;
        return self ;
    } ,
    area : function(){
        return this.width * this.height ;
    }
} ;

Object.prototype.extend = function(){
    var hasOwnProperty = Object.hasOwnProperty ;
    var object = Object.create(this) ;
    var length = arguments.length ;
    var index = length ;
    
    while(index){
        var extension = arguments[length - (index--)] ;
        for(var property in extension){
            if(hasOwnProperty.call(extension,property)||
                typeof object[property] === 'undefined'){
                //這里同樣應該使用深復制
                object[property] = extension[property] ;
            }
        }
    }
    return object;
} ;

var eventEmitter = {
    on : function(event,listener){
        if(typeof this[event] !== 'undefined')
            this[event].push(listener) ;
        else
            this[event] = [listener] ;
    } ,
    emit : function(event){
        if(typeof this[event] !== 'undefined'){
            var listeners = this[event] ;
            var length = listeners.length,index = length ;
            var args = Array.prototype.slice.call(arguments,1) ;
            
            while(index){
                var listener = listeners[length - (index--)] ;
                listener.apply(this,args) ;
            }
        }
    }
} ;

var square = rectangle.extend(eventEmitter,{
    create : function(side){
        return rectangle.create.call(this,side,side) ;
    } ,
    resize : function(newSize){
        var oldSize = this.width ;
        this.width = this.height = newSize ;
        this.emit('resizeLog',oldSize,newSize) ;
    }
}) ;
var sq = square.create(5) ;
sq.on('resizeLog',function(oldSize,newSize){
    alert('sq resized from ' + oldSize + 'to' + newSize + '.') ;
}) ;

sq.resize(10) ;
alert(sq.area()) ;

這個譯者肯定沒有運行這里的代碼,因為這里的代碼報錯了!

只因為發射器里的on方法在對象已經有event屬性的時候,直接給對象的event屬性push進去回調函數,也就是這一句:this[event].push(listener) ;

這里明顯有問題,於是我把resize方法里的this.emit('resize', oldSize, newSize);改成了this.emit('resizeLog', oldSize, newSize);

並且把

sq.on('resize',function(oldSize,newSize){
    alert('sq resized from ' + oldSize + 'to' + newSize + '.') ;
}) ;

改成了

sq.on('resizeLog',function(oldSize,newSize){
    alert('sq resized from ' + oldSize + 'to' + newSize + '.') ;
}) ;

就是給resize換了個名字resizeLog。然后就不報錯了,這里不知為何原作者要這么寫,因為同名的resize屬性明明是對象,他使用push肯定會報錯。

Mixin的藍圖(Buleprint)

在上面的例子中你肯定注意到eventEmitter原型並沒有一個create方法。這是因為你不應該直接創建一個eventEmitter對象。相反eventEmitter是用來作為其他原型的原型。這類原型稱為mixin。它們等價於抽象類。mixin用來通過提供一系列可重用的方法來擴展對象的功能。

然而有時候mixin需要私有的狀態。例如eventEmitter如果能夠把它的事件監聽者列表放在私有變量中而不是放在this對象上會安全得多。但是mixin沒有create方法來封裝私有狀態。因此我們需要為mixin創建一個藍圖(blueprint)來創建閉包。藍圖(blueprint)看起來會像是構造函數但是它們並不用像構造函數那樣使用。例如:

function eventEmitter(){
    var evnets = Object.create(null) ;
    
    this.on = function(event,listener){
        if(typeof events[event] !== 'undefined')
            events[event].push(listener) ;
        else
            events[event] = [listener] ;
    } ;
    this.emit = function(event){
        if(typeof events[event] !== 'undefined'){
            var listeners = events[event] ;
            var length = listeners.length ,index = length ;
            var args = Array.prototype.slice.call(arguments,1) ;
        }
    } ;
} ;

一個藍圖用來在一個對象創建之后通過合並來擴展它(我覺得有點像裝飾者模式)。Eric Elliot把它們叫做閉包原型。我們可以使用藍圖版本的eventEmitter來重寫square的代碼,如下:

var rectangle = {
    create : function(width,height){
        var self = Object.create(this) ;
        self.height = height ;
        self.width = width ;
        return self ;
    } ,
    area : function(){
        return this.width * this.height ;
    }
} ;

Object.prototype.extend = function(){
    var hasOwnProperty = Object.hasOwnProperty ;
    var object = Object.create(this) ;
    var length = arguments.length ;
    var index = length ;
    
    while(index){
        var extension = arguments[length - (index--)] ;
        for(var property in extension){
            if(hasOwnProperty.call(extension,property)||
                typeof object[property] === 'undefined'){
                //這里同樣應該使用深復制
                object[property] = extension[property] ;
            }
        }
    }
    return object;
} ;

var square = rectangle.extend({
    create : function(side){
        var self = rectangle.create.call(this,side,side) ;
        eventEmitter.call(self) ;
        return self ;
    } ,
    resize : function(newSize){
        var oldSize = this.width ;
        this.width = this.height = newSize ;
        this.emit('resizeLog',oldSize,newSize) ;
    }
}) ;
var sq = square.create(5) ;

sq.on('resizeLog',function(oldSize,newSize){
    alert('sq resized from ' + oldSize + 'to' + newSize + '.') ;
}) ;

sq.resize(10) ;

alert(sq.area()) ;

這里的on操作里的resize還有emit里的resize也會報錯,我都更換了名字。

修復instanceof操作

instanceof操作可以像下面這樣實現:

Object.prototype.instanceof = function(prototype){
    var object = this ;
    do{
        if(object === prototype) return true ;
        var object = Object.getPrototypeOf(object) ;
    }while(object) ;
    return false ;
}

sq.instanceof(square) ;

這個instanceof方法現在可以被用來測試一個對象是否是通過委派從一個原型繼承的。

然而還是沒有辦法判斷一個對象是否是通過合並的方式從一個原型繼承的,因為實例的關聯信息丟失了。為了解決這個問題我們將一個原型的所有克隆的引用保存在原型自身中,然后使用這個信息來判斷一個對象是否是一個原型的實例。這個可以通過修改extend方法來實現:

Object.prototype.extend = function(){
    var hasOwnProperty = Object.hasOwnProperty ; 
    var object = Object.create(this) ;
    var length = arguments.lenght ;
    var index = length ;

    while(index){
        var extension = arguments[length - (index--)] ;

        for(var property in extension){
            if(property !== 'clones' &&
                hasOwnProperty.call(extension,property) ||
                typeof object[property] === 'undefined')
                object[property] = extension[property] ;

        if(hasOwnProperty.call(extension,'clones')})
            extension.clones.unshift(object) ;
        else
            extension.clones = [object] ;
        }
    }
    return object;
} ;

它其實就是給用來擴展的對象,比如eventEmitter添加了一個clones屬性,存下每一次用它擴展原型的時候生成的實例名字,這樣用相應的instanceof就可以判斷了。

Object.prototype.instanceof = function(prototype){
    if (Object.hasOwnProperty.call(prototype, "clones"))
        var clones = prototype.clones;
    var object = this;
    
    do {
        if (object === prototype ||
            clones && clones.indexOf(object) >= 0)
            return true;
        var object = Object.getPrototypeOf(o  bject);
    } while (object);

    return false;
} ;

在上面的程序中instanceof會返回true如果使用mixin版本的eventEmitter。然而如果使用藍圖版本的eventEmitter它會返回false。為了解決這個問題我創建了一個藍圖函數,這個函數接收一個藍圖作為參數,向它添加一個clones屬性然后返回一個記錄了它的克隆的新藍圖:

function blueprint(f){
    var g = function(){
        f.apply(this,arguments) ;
        g.clones.unshift(this) ;
    } ;
    g.clones = [] ;
    return g ;
} ;
var eventEmitter = blueprint(function(){
    var events = Object.create(null);
    this.on = function (event, listener) {
        if (typeof events[event] !== "undefined")
            events[event].push(listener);
        else events[event] = [listener];
    };

    this.emit = function (event) {
        if (typeof events[event] !== "undefined") {
            var listeners = events[event];
            var length = listeners.length, index = length;
            var args = Array.prototype.slice.call(arguments, 1);

            while (index) {
                var listener = listeners[length - (index--)];
                listener.apply(this, args);
            }
        }
    };
}) ;

向原型發送變化

上面例子中的clones屬性有雙重作用。它可以用來判斷一個對象是否是通過合並繼承自一個原型的,然后他可以用來發送原型改變給所有它的克隆。

Object.prototype.define = function (property, value) {
    this[property] = value;

    if (Object.hasOwnProperty.call(this, "clones")) {
        var clones = this.clones;
        var length = clones.length;

        while (length) {
            var clone = clones[--length];
            if (typeof clone[property] === "undefined")
                clone.define(property, value);
        }
    }
};

現在我們可以修改原型然后這個修改會反映在所有的克隆上。例如我們可以創建創建一個別名addEventListener針對eventEmitter上的on方法:

var square = rectangle.extend(eventEmitter, {
    create: function (side) {
        return rectangle.create.call(this, side, side);
    },
    resize: function (newSize) {
        var oldSize = this.width;
        this.width = this.height = newSize;
        this.emit("resizeLog", oldSize, newSize);
    }
});

var sq = square.create(5);

eventEmitter.define("addEventListener", eventEmitter.on);

sq.addEventListener("resizeLog", function (oldSize, newSize) {
    alert("sq resized from " + oldSize + " to " + newSize + ".");
});

sq.resize(10);
 
alert(sq.area());

藍圖需要特別注意。盡管對於藍圖的修改會被發送到它的克隆,但是藍圖的新的克隆並不會反映這些修改。幸運的是這個問題的解決方法很簡單。我們只需要對blueprint方法進行小小的修改,然后任何對於藍圖的修改就會反映在克隆上了。

function blueprint(f) {
    var g = function () {
        f.apply(this, arguments);
        g.clones.unshift(this);

        var hasOwnProperty = Object.hasOwnProperty;

        for (var property in g)
            if (property !== "clones" &&
                hasOwnProperty.call(g, property))
                    this[property] = g[property];
    };

    g.clones = [];

    return g;
};

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM