<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                <a name="a1"></a> # 代碼復用模式 代碼復用是一個既重要又有趣的話題,因為努力在自己或者別人寫的代碼上寫盡量少且可以復用的代碼是件很自然的事情,尤其當這些代碼是經過測試的、可維護的、可擴展的、有文檔的時候。 當我們說到代碼復用的時候,想到的第一件事就是繼承,本章會有很大篇幅講述這個話題。你將看到好多種方法來實現“類式(classical)”和一些其它方式的繼承。但是,最最重要的事情,是你需要記住終極目標——代碼復用。繼承是達到這個目標的一種方法,但是不是唯一的。在本章,你將看到怎樣基于其它對象來構建新對象,怎樣使用混元,以及怎樣在不使用繼承的情況下只復用你需要的功能。 在做代碼復用的工作的時候,謹記Gang of Four 在書中給出的關于對象創建的建議:“優先使用對象創建而不是類繼承”。(譯注:《設計模式:可復用面向對象軟件的基礎》(Design Patterns: Elements of Reusable Object-Oriented Software)是一本設計模式的經典書籍,該書作者為Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides,被稱為“Gang of Four”,簡稱“GoF”。) <a name="a2"></a> ## 類式繼承 vs 現代繼承模式 在討論JavaScript的繼承這個話題的時候,經常會聽到“類式繼承”的概念,那我們先看一下什么是類式(classical)繼承。classical一詞并不是來自某些古老的、固定的或者是被廣泛接受的解決方案,而僅僅是來自單詞“class”。(譯注:classical也有“經典”的意思。) 很多編程語言都有原生的類的概念,作為對象的藍本。在這些語言中,每個對象都是一個指定類的實例(instance),并且(以Java為例)一個對象不能在不存在對應的類的情況下存在。在JavaScript中,因為沒有類,所以類的實例的概念沒什么意義。JavaScript的對象僅僅是簡單的鍵值對,這些鍵值對都可以動態創建或者是改變。 但是JavaScript擁有構造函數(constructor functions),并且有語法和使用類非常相似的new運算符。 在Java中你可能會這樣寫: Person adam = new Person(); 在JavaScript中你可以這樣: var adam = new Person(); 除了Java是強類型語言需要給adam添加類型Person外,其它的語法看起來是一樣的。JavaScript的創建函數調用看起來感覺Person是一個類,但事實上,Person僅僅是一個函數。語法上的相似使得非常多的開發者陷入對JavaScript類的思考,并且給出了很多模擬類的繼承方案。這樣的實現方式,我們叫它“類式繼承”。順便也提一下,所謂“現代”繼承模式是指那些不需要你去想類這個概念的模式。 當需要給項目選擇一個繼承模式時,有不少的備選方案。你應該盡量選擇那些現代繼承模式,除非團隊已經覺得“無類不歡”。 本章先討論類式繼承,然后再關注現代繼承模式。 <a name="a3"></a> ## 類式繼承的期望結果 實現類式繼承的目標是基于構造函數Child()來創建一個對象,然后從另一個構造函數Parent()獲得屬性。 > 盡管我們是在討論類式繼承,但還是盡量避免使用“類”這個詞。“構造函數”或者“constructor”雖然更長,但是更準確,不會讓人迷惑。通常情況下,應該努力避免在跟團隊溝通的時候使用“類”這個詞,因為在JavaScript中,很可能每個人都會有不同的理解。 下面是定義兩個構造函數Parent()和Child()的例子: //parent構造函數 function Parent(name) { this.name = name || 'Adam'; } //給原型增加方法 Parent.prototype.say = function () { return this.name; }; //空的child構造函數 function Child(name) {} //繼承 inherit(Child, Parent); 上面的代碼定義了兩個構造函數Parent()和Child(),say()方法被添加到了Parent()構建函數的原型(prototype)中,inherit()函數完成了繼承的工作。inherit()函數并不是原生提供的,需要自己實現。讓我們來看一看比較大眾的實現它的幾種方法。 <a name="a4"></a> ## 類式繼承1——默認模式 最常用的一種模式是使用Parent()構造函數來創建一個對象,然后把這個對象設為Child()的原型。這是可復用的inherit()函數的第一種實現方法: function inherit(C, P) { C.prototype = new P(); } 需要強調的是原型(prototype屬性)應該指向一個對象,而不是函數,所以它需要指向由父構造函數創建的實例(對象),而不是構造函數自己。換句話說,請注意new運算符,有了它這種模式才可以正常工作。 之后在應用中使用new Child()創建對象的時候,它將通過原型擁有Parent()實例的功能,像下面的例子一樣: var kid = new Child(); kid.say(); // "Adam" <a name="a5"></a> ### 跟蹤原型鏈 在這種模式中,子對象既繼承了(父對象)“自己的屬性”(添加給this的實例屬性,比如name),也繼承了原型中的屬性和方法(比如say())。 我們來看一下在這種繼承模式中原型鏈是怎么工作的。為了討論方便,我們假設對象是內在中的一塊空間,它包含數據和指向其它空間的引用。當使用new Parent()創建一個對象時,這樣的一塊空間就被分配了(圖6-1中的2號)。它保存著name屬性的數據。如果你嘗試訪問say()方法(比如通過(new Parent).say()),2號空間中并沒有這個方法。但是在通過隱藏的鏈接__proto__指向Parent()構建函數的原型prototype屬性時,就可以訪問到包含say()方法的1號空間(Parent.prototype)了。所有的這一塊都是在幕后發生的,不需要任何額外的操作,但是知道它是怎樣工作的以及你正在訪問或者修正的數據在哪是很重要的。注意,__proto__在這里只是為了解釋原型鏈,這個屬性在語言本身中是不可用的,盡管有一些環境提供了(比如Firefox)。 ![圖6-1 Parent()構造函數的原型鏈](https://box.kancloud.cn/2016-05-03_5728163019952.jpg) 圖6-1 Parent()構造函數的原型鏈 現在我們來看一下在使用inherit()函數之后再使用var kid = new Child()創建一個新對象時會發生什么。見圖6-2。 ![圖6-2 繼承后的原型鏈](https://box.kancloud.cn/2016-05-03_572816302c471.jpg) 圖6-2 繼承后的原型鏈 Child()構造函數是空的,也沒有屬性添加到Child.prototype上,這樣,使用new Child()創建出來的對象都是空的,除了有隱藏的鏈接__proto__。在這個例子中,__proto__指向在inherit()函數中創建的new Parent()對象。 現在使用kid.say()時會發生什么?3號對象沒有這個方法,所以通過原型鏈找到2號。2號對象也沒有這個方法,所以也通過原型鏈找到1號,剛好有這個方法。接下來say()方法引用了this.name,這個變量也需要解析。于是沿原型鏈查找的過程又走了一遍。在這個例子中,this指向3號對象,它沒有name屬性。然后2號對象被訪問,并且有name屬性,值為“Adam”。 最后,我們多看一點東西,假如我們有如下的代碼: var kid = new Child(); kid.name = "Patrick"; kid.say(); // "Patrick" 圖6-3展現了這個例子的原型鏈: ![圖6-3 繼承并且給子對象添加屬性后的原型鏈](https://box.kancloud.cn/2016-05-03_5728163040938.jpg) 圖6-3 繼承并且給子對象添加屬性后的原型鏈 設定kid.name并沒有改變2號對象的name屬性,但是它直接在3號對象上添加了自己的name屬性。當kid.say()執行時,say方法在3號對象中找,然后是2號,最后到1號,像前面說的一樣。但是這一次在找this.name(和kid.name一樣)時很快,因為這個屬性在3號對象中就被找到了。 如果通過delete kid.name的方式移除新添加的屬性,那么2號對象的name屬性將暴露出來并且在查找的時候被找到。 <a name="a6"></a> ### 這種模式的缺點 這種模式的一個缺點是既繼承了(父對象)“自己的屬性”,也繼承了原型中的屬性。大部分情況下你可能并不需要“自己的屬性”,因為它們更可能是為實例對象添加的,并不用于復用。 > 一個在構造函數上常用的規則是,用于復用的成員(譯注:屬性和方法)應該被添加到原型上。 在使用這個inherit()函數時另外一個不便是它不能夠讓你傳參數給子構造函數,這些參數有可能是想再傳給父構造函數的。考慮下面的例子: var s = new Child('Seth'); s.say(); // "Adam" 這并不是我們期望的結果。事實上傳遞參數給父構造函數是可能的,但這樣需要在每次需要一個子對象時再做一次繼承,很不方便,因為需要不斷地創建父對象。 <a name="a7"></a> ## 類式繼承2——借用構造函數 下面這種模式解決了從子對象傳遞參數到父對象的問題。它借用了父對象的構造函數,將子對象綁定到this,同時傳入參數: function Child(a, c, b, d) { Parent.apply(this, arguments); } 使用這種模式時,只能繼承在父對象的構造函數中添加到this的屬性,不能繼承原型上的成員。 使用借用構造函數的模式,子對象通過復制的方式繼承父對象的成員,而不是像類式繼承1中那樣獲得引用。下面的例子展示了這兩者的不同: //父構造函數 function Article() { this.tags = ['js', 'css']; } var article = new Article(); //BlogPost通過類式繼承1(默認模式)從article繼承 function BlogPost() {} BlogPost.prototype = article; var blog = new BlogPost(); //注意你不需要使用`new Article()`,因為已經有一個實例了 //StaticPage通過借用構造函數的方式從Article繼承 function StaticPage() { Article.call(this); } var page = new StaticPage(); alert(article.hasOwnProperty('tags')); // true alert(blog.hasOwnProperty('tags')); // false alert(page.hasOwnProperty('tags')); // true 在上面的代碼片段中,Article()被兩種方式分別繼承。默認模式使blog可以通過原型鏈訪問到tags屬性,所以它自己并沒有tags屬性,hasOwnProperty()返回false。page對象有自己的tags屬性,因為它是使用借用構造函數的方式繼承,復制(而不是引用)了tags屬性。 注意在修改繼承后的tags屬性時的不同: blog.tags.push('html'); page.tags.push('php'); alert(article.tags.join(', ')); // "js, css, html" 在這個例子中,blog對象修改了tags屬性,同時,它也修改了父對象,因為實際上blog.tags和article.tags是引向同一個數組。而對pages.tags的修改并不影響父對象article,因為pages.tags在繼承的時候是一份獨立的拷貝。 <a name="a8"></a> ### 原型鏈 我們來看一下當我們使用熟悉的Parent()和Child()構造函數和這種繼承模式時原型鏈是什么樣的。為了使用這種繼承模式,Child()有明顯變化: //父構造函數 function Parent(name) { this.name = name || 'Adam'; } //在原型上添加方法 Parent.prototype.say = function () { return this.name; }; //子構造函數 function Child(name) { Parent.apply(this, arguments); } var kid = new Child("Patrick"); kid.name; // "Patrick" typeof kid.say; // "undefined" 如果看一下圖6-4,就能發現new Child對象和Parent之間不再有鏈接。這是因為Child.prototype根本就沒有被使用,它指向一個空對象。使用這種模式,kid擁有了自己的name屬性,但是并沒有繼承say()方法,如果嘗試調用它的話會出錯。這種繼承方式只是一種一次性地將父對象的屬性復制為子對象的屬性,并沒有__proto__鏈接。 ![圖6-4 使用借用構造函數模式時沒有被關聯的原型鏈](https://box.kancloud.cn/2016-05-03_572816305812c.jpg) 圖6-4 使用借用構造函數模式時沒有被關聯的原型鏈 <a name="a9"></a> ### 利用借用構造函數模式實現多繼承 使用借用構造函數模式,可以通過借用多個構造函數的方式來實現多繼承: function Cat() { this.legs = 4; this.say = function () { return "meaowww"; } } function Bird() { this.wings = 2; this.fly = true; } function CatWings() { Cat.apply(this); Bird.apply(this); } var jane = new CatWings(); console.dir(jane); 結果如圖6-5,任何重復的屬性都會以最后的一個值為準。 ![圖6-5 在Firebug中查看CatWings對象](https://box.kancloud.cn/2016-05-03_572816306cd3c.jpg) 圖6-5 在Firebug中查看CatWings對象 <a name="a10"></a> ### 借用構造函數的利與弊 這種模式的一個明顯的弊端就是無法繼承原型。如前面所說,原型往往是添加可復用的方法和屬性的地方,這樣就不用在每個實例中再創建一遍。 這種模式的一個好處是獲得了父對象自己成員的拷貝,不存在子對象意外改寫父對象屬性的風險。 那么,在上一個例子中,怎樣使一個子對象也能夠繼承原型屬性呢?怎樣能使kid可以訪問到say()方法呢?下一種繼承模式解決了這個問題。 <a name="a11"></a> ## 類式繼承3——借用并設置原型 綜合以上兩種模式,首先借用父對象的構造函數,然后將子對象的原型設置為父對象的一個新實例: function Child(a, c, b, d) { Parent.apply(this, arguments); } Child.prototype = new Parent(); 這樣做的好處是子對象獲得了父對象自己的成員,也獲得了父對象中可復用的(在原型中實現的)方法。子對象也可以傳遞任何參數給父構造函數。這種行為可能是最接近Java的,子對象繼承了父對象的所有東西,同時可以安全地修改自己的屬性而不用擔心修改到父對象。 一個弊端是父構造函數被調用了兩次,所以不是很高效。最后,(父對象)自己的屬性(比如這個例子中的name)也被繼承了兩次。 我們來看一下代碼并做一些測試: //父構造函數 function Parent(name) { this.name = name || 'Adam'; } //在原型上添加方法 Parent.prototype.say = function () { return this.name; }; //子構造函數 function Child(name) { Parent.apply(this, arguments); } Child.prototype = new Parent(); var kid = new Child("Patrick"); kid.name; // "Patrick" kid.say(); // "Patrick" delete kid.name; kid.say(); // "Adam" 跟前一種模式不一樣,現在say()方法被正確地繼承了。可以看到name也被繼承了兩次,在刪除掉自己的拷貝后,在原型鏈上的另一個就被暴露出來了。 圖6-6展示了這些對象之間的關系。這些關系有點像圖6-3中展示的,但是獲得這種關系的方法是不一樣的。 ![圖6-6 除了繼承“自己的屬性”外,原型鏈也被保留了](https://box.kancloud.cn/2016-05-03_572816307e522.jpg) 圖6-6 除了繼承“自己的屬性”外,原型鏈也被保留了 <a name="a12"></a> ## 類式繼承4——共享原型 不像前一種類式繼承模式需要調用兩次父構造函數,下面這種模式根本不會涉及到調用父構造函數的問題。 一般的經驗是將可復用的成員放入原型中而不是this。從繼承的角度來看,則是任何應該被繼承的成員都應該放入原型中。這樣你只需要設定子對象的原型和父對象的原型一樣即可: function inherit(C, P) { C.prototype = P.prototype; } 這種模式的原型鏈很短并且查找很快,因為所有的對象實際上共享著同一個原型。但是這樣也有弊端,那就是如果子對象或者在繼承關系中的某個地方的任何一個子對象修改這個原型,將影響所有的繼承關系中的父對象。(譯注:這里應該是指會影響到所有從這個原型中繼承的對象。) 如圖6-7,子對象和父對象共享同一個原型,都可以訪問say()方法。但是,子對象不繼承name屬性。 ![圖6-7 (父子對象)共享原型時的關系](https://box.kancloud.cn/2016-05-03_57281630929d2.jpg) 圖6-7 (父子對象)共享原型時的關系 <a name="a13"></a> ## 類式繼承5——臨時構造函數 下一種模式通過打斷父對象和子對象原型的直接鏈接解決了共享原型時的問題,同時還從原型鏈中獲得其它的好處。 下面是這種模式的一種實現方式,F()函數是一個空函數,它充當了子對象和父對象的代理。F()的prototype屬性指向父對象的原型。子對象的原型是一這個空函數的一個實例: function inherit(C, P) { var F = function () {}; F.prototype = P.prototype; C.prototype = new F(); } 這種模式有一種和默認模式(類式繼承1)明顯不一樣的行為,因為在這里子對象只繼承原型中的屬性(圖6-8)。 ![圖6-8 使用臨時(代理)構造函數F()實現類式繼承](https://box.kancloud.cn/2016-05-03_57281630aa657.jpg) 圖6-8 使用臨時(代理)構造函數F()實現類式繼承 這種模式通常情況下都是一種很棒的選擇,因為原型本來就是存放復用成員的地方。在這種模式中,父構造函數添加到this中的任何成員都不會被繼承。 我們來創建一個子對象并且檢查一下它的行為: var kid = new Child(); 如果你訪問kid.name將得到undefined。在這個例子中,name是父對象自己的屬性,而在繼承的過程中我們并沒有調用new Parent(),所以這個屬性并沒有被創建。當訪問kid.say()時,它在3號對象中不可用,所以在原型鏈中查找,4號對象也沒有,但是1號對象有,它在內在中的位置會被所有從Parent()創建的構造函數和子對象所共享。 <a name="a14"></a> ### 存儲父類(Superclass) 在上一種模式的基礎上,還可以添加一個指向原始父對象的引用。這很像其它語言中訪問超類(superclass)的情況,有時候很方便。 我們將這個屬性命名為“uber”,因為“super”是一個保留字,而“superclass”則可能誤導別人認為JavaScript擁有類。下面是這種類式繼承模式的一個改進版實現: function inherit(C, P) { var F = function () {}; F.prototype = P.prototype; C.prototype = new F(); C.uber = P.prototype; } <a name="a15"></a> ### 重置構造函數引用 這個近乎完美的模式上還需要做的最后一件事情就是重置構造函數(constructor)的指向,以便未來在某個時刻能被正確地使用。 如果不重置構造函數的指向,那所有的子對象都會認為Parent()是它們的構造函數,而這個結果完全沒有用。使用前面的inherit()的實現,你可以觀察到這種行為: // parent, child, inheritance function Parent() {} function Child() {} inherit(Child, Parent); // testing the waters var kid = new Child(); kid.constructor.name; // "Parent" kid.constructor === Parent; // true constructor屬性很少用,但是在運行時檢查對象很方便。你可以重新將它指向期望的構造函數而不影響功能,因為這個屬性更多是“信息性”的。(譯注:即它更多的時候是在提供信息而不是參與到函數功能中。) 最終,這種類式繼承的Holy Grail版本看起來是這樣的: function inherit(C, P) { var F = function () {}; F.prototype = P.prototype; C.prototype = new F(); C.uber = P.prototype; C.prototype.constructor = C; } 類似這樣的函數也存在于YUI庫(也許還有其它庫)中,它將類式繼承的方法帶給了沒有類的語言。如果你決定使用類式繼承,那么這是最好的方法。 > “代理函數”或者“代理構造函數”也是指這種模式,因為臨時構造函數是被用作獲取父構造函數原型的代理。 一種常見的對Holy Grail模式的優化是避免每次需要繼承的時候都創建一個臨時(代理)構造函數。事實上創建一次就足夠了,以后只需要修改它的原型即可。你可以用一個立即執行的函數來將代理函數存儲到閉包中: var inherit = (function () { var F = function () {}; return function (C, P) { F.prototype = P.prototype; C.prototype = new F(); C.uber = P.prototype; C.prototype.constructor = C; } }()); <a name="a16"></a> ## Klass 有很多JavaScript類庫模擬了類,創造了新的語法糖。具體的實現方式可能會不一樣,但是基本上都有一些共性,包括: - 有一個約定好名字的方法,如initialize、_init或者其它相似的名字,會被自動調用,來充當類的構造函數。 - 類可以從其它類繼承 - 在子類中可以訪問到父類(superclass) > 我們在這里做一下變化,在本章的這部分自由地使用“class”單詞,因為主題就是模擬類。 為避免討論太多細節,我們來看一下JavaScript中一種模擬類的實現。首先,這種解決方案從客戶的角度來看將如何被使用? var Man = klass(null, { __construct: function (what) { console.log("Man's constructor"); this.name = what; }, getName: function () { return this.name; } }); 這種語法糖的形式是一個名為klass()的函數。在一些實現方式中,它可能是Klass()構造函數或者是增強的Object.prototype,但是在這個例子中,我們讓它只是一個簡單的函數。 這個函數接受兩個參數:一個被繼承的類和通過對象字面量提供的新類的實現。受PHP的影響,我們約定類的構造函數必須是一個名為\_\_construct的方法。在前面的代碼片段中,建立了一個名為Man的新類,并且它不繼承任何類(意味著繼承自Object)。Man類有一個在\_\_construct建立的自己的屬性name和一個方法getName()。這個類是一個構造函數,所以下面的代碼將正常工作(并且看起來像類實例化的過程): var first = new Man('Adam'); // logs "Man's constructor" first.getName(); // "Adam" 現在我們來擴展這個類,創建一個SuperMan類: var SuperMan = klass(Man, { __construct: function (what) { console.log("SuperMan's constructor"); }, getName: function () { var name = SuperMan.uber.getName.call(this); return "I am " + name; } }); 這里,klass()的第一個參數是將被繼承的Man類。值得注意的是,在getName()中,父類的getName()方法首先通過SuperMan類的uber靜態屬性被調用。我們來測試一下: var clark = new SuperMan('Clark Kent'); clark.getName(); // "I am Clark Kent" 第一行在console中記錄了“Man's constructor”,然后是“Superman's constructor”。在一些語言中,父類的構造函數在子類構造函數被調用的時候會自動執行,這個特性也可以模擬。 用instanceof運算符測試返回希望的結果: clark instanceof Man; // true clark instanceof SuperMan; // true 最后,我們來看一下klass()函數是怎樣實現的: var klass = function (Parent, props) { var Child, F, i; // 1. // new constructor Child = function () { if (Child.uber && Child.uber.hasOwnProperty("__construct")) { Child.uber.__construct.apply(this, arguments); } if (Child.prototype.hasOwnProperty("__construct")) { Child.prototype.__construct.apply(this, arguments); } }; // 2. // inherit Parent = Parent || Object; F = function () {}; F.prototype = Parent.prototype; Child.prototype = new F(); Child.uber = Parent.prototype; Child.prototype.constructor = Child; // 3. // add implementation methods for (i in props) { if (props.hasOwnProperty(i)) { Child.prototype[i] = props[i]; } } // return the "class" return Child; }; 這個klass()實現有三個明顯的部分: 1. 創建Child()構造函數,這也是最后返回的將被作為類使用的函數。在這個函數里面,如果\_\_construct方法存在的話將被調用。同樣是在父類的\_\_construct(如果存在)被調用前使用靜態的uber屬性。也可能存在uber沒有定義的情況——比如從Object繼承,因為它是在Man類中被定義的。 2. 第二部分主要完成繼承。只是簡單地使用前面章節討論過的Holy Grail類式繼承模式。只有一個東西是新的:如果Parent沒有傳值的話,設定Parent為Object。 3. 最后一部分是類真正定義的地方,循環需要實現的方法(如例子中的\_\_constructt和getName),并將它們添加到Child的原型中。 什么時候使用這種模式?其實,最好是能避免則避免,因為它帶來了在這門語言中不存在的完整的類的概念,會讓人疑惑。使用它需要學習新的語法和新的規則。也就是說,如果你或者你的團隊對類感到習慣并且同時對原型感到不習慣,這種模式可能是一個可以探索的方向。這種模式允許你完全忘掉原型,好處就是你可以將語法變種得像其它你所喜歡的語言一樣。 <a name="a17"></a> ## 原型繼承 現在,讓我們從一個叫作“原型繼承”的模式來討論沒有類的現代繼承模式。在這種模式中,沒有任何類進來,在這里,一個對象繼承自另外一個對象。你可以這樣理解它:你有一個想復用的對象,然后你想創建第二個對象,并且獲得第一個對象的功能。下面是這種模式的用法: //需要繼承的對象 var parent = { name: "Papa" }; //新對象 var child = object(parent); //測試 alert(child.name); // "Papa" 在這個代碼片段中,有一個已經存在的使用對象字面量創建的對象叫parent,我們想創建一個和parent有相同的屬性和方法的對象叫child。child對象使用object()函數創建。這個函數在JavaScript中并不存在(不要與構造函數Object()混淆),所以我們來看看怎樣定義它。 與Holy Grail類式繼承相似,可以使用一個空的臨時構造函數F(),然后設定F()的原型為parent對象。最后,返回一個臨時構造函數的新實例。 function object(o) { function F() {} F.prototype = o; return new F(); } 圖6-9展示了使用原型繼承時的原型鏈。在這里child總是以一個空對象開始,它沒有自己的屬性但通過原型鏈(\_\_proto\_\_)擁有父對象的所有功能。 ![圖6-9 原型繼承模式](https://box.kancloud.cn/2016-05-03_57281630c2853.jpg) 圖6-9 原型繼承模式 <a name="a18"></a> ### 討論 在原型繼承模式中,parent不需要使用對象字面量來創建。(盡管這是一種更覺的方式。)可以使用構造函數來創建parent。注意,如果你這樣做,那么自己的屬性和原型上的屬性都將被繼承: // parent constructor function Person() { // an "own" property this.name = "Adam"; } // a property added to the prototype Person.prototype.getName = function () { return this.name; }; // create a new person var papa = new Person(); // inherit var kid = object(papa); // test that both the own property // and the prototype property were inherited kid.getName(); // "Adam" 在這種模式的另一個變種中,你可以選擇只繼承已存在的構造函數的原型對象。記住,對象繼承自對象,不管父對象是怎么創建的。這是前面例子的一個修改版本: // parent constructor function Person() { // an "own" property this.name = "Adam"; } // a property added to the prototype Person.prototype.getName = function () { }; // inherit var kid = object(Person.prototype); typeof kid.getName; // "function", because it was in the prototype typeof kid.name; // "undefined", because only the prototype was inherited <a name="a19"></a> ###例外的ECMAScript 5 在ECMAScript 5中,原型繼承已經正式成為語言的一部分。這種模式使用Object.create方法來實現。換句話說,你不再需要自己去寫類似object()的函數,它是語言原生的了: var child = Object.create(parent); Object.create()接收一個額外的參數——一個對象。這個額外對象中的屬性將被作為自己的屬性添加到返回的子對象中。這讓我們可以很方便地將繼承和創建子對象在一個方法調用中實現。例如: var child = Object.create(parent, { age: { value: 2 } // ECMA5 descriptor }); child.hasOwnProperty("age"); // true 你可能也會發現原型繼承模式已經在一些JavaScript類庫中實現了,比如,在YUI3中,它是Y.Object()方法: YUI().use('*', function (Y) { var child = Y.Object(parent); }); <a name="a20"></a> ## 通過復制屬性繼承 讓我們來看一下另外一種繼承模式——通過復制屬性繼承。在這種模式中,一個對象通過簡單地復制另一個對象來獲得功能。下面是一個簡單的實現這種功能的extend()函數: function extend(parent, child) { var i; child = child || {}; for (i in parent) { if (parent.hasOwnProperty(i)) { child[i] = parent[i]; } } return child; } 這是一個簡單的實現,僅僅是遍歷了父對象的成員然后復制它們。在這個實現中,child是可選參數,如果它沒有被傳入一個已有的對象,那么一個全新的對象將被創建并被返回: var dad = {name: "Adam"}; var kid = extend(dad); kid.name; // "Adam" 上面給出的實現叫作對象的“淺拷貝”(shallow copy)。另一方面,“深拷貝”是指檢查準備復制的屬性本身是否是對象或者數組,如果是,也遍歷它們的屬性并復制。如果使用淺拷貝的話(因為在JavaScript中對象是按引用傳遞),如果你改變子對象的一個屬性,而這個屬性恰好是一個對象,那么你也會改變父對象。實際上這對方法來說可能很好(因為函數也是對象,也是按引用傳遞),但是當遇到其它的對象和數組的時候可能會有些意外情況。考慮這種情況: var dad = { counts: [1, 2, 3], reads: {paper: true} }; var kid = extend(dad); kid.counts.push(4); dad.counts.toString(); // "1,2,3,4" dad.reads === kid.reads; // true 現在讓我們來修改一下extend()函數以便做深拷貝。所有你需要做的事情只是檢查一個屬性的類型是否是對象,如果是,則遞歸遍歷它的屬性。另外一個需要做的檢查是這個對象是真的對象還是數組。我們可以使用第3章討論過的數組檢查方式。最終深拷貝版的extend()是這樣的: function extendDeep(parent, child) { var i, toStr = Object.prototype.toString, astr = "[object Array]"; child = child || {}; for (i in parent) { if (parent.hasOwnProperty(i)) { if (typeof parent[i] === "object") { child[i] = (toStr.call(parent[i]) === astr) ? [] : {}; extendDeep(parent[i], child[i]); } else { child[i] = parent[i]; } } } return child; } 現在測試時這個新的實現給了我們對象的真實拷貝,所以子對象不會修改父對象: var dad = { counts: [1, 2, 3], reads: {paper: true} }; var kid = extendDeep(dad); kid.counts.push(4); kid.counts.toString(); // "1,2,3,4" dad.counts.toString(); // "1,2,3" dad.reads === kid.reads; // false kid.reads.paper = false; kid.reads.web = true; dad.reads.paper; // true 通過復制屬性繼承的模式很簡單且應用很廣泛。例如Firebug(JavaScript寫的Firefox擴展)有一個方法叫extend()做淺拷貝,jQuery的extend()方法做深拷貝。YUI3提供了一個叫作Y.clone()的方法,它創建一個深拷貝并且通過綁定到子對象的方式復制函數。(本章后面將有更多關于綁定的內容。) 這種模式并不高深,因為根本沒有原型牽涉進來,而只跟對象和它們的屬性有關。 <a name="a21"></a> ## 混元(Mix-ins) 既然談到了通過復制屬性來繼承,就讓我們順便多說一點,來討論一下“混元”模式。除了前面說的從一個對象復制,你還可以從任意多數量的對象中復制屬性,然后將它們混在一起組成一個新對象。 實現很簡單,只需要遍歷傳入的每個參數然后復制它們的每個屬性: function mix() { var arg, prop, child = {}; for (arg = 0; arg < arguments.length; arg += 1) { for (prop in arguments[arg]) { if (arguments[arg].hasOwnProperty(prop)) { child[prop] = arguments[arg][prop]; } } } return child; } 現在我們有了一個通用的混元函數,我們可以傳遞任意數量的對象進去,返回的結果將是一個包含所有傳入對象屬性的新對象。下面是用法示例: var cake = mix( {eggs: 2, large: true}, {butter: 1, salted: true}, {flour: "3 cups"}, {sugar: "sure!"} ); 圖6-10展示了在Firebug的控制臺中用console.dir(cake)展示出來的混元后cake對象的屬性。 ![圖6-10 在Firebug中查看cake對象](https://box.kancloud.cn/2016-05-03_57281630d60cd.jpg) 圖6-10 在Firebug中查看cake對象 > 如果你習慣了某些將混元作為原生部分的語言,那么你可能期望修改一個或多個父對象時也影響子對象。但在這個實現中這是不會發生的事情。這里我們只是簡單地遍歷、復制自己的屬性,并沒有與父對象的鏈接。 <a name="a22"></a> ## 借用方法 有時候會有這樣的情況:你希望使用某個已存在的對象的一兩個方法,你希望能復用它們,但是又真的不希望和那個對象產生繼承關系,因為你只希望使用你需要的那一兩個方法,而不繼承那些你永遠用不到的方法。受益于函數方法call()和apply(),通過借用方法模式,這是可行的。在本書中,你其實已經見過這種模式了,甚至在本章extendDeep()的實現中也有用到。 如你所熟知的一樣,在JavaScript中函數也是對象,它們有一些有趣的方法,比如call()和apply()。這兩個方法的唯一區別是后者接受一個參數數組以傳入正在調用的方法,而前者只接受一個一個的參數。你可以使用這兩個方法來從已有的對象中借用方法: //call() example notmyobj.doStuff.call(myobj, param1, p2, p3); // apply() example notmyobj.doStuff.apply(myobj, [param1, p2, p3]); 在這個例子中有一個對象myobj,而且notmyobj有一個用得著的方法叫doStuff()。你可以簡單地臨時借用doStuff()方法,而不用處理繼承然后得到一堆myobj中你永遠不會用的方法。 你傳一個對象和任意的參數,這個被借用的方法會將this綁定到你自己的對象上。簡單地說,你的對象會臨時假裝成另一個對象以使用它的方法。這就像實際上獲得了繼承但又免除了“繼承稅”(指你不需要的屬性和方法)。 <a name="a23"></a> ### 例:從數組借用 這種模式的一種常見用法是從數組借用方法。 數組有很多很有用但是一些“類數組”對象(如arguments)不具備的方法。所以arguments可以借用數組的方法,比如slice()。這是一個例子: function f() { var args = [].slice.call(arguments, 1, 3); return args; } // example f(1, 2, 3, 4, 5, 6); // returns [2,3] 在這個例子中,有一個空數組被創建了,因為要借用它的方法。同樣的事情也可以使用一種看起來代碼更長的方法來做,那就是直接從數組的原型中借用方法,使用Array.prototype.slice.call(...)。這種方法代碼更長一些,但是不用創建一個空數組。 <a name="a24"></a> ### 借用并綁定 當借用方法的時候,不管是通過call()/apply()還是通過簡單的賦值,方法中的this指向的對象都是基于調用的表達式來決定的。但是有時候最好的使用方式是將this的值鎖定或者提前綁定到一個指定的對象上。 我們來看一個例子。這是一個對象one,它有一個say()方法: var one = { name: "object", say: function (greet) { return greet + ", " + this.name; } }; // test one.say('hi'); // "hi, object" 現在另一個對象two沒有say()方法,但是它可以從one借用: var two = { name: "another object" }; one.say.apply(two, ['hello']); // "hello, another object" 在這個例子中,say()方法中的this指向了two,this.name是“another object”。但是如果在某些場景下你將th函數賦值給了全局變量或者是將這個函數作為回調,會發生什么?在客戶端編程中有非常多的事件和回調,所以這種情況經常發生: // assigning to a variable // `this` will point to the global object var say = one.say; say('hoho'); // "hoho, undefined" // passing as a callback var yetanother = { name: "Yet another object", method: function (callback) { return callback('Hola'); } }; yetanother.method(one.say); // "Holla, undefined" 在這兩種情況中say()中的this都指向了全局對象,所以代碼并不像我們想象的那樣正常工作。要修復(換言之,綁定)一個方法的對象,我們可以用一個簡單的函數,像這樣: function bind(o, m) { return function () { return m.apply(o, [].slice.call(arguments)); }; } 這個bind()函數接受一個對象o和一個方法m,然后把它們綁定在一起,再返回另一個函數。返回的函數通過閉包可以訪問到o和m。也就是說,即使在bind()返回之后,內層的函數仍然可以訪問到o和m,而o和m會始終指向原始的對象和方法。讓我們用bind()來創建一個新函數: var twosay = bind(two, one.say); twosay('yo'); // "yo, another object" 正如你看到的,盡管twosay()是作為一個全局函數被創建的,但this并沒有指向全局對象,而是指向了通過bind()傳入的對象two。不論無何調用twosay(),this將始終指向two。 綁定是奢侈的,你需要付出的代價是一個額外的閉包。 <a name="a25"></a> ### Function.prototype.bind() ECMAScript5在Function.prototype中添加了一個方法叫bind(),使用時和apply和call()一樣簡單。所以你可以這樣寫: var newFunc = obj.someFunc.bind(myobj, 1, 2, 3); 這意味著將someFunc()主myobj綁定了并且傳入了someFunc()的前三個參數。這也是一個在第4章討論過的部分應用的例子。 讓我們來看一下當你的程序跑在低于ES5的環境中時如何實現Function.prototype.bind(): if (typeof Function.prototype.bind === "undefined") { Function.prototype.bind = function (thisArg) { var fn = this, slice = Array.prototype.slice, args = slice.call(arguments, 1); return function () { return fn.apply(thisArg, args.concat(slice.call(arguments))); }; }; } 這個實現可能看起來有點熟悉,它使用了部分應用,將傳入bind()的參數串起來(除了第一個參數),然后在被調用時傳給bind()返回的新函數。這是用法示例: var twosay2 = one.say.bind(two); twosay2('Bonjour'); // "Bonjour, another object" 在這個例子中,除了綁定的對象外,我們沒有傳任何參數給bind()。下一個例子中,我們來傳一個用于部分應用的參數: var twosay3 = one.say.bind(two, 'Enchanté'); twosay3(); // "Enchanté, another object" <a name="a26"></a> ##小結 在JavaScript中,繼承有很多種方案可以選擇。學習和理解不同的模式是有好處的,因為這可以增強你對這門語言的掌握能力。在本章中你看到了很多類式繼承和現代繼承的方案。 但是,也許在開發過程中繼承并不是你經常面對的一個問題。這一部分是因為這個問題已經被使用某種方式或者某個你使用的類庫解決了,另一部分是因為你不需要在JavaScript中建立很長很復雜的繼承鏈。在靜態強類型語言中,繼承可能是唯一可以利用代碼的方法,但在JavaScript中你可能有更多更簡單更優化的方法,包括借用方法、綁定、復制屬性、混元等。 記住,代碼復用才是目標,繼承只是達成這個目標的一種手段。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看