全方位完全讀懂<你不知道的JavaScript(上)>--一篇六萬多字的讀書筆記

前言

Q&A

  • 1.問:爲何要寫這麼長,有必要嗎?是否是腦子秀逗了?
    答:我想這是大部分人看到這個標題都會問的問題.由於做爲一個男人,我喜歡長一點,也不喜歡分割成幾個部分.一家人就要在一塊兒,整整齊齊.好吧,正經點,其實整篇前言能夠說都是在回答這個問題.你能夠選擇先看完前言,再決定要不要和書本搭配起來閱讀. 這裏先簡單捋一下:1,內容多:首先這篇讀書筆記原本內容就不少,是對書本的全方位詳解.2,針對新人:針對那種紅寶書草草讀過一遍,對js只浮於接口調用的新手.3,留給讀者本身提煉:讀這種社科類書籍通常是先讀厚,再讀薄.這篇筆記就屬於最開始'讀厚'的階段.在讀者完全讀懂後,再本身進一步提煉.關於怎麼讀書,我後面會詳細介紹.前端

  • 2.問:這麼長,那到底包含了些什麼內容?
    答:筆記的目錄結構和書本的徹底一致.對每一節的內容進行更通俗的解讀(針對新人),對示例進行更深的說明,有的會輔以流程圖,並提供對應的mdn鏈接;對內容進行概括,小節脈絡更清晰;添加了大量實際工做時的注意事項,增長了更加清晰和易懂的示例及註釋,並在原文基礎上進行了拓展和總結;對書中的錯誤和說了後面會進行介紹,而沒有介紹的填坑,翻譯或者容易引發誤會的稱呼的說明;添加了我的讀書時的感覺和吐槽.vue

  • 3.問:書已經夠多了,還要看你這麼長的筆記?
    答:首先你要知道讀這種技術類書籍,不是讀小說!讀完並不意味着你讀懂了.而是須要將書中的知識轉換成你本身的.這篇筆記就是,幫助新手更方便地理解知識點,更流暢地進行閱讀.也能夠在讀完一節後,經過對比,發現本身有什麼知識點是不懂或者遺漏,理解有誤的. 而且一些注意事項,容易被誤導的,關於書中觀點的吐槽等等,其實想說的都已經寫在筆記裏了.node

  • 4.問:這本書到底怎麼樣,有沒有其餘人說的那麼好?
    答:這是一個先揚後抑的回答.首先毫無疑問這是一本很是不錯的書!它系統地全面地對JavaScript進行解讀,優勢缺點全都有.當你完全讀懂這本書後,你對JavaScript的幾乎全部疑問都會獲得解答(我對做用域是否是"對象"的疑問?也獲得瞭解答).但它也是有必定門檻的,若是你對JS不熟,經常使用接口都不熟,不少名詞的表層意思都不太理解.這本書並不適合你,你花在問谷歌孃的時間可能比你讀書的都長,讀起來也是隻知其一;不知其二;不一樣於其餘書,這本書不少時候沒有給出明確的概念定義,須要你本身反覆閱讀理解他的話.每一小節的脈絡結構也不是那麼清晰,有時候須要本身去梳理;不知道是否是翻譯的鍋,不少東西解釋得有點迷,原本很簡單,但卻說一堆並不經常使用的術語(可能國內不是這麼叫的),看得你一臉懵逼!有時候同一個概念,先後會出現三四個不一樣的名詞進行指代,沒有任何說明;整本書,具備很強的做者主觀情感在裏面.前半段,把JS捧得很高,說它引擎的各類優化好!但到後半段關於JavaScript中模擬類和繼承"的批評,說它們具備很大誤導性!更是嗤之以鼻!就差爆粗口了,好像JavaScript就是一個異教徒,應該綁在十字架上被燒死!可是他這樣的觀點,都是站在其餘類語言的角度來看待,產生的.我想更多的讀者多是隻接觸過JavaScript這一種語言,對他們來講,實際上是根本沒有這些"疑惑"的!jquery

讀書建議:

  • 1.不要抱任何功利和浮躁的心來讀書!
    這種以理論,概念爲主的書,其實你們都是不那麼願意讀的.一是讀起來很費勁,抽象.二是實際工做,幾乎不會用到,在如今浮躁的前端圈這是吃力不討好.那這本書最大的用處是什麼?沒錯,就是被不少人用來應付面試!? 這自己沒什麼問題,你讀懂系列三本書,全部涉及JS的面試都能輕鬆應對.可是當抱着功利心時,你更多的則是敷衍.對書中的概念進行機械的複製,再粘貼上本身膚淺的理解.OK,應付那些也是跟風的面試官足夠了.通常你回答了,他們也不會繼續往下問,問深了本身也不清楚,也很差否認你.若是你夠自信,'瞎扯'也能夠唬住.若是你答不上,臉皮厚的會讓你回去本身查.真正知道的面試官,其實都是會給你解釋的,他們也不會忙到差這點時間.其實他們內心也是很樂意展現本身學識豐富的一面.
    這種功利讀書方式,即便你讀完了(更多人是半途而廢),對你的技術也不會有任何幫助.由於讀完,你實際上是隻知其一;不知其二的.這樣反而更糟,甚至可能會對你以前JavaScript正確的理解產生混淆.webpack

  • 2.認認真真讀完一本書好過收藏一百篇相關文章(其實你壓根連一半都不會看)!ios

我一直認爲想系統弄懂一門知識,書本纔是最好的選擇,它絕對比你東拼西湊找來的一堆文章要好得多!如今前端圈隨便看看,一大堆全是原型鏈,閉包,this...這些內容.裏面的內容大同小異,不少理解也是比較淺顯,考慮的也比較片面.但浮躁的人就是喜歡這種文章,以爲本身收藏了,看了就完全理解了(!?).其實這些文章裏有不少都是借鑑了本書.程序員

首先,你必須知道知識都是有體系的,不是徹底獨立的.例如想要完全理解,原型鏈,閉包,this.就必須先弄清做用域和函數.知識都是環環相扣,相互關聯的.若是你想完全弄懂,仍是選擇讀書吧,由淺入深,全面理清全部知識點的關聯.記住 "只知其一;不知其二"永遠比"無知"更糟!(固然不懂裝懂,還振振有詞的人另當別論).es6

  • 3.如何讀書:先讀厚,再讀薄!
    首先先把書讀厚: 將每一節裏的全部知識點弄懂,不留遺漏.記下全部提到的知識點,並將重要的知識點高亮標識(電子書的話).而後在本身本地的MD筆記裏,按照必定的邏輯順序,儘可能用本身的話語進行闡述總結這些知識點.若是有讀幾遍也不理解的地方,能夠查詢MDN,結合本身的實際工做經驗,或者先圈起來,繼續往下讀,隨着後面理解的深刻,前面不懂的地方天然也就明瞭了.這篇讀書筆記就是帶你怎麼把書讀厚.
    而後把書讀薄: 這部分需讀者你本身在完全理解的基礎上,並站在全局的角度進行概括去總結.先是按章進行思惟導圖式的總結.而後章與章之間進行規律總結,並記住特例.例如:做用域與原型鏈都有一個相似的"就近原則",因爲就近原則因此就產生了"屏蔽".這些都是須要本身站在全局融會貫通的角度去總結.雖然網上有別人總結好的,但咱們不該該養成什麼都依賴別人,本身直接複製的習慣(若是你想一直作一個'複製粘貼'程序員的話).

第一部分 做用域和閉包

第一章 做用域是什麼

1.1 編譯原理

傳統編譯的三個步驟web

  • 1,分詞/詞法分析(Tokenizing/Lexing) : 這個過程會將由字符組成的字符串分解成(對編程語言來講)有意義的代碼塊,這些代碼塊被稱爲詞法單元(token)。例如,考慮程序var a = 2;。這段程序一般會被分解成 爲下面這些詞法單元:var、a、=、二、;。空格是否會被看成詞法單元,取決於空格在 這門語言中是否具備意義。
  • 2,解析/語法分析(Parsing): 這個過程是將詞法單元流(數組)轉換成一個由元素逐級嵌套所組成的表明了程序語法結構的樹。這個樹被稱爲「抽象語法樹」(Abstract Syntax Tree,AST)。var a = 2; 的抽象語法樹中可能會有一個叫做 VariableDeclaration 的頂級節點,接下來是一個叫做Identifier(它的值是a)的子節點,以及一個叫做 AssignmentExpression 的子節點。AssignmentExpression 節點有一個叫做 NumericLiteral(它的值是 2)的子節點。
  • 3,代碼生成: 將 AST 轉換爲可執行代碼的過程稱被稱爲代碼生成。這個過程與語言、目標平臺等息息相關。拋開具體細節,簡單來講就是有某種方法能夠將 var a = 2; 的 AST 轉化爲一組機器指令,用來建立一個叫做 a 的變量(包括分配內存等),並將一個值儲存在 a 中。

說明: 此處只需記住第一步:分詞/詞法分析.第二步:解析/語法分析,獲得抽象語法樹(AST).第三步:代碼生成,將抽象語法樹轉換爲機器指令.面試

JavaScript與傳統編譯的不一樣點:

  • 1,JavaScript 引擎不會有大量的(像其餘語言編譯器那麼多的)時間用來進行優化.
  • 2,JavaScript與傳統的編譯語言不一樣,它不是在構建以前提早編譯的,大部分狀況下,它是在代碼執行前的幾微秒(甚至更短)進行編譯.
  • 3,JavaScript 引擎用盡了各類辦法(好比 JIT,能夠延 遲編譯甚至實施重編譯)來保證性能最佳。
  • 4,JavaScript的編譯結果不能在分佈式系統中進行移植。

1.2 理解做用域

1.2.1 演員表(代碼編譯到執行的參與者)

首先介紹將要參與到對程序 var a = 2; 進行處理的過程當中的演員們,這樣才能理解接下來將要聽到的對話。

  • 引擎 從頭至尾負責整個 JavaScript 程序的編譯及執行過程。
  • 編譯器 引擎的好朋友之一,負責語法分析及代碼生成等髒活累活(詳見前一節的內容)。
  • 做用域 引擎的另外一位好朋友,負責收集並維護由全部聲明的標識符(變量)組成的一系列查詢,並實施一套很是嚴格的規則,肯定當前執行的代碼對這些標識符的訪問權限。

1.2.2 對話(代碼編譯執行過程)

JavaScript對var a =2;的處理過程

1.2.3 做用域的LHS查詢和RHS查詢

由上圖可知,引擎在得到編譯器給的代碼後,還會對做用域進行詢問變量.

如今將例子改成var a = b;此時引擎會對變量a和變量b都向做用域進行查詢.查詢分爲兩種:LHS和RHS.其中L表明左.R表明右.即對變量a進行LHS查詢.對變量b進行RHS查詢.

單單從表象上看.LHS就是做用域對=左邊變量的查詢.RHS就是做用域對=右邊變量的查詢.但實際上並非這麼簡單,首先LHS和RHS都是對變量進行查詢,這也是我爲何要將例子從var a=2;改成var a=b;二者的區別是二者最終要查詢到的東西並不一致.LHS是要查詢到變量的聲明(而不是變量的值),從然後面能夠爲其賦值.RHS是要查詢到變量最終的值.還有一點,LHS 和 RHS 的含義是「賦值操做的左側或右側」並不必定意味着就是「= 賦值操做符的左側或右側」。賦值操做還有其餘幾種形式,所以在概念上最 好將其理解爲「賦值操做的目標是誰(LHS)」以及「誰是賦值操做的源頭(RHS)」.或者這樣理解若是這段代碼須要獲得該變量的'源值',則會進行RHS查詢.

1.2.4 引擎和做用域的對話

這部分比較簡單就是經過擬人方式比喻引擎和做用域的合做過程.一句話歸納就是,引擎進行LHS和RHS查詢時都會找做用域要.

function foo(a) { 
  console.log( a ); // 2
}
foo( 2 );
複製代碼

讓咱們把上面這段代碼的處理過程想象成一段對話,這段對話多是下面這樣的。

引擎:我說做用域,我須要爲 foo 進行 RHS 引用。你見過它嗎?
做用域:別說,我還真見過,編譯器那小子剛剛聲明瞭它。它是一個函數,給你。
引擎:哥們太夠意思了!好吧,我來執行一下 foo。
引擎:做用域,還有個事兒。我須要爲 a 進行 LHS 引用,這個你見過嗎?
做用域:這個也見過,編譯器最近把它聲名爲 foo 的一個形式參數了,拿去吧。
引擎:大恩不言謝,你老是這麼棒。如今我要把 2 賦值給 a。
引擎:哥們,很差意思又來打擾你。我要爲 console 進行 RHS 引用,你見過它嗎?
做用域:咱倆誰跟誰啊,再說我就是幹這個。這個我也有,console 是個內置對象。 給你。
引擎:麼麼噠。我得看看這裏面是否是有 log(..)。太好了,找到了,是一個函數。
引擎:哥們,能幫我再找一下對 a 的 RHS 引用嗎?雖然我記得它,但想再確認一次。
做用域:放心吧,這個變量沒有變更過,拿走,不謝。
引擎:真棒。我來把 a 的值,也就是 2,傳遞進 log(..)。

1.3做用域嵌套

當一個塊或函數嵌套在另外一個塊或函數中時,就發生了做用域的嵌套。進而造成了一條做用域鏈.所以,在當前做用 域中沒法找到某個變量時,引擎就會在外層嵌套的做用域中繼續查找,直到找到該變量, 或抵達最外層的做用域(也就是全局做用域)爲止。

當引擎須要對做用域進行查詢時.引擎會從當前的執行做用域開始查找變量,若是找不到, 就向上一級繼續查找。當抵達最外層的全局做用域時,不管找到仍是沒找到,查找過程都 會中止。

1.4 異常

例子:

function foo(a) { 
  console.log( a + b ); 
  b = a;
}
foo( 2 );
複製代碼
  • 若是 RHS 查詢在全部嵌套的做用域中遍尋不到所需的變量,引擎就會拋出 ReferenceError 異常。例如上面例子中console.log(a+b)因爲RHS此時是找不到b的值.故會拋出ReferenceError.
  • 若是 RHS 查詢找到了一個變量,可是你嘗試對這個變量的值進行不合理的操做, 好比試圖對一個非函數類型的值進行函數調用,或着引用 null 或 undefined 類型的值中的 屬性,那麼引擎會拋出另一種類型的異常,叫做 TypeError
  • 當引擎執行 LHS 查詢時,若是在頂層(全局做用域)中也沒法找到目標變量,全局做用域中就會建立一個具備該名稱的變量,並將其返還給引擎,前提是程序運行在非 「嚴格模式」下。例如上面例子中的b=a;.
  • 在嚴格模式中 LHS 查詢失敗時,並不會建立並返回一個全局變量,引擎會拋出同 RHS 查詢 失敗時相似的 ReferenceError 異常。

1.5 LHS與RHS小結

  • LHS和RHS查詢都是引擎對做用域的查詢
  • LHS和RHS查詢都是隻對變量進行查詢
  • LHS和RHS都會沿着做用域鏈進行查詢,直到最上層的全局做用域.若是沒找到的話,在非嚴格模式下,LHS則會在全局建立一個相同名稱的變量.RHS則會拋出ReferenceError的異常.
  • 若是查找的目的是對變量進行賦值,那麼就會使用 LHS 查詢;若是目的是獲取變量的值,就會使用 RHS 查詢。
  • LHS只是找到變量的容器而已,方便進行賦值
  • =操做符或調用函數時傳入參數的操做都會致使關聯做用域的賦值操做。此時都會進行LHS查詢
  • RHS查詢則須要找到變量的值.

第二章 詞法做用域

做用域分爲兩種工做模式:

  • 1,詞法做用域.是目前最爲廣泛的,被大多數編程語言所採用的模式.固然JavaScript也是使用的詞法做用域.
  • 2,動態做用域.使用較少,好比 Bash 腳本、Perl 中的一些模式等.

2.1 詞法階段

詞法階段: 大部分標準語言編譯器的第一個工做階段叫做詞法化(也叫單詞化)。詞法化的過程會對源代碼中的字符進行檢查,若是是有狀態的解析過程,還會賦予單詞語義。

詞法做用域: 詞法做用域就是定義在詞法階段的做用域也被稱爲靜態做用域。即在JavaScript裏做用域的產生是在編譯器出來的第一階段詞法階段產生的,而且是你在書寫完代碼時就已經肯定了的.

詞法做用域位置: 詞法做用域位置範圍徹底由寫代碼期間函數所聲明的位置來決定.

理解詞法做用域及嵌套: 看下例子:

function foo(a) { 
  var b = a * 2;
  
  function bar(c) { 
    console.log( a, b, c );
  }

  bar( b * 3 ); 
}
foo( 2 ); // 2, 4, 12

複製代碼

在這個例子中有三個逐級嵌套的做用域。爲了幫助理解,能夠將它們分紅3個逐級包含的"氣泡做用域"。

  • 1:包含着整個全局做用域,其中只有一個標識符:foo。
  • 2:包含着 foo 所建立的做用域,其中有三個標識符:a、bar 和 b。
  • 3:包含着 bar 所建立的做用域,其中只有一個標識符:c。

注意: 沒有任何函數的氣泡能夠(部分地)同時出如今兩個外部做用域的氣泡中,就如同沒有任何函數能夠部分地同時出如今兩個父級函數中同樣。

