你不知道的JavaScript讀書筆記-1

relsoul發表於2016-06-26

書評

豆瓣 這本書很適合初級前端開發者上升至中級前端開發者,很好的闡述了JavaScript的閉包,原型,類,編譯,賦值的問題.而且這還是上卷,還會有中卷,下卷,等等之類的.我會從這本書裡選取一些比較重要的內容放在這篇文章當中(實際上這本書全部內容都重要). let's do it

作用域

編譯器原理簡釋

var a=2 當我們看到var a=2的時候引擎和編譯器會做什麼呢?

  1. 遇到var a,編譯器會詢問作用域是否已經有一個該名稱的變數存在於同一個作用域的合集中.如果是.編譯器會忽略該宣告,繼續進行編譯.否則它會要求作用域在當前的作用域合集中宣告一個新的變數,並且命名為a.
  2. 接下來編譯器會為這個引擎生成執行時所需的程式碼,這些程式碼被用來處理a = 2這個賦值操作.引擎執行時會首先詢問作用域,在當前的作用域合集中是否存在一個叫a的變數,如果否,引擎就會使用這個變數;如果不是,引擎就會繼續查詢該變數.

引擎與作用域的對話

RHS引用是找到這個變數所在的地址,但是不賦值 賦值是等號做的事情 LHS引用是賦值時把RHS找到的地址賦值給LHS

