學習NodeJS第五天:JavaScript的繼承

來源:互聯網
上載者:User
文章目錄
  • 一、process.inherits() v.s sys.inherits()
  • 二、基於對象
  • 三、類
  • 四、結語
  • 參考:

    人們接觸 JavaScript,都被他單純的外表給騙了,殊不知,一下子又 FP 又 OO 又前台又跑到後台,活蹦亂跳。一旦你遇到某些障礙,面對的 JavaScript 也表現得脾氣好,你怎麼弄它,改造它,它也不會生氣,卻太容易讓人迷惑,造成生氣的居然是你或者我。真不知道是你玩 JS 還是變成 JS 玩你……

    許多人被 JS “蠱惑”過之後,深感不爽,立意要重新改造乃萬惡的 JS,首當其衝抓住的是便是“原型繼承(Prototypical Inherit)”。關於“原型繼承”和“類繼承(Class Inherit)”,JavaScript 業界教父、Yahoo!UI 架構師 Douglas Crockford(D.C.) 認為是派別的問題(School),就像 FP 函數式較之於 OO,OO 蔚然成為主流卻不等於 FP 便消退其光芒,而難以能成為為一宗一派立論,否則便是非黑即白的二元對立。

    如右圖是 D.C 本人,老人家了,常言道,老馬識途,呵呵。

    D.C 主要的意思是,學術上討論“原型繼承”向來佔有一席位置,也有一批語言的思想亦立足於此“原型繼承”,但是,當今人們之所以不認識或少見識“原型繼承”的 OO 方法論,本質裡頭受 Java/C# 一派的影響,造成熟悉“類繼承”的人群就占絕大多數。而回到“原型繼承”的問題上,“原型繼承”肯定也有“原型繼承”的優點,有其可取的地方,不然也不能自成一派。至於具體是什麼的優點?恕在下技淺、鮮知,大家有空問 Google 或 D.C 的文章當可,讓小弟說也是重複 D.C 說過的話。不管怎麼樣,甚幸 D.C
如此替 JavaScript  的“原型繼承”說話,儘管大家還不容易接受,然而那自然是一定無疑的——試問,你我眼中,類的概念已經普遍深入民心,根深蒂固,怎麼可以說改就改?過於顛覆了吧,於是你我繼續改造 JavaScript 的繼承,使之符合為自己一套的生產經驗,去實踐應用……

    隨著 JavaScript 一路發展,現在已有幾套可實作類別的繼承的方式或者途徑呈現在大家面前,如今 NodeJS 的繼承卻是怎麼的一種樣子呢?咱們一起觀察一下吧。

     NodeJS 的繼承一方面沒摒棄原型繼承,一方面也大量應用類繼承,一個類繼承一個類,一個類繼承一個類下去……sys.inherits() 即是繼承任意兩個類的方法。該方法支援傳入兩個 function 參數:sys.inherits(subFn, baseFn);,sunFn 是子類,baseFn 是父類。

一、process.inherits() v.s sys.inherits()

    值得稍作討論的是繼承方法所屬的命名空間。原本 inherits() 是依存在 process 對象身上的,後來改為 sys 對象。如果使用者鍵入 process.inherits(...) 舊方法,NodeJS 會提示你這個用法已經棄置了,改用 sys.inherits ,即源碼中:

……<br />process.inherits = removed("process.inherits() has moved to sys.inherits.");<br />……

    新版 Nodejs 還有其他 API 命名的修改,inherits 只是其中的一項。顯然作者 Ry 作修改有他自己的原因,才會有這樣的決定,新版總是比舊版來的有改進,但有沒有其他人建議他那樣做卻無從而知了:)。不過私下判斷,從語意上來說繼承方法應該定義在語言核心層次的,至少在 sys(System)上比在 process 的語意更為合適,更為貼切。要不然,進程 process 怎麼會有繼承的功能呢,感覺怪怪的,呵呵。不過話說回來,sys 必須要 require 一下才能用,而 process 卻是預設的全域對象,不需要使用到
