深刻淺出JS的封裝與繼承

    JS雖然是一個面向對象的語言,可是不是典型的面嚮對象語言。Java/C++的面向對象是object - class的關係,而JS是object - object的關係,中間經過原型prototype鏈接,父類和子類造成一條原型鏈。本文經過分析JS的對象的封裝,再探討正確實現繼承的方式,而後討論幾個問題,最後再對ES6新引入的類class關鍵字做一個簡單的說明。html

    JS的類實際上是一個函數function,因爲不是典型的OOP的類,所以也叫僞類。理解JS的類,須要對JS裏的function有一個比較好的認識。首先,function自己就是一個object,能夠看成函數的參數,也能夠看成返回值,跟普通的object無異。而後function能夠看成一個類來使用,例如要實現一個String類git

    第一行聲明瞭一個MyString的函數,獲得一個MyString類,同時這個函數也是MyString的構造函數。第5行new一個對象,會去執行構造函數,this指向新產生的對象,第2行給這個對象添加一個content的屬性,而後將新對象的地址賦值給name。第6行又去新建一object,注意這裏的this指向了新的對象,所以新產生的content和前面是不同的。github

    上面的代碼在瀏覽器運行有一點問題,由於這段代碼是在全局做用域下運行,定義的name變量也是全局的,所以實際上執行var name = new MyString(「」)等同於window.name = new MyString(「」),因爲name是window已經存在的一個變量,做爲window.open的第二個參數,可用來跨域的時候傳數據。但因爲window.name不支持設置成自定義函數的實例,所以設置無效,仍是保持默認值:值爲」[object Object]」的String。解決辦法是把代碼的運行環境改爲局部的,也就是說用一個function包起來:web

    因此今後處看到,代碼用一個function包起來,不去污染全局做用域,仍是挺有必要的。接下來,回到正題。chrome

    JS裏的每個function都有一個prototype屬性,這個屬性指向一個普通的object,即存放了這個object的地址。這個function new出來的每一個實例都會被帶上一個指針(一般爲__proto__)指向prototype指向的那個object。其過程相似於:編程

    以下圖所示,name和addr的__proto__指向MyString的prototype對象:設計模式

72

    能夠看出在JS裏,將類的方法放在function的prototype裏面,它的每一個實例都將得到類的方法。       跨域

    如今爲MyString添加一個toString的方法:瀏覽器

   MyString的prototype對象(object)將會添加一個新的屬性。app

73

   這個時候實例name和addr就擁有了這個方法,調用這個方法:

    這樣就實現了基本的封裝——類的屬性在構造函數裏定義,如MyString的content;而類的方法在函數的prototype裏添加,如MyString的toString方法。

    這個時候,考慮一個基礎的問題,爲何在原型上添加的方法就能夠被類的對象引用到呢?由於JS首先會在該對象上查找該方法,若是沒有找到就會去它的原型上查找。例如執行name.toString(),第一步name這個object自己沒有toString(只有一個content屬性),因而向name的原型對象查找,即__proto__指向的那個object,發現有toString這個屬性,所以就找到了。

    要是沒有爲MyString添加toString方法呢?因爲MyString其實是一個Function對象,上面定義MyString語法做用等效於:

經過比較MyString和Function的__proto__,能夠從側面看出MyString實際上是Function的一個實例:

    MyString的__proto__的指針,指向Function的prototype,經過瀏覽器的調試功能,能夠看到,這個原型就是Object的原型,以下圖所示:

 74

 

    由於Object是JS裏面的根類,全部其它的類都繼承於它,這個根類提供了toString、valueOf等6個方法。

    所以,找到了Object原型的toString方法,查找完成並執行:

到這裏能夠看到,JS裏的繼承就是讓function(如MyString)的原型的__proto__指向另外一個function(如Object)的原型。基於此,寫一個自定義的類UnicodeString繼承於MyString

    實現繼承:

    注意上面的繼承方法是錯誤的,這樣只是簡單的將UString的原型指向了MyString的原型,即UString和MyString使用了相同的原型,子類UString增刪改原型的方法,MyString也會相應地變化,另一個繼承MyString如AsciiString的類也會相應地變化。依照上文分析,應該是讓UString的原型裏的的__proto__屬性指向MyString的原型,而不是讓UString的原型指向MyString。也就是說,得讓UString有本身的獨立的原型,在它的原型上添加一個指針指向父類的原型:

由於__proto__不是一個標準的語法,在有些瀏覽器上是不可見的,若是在Firefox上運行上面這段代碼,Firefox會給出警告:

mutating the [[Prototype]] of an object will cause your code to run very slowly; instead create the object with the correct initial [[Prototype]] value using Object.create

    合理的作法應該是讓prototype等於一個object,這個object的__proto__指向父類的原型,所以這個object需要是一個function的實例,而這個function的prototype指向父類的原型,因此得出如下實現:

   代碼第2行,定義一個臨時的function,第3行讓這個function的原型指向父類的原型,第4行返回一個實例,這個實例的__proto__就指向了父類的prototype,第7行再把這個實例賦值給子類的prototype。繼承的實現到這裏基本上就完成了。

   可是還有一個小問題。正常的prototype裏面會有一個constructor指向構造函數function自己,例如上面的MyString:

75

    這個constructor的做用就在於,可在原型裏面調用構造函數,例如給MyString類增長一個copy拷貝函數:

   問題就於:Object.create的那段代碼裏第7行,徹底覆蓋掉了UString的prototype,取代的是一個新的object,這個object的__proto__指向父類即MyString的原型,所以UString.prototype.constructor在查找的時候,UString.prototype沒有constructor這個屬性,因而向它指向的__proto__查找,找到了MyString的constructor,所以UString的constructor其實是MyString的constuctor,以下所示,ustr2其實是MyString的實例,而不是指望的UString。而不用constructor,直接使用名字進行調用(上面代碼第2行)也會有這個問題。

    因此實現繼承後須要加多一步操做,將子類UString原型裏的constructor指回它本身:

    在執行copy函數裏的this.constructor()時,實際上就是UString()。這時候再作instanseof判斷就正常了:

    能夠把相關操做封裝成一個函數,方便複用。

    基本的繼承核心的地方到這裏就結束了,接下來還有幾個問題須要考慮。

    第一個是子類構造函數裏如何調用父類的構造函數,直接把父類的構造函數看成一個普通的函數用,同時傳一個子類的this指針:

    注意第3行傳了一個this指針,在調用MyString的時候,這個this就指向了新產生的UString對象,若是直接使用第2行,那麼執行的上下文是window,this將會指向window,this.content = str等價於window.content = str。

    第二個問題是私有屬性的實現,在最開始的構造函數裏定義的變量,其實例是公有的,能夠直接訪問,以下:

    可是典型的面向對象編程裏,屬性應該是私有的,操做屬性應該經過類提供的方法/接口進行訪問,這樣才能達到封裝的目的。在JS裏面要實現私有,得藉助function的做用域:

可是這樣的一個問題是,必須將函數的定義放在構造函數裏,而不是以前討論的原型,致使每生成一個實例,就會給這個實例添加一個如出一轍的函數,形成內存空間的浪費。因此這樣的實現是內存爲代價的。若是產生不少實例,內存空間會大幅增長,這個問題是不可忽略的,所以在JS裏面實現屬性私有不太現實,即便在ES6的class語法也沒有實現。可是能夠給類添加靜態的私有成員變量,這個私有的變量爲類的全部實例所共享,以下面的案例:

    上面的例子使用了類的靜態變量,給每一個worker產生惟一的id。同時這個id是不容許worker實例直接修改的。

    第三個問題是虛函數,在JS裏面討論虛函數是沒有太大的意義的。虛函數的一個很大的做用是實現運行時的動態,這個運行時的動態是根據子類的類型決定的,可是JS是一種弱類型的語言,子類的類型都是var,只要子類有相應的方法,就能夠傳參「多態」運行了。比強類型的語言如C++/Java做了很大的簡化。

    最後再簡單說下ES6新引入的class關鍵字

    從輸出的結果來看,新的class仍是沒有實現屬性私有的功能,見第27行。而且從第26行看出,所謂的class其實就是編譯器幫咱們實現了上面複雜的過程,其本質是同樣的,可是讓代碼變得更加簡化明瞭。一個不一樣點是,多了static關鍵字,直接用類名調用類的函數。ES6的支持度還不高,最新的chrome和safari已經支持class,firefox的支持性還不太好。

    最後,雖然通常的網頁的JS不少都是小工程,看似不須要封裝、繼承這些技術,可是若是若是可以用面向對象的思想編寫代碼,無論工程大小,只要應用得當,甚至結合一些設計模式的思想,會讓代碼的可維護性和擴展性更高。因此平時能夠嘗試着這樣寫。

 

原博客園地址:http://www.cnblogs.com/yincheng/p/4943789.html

參考:

1. Professional Javascript for web developers(JavaScript高級程序設計) 第6章 Object – Oriented Programming

2. The Node Craftsman Book第一部分 Object-oriented JavaScript

3. Why is it necessary to set the prototype constructor Stackoverflow

相關文章
相關標籤/搜索