Base

可通过添加微信公共帐号icodekata,或者微博帐号姜志辉iS与我讨论

Base.extend

John Resing 曾经提到过一个Base库,由Dean Edwards开发。

让我们先来体验一下Base的基本用例:

var Person = Base.extend({
    constructor: function(name) {
        this.name = name;
    },
    get_name: function() {
        return this.name;
    }
});


describe('Base', function() {
    it('extend', function() {
        var jobs = new Person('jobs');
        assert.equal('jobs', jobs.get_name());
    });
});

new Person

var jobs = new Person('jobs')。使用new构建对象,那么意味着Person必然是一个函数。其大概形式应该如var Person = function(){}的样子。因此,Base.extend返回的应该是一个function函数。

var Base = {};
Base.extend = function() {
    return function() {
        //...
    };
};

constructor

constructor的本意是作为构造函数来调用的。当使用new Person('jobs')时,会自动调用constructor的方法。所以我们需要在返加的function函数里调用constructor:

return function() {
    if (typeof conf['constructor'] !== 'undefined') {
        conf['constructor'].apply(this, arguments);
    }
};

这里涉及了三个知识点:

  • Javascript的反射操作符typeof可以用来试着检索对象的属性
  • Javascript的对象本身即是属性的容器,其中每个属性都拥有名字和值。所以可以使用for检索对象的属性,也可以使用[]获取属性的值。
  • apply方法让我们构建一个参数数组传递给调用函数。

get_name

jobs.get_name()应该是从extend()的参数继承而来的。那最简单的方法是将参数对象的方法复制给Person的实例。如我们之前所述Javascript的对象本身即是属性的容器。所以我们只需将参数的属性复制给Person的实例即可:

return function() {
    if (typeof conf['constructor'] !== 'undefined') {
        conf['constructor'].apply(this, arguments);
    }
    for (var pro in conf) {
        this[pro] = conf[pro];
    }
};

constructor属性其实不需要复制的,那么在复制之前需要通过delete conf['constructor'];将其删除。

 小结

让我们把这些小构件联系起来,最终的代码如下:

'use strict';
var assert = require('assert');

var Base = {};
Base.extend = function(conf) {
    return function() {
        if (typeof conf['constructor'] !== 'undefined') {
            conf['constructor'].apply(this, arguments);
            delete conf['constructor'];
        }
        for (var pro in conf) {
            this[pro] = conf[pro];
        }
    };
};

var Person = Base.extend({
    constructor: function(name) {
        this.name = name;
    },
    get_name: function() {
        return this.name;
    }
});


describe('Base', function() {
    it('extend', function() {
        var jobs = new Person('jobs');
        assert.equal('jobs', jobs.get_name());
    });
});

Person.extend

如果只使用Base.extend(),与直接复制对象没有什么太大的差别。问题的关键在于Base.extend()返回的函数仍然可以使用extend()。如Person.extend()

Person.extend可以返回一个继承自Person的函数。让我们来看一下它的使用:

var Person = Base.extend({
    constructor: function(name) {
        this.name = name;
    },
    get_name: function() {
        return this.name;
    }
});

var User = Person.extend({
    constructor: function(name, password) {
        this.name = name;
        this.password = password;
    },
    get_password: function() {
        return this.password;
    }
});


describe('Base', function() {
    it('extend', function() {
        var jobs = new User('jobs', '123');
        assert.equal('jobs', jobs.get_name());
        assert.equal('123', jobs.get_password());
    });
});

Person.extend

Person函数作为Base.extend返回的值,它也有自己的extend,从外观上看,它们的职责应该是一致的。所以Person.extend应该==Base.extend。Person有了extend方法,它是在Base.extend方法中直接返回的。所以我们不能直接返回这个function对象,而是为其命名,并指定extend方法给它:

var Base = {};
Base.extend = function(conf) {
    var klass =  function() {
        if (typeof conf['constructor'] !== 'undefined') {
            conf['constructor'].apply(this, arguments);
        }
        for (var pro in conf) {
            this[pro] = conf[pro];
        }
    };
    klass.extend = Base.extend;

    return klass;
};