require 才能訪問。

    再說說 inherits() 這個方法和本身這個 inherits 單詞性質(呵,真是無聊的俺)。君不見,許多的JS庫都有專門針對繼承的方法,Prototype.js 的 extend 純粹是拷貝對象,早期 jQuery 還尚未考慮所謂“繼承”,還好留有餘地,後來作者 John 把 JavaScript 繼承堆到一個層次,連模仿 Java 的 super() 語句都有,實現了基於類 Class 的 JavaScript 繼承。為此 John 還寫了博文,特別是這篇博文,讓好奇的我很受益,瞭解脫殼的
JS 的方法繼承——當然,那些是後話了,不過不能不提的是 jQuery 的繼承方法其命名可是“extend()”,而且再說 YUI/ExtJS 之流亦概莫如是,然而為啥這個 NodeJS 的繼承方法管叫做 inherits 呢,並有意無意地還加上第三人稱的 -s 的時態!話說 inherits 照譯也都是“繼承”的意思,跟足了 OO 的原意,但卻好像沒有 extend 好記好背,敲鍵盤時便知道……

    前面交待了一些背景後,目的只是想增加大家對繼承 inherit 的興趣,以便接著更深入主題。好了,真的進入主題,立馬看看 sys.inherits 源碼(exports.inherits,lib/sys.js第327行):

/**<br /> * Inherit the prototype methods from one constructor into another.<br /> *<br /> * The Function.prototype.inherits from lang.js rewritten as a standalone<br /> * function (not on Function.prototype). NOTE: If this file is to be loaded<br /> * during bootstrapping this function needs to be revritten using some native<br /> * functions as prototype setup using normal JavaScript does not work as<br /> * expected during bootstrapping (see mirror.js in r114903).<br /> *<br /> * @param {function} ctor Constructor function which needs to inherit the<br /> * prototype<br /> * @param {function} superCtor Constructor function to inherit prototype from<br /> */<br />exports.inherits = function (ctor, superCtor) {<br /> ctor.super_ = superCtor;<br /> ctor.prototype = Object.create(superCtor.prototype, {<br /> constructor: {<br /> value: ctor,<br /> enumerable: false<br /> }<br /> });<br />};

    看來 NodeJS 有點特殊,與 yui、ext 的實現不太一樣。可是,究竟是什麼道理令到這個繼承方法與眾不同呢?依據源碼錶述,比較關鍵的是,似乎在於 Object.create() 該方法之上。Object.create() 又是什麼呢?要皰丁解牛,揭開謎底的答案,我們可以從“基於對象的繼承”和“基於類的繼承”的認識來入手。

二、基於對象

    首先是“基於對象”的繼承。“基於對象”繼承的概念,是可以允許沒有“類(Class)”的概念存在的。所有的對象都是對 Object 的繼承。我要從一個父物件,得到一個新的子物件,例如,兔子可以由“動物”這一對象直接繼承。我們在js中:

// 定義父物件animal<br />var animal = new Object();<br />animal.age = new Number();<br />animal.eat = function(food){...}<br />// 定義子物件兔子rabbit<br />var rabbit = new Object();<br />rabbit.__proto__ = animal;

    所以這裡我們一律說“什麼、什麼對象”,而不出現“類”。Object 就是最原始的“對象”,處於頂層的父物件。JavaScript 中任何子物件其終極的對象便是這個 Object。“new Object”的意思是調用命令符 new,執行 Object 建構函式。這是一個空的對象。animal.age = new Number();這一句是分配一個名叫 age 的屬性予以 animal 對象,其類型是數字 number;animal.eat = function(food){...} 就是分配一個名叫
