一、廢話
總覺得面向對象這東西,如果做的東西不是十分復雜的話,其實不太有場景能用上。最近重新學習了《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