function foo(a){ console.log(a);//2 } foo(2) 把這段程式碼想象成一段對話是這樣的

  • 引擎:我說作用域,我需要為foo進行RHS引用,你見過他嗎?
  • 作用域:別說,我還真見過,編譯器那小子剛剛宣告瞭它.它是一個函式,給你.
  • 引擎: 哥們太夠意思了!好吧,我來執行以下foo.
  • 引擎:作用域還有個事,我需要為a(函式傳參中的a)進行LHS引用,這個你見過嗎?
  • 作用域:這個也見過,編譯器最近把它宣告為foo的一個形式引數,拿去吧.
  • 引擎:大恩不言謝,你總是這麼棒,現在我要把2賦值給a.
  • 引擎:哥們,不好意思又來打擾你.我需要為console進行RHS引用.你見過它嗎?
  • 作用域:咋們誰跟誰啊.console是個內建物件,給你
  • 引擎:麼麼噠,我的看看這裡面是不是有個log(...).太好了,找到了,是一個函式.
  • 引擎:哥們,能再幫我找一下對a的RHS引用嗎?雖然我記得它,但想再確認一次.
  • 作用域:放心吧.這個變數也沒有變動過.拿走.不謝.
  • 引擎:真棒,我來把a的值,也就是2.傳遞進log(...)

作用域巢狀

function foo(a){ console.log(a+b) } var b=2; foo(2); 首先在瀏覽器中最頂端的作用域就是window也就是全域性作用域.那麼上述程式碼在全域性作用域中建立了一個作用域叫foo.當引擎去解析執行的時候.對b進行RHS引用的時候在當前foo作用域是找不到的.於是去foo的上級作用域即全域性作用域去查詢b這個變數.

異常

function foo(a){ console.log(a+b); b=a; } foo(2) 如果RHS遍歷了所有的巢狀作用域都找不到該變數,引擎就會丟擲ReferenceError異常. 如果執行的是LHS查詢時.如果在頂層(全域性作用域)中也無法找到目標變數,那麼全域性作用域中就會建立一個具有該名稱的變數.並且將其返回給引擎 ReferenceError異常的意思是作用域判別失敗相關. TypeError則代表作用域判別成功,但是對結果的操作是非法或不合理的.

詞法作用域

詞法階段

詞法作用域也叫靜態作用域.其作用域只在引擎初始化的時候就已經定好了.不會跟隨程式碼的執行而動態改變作用域 function foo(a){ var b=a*2; function bar(c){ console.log(a,b,c); } bar(b*3); } foo(2);//2,4,12 這裡面有三個巢狀的作用域 這裡來分析一下

  • window(全域性作用域)
  • window=>foo
  • window=>foo=>bar

作用域是巢狀的,上面也說了當編譯器在當前作用域找不到的時候會在當前作用域建立一個變數.賦值的時候則會逐級遞迴查詢當前作用域是否存在當前變數.不存在則會建立全域性變數. 因為作用域是巢狀的.巢狀中的作用域可以訪問上層作用域的值.所以在bar這個函式裡,並沒有a變數.但是它會從它上層作用域foo去查詢. 全域性變數自動會成為window(瀏覽器)的屬性.比如上述的foo(2)可以用window.foo(2) 來寫 無論函式在哪裡被呼叫,也無論它如何被呼叫,它的詞法作用域都只由函式被宣告時所處的位置決定 詞法作用域只會查詢一級識別符號.比如上述的foo.bar.baz,詞法作用域查詢只會試圖查詢foo識別符號.找到這個變數後.物件屬性訪問規則會分別接管對bar和baz屬性的訪問.

欺騙詞法

欺騙詞法作用域會導致效能下降

eval

function foo(str,a){ eval(str);//欺騙 console.log(a,b); } var b=2; foo("var b=3;",1);//1,3 eval動態在foo作用域中建立了一個b變數,並且遮蔽掉了外部(window)中的b變數

with

with通常被當做重複引用同一個物件中的多個屬性的快捷方式,可以不需要重複引用物件本身 var obj={ a:1, b:2, } with(obj){ a=3; b=4; } 但實際上這不僅僅是為了方便訪問物件屬性. ``` 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 洩露到全域性作用域中了 ``` 之所以o1能夠正確被賦值是因為o1存在a這個屬性.o2不存在a這個屬性.所以with中引擎找不到這個屬性的時候則會使用正常的LHS引用.所以洩露到全域性中了

函式作用域與塊狀作用域

這一節主要講述的是如何利用匿名函式來建立作用域,這樣就不會汙染全域性名稱空間了 匿名函式是可以具名的,並且效果一樣 setTimeout(function timeoutHandler(){ console.log("i waited 1 second") },1000) 如果用匿名函式在執行的過程中報錯了那麼瀏覽器只會返回一個anonymous function 如果具名的話那麼會返回當前那個函式的名稱 (function(){throw new Error("")})() //結果 (anonymous function) @ VM419:2 (anonymous function) @ VM419:2 InjectedScript._evaluateOn @ VM410:904 InjectedScript._evaluateAndWrap @ VM410:837 InjectedScript.evaluate @ VM410:693

JavaScript的塊級作用域有三個 一個是ifelse建立的 一個是try catch 還有一個是with 用with從物件建立出的作用域僅在with宣告中而非外部作用域中有效

提升

JavaScript有兩種提升 一種是var xxx;一種是函式提升 function xxx(){} 其中函式提升的優先順序要大於var foo();//1 var foo; function foo(){ console.log(1); } foo=function(){ console.log(2); } 編譯器首先會提升function宣告至作用域頂端 然後再執行foo() 並且函式提升無法被if else所控制 foo();//a var a=true if(a){ function foo(){console.log("a")} }else{ function foo(){console.log("b")} } 無論作用域的宣告出現在什麼地方,都將在程式碼本身被執行前首先進行處理.可以將這個過程形象地想象成所有的宣告(變數和函式)都會被"移動"到各自作用域的最頂端.這個過程被稱為提升

作用域閉包

具體的閉包這裡不再做筆記了,這裡只做如何用閉包來實現module(模組) 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! 這個模式在JavaScript稱為模組,上述的返回值可以看成模組的公共API. 實際上從模組返回一個實際的物件並不是必須的,也可以直接返回一個內部函式.jquery和$就是一個很好的例子.jquery與$就是jquery模組的公共API,但它們本身都是函式(由於函式也是物件,它們本身也可以擁有屬性)

模組模式需要具備兩個必須條件

  1. 必須有外部的封閉函式,該函式必須至少被呼叫一次(每次呼叫都會建立一個新的模組例項).
  2. 封閉函式必須返回至少一個內部函式,這樣內部函式才能在私有作用域中形成閉包.並且可以訪問或者修改私有的狀態.

一個具有函式屬性的物件本身並不是真正的模組,從方便觀察的角度看.一個從函式呼叫所返回的,只有資料屬性而沒有閉包函式的物件並不是真正的模組. 模組也可以輕而易舉的實現單例模式 只需要改成IIFE(匿名函式立即執行) var foo=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 } }() foo.doSomething()//cool foo.doAnother();//1!2!3! 模組模式另一個簡單但強大的變化用法是.命名將要作為公共API返回的物件 ``` var foo=(function CoolModule(id){ function change(){ //修改公共API publicAPI.identify=identify2; };

    function identifty1(){
        console.log(id)
    };

    function identifty2(){
        console.log(id.toUpperCase());
    };

    var publicAPI={
        change:change,
        identifty:identifty1
    }     

})("foo module");