如此,当使用new klass()时,conf的属性会被复制给new klass()构建的对象上。这其实非常的耗费内存。比如同样使用new klass()构建的每个对象都会从conf中复制方法,而这些方法在使用时的行为是一样的。我们没有必要让每一个new klass()产生的对象都执有这些方法的副本,所以将这些方法定义在klass.prototype上应该是一个不错的选择:

var Base = {};
Base.extend = function(conf) {
    var klass =  function() {
        if (typeof conf['constructor'] !== 'undefined') {
            conf['constructor'].apply(this, arguments);
            delete conf['constructor'];
        }
    };
    for (var pro in conf) {
        klass.prototype[pro] = conf[pro];
    }

    klass.extend = Base.extend;

    return klass;
};

OK,如此所有使和new klass()创建的对象都会调用同一个方法,避免了内存的浪费。但是这里有一个小麻烦。所有的function都有一个prototype,而这个prototype都有一个constructor属性指向function。如klass.prototype.constructor实际是指向klass的。但是当我们将conf中的constructor复制给klass.prototype的原型时,这个constructor的属性被改写了,我们希望保留这个constructor,以便于识别对象的类型。所以,在复制的时候决定将constructor指向的函数命名为init。

    for (var pro in conf) {
        if(pro === 'constructor'){
            klass.prototype['init'] = conf[pro];
        }
        else{
            klass.prototype[pro] = conf[pro];
        }
    }

再做一点小的改动。当执行new Person()时,不再使用conf中的constructor,而是函数原型中的init:

    var klass =  function() {
        if (typeof this['init'] !== 'undefined') {
            this['init'].apply(this, arguments);
        }
    };

get_name()

如果此时我们通过mocha运行这个测试,会提示我们找不到jobs找不到get_name方法。get_name方法是定义在Person函数中的。因此我们需要让User函数继承自Person函数。JS中继承的方式比较多,考虑到继承链的延续,我采用了原型链式继承:

    var proto = {};
    for(var attr in this.prototype){
        if(typeof this.prototype[attr] === 'function'){
            proto[attr] = this.prototype[attr];
        }
    }
    klass.prototype.__proto__ = proto;

我没有替换掉klass.prototype的值,而是将其proto指向一个复制于基类原型的新对象。这样即可以复用基类的方法,又不至破坏klass.prototype的构造型。

小结

'use strict';
var assert = require('assert');

var Base = {};
Base.extend = function(conf) {
    var klass = function() {
        if (typeof this['init'] !== 'undefined') {
            this['init'].apply(this, arguments);
        }
    };
    for (var pro in conf) {
        if (pro === 'constructor') {
            klass.prototype['init'] = conf[pro];
        } else {
            klass.prototype[pro] = conf[pro];
        }
    }

    var proto = {};
    for (var attr in this.prototype) {
        if (typeof this.prototype[attr] === 'function') {
            proto[attr] = this.prototype[attr];
        }
    }
    klass.prototype.__proto__ = proto;

    klass.extend = Base.extend;

    return klass;
};

var Person = Base.extend({
    constructor: function(name) {
        this.name = name;
    },
    get_name: function() {
        return this.name;
    }
});

var User = Person.extend({
    constructor: function(name, password) {
        this.name = name;
        this.password = password;
    },
    get_password: function() {
        return this.password;
    }
});

describe('Base', function() {
    it('extend', function() {
        var jobs = new User('jobs', '123');
        assert.equal('jobs', jobs.get_name());
        assert.equal('123', jobs.get_password());
    });
});

this.base

在一些静态的类式语言里,子类型的方法可以通过base或者super调用基类的方法。然而在JS里很麻烦。因为当子类里存在和基类一样的方法时,它往往被修改或者遮挡住了。所以修改User中的constructor方法,其调用基类的构造函数this.base(name):

var User = Person.extend({
    constructor: function(name, password) {
        this.base(name);
        this.password = password;
    },
    get_password: function() {
        return this.password;
    }
});

this.base