eat 的方法予以 animal 對象,其參數是 food 食物。這樣,animal 動物對象擁有了年齡 age 的屬性和吃 eat 的方法,形成一個標準的對象。

    接著,因為兔子肯定符合對象的意思,所以先聲明一個Null 物件,賦予給 rabbit 變數。像這句話:var rabbit = new Object(); 然後注意了,rabbit.__proto__ = animal;就是建立繼承關係的語句。_proto_ 是任何對象都有的屬性(前提是在 Firefox 的JS Enginer 運行下),也就是說每一個對象都有 _proto_ 屬性。改變_proto_ 的指向等於改變對象原型——也就是我們所說的“父物件”到底是哪一個。沒錯就是這麼簡單完成的JavaScript的繼承。

    但是有一個相容性的問題。這麼好用的_proto_居然只准在 Firefox 的 JS 引擎中開放,別家的 JS 引擎就不能夠讓程式員接觸得到。(Thx toqinfanpeng)原因有多種多樣。總之不能夠這樣直接使用——無法使用。

    也就是說不提倡 _proto_ 的用法。還好我們知道 JavaScript 作為動態語言,是支援晚綁定的特性的,就是可以讓使用者任意在某個對象上添加或刪除某個成員。既然可以那樣,我們就可以透過複製對象的成員達到派生新的對象的操作,如下例:

// 定義父物件animal<br />var animal = new Object();<br />animal.age = new Number();<br />animal.eat = function(food){...}<br />// 定義子物件兔子rabbit<br />var rabbit = new Object();<br />for(var i in animal){<br /> rabbit[i] = animal[i];<br />}

    寫一個 for 列出 animal 身上的所有成員,統統複製到 rabbit 這樣原本空的對象身上。迴圈過後就算達到“繼承”之目的了。再提煉一下,將 for 寫成一個通用的 apply() 方法,如下:

Object.apply = function(superObject, sonObject){<br /> for(var i in superObject)<br /> sonObject[i] = sonObject[i];<br />}

    應當指出,上面的“複製成員理念”可以是可以,並且運行無誤,但大家有沒有留意到,apply() 主要是一個 for(...){...} 迴圈。咱們一想到“迴圈語句”便很容易聯想到耗時、是否會引致死迴圈等的問題,都是不好的問題,所以看能不能使用這個 for 迴圈,總之可以避免迴圈就好。——問題的深層次就涉及到代碼是否優雅的問題:使用 apply() 被認為是不優雅的,尤其當越來越多使用 apply() 的時候,結果是遍布 for(...){...}。當然解決是否優雅最直接的方法是,JavaScript
語言提供直接可以代替 apply()。你們看,雖然那是如此小的問題,還是值得去修正,看來一再提倡的,追求完美、追求極致、追求越來越好可不是空喊的一句口號。

   於是,ECMAScript v5.0(一說 v3.1)也就是新版 JavaScript 規定了 Object.create() 方法,提供了一個由父物件派生子物件的方法。新用法如下:

// 定義父物件animal<br />var animal = new Object();<br />animal.age = new Number();<br />animal.eat = function(food){...}<br />var rabbit = Object.create(animal);

    非常直觀是吧~一個 create() 搞掂了~實則回頭看看也是表達要封裝 _proto_ 的這麼一層意義,此處順便給出實現的方法(嘮叨一下,除 Mozllia,V8 寫法亦如此,參見v8natives.js 第694行):

Object.create = function( proto) {<br /> var obj = new Object();<br /> obj.__proto__ = proto;</p><p> return obj;<br />};

當然,for 的方法也等價的,

Object.create = function( proto) {<br /> var obj = new Object();<br /> for(var i in proto)<br /> obj[i] = proto[i];</p><p> return obj;<br />};

    如果你偏要走捷徑,僅僅理解 es3.1 的改變只是換了馬甲的話,變為 Object.create(),那隻能說是“捷徑”。其實它背後還有其他內容的(一些過程、一些參數……若干原理),俺作了刪減,但絕不影響主幹意思。如來大家能夠理解到這裡,就不錯了,留個機會大家發掘其他的內容,也省得我費舌^_^。(重點提示那個 constructor,在第二參數)。

    到了這裡已經完成了第一個派別“基於對象”的繼承。我覺得,“基於對象繼承”的說法可以說是多餘的,因為對象就像一個框,什麼都可往裡在裝。繼承除了為物件服務外總不會指別的的意思吧!?所以基於對象的說法,可以說,只為後來,出現更高明的其他思想與之相對,才有基於對象的說法。

    到這裡,可以瞭解“原型繼承(Prototypical Inherit)”是怎麼一回事了。process.inherits 它的原理,在揭開 Object.create() 神秘面紗後,大概已經呼之欲出了。

三、類

   前文裡頭賣了一個關子,所謂更高明的“思想”,就是類啦!表面上,類其實和對象沒什麼不同,也有方法、屬性、事件等的概念,實際上,類就是對象的模板。好,明確這點後,我們清楚“類”作為一種特殊的“事物”,當然也不是憑空而生的。下面的JS語句結果是一樣的,我們可以通過兩者對比理解一下由“對象”到“類對象”的過程:

// 定義一個JS類(類在js中表現為Function)<br />function Foo(){<br /> // ……構造器過程<br />}<br />var o = new Object();<br />o.constructor = Foo;<br />Foo.call(o); // <---此步就是調用Foo(),更確切地說,是調用Foo建構函式。 其作用相當於var o = new Foo();

為什麼要 call() 呢?因為 new 命令調用構造器 function Foo(){},最後必然會返回 this 當前執行個體對象,即:

funtion Foo(){<br /> // ……構造器過程<br /> // return this;<br />}

    這個當前執行個體對象是啥呢?——在例子中便是 o 了。o 事先聲明罷了。這樣我們看到對象到類的“升華”!

    是不是還是覺得不夠透切呢?咱們還沒說完咧~我們可以結合兔子的例子,同樣是動物和兔子,寫成類,從而誕生了 JS 中一種類的寫法!

animal = function(){}<br />animal.prototype.age = new Number();<br />animal.prototype.eat = function(food){<br />}<br />rabbit = function(){}<br />rabbit.prototype = new animal();

    開玩笑了, 這這種寫法才是 JS 的地道寫法,老早就有了。上述寫法徹底告別一個對象一個對象去定義層次關係。簡單說,其涵義就是通過函數的原型 prototype,加多一層 function 來確定父物件是什麼。首先有個認識,就是比起“基於對象”的繼承,我們現在可以加入了“建構函式”,例如 animial = function(){} 和 rabbit = function(){} 分別就是父類的建構函式和子類的建構函式。但是如果我不需要子類的建構函式,卻又不行,因為不可能不寫一個 function,只有
function才可以有 prototype 屬性去定義成員。前面我們不是說過 _proto_是不開放的屬性嗎?惟獨 Function 的 _proto_ 就總是開放的,也就是說 Function 對象都有的_proto_的作用 apply() 和 call() 的作用,但是 _proto_ 的名字就變為沒有底線了,也就是 Function.prototype。況且 JS 之中,藉助 Function 定義對象的模板是經常的寫法,new 某個類就是建立對象,也讓 prototype 發揮定義繼承鏈作用。

    既然 Function.prototype 總是開放的,那麼用它代替 _proto_ 也行吧?沒錯,藉助一個空的建構函式就行了,原來 Object.create 也可以這樣寫的:

Object.create = function (o) {<br /> function F() {}<br /> F.prototype = o;<br /> return new F();<br /> }

當然這個 object 方法又迴歸到“基於對象繼承”的方法上了 呵呵。我們可以從 D.C 介紹過的方法看出一點源頭,藉助網路,這些淵源都是有跡可循的。詳見參考網址http://javascript.crockford.com/prototypal.html。實際上 Object.create 應該就從 D.C 方法來,好像他也是極力的推動者,不知道了……最後抄多個 Extend 代碼協助理解,原理沒啥區別,關鍵勝在夠簡單清晰。

extend = function (Klass, Zuper) {<br /> Klass.prototype = Object.create(Zuper.prototype);<br /> Klass.prototype.constructor = Klass;<br />}

四、結語

今天說的大概可分後兩部分,前部分打算從 nodejs 的繼承說開去,雖然不知道是不是廢話較多,以致於後半部分銜接得不夠好,但是還是說出了我的心底話,來來去去都是那幾樣事物,因為高度抽象,可能不易釐清,俺盡量也想說的圓,可不容易,他日有機會修本文章,這份讀到的便當草稿吧,有緣人請將就過目,感覺有疑的話請儘管斧正,當然有竅門請解囊告知,多多提攜!

參考:
  • http://ejohn.org/blog/ecmascript-5-objects-and-properties/
  • http://javascript.crockford.com/prototypal.html
  • Ext.extend()中使用super關鍵字
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.