foo.identifty();//foo module foo.change(); foo.identifty();//FOO MODULE ``` 通過在模組例項內部保留對公共API物件的內部引用,可以從內部對模組例項進行修改,包括新增或者刪除方法和屬性已經修改他們的值

現代的模組機制

這裡簡略實現下模組引入機制 var MyModule=(function Manager(){ var modules={}; //name 定義的模組名字 //deps 需要載入引入的模組名字 //impl 新的模組函式 function define(name,deps,impl){ for(var i=0;i<deps.length;i++){ deps[i]=modules[deps[i]] } //儲存模組 執行impl同時傳遞deps的返回值 modules[name]=impl.apply(impl,deps) } function get(name){ return mdoules[name] } return{ define:define, get:get } })() 使用就不演示了,比較簡單的一段程式碼. 模組有兩個主要特徵

  • 為建立內部作用域而呼叫了一個包裝函式;
  • 包裝函式的返回值必須至少包括一個內部函式的引用,這樣就會建立涵蓋整個包裝函式內部作用域的閉包

this

首先需要明白 this並不是總是指向函式本身.this在任何情況下都不指向函式的詞法作用域.this指向的是物件.在JavaScript內部,作用域的確和物件相似.可見的識別符號都是它的數學.但是作用域"物件"無法通過JavaScript程式碼訪問,它存在於JavaScript引擎內部

this到底是什麼

this是執行時進行繫結的.並不是在編寫時繫結的,它的上下文取決於函式呼叫時的各種條件.this的繫結和函式宣告的位置沒有任何關係.只取決於函式的呼叫方式. 當一個函式被呼叫時,會建立一個活動記錄(上下文),這個記錄會包含函式在哪裡被呼叫(呼叫棧),函式的呼叫方法,傳入的引數等資訊,this就是記錄其中的一個屬性,會在函式執行的過程中用到

this的呼叫位置

呼叫位置就是函式在程式碼中被呼叫的位置而不是宣告的位置. 最重要的是分析呼叫棧(就是為了到達當前執行位置所呼叫的所有函式) ``` function baz(){ //呼叫棧:baz //呼叫位置 全域性作用域

console.log("baz");
bar();// bar的呼叫位置

}

function bar(){ // 呼叫棧 baz->bar // 呼叫位置在bar中

console.log("bar");
foo()// foo的呼叫位置

}

function foo(){ // 呼叫棧 bz->bar->foo // 呼叫位置 bar console.log("foo"); } baz();// baz的呼叫位置 ```

this的預設繫結

this預設是繫結在window下的 function foo(){ console.log(this.a) } var a=2; foo();//2

this的隱式繫結

物件屬性引用鏈中最有最頂層或者最後一層會影響呼叫位置 function foo(){ console.log(this.a); } var obj2={ a:42, foo:foo } var obj1={ a:2, obj2:obj2 } obj1.obj2.foo();//42

隱式丟失

被隱式繫結的函式會丟失繫結物件,也就是說它會應用預設繫結.從而把this繫結到全域性物件中 function foo(){ console.log(this.a) } function doFoo(fn){ // fn其實是引用的foo fn();//呼叫位置 } var obj={ a:2, foo:foo } var a="oops,global"; doFoo(obj.foo);//"oops,global"

this的顯式繫結

利用call和apply來修復this繫結物件

硬繫結

function foo(){ console.log(this.a) } var obj={ a:2 } var bar=function(){ foo.call(obj) } bar();//2 //硬繫結的bar不可能再修改它的this bar.call(window);//2 上述是封裝了一層也就是在bar內強制性的繫結了一個物件 所以外界怎麼修改bar的呼叫位置都不可能印象到foo函式 另外ES5的bind就是一種硬繫結

new繫結

使用new來呼叫函式,或者發生建構函式呼叫時,會自動執行下面的操作

  1. 建立(或者說構造)一個全新的物件.
  2. 這個新物件會被執行[[原型]]連線
  3. 這個新物件會繫結到函式呼叫的this
  4. 如果函式沒有返回其他物件,那麼new表示式中的函式呼叫會自動返回這個新物件

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

優先順序

1.函式是否在new中呼叫(new 繫結)?如果是的話this繫結的是新建立的物件 var var = new foo() 2. 函式是否通過call,apply(顯式繫結)或者硬繫結呼叫?如果是的話,this繫結的是指定物件 var bar = foo.call(obj2) 3. 函式是否在某個上下文物件中呼叫(隱式繫結)?如果是的話,this繫結的是那個上下文物件 var bar=obj1.foo() 4. 如果都不是的話,那麼使用預設繫結.非嚴格模式下繫結到全域性 var bar=foo()

如果call,apply傳遞的是null那麼實際上應用的是預設繫結規則

間接引用

間接引用最容易發生在賦值的時候 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 賦值表示式p.foo=o.foo的返回值是目標函式的引用,因此呼叫位置是foo而不是p.foo()或者o.foo().根據我們之前說過的這裡會引用預設繫結

軟繫結

硬繫結是把this強制性繫結到指定的物件(除了new),問題在於硬繫結會大大降低函式的靈活性.使用硬繫結之後就無法使用隱式繫結或者顯式繫結來修改this ``` if(!Function.prototype.softBind){ Function.prototype.softBind=function(obj){ var fn=this;//當前函式

    var curried=[].slice.call(arguments,1);

    var bound=function(){
        //如果不存在this或者預設的this指向全域性 那麼則動態設定this為傳遞進來的obj否則就預設繫結的this
        return fn.apply((!this||this===(window||global))?obj:this,curried.concate.apply(curried,arguments))
    }
    //繼承fn
    bound.prototype.Object.create(fn.prototype)
    return bound
}

} 下面來看看softBind是否實現了軟繫結功能 function foo(){ console.log("name"+this.name); }

var obj={name:"obj1"}, obj2={name:"obj2"}, obj3={name:"obj3"};

var fooObj=foo.softBind(obj); fooObj();//name:obj

obj2.foo=foo.softBind(obj); obj2.foo();//name:obj2 //這裡的this不等於window 所以繫結的是呼叫者也就是obj2

fooObj.call(obj3);//name:obj3 //這裡的this等於window 所以應用的是obj

setTimeout(obj2.foo,10);//name:obj 這裡的this為window但是因為沒有執行這個函式 所有這裡引用的是obj 這裡的程式碼可以這樣分析

obj2.foo=function(){ return fn.apply((!this||this===(window||global))?obj:this,curried.concate.apply(curried,arguments)) } ```

this詞法

function foo(){ var self=this; setTimeout(function(){ console.log(self.a) },1000) } var obj={ a:2 } foo.call(obj);//2 如果你經常編寫this風格的程式碼,但是絕大部分都會至用self=this來否定this的機制,那你或許應該

  1. 只使用詞法作用域並完全拋棄錯誤this風格的程式碼(如module模式)
  2. 完成採用this風格,在必要時使用bind(...),儘量避免使用self=this

相關文章