this.base是来自于基类的构造函数。好在我们在选择继承体系的时候采用的是原型链,而不是类抄写的方式。所以当我们需要调用基类的构造函数时,可以调用prototype.proto的'init'方法。

klass.prototype.base = proto['init'];

运行mocha,测试通过。

User.extend

现在只是一层继承体系,让我们再添加一层。

var Person = Base.extend({
    constructor: function(name) {
        this.name = name;
    },
    get_name: function() {
        return this.name;
    }
});

var User = Person.extend({
    constructor: function(name, password) {
        this.base(name);
        this.password = password;
    },
    get_password: function() {
        return this.password;
    }
});

var Teacher = User.extend({
    constructor: function(name) {
        this.base(name, '123');
    },
    say_hello: function() {
        return 'hello,' + this.get_name();
    }
});

describe('Base', function() {
    it('extend', function() {
        var jobs = new Teacher('jobs');
        assert.equal('jobs', jobs.get_name());
        assert.equal('123', jobs.get_password());
        assert.equal('hello,jobs', jobs.say_hello());
    });
});

Teacher是基于User的扩展,new Teacher()时,通过this.base调用User的构造函数;而在User的构造函数中再次通过this.base()调用Person的构造函数。当我们使用mocha测试时,会报出Maximum call stack size exceeded错误。很显然进行死循环了。

为什么呢?我们来看,当new Teacher()时,这个时候的this实际上就是Teacher类型的对象。this.base会调用User的构造函数,而User的constructor中也会调用this.base。然而此时的this仍然是new Teacher()创造的对象。而this.base,好吧,我们进入了死循环。

看起来我们调用base方法时,需要指定this对象的值。apply可以帮助我们。

klass.prototype.base = function() {
    if(typeof proto['init'] !== 'undefined'){
        proto['init'].apply(proto,arguments);
    }
};

使用mocha测试。这回没有死循环了 :)

clone

但是测试却失败了。因为我们实际上是为proto赋值的,而本体对象实际上并没有被赋值。所以我们需要将刚刚执行时获取的值设回给本体对象,但是方法除外(因为基对象的方法会覆盖掉子对象)。所以:

        for(var attr in proto){
            if(typeof proto[attr] != 'function'){
                this[attr] = proto[attr];
            }
        }

只复制属性值,而不需要复制方法。

 小结

让我们把它整理一下吧:

'use strict';
var assert = require('assert');

var Base = {};
Base.extend = function(conf) {
    var klass = function() {
        if (typeof this['init'] !== 'undefined') {
            this['init'].apply(this, arguments);
        }
    };
    for (var pro in conf) {
        if (pro === 'constructor') {
            klass.prototype['init'] = conf[pro];
        } else {
            klass.prototype[pro] = conf[pro];
        }
    }

    var proto = {};
    for (var attr in this.prototype) {
        if (typeof this.prototype[attr] === 'function') {
            proto[attr] = this.prototype[attr];
        }
    }
    klass.prototype.__proto__ = proto;

    klass.prototype.base = function() {
        if (typeof proto['init'] !== 'undefined') {
            proto['init'].apply(proto, arguments);

            for (var attr in proto) {
                if (typeof proto[attr] != 'function') {
                    this[attr] = proto[attr];
                }
            }
        }
    };

    klass.extend = Base.extend;

    return klass;
};

var Person = Base.extend({
    constructor: function(name) {
        this.name = name;
    },
    get_name: function() {
        return this.name;
    }
});

var User = Person.extend({
    constructor: function(name, password) {
        this.base(name);
        this.password = password;
    },
    get_password: function() {
        return this.password;
    }
});

var Teacher = User.extend({
    constructor: function(name) {
        this.base(name, '123');
    },
    say_hello: function() {
        return 'hello,' + this.get_name();
    }
});

describe('Base', function() {
    it('extend', function() {
        var jobs = new Teacher('jobs');
        assert.equal('jobs', jobs.get_name());
        assert.equal('123', jobs.get_password());
        assert.equal('hello,jobs', jobs.say_hello());
    });
});

mocha测试,绿色。