JavaScript OO不XX 学习总结
一、废话
总觉得面向对象这东西,如果做的东西不是十分复杂的话,其实不太有场景能用上。最近重新学习了《JavaScript高级程序设计》中面向对象程序部分的知识,有一些收获,特此记录。
二、JavaScript创建对象最佳实践
2.1 理论
JavaScript是基于原型的语言,创建对象比较常用的方法是采用“构造函数+挂载原型”的方式。
举个例子:
var Engineer = function (name) { this.name = name; }; Engineer.prototype.codeWith = function (tools) { return this.name + ' is coding with ' + tools.join(','); }; Engineer.prototype.solve = function (problem) { return this.name + ' is solving ' + problem; }; var a = new Engineer('kohpoll'); var b = new Engineer('xp'); console.log(a, b); console.log(a.codeWith(['vim'])); console.log(a.solve('oo'));
这段代码执行后,事实上的结构是这样的:
这里总结2点:1)每创建一个函数,该函数默认都会拥有一个prototype属性,这个prototype是一个对象,默认会拥有一个constructor属性反过来指向该函数(比如:例子里的函数Engineer拥有的原型属性prototype,prototype里拥有constructor指向Engineer);2)每创建一个对象,该对象都会拥有一个内置属性__proto__,该属性指向构造了该对象的构造函数的prototype属性(比如:例子里的a对象的__proto__指向构造了它的构造函数Engineer的prototype)。
那为什么Engineer.prototype拥有__proto__属性,且指向Object.prototype呢?这是因为原型对象prototype也是一个对象,那是谁构造了这个对象呢?当然是Object构造函数,所以Engineer.prototype的__proto__指向Object的原型(即:Object.prototype)。Object.prototype的__proto__已经到达顶端了,直接指向空。这就是所谓的原型链。
当我们访问某个成员时,如:a.codeWith(['vim'])。会先从对象自身搜寻(上图的第一个方块),没有找到的话,就顺着__proto__来到Engineer.prototye,发现找到了这个方法,于是进行调用,若这里还没有找到,那就继续顺着Engineer.prototype的__proto__来到Object.prototype,如果这里还是找不到,若是访问属性就返回undefined,若是访问方法就报“Uncaught TypeError: Object [object Object] has no method"错误。
所以,JavaScript中所有对象都继承自Object其实是说访问成员时,最终会在Object.prototype上结束。我们创建出一个对象后,toString、valueOf方法自动就可用,正是因为它们是挂载在Object.prototype上的。
通过构造函数初始化属性,将方法挂载在原型上,我们实现了多个对象复用原型上的共有方法(不必在每个对象中都得定义一次),每个对象又分别拥有自己的属性。
2.2 实践
反过来,看上面的代码,总觉得每次都要这样编写(尤其是写一堆prototype的部分),是件比较烦人的事。我们可以将生成构造函数这个过程进行一个封装:
var construct = function () {// 构造器 var Klass = function () { this.initialize.apply(this, arguments); };
// 添加实列成员(属性,方法) Klass.include = function (obj) { for (var name in obj) { this.prototype[name] = obj[name]; } return this; }; return Klass; };
那我们的示例代码可以这样来写:
var Engineer = construct().include({ initialize: function (name) { this.name = name; }, codeWith: function (tools) { return this.name + ' is coding with ' + tools.join(','); }, solve: function (problem) { return this.name + ' is solving ' + problem; } }); var a = new Engineer('kohpoll'); var b = new Engineer('xp'); console.log(a, b); console.log(a.codeWith(['vim'])); console.log(a.solve('oo'));
三、JavaScript继承最佳实践
3.1 理论
前面说过,JavaScript是基于原型的语言,其实从对原型链的说明中,我们已经大概能看出JavaScript实现继承的方法了,那就是构造原型链。如果,现在我们添加一个FrontEndEngineer子类继承Engineer,那我们想的效果应该是这样的结构:
也就是说,如果我们能够打断默认情况下FronEndEngineer.prototype.__proto__的指向,让FrontEnginner.prototype.__proto__属性指向父类Engineer的原型prototype,根据前面说明过的原型链搜寻过程,我们就实现了FrontEnginner继承Engineer。即:FrontEndEngineer.prototype.__proto__=Engineer.prototype。
可惜的是,__proto__是一个内部属性,各个浏览器的内部实现不一样,也许并不都叫__proto__,而且我们也不能直接修改。回想我们前面总结的2点中的第二点:每创建一个对象,该对象都会拥有一个内置属性__proto__,该属性指向构造了该对象的构造函数的prototype属性。对照我们的目的,让FrontEndEngineer.prototype.__proto__指向Enginner.prototype。于是,我们的方法就出现了:FroneEndEngineer.prototype = new Engineer()。
说明如下:FrontEndEngineer.prototype是一个对象,拥有__proto__属性,默认情况下是指向Object.prototype(因为是Object构造函数构造了FrontEndEngineer.prototype),现在我们让FrontEndEngineer.prototype等于new Engineer(),等价于说FrontEndEngineer.prototype现在是由Engineer函数构造出的,根据上面提到的“每创建一个对象,该对象都会拥有一个内置属性__proto__,该属性指向构造了该对象的构造函数的prototype属性”,那此时FrontEndEngineer.prototype这个对象的__proto__应该指向构造了FrontEndEngineer.prototype这个对象的构造函数的prototype,即:Engineer.prototype。
代码如下:
var Engineer = construct().include({ initialize: function (name) { this.name = name; }, codeWith: function (tools) { return this.name + ' is coding with ' + tools.join(','); }, solve: function (problem) { return this.name + ' is solving ' + problem; } }); var FrontEndEngineer = function (name) { this.name = name; }; FrontEndEngineer.prototype = new Engineer(); FrontEndEngineer.prototype.fuckIE6 = function () { return this.name + 'fuck ie6'; };
FrontEndEngineer.prototype.constructor = FrontEndEngineer; var a = new FrontEndEngineer('kohpoll'); var b = new FrontEndEngineer('xp'); console.log(a, b); console.log(a.codeWith(['vim'])); console.log(a.solve('oo')); console.log(a.fuckIE6());
console.log(a instanceof Engineer, a instanceof FrontEndEngineer);
于是,通过重写原型,我们实现了继承。通过这种方法需要注意的问题是:1)为子类FrontEndEngineer新添加的方法要在重写原型(即:FrontEndEngineer.prototype=new Engineer)后进行添加,否则,会被直接覆盖掉;2)由于我们重写了原型,原型的constructor属性也会改变,如果很在意,可以重新进行赋值;3)每次重写原型都调用了父类的构造函数,其实完全可以避免(下面说)。
3.2 实践
上面实现了继承,但是写起来也是有很多要注意的地方,比较麻烦。我们可以进行一个封装,代码如下:
var inherits = function (klass, supr, protoProps) { // 用于共享原型的空函数 var F = function () {}; // 重写原型实现继承 F.prototype = supr.prototype; klass.prototype = new F(); // 添加实列成员 klass.include(protoProps); // 设置构造器的constructor(因为重写了原型) klass.prototype.constructor = klass; return klass; };
上面提到重写原型时会调用父类的构造函数,其实我们的目的仅仅是要让FrontEndEngineer.prototype的__proto__指向父类的prototype就好,根本不关心是不是真的是父类Engineer构造了FrontEndEngineer.prototype对象。于是,我们使用一个空函数F,将父类Engineer的prototype原型赋值给空函数F的prototype原型,然后让这个F来构造子类的prototype原型,就完成了原型的重写。
于是,我们的示例代码可以这样来编写:
var Engineer = construct().include({ initialize: function (name) { this.name = name; }, codeWith: function (tools) { return this.name + ' is coding with ' + tools.join(','); }, solve: function (problem) { return this.name + ' is solving ' + problem; } }); var FrontEndEngineer = construct(); inherits(FrontEndEngineer, Engineer, { fuckIE6: function () { return this.name + 'fuck ie6'; } }); var a = new FrontEndEngineer('kohpoll'); var b = new FrontEndEngineer('xp'); console.log(a, b); console.log(a.codeWith(['vim'])); console.log(a.solve('oo')); console.log(a.fuckIE6()); console.log(a instanceof Engineer, a instanceof FrontEndEngineer);
四、改进
4.1 接口使用上改进
上面实现的封装使用起来还是不太爽,我们参考下prototype(http://prototypejs.org/learn/class-inheritance),改进后得到如下代码:
var Class = { create: function () { var supr = Object; var protoProps = arguments[0] || {}; var klass; if (typeof arguments[0] == 'function') { supr = arguments[0]; protoProps = arguments[1] || {}; } if (typeof protoProps.initialize != 'function') { protoProps.initialize = function () {}; } klass = this._construct(); this._inherits(klass, supr, protoProps); return klass; }, _construct: function () {//{{{// 构造器 var Klass = function () { this.initialize.apply(this, arguments); };
// 添加实列成员(属性,方法) Klass.include = function (obj) { for (var name in obj) { this.prototype[name] = obj[name]; } return this; }; return Klass; },//}}} _inherits: function (klass, supr, protoProps) {//{{{ // 用于共享原型的空函数 var F = function () {}; // 重写原型实现继承 F.prototype = supr.prototype; klass.prototype = new F(); // 添加实列成员 klass.include(protoProps); // 设置构造器的constructor(因为重写了原型) klass.prototype.constructor = klass; return klass; }//}}} };
于是,现在我们的示例代码可以这样来写了:
var Engineer = Class.create({ initialize: function (name) { this.name = name; }, codeWith: function (tools) { return this.name + ' is coding with ' + tools.join(','); }, solve: function (problem) { return this.name + ' is solving ' + problem; } }); var FrontEndEngineer = Class.create(Engineer, { initialize: function (name) { this.name = name; }, fuckIE6: function () { return this.name + 'fuck ie6'; } }); var a = new FrontEndEngineer('kohpoll'); var b = new FrontEndEngineer('xp'); console.log(a, b); console.log(a.codeWith(['vim'])); console.log(a.solve('oo')); console.log(a.fuckIE6()); console.log(a instanceof Engineer, a instanceof FrontEndEngineer);
4.2 继承使用上改进
经过这样改进,代码看起来比较清晰了。但是,继承有一个很关键的问题没有解决,就是子类怎么调用父类的方法,从而实现代码复用?下面我们就来解决这个问题。
step1 事实上,我们可以直接通过父类的prototype属性来访问父类的方法,为了方便,我们在生成构造器时添加一个$super属性。于是,代码变成如下(标红的是新增代码):
var Class = { create: function () { var supr = Object; var protoProps = arguments[0] || {}; var klass; if (typeof arguments[0] == 'function') { supr = arguments[0]; protoProps = arguments[1] || {}; } if (typeof protoProps.initialize != 'function') { protoProps.initialize = function () {}; } klass = this._construct(); this._inherits(klass, supr, protoProps); return klass; }, _construct: function () {//{{{ // 构造器 var Klass = function () { // 访问父类成员快捷方式 this.$super = Klass.$super; this.initialize.apply(this, arguments); }; // 添加实列成员(属性,方法) Klass.include = function (obj) { for (var name in obj) { this.prototype[name] = obj[name]; } return this; }; return Klass; },//}}} _inherits: function (klass, supr, protoProps) {//{{{ // 用于共享原型的空函数 var F = function () {}; // 重写原型实现继承 F.prototype = supr.prototype; klass.prototype = new F(); // 保存父类原型 klass.$super = supr.prototype; // 添加实列成员、类成员 klass.include(protoProps); // 设置构造器的constructor(因为重写了原型) klass.prototype.constructor = klass; return klass; }//}}} };
于是,示例代码可以这样使用:
var Engineer = Class.create({ initialize: function (name) { this.name = name; }, codeWith: function (tools) { return this.name + ' is coding with ' + tools.join(','); }, solve: function (problem) { return this.name + ' is solving ' + problem; } }); var FrontEndEngineer = Class.create(Engineer, { initialize: function (name) { this.$super.initialize.call(this, name); // this.name = name; }, codeWith: function (tools) { return 'fron end ' + this.$super.codeWith.call(this, tools); }, fuckIE6: function () { return this.name + 'fuck ie6'; } }); var a = new FrontEndEngineer('kohpoll'); var b = new FrontEndEngineer('xp'); console.log(a, b); console.log(a.codeWith(['vim']));
step2 其实经过这样改进,已经比较不错了,只是每次调用父类方法时都得使用call方法来确保this正确的指向子类实例。如果不用call,那this会指向什么呢?答案是父类的prototye,在本例中,$super存的是Engineer.prototype,那当我们使用$super.codeWith()时,实际上,是指Engineer.prototype.codeWith(),this指向Engineer.prototype。
我们想办法来改进这一点,得到代码如下(标红为新增):
var Class = { create: function () { var supr = Object; var protoProps = arguments[0] || {}; var klass; if (typeof arguments[0] == 'function') { supr = arguments[0]; protoProps = arguments[1] || {}; } if (typeof protoProps.initialize != 'function') { protoProps.initialize = function () {}; } klass = this._construct(); this._inherits(klass, supr, protoProps); return klass; }, _construct: function () {//{{{ var slice = Array.prototype.slice; // 构造器 var Klass = function () { // 访问父类成员快捷方式 this.$super = function (name) { var args = slice.call(arguments, 1) || []; var fn = Klass.$super[name]; return typeof fn == 'function' ? fn.apply(this, args) : fn; }; this.initialize.apply(this, arguments); }; // 添加实列成员(属性,方法) Klass.include = function (obj) { for (var name in obj) { this.prototype[name] = obj[name]; } return this; }; return Klass; },//}}} _inherits: function (klass, supr, protoProps) {//{{{ // 用于共享原型的空函数 var F = function () {}; // 重写原型实现继承 F.prototype = supr.prototype; klass.prototype = new F(); // 保存父类原型 klass.$super = supr.prototype; // 添加实列成员、类成员 klass.include(protoProps); // 设置构造器的constructor(因为重写了原型) klass.prototype.constructor = klass; return klass; }//}}} };
现在,我们将$super重写成函数,通过传入的函数名在父类的prototype里查找对应方法,若找到了就通过apply来调用,此时传入的this就指向了子类实例(因为$super现在是被子类的实例调用,$super函数内部this就指向子类实例)。
于是,使用方法如下:
var Engineer = Class.create({ initialize: function (name) { this.name = name; }, codeWith: function (tools) { return this.name + ' is coding with ' + tools.join(','); }, solve: function (problem) { return this.name + ' is solving ' + problem; } }); var FrontEndEngineer = Class.create(Engineer, { initialize: function (name) { this.$super('initialize', name); // this.name = name; }, codeWith: function (tools) { return 'front end ' + this.$super('codeWith', tools); }, solve: function (problem) { return 'front end ' + this.$super('codeWith', ['html', 'js', 'css']) + this.fuckIE6(); }, fuckIE6: function () { return ' and fuck ie6'; } }); var a = new FrontEndEngineer('kohpoll'); console.log(a.solve('work'));
4.3 添加静态成员
最后一点改进,是添加类似static的所有类共享的方法和属性。实现方法就是,直接将这些成员挂载到构造函数上面。这个就不多说了。于是得到最终代码如下:
var Class = { create: function () { var supr = Object; var protoProps = arguments[0] || {}, staticProps = arguments[1] || {}; var klass; if (typeof arguments[0] == 'function') { supr = arguments[0]; protoProps = arguments[1] || {}; staticProps = arguments[2] || {}; } if (typeof protoProps.initialize != 'function') { protoProps.initialize = function () {}; } klass = this._construct(); this._inherits(klass, supr, protoProps, staticProps); return klass; }, _construct: function () {//{{{ var slice = Array.prototype.slice; // 构造器 var Klass = function () { // 访问类成员快捷方式 this.$self = Klass.$self; // 访问父类成员快捷方式 this.$super = function (name) { var args = slice.call(arguments, 1) || []; var fn = Klass.$super[name]; return typeof fn == 'function' ? fn.apply(this, args) : fn; }; this.initialize.apply(this, arguments); }; // 用于添加类成员(属性,方法) Klass.extend = function (obj) { for (var name in obj) { this[name] = obj[name]; } return this; }; // 添加实列成员(属性,方法) Klass.include = function (obj) { for (var name in obj) { this.prototype[name] = obj[name]; } return this; }; return Klass; },//}}} _inherits: function (klass, supr, protoProps, staticProps) {//{{{ // 用于共享原型的空函数 var F = function () {}; // 重写原型实现继承 F.prototype = supr.prototype; klass.prototype = new F(); // 保存父类原型 klass.$super = supr.prototype; // 保存类自身 klass.$self = klass; // 添加实列成员、类成员 klass.include(protoProps).extend(staticProps); // 设置构造器的constructor(因为重写了原型) klass.prototype.constructor = klass; return klass; }//}}} };
最后的最后,可以在这里获取所有源码:https://github.com/KohPoll/zuki