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
傳統編譯的三個步驟web
說明: 此處只需記住第一步:分詞/詞法分析.第二步:解析/語法分析,獲得抽象語法樹(AST).第三步:代碼生成,將抽象語法樹轉換爲機器指令.面試
JavaScript與傳統編譯的不一樣點:
首先介紹將要參與到對程序 var a = 2; 進行處理的過程當中的演員們,這樣才能理解接下來將要聽到的對話。
由上圖可知,引擎在得到編譯器給的代碼後,還會對做用域進行詢問變量.
如今將例子改成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查詢.
這部分比較簡單就是經過擬人方式比喻引擎和做用域的合做過程.一句話歸納就是,引擎進行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(..)。
當一個塊或函數嵌套在另外一個塊或函數中時,就發生了做用域的嵌套。進而造成了一條做用域鏈.所以,在當前做用 域中沒法找到某個變量時,引擎就會在外層嵌套的做用域中繼續查找,直到找到該變量, 或抵達最外層的做用域(也就是全局做用域)爲止。
當引擎須要對做用域進行查詢時.引擎會從當前的執行做用域開始查找變量,若是找不到, 就向上一級繼續查找。當抵達最外層的全局做用域時,不管找到仍是沒找到,查找過程都 會中止。
例子:
function foo(a) {
console.log( a + b );
b = a;
}
foo( 2 );
複製代碼
console.log(a+b)
因爲RHS此時是找不到b的值.故會拋出ReferenceError.b=a;
.=
操做符或調用函數時傳入參數的操做都會致使關聯做用域的賦值操做。此時都會進行LHS查詢做用域分爲兩種工做模式:
詞法階段: 大部分標準語言編譯器的第一個工做階段叫做詞法化(也叫單詞化)。詞法化的過程會對源代碼中的字符進行檢查,若是是有狀態的解析過程,還會賦予單詞語義。
詞法做用域: 詞法做用域就是定義在詞法階段的做用域也被稱爲靜態做用域。即在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個逐級包含的"氣泡做用域"。
注意: 沒有任何函數的氣泡能夠(部分地)同時出如今兩個外部做用域的氣泡中,就如同沒有任何函數能夠部分地同時出如今兩個父級函數中同樣。
引擎對做用域的查找:
這一部分在上一節中已經說過,就是從當前做用域逐級向上,直到最上層的全局做用域.這裏再進一步進行講解.做用域查找會在找到第一個匹配的標識符時中止。在多層的嵌套做用域中能夠定義同名的標識符,這叫做「遮蔽效應」(內部的標識符「遮蔽」了外部的標識符)。拋開遮蔽效應, 做用域查找始終從運行時所處的最內部做用域開始,逐級向外或者說向上進行,直到碰見第一個匹配的標識符爲止。
注意:
window.a
經過這種技術能夠訪問那些被同名變量所遮蔽的全局變量。但非全局的變量若是被遮蔽了,不管如何都沒法被訪問到。欺騙詞法: 引擎在運行時來「修改」(也能夠說欺騙)詞法做用域.或者說就是在引擎運行時動態地修改詞法做用域(原本在編譯詞法化就已經肯定的).
欺騙詞法的兩種機制:(下面這兩種機制理解了解便可,不推薦實際開發使用)
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」。
注意:
var sum = new Function("a", "b", "return a + b;");
console.log(sum(1, 1111)); //1112
複製代碼
例子:
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(..) 也被禁止了。
JavaScript 引擎會在編譯階段進行數項的性能優化。其中有些優化依賴於可以根據代碼的詞法進行靜態分析,並預先肯定全部變量和函數的定義位置,才能在執行過程當中快速找到標識符。可是eval(..) 和 with會在運行時修改或建立新的做用域,以此來欺騙其餘在書寫時定義的詞法做用域。這麼作就會致使引擎沒法知道eval和with它們對詞法做用域進行什麼樣的改動.只能對部分不進行處理和優化!所以若是代碼中大量使用 eval(..) 或 with,那麼運行起來必定會變得很是慢!。
由於
因此用函數聲明對代碼進行包裝,實際上就是把這些代碼「隱藏」起來了。
爲何要將代碼進行"隱藏"? 由於最小受權或最小暴露原則。這個原則是指在軟件設計中,應該最小限度地暴露必 要內容,而將其餘內容都「隱藏」起來,好比某個模塊或對象的 API 設計。 隱藏的好處:
函數聲明與函數表達式:
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(){console.log(1)}()
的結果是1,並不會報錯)//Function()構造器
var f =new Function()
// 函數表達式
var f = function() {
console.log(1);
}
// 函數聲明
function f (){
console.log(2);
}
console.log(f())
//思考一下,這裏會打印出什麼
複製代碼
怎麼區分函數聲明和函數表達式: 看 function 關鍵字出如今聲明中的位置(不只僅是一行代碼,而是整個聲明中的位置)。若是 function 是聲明中的第一個詞,那麼就是一個函數聲明,不然就是一個函數表達式。例如上例中,是從(
開始而不是function.
補充: 上面這段是原書的解釋,我以爲這個解釋並不徹底,這裏給出我本身的解釋.
var f = function fun(){console.log(fun)}
其餘地方是沒有的.這也避免了全局污染,也方便遞歸.函數表達式能夠是匿名的,而函數聲明則不能夠省略函數名.有函數名的就是具名函數,沒有函數名的就是匿名函數.
匿名函數的缺點:
因此給函數表達式指定一個函數名能夠有效解決以上問題。始終給函數表達式命名是一個最佳實踐.
PS: 我的意見是若是函數表達式有賦值給變量或屬性名或者就是一次性調用的.實際上是不必加上函數名.由於代碼裏取名原本就很難,取很差反而會形成誤解.
好比 (function foo(){ .. })()。第一個 ( ) 將函數變成表達式,第二個 ( ) 執行了這個函數。這就是當即執行函數表達式,也被稱爲IIFE,表明當即執行函數表達式 (Immediately Invoked Function Expression);
IIFE能夠具名也能夠匿名.好處和上面提到的同樣.IIFE還能夠是這種形式(function(){ .. }())
.這兩種形式在功能上是一致的。
函數做用域是JavaScript最多見的做用域單元,有時咱們僅會將var賦值變量在if或for的{...}內使用,而不會在其餘地方使用.但它仍然會對外層的函數做用域形成污染.這個時候就會但願能有一個做用域能將其外部的函數做用域隔開,聲明的變量僅在此做用域有效.塊做用域(一般就是{...}包裹的內部)就能夠幫咱們作到這點.
從 ES3 發佈以來,JavaScript 中就有了塊做用域,而 with 和 catch 分句就是塊做用域的兩個小例子。
咱們在第 2 章討論過 with 關鍵字。它不只是一個難於理解的結構,同時也是塊做用域的一個例子(塊做用域的一種形式),用 with 從對象中建立出的做用域僅在 with 聲明中而非外部做用域中有效。
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以前咱們能夠使用這種方法來使用塊做用域.
ES6 引入了新的 let 關鍵字,提供了除 var 之外的另外一種變量聲明方式。let 關鍵字能夠將變量綁定到所在的任意做用域中(一般是 { .. } 內部)。
用 let 將變量附加在一個已經存在的塊做用域上的行爲是隱式的。例如在if的{...}內用let聲明一個變量.那什麼是顯式地建立塊做用域呢?就是單首創建{}
來做爲let的塊做用域.而不是借用if或者for提供的{}
.例如{let a=2;console.log(a)}
注意: 使用 let 進行的聲明不會在塊做用域中進行提高.
塊做用域的好處:
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);
複製代碼
for (let i=0; i<10; i++) {
console.log( i );
}
console.log( i ); // ReferenceError
複製代碼
for 循環頭部的 let 不只將 i 綁定到了 for 循環的塊中,事實上它將其從新綁定到了循環的每個迭代中,確保使用上一個循環迭代結束時的值從新進行賦值。這樣就避免了i對外部函數做用域的污染.
除了 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!
複製代碼
函數是 JavaScript 中最多見的做用域單元。本質上,聲明在一個函數內部的變量或函數會在所處的做用域中「隱藏」起來,能夠有效地與外部做用域隔開.
但函數不是惟一的做用域單元。塊做用域指的是變量和函數不只能夠屬於所處的做用域,也能夠屬於某個代碼塊(一般指 { .. } 內部)即塊做用域。ES6中就提供了let和const來幫助建立塊做用域.
考慮第一段代碼
a = 2;
var a;
console.log( a );
複製代碼
輸出結果是2,而不是undefined
考慮第二段代碼
console.log( a );
var a = 2;
複製代碼
輸出結果是undefined,而不是ReferenceError 考慮完以上代碼,你應該會考慮這個問題.究竟是聲明(蛋)在前,仍是賦值(雞)在前?
編譯器的內容,回憶一下,引擎會在解釋 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;
即不包含有賦值操做的聲明.函數聲明和變量聲明都會被提高。可是一個值得注意的細節(這個細節能夠出如今有多個「重複」聲明的代碼中)是函數會首先被提高,而後纔是變量。 考慮如下代碼:
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會忽略前面已經聲明的聲明(無論是變量聲明仍是函數聲明,只要其名稱相同,則後續不會再進行重複聲明).可是對該變量新的賦值,會覆蓋以前的值.
一句話歸納:函數聲明的優先級高於變量聲明,會排在它前面.
var a = 2
JavaScript引擎會將var a和 a = 2看成兩個單獨的聲明,第一個是編譯階段的任務,而第二個則是執行階段的任務。由於這兩小節理解透了其實發現書裏也沒講什麼,這裏就進行合併,並補充拓展我本身的理解和總結.
什麼是閉包?(廣義版)
書中解釋: 當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用域以外執行。
MDN的解釋: 閉包是函數和聲明該函數的詞法環境的組合。
個人解釋(詳細版): 必須包含兩點:
綜上簡單版就是:MDN的解釋閉包是函數和聲明該函數的詞法環境的組合。
還能夠繼續延伸成極簡版:JavaScript中的函數就會造成閉包。
Tips: 注意到上面對詞法做用域
和詞法環境
兩詞的分開使用了嗎?1,
裏此時函數還沒被執行,因此使用的是詞法做用域即靜態做用域.2,
裏,此時函數被執行,此時詞法做用域就會變成詞法環境(包含靜態做用域與動態做用域).因此其實MDN的解釋其實更準確一點,
咱們平常使用時所說的閉包(狹義版,嚴格意義上的):
爲了便於對閉包做用域的觀察和使用.咱們實際使用時會將閉包的函數做用域暴露給當前詞法做用域以外.也就是本書一直強調的閉包函數須要在它自己的詞法做用域之外執行.做者認爲符合這個條件才稱得上是真正的閉包(也就是咱們平常使用常說的'使用閉包',而且使用任何回調函數其實也是閉包).
因此狹義版就是:閉包是函數和聲明該函數的詞法環境的組合,而且將閉包的函數做用域暴露給當前詞法做用域以外.
閉包暴露函數做用域的三種方式:
下面部分是書中沒有的,是本身實際使用時的總結,而且符合這三種形式之一的就是咱們平常使用時所說的閉包(狹義版)
function foo() {
var a = 2;
function bar() {
baz(a) //經過外部函數的參數進行暴露
}
bar();
};
function baz(val) {
console.log( val ); // 2
}
foo();
複製代碼
var val;
function foo() {
var a = 2;
function bar() {
val=a //經過外部做用域的變量進行暴露
}
bar();
};
foo();
console.log(val) //2
複製代碼
function foo() {
var a = 2;
function bar() {
console.log(a)
}
return bar //經過return直接將整個函數進行暴露
};
var val=foo();
val() //2
複製代碼
關於閉包的內存泄露問題:
首先必須聲明一點:使用閉包並不必定會形成內存泄露,只有使用閉包不當纔可能會形成內存泄露.(吐槽:面試不少新人時,張口就說閉包會形成內存泄露)
爲何閉包可能會形成內存泄露呢?緣由就是上面提到的,由於它通常會暴露自身的做用域給外部使用.若是使用不當,就可能致使該內存一直被佔用,沒法被JS的垃圾回收機制回收.就形成了內存泄露.
注意: 即便閉包裏面什麼都沒有,閉包仍然會隱式地引用它所在做用域裏的所用變量. 正由於這個隱藏的特色,閉包常常會發生不易發現的內存泄漏問題.
常見哪些狀況使用閉包會形成內存泄露:
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
.
總而言之,解決辦法就是使閉包的能正常引用,能被正常回收.若是實在不行,就是在使用完後,手動將變量賦值null,強行進行垃圾回收.
看以下例子:
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,塊做用域不能和函數同樣有名稱(函數名) 不少不方便使用閉包或者比較麻煩的時候,是能夠考慮經過塊做用域進行解決.
總結一下通常何時考慮使用閉包:
這部分也是本身工做使用的總結,若是有補充或者不對的地方,歡迎留言指正.
首先看下面的例子:
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個條件:(這裏結合上面的例子,對書中的定義進行說明方便理解)
模塊:
表面上看由模塊函數(例子中的CoolModule)所返回的對象就是模塊.但模塊還必須還包含模塊函數的內部函數(即閉包函數).只有包含了才能真正稱得上是模塊.才強調一次這裏的模塊與模塊化裏的模塊是有區別的,也不是nodejs裏的模塊.
模塊函數:
模塊函數也就是模塊構造器,例子中的CoolModule().通常它有兩個常見用法.
命名將要做爲公共API返回的對象
.我以爲命名
應該是用錯了,應該是修改
即增刪改查更好)大多數模塊依賴加載器 / 管理器本質上都是將這種模塊定義封裝進一個友好的 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
複製代碼
這節的主要內容仍是瞭解如今是如何對模塊進行一個規範處理.主要是兩部份內容,一個是經過名稱和依賴合理定義模塊並儲存.另外一個則是經過名稱對存儲的模塊的調用.其實還能夠再增長一個刪除模塊的方法.
ok,這節說的模塊,就是咱們常說的模塊化開發.而且主要提到的就是ES6裏經常使用的import.沒什麼好說的.
吐槽: 同一個函數概念在5.5這一個小節裏,竟然換着花樣蹦出了三個名字!一會叫模塊構造器!一會叫模塊函數!以及最後的包裝函數!每變化一次,都得想一遍它指的是啥!真的是無力吐槽了!!!!
閉包:當函數能夠記住並訪問所在的詞法做用域,而且函數是在當前詞法做用域以外執行,這時 就產生了閉包。
模塊有兩個主要特徵:
由於this 提供了一種更優雅的方式來隱式「傳遞」一個對象(即上下文對象)引用,所以能夠將 API 設計得更加簡潔而且易於複用。
下面兩種常見的對於 this 的解釋都是錯誤的(看看就好,就不過多解讀了,以避免增長了對錯誤的印象)。
人們很容易把 this 理解成指向函數自身.
具名函數,能夠在它內部能夠使用函數名來引用自身進行遞歸,添加屬性等。(這個知識點其實在第三章提過,既然這裏又提了一遍,我也再說一遍.)例如:
function foo() {
foo.count = 4; // foo 指向它自身
}
複製代碼
匿名函數若是想要調用自身則,須要使用arguments.callee
不過這個屬性在ES5嚴格模式下已經禁止了,也不建議使用.詳情能夠查看MDN的說明.
切記: this 在任何狀況下都不指向函數的詞法做用域。你不能使用 this 來引用一個詞法做用域內部的東西。 這部分只需記住這一段話就行.
終極疑問: JavaScript裏的做用域究竟是對象嗎? 這小節最令我在乎的是裏面這句話"在 JavaScript 內部,做用域確實和對象相似,可見的標識符都是它的屬性。可是做用域「對象」沒法經過 JavaScript代碼訪問,它存在於JavaScript 引擎內部。"它讓我想起了最開始學JS的一個疑問,JavaScript裏的做用域究竟是對象嗎.雖然"在JS裏萬物皆對象".可是做用域給人的感受卻不像是一個對象.更像是一個範圍,由函數的{}
圍城的範圍,限制了其中變量的訪問.但直覺告訴我它和對象仍是應該有點聯繫的.直到讀到書中的這段話,更加印證了個人感受. 在JavaScript裏,做用域實際上是一個比較特殊的對象,做用域裏全部可見的標識符都是它的屬性.只是做用域對象並不能經過JavaScript代碼被咱們訪問,它只存在於JavaScript引擎內部.因此做用域做爲一個"對象"是常常被咱們忽略.
this 是在運行時(runtime)進行綁定的,並非在編寫時綁定,它的上下文(對象)取決於函數調用時的各類條件。this 的綁定和函數聲明的位置沒有任何關係,只取決於函數的調用方式。
當一個函數被調用時,會建立一個活動記錄(有時候也稱爲執行上下文)。這個記錄會包含函數在哪裏被調用(調用棧)、函數的調用方法、傳入的參數等信息。this 就是記錄的其中一個屬性,會在函數執行的過程當中用到。(PS:因此this並不等價於執行上下文)
經過上節咱們知道,this的綁定與函數的調用位置有關.那調用位置是什麼.調用位置就是函數在代碼中被調用的位置(而不是聲明的位置)。
要尋找調用位置,最重要的是要分析調用棧(就是爲了到達當前執行位置所調用的全部函數)。咱們關心的調用位置就在當前正在執行的函數的前一個調用中。PS:調用棧實際上是一個解釋起來有點複雜的概念.這裏我就不過多解釋,這裏推薦一篇文章,解釋得不錯.
這節書裏的例子解釋得不錯,這裏就不復制代碼了.其實分析調用棧只是爲了在運行時找到咱們關心的函數到底在哪裏和被誰調用了. 可是實際別寫代碼時,其實並不會分析得這麼清楚的,咱們仍是隻需記住this的指向就是咱們調用該函數的上下文對象.意思就是咱們在哪裏調用該函數,this就指向哪裏
.而且查看調用棧還能夠經過瀏覽器的開發者工具,只需在疑惑的代碼上一行加上debugger便可.瀏覽器在調試模式時,咱們就能夠在調用列表裏查看調用棧.咱們通常也僅在查找bug時,會使用該方法.
在找到調用位置後,則須要斷定代碼屬於下面四種綁定規則中的哪種.而後才能對this進行綁定.
注意: this綁定的是上下文對象,並非函數自身也不是函數的詞法做用域
什麼是獨立函數調用:對函數直接使用而不帶任何修飾的函數引用進行調用.簡單點一個函數直接是func()
這樣調用,前面什麼都沒有.不一樣於經過對象屬性調用例如obj.func()
,也沒有經過new關鍵字new Function()
;也沒有經過apply,bind,call強制改變this指向.
默認綁定: 當被用做獨立函數調用時(不論這個函數在哪被調用,無論全局仍是其餘函數內),this默認指向到window;
注意: 若是使用嚴格模式(strict mode),那麼全局對象將沒法使用默認綁定,所以 this 會綁定到 undefined.
隱式綁定: 函數被某個對象擁有或者包含.也就是函數被做爲對象的屬性所引用.例如obj.func()
.此時this會綁定到該對象上.
隱式丟失: 無論是經過函數別名或是將函數做爲入參形成的隱式丟失.只需找到它真正的調用位置,而且函數前沒有任何修飾也沒有顯式綁定(下節會講到)(非嚴格模式下).那麼this則會進行默認綁定,指向window.
注意: 實際工做中,大部分this使用錯誤都是由對隱式丟失的不理解形成的.記住函數調用前沒有任何修飾和顯式綁定(其實就是call、apply、bind),this就指向window
在分析隱式綁定時,咱們必須在一個對象內部包含一個指向函數的屬性,並經過這個屬性間接引用函數,從而把 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。
JavaScript 中 new 的機制實際上和麪向類的語言徹底不一樣。在 JavaScript 中,構造函數只是一些 使用 new 操做符時被調用的函數。它們並不會屬於某個類,也不會實例化一個類。實際上, 它們甚至都不能說是一種特殊的函數類型,它們只是被 new 操做符調用的普通函數而已。實際上並不存在所謂的「構造函數」,只有對於函數的「構造調用」。
使用 new 來調用函數,或者說發生構造函數調用時,會自動執行下面的操做。
示例:
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
複製代碼
使用 new 來調用 foo(..) 時,咱們會構造一個新對象並把它綁定到 foo(..) 調用中的 this 上。
說明:對於上面這句話進行解釋下,若是在一個函數前面帶上 new 關鍵字來調用, 那麼背地裏將會建立一個鏈接到該函數的 prototype 的新對象,this就指向這個新對象;
直接上結論:
new綁定=顯示綁定>隱式綁定>默認綁定
說明: new綁定與顯示綁定是不能直接進行測試比較,但經過分析發現new綁定內部實際上是使用了硬綁定(顯示綁定的一種),因此new綁定和顯示綁定優先級應該差很少.但話說回來,通常實際使用時,不會這種複雜的交錯綁定.因此只需記住下面的斷定便可.
判斷this:
如今咱們能夠根據優先級來判斷函數在某個調用位置應用的是哪條規則。能夠按照下面的順序來進行判斷:
若是你把 null 或者 undefined 做爲 this 的綁定對象傳入 call、apply 或者 bind,這些值在調用時會被忽略,實際應用的是默認綁定規則,this會綁定到window上.
使用情景:
一種很是常見的作法是使用 apply(..) 來「展開」一個數組(也能夠用來方便地參數注入),並看成參數傳入一個函數。相似地,bind(..) 能夠對參數進行柯里化(預先設置一些參數).經過自帶bind方法實現柯里化是很方便的,比本身寫要簡化好多.
注意:
更安全的this
若是函數內使用了this,直接使用null則可能會對全局形成破壞.所以咱們能夠經過建立一個「DMZ」(demilitarized zone,非軍事區)對象——它就是一個空的非委託的對象(委託在第 5 章和第 6 章介紹)。讓this綁定到這個"DMZ上.這樣就不會對全局形成破壞. 怎麼建立DMZ呢.就是經過Object.create(null) 建立一個空對象.這種方法和 {} 很像,可是並不會建立 Object.prototype 這個委託,因此它比 {}「更空」更加安全.
PS:實際使用通常不會遇到這種狀況(也多是我太菜,沒遇到),若是函數內有this,那確定是有須要調用的變量或函數,直接把它綁定到一個空對象上.那什麼都取不到,還有什麼意義?因此函數沒有this就傳入null.若是有this就把它綁定到真正須要它的對象上,而不是一個空對象上.這些是我本身的看法,若是有不妥的,歡迎留言指正.
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節就應該說了!)
硬綁定會大大下降函數的靈活性,使用硬綁定以後就沒法使用隱式綁定或者顯式綁定來修改 this。這時候則須要使用軟綁定.
Tips: 這裏給的軟綁定方法仍是挺好的.可是建議仍是在本身的代碼裏使用,並註釋清除.以避免別人使用,對this錯誤的判斷.
ES6 中介紹了一種沒法使用上面四條規則的特殊函數類型:箭頭函數。
箭頭函數不使用 this 的四種標準規則,而是根據外層(函數或者全局)做用域來決定 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的編碼規範建議:
在本身實際工做中,實際上是兩種混用的,絕大部分狀況下都會使用詞法做用域風格.由於有時候你真的很難作到徹底統一.我如今的習慣是,在寫任何函數時,開頭第一個就是var me =this;
這樣在看到函數第一眼,就知道:哦,這個函數是用詞法做用域風格的.尤爲函數內涉及到回調.這樣就避免了寫着寫着發現this綁定到其餘地方去了,一個函數裏面this不統一的狀況.
(這裏總結得很好,我就所有copy了) 若是要判斷一個運行中函數的 this 綁定,就須要找到這個函數的直接調用位置。找到以後就能夠順序應用下面這四條規則來判斷 this 的綁定對象。
必定要注意,有些調用可能在無心中使用默認綁定規則。若是想「更安全」地忽略 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 這裏再補充說明 上下文(對象)與函數做用域的區別於聯繫:
對象能夠經過兩種形式定義:聲明(文字)形式(就是常說的對象字面量)和構造形式。
var myObj = {
key: value
// ...
};
複製代碼
var myObj = new Object();
myObj.key = value;
複製代碼
構造形式和文字形式生成的對象是同樣的。惟一的區別是,在文字聲明中你能夠添加多個 鍵 / 值對,可是在構造形式中你必須逐個添加屬性。 PS:其實咱們絕大部分狀況下都是使用對象字面量形式建立對象.
在JavaScript中一共有6中主要類型(術語是"語言類型")
簡單數據類型: 其中string、boolean、number、null 和 undefined屬於簡單基本類型,並不屬於對象. null 有時會被看成一種對象類型,可是這其實只是語言自己的一個 bug,即對 null 執行typeof null 時會返回字符串 "object"。實際上,null 自己是基本類型。
PS: 緣由是這樣的,不一樣的對象在底層都表示爲二進制,在 JavaScript 中二進制前三位都爲 0 的話會被判 斷爲 object 類型,null 的二進制表示是全 0,天然前三位也是 0,因此執行 typeof 時會返回「object」。
對象:
對象除了咱們本身手動建立的,JavaScript其實內置了不少對象,也能夠說是對象的一個子類型.
內置對象:
在 JavaScript 中,這些內置對象實際上只是一些內置函數。這些內置函數能夠看成構造函數(由 new 產生的函數調用——參見第 2 章)來使用.
幾點說明:
對象屬性:由一些存儲在特定命名位置的(任意類型的)值. 屬性名:存儲在對象容器內部的屬性的名稱.屬性值並不會存在對象內.而是經過屬性名(就像指針,從技術角度來講就是引用)來指向這些值真正的存儲位置(就像房門號同樣).
屬性名的兩種形式:
.
操做符.也是咱們最經常使用的形式.它一般被稱爲"屬性訪問". .
操做符會要求屬性名知足標識符的命名規範.[".."]
語法進行訪問.這個一般被稱爲"鍵訪問".[".."]
語法能夠接受任意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一樣符合上一章所講的規則
//結果是否和你所想得同樣呢?
複製代碼
ES6 增長了可計算屬性名,能夠在文字形式中使用 [] 包裹一個表達式來看成屬性名:
var prefix = "foo";
var myObject = {
[prefix + "bar"]:"hello",
[prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
複製代碼
[]
形式訪問儲存的值,其中[]
內的值默認形式爲數值下標(爲從0開始的整數,也就是常說的索引).例如myArray[0]
myArray.baz = "baz"
.注意:添加新屬性後,雖然能夠訪問,但數組的 length 值不會改變.myArray[1]=11;myArray["2"]=22;
這種形式對數組內容進行修改,添加.鍵/值 對
形式來使用.但JS已經對數組的行爲和用途進行了優化.因此仍是建議使用默認的下標/值 對
形式來使用.var newObj = JSON.parse( JSON.stringify( someObj ) );
.但須要指出的是這種方法對於包含function函數或者Date類型的對象則無論用!從 ES5 開始,全部的屬性都具有了屬性描述符。
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。下面分別介紹剩下的三個屬性描述符鍵值:
除了上面提到的Object.defineProperty(..),ES5還能夠經過不少種方法來實現屬性或者對象的不可變.
注意: 這些全部方法都是隻能淺不變,若是目標對象引用了其餘對象(數組、對象、函數,等),其餘對象的內容不受影響,仍然是可變的.相似於淺拷貝.
說明: 在 JavaScript 程序中不多須要深不可變性。 有些特殊狀況可能須要這樣作,可是根據通用的設計模式,若是你發現須要密封或者凍結全部的對象,那你或許應當退一步,從新思考一下程序的設計,讓它能更好地應對對象值的改變。
方法:
注意: 你能夠「深度凍結」一個對象(連引用的對象也凍結),具體方法爲,首先在這個對象上調用 Object.freeze(..), 而後遍歷它引用的全部對象並在這些對象上調用 Object.freeze(..)。可是必定要謹慎!由於你引用的對象可能會在其餘地發也被引用.
說明: 在 JavaScript 程序中不多須要深不可變性。有些特殊狀況可能須要這樣作, 可是根據通用的設計模式,若是你發現須要密封或者凍結全部的對象,那你或許應當退一步,從新思考一下程序的設計,讓它能更好地應對對象值的改變。
var myObject = {
a: 2
};
myObject.a; // 2
複製代碼
myObject.a是怎麼取到值2的?
myObject.a 經過對象默認內置的[[Get]] 操做(有點像函數調用:[Get]).首先它會在對象中查找是否有名稱相同的屬性, 若是找到就會返回這個屬性的值。若是沒有找到名稱相同的屬性,按照 [[Get]] 算法的定義會執行另一種很是重要的行爲。其實就是遍歷可能存在的 [[Prototype]] 鏈,也就是在原型鏈上尋找該屬性。若是仍然都沒有找到名稱相同的屬性,那 [[Get]] 操做會返回值 undefined.
注意: 若是你引用了一個當前詞法做用域中不存在的變量,並不會像對象屬性同樣返回 undefined,而是會拋出一個 ReferenceError 異常.
既然有能夠獲取屬性值的 [[Get]] 操做,就必定有對應的 [[Put]] 來設置或者建立屬性.
[[Put]] 被觸發時的操做分爲兩個狀況:1. 對象中已經存在這個屬性 2. 對象中不存在這個屬性.
若是對象中已經存在這個屬性,[[Put]] 算法大體會檢查下面這些內容:
若是對象中不存在這個屬性,[[Put]] 操做會更加複雜。會在第 5 章討論 [[Prototype]] 時詳細進行介紹。
對象默認的 [[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操做符刪除
複製代碼
屬性存在性: 如何判斷一個對象是否存在某個屬性(準確來講是檢查這個屬性名是否存在),這時就須要用到:
in
操做符 in 操做符會檢查屬性是否在對象及其 [[Prototype]] 原型鏈中(參見第 5 章)。注意:
屬性可枚舉性: 若是一個屬性存在,且它的enumerable 屬性描述符爲true時.則它是可枚舉的.而且能夠被for..in 循環. 一個屬性不只僅須要存在,還須要它的enumerable 爲true纔是可枚舉的,才能被for...in遍歷到.
注意: for...in不適合對數組進行遍歷,對數組的遍歷仍是使用傳統的for循環.
對屬性的可枚舉性判斷,則須要用到如下幾種方法:
關於這節我以爲仍是以理清for..in和for..of爲主.後面延伸的@@iterator及Symbol.iterator的使用,不必過於深究.注意書中123頁第二行done 是一個布爾值,表示是否還有能夠遍歷的值。
有個錯誤,應該改爲done 是一個布爾值,表示遍歷是否結束。
不然你在看後面它的說明時會感受到自相矛盾.這裏我也是以for..in和for..of爲主進行說明,也更貼近咱們實際使用.
for..in
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]);
}
複製代碼
書中小結總結得挺全的,這裏我就搬運下
注意: 正如書中提示的那樣,整章一半以上幾乎都是講面向對象和類的概念.會讀得人云裏霧裏,給人哦,也許大概就是這樣子
的感受.後面我仍是會對那些抽象的概念找到在JavaScript裏對應的"立足點",不至於對這些概念太"飄".
說明:
注意: Javascript語言不支持「類」,所謂的"類"也是模擬出的「類」。即便是ES6引入的"類"實質上也是 JavaScript 現有的基於原型的繼承的語法糖。
一句話:類其實也是一種設計模式!
JavaScript 只有一些近似類的語法元素 (好比 new 和 instanceof),不過在後來的 ES6 中新增了一些元素,好比 class 關鍵字,其實質上也是 JavaScript 現有的基於原型的繼承的語法糖。也不是真正的類.
這部分書中的描述,我理解起來也比較費勁,主要是它提到的棧,堆與我理解中內存裏的棧,堆相沖突了.這裏簡單說下個人理解,若有誤,感激指正.
stack類實際上是一種數據結構.它能夠儲存數據,並提供一些公用的方法(這和上面提到的類很類似).可是stack類其實只是一個抽象的表示,你想對它進行操做,就須要先對它進行實例化.
這節主要就是說明"類"和"實例"的關係. 在JavaScript裏"類"主要是構造函數,"實例"就是對象.
一個類就像一張藍圖。爲了得到真正能夠交互的對象,咱們必須按照類來實例化一個東西,這個東西(對象)一般被稱爲實例,有須要的話,咱們能夠直接在實例上調用方法並訪問其全部公有數據屬性。
總而言之:類經過實例化獲得實例對象.
在面向類的語言中,你能夠先定義一個類,而後定義一個繼承前者的類。後者一般被稱爲「子類」,前者一般被稱爲「父類」。子類能夠繼承父類的行爲,而且能夠根據本身的需求,修改繼承的行爲(通常並不會修改父類的行爲).注意:咱們討論的父類和子類並非實例,在JavaScript裏類通常都是構造函數。
大概你看了它的"解釋",對多態仍是懵懵懂懂.這裏我再解釋下:
什麼是多態?
同一個操做,做用於不一樣的對象,會產生不一樣的結果。發出一個相同的指令後,不一樣的對象會對這個指令有不一樣的反應,故稱爲多態。 說明: 書中例子中的inherited其實就是至關於super.而且注意書中的這些例子都是僞代碼! 並非真的在JavaScript裏就是這樣實現的.補充:這裏是關於super的mdn連接.
當調用方法時會自動選擇合適的定義
,這句話怎麼理解,當子類實例化後,執行drive()方法時,它並不會直接去執行父類的drive().而是子類上的drive().簡單來講就是實例來源於那個類,它就使用那個類的方法.說明:
多重繼承: 一個子類能夠繼承來自多個父類的方法.
多重繼承引起的問題: 多重繼承可能會出現,多個父類中方法名衝突的問題,這樣子類
到底引用哪一個方法?
多重繼承與JavaScript: JavaScript自己並無提供多重繼承功能.但它能夠經過其餘方法來達到多重繼承的效果.
JavaScript 中只有對象,並不存在能夠被實例化的「類」。一個對象並不會被複制到其餘對象,它們會被關聯起來(參見第 5 章)(其實就是引用,因此它的多態是"相對"的)。 因爲在其餘語言中類表現出來的都是複製行爲,所以 JavaScript 開發者也想出了一個方法來模擬類的複製行爲,這個方法就是混入(就是經過混入來模擬實現類的多重繼承)。
鄭重提醒: 書中這裏的類都是對象形式的.例子裏的sourceObj, targetObj,這就可能形成一個"誤導",在JavaScript裏是沒有真正的類,所謂的類也不過是咱們模擬出來的"類",不過是一種語法糖(包括ES6裏的class).在JavaScript裏"所謂的類"常常是一個構造函數,你並不能這樣進行遍歷,只能對它的實例對象進行這種操做.不要被書中例子帶進去了,不要混淆,畢竟咱們最終使用的是JavaScript(而不是其餘面向對象的語言.),它裏面的類經常並非一個對象!
顯式混入: 書中沒有給出明確的顯式混入的定義,可是讀完整章.基本就知道什麼是顯式混入了.顯式混入就是經過相似mixin()方法,顯式地將父對象屬性逐一複製,或者有選擇地複製(即例子中的存在性檢查)到子對象上.
顯式混入經常使用方法: 就是書中的例子, 首先有子對象,並對其進行特殊化(定義本身的屬性或方法).而後再經過mixin()方法將父對象有選擇地複製(即存在性檢查,過濾子對象已有的屬性,避免衝突)到子對象上.
顯式混入注意點: 顯式混入時,切記一點你要避免父對象的屬性與子對象特殊化的屬性衝突.這就是爲何例子中要進行存在性檢查,以及後面要說的混合複製,可能存在的重寫風險.
1. 再說多態(其實說的就是js裏的多態)
顯式多態: 將父對象裏的方法經過顯式綁定到子對象上.就是顯式多態.例如書中的例子:Vehicle.drive.call( this )。顯式多態也是爲了JS來模擬實現多重繼承的!
說明: 在ES6以前是沒有相對多態的機制。因此就使用call這種進行顯式綁定實現顯式動態.注意JavaScript裏實現多態的方法也被稱爲"僞多態".因此不要對後面忽然冒出的僞多態概念而一臉懵逼(其實整本書常常作這樣的事)
顯式多態(僞多態)的缺陷: 由於在JavaScript 中存在屏蔽(實際是函數引用的上下文不一樣),因此在引用的時候就須要使用顯式僞多態的方法建立一個函數關聯. 這些都會增長代碼的複雜度和維護難度(過多的this綁定,真的會讓代碼很難讀)。
2. 混合複製(顯式混入另外一種不經常使用方法)
前面的顯式混入的方法是先有子對象並進行特殊化,而後再有選擇地複製父對象屬性.這個不經常使用的方法則是反過來的,結合書中例子,它先用一個空對象徹底複製父對象的屬性,而後該對象複製特殊化對象的屬性,最後獲得子對象.這種方法明顯是比第一種麻煩的,而且在複製特殊化對象時,可能會對以前重名的屬性(即複製獲得的父對象屬性)進行重寫覆蓋.因此這種方法是存在風險,且效率低下的.
顯式混入的缺陷:
3. 寄生繼承
顯式混入模式的一種變體被稱爲「寄生繼承」,它既是顯式的又是隱式的. 首先會複製一份父類(對象)的定義,而後混入子類(對象)的定義(若是須要的話保留到父類的特殊引用),而後用這個複合對象構建實例。
說明: 寄生繼承與混合複製是很類似的,最大的區別是寄生繼承是經過實例化構造函數(JS中的"類")來實現複製的.
隱式混入: 它與顯示混入最大的區別,就是它沒有明顯的對父類(對象)屬性進行復制的過程.它是經過在構造函數調用或者方法調用中使用顯式綁定例如: Something.cool.call( this)來實現混入(多重繼承).其本質就是經過改變this指向來實現混入.
整章的重點其實就是讓你理解什麼叫類.除了最後一小節的混入和JavaScript有那麼一點點關係.其他的小結和JavaScript都沒什麼關係.重要的是理解類這種思想和設計模式.
重點:
注意:本章的前提是你已經比較熟悉原型及原型鏈.不太熟或者不知道的能夠,經過這篇文章熟悉下.
[[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.
補充:
全部普通的 [[Prototype]] 鏈最終都會指向內置的 Object.prototype。(Object.prototype的[[Prototype]] 最終會指向null.null就是最後的終點). 這個 Object.prototype 對象,包含了 JavaScript 中許多通用的功能,例如:toString() , valueOf(), hasOwnProperty(..)和 isPrototypeOf(..)。
說明: 看完本節時,切記不要對myObject.foo = "bar"
這種簡單的對象屬性賦值產生顧慮和疑惑.這種賦值絕對不會對原型鏈產生任何影響!基本也不會出現賦值不成功的狀況.若是有人敢在團隊項目裏修改對象的屬性描述符,早就被拖出去打死了!!! 這部分能夠看作補充知識,知道有這些奇葩設定就行.其實這節更應該關注的是myObject.foo
的返回值.
注意: 書中提到的動詞屏蔽
其實指的就是在對象上建立同名屬性(原型鏈上已有該屬性).注意不要被繞暈了.還有++就至關於myObject.a=myObject.a+1,注意分解就行,不存在什麼特別須要小心的.
吐槽:模仿類居然被說成奇怪的無恥濫用!?不這樣作,js那些高級用法怎麼實現?怎麼會有如今前端的百花齊放(輪子滿地跑)?這也是冒得辦法的辦法啊!畢竟當時js只是小衆,不期望它有多大能耐.畢竟只是一我的用7天"借鑑"出來的東西.
"類"函數: JavaScript用來模仿類的函數就被稱爲類函數,其實就是咱們常說的構造函數.
"類"函數模擬類的關鍵: 全部的函數默認都會擁有一個名爲 prototype 的公有而且不可枚舉(參見第 3 章)的屬性,它會指向另外一個對象.當咱們經過new 函數(構造函數)來獲得實例對象時,此時new會給實例對象一個內部的 [[Prototype]]屬性,實例對象內部的[[Prototype]]屬性與構造函數的prototype屬性都指向同一個對象.那JS的這個特性怎麼模擬類呢?首先類的本質就是複製!.明白這點後,咱們就須要實現僞複製.咱們能夠將類裏的屬性,放在函數的prototype屬性裏.這樣該函數的實例對象就能夠經過[Prototype]訪問這些屬性.咱們也常常把這種行爲稱爲原型繼承(做者後面會瘋狂吐槽這個稱呼,我後面再解釋爲何吐槽).這樣就實現了僞"複製". 能夠達到和類類似的效果.
注意: 雖說全部的函數默認都會擁有一個名爲 prototype屬性.但也有特殊的時候.就不是默認的狀況.就是經過bind()硬綁定時.所返回的綁定函數,它是沒有prototype屬性的!
圖解真正的類與JS的模擬類:
關於原型繼承這個名字的瘋狂吐槽: 做者的吐槽主要集中在"繼承"兩個字,緣由是在面向類的語言中,"繼承"意味着複製,但在JavaScript裏原型繼承卻根本不是這個意思,它並無複製,而是用原型鏈來實現.因此瘋狂吐槽其誤導.
什麼是差別繼承? 我根本沒聽過這個術語,初次看做者所謂的解釋,這是啥?他想說啥?後來讀了好多遍,終於大概理解了.若是你也看不懂做者想在表達什麼,就pass這部分.不必理解.反而會把你看得更迷惑. 好了,我來解釋下什麼叫差別繼承.差別繼承就是原型繼承的一個不經常使用的別名.咱們知道對象能夠經過原型鏈繼承一部分屬性,但咱們仍能夠給對象設置其餘有差別不一樣的屬性.這也就能夠稱爲差別繼承.
構造函數之因此是構造函數,是由於它被new調用,若是沒被new調用,它就是一個普通函數.實際上,new會劫持全部普通函數並用構造對象的形式來調用它,而且不管如何都會構造返回一個對象.
關於兩種「面向類」的技巧,我這就不說明了,理解了這部分第一第二章關於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..遍歷到.
複製代碼
原型對象到原型對象的繼承: 例如: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"
複製代碼
錯誤用法:
Bar.prototype = Foo.prototype;
此時並不會建立一個關聯到 Bar.prototype 的新對象,它只是讓 Bar.prototype 直接引用 Foo.prototype 對象。 所以當你執行相似 Bar.prototype. myLabel = ... 的賦值語句時會直接修改 Foo.prototype 對象自己。Bar.prototype = new Foo();
它使用 了 Foo(..) 的「構造函數調用」,若是函數 Foo 有一些其餘操做的話,尤爲是與this有關的的話,就會影響到 Bar() 的「後代」,後果不堪設想。結論: 要建立一個合適的關聯對象,咱們需使用 Object.create(..) 而不是使用具備反作用的 Foo(..)。這樣作惟一的缺點就是須要建立一個新對象而後把舊對象拋棄掉(主要是須要手動設置constructor),不能直接修改已有的默認對象。
檢查"類"關係
a instanceof Foo
。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(見第三章).
[[Prototype]] 機制就是存在於對象中的一個內部連接,它會引用其餘對象。
這個連接的做用是:若是在對象上沒有找到須要的屬性或者方法引用,引擎就會繼續在 [[Prototype]] 關聯的對象上進行查找。同理,若是在後者中也沒有找到須要的 引用就會繼續查找它的 [[Prototype]],以此類推。這一系列對象的連接被稱爲原型鏈。
問:"咱們已經明白了爲何 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以前的語法了.因此這部分你們瞭解下便可.
[[Prototype]] 的本質做用: 書中提到了一個觀點"處理「缺失」屬性或者方法時的一種備用選項。"(即備用設計模式).但隨後進行了否認"可是這在 JavaScript 中並非很常見。因此若是你使用的是這種模式,那或許應當退後一步並從新思考一下這種模式是否合適。" 做者給出的觀點是:"進行委託設計模式,即例子中的內部委託(就是在對象裏套了個殼再引用了一遍,爲的是將委託進行隱藏).這樣能夠使咱們的API設計得更加清晰."文中的清晰是指,當咱們須要引用原型對象的屬性方法時,咱們在對象內部設置對應專門的屬性(例子中的doCool),進行內部委託(其實就是套個殼進行隱藏).這樣咱們對象的屬性就是"完整"的.
在實際工做中,咱們經常就是把原型對象做爲存放對象的公共屬性方法的地方.對於通常比較重要的操做纔會在對象裏進行內部委託(隱藏委託)!
總結得很好很全面,這裏我仍是直接摘抄了,不是偷懶哦!
第 5 章的結論:[[Prototype]] 機制就是指對象中的一個內部連接引用另外一個對象。換句話說,JavaScript 中這個機制的本質就是對象之間的關聯關係。在第六章又被稱爲委託. PS:前面在講原型的時候我就習慣用父對象指代原型對象(相似"父類"),用子對象指代其實例對象(相似"子類").本章也將採用這種稱呼,故下面再也不說明.(其實我以爲用父對象和子對象稱呼更形象)
一句話:[[Prototype]]機制是面向委託的設計,是不一樣於面向類的設計. 下面將分別介紹類理論和委託理論.
類理論設計方法: 首先定義一個通用父(基)類,在 父類類中定義全部任務都有(通用)的行爲。接着定義子類 ,它們都繼承自 父類而且會添加一些特殊的行爲來處理對應的任務,而且在繼承時子類能夠使用方法重寫(和多態)父類的行爲.
類理論中許多行爲能夠先「抽象」到父類而後再用子類進行特殊化(重寫)。 ps:這部分了解便可,着重理解下面JavaScript用到的委託.
類理論設計方法: 首先你會定義一個"父"對象(至關於上節中的父類),它會包含全部任務均可以使用(委託)的具體行爲。接着,對於每一個任務你均可以定義一個對象("子"對象)來存儲對應的數據和行爲。你能夠把特定的任務對象都關聯到父對象上,讓它們在須要的時候能夠進行委託。 (其實咱們通常都是用父對象來定義通用的方法,子對象進行委託.而後子對象自身個性的屬性方法就寫在子對象自己,並避免與父對象的屬性名衝突)
ps: 這節書中這段話可是咱們並不須要把這些行爲放在一塊兒,**經過類的複製**,咱們能夠把它們分別放在各自獨立 的對象中,須要時能夠容許 XYZ 對象委託給 Task。
有個錯誤."經過類的複製"應該改成"經過"[[Prototype]]機制".這裏應該是做者的手誤. 在 JavaScript 中,[[Prototype]] 機制會把對象關聯到其餘對象。不管你多麼努力地說服自 己,JavaScript 中就是沒有相似「類」的抽象機制。(其實主要緣由仍是是JavaScript沒有完整的複製機制)
委託理論的使用建議:
PS:書中這裏寫了3條,其實只有2條,第三條不過是對第一條的說明,這裏我進行了合併.
委託理論的使用注意:
實際上,在編寫本書時,這個行爲被認定是 Chrome 的一個 bug, 當你讀到此書時,它可能已經被修復了。
我只想說WTF! 好吧,我知道chrome之前可能出現過這個"bug"了=.=)這節主要是比較了"經過構造函數(模擬類)實現原型繼承"與"經過對象關聯(委託形式,Object.create( ... ))實現原型繼承"兩種方式的區別.
結論: 經過對象關聯,委託形式,更加簡潔,更加清晰易懂.
PS:這裏我本來本身對例子畫出原型示意圖.可是發現是真的複雜,而且和書中簡潔後的示意圖是差很少的,因此這裏就不展現了,省得讓讀者看得更頭大.這裏建議,讀者本身在草稿紙上畫出原型示意圖.
其實這節講得仍是"經過構造函數(模擬類)實現原型繼承"與"經過對象關聯(委託形式,Object.create( ... ))實現原型繼承"兩種方式的區別.不過此次主要是之前端實際使用場景進行講解.
這裏我就不以書中的例子進行講解了,而是直接站在更高的角度對這種"類"風格的代碼進行講解.
最大特色: 1是經過構造函數進行模擬類,2是經過顯式僞多態(硬綁定函數)關聯兩個函數.
注意:
雖然書中對顯式僞多態稱爲"醜陋的",還用了一個語氣動詞"呸!".雖然這樣很差,但有時用call真的很方便,因此用得也不少.
最大特色: 經過對象載體來模擬父子,並經過Object,create(...)來對兩個對象進行關聯.並經過委託的形式進行引用.與上節中提到的類形式還有一個區別:對象foo構建後,須要手動調用setUp方法進行初始化.故對象的構建與初始化是分開的.而構造函數形式則是在new 構造函數時, 同時進行了對象構建與初始化.(關於這點我下面還會再說明的)
關於書中這句使用類構造函數的話,你須要(並非硬性要求,可是強烈建議)在同一個步驟中實現構造和初始化。然而,在許多狀況下把這兩步分開(就像對象關聯代碼同樣)更靈活。
的理解:使用類構造函數形式,當咱們使用new 構造函數
時,實際上是在一步實現對象的構建和對象數據的初始化(經過構造函數裏的call) ;使用這種委託形式,咱們是分別經過Object.create( ... );
構建對象和foo.setUp( ...);
來初始化的.即咱們是分兩步實現的.這樣分開的話實際上是更加靈活,也更符合編程中的關注分離原則.
這節也是同樣經過二者的對比來突顯委託設計模式的各類優勢.這裏我就再也不對書中的例子進行解讀.若是你真正理解了類和委託的話,實際上是很簡單的.若是以爲複雜的話,能夠在紙上理一下函數和對象之間的關係,下面我就只總結下這裏提到委託設計模式的優勢,固然核心是更簡潔.
簡潔體如今:
這節主要是介紹ES6提供的2個簡潔寫法與其中的隱患.
語法:
var Foo = {
bar() { /*..*/ },//字面形式聲明
};
複製代碼
// 使用更好的對象字面形式語法和簡潔方法
var AuthController = {
errors: [],
checkAuth() {
// ...
},
server(url,data) {
// ...
}
// ...
};
// 如今把 AuthController 關聯到 LoginController
Object.setPrototypeOf( AuthController, LoginController );
複製代碼
弊端:
吐槽: 縱觀整本書,做者關於JavaScript中模擬類和繼承"的批評,說它們具備很大誤導性!更是嗤之以鼻!就差爆粗口了,JavaScript就像一個異教徒,應該綁在十字架上被燒死!可是他這樣的觀點,都是站在其餘語言的角度來看待時,產生的.我想更多的讀者多是隻接觸過JavaScript.那麼他實際上是沒有這些疑惑的!!!你反而給他們講這一大堆其餘語言的"正確"含義,有時候會時得其反!讓讀者更加困惑,若是是理解不透徹的,反而會懷疑本身原本寫的是對的代碼!因此讀者應該作一個能夠理解做者意圖,而且擁有自我看法和觀點立場!
什麼是內省(自省)? 首先,本節須要弄懂一個問題,什麼是內省,或者是自省。書中的解釋是自省就是檢查實例的類型。類實例的自省主要目的是經過建立方式來判斷對象的結構和功能。
我這裏再更通俗地解釋下:當咱們構建獲得一個實例對象時,有時候咱們是不太清除它的屬性和方法的.尤爲是第三方庫.有時候貿然使用會致使不少錯誤(例如調用的方法不存在,或者報錯等).這個時候咱們就須要經過自省.其實就是經過一系列操做,來確認實例是否是咱們想要的那個,實例的方法是否是咱們想要的(存在且可用).
內省的方法:
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
進行判斷.而且也不能直接判斷兩個對象是否關聯.
//若是a1的something存在的話,則咱們能夠進行調用
if ( a1.something) {
a1.something();
}
複製代碼
其實這種方法是很是經常使用的,排除了在不知道存在性狀況下,貿然調用的風險.
缺陷: 關於書中提到的缺點,四個字歸納就是"以偏概全" .書中關於Promise的例子,就是以偏概全的例子.因此咱們在使用時,在if判斷a1.something存在時,纔會在後面使用something方法.不要直接使用anotherthing,這種沒確認過的方法.
Object.getPrototypeOf(..)
進行判斷.例如Object.getPrototypeOf(a)===A
其中a,A都是對象.若是爲true,則說明a的原型鏈上含有對象A.後面關於<你不知道的JavaScript>中和下.還在寫做當中,手頭上還有一篇webpack徹底指北的文章,目前寫了一半2w字,也都是面向新手,真正的全面地由淺入深.最近,空降一個新項目組,開發到測試只有一個月,還要帶新人,更新會很慢.不過我會爭取年前所有放出.若是你們喜歡的話,能夠關注我一下,文章首發仍是在掘金的.
最後求一個內推,目前筆者在一家軟件國企(半養老型).年末或者明年初就會考慮離職.但願進入一家比較好的互聯網企業.若是有小夥伴有好的機會能夠發我郵箱:bupabuku@foxmail.com.謝謝!
目前暫時優先考慮武漢(房子,盆友,東西都在這邊,去外地太不方便了-.-)
爲了方便你們下載到本地查看,目前已經將MD文件上傳到百度網盤上了. 連接: pan.baidu.com/s/1ylKgPCw3… 提取碼: jags (相信進去後,大家還會回來點讚的! =.=)