Base
2013-05-15
可通过添加微信公共帐号
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测试,绿色。