引擎對做用域的查找:
這一部分在上一節中已經說過,就是從當前做用域逐級向上,直到最上層的全局做用域.這裏再進一步進行講解.做用域查找會在找到第一個匹配的標識符時中止。在多層的嵌套做用域中能夠定義同名的標識符,這叫做「遮蔽效應」(內部的標識符「遮蔽」了外部的標識符)。拋開遮蔽效應, 做用域查找始終從運行時所處的最內部做用域開始,逐級向外或者說向上進行,直到碰見第一個匹配的標識符爲止。

注意:

  • 全局變量會自動成爲全局對象(好比瀏覽器中的 window對象)的屬性,所以能夠不直接經過全局對象的詞法名稱,而是間接地經過對全局對象屬性的引 用來對其進行訪問。例如:window.a 經過這種技術能夠訪問那些被同名變量所遮蔽的全局變量。但非全局的變量若是被遮蔽了,不管如何都沒法被訪問到。
  • 詞法做用域查找只會查找一級標識符,好比 a、b 和 c。若是代碼中引用了 foo.bar.baz,詞法做用域查找只會試圖查找 foo 標識符,找到這個變量後,對象屬性訪問規則會分別接管對 bar 和 baz 屬性的訪問。

2.2 欺騙詞法

欺騙詞法: 引擎在運行時來「修改」(也能夠說欺騙)詞法做用域.或者說就是在引擎運行時動態地修改詞法做用域(原本在編譯詞法化就已經肯定的).

欺騙詞法的兩種機制:(下面這兩種機制理解了解便可,不推薦實際開發使用)

2.2.1 eval

JavaScript 中的 eval(..) 函數能夠接受一個字符串爲參數,並將其中的內容視爲好像在書寫時就存在於程序中這個位置的代碼。即將eval放在該詞法做用域,而後eval攜帶的代碼就會動態加入到該詞法做用域.

經過下面的例子加深理解:

function foo(str, a) { 
  eval( str ); // 欺騙! 
  console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3

複製代碼

eval(..) 調用中的 "var b = 3;" 這段代碼會被看成原本就在那裏同樣來處理。因爲那段代碼聲明瞭一個新的變量 b,所以它對已經存在的 foo(..) 的詞法做用域進行了修改。當 console.log(..) 被執行時,會在 foo(..) 的內部同時找到 a 和 b,可是永遠也沒法找到外部的 b。所以會輸出「1, 3」而不是正常狀況下會輸出的「1, 2」。

注意:

  • eval(..) 一般被用來執行動態建立的代碼.能夠據程序邏輯動態地將變量和函數以字符形式拼接在一塊兒以後傳遞進去。
  • 在嚴格模式下,eval(...)沒法修改所在的做用域。
  • 與eval(...)相似,setTimeout(..)和 setInterval(..) 的第一個參數能夠是字符串,字符串的內容能夠被解釋爲一段動態生成的函數代碼。
  • new Function(..) 函數的行爲也很相似,最後一個參數能夠接受代碼字符串,並將其轉化爲動態生成的函數(前面的參數是這個新生成的函數的形參)。這種構建函數的語法比 eval(..) 略微安全一些,但也要儘可能避免使用。
var sum = new Function("a", "b", "return a + b;");
console.log(sum(1, 1111));  //1112

複製代碼

2.2.2 with(不推薦實際使用)

例子:

function foo(obj) { 
  with (obj) {
    a = 2; 
  }
}

var o1 = {
  a: 3
};

var o2 = { 
  b: 3
};
foo( o1 );
console.log( o1.a ); // 2

foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——很差,a 被泄漏到全局做用域上了!
複製代碼

起初你會以爲o1的a屬性被with裏的a進行了詞法引用被遮蔽了成爲了2.而o2沒有a屬性,此時with不能進行詞法引用,因此此時o2.a就會變成undefined.可是,爲何最後console.log(a)會爲2?由於在執行foo(o2)時,with會對其中的a=2進行LHS查詢,但它在o2做用域,foo()做用域,全局做用域都沒找到,所以就建立了一個全局變量a並隨後賦值2.

總的來講,with就是將一個沒有或有多個屬性的對象處理爲一個徹底隔離的詞法做用域,所以這個對象的屬性也會被處理爲定義在這個做用域中的詞法標識符。

注意: 使用 eval(..) 和 with 的緣由是會被嚴格模式所影響(限制)。with 被徹底禁止,而在保留核心功能的前提下,間接或非安全地使用 eval(..) 也被禁止了。

2.2.3 性能

JavaScript 引擎會在編譯階段進行數項的性能優化。其中有些優化依賴於可以根據代碼的詞法進行靜態分析,並預先肯定全部變量和函數的定義位置,才能在執行過程當中快速找到標識符。可是eval(..) 和 with會在運行時修改或建立新的做用域,以此來欺騙其餘在書寫時定義的詞法做用域。這麼作就會致使引擎沒法知道eval和with它們對詞法做用域進行什麼樣的改動.只能對部分不進行處理和優化!所以若是代碼中大量使用 eval(..) 或 with,那麼運行起來必定會變得很是慢!。

2.3 小結

  • 詞法做用域是在你書寫代碼時就已經決定了的.在編譯的第一階段詞法分析階段產生詞法做用域.此時詞法做用域基本可以知道所有標識符在哪裏以及是如何聲明的,從而可以預測在執行過程當中如何對它 們進行查找。
  • eval(..) 和 with。前者能夠對一段包含一個或多個聲明的「代碼」字符串進行演算,並藉此來修改已經存在的詞法做用域(在運行時)。後者本質上是經過將一個對象的引用看成做用域來處理,將對象的屬性看成做用域中的標識符來處理,從而建立了一個新的詞法做用域(一樣是在運行時)。
  • 通常不要在實際代碼中使用eval(...)和with,由於不只危險,並且會形成性能問題!

第三章 函數做用域和塊做用域

3.1 函數中的做用域

  • JavaScript 具備基於函數的做用域,通常狀況下每聲明 一個函數都會建立一個函數做用域.
  • 函數做用域的含義是指,屬於這個函數的所有變量均可以在整個函數的範圍內使用及複用(事實上在嵌套的做用域中也能夠使用)。這樣的好處是JavaScript 變量能夠根據須要改變值類型。

3.2 隱藏內部實現

由於

  • 子級函數做用域能夠直接訪問父級函數做用域裏的標識符;
  • 父級函數做用域不能直接訪問子級函數做用域裏的標識符.

因此用函數聲明對代碼進行包裝,實際上就是把這些代碼「隱藏」起來了。

爲何要將代碼進行"隱藏"? 由於最小受權或最小暴露原則。這個原則是指在軟件設計中,應該最小限度地暴露必 要內容,而將其餘內容都「隱藏」起來,好比某個模塊或對象的 API 設計。 隱藏的好處:

  • 實現代碼私有化,減小外部對內部代碼的干擾,保持其穩定性.
  • 規避衝突: 能夠避免同名標識符之間的衝突, 兩個標識符可能具備相同的名字但用途卻不同,無心間可能形成命名衝突。衝突會致使 變量的值被意外覆蓋。那麼通常規避衝突的手段有哪些?
      1. 全局命名空間: 變量衝突的一個典型例子存在於全局做用域中。當程序中加載了多個第三方庫時,若是它們沒有妥善地將內部私有的函數或變量隱藏起來,就會很容易引起衝突。這些庫一般會在全局做用域中聲明一個名字足夠獨特的變量,一般是一個對象。這個對象被用做庫的命名空間,全部須要暴露給外界的功能都會成爲這個對象(命名空間)的屬性,而不是將本身的標識符暴漏在頂級的詞法做用域中。
    • 2.模塊管理: 另一種避免衝突的辦法和現代的模塊機制很接近,就是從衆多模塊管理器中挑選一個來 使用。實際上就是咱們經常使用的amd,commonjs,import模塊機制.

3.3 函數做用域

函數聲明與函數表達式:

function foo() {
	...
}
複製代碼

咱們知道函數foo內的變量和函數被隱藏起來了,是不會對全局做用域形成污染.可是變量名foo仍然存在於全局做用域中,會形成污染.那有什麼方法能避免函數名的污染呢?那就是做爲函數表達式,而不是一個標準的函數聲明.這樣函數名只存在於它本身的函數做用域內,而不會存在於其父做用域,這樣就沒有了污染.舉個函數聲明的例子:

var a = 2;
(function foo(){ 
  var a = 3;
  console.log( a ); // 3 
})(); 
  console.log( a ); // 2
複製代碼

當咱們用()包裹一個函數,並當即執行.此時這個包裝函數聲明是從(function開始的而不是從function關鍵字開始.這樣foo就會被當作一個函數表達式,而不是一個函數聲明(即foo不會存在於父級做用域中).回到上面的例子中,全局做用域是訪問不到foo的,foo只存在於它本身的函數做用域中.

補充: 什麼是函數聲明和函數表達式 首先咱們得了解JS聲明函數的三種方式:

  • 函數表達式(Function Expression): 將函數定義爲表達式語句(一般是變量賦值,也能夠是自調用形式)的一部分。經過函數表達式定義的函數能夠是命名的,也能夠是匿名的。由於它能夠沒有函數名,所以常被用做匿名函數.若是有,其函數名也只存在自身的函數做用域.而且函數表達式不能以「function」開頭.函數表達式能夠存儲在變量或者對象屬性裏. (在函數聲明前加上運算符是能夠將其轉化爲函數表達式的.例如!,+,-,().舉個例子:!function(){console.log(1)}()的結果是1,並不會報錯)
  • 函數聲明(Function Declaration):  函數聲明是一種獨立的結構,它會聲明一個具名函數,並必須以function開頭. 且函數聲明會進行函數提高.使它能在其所在做用域的任意位置被調用,即後面的代碼中能夠將此函數經過函數名賦值給變量或者對象屬性.
  • Function()構造器: 即便用Function構造器建立函數.不推薦這種用法, 容易出問題
//Function()構造器
var f =new Function()

// 函數表達式
var f = function() {
      console.log(1);  
}

// 函數聲明
function f (){
     console.log(2);
}

console.log(f())
//思考一下,這裏會打印出什麼
複製代碼

怎麼區分函數聲明和函數表達式: 看 function 關鍵字出如今聲明中的位置(不只僅是一行代碼,而是整個聲明中的位置)。若是 function 是聲明中的第一個詞,那麼就是一個函數聲明,不然就是一個函數表達式。例如上例中,是從(開始而不是function.

補充: 上面這段是原書的解釋,我以爲這個解釋並不徹底,這裏給出我本身的解釋.

  • 表象區別:和它說的同樣,只要是以function開頭進行聲明,而且含有函數名的就必定是函數聲明.
  • 內在區別:其實我在上面補充二者的定義時已經說得很清楚了,我再對比總結下.
    • 函數提高:函數聲明,會將整個函數進行提高.而函數表達式則不會提高,它是在引擎運行時進行賦值,且要等到表達式賦值完成後才能調用。
    • 函數表達式是能夠沒有函數名的,若是有,它的函數名也只存在於自身的做用域,var f = function fun(){console.log(fun)}其餘地方是沒有的.這也避免了全局污染,也方便遞歸.

3.3.1 匿名和具名

函數表達式能夠是匿名的,而函數聲明則不能夠省略函數名.有函數名的就是具名函數,沒有函數名的就是匿名函數.

匿名函數的缺點:

    1. 匿名函數在棧追蹤中不會顯示出有意義的函數名,使得調試很困難。
    1. 若是沒有函數名,當函數須要引用自身時只能使用已通過期的arguments.callee引用,好比在遞歸中。另外一個函數須要引用自身的例子,是在事件觸發後事件監聽器須要解綁自身。
    1. 匿名函數省略了對於代碼可讀性/可理解性很重要的函數名。一個描述性的名稱可讓代碼不言自明。

因此給函數表達式指定一個函數名能夠有效解決以上問題。始終給函數表達式命名是一個最佳實踐.

PS: 我的意見是若是函數表達式有賦值給變量或屬性名或者就是一次性調用的.實際上是不必加上函數名.由於代碼裏取名原本就很難,取很差反而會形成誤解.

3.3.2 當即執行函數表達式

好比 (function foo(){ .. })()。第一個 ( ) 將函數變成表達式,第二個 ( ) 執行了這個函數。這就是當即執行函數表達式,也被稱爲IIFE,表明當即執行函數表達式 (Immediately Invoked Function Expression);

IIFE能夠具名也能夠匿名.好處和上面提到的同樣.IIFE還能夠是這種形式(function(){ .. }()).這兩種形式在功能上是一致的。

3.4 塊做用域

函數做用域是JavaScript最多見的做用域單元,有時咱們僅會將var賦值變量在if或for的{...}內使用,而不會在其餘地方使用.但它仍然會對外層的函數做用域形成污染.這個時候就會但願能有一個做用域能將其外部的函數做用域隔開,聲明的變量僅在此做用域有效.塊做用域(一般就是{...}包裹的內部)就能夠幫咱們作到這點.

從 ES3 發佈以來,JavaScript 中就有了塊做用域,而 with 和 catch 分句就是塊做用域的兩個小例子。

3.4.1 with

咱們在第 2 章討論過 with 關鍵字。它不只是一個難於理解的結構,同時也是塊做用域的一個例子(塊做用域的一種形式),用 with 從對象中建立出的做用域僅在 with 聲明中而非外部做用域中有效。

3.4.2 try/catch

try/catch 的 catch 分句會建立一個塊做用域,其中聲明的變量僅在 catch 內部有效。

try {
  undefined(); // 執行一個非法操做來強制製造一個異常
}
catch (err) {
  console.log( err ); // 可以正常執行! 
}
console.log( err ); // ReferenceError: err not found
複製代碼

err 僅存在 catch 分句內部,當試圖從別處引用它時會拋出錯誤。 那麼若是咱們想用catch建立一個不是僅僅接收err的塊做用域,該怎麼作呢?

try{throw 2;}catch(a){ 
  console.log( a ); // 2
}
console.log( a ); // ReferenceError
複製代碼

這樣就建立了一個塊做用域,且a=2,僅在catch分句中存在.在ES6以前咱們能夠使用這種方法來使用塊做用域.

3.4.3 let

ES6 引入了新的 let 關鍵字,提供了除 var 之外的另外一種變量聲明方式。let 關鍵字能夠將變量綁定到所在的任意做用域中(一般是 { .. } 內部)。

用 let 將變量附加在一個已經存在的塊做用域上的行爲是隱式的。例如在if的{...}內用let聲明一個變量.那什麼是顯式地建立塊做用域呢?就是單首創建{}來做爲let的塊做用域.而不是借用if或者for提供的{}.例如{let a=2;console.log(a)}
注意: 使用 let 進行的聲明不會在塊做用域中進行提高.
塊做用域的好處:

  • 1,垃圾收集
function process(data){
        // 在這裏作點有趣的事情
     }
     var someReallyBigData=function(){
         //dosomeing
     }
     process(someReallyBigData);

     var btn=document.getElementById("my_button");
     btn.addEventListener("click",function click(evt){
        alert("button click");
		//假如咱們在這裏繼續調用someReallyBigData就會造成閉包,致使不能垃圾回收(這段是書裏沒有,我加上方便理解的)
     },false);
複製代碼

click 函數的點擊回調並不須要 someReallyBigData 變量。理論上這意味着當 process(..) 執行後,在內存中佔用大量空間的數據結構就能夠被垃圾回收了。可是,因爲 click 函數造成了一個覆蓋整個做用域的閉包,JavaScript 引擎極有可能依然保存着這個結構(取決於具體實現)。 但顯式使用塊做用域可讓引擎清楚地知道沒有必要繼續保存 someReallyBigData 了:

function process(data){
       // 在這裏作點有趣的事情
    }
    // 在這個塊中定義的內容能夠銷燬了! 
    {
      let someReallyBigData = { .. }; 
      process( someReallyBigData );
    }
    var btn=document.getElementById("my_button");
    btn.addEventListener("click",function click(evt){
       alert("button click");
    },false);
複製代碼
    1. let循環
for (let i=0; i<10; i++) { 
	  console.log( i );
     }
console.log( i ); // ReferenceError
複製代碼

for 循環頭部的 let 不只將 i 綁定到了 for 循環的塊中,事實上它將其從新綁定到了循環的每個迭代中,確保使用上一個循環迭代結束時的值從新進行賦值。這樣就避免了i對外部函數做用域的污染.

3.4.4 const

除了 let 之外,ES6 還引入了 const,一樣能夠用來建立塊做用域變量,但其值是固定的(常量)。以後任何試圖修改值的操做都會引發錯誤。

var foo = true;
if (foo) {
  var a = 2;
  const b = 3; // 包含在 if 中的塊做用域常量
  a = 3; // 正常!
  b = 4; // 錯誤! 
}
console.log( a ); // 3
console.log( b ); // ReferenceError!
複製代碼

3.5 小結

函數是 JavaScript 中最多見的做用域單元。本質上,聲明在一個函數內部的變量或函數會在所處的做用域中「隱藏」起來,能夠有效地與外部做用域隔開.

但函數不是惟一的做用域單元。塊做用域指的是變量和函數不只能夠屬於所處的做用域,也能夠屬於某個代碼塊(一般指 { .. } 內部)即塊做用域。ES6中就提供了let和const來幫助建立塊做用域.

第四章 提高

4.1 先有雞(賦值)仍是先有蛋(聲明)

考慮第一段代碼

a = 2;
var a; 
console.log( a );
複製代碼

輸出結果是2,而不是undefined

考慮第二段代碼

console.log( a ); 
var a = 2;
複製代碼

輸出結果是undefined,而不是ReferenceError 考慮完以上代碼,你應該會考慮這個問題.究竟是聲明(蛋)在前,仍是賦值(雞)在前?

4.2 編譯器再度來襲

編譯器的內容,回憶一下,引擎會在解釋 JavaScript 代碼以前首先對其進行編譯。編譯階段中的一部分工做就是找到全部的聲明,並用合適的做用域將它們關聯起來。 以後引擎會詢問做用域,對聲明進行賦值操做.

那麼,在編譯階段找到全部的聲明後,編譯器又作了什麼?答案就是提高 以上節的第一段代碼爲例,當你看到 var a = 2; 時,可能會認爲這是一個聲明。但 JavaScript 實際上會將其當作兩個聲明:var a;和a = 2;。 第一個定義聲明是在編譯階段進行的。第二個賦值聲明會被留在原地等待執行階段。在第一個聲明在編譯階段時,編譯器會對var a;聲明進行提高(即把var a;置於所在做用域的最上面).而a = 2;則會保持所在位置不動.此時代碼會變成

var a; 
a = 2;
console.log( a );
複製代碼

由此可知,在編譯階段,編譯器會對聲明進行提高.即先有蛋(聲明)後有雞(賦值)。 哪些聲明會被進行提高?

  • 變量聲明:例如上例中的var a;.不包括後面的a = 2;不包含有賦值操做的聲明.
  • 函數聲明:注意是函數聲明,而不是函數表達式!(不清楚能夠看前面的3.3節,我有詳細說明).函數聲明提高,是將整個函數進行提高,而不是僅僅函數名的提高.

4.3 函數優先

函數聲明和變量聲明都會被提高。可是一個值得注意的細節(這個細節能夠出如今有多個「重複」聲明的代碼中)是函數會首先被提高,而後纔是變量。 考慮如下代碼:

foo(); // 1
var foo;
function foo() { 
  console.log( 1 );
}
foo = function() { 
  console.log( 2 );
};
複製代碼

會輸出 1 而不是 2 !這個代碼片斷會被引擎理解爲以下形式:

function foo() { 
  console.log( 1 );
}
foo(); // 1
foo = function() { 
  console.log( 2 );
};
複製代碼

注意,var foo 儘管出如今 function foo()... 的聲明以前,但它是重複的聲明(所以被忽略了),由於函數聲明會被提高到普通變量以前。 注意: js會忽略前面已經聲明的聲明(無論是變量聲明仍是函數聲明,只要其名稱相同,則後續不會再進行重複聲明).可是對該變量新的賦值,會覆蓋以前的值.
一句話歸納:函數聲明的優先級高於變量聲明,會排在它前面.

4.4 小結

  • 對於var a = 2 JavaScript引擎會將var a和 a = 2看成兩個單獨的聲明,第一個是編譯階段的任務,而第二個則是執行階段的任務。
  • 論做用域中的聲明出如今什麼地方,都將在代碼自己被執行前首先進行處理。 能夠將這個過程形象地想象成全部的聲明(變量和函數)都會被「移動」到各自做用域的最頂端,這個過程被稱爲提高。
  • 聲明自己會被提高,而包括函數表達式的賦值在內的賦值操做並不會提高(即賦值操做都不會提高)。
  • 注意:,當普通的 var 聲明和函數聲明混合在一塊兒的時候,而且聲明相同時(var的變量名和函數名相同時,會引起js對重複聲明的忽略)!必定要注意避免重複聲明!

第五章 做用域閉包

5.1 啓示

  • JavaScript中閉包無處不在,你只須要可以識別並擁抱它。
  • 閉包是基於詞法做用域書寫代碼時所產生的天然結果,你甚至不須要爲了利用它們而有意識地建立閉包。

5.2 實質問題 && 5.3 如今我懂了

由於這兩小節理解透了其實發現書裏也沒講什麼,這裏就進行合併,並補充拓展我本身的理解和總結.
什麼是閉包?(廣義版)
書中解釋: 當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用域以外執行。
MDN的解釋: 閉包是函數和聲明該函數的詞法環境的組合。
個人解釋(詳細版): 必須包含兩點:

  • 1,有函數.因爲函數自身的特性,它能訪問所在的詞法做用域.並能保存外部詞法做用域的變量和函數到本身的函數做用域.
  • 2,有該函數所在的詞法環境.其實在JavaScript中任何函數都會處在一個詞法環境中.無論是全局做用域仍是函數做用域.

綜上簡單版就是:MDN的解釋閉包是函數和聲明該函數的詞法環境的組合。
還能夠繼續延伸成極簡版:JavaScript中的函數就會造成閉包
Tips: 注意到上面對詞法做用域詞法環境兩詞的分開使用了嗎?1,裏此時函數還沒被執行,因此使用的是詞法做用域即靜態做用域.2,裏,此時函數被執行,此時詞法做用域就會變成詞法環境(包含靜態做用域與動態做用域).因此其實MDN的解釋其實更準確一點,

咱們平常使用時所說的閉包(狹義版,嚴格意義上的):
爲了便於對閉包做用域的觀察和使用.咱們實際使用時會將閉包的函數做用域暴露給當前詞法做用域以外.也就是本書一直強調的閉包函數須要在它自己的詞法做用域之外執行.做者認爲符合這個條件才稱得上是真正的閉包(也就是咱們平常使用常說的'使用閉包',而且使用任何回調函數其實也是閉包).
因此狹義版就是:閉包是函數和聲明該函數的詞法環境的組合,而且將閉包的函數做用域暴露給當前詞法做用域以外.

閉包暴露函數做用域的三種方式:
下面部分是書中沒有的,是本身實際使用時的總結,而且符合這三種形式之一的就是咱們平常使用時所說的閉包(狹義版)

  • 1,經過外部函數的參數進行暴露.
function foo() { 
  var a = 2;
  function bar() { 
   baz(a) //經過外部函數的參數進行暴露
  }
  bar(); 
};
function baz(val) { 
   console.log( val ); // 2 
}
foo();
複製代碼
  • 2,經過外部做用域的變量進行暴露
var val;
function foo() { 
  var a = 2;
  function bar() { 
   val=a //經過外部做用域的變量進行暴露
  }
  bar(); 
};
foo();
console.log(val)  //2
複製代碼
  • 3,經過return直接將整個函數進行暴露
function foo() { 
   var a = 2;
   function bar() { 
    console.log(a)
   }
   return bar //經過return直接將整個函數進行暴露
};
var val=foo();
val()  //2
複製代碼

關於閉包的內存泄露問題:
首先必須聲明一點:使用閉包並不必定會形成內存泄露,只有使用閉包不當纔可能會形成內存泄露.(吐槽:面試不少新人時,張口就說閉包會形成內存泄露)
爲何閉包可能會形成內存泄露呢?緣由就是上面提到的,由於它通常會暴露自身的做用域給外部使用.若是使用不當,就可能致使該內存一直被佔用,沒法被JS的垃圾回收機制回收.就形成了內存泄露.
注意: 即便閉包裏面什麼都沒有,閉包仍然會隱式地引用它所在做用域裏的所用變量. 正由於這個隱藏的特色,閉包常常會發生不易發現的內存泄漏問題.
常見哪些狀況使用閉包會形成內存泄露:

  • 1,使用定時器未及時清除.由於計時器只有先中止纔會被回收.因此決辦法很簡單,將定時器及時清除,並將形成內存的變量賦值爲null(變成空指針)
  • 2,相互循環引用.這是常常容易犯的錯誤,而且也不容易發現.舉個栗子:
function foo() { 
  var a = {}; 
  function bar() { 
    console.log(a); 
  }; 
  a.fn = bar; 
  return bar; 
};
複製代碼

這裏建立了一個a 的對象,該對象被內部函數bar引用。而後,a建立了一個屬性fn指向了bar,最後返回了innerFn()。這樣就造成了bar和a的相互循環引用.可能有人說bar裏不使用console.log(a)不就沒有引用了嗎就不會形成內存泄露了.NONONO,bar做爲一個閉包,即便它內部什麼都沒有,foo中的全部變量都仍是隱使地被 bar所引用。這個知識點是我前面忘記提到的,也是書中沒有提到的.算了我如今加到前面去吧.因此即便bar內什麼都沒有仍是形成了循環引用,那真正的解決辦法就是,不要將a.fn = bar.

  • 3,將閉包引用到全局變量上.由於全局變量是隻有當頁面被關閉的時候纔會被回收.
  • 4,在閉包中對DOM進行不當的引用.這個常見於老IE瀏覽器,現代瀏覽器已經長大了,已經學會了本身處理這種狀況了.這裏就不贅述了.想知道的能夠自行問谷娘和度娘.

總而言之,解決辦法就是使閉包的能正常引用,能被正常回收.若是實在不行,就是在使用完後,手動將變量賦值null,強行進行垃圾回收.

5.4 循環和閉包

看以下例子:

for (var i=1; i<=5; i++) { 
  setTimeout( function timer() {
    console.log( i );
  }, i*1000 );
}
複製代碼

咱們指望的結果是分別輸出數字 1~5,每秒一次,每次一個。
但實際結果是,這段代碼在運行時會以每秒一次的頻率輸出五次 6。
(關於書裏的解釋,我以爲有點說複雜了,沒說到點子上,下面是個人解釋.)
爲何會是這樣的結果?
timer毫無疑問是一個閉包,它是能夠訪問到外部的變量i.在進行for循環時,timer()會被重複執行5次,也就是它會 console.log( i )5次.(關鍵部分來了!)這5次i實際上是同一個i.它是來自於外部做用域,即for裏面聲明的i.在詞法做用域中變量i只可能對應一個惟一的值,即變量和它的值是一一對應的.不會變化的.那這個值究竟是多少呢?這個值就是最終值! i的最終值就是6即for循環完後i的值.當引擎執行console.log( i )時,它會詢問i所對應的做用域,問它i的值是多少.這個時候做用域進行RHS查詢獲得的結果就是最終值6.

爲何咱們會覺得分別輸出1~5?
由於在for循環中,咱們錯覺得每一次循環時,函數所輸出的i是根據循環動態變化的.便是1~5累加變化的.但實際上它所訪問的i是同一個固定不變的值,即最終值6.可能你會有這樣的疑惑,那我循環還有意義嗎?i其實一開始就肯定是6了.沒有變化過!錯!i變化過,它的確是從1逐步增長到6的.只是外部做用域的i值只多是循環完後的最終值,而且函數timer()並無保存每次i變化的值.它只是訪問了外部做用域的i值即最終的值6. OK咱們知道了出錯的地方,就是咱們沒有把每次i的值保存在一個獨立的做用域中. 接下來,看下這個改進的例子結果是多少.

for (var i=1; i<=5; i++) { 
  (function() {
    setTimeout( function timer() { 
	  console.log( i );
    }, i*1000 );
  })();
}
複製代碼

它的最終值仍然是5個6.爲何?咱們來分析下,上例中,它用了一個匿名函數包裹了定時器,並當即執行.在進行for循環時,會創造5個獨立的函數做用域(由匿名函數建立的,由於它是閉包函數).可是這5個獨立的函數做用域裏的i也全都是對外部做用域的引用.即它們訪問的都是i的最終值6.這並非咱們想要的,咱們要的是5個獨立的做用域,而且每一個做用域都保存一個"當時"i的值.

解決辦法: 那咱們這樣改寫.

for (var i=1; i<=5; i++) { 
  (function () {
    var j =i;
    setTimeout( function timer() { 
	  console.log( j );
    }, j*1000 );
  })();
}
//此次終於結果是分別輸出數字 1~5,每秒一次,每次一個。 
複製代碼

這樣改寫後,匿名函數每次都經過j保存了每次i值,這樣i值就經過j保存在了獨立的做用域中.注意此時保存的i值是'當時'的值,並非循環完後的最終值.這樣循環完後,實際上就建立了5個獨立的做用域,每一個做用域都保存了一個'當時'i的值(經過j).當引擎執行console.log( j )詢問其對應的獨立做用域時,獲得的值就是'當時'保存的值,不再是6了. 咱們還能夠進一步簡寫爲這樣:

for (var i=1; i<=5; i++) { 
  (function(j) {
    setTimeout( function timer() { 
	  console.log( j );
    }, j*1000 );
  })(i);
}
//結果是分別輸出數字 1~5,每秒一次,每次一個。 
複製代碼

利用塊做用域進行解決:
在es6中,咱們不只能夠使用函數來建立一個獨立的做用域,咱們還能夠使用let聲明來建立一個獨立的塊做用域(在{}內).因此咱們還能夠這樣改寫:

for (let i=1; i<=5; i++) { 
  setTimeout( function timer() {
    console.log( i );
  }, i*1000 );
}
//結果是分別輸出數字 1~5,每秒一次,每次一個。 
複製代碼

這樣改寫,在每次循環時,let都會對i進行聲明.並經過循環自帶的{}建立一個獨立的塊做用域.而且let聲明的i,保存了'當時'i的值在當前塊做用域裏.所以當引擎執行console.log( i )時,它會詢問對應的塊做用域上i的值,獲得的結果就是'當時'保存的值.

延伸:
實際上塊做用域能夠稱得上一個'僞'閉包(之因此是僞,是由於閉包規定了只能是函數).由於它幾乎擁有閉包的全部特性.它也能夠建立一個獨立的做用域,一樣外部做用域不能訪問塊做用域的變量.但塊做用域能夠訪問外部做用域.舉個栗子:

function foo() { 
  var a = 2;
  {  //經過{} 顯示錶示塊做用域
    let b = a;
	console.log('塊做用域內',b) //2
  }
  console.log('塊做用域外',b) //b is not defined
}
foo()
複製代碼

說了相同點,說說不一樣點:1,保存變量到塊做用域,必須經過let聲明.2,塊做用域不能和函數同樣有名稱(函數名) 不少不方便使用閉包或者比較麻煩的時候,是能夠考慮經過塊做用域進行解決.

總結一下通常何時考慮使用閉包:
這部分也是本身工做使用的總結,若是有補充或者不對的地方,歡迎留言指正.

  • 1,須要建立一個獨立的做用域並隱藏一些變量或函數,不被外部使用;或者想保存一些外部做用域的變量或函數到這個獨立做用域.
  • 2,只想暴露一部分自身做用域的變量或函數給外部使用.

5.5 模塊

首先看下面的例子:

function CoolModule() {
  var something = "cool";
  var another = [1, 2, 3];
  function doSomething() { 
    console.log( something );
  }
  function doAnother() {
    console.log( another.join( " ! " ) );
  }
  return {
      doSomething: doSomething,
	  doAnother: doAnother
  }; 
}
var foo = CoolModule(); 

foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
複製代碼

首先咱們對上面這段代碼進成分行分析:
私有數據變量:something, another
內部函數:doSomething, doAnother
直接說結論,上面這個例子就是模塊模式.它return返回的這個對象也就是模塊也被稱爲公共API(至少書中是這樣稱呼的).CoolModule()就是模塊構造器或者叫模塊函數.
注意:

  • 這裏的模塊和咱們所說的模塊化開發不是徹底同樣的!
  • 模塊不必定非要是標準對象,也能夠是一個函數,函數本質上也是對象,函數也能夠有本身的屬性.
  • 書中有這樣一句話CoolModule() 只是一個函數,必需要經過調用它來建立一個模塊實例。若是不執行外部函數,內部做用域和閉包都沒法被建立。我以爲這句話有必要延伸說一下.函數調用一次就會建立一個該函數的做用域(不調用就不會建立),包括建立它裏面的變量和函數.

模塊模式:
模塊模式須要具有如下2個條件:(這裏結合上面的例子,對書中的定義進行說明方便理解)

  • 1, 必須有外部的封閉函數(即CoolModule),該函數必須至少被調用一次(每次調用都會建立一個新的模塊實例-->模塊實例指的就是函數return返回的對象)。
  • 2, 封閉函數(即CoolModule)必須返回至少一個內部函數(即doSomething, doAnother),這樣內部函數才能在私有做用域中造成閉包,而且能夠訪問或者修改私有的狀態(即something, another)。

模塊:
表面上看由模塊函數(例子中的CoolModule)所返回的對象就是模塊.但模塊還必須還包含模塊函數的內部函數(即閉包函數).只有包含了才能真正稱得上是模塊.才強調一次這裏的模塊與模塊化裏的模塊是有區別的,也不是nodejs裏的模塊.

模塊函數:
模塊函數也就是模塊構造器,例子中的CoolModule().通常它有兩個常見用法.

  • 經過接受參數,對輸出的模塊進行修改.
  • 經過添加模塊裏添加相關的內部函數,實現對輸出模塊數據的增刪改查.(書中用命名將要做爲公共API返回的對象.我以爲命名應該是用錯了,應該是修改即增刪改查更好)

5.5.1 現代的模塊機制

大多數模塊依賴加載器 / 管理器本質上都是將這種模塊定義封裝進一個友好的 API。 下面就介紹一個簡單的模塊管理器實現例子(對書中的例子進行逐行解讀):

//首先實例化咱們的模塊管理器,取名myModules
var MyModules=(function Manager() {
    
    //做爲咱們的模塊池,保存全部定義的模塊
    var modules={};

    /** *使用相似AMD的方式定義新模塊,接收3個參數 *name:模塊名 *deps:數組形式表示所依賴的其餘模塊 *impl:模塊功能的實現 **/ 
    function define(name,deps,impl) {
        
        //遍歷依賴模塊數組的每一項,從程序池中取出對應的模塊,並賦值.
		//循環完後,deps由保存模塊名的數組變成了保存對應模塊的數組.
        for (var i=0;i<deps.length;i++) {
            deps[i]=modules[deps[i]];
        }
        //將新模塊存儲進模塊池,並經過apply注入它所依賴的模塊(即遍歷後的deps,實際上就是用deps做爲impl的入參)
        modules[name]=impl.apply(impl,deps);
    }
    //從模塊池中取出對應模塊
    function get (name) {
        return modules[name];
    }
    //暴露定義模塊和獲取模塊的兩個api
    return {
        define: define,
        get: get
    }
})()
複製代碼

說明: 後面書中說了這麼一句爲了模塊的定義引入了包裝函數(能夠傳入任何依賴),這裏包裝函數指的是Manger(),一樣也是咱們上節提到的模塊函數.首先說明下什麼是包裝函數.例如函數A當中還有一個函數B.當咱們想要調用函數B的時候,則須要先調用函數A.那麼函數A就叫作函數B的包裝函數.也就是說咱們想調用某個模塊時,須要先調用它的包裝函數即這裏的Manger().接着是後面那句而且將返回值,也就是模塊的 API,儲存在一個根據名字來管理的模塊列表中。注意這裏的返回值是指impl的返回值.

接着看經過管理器來定義和使用模塊

MyModules.define('bar',[],function () {
    function hello (who) {
        return "Let me introduce: " + who;
    }
	//返回公共API 即提供一個hello的接口
    return {
        hello:hello
    };
});

MyModules.define('foo',['bar'],function (bar) {
    var hungry = "hippo";
	
    functin awesome () {
	//這裏的bar爲返回模塊bar返回的公共API
        console.log( bar.hello( hungry ).toUpperCase() );
    }
	//返回公共API 即提供一個awesome的接口
    return {
        awesome:awesome
    }
})

var bar=MyModules.get('bar');//經過管理器獲取模塊'bar'
var foo=MyModules.get('foo');//經過管理器獲取模塊'foo'

console.log(
//調用模塊bar的hello接口
         bar.hello( "hippo" ) 
); // Let me introduce: hippo 

//調用模塊foo的awesome接口
foo.awesome(); // LET ME INTRODUCE: HIPPO

複製代碼

這節的主要內容仍是瞭解如今是如何對模塊進行一個規範處理.主要是兩部份內容,一個是經過名稱和依賴合理定義模塊並儲存.另外一個則是經過名稱對存儲的模塊的調用.其實還能夠再增長一個刪除模塊的方法.

5.5.2 將來的模塊機制

ok,這節說的模塊,就是咱們常說的模塊化開發.而且主要提到的就是ES6裏經常使用的import.沒什麼好說的.

5.6 小結

吐槽: 同一個函數概念在5.5這一個小節裏,竟然換着花樣蹦出了三個名字!一會叫模塊構造器!一會叫模塊函數!以及最後的包裝函數!每變化一次,都得想一遍它指的是啥!真的是無力吐槽了!!!!

閉包:當函數能夠記住並訪問所在的詞法做用域,而且函數是在當前詞法做用域以外執行,這時 就產生了閉包。

模塊有兩個主要特徵:

  • (1)爲建立內部做用域而調用了一個包裝函數(模塊構造器的實例化,不想對頻繁換名字吐槽了);
  • (2)包裝函數的返回值(也就是模塊)必須至少包括一個對內部函數的引用,這樣就會建立涵蓋整個包裝函數內部做用域的閉包。

第二部分

第一章 關於this

1.1 爲何要用this

由於this 提供了一種更優雅的方式來隱式「傳遞」一個對象(即上下文對象)引用,所以能夠將 API 設計得更加簡潔而且易於複用。

1.2 誤解

下面兩種常見的對於 this 的解釋都是錯誤的(看看就好,就不過多解讀了,以避免增長了對錯誤的印象)。

1.2.1 指向自身

人們很容易把 this 理解成指向函數自身.

具名函數,能夠在它內部能夠使用函數名來引用自身進行遞歸,添加屬性等。(這個知識點其實在第三章提過,既然這裏又提了一遍,我也再說一遍.)例如:

function foo() {
  foo.count = 4; // foo 指向它自身
}
複製代碼

匿名函數若是想要調用自身則,須要使用arguments.callee不過這個屬性在ES5嚴格模式下已經禁止了,也不建議使用.詳情能夠查看MDN的說明.

1.2.2 它的做用域

切記: this 在任何狀況下都不指向函數的詞法做用域。你不能使用 this 來引用一個詞法做用域內部的東西。 這部分只需記住這一段話就行.

終極疑問: JavaScript裏的做用域究竟是對象嗎? 這小節最令我在乎的是裏面這句話"在 JavaScript 內部,做用域確實和對象相似,可見的標識符都是它的屬性。可是做用域「對象」沒法經過 JavaScript代碼訪問,它存在於JavaScript 引擎內部。"它讓我想起了最開始學JS的一個疑問,JavaScript裏的做用域究竟是對象嗎.雖然"在JS裏萬物皆對象".可是做用域給人的感受卻不像是一個對象.更像是一個範圍,由函數的{}圍城的範圍,限制了其中變量的訪問.但直覺告訴我它和對象仍是應該有點聯繫的.直到讀到書中的這段話,更加印證了個人感受. 在JavaScript裏,做用域實際上是一個比較特殊的對象,做用域裏全部可見的標識符都是它的屬性.只是做用域對象並不能經過JavaScript代碼被咱們訪問,它只存在於JavaScript引擎內部.因此做用域做爲一個"對象"是常常被咱們忽略.

1.3 this究竟是什麼

this 是在運行時(runtime)進行綁定的,並非在編寫時綁定,它的上下文(對象)取決於函數調用時的各類條件。this 的綁定和函數聲明的位置沒有任何關係,只取決於函數的調用方式

當一個函數被調用時,會建立一個活動記錄(有時候也稱爲執行上下文)。這個記錄會包含函數在哪裏被調用(調用棧)、函數的調用方法、傳入的參數等信息。this 就是記錄的其中一個屬性,會在函數執行的過程當中用到。(PS:因此this並不等價於執行上下文)

1.4 小結

  • 學習 this 的第一步是明白 this 既不指向函數自身也不指向函數的詞法做用域
  • this 其實是在函數被調用時發生的綁定,它指向什麼徹底取決於函數在哪裏被調用(關於this你必須記住的話)

第二章 this全面解析

2.1 調用位置

經過上節咱們知道,this的綁定與函數的調用位置有關.那調用位置是什麼.調用位置就是函數在代碼中被調用的位置(而不是聲明的位置)。

要尋找調用位置,最重要的是要分析調用棧(就是爲了到達當前執行位置所調用的全部函數)。咱們關心的調用位置就在當前正在執行的函數的前一個調用中。PS:調用棧實際上是一個解釋起來有點複雜的概念.這裏我就不過多解釋,這裏推薦一篇文章,解釋得不錯.

這節書裏的例子解釋得不錯,這裏就不復制代碼了.其實分析調用棧只是爲了在運行時找到咱們關心的函數到底在哪裏和被誰調用了. 可是實際別寫代碼時,其實並不會分析得這麼清楚的,咱們仍是隻需記住this的指向就是咱們調用該函數的上下文對象.意思就是咱們在哪裏調用該函數,this就指向哪裏.而且查看調用棧還能夠經過瀏覽器的開發者工具,只需在疑惑的代碼上一行加上debugger便可.瀏覽器在調試模式時,咱們就能夠在調用列表裏查看調用棧.咱們通常也僅在查找bug時,會使用該方法.

2.2 綁定規則

在找到調用位置後,則須要斷定代碼屬於下面四種綁定規則中的哪種.而後才能對this進行綁定.
注意: this綁定的是上下文對象,並非函數自身也不是函數的詞法做用域

2.2.1 默認綁定

什麼是獨立函數調用:對函數直接使用而不帶任何修飾的函數引用進行調用.簡單點一個函數直接是func()這樣調用,前面什麼都沒有.不一樣於經過對象屬性調用例如obj.func(),也沒有經過new關鍵字new Function();也沒有經過apply,bind,call強制改變this指向.
默認綁定: 當被用做獨立函數調用時(不論這個函數在哪被調用,無論全局仍是其餘函數內),this默認指向到window;
注意: 若是使用嚴格模式(strict mode),那麼全局對象將沒法使用默認綁定,所以 this 會綁定到 undefined.

2.2.2 隱式綁定

隱式綁定: 函數被某個對象擁有或者包含.也就是函數被做爲對象的屬性所引用.例如obj.func().此時this會綁定到該對象上.
隱式丟失: 無論是經過函數別名或是將函數做爲入參形成的隱式丟失.只需找到它真正的調用位置,而且函數前沒有任何修飾也沒有顯式綁定(下節會講到)(非嚴格模式下).那麼this則會進行默認綁定,指向window.
注意: 實際工做中,大部分this使用錯誤都是由對隱式丟失的不理解形成的.記住函數調用前沒有任何修飾和顯式綁定(其實就是call、apply、bind),this就指向window

2.2.3 顯式綁定

在分析隱式綁定時,咱們必須在一個對象內部包含一個指向函數的屬性,並經過這個屬性間接引用函數,從而把 this 間接(隱式)綁定到這個對象上。若是咱們不想在對象內部包含函數引用,而想在某個對象上強制調用函數,此時則須要顯式綁定.
顯式綁定: 能夠直接指定 this 的綁定對象,被稱之爲顯式綁定。基本上就是咱們常使用的call、apply、bind方法都是顯式綁定.(若是這三個方法不能熟練使用的,建議找度娘或者谷娘學習後,再看這節.)
注意: 若是你傳入了一個原始值(字符串類型、布爾類型或者數字類型)來看成 this 的綁定對 象,這個原始值會被轉換成它的對象形式(也就是new String(..)、new Boolean(..)或者 new Number(..))。這一般被稱爲「裝箱」。

硬綁定: 使用call、apply、bind方法強制顯式地將this進行綁定,稱之爲硬綁定。 硬綁定的典型應用場景就是建立一個包裹函數(其實就是常說的封裝函數),傳入全部的參數並返回接收到的全部值. 在封裝函數中,咱們常使用apply.一方面是由於它能夠手動綁定this,更重要的是由於能夠用apply的第二個參數,方便地注入全部傳入的參數.例如以前提到的modules[name]=impl.apply(impl,deps).由於咱們不知道傳入的參數有多少個,但咱們能夠方便地使用一個deps將其所有注入.另外一個經常使用的是foo.apply( null,argue)當咱們將apply的第一個參數設置爲null時,此時this就會默認綁定到window.切記使用這種用法時確保函數foo內沒有使用this. 不然極可能會形成全局污染.若是是第三方庫的函數就建議不要使用了,由於你不知作別人的函數是否使用了this(關於這部份內容,下節會繼續提到).還有一種經常使用就是foo.call( this).這樣foo裏的this都會指向當前調用的上下文環境.

API調用的「上下文」: 第三方庫的許多函數,以及 JavaScript 語言和宿主環境中許多新的內置函數,都提供了一個可選的參數,一般被稱爲「上下文」(context),其做用和 bind(..) 同樣,確保你的回調函數使用指定的 this。

2.2.4 new綁定

JavaScript 中 new 的機制實際上和麪向類的語言徹底不一樣。在 JavaScript 中,構造函數只是一些 使用 new 操做符時被調用的函數。它們並不會屬於某個類,也不會實例化一個類。實際上, 它們甚至都不能說是一種特殊的函數類型,它們只是被 new 操做符調用的普通函數而已。實際上並不存在所謂的「構造函數」,只有對於函數的「構造調用」。

使用 new 來調用函數,或者說發生構造函數調用時,會自動執行下面的操做。

  • 1,建立(或者說構造)一個全新的對象。
  • 2,這個新對象會被執行[[原型]]鏈接。
  • 3,這個新對象會綁定到函數調用的this。
  • 4,若是函數沒有返回其餘對象,那麼new表達式中的函數調用會自動返回這個新對象。

示例:

function foo(a) { 
  this.a = a;
}
var bar = new foo(2); 
console.log( bar.a ); // 2
複製代碼

使用 new 來調用 foo(..) 時,咱們會構造一個新對象並把它綁定到 foo(..) 調用中的 this 上。
說明:對於上面這句話進行解釋下,若是在一個函數前面帶上 new 關鍵字來調用, 那麼背地裏將會建立一個鏈接到該函數的 prototype 的新對象,this就指向這個新對象;

2.3 優先級

直接上結論:
new綁定=顯示綁定>隱式綁定>默認綁定
說明: new綁定與顯示綁定是不能直接進行測試比較,但經過分析發現new綁定內部實際上是使用了硬綁定(顯示綁定的一種),因此new綁定和顯示綁定優先級應該差很少.但話說回來,通常實際使用時,不會這種複雜的交錯綁定.因此只需記住下面的斷定便可.

判斷this:
如今咱們能夠根據優先級來判斷函數在某個調用位置應用的是哪條規則。能夠按照下面的順序來進行判斷:

  • 1,函數是否在new中調用(new綁定)?若是是的話this綁定的是新建立的對象。 var bar = new foo()
  • 2,函數是否經過call、apply(顯式綁定)或者硬綁定調用?若是是的話,this綁定的是 指定的對象。var bar = foo.call(obj2)
  • 3,函數是否在某個上下文對象中調用(隱式綁定)?若是是的話,this 綁定的是那個上 下文對象。var bar = obj1.foo()
  • 4,若是都不是的話,使用默認綁定。若是在嚴格模式下,就綁定到undefined,不然綁定到 全局對象。var bar = foo() 就是這樣。對於正常的函數調用來講,理解了這些知識你就能夠明白 this 的綁定原理了。

2.4 綁定例外

2.4.1 被忽略的this

若是你把 null 或者 undefined 做爲 this 的綁定對象傳入 call、apply 或者 bind,這些值在調用時會被忽略,實際應用的是默認綁定規則,this會綁定到window上.
使用情景:
一種很是常見的作法是使用 apply(..) 來「展開」一個數組(也能夠用來方便地參數注入),並看成參數傳入一個函數。相似地,bind(..) 能夠對參數進行柯里化(預先設置一些參數).經過自帶bind方法實現柯里化是很方便的,比本身寫要簡化好多.

注意:

  • 在 ES6 中,能夠用 ... 操做符代替 apply(..) 來「展 開」數組,foo(...[1,2]) 和 foo(1,2)是同樣的,這樣能夠避免沒必要要的 this 綁定。惋惜,在 ES6 中沒有柯里化的相關語法,所以仍是須要使用 bind(..)。
  • 當使用null或者undefined進行綁定時,要確保該函數內沒有使用this,不然此時很容易對全局變量形成破壞!尤爲是使用第三方庫的方法!

更安全的this
若是函數內使用了this,直接使用null則可能會對全局形成破壞.所以咱們能夠經過建立一個「DMZ」(demilitarized zone,非軍事區)對象——它就是一個空的非委託的對象(委託在第 5 章和第 6 章介紹)。讓this綁定到這個"DMZ上.這樣就不會對全局形成破壞. 怎麼建立DMZ呢.就是經過Object.create(null) 建立一個空對象.這種方法和 {} 很像,可是並不會建立 Object.prototype 這個委託,因此它比 {}「更空」更加安全.

PS:實際使用通常不會遇到這種狀況(也多是我太菜,沒遇到),若是函數內有this,那確定是有須要調用的變量或函數,直接把它綁定到一個空對象上.那什麼都取不到,還有什麼意義?因此函數沒有this就傳入null.若是有this就把它綁定到真正須要它的對象上,而不是一個空對象上.這些是我本身的看法,若是有不妥的,歡迎留言指正.

2.4.2 間接引用

function foo() { 
  console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo }; 
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2 其實就是foo() 此時this默認綁定到window
複製代碼

例子中的間接引用實際上是對函數的理解不深形成的.其實(p.foo = o.foo)()就是(foo)(),這樣就是全局調用foo()因此this默認就綁定到了window上.
注意: 對於默認綁定來講,決定 this 綁定對象的並非調用位置是否處於嚴格模式,而是 函數體是否處於嚴格模式。若是函數體處於嚴格模式,this 會被綁定到 undefined,不然 this 會被綁定到全局對象。(對於這段話其實在2.2.1節就應該說了!)

2.4.3 軟綁定

硬綁定會大大下降函數的靈活性,使用硬綁定以後就沒法使用隱式綁定或者顯式綁定來修改 this。這時候則須要使用軟綁定.
Tips: 這裏給的軟綁定方法仍是挺好的.可是建議仍是在本身的代碼裏使用,並註釋清除.以避免別人使用,對this錯誤的判斷.

2.5 this詞法

ES6 中介紹了一種沒法使用上面四條規則的特殊函數類型:箭頭函數。
箭頭函數不使用 this 的四種標準規則,而是根據外層(函數或者全局)做用域來決定 this。(而傳統的this與函數做用域沒有任何關係,它只與調用位置的上下文對象有關.這點在本章開頭就已經反覆強調了.)

重要:

  • 箭頭函數最經常使用於回調函數中,例如事件處理器或者定時器.
  • 箭頭函數能夠像 bind(..) 同樣確保函數的 this 被綁定到指定對象
  • 箭頭函數用更常見的詞法做用域取代了傳統的 this 機制。

注意: 這種狀況:

function module() {
  return this.x;
}
var foo = {
  x: 99,
  bar:module.bind(this) //此時bind綁定的this爲window.
  
}
var x="window"

console.log(foo.bar())//window

複製代碼

在 ES6 以前咱們就已經在使用一種幾乎和箭頭函數徹底同樣的模式:

function foo() {
var self = this; // lexical capture of this 
  setTimeout( function(){
             console.log( self.a );
         }, 100 );
  }
var obj = { 
    a: 2
};
foo.call( obj ); // 2
複製代碼

雖然 self = this 和箭頭函數看起來均可以取代 bind(..),可是從本質上來講,它們想替代的是 this 機制。(的確是這樣,我通常會用me替代self.由於少兩個單詞=.=)

關於this的編碼規範建議:

    1. 只使用詞法做用域並徹底拋棄錯誤this風格的代碼;
    1. 徹底採用 this 風格,在必要時使用 bind(..),儘可能避免使用 self = this 和箭頭函數。

在本身實際工做中,實際上是兩種混用的,絕大部分狀況下都會使用詞法做用域風格.由於有時候你真的很難作到徹底統一.我如今的習慣是,在寫任何函數時,開頭第一個就是var me =this;這樣在看到函數第一眼,就知道:哦,這個函數是用詞法做用域風格的.尤爲函數內涉及到回調.這樣就避免了寫着寫着發現this綁定到其餘地方去了,一個函數裏面this不統一的狀況.

2.6 小結

(這裏總結得很好,我就所有copy了) 若是要判斷一個運行中函數的 this 綁定,就須要找到這個函數的直接調用位置。找到以後就能夠順序應用下面這四條規則來判斷 this 的綁定對象。

    1. 由new調用?綁定到新建立的對象。
    1. 由call或者apply(或者bind)調用?綁定到指定的對象。
    1. 由上下文對象調用?綁定到那個上下文對象。
    1. 默認:在嚴格模式下綁定到undefined,不然綁定到全局對象。

必定要注意,有些調用可能在無心中使用默認綁定規則。若是想「更安全」地忽略 this 綁定,你能夠使用一個 DMZ 對象,好比 ø = Object.create(null),以保護全局對象。

ES6 中的箭頭函數並不會使用四條標準的綁定規則,而是根據當前的詞法做用域來決定 this,具體來講,箭頭函數會繼承外層函數調用的 this 綁定(不管 this 綁定到什麼)。這其實和 ES6 以前代碼中的 self = this 機制同樣。

特別注意: 其中最須要注意的就是當你使用jquery或vue時,此時this是被動態綁定了的.大多數 jQuery 方法將 this 設置爲已選擇的 dom 元素。使用 Vue.js時,則方法和計算函數一般將 this 設置爲 Vue 組件實例。vue文檔中全部的生命週期鉤子自動綁定 this 上下文到實例中,所以你能夠訪問數據,對屬性和方法進行運算。這意味着你不能使用箭頭函數來定義一個生命週期方法 (例如 created: () => this.fetchTodos())。這是由於箭頭函數綁定了父上下文,所以 this 與你期待的 Vue 實例不一樣,this.fetchTodos 的行爲未定義。 也包括使用第三方ajax時,例如axios.解決方法也很簡單,要麼使用傳統的function或者使用let _this=this進行接管.其實當你使用vue時,你默認的思想就是this指的就是vue實例.因此除了鉤子函數和axios裏會有點影響外,其他還好.

PS 這裏再補充說明 上下文(對象)與函數做用域的區別於聯繫:

  • 上下文: 能夠理解爲一個對象,全部的變量都儲存在裏面.上下文環境是在函數被調用並被引擎執行時建立的.若是你沒調用,那麼就沒有上下文.
  • 做用域: 除了全局做用域,只有函數和ES6新增的let,const才能建立做用域.建立一個函數就建立了一個做用域,不管你調用不調用,函數只要建立了,它就有獨立的做用域.做用域控制着被調用函數中的變量訪問.
  • 二者: 做用域是基於函數的,而上下文是基於對象的。做用域涉及到所被調用函數中的變量訪問,而且不一樣的調用場景是不同的。上下文始終是this關鍵字有關, 它控制着this的引用。一個做用域下可能包含多個上下文。有可能歷來沒有過上下文(函數沒有被調用);有可能有過,如今函數被調用完畢後,上下文環境被銷燬了(垃圾回收);有可能同時存在一個或多個(閉包)。

第三章 對象

3.1 語法

對象能夠經過兩種形式定義:聲明(文字)形式(就是常說的對象字面量)和構造形式。

  • 聲明形式(對象字面量):
var myObj = { 
  key: value
  // ... 
};
複製代碼
  • 構造形式:
var myObj = new Object(); 
myObj.key = value;
複製代碼

構造形式和文字形式生成的對象是同樣的。惟一的區別是,在文字聲明中你能夠添加多個 鍵 / 值對,可是在構造形式中你必須逐個添加屬性。 PS:其實咱們絕大部分狀況下都是使用對象字面量形式建立對象.

3.2 類型

在JavaScript中一共有6中主要類型(術語是"語言類型")

  • string
  • number
  • boolean
  • null
  • undefined
  • object

簡單數據類型: 其中string、boolean、number、null 和 undefined屬於簡單基本類型,並不屬於對象. null 有時會被看成一種對象類型,可是這其實只是語言自己的一個 bug,即對 null 執行typeof null 時會返回字符串 "object"。實際上,null 自己是基本類型。
PS: 緣由是這樣的,不一樣的對象在底層都表示爲二進制,在 JavaScript 中二進制前三位都爲 0 的話會被判 斷爲 object 類型,null 的二進制表示是全 0,天然前三位也是 0,因此執行 typeof 時會返回「object」。

對象:
對象除了咱們本身手動建立的,JavaScript其實內置了不少對象,也能夠說是對象的一個子類型.
內置對象:

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

在 JavaScript 中,這些內置對象實際上只是一些內置函數。這些內置函數能夠看成構造函數(由 new 產生的函數調用——參見第 2 章)來使用.
幾點說明:

  • 函數就是對象的一個子類型(從技術角度來講就是「可調用的對象」)。JavaScript 中的函數是「一等公民」,由於它們本質上和普通的對象同樣(只是能夠調用),因此能夠像操做其餘對象同樣操做函數(好比看成另外一個函數的參數)。
  • 經過字面量形式建立字符串,數字,布爾時,引擎會自動把字面量轉換成 String 對象,Number對象,Boolean對象,因此它們是能夠訪對應對象內置的問屬性和方法。
  • null 和 undefined 沒有對應的構造形式,它們只有文字形式。相反,Date 只有構造,沒有文字形式。
  • 對於 Object、Array、Function 和 RegExp(正則表達式)來講,不管使用文字形式仍是構造形式,它們都是對象,不是字面量(這是確定的,由於無論哪一種形式一建立出來就是對象類型,不多是其餘類型,其實是不存在字面量這一說的)。可是使用構造形式能夠提供一些額外選項(內置)。
  • Error 對象不多在代碼中顯式建立,通常是在拋出異常時被自動建立。也能夠使用 new Error(..) 這種構造形式來建立,不過通常來講用不着。

3.3 內容

對象屬性:由一些存儲在特定命名位置的(任意類型的)值. 屬性名:存儲在對象容器內部的屬性的名稱.屬性值並不會存在對象內.而是經過屬性名(就像指針,從技術角度來講就是引用)來指向這些值真正的存儲位置(就像房門號同樣).
屬性名的兩種形式:

    1. 使用.操做符.也是咱們最經常使用的形式.它一般被稱爲"屬性訪問". . 操做符會要求屬性名知足標識符的命名規範.
    1. 使用[".."]語法進行訪問.這個一般被稱爲"鍵訪問".[".."]語法能夠接受任意UTF-8/Unicode 字符串做爲屬性名。而且[".."]語法使用字符串來訪問屬性,若是你的屬性名是一個變量,則能夠使用書中的例子myObject[idx]形式進行訪問.這也是最常使用"鍵訪問"的狀況.但若是idx是屬性名則仍是需寫成myObject["idx"]字符串形式.

注意: 書中說在對象中,屬性名永遠都是字符串。若是你使用 string(字面量)之外的其餘值做爲屬性名,那它首先會被轉換爲一個字符串。即便是數字也不例外,雖然在數組下標中使用的的確是數字,可是在對象屬性名中數字會被轉換成字符串 . 在ES6以前這段話是正確的,可是如今有了symbol. symbol也能夠做爲對象屬性名使用,而且symbol是不能夠轉化爲字符串形式的!

補充: 這裏我在書中的例子基礎上進行了修改,獲得這個例子:

var myObject = { 
  a:2,
  idx:111
};
var idx="a";
console.log( myObject[idx] ); //2
console.log( myObject["idx"] ); //111
console.log( myObject[this.idx] );  // 2 此時this是指向window.[]裏的this一樣符合上一章所講的規則
//結果是否和你所想得同樣呢?
複製代碼

3.3.1 可計算屬性名

ES6 增長了可計算屬性名,能夠在文字形式中使用 [] 包裹一個表達式來看成屬性名:

var prefix = "foo";

var myObject = {
   [prefix + "bar"]:"hello", 
   [prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
複製代碼

3.3.2 屬性與方法

  • 咱們常常把對象內部引用的函數稱爲「方法」(的確如此).
  • 實際上函數並不屬於該對象,它不過是對函數的引用罷了.對象屬性訪問返回的函數和其餘函數沒有任何區別(除了可能發生的隱式綁定this到該對象)。
  • 即便你在對象的文字形式中聲明一個函數表達式,這個函數也不會「屬於」這個對象—— 它們只是對於相同函數對象的多個引用。

3.3.3 數組

  • 數組支持[]形式訪問儲存的值,其中[]內的值默認形式爲數值下標(爲從0開始的整數,也就是常說的索引).例如myArray[0]
  • 數組也是對象,因此雖然每一個下標都是整數,你仍然能夠給數組添加屬性.例如myArray.baz = "baz".注意:添加新屬性後,雖然能夠訪問,但數組的 length 值不會改變.
  • 數組能夠經過myArray[1]=11;myArray["2"]=22;這種形式對數組內容進行修改,添加.
  • 雖然數組也能夠和對象同樣經過鍵/值 對 形式來使用.但JS已經對數組的行爲和用途進行了優化.因此仍是建議使用默認的下標/值 對 形式來使用.

3.3.4 複製對象

  • 複製分爲淺拷貝和深拷貝.淺拷貝會對對象中的基本數據類型進行復制(在內存中開闢新的區域),對於對象則是繼續引用.而不是從新建立一個"同樣的"對象.深拷貝則是對其中的全部內(容包括對象)進行深層次的複製.
  • 通常狀況下咱們能夠經過JSON來複制對象.var newObj = JSON.parse( JSON.stringify( someObj ) );.但須要指出的是這種方法對於包含function函數或者Date類型的對象則無論用!
  • ES6 定義了 Object.assign(..) 方法來實現淺複製。具體用法在這就不贅述了.

3.3.5 屬性描述符

從 ES5 開始,全部的屬性都具有了屬性描述符。

  • 查看屬性描述符: 能夠使用Object.getOwnPropertyDescriptor( myObject, "a" );方法查看myObject對象裏屬性a的屬性描述符.
  • 配置屬性描述符: 能夠使用Object.defineProperty(..)方法對屬性的屬性描述符就像配置.舉個例子:
var myObject = {};
Object.defineProperty( myObject, "a", {
        value: 2,
        writable: true, 
   	  configurable: true, 
   	  enumerable: true
    } );
myObject.a; // 2
//該方法能夠配置四個屬性描述符
複製代碼

注意: 書中關於屬性描述符也被稱爲「數據描述符」實際上是不夠準確的. 對象裏目前存在的屬性描述符有兩種主要形式:數據描述符存取描述符。數據描述符是一個具備值的屬性,該值多是可寫的,也可能不是可寫的。存取描述符是由getter和setter函數對描述的屬性。描述符必須是這兩種形式之一;不能同時是二者。(getter和setter是後面立刻要講到的兩個描述符)它們的關係以下:(詳情能夠查看MDN的解釋)

configurable enumerable value writable get set
數據描述符 Yes Yes Yes Yes No No
存取描述符 Yes Yes No No Yes Yes

若是一個描述符不具備value,writable,get 和 set 任意一個關鍵字,那麼它將被認爲是一個數據描述符。若是一個描述符同時有(value或writable)和(get或set)關鍵字,將會產生一個異常。

value就是該屬性對應的值。默認爲 undefined。下面分別介紹剩下的三個屬性描述符鍵值:

  • 1. Writable 決定是否能夠修改屬性的值。當被設置爲false後,再對屬性值進行修改,則會靜默失敗(silently failed,修改不成功,也不報錯)了。若是在嚴格模式下,則會報出TypeError錯誤.
  • 2. Configurable 決定屬性描述符是否可配置.若是爲true,就能夠使用 defineProperty(..) 方法來修改屬性描述符.注意:無論是否是處於嚴格模式,修改一個不可配置的屬性描述符都會出錯。而且把 configurable 修改爲 false 是單向操做,沒法撤銷! 可是有個例外即使屬性是 configurable:false,咱們仍是能夠 把 writable 的狀態由 true 改成 false,可是沒法由 false 改成 true。除了沒法修改,configurable:false 還會禁止刪除這個屬性.
  • 3. Enumerable 決定該屬性是否會出如今對象的屬性枚舉中.好比說 for..in 循環。若是把 enumerable 設置成 false,這個屬性就不會出如今枚舉中,雖然仍然能夠正常訪問它。相對地,設置成 true 就會讓它出如今枚舉中。

3.3.6 不變性

除了上面提到的Object.defineProperty(..),ES5還能夠經過不少種方法來實現屬性或者對象的不可變.
注意: 這些全部方法都是隻能淺不變,若是目標對象引用了其餘對象(數組、對象、函數,等),其餘對象的內容不受影響,仍然是可變的.相似於淺拷貝.

說明: 在 JavaScript 程序中不多須要深不可變性。 有些特殊狀況可能須要這樣作,可是根據通用的設計模式,若是你發現須要密封或者凍結全部的對象,那你或許應當退一步,從新思考一下程序的設計,讓它能更好地應對對象值的改變。

方法:

  • 1. 對象常量(不可改) 結合 writable:false 和 configurable:false 就能夠建立一個真正的常量屬性(不可修改、重定義或者刪除)
  • 2. 禁止擴展(不可增) 使用 Object.prevent Extensions(myObject),能夠禁止一個對象添加新屬性而且保留已有屬性.在非嚴格模式下,建立屬性 b 會靜默失敗。在嚴格模式下,將會拋出 TypeError 錯誤。
  • 3. 密封(不可配置,但可修改) 使用Object.seal(..) 會建立一個「密封」的對象,這個方法實際上會在一個現有對象上調用Object.preventExtensions(..) 並把全部現有屬性標記爲 configurable:false。密封以後不只不能添加新屬性,也不能從新配置或者刪除任何現有屬性(雖然能夠修改屬性的值)。
  • 4. 凍結(不可配置,也不可修改) Object.freeze(..) 會建立一個凍結對象,這個方法實際上會在一個現有對象上調用 Object.seal(..) 並把全部「數據訪問」屬性標記爲 writable:false,這樣就沒法修改它們的值。這個方法是你能夠應用在對象上的級別最高的不可變性,它會禁止對於對象自己及其任意直接屬性的修改(不過就像咱們以前說過的,這個對象引用的其餘對象是不受影響的)。

注意: 你能夠「深度凍結」一個對象(連引用的對象也凍結),具體方法爲,首先在這個對象上調用 Object.freeze(..), 而後遍歷它引用的全部對象並在這些對象上調用 Object.freeze(..)。可是必定要謹慎!由於你引用的對象可能會在其餘地發也被引用.

說明: 在 JavaScript 程序中不多須要深不可變性。有些特殊狀況可能須要這樣作, 可是根據通用的設計模式,若是你發現須要密封或者凍結全部的對象,那你或許應當退一步,從新思考一下程序的設計,讓它能更好地應對對象值的改變。

3.3.7 [[Get]]

var myObject = { 
   a: 2
};
myObject.a; // 2
複製代碼

myObject.a是怎麼取到值2的?
myObject.a 經過對象默認內置的[[Get]] 操做(有點像函數調用:[Get]).首先它會在對象中查找是否有名稱相同的屬性, 若是找到就會返回這個屬性的值。若是沒有找到名稱相同的屬性,按照 [[Get]] 算法的定義會執行另一種很是重要的行爲。其實就是遍歷可能存在的 [[Prototype]] 鏈,也就是在原型鏈上尋找該屬性。若是仍然都沒有找到名稱相同的屬性,那 [[Get]] 操做會返回值 undefined.

注意: 若是你引用了一個當前詞法做用域中不存在的變量,並不會像對象屬性同樣返回 undefined,而是會拋出一個 ReferenceError 異常.

3.3.8 [[Put]]

既然有能夠獲取屬性值的 [[Get]] 操做,就必定有對應的 [[Put]] 來設置或者建立屬性.

[[Put]] 被觸發時的操做分爲兩個狀況:1. 對象中已經存在這個屬性 2. 對象中不存在這個屬性.

若是對象中已經存在這個屬性,[[Put]] 算法大體會檢查下面這些內容:

    1. 屬性是不是訪問描述符(參見下一節)?若是是而且存在setter就調用setter。
    1. 屬性的數據描述符中writable是不是false?若是是,在非嚴格模式下靜默失敗,在嚴格模式下拋出 TypeError 異常。
    1. 若是都不是,將該值設置爲屬性的值。

若是對象中不存在這個屬性,[[Put]] 操做會更加複雜。會在第 5 章討論 [[Prototype]] 時詳細進行介紹。

3.3.9 Getter和Setter

對象默認的 [[Put]] 和 [[Get]] 操做分別能夠控制屬性值的設置和獲取。 目前咱們還沒法操做[[Get]] 和 [[Put]]來改寫整個對象 ,可是在ES5中能夠使用 getter 和 setter 改寫部分默認操做,只能應用在單個屬性上,沒法應用在整個對象上

注意: 書中後面說的訪問描述符就是存取描述符.關於屬性描述符,存取描述符及數據描述符能夠查看MDN的解釋)

getter: getter 是一個隱藏函數,會在獲取屬性值時調用。同時會覆蓋該單個屬性默認的 [[Get]]操做.當你設置getter時,不能同時再設置value或writable,不然就會產生一個異常.而且當你設置getter或setter時,JavaScript 會忽略它們的 value 和 writable 特性.

語法: {get prop() { ... } }{get [expression]() { ... } }.其中prop:要設置的屬性名. expression:從 ECMAScript 2015 開始能夠使用計算屬性名. 使用方式:

var myObject = {
  a: 1111, //在後面會發現myObject.a爲2,這是由於設置了getter因此忽略了value特性.
  //方式一:在新對象初始化時定義一個getter
  get a() {
    return 2
  }
};

Object.defineProperty( 
  myObject, // 目標對象 
  "b", // 屬性名
  {
    // 方式二:使用defineProperty在現有對象上定義 getter
    get: function(){ return this.a * 2 },
    // 確保 b 會出如今對象的屬性列表中
    enumerable: true
   }
);

myObject.a = 3;  //由於設置了getter因此忽略了writable特性.因此這裏賦值沒成功
myObject.a; // 2
myObject.b; // 4

delete myObject.a;//能夠使用delete操做符刪除
複製代碼

setter: setter 是一個隱藏函數,會在獲取屬性值時調用。同時會覆蓋該單個屬性默認的 [[Put]]操做(也就是賦值操做).當你設置setter時,不能同時再設置value或writable,不然就會產生一個異常.而且當你設置getter或setter時,JavaScript 會忽略它們的 value 和 writable 特性.

語法: {set prop(val) { . . . }}{set [expression](val) { . . . }}.其中prop:要設置的屬性名. val:用於保存嘗試分配給prop的值的變量的一個別名。expression:從 ECMAScript 2015 開始能夠使用計算屬性名. 使用方式:

var myObject = {
  //注意:一般來講 getter 和 setter 是成對出現的(只定義一個的話 一般會產生意料以外的行爲):
  //方式一:在新對象初始化時定義一個setter
  set a(val) {
    this._a_ = val * 2
  },
  get a() {
    return this._a_ 
  }
};

Object.defineProperty( 
  myObject, // 目標對象 
  "b", // 屬性名
  {
    set: function(val){ this._b_ = val * 3 },
    // 方式二:使用defineProperty在現有對象上定義 setter
    get: function(){ return this._b_ },
    // 確保 b 會出如今對象的屬性列表中
    enumerable: true
   }
);

myObject.a = 2;  
myObject.b = 3;  
console.log(myObject.a); //4
console.log(myObject.b);//9

console.log(myObject._a_);//4
console.log(myObject._b_);//9

delete myObject.a;//能夠使用delete操做符刪除
複製代碼

3.3.10 存在性

屬性存在性: 如何判斷一個對象是否存在某個屬性(準確來講是檢查這個屬性名是否存在),這時就須要用到:

    1. in操做符 in 操做符會檢查屬性是否在對象及其 [[Prototype]] 原型鏈中(參見第 5 章)。
    1. hasOwnProperty(..) hasOwnProperty(..) 只會檢查屬性是否在 myObject 對象中,不會檢查 [[Prototype]] 鏈。

注意:

  • 1.若是有的對象可能沒有鏈接到 Object.prototype( 經過Object. create(null) 來建立——參見第 5 章)。在這種狀況下,形如myObejct.hasOwnProperty(..) 就會失敗。這時能夠使用一種更增強硬的方法來進行判斷:Object.prototype.hasOwnProperty. call(myObject,"a"),它借用基礎的 hasOwnProperty(..) 方法並把它顯式綁定(參見第2章)到 myObject 上。
  • 2.對於數組來講,不要使用in操做符,由於它檢查的是屬性名,在數組中屬性名就是索引,它並非咱們所關注的重點.對於數組咱們更關注的是它所存的值,因此對於數組檢查某個值是否存在仍是採用indexOf方法.

屬性可枚舉性: 若是一個屬性存在,且它的enumerable 屬性描述符爲true時.則它是可枚舉的.而且能夠被for..in 循環. 一個屬性不只僅須要存在,還須要它的enumerable 爲true纔是可枚舉的,才能被for...in遍歷到.
注意: for...in不適合對數組進行遍歷,對數組的遍歷仍是使用傳統的for循環.

對屬性的可枚舉性判斷,則須要用到如下幾種方法:

    1. propertyIsEnumerable(..) 會檢查給定的屬性名是否直接存在於對象中(而不是在原型鏈上)而且知足 enumerable:true。
    1. Object.keys(..) 會返回一個數組,包含全部可枚舉屬性.
    1. Object.getOwnPropertyNames(..)會返回一個數組,包含全部屬性,不管它們是否可枚舉。

3.4 遍歷

關於這節我以爲仍是以理清for..in和for..of爲主.後面延伸的@@iterator及Symbol.iterator的使用,不必過於深究.注意書中123頁第二行done 是一個布爾值,表示是否還有能夠遍歷的值。有個錯誤,應該改爲done 是一個布爾值,表示遍歷是否結束。不然你在看後面它的說明時會感受到自相矛盾.這裏我也是以for..in和for..of爲主進行說明,也更貼近咱們實際使用.

for..in

  • for..in 循環能夠用來遍歷對象的可枚舉屬性列表(包括 [[Prototype]] 鏈)。
  • 實際上for..in遍歷的並非屬性值,而是屬性名(即鍵名 key).因此你想獲取屬性值仍是須要手動使用obj[key]來獲取.
  • 通常在遍歷對象時,推薦使用for..in.固然數組也是能夠使用for..in的.在遍歷數組時,推薦仍是使用for..of.

for..of

  • ES6 增長了一種用來遍歷數組的 for..of 循環語法(若是對象自己定義了迭代器的話也能夠遍歷對象)
  • for..of與for..in最大的不一樣點是,它循環的是屬性值,而不是屬性名.不過它只循環數組裏存放的值,不會涉及到對象裏的key.(關於這個我後面的例子裏會說具體對比明的)
  • for..of 循環首先會向被訪問對象請求一個迭代器對象,而後經過調用迭代器對象的next() 方法來遍歷全部返回值。數組有內置的 @@iterator,(對象沒有,因此不能使用for..of,除非咱們本身定義一個)所以 for..of 能夠直接應用在數組上。

例子比較

let arr = ['shotCat',111,{a:'1',b:'2'}]
arr.say="IG niu pi!"
//使用for..in循環
for(let index in arr){
    console.log(arr[index]);//shotCat 111 {a:'1',b:'2'} IG niu pi!
}
//使用for..of循環
for(var value of arr){
    console.log(value);//shotCat 111 {a:'1',b:'2'}
}
//注意 for..of並無遍歷獲得` IG niu pi!`.緣由我前面說過`它只循環數組裏存放的值,不會涉及到對象裏的key.`更不用說 [[Prototype]] 鏈.(for..in則會)
複製代碼

如何讓對象也能使用for..of ?
你能夠選擇使用書中的本身經過Object.defineProperty()定義一個Symbol.iterator屬性來實現.這裏我就不贅述了.也是最接近原生使用感覺的.不過我這裏要介紹一個稍微簡單點的方法來實現.就是使用上節講到的Object.keys()搭配使用.舉個例子:

var shotCat={
    name:'shotCat',
    age:'forever18',
    info:{
	sex:'true man',
    city:'wuhan',
    girlFriend:'新垣結衣!'
    }
}
for(var key of Object.keys(shotCat)){
    //使用Object.keys()方法獲取對象key的數組
    console.log(key+": "+shotCat[key]);
}
複製代碼

3.5 小結

書中小結總結得挺全的,這裏我就搬運下

  • JavaScript 中的對象有字面形式(好比 var a = { .. })和構造形式(好比 var a = new Array(..))。字面形式更經常使用,不過有時候構造形式能夠提供更多選項。
  • 對象是 6 個(或者是 7 個,取決於你的觀點)基礎類型之一。對象有包括 function 在內的子類型,不一樣子類型具備不一樣的行爲,好比內部標籤 [object Array] 表示這是對象的子類型數組。
  • 對象就是鍵 / 值對的集合。能夠經過 .propName 或者 ["propName"] 語法來獲取屬性值。訪問屬性時,引擎實際上會調用內部的默認 [[Get]] 操做(在設置屬性值時是 [[Put]]), [[Get]] 操做會檢查對象自己是否包含這個屬性,若是沒找到的話它還會查找 [[Prototype]] 鏈(參見第 5 章)。
  • 屬性的特性能夠經過屬性描述符來控制,好比 writable 和 configurable。此外,能夠使用 Object.preventExtensions(..)、Object.seal(..) 和 Object.freeze(..) 來設置對象(及其屬性)的不可變性級別。
  • 屬性不必定包含值——它們多是具有 getter/setter 的「訪問描述符」。此外,屬性能夠是可枚舉或者不可枚舉的,這決定了它們是否會出如今 for..in 循環中。
  • 能夠使用 ES6 的 for..of 語法來遍歷數據結構(數組、對象,等等)中的值,for..of 會尋找內置或者自定義的 @@iterator 對象並調用它的 next() 方法來遍歷數據值。

第四章 混合對象"類"

注意: 正如書中提示的那樣,整章一半以上幾乎都是講面向對象和類的概念.會讀得人云裏霧裏,給人哦,也許大概就是這樣子的感受.後面我仍是會對那些抽象的概念找到在JavaScript裏對應的"立足點",不至於對這些概念太"飄".

4.1 類理論

說明:

  • 類其是描述了一種代碼的組織結構形式.
  • 在js中類常見的就是構造函數,也能夠是經過ES6提供的class關鍵字;繼承就是函數;實例化就是對象,常見的就是經過new構造函數實現的.

類、繼承和實例化

注意: Javascript語言不支持「類」,所謂的"類"也是模擬出的「類」。即便是ES6引入的"類"實質上也是 JavaScript 現有的基於原型的繼承的語法糖。

4.1.1 「類」設計模式

一句話:類其實也是一種設計模式!

  • 類並非必須的編程基礎,而是一種可選的代碼抽象.
  • 有些語言(好比 Java)並不會給你選擇的機會,類並非可選的——萬物皆是類。
  • 其餘語言(好比 C/C++ 或者 PHP)會提供過程化和麪向類這兩種語法,開發者能夠選擇其中一種風格或者混用兩種風格。

4.1.2 JavaScript中的「類」

JavaScript 只有一些近似類的語法元素 (好比 new 和 instanceof),不過在後來的 ES6 中新增了一些元素,好比 class 關鍵字,其實質上也是 JavaScript 現有的基於原型的繼承的語法糖。也不是真正的類.

4.2 類的機制

這部分書中的描述,我理解起來也比較費勁,主要是它提到的棧,堆與我理解中內存裏的棧,堆相沖突了.這裏簡單說下個人理解,若有誤,感激指正.

stack類實際上是一種數據結構.它能夠儲存數據,並提供一些公用的方法(這和上面提到的類很類似).可是stack類其實只是一個抽象的表示,你想對它進行操做,就須要先對它進行實例化.

4.2.1 建造

這節主要就是說明"類"和"實例"的關係. 在JavaScript裏"類"主要是構造函數,"實例"就是對象.

一個類就像一張藍圖。爲了得到真正能夠交互的對象,咱們必須按照類來實例化一個東西,這個東西(對象)一般被稱爲實例,有須要的話,咱們能夠直接在實例上調用方法並訪問其全部公有數據屬性。

總而言之:類經過實例化獲得實例對象.

4.2.2 構造函數

  • 類實例是由一個特殊的類方法構造的,這個方法名一般和類名相同,被稱爲構造函數。
  • 實例就是由構造函數實例化的: new 構造函數.
  • 構造函數大多須要用 new 來調,這樣語言引擎才知道你想要構造一個新的類實例。
  • 構造函數會返回一個對象,這個對象就是實例.這個對象能夠調用類的方法.

4.3 類的繼承

在面向類的語言中,你能夠先定義一個類,而後定義一個繼承前者的類。後者一般被稱爲「子類」,前者一般被稱爲「父類」。子類能夠繼承父類的行爲,而且能夠根據本身的需求,修改繼承的行爲(通常並不會修改父類的行爲).注意:咱們討論的父類和子類並非實例,在JavaScript裏類通常都是構造函數。

4.3.1 多態

大概你看了它的"解釋",對多態仍是懵懵懂懂.這裏我再解釋下:
什麼是多態?
同一個操做,做用於不一樣的對象,會產生不一樣的結果。發出一個相同的指令後,不一樣的對象會對這個指令有不一樣的反應,故稱爲多態。 說明: 書中例子中的inherited其實就是至關於super.而且注意書中的這些例子都是僞代碼! 並非真的在JavaScript裏就是這樣實現的.補充:這裏是關於super的mdn連接.

  • 多態:
    • 相對性: 其實相對性就是子類相對於父類的引用(例如使用super實現引用),而且子類對父類的引用並不會對父類的行爲形成任何影響(並不會對父類自身的行爲進行從新定義),例如書中例子子類對drive()的引用.
    • 可重複定義: 子類繼承父類的某個方法,並能夠對這個方法進行再次定義,例如書中子類對drive()中的output進行修改.當調用方法時會自動選擇合適的定義,這句話怎麼理解,當子類實例化後,執行drive()方法時,它並不會直接去執行父類的drive().而是子類上的drive().簡單來講就是實例來源於那個類,它就使用那個類的方法.

說明:

  • 在 JavaScript 中「類」是屬於構造函數的(相似 Foo.prototype... 這樣的類型引用)。因爲 JavaScript中父類和子類的關係只存在於二者構造函數對應的 .prototype 對象中,所以它們的構造函數之間並不存在直接聯繫,從而沒法簡單地實現二者的相對引用(在 ES6 的類中能夠經過 super來「解決」這個問題,參見附錄 A)。
  • 多態並不表示子類和父類有關聯,子類獲得的只是父類的一份副本。類的繼承其實就是複製。
  • 其實這裏討論的這些概念其實在咱們實際工做中,已經使用了無數次,只是如今你須要理解"原來你是叫這個名字啊!"

4.3.2 多重繼承

多重繼承: 一個子類能夠繼承來自多個父類的方法.
多重繼承引起的問題: 多重繼承可能會出現,多個父類中方法名衝突的問題,這樣子類到底引用哪一個方法?
多重繼承與JavaScript: JavaScript自己並無提供多重繼承功能.但它能夠經過其餘方法來達到多重繼承的效果.

4.4 混入

JavaScript 中只有對象,並不存在能夠被實例化的「類」。一個對象並不會被複制到其餘對象,它們會被關聯起來(參見第 5 章)(其實就是引用,因此它的多態是"相對"的)。 因爲在其餘語言中類表現出來的都是複製行爲,所以 JavaScript 開發者也想出了一個方法來模擬類的複製行爲,這個方法就是混入(就是經過混入來模擬實現類的多重繼承)。

4.4.1 顯式混入

鄭重提醒: 書中這裏的類都是對象形式的.例子裏的sourceObj, targetObj,這就可能形成一個"誤導",在JavaScript裏是沒有真正的類,所謂的類也不過是咱們模擬出來的"類",不過是一種語法糖(包括ES6裏的class).在JavaScript裏"所謂的類"常常是一個構造函數,你並不能這樣進行遍歷,只能對它的實例對象進行這種操做.不要被書中例子帶進去了,不要混淆,畢竟咱們最終使用的是JavaScript(而不是其餘面向對象的語言.),它裏面的類經常並非一個對象!

顯式混入: 書中沒有給出明確的顯式混入的定義,可是讀完整章.基本就知道什麼是顯式混入了.顯式混入就是經過相似mixin()方法,顯式地將父對象屬性逐一複製,或者有選擇地複製(即例子中的存在性檢查)到子對象上.

顯式混入經常使用方法: 就是書中的例子, 首先有子對象,並對其進行特殊化(定義本身的屬性或方法).而後再經過mixin()方法將父對象有選擇地複製(即存在性檢查,過濾子對象已有的屬性,避免衝突)到子對象上.

顯式混入注意點: 顯式混入時,切記一點你要避免父對象的屬性與子對象特殊化的屬性衝突.這就是爲何例子中要進行存在性檢查,以及後面要說的混合複製,可能存在的重寫風險.

1. 再說多態(其實說的就是js裏的多態)
顯式多態: 將父對象裏的方法經過顯式綁定到子對象上.就是顯式多態.例如書中的例子:Vehicle.drive.call( this )。顯式多態也是爲了JS來模擬實現多重繼承的!
說明: 在ES6以前是沒有相對多態的機制。因此就使用call這種進行顯式綁定實現顯式動態.注意JavaScript裏實現多態的方法也被稱爲"僞多態".因此不要對後面忽然冒出的僞多態概念而一臉懵逼(其實整本書常常作這樣的事)

顯式多態(僞多態)的缺陷: 由於在JavaScript 中存在屏蔽(實際是函數引用的上下文不一樣),因此在引用的時候就須要使用顯式僞多態的方法建立一個函數關聯. 這些都會增長代碼的複雜度和維護難度(過多的this綁定,真的會讓代碼很難讀)。

2. 混合複製(顯式混入另外一種不經常使用方法)
前面的顯式混入的方法是先有子對象並進行特殊化,而後再有選擇地複製父對象屬性.這個不經常使用的方法則是反過來的,結合書中例子,它先用一個空對象徹底複製父對象的屬性,而後該對象複製特殊化對象的屬性,最後獲得子對象.這種方法明顯是比第一種麻煩的,而且在複製特殊化對象時,可能會對以前重名的屬性(即複製獲得的父對象屬性)進行重寫覆蓋.因此這種方法是存在風險,且效率低下的.

顯式混入的缺陷:

    1. 沒法作到真正的複製: 若是複製的對象中存在對函數的引用,那麼子對象獲得的是和父對象同樣的,對同一個函數的引用.若是某個子對象對函數進行了修改,那麼父對象及其餘子對象都會受到影響.很明顯這是不安全的.緣由是JavaScript 中的函數沒法進行真正地複製,你只能複製對共享函數對象的引用.
    1. 函數名和屬性名同名: 若是混入多個對象,則可能會出現這種狀況.目前如今仍沒有比較好的方式來處理函數和屬性的同名問題(提問:這種狀況下誰的優先級更高?滑稽臉)。

3. 寄生繼承
顯式混入模式的一種變體被稱爲「寄生繼承」,它既是顯式的又是隱式的. 首先會複製一份父類(對象)的定義,而後混入子類(對象)的定義(若是須要的話保留到父類的特殊引用),而後用這個複合對象構建實例。
說明: 寄生繼承與混合複製是很類似的,最大的區別是寄生繼承是經過實例化構造函數(JS中的"類")來實現複製的.

4.4.2 隱式混入

隱式混入: 它與顯示混入最大的區別,就是它沒有明顯的對父類(對象)屬性進行復制的過程.它是經過在構造函數調用或者方法調用中使用顯式綁定例如: Something.cool.call( this)來實現混入(多重繼承).其本質就是經過改變this指向來實現混入.

4.5 小結

整章的重點其實就是讓你理解什麼叫類.除了最後一小節的混入和JavaScript有那麼一點點關係.其他的小結和JavaScript都沒什麼關係.重要的是理解類這種思想和設計模式.
重點:

  • 1.類意味着複製!
    1. 傳統的類被實例化時,它的行爲會被複制到實例中。類被繼承時,行爲也會被複制到子類中。
    1. 多態(在繼承鏈的不一樣層次名稱相同可是功能不一樣的函數)看起來彷佛是從子類引用父類,可是本質上引用的實際上是複製的結果。
    1. JavaScript 並不會(像類那樣)自動建立對象的副本。(你只能本身手動複製,並且複製的還不完全!)
    1. 混入模式(不管顯式仍是隱式)能夠用來模擬類的複製行爲,可是一般會產生醜陋而且脆弱的語法,好比顯式僞多態(OtherObj.methodName.call(this, ...)),這會讓代碼更加難 懂而且難以維護。
    1. 顯式混入實際上沒法徹底模擬類的複製行爲,由於對象(和函數!別忘了函數也是對象)只能複製引用,沒法複製被引用的對象或者函數自己。忽視這一點會致使許多問題。
    1. 在 JavaScript 中模擬類是得不償失的,雖然能解決當前的問題,可是可能會埋下更多的隱患。(但實際,咱們用得卻不少)

第五章 原型

注意:本章的前提是你已經比較熟悉原型及原型鏈.不太熟或者不知道的能夠,經過這篇文章熟悉下.

5.1 [[Prototype]]

JavaScript 中的對象有一個特殊的 [[Prototype]] 內置屬性,其實就是對於其餘對象的引用(通常就是其構造函數prototype屬性的引用)。幾乎全部的對象在建立時 [[Prototype]] 屬性都會被賦予一個非空的值。
吐槽: 書中有這樣一句話 "注意:很快咱們就能夠看到,對象的 [[Prototype]] 連接能夠爲空,雖然不多見。"我前先後後看了三遍都沒找到它所說的對象的 [[Prototype]] 連接能夠爲空.的狀況!應該是做者寫忘記了.ok,這裏我來講下對象的 [[Prototype]] 連接能夠爲空的狀況.就是經過Object.create(null)獲得的對象.它的 [[Prototype]] 是爲空的.應該說它的全部都是空的.爲何?由於null是原型鏈的頂端.它是沒有[[Prototype]]的.對應的能夠對比下console.log(Object.create({}))console.log(Object.create(null))

[[Prototype]]有什麼用?
我原覺得做者會說能夠做爲存放實例對象的公共屬性,而後像類同樣講得更深入點.不過此次只是說了它代表的做用.

做用: 就是存放哪些不在對象自身的屬性. 當咱們訪問一個對象的屬性時,此時對象的內部默認操做[[Get]],首先會檢查對象自己是否有這個屬性,若是有的話就使用它。若是沒有的話,[[Get]] 就會繼續訪問對象的 [[Prototype]] 鏈.([[Prototype]]其實就是其構造函數的prototype屬性.也是一個對象.)若是找到,就返回該屬性值.若是沒有就繼續尋找下一個[[Prototype]]鏈.直到找完整條[[Prototype]]鏈.仍是沒有的話,[[Get]] 就會返回undefined.

補充:

  • 使用 for..in 遍歷對象時 任何能夠經過原型鏈訪問到 (而且是 enumerable:true)的屬性都會被枚舉。(其實這個在第三章裏我說過)
  • 使用 in 操做符 一樣會查找對象的整條原型鏈(不管屬性是否可枚舉)

5.1.1 Object.prototype

全部普通的 [[Prototype]] 鏈最終都會指向內置的 Object.prototype。(Object.prototype的[[Prototype]] 最終會指向null.null就是最後的終點). 這個 Object.prototype 對象,包含了 JavaScript 中許多通用的功能,例如:toString() , valueOf(), hasOwnProperty(..)和 isPrototypeOf(..)。

5.1.2 屬性設置和屏蔽

說明: 看完本節時,切記不要對myObject.foo = "bar"這種簡單的對象屬性賦值產生顧慮和疑惑.這種賦值絕對不會對原型鏈產生任何影響!基本也不會出現賦值不成功的狀況.若是有人敢在團隊項目裏修改對象的屬性描述符,早就被拖出去打死了!!! 這部分能夠看作補充知識,知道有這些奇葩設定就行.其實這節更應該關注的是myObject.foo的返回值.
注意: 書中提到的動詞屏蔽其實指的就是在對象上建立同名屬性(原型鏈上已有該屬性).注意不要被繞暈了.還有++就至關於myObject.a=myObject.a+1,注意分解就行,不存在什麼特別須要小心的.

5.2 「類」

  • JavaScript裏只有對象,沒有類!
  • JavaScript不須要經過類來抽象對象.而是本身直接建立對象,並定義對象的行爲.

5.2.1 「類」函數

吐槽:模仿類居然被說成奇怪的無恥濫用!?不這樣作,js那些高級用法怎麼實現?怎麼會有如今前端的百花齊放(輪子滿地跑)?這也是冒得辦法的辦法啊!畢竟當時js只是小衆,不期望它有多大能耐.畢竟只是一我的用7天"借鑑"出來的東西.

"類"函數: JavaScript用來模仿類的函數就被稱爲類函數,其實就是咱們常說的構造函數.

"類"函數模擬類的關鍵: 全部的函數默認都會擁有一個名爲 prototype 的公有而且不可枚舉(參見第 3 章)的屬性,它會指向另外一個對象.當咱們經過new 函數(構造函數)來獲得實例對象時,此時new會給實例對象一個內部的 [[Prototype]]屬性,實例對象內部的[[Prototype]]屬性與構造函數的prototype屬性都指向同一個對象.那JS的這個特性怎麼模擬類呢?首先類的本質就是複製!.明白這點後,咱們就須要實現僞複製.咱們能夠將類裏的屬性,放在函數的prototype屬性裏.這樣該函數的實例對象就能夠經過[Prototype]訪問這些屬性.咱們也常常把這種行爲稱爲原型繼承(做者後面會瘋狂吐槽這個稱呼,我後面再解釋爲何吐槽).這樣就實現了僞"複製". 能夠達到和類類似的效果.

注意: 雖說全部的函數默認都會擁有一個名爲 prototype屬性.但也有特殊的時候.就不是默認的狀況.就是經過bind()硬綁定時.所返回的綁定函數,它是沒有prototype屬性的!

圖解真正的類與JS的模擬類:

關於原型繼承這個名字的瘋狂吐槽: 做者的吐槽主要集中在"繼承"兩個字,緣由是在面向類的語言中,"繼承"意味着複製,但在JavaScript裏原型繼承卻根本不是這個意思,它並無複製,而是用原型鏈來實現.因此瘋狂吐槽其誤導.

什麼是差別繼承? 我根本沒聽過這個術語,初次看做者所謂的解釋,這是啥?他想說啥?後來讀了好多遍,終於大概理解了.若是你也看不懂做者想在表達什麼,就pass這部分.不必理解.反而會把你看得更迷惑. 好了,我來解釋下什麼叫差別繼承.差別繼承就是原型繼承的一個不經常使用的別名.咱們知道對象能夠經過原型鏈繼承一部分屬性,但咱們仍能夠給對象設置其餘有差別不一樣的屬性.這也就能夠稱爲差別繼承.

5.2.2 「構造函數」

構造函數之因此是構造函數,是由於它被new調用,若是沒被new調用,它就是一個普通函數.實際上,new會劫持全部普通函數並用構造對象的形式來調用它,而且不管如何都會構造返回一個對象.

5.2.3 技術

關於兩種「面向類」的技巧,我這就不說明了,理解了這部分第一第二章關於this的使用,就很簡單了.

prototype.constructor: 爲了正確理解constructor.我特地在標題上加上prototype.是想強調:一個對象訪問constructor時,會默認訪問其原型對象上的constructor屬性.

注意:

function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 有時候咱們會須要建立一個新原型對象,所以也不會有默認的constructor屬性指向構造函數
// 須要在 Foo.prototype 上「修復」丟失的 .constructor 屬性
// 關於 defineProperty(..),參見第 3 章 
Object.defineProperty( Foo.prototype, "constructor" , {
  enumerable: false,//不可枚舉
  writable: true,
  configurable: true,
  value: Foo // 讓 .constructor 指向 Foo
} );
//上面這種方法是比較嚴謹,也比較麻煩的.而且使用Object.defineProperty()風險是很大的.
//因此咱們實際是這樣修改的
Foo.prototype.constructor=Foo; //直接將其賦值Foo 惟一要注意的是此時constructor是可枚舉的.會被實例對象的for..in..遍歷到.
複製代碼

5.3 (原型)繼承

原型對象到原型對象的繼承: 例如:Bar.prototype 到 Foo.prototype 的委託關係, 正確的JavaScript中「原型風格」:

function Foo(name) {
  this.name = name;
}
Foo.prototype.myName = function() { 
  return this.name;
};
function Bar(name,label) { 
  Foo.call( this, name ); 
  this.label = label;
}

// 咱們建立了一個新的 Bar.prototype 對象,而且它的[[Prototype]] 關聯Foo.prototype 
Bar.prototype = Object.create( Foo.prototype );
// 注意!Object.create()是返回一個新的對象,因此如今沒有 Bar.prototype.constructor 了 
// 若是你須要這個屬性的話可能須要手動修復一下它
Bar.prototype.myLabel = function() { 
  return this.label;
};

var a = new Bar( "a", "obj a" );

a.myName(); // "a"
a.myLabel(); // "obj a"
複製代碼

錯誤用法:

  • 1, Bar.prototype = Foo.prototype; 此時並不會建立一個關聯到 Bar.prototype 的新對象,它只是讓 Bar.prototype 直接引用 Foo.prototype 對象。 所以當你執行相似 Bar.prototype. myLabel = ... 的賦值語句時會直接修改 Foo.prototype 對象自己。
  • 2, Bar.prototype = new Foo(); 它使用 了 Foo(..) 的「構造函數調用」,若是函數 Foo 有一些其餘操做的話,尤爲是與this有關的的話,就會影響到 Bar() 的「後代」,後果不堪設想。

結論: 要建立一個合適的關聯對象,咱們需使用 Object.create(..) 而不是使用具備反作用的 Foo(..)。這樣作惟一的缺點就是須要建立一個新對象而後把舊對象拋棄掉(主要是須要手動設置constructor),不能直接修改已有的默認對象。

檢查"類"關係

  • instanceof 操做符: 驗證左邊的普通對象的整條[[prototype]]鏈是否有指向右邊函數的prototype,例如:a instanceof Foo
  • isPrototypeOf(..) 方法: 驗證在對象 a 的整條 [[Prototype]] 鏈中是否出現過 原型對象b.例如:b.isPrototypeOf( a );

注意: 若是使用內置的 .bind(..) 函數來生成一個硬綁定函數(參見第 2 章)的話, 該函數是沒有 .prototype 屬性的。若是硬綁定函數instanceof 的話,則其bind的 目標函數的prototype會成爲硬綁定函數的prototype.

關於__proto__: 咱們知道函數能夠直接經過prototype屬性直接訪問原型對象.那對象怎麼訪問呢?咱們知道是經過[[prototype]]鏈.怎麼訪問呢? 在ES5之中的標準方法:經過Object.getPrototypeOf( )方法來獲取對象原型.Object.getPrototypeOf( a ) === Foo.prototype; // true, 另外一種方法:在 ES6 以前並非標準,但卻被絕大多數瀏覽器支持的一種方法,能夠訪問內部[[prototype]]對象.那就是__proto__.例如:a.__proto__ === Foo.prototype; // true.你甚至能夠經過.__proto__.__ptoto__...來訪問整個原型鏈. .__proto__實際上並不存在於你正在使用的對象中.而且它看起來很像一個屬性,可是實際上它更像一個 getter/setter(見第三章).

5.4 對象關聯

[[Prototype]] 機制就是存在於對象中的一個內部連接,它會引用其餘對象。

這個連接的做用是:若是在對象上沒有找到須要的屬性或者方法引用,引擎就會繼續在 [[Prototype]] 關聯的對象上進行查找。同理,若是在後者中也沒有找到須要的 引用就會繼續查找它的 [[Prototype]],以此類推。這一系列對象的連接被稱爲原型鏈

5.4.1 建立關聯

問:"咱們已經明白了爲何 JavaScript 的 [[Prototype]] 機制和類不同,也明白了它如何創建對象間的關聯。"
答: 類的機制是複製,JavaScript裏原型鏈的機制是引用.

問:"那 [[Prototype]] 機制的意義是什麼呢?爲何 JavaScript 開發者費這麼大的力氣(模擬類)在代碼中建立這些關聯呢?"
答: 意義就是模擬類,JavaScript不須要複製(我以爲這不是個優勢)而經過原型鏈實現"實例"對"類"的"繼承(其實就是引用)".這樣就達到了實例對象對某些屬性(即原型對象裏的屬性)的複用.

Object.create(..) 這個方法其實咱們在前面已經使用過不少次."Object.create(..) 會建立一個新對象(bar)並把它關聯到咱們指定的對象(foo),這樣咱們就能夠充分發揮 [[Prototype]] 機制的威力(委託)而且避免沒必要要的麻煩(好比使用 new 的構造函數調用會生成 .prototype 和 .constructor 引用)。"實際上這個方法就是建立返回一個新對象,這個新對象的原型([[Prototype]])會綁定爲咱們輸入的參數對象foo.而且因爲不是經過構造函數的形式,因此不須要爲函數單獨設置prototype.雖然Object.create(..)很好,但實際咱們使用的更多的仍是構造函數形式.
注意: Object.create(..) 的第二個參數指定了須要添加到新對象中的屬性名以及這些屬性的屬性描述符(參見第 3 章)。

Object.create(null)
這個方法其實咱們在前面也講解過幾回."Object.create(null) 會建立一個擁有空(或者說null)[[Prototype]] 連接的對象,這個對象沒法進行委託。因爲這個對象沒有原型鏈,因此 instanceof 操做符(以前解釋過)沒法進行判斷,所以老是會返回 false。 這些特殊的空 [[Prototype]] 對象一般被稱做「字典」,它們徹底不會受到原型鏈的干擾,所以很是適合用來存儲數據。"

"Object.create()的polyfill代碼."這部分我就不作解讀了,由於如今都8102年,es6早就普及了,你幾乎不可能再用到es5以前的語法了.因此這部分你們瞭解下便可.

5.4.2 關聯關係是備用

[[Prototype]] 的本質做用: 書中提到了一個觀點"處理「缺失」屬性或者方法時的一種備用選項。"(即備用設計模式).但隨後進行了否認"可是這在 JavaScript 中並非很常見。因此若是你使用的是這種模式,那或許應當退後一步並從新思考一下這種模式是否合適。" 做者給出的觀點是:"進行委託設計模式,即例子中的內部委託(就是在對象裏套了個殼再引用了一遍,爲的是將委託進行隱藏).這樣能夠使咱們的API設計得更加清晰."文中的清晰是指,當咱們須要引用原型對象的屬性方法時,咱們在對象內部設置對應專門的屬性(例子中的doCool),進行內部委託(其實就是套個殼進行隱藏).這樣咱們對象的屬性就是"完整"的.

在實際工做中,咱們經常就是把原型對象做爲存放對象的公共屬性方法的地方.對於通常比較重要的操做纔會在對象裏進行內部委託(隱藏委託)!

5.5 小結

總結得很好很全面,這裏我仍是直接摘抄了,不是偷懶哦!

  • 若是要訪問對象中並不存在的一個屬性,[[Get]] 操做(參見第 3 章)就會查找對象內部[[Prototype]] 關聯的對象。 這個關聯關係實際上定義了一條「原型鏈」(有點像嵌套的做用域鏈),在查找屬性時會對它進行遍歷。
  • 全部普通對象都有內置的 Object.prototype, 指向原型鏈的頂端(好比說全局做用域),如 果在原型鏈中找不到指定的屬性就會中止。toString()、valueOf() 和其餘一些通用的功能 都存在於 Object.prototype 對象上,所以語言中全部的對象均可以使用它們。
  • 關聯兩個對象最經常使用的方法是使用 new 關鍵詞進行函數調用,在調用的 4 個步驟(第 2 章)中會建立一個關聯其餘對象的新對象。
  • 使用 new 調用函數時會把新對象的 .prototype 屬性關聯到「其餘對象」(就是構造函數prototype所指的對象)。帶 new 的函數調用 一般被稱爲「構造函數調用」,儘管它們實際上和傳統面向類語言中的類構造函數不同。
  • JavaScript 中的機制有一個核心區別,那就是不會進行復制,對象之間是經過內部的 [[Prototype]] 鏈關聯的。
  • 「委託」是一個更合適的術語,由於對象之間的關係不是複製而是委託。(意思就是原先繼承應該改成原先委託?)

第六章 行爲委託

第 5 章的結論:[[Prototype]] 機制就是指對象中的一個內部連接引用另外一個對象。換句話說,JavaScript 中這個機制的本質就是對象之間的關聯關係。在第六章又被稱爲委託. PS:前面在講原型的時候我就習慣用父對象指代原型對象(相似"父類"),用子對象指代其實例對象(相似"子類").本章也將採用這種稱呼,故下面再也不說明.(其實我以爲用父對象和子對象稱呼更形象)

6.1 面向委託的設計

一句話:[[Prototype]]機制是面向委託的設計,是不一樣於面向類的設計. 下面將分別介紹類理論和委託理論.

6.1.1 類理論

類理論設計方法: 首先定義一個通用父(基)類,在 父類類中定義全部任務都有(通用)的行爲。接着定義子類 ,它們都繼承自 父類而且會添加一些特殊的行爲來處理對應的任務,而且在繼承時子類能夠使用方法重寫(和多態)父類的行爲.

類理論中許多行爲能夠先「抽象」到父類而後再用子類進行特殊化(重寫)。 ps:這部分了解便可,着重理解下面JavaScript用到的委託.

6.1.2 委託理論

類理論設計方法: 首先你會定義一個"父"對象(至關於上節中的父類),它會包含全部任務均可以使用(委託)的具體行爲。接着,對於每一個任務你均可以定義一個對象("子"對象)來存儲對應的數據和行爲。你能夠把特定的任務對象都關聯到父對象上,讓它們在須要的時候能夠進行委託。 (其實咱們通常都是用父對象來定義通用的方法,子對象進行委託.而後子對象自身個性的屬性方法就寫在子對象自己,並避免與父對象的屬性名衝突)

ps: 這節書中這段話可是咱們並不須要把這些行爲放在一塊兒,**經過類的複製**,咱們能夠把它們分別放在各自獨立 的對象中,須要時能夠容許 XYZ 對象委託給 Task。有個錯誤."經過類的複製"應該改成"經過"[[Prototype]]機制".這裏應該是做者的手誤. 在 JavaScript 中,[[Prototype]] 機制會把對象關聯到其餘對象。不管你多麼努力地說服自 己,JavaScript 中就是沒有相似「類」的抽象機制。(其實主要緣由仍是是JavaScript沒有完整的複製機制)

委託理論的使用建議:
PS:書中這裏寫了3條,其實只有2條,第三條不過是對第一條的說明,這裏我進行了合併.

    1. 一般來講,在 [[Prototype]] 委託中最好把狀態保存在委託者(子對象)而不是委託目標(父對象)上。那怎麼實現呢,就是經過"this 的隱式綁定".在委託目標(父對象)上的函數裏經過this定義保存狀態.當委託者(子對象)引用該函數方法時,此時的this就自動綁定到委託者上了.
    1. 在委託中咱們會盡可能避免在 [[Prototype]] 鏈的不一樣級別中使用相同的命名,不然就須要使用笨拙而且脆弱的語法來消除引用歧義(參見第 4 章)。
  • 補充: 3. 在 API 接口的設計中,委託最好在內部實現,不要直接暴露出去。 這麼作更多的是出於安全和接口穩定的考慮.建議子對象將全部引用父對象的方法都套個函數隱藏起來,並取一個語義化的屬性名.

委託理論的使用注意:

    1. 禁止兩個對象互相委託:當你將第二個對象反向委託給前一個對象時,就會報錯.
    1. 調試: 這個瞭解下就行.知道不一樣瀏覽器和工具對委託的解析結果並不必定相同.(吐槽:看了半天到後面出現實際上,在編寫本書時,這個行爲被認定是 Chrome 的一個 bug, 當你讀到此書時,它可能已經被修復了。我只想說WTF! 好吧,我知道chrome之前可能出現過這個"bug"了=.=)

6.1.3 比較思惟模型

這節主要是比較了"經過構造函數(模擬類)實現原型繼承"與"經過對象關聯(委託形式,Object.create( ... ))實現原型繼承"兩種方式的區別.

結論: 經過對象關聯,委託形式,更加簡潔,更加清晰易懂.

PS:這裏我本來本身對例子畫出原型示意圖.可是發現是真的複雜,而且和書中簡潔後的示意圖是差很少的,因此這裏就不展現了,省得讓讀者看得更頭大.這裏建議,讀者本身在草稿紙上畫出原型示意圖.

6.2 類與對象

其實這節講得仍是"經過構造函數(模擬類)實現原型繼承"與"經過對象關聯(委託形式,Object.create( ... ))實現原型繼承"兩種方式的區別.不過此次主要是之前端實際使用場景進行講解.

6.2.1 控件「類」

這裏我就不以書中的例子進行講解了,而是直接站在更高的角度對這種"類"風格的代碼進行講解.
最大特色: 1是經過構造函數進行模擬類,2是經過顯式僞多態(硬綁定函數)關聯兩個函數.
注意:

  • 無論是類仍是對象.這兩種形式通常都須要定義兩種數據.第一種就是實例對象要用到的"初始保存的數據";第二種就是通用行爲的定義,包括對實例對象數據的增刪改查.
  • 下面提到的顯式僞多態(詳見第四章),其實指的就是使用call()方法這種硬綁定.
  • 注意ES6 class模擬類的寫法我就沒具體列出了.實際上class 仍然是經過 [[Prototype]] 機制實現的,不過是個表面簡潔的語法糖.

雖然書中對顯式僞多態稱爲"醜陋的",還用了一個語氣動詞"呸!".雖然這樣很差,但有時用call真的很方便,因此用得也不少.

6.2.2 委託控件對象

最大特色: 經過對象載體來模擬父子,並經過Object,create(...)來對兩個對象進行關聯.並經過委託的形式進行引用.與上節中提到的類形式還有一個區別:對象foo構建後,須要手動調用setUp方法進行初始化.故對象的構建與初始化是分開的.而構造函數形式則是在new 構造函數時, 同時進行了對象構建與初始化.(關於這點我下面還會再說明的)

關於書中這句使用類構造函數的話,你須要(並非硬性要求,可是強烈建議)在同一個步驟中實現構造和初始化。然而,在許多狀況下把這兩步分開(就像對象關聯代碼同樣)更靈活。的理解:使用類構造函數形式,當咱們使用new 構造函數時,實際上是在一步實現對象的構建和對象數據的初始化(經過構造函數裏的call) ;使用這種委託形式,咱們是分別經過Object.create( ... );構建對象和foo.setUp( ...);來初始化的.即咱們是分兩步實現的.這樣分開的話實際上是更加靈活,也更符合編程中的關注分離原則.

6.3 更簡潔的設計

這節也是同樣經過二者的對比來突顯委託設計模式的各類優勢.這裏我就再也不對書中的例子進行解讀.若是你真正理解了類和委託的話,實際上是很簡單的.若是以爲複雜的話,能夠在紙上理一下函數和對象之間的關係,下面我就只總結下這裏提到委託設計模式的優勢,固然核心是更簡潔.

簡潔體如今:

  • 1, 委託重點在於只須要兩個實體(兩個對象相互關聯),而以前的"類"模式須要三個(父"類",子"類",實例對象)其實能夠這麼理解:委託模式將"子類"和"實例對象"合爲一個對象了。
  • 2, 不須要基類(父類)來"共享"兩個實體之間的行爲.不須要實例化類,也不須要合成.其實這第二條就是對第一條這種結果的說明.
  • 額外補充強調:在使用構造函數模擬類時,子類一般會對父類的行爲進行重寫(屬性名相同);但委託模式則不會,它會從新取個屬性名,再引用父對象上的行爲.

6.4 更好的語法

這節主要是介紹ES6提供的2個簡潔寫法與其中的隱患.

語法:

  • 在 ES6 中咱們能夠在任意對象的字面形式中使用簡潔方法聲明,例如:
var Foo = {
 bar() { /*..*/ },//字面形式聲明
};

複製代碼
  • 在 ES6 中咱們能夠用 Object. setPrototypeOf(..) 來修改對象的 [[Prototype]],具體用法能夠查看MDN例如:
// 使用更好的對象字面形式語法和簡潔方法 
var AuthController = {
         errors: [],
         checkAuth() {
           // ... 
         },
         server(url,data) {
             // ...
         }
         // ... 
};
// 如今把 AuthController 關聯到 LoginController 
Object.setPrototypeOf( AuthController, LoginController );
複製代碼

弊端:

  • 對象字面形式語法:實際上就是一個匿名函數表達式.匿名函數會致使3個缺點:1. 調試棧更難追蹤;2. 自我引用(遞歸、事件(解除)綁定,等等)更難; 3. 代碼(稍微)更難理解。(其實我以爲這3個缺點還好,影響不是那麼大).可是這種簡潔語法很特殊,會給對應的函數對象設置一個內部的 name 屬性,這樣理論上能夠用在追 蹤棧中。因此實際上惟一的弊端就只剩第2條了.終極建議就是:若是你須要自我引用的話,那最好使用傳統的具名函數表達式來定義對應的函數,不要使用簡潔方法。
  • Object. setPrototypeOf(..) 這個是書中沒有提的,我以爲有必要進行補充下.首先,Object. setPrototypeOf(..)可能會帶來性能問題,若是關心性能,則應該使用Object.create()替代.Object. setPrototypeOf(..)與Object.create()的主要區別: Object. setPrototypeOf(..)會直接修改現有對象的[[prototype]],Object.create()則是返回一個新對象.因此你須要手動設置一下丟失的的constructor屬性(若是你須要的話).而使用setPrototypeOf(..)則不須要.

6.5 內省

吐槽: 縱觀整本書,做者關於JavaScript中模擬類和繼承"的批評,說它們具備很大誤導性!更是嗤之以鼻!就差爆粗口了,JavaScript就像一個異教徒,應該綁在十字架上被燒死!可是他這樣的觀點,都是站在其餘語言的角度來看待時,產生的.我想更多的讀者多是隻接觸過JavaScript.那麼他實際上是沒有這些疑惑的!!!你反而給他們講這一大堆其餘語言的"正確"含義,有時候會時得其反!讓讀者更加困惑,若是是理解不透徹的,反而會懷疑本身原本寫的是對的代碼!因此讀者應該作一個能夠理解做者意圖,而且擁有自我看法和觀點立場!

什麼是內省(自省)? 首先,本節須要弄懂一個問題,什麼是內省,或者是自省。書中的解釋是自省就是檢查實例的類型。類實例的自省主要目的是經過建立方式來判斷對象的結構和功能。我這裏再更通俗地解釋下:當咱們構建獲得一個實例對象時,有時候咱們是不太清除它的屬性和方法的.尤爲是第三方庫.有時候貿然使用會致使不少錯誤(例如調用的方法不存在,或者報錯等).這個時候咱們就須要經過自省.其實就是經過一系列操做,來確認實例是否是咱們想要的那個,實例的方法是否是咱們想要的(存在且可用).

內省的方法:

  • 1.經過 instanceof 語法:
function Foo() { 
  // ...
}
Foo.prototype.something = function(){
  // ... 
}
var a1 = new Foo();
// 假設咱們不知道上面的過程,只知道獲得實例對象a1
//咱們想知道a1是否是我所但願的函數Foo所構建的
if (a1 instanceof Foo) { 
  a1.something();
}
複製代碼

例子中咱們有一個實例對象a1,可是咱們不知道a1是否是咱們所但願的函數Foo所構造的,此時就能夠經過instanceof進行判斷. instanceof比較適合判斷實例對象和構造函數之間的關係.
缺陷: 可是若是咱們想判斷函數A是否是函數B的"子類"時,則會稍微麻煩點,咱們須要像這樣A.prototype instanceof B進行判斷.而且也不能直接判斷兩個對象是否關聯.

  • 2.經過 "鴨子類型": 爲何會叫這個名字?看了做者的解釋,仍是不太能接受.不太理解外國人的腦回路.你在國內和別人說"鴨子類型",估計也是一臉懵逼.其實很簡單,所謂的"鴨子類型"其實也是咱們實際工做中經常使用的:
//若是a1的something存在的話,則咱們能夠進行調用
if ( a1.something) { 
  a1.something();
}
複製代碼

其實這種方法是很是經常使用的,排除了在不知道存在性狀況下,貿然調用的風險.
缺陷: 關於書中提到的缺點,四個字歸納就是"以偏概全" .書中關於Promise的例子,就是以偏概全的例子.因此咱們在使用時,在if判斷a1.something存在時,纔會在後面使用something方法.不要直接使用anotherthing,這種沒確認過的方法.

  • 3.若是使用對象關聯時: 則能夠比較簡單的使用Object.getPrototypeOf(..)進行判斷.例如Object.getPrototypeOf(a)===A其中a,A都是對象.若是爲true,則說明a的原型鏈上含有對象A.

6.6 小結

  • 除了類和繼承設計模式,行爲委託實際上是更強大,更值得推廣的模式(本觀點僅表明做者的觀點!)
  • 行爲委託認爲對象之間是兄弟關係,互相委託,而不是父類和子類的關係。JavaScript 的 [[Prototype]] 機制本質上就是行爲委託機制。(我以爲仍是父子對象關係.個人解說裏也都是父子相稱)
  • 當你只用對象來設計代碼時,不只可讓語法更加簡潔,並且可讓代碼結構更加清晰。
  • 對象關聯(對象以前互相關聯)是一種編碼風格,它倡導的是直接建立和關聯對象,不把它們抽象成類。對象關聯能夠用基於 [[Prototype]] 的行爲委託很是天然地實現

後續更新

後面關於<你不知道的JavaScript>中和下.還在寫做當中,手頭上還有一篇webpack徹底指北的文章,目前寫了一半2w字,也都是面向新手,真正的全面地由淺入深.最近,空降一個新項目組,開發到測試只有一個月,還要帶新人,更新會很慢.不過我會爭取年前所有放出.若是你們喜歡的話,能夠關注我一下,文章首發仍是在掘金的.

最後求一個內推,目前筆者在一家軟件國企(半養老型).年末或者明年初就會考慮離職.但願進入一家比較好的互聯網企業.若是有小夥伴有好的機會能夠發我郵箱:bupabuku@foxmail.com.謝謝!
目前暫時優先考慮武漢(房子,盆友,東西都在這邊,去外地太不方便了-.-)

百度網盤下載

爲了方便你們下載到本地查看,目前已經將MD文件上傳到百度網盤上了. 連接: pan.baidu.com/s/1ylKgPCw3… 提取碼: jags (相信進去後,大家還會回來點讚的! =.=)

相關文章
相關標籤/搜索