由于我的第一篇文章里通過圖解描述JavaScript語義的方式大受歡迎,因此我決定嘗試用這種方法來講解一些高級內(nèi)容。在本文中,我會講解三種常用的創(chuàng)建對象的技術(shù),它們分別是:構(gòu)造器(constructor)加原型(prototype)的方式、純原型的方式以及對象工廠(object factory)的方式。
我的目的是希望能夠幫助大家理解每種技術(shù)的優(yōu)缺點(diǎn),并理解其運(yùn)行機(jī)理。
經(jīng)典的JavaScript構(gòu)造器
首先我們通過原型來創(chuàng)建一個簡單的構(gòu)造器。這是在原生的JavaScript中最接近類(class)的一種方式。它非常強(qiáng)大而有效,但是我們并不能奢望它像其他包含類的語言一樣強(qiáng)大。
//長方形function Rectangle(width, height) {this.width = width;this.height = height;}Rectangle.prototype.getArea = function getArea() {return this.width * this.height;};Rectangle.prototype.getPerimeter = function getPerimeter() {return 2 * (this.width + this.height);};Rectangle.prototype.toString = function toString() {return this.constructor.name + " a=" + this.getArea() + " p=" + this.getPerimeter();};//正方形function Square(side) {this.width = side;this.height = side;}Square.prototype.__proto__ = Rectangle.prototype;Square.prototype.getPerimeter = function getPerimeter() {return this.width * 4;};//測試var rect = new Rectangle(6, 4);var sqr = new Square(5);console.log(rect.toString())console.log(sqr.toString())
現(xiàn)在我們新定義一個叫做Square的類對象,它繼承自Rectangle。為了實現(xiàn)繼承,構(gòu)造器的prototype必須繼承自父構(gòu)造器的prototype。這里我們覆蓋了getPerimeter使其更加高效,順便展示一下如何來覆蓋函數(shù)。
function Square(side) {this.width = side;this.height = side;}Square.prototype.__proto__ = Rectangle.prototype;Square.prototype.getPerimeter = function getPerimeter() {return this.width * 4;};
用法就很簡單了,只要給每個都創(chuàng)建一個實例(instance)并在實例上調(diào)用函數(shù)即可。
var rect = new Rectangle(6, 4);var sqr = new Square(5);console.log(rect.toString())console.log(sqr.toString())
輸出:
Rectangle a=24 p=20 Square a=25 p=20
下圖是生成的數(shù)據(jù)結(jié)構(gòu),虛線表示對象的繼承。
注意,雖然它們都是繼承自Rectangle.prototype的對象,但在rect實例和Square.prototype之間還是有一點(diǎn)小區(qū)別。如果你仔細(xì)研究的話,會發(fā)現(xiàn)JavaScript不過是一系列相互關(guān)聯(lián)的對象而已。唯一特殊的對象就是函數(shù)(function)了,在函數(shù)中可以接受參數(shù)并且可以包含可執(zhí)行的代碼,函數(shù)還可以指向作用域(scope)。
純原型對象
再看剛才的例子,這次我們不使用構(gòu)造函數(shù),而只使用純原型繼承。
我們來定義一個Rectangle原型來作為構(gòu)建其他對象的基礎(chǔ)。
var Rectangle = {name: "Rectangle",getArea: function getArea() {return this.width * this.height;},getPerimeter: function getPerimeter() {return 2 * (this.width + this.height);},toString: function toString() {return this.name + " a=" + this.getArea() + " p=" + this.getPerimeter();}};
現(xiàn)在我們來定義一個名為Square的子對象,并且覆蓋一些屬性來改變它的某些行為。
var Square = {name: "Square",getArea: function getArea() {return this.width * this.width;},getPerimeter: function getPerimeter() {return this.width * 4;},};Square.__proto__ = Rectangle;
為了創(chuàng)建這些原型的實例,首先我們簡單地創(chuàng)建一個繼承自原型對象的新對象,然后再手動設(shè)置一些局部狀態(tài)。
var rect = Object.create(Rectangle);rect.width = 6;rect.height = 4;var square = Object.create(Square);square.width = 5;console.log(rect.toString());console.log(square.toString());
輸出:
Rectangle a=24 p=20 Square a=25 p=20
下面是生成的對象圖:
這個方法沒有構(gòu)造器+原型的方法那么強(qiáng)大,但是通常更容易理解一點(diǎn),因為它沒有那么拐彎抹角。當(dāng)然了,如果你之前使用的語言包含純原型繼承,那么你會很高興地發(fā)現(xiàn)在JavaScript中也是可以實現(xiàn)的。
對象工廠
我最喜歡的創(chuàng)建對象的方法之一就是使用工廠函數(shù)。它的不同之處在于,你不必定義包含所有共享函數(shù)的原型對象,然后再創(chuàng)建這些對象的實例,每次只需要簡單地調(diào)用一個可以返回新對象的函數(shù)即可。
這個例子是一個超簡單的MVC系統(tǒng)??刂破鳎╟ontroller)函數(shù)接受作為參數(shù)的模型(model)和視圖(view)對象并且輸出一個新的控制器對象。所有狀態(tài)都通過作用域保存在閉包中。
function Controller(model, view) {view.update(model.value);return {up: function onUp(evt) {model.value++;view.update(model.value);},down: function onDown(evt) {model.value--;view.update(model.value);},save: function onSave(evt) {model.save();view.close();}};}
若想使用該函數(shù),只需要傳入所需的參數(shù)調(diào)用函數(shù)即可。注意一下我們是如何用它來作為事件處理函數(shù)(setTimeout)而不用事先將函數(shù)綁定到對象上的。由于它(該函數(shù))在內(nèi)部不使用this關(guān)鍵字,因此就沒有必要搞亂this的值了。
var on = Controller(// 內(nèi)嵌模擬的模型{value: 5,save: function save() {console.log("Saving value " + this.value + " somewhere");}},// 內(nèi)嵌模擬的視圖{update: function update(newValue) {console.log("View now has " + newValue);},close: function close() {console.log("Now hiding view");}});setTimeout(on.up, 100);setTimeout(on.down, 200);setTimeout(on.save, 300);// 輸出View now has 5View now has 6View now has 5Saving value 5 somewhereNow hiding view
下面是這段代碼生成的對象圖。注意我們是通過函數(shù)隱藏的[scope]屬性來訪問傳入的兩個匿名對象的,或者換句話說,我們通過工廠函數(shù)創(chuàng)建的閉包可以訪問到model和view。
結(jié)論
這里面有太多我想探索的細(xì)節(jié)了,不過我更喜歡保持文章的簡短易讀。如果大家有需求的話,我會再寫第三篇文章來講解如何使用ruby風(fēng)格的mixin以及其他一些高級內(nèi)容。