关于原型继承的重要性


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