你不懂的JS學習筆記(做用域和閉包)

You don't KnowJS

引語:你不懂的JS這本書github上已經有了7w的star最近也是張野大大給我推薦了一波,閱讀過以後感受對js的基礎又有了更好的理解。原本我是歷來不這種讀書筆記的,可是這本書的內容實在是太多太多哪哪都是重點。因此
也就決定記錄如下重要的地方便於之後複習方便若是有錯誤,感謝指出html

第一部分:做用域和閉包

第一章

編譯的三個步驟(固然也就是編譯器乾的事情了)

  1. 分詞/詞法分析
    通俗來講就是編譯器會將咱們寫的代碼首先拆分紅能夠進行編譯的代碼 eg:var a = 2;能夠被編譯器分割爲var,a,=,2,; 空格是否會被看成詞法單元,取決於空格在這門語言中是否具備意義。
  2. 解析/語法分析
    AST:抽象語法樹的概念他會把上述分割好的代碼進組裝成爲一個語法樹m,=,var,a,2 都回變成語法樹的各個節點,從而爲編譯作準備。
  3. 代碼生成
    編譯器最終會將這樣的AST語法樹編譯爲可執行的底層代碼。特別要強調的是JS的引擎在編譯器執行是會幫助編譯器作代碼優化,同一般來講他不會編譯的過程就發生在引擎執行代碼的前很短的時間,並非像執行C/C++等這些代碼須要先build完整個文件再進行run這樣的方式。

理解做用域 (一般指的是詞法做用域或者也能夠叫作靜態做用域)

首先說一下基本的執行順序首先是編譯器由上面的步驟編譯代碼而後對於一些變量的聲明會在編譯期間交給做用域而後做用域就會組成一個
像是一個樹的結構,全局做用域下面會有嵌套的函數做用域。最後JS引擎根據做用域去執行代碼,大概就是這樣的一個流程。
介紹如下三個關鍵的概念: 前端

  1. 編譯器: 用來在引擎執行代碼前提供給引擎代碼而且向做用域提供組成「樹」的節點
  2. 引擎:用來負責執行和編譯的環境 配合做用域組成本身的上下文
  3. 做用域:負責收集並維護由全部聲明的標識符(變量)組成的一系列查詢,並實施一套很是嚴格的規則,肯定當前執行的代碼對這些標識符的訪問權限。
    LHS和RHS:「賦值操做的目標是誰(LHS)」以及「誰是賦值操做的源頭(RHS)」。PS:rhs參考對象爲語句中的常量例如:console.log(1)就是誰來對於1進行操做,lh參考對象爲語句中的常量例如:a = 22應該賦值給誰若是是 console.log(a)應該就是RHS和LHS一塊兒

引擎和做用域的關係

下面是我寫的書上的題
測驗的答案:LHS:foo->c,2->a,a->b
ps:1. 能夠理解爲foo須要知道本身應該賦值給誰因此LHR
ps:2. 能夠理解爲2須要知道本身賦值給誰這裏是foo的參數a
ps:3. 能夠理解爲a須要給誰賦值
測試的答案:RHS:2->foo,a->foo,b->a,a+b->return
ps: 1. 能夠理解爲是誰調用2,因此是foo
ps:2. 同上能夠知道後續的三個答案git

做用域的嵌套

做用域的嵌套:做用域是個家族,爸爸認識兒子的人,爺爺認識爸爸認識的人,每次問兒子有沒有有認識的人,若是沒有再問爸爸。 也就是
上文提到的樹結構
ReferenceError:你找了做用域整個家族都不認識的人就會出錯,並無申明這個引用了沒有聲明的變量的錯誤.
LHS查詢的時候須要特別注意的是 若是LHS在全局做用域當中都沒法找到變量就會建立一個變量(非嚴格模式)
若是查找的目的是對變量進行賦值,那麼就會使用 LHS 查詢;若是目的是獲取變量的值,就會使用 RHS 查詢es6

第二章: 詞法的做用域

詞法階段

1.一個詞法不可能同時在兩個做用域中,做用域查找會在找到第一個匹配的標識符時中止
2.全局變量會自動成爲全局對象的屬性(據阮老師的博客上說這是因爲js的設計這爲了減小內存了留下的歷史問題)
3.不管函數在哪裏被調用,也不管它如何被調用,它的詞法做用域都只由函數被聲明時所處的位置決定。(詞法做用域是靜態做用域和動態的沒有關係)github

欺騙詞法做用域

1.eval函數:接受字符串代碼他會在編譯器執行在引擎快要執行的時候將這段代碼寫在他位於的位置,不推薦使用.不過能夠解決var出現的變量死區的問題
2.with函數:簡單來講with函數{}之內的語句在當前的位置之內建立了一個做用域並且自動放入了吧obj對象當中的屬性放了進去,這就有點想是在Chrome中的命令行寫global.a = 0而後a=1進行賦值時同樣的,依然不推薦使用web

//with的用法
var obj = {  
    a: 1,
    b: 2,
    c: 3 
};
// 單調乏味的重複 "obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 簡單可是不快捷的方式 
with (obj) {
    a = 3;
    b = 4;
    c = 5;
}
//obj.a = 3
/***************我是分界線*****************/
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 被泄漏到全局做用域上了!複製代碼

這就是前面說的o2.a會進行LHS查詢當查詢到頂級時就會給全局變量賦值
eval和with我認爲並非做爲詞法做用域的範圍,由於詞法做用域是在編譯前就作好的,因此這個叫作欺騙詞法做用域,固然由於是動態的因此會消耗性能編程

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

函數中的做用域

先看下面代碼函數bar(..) 擁有本身的做用域範圍,全局做用域也有本身的做用域範圍,它只包含了一個標識符:foo。而foo能夠理解爲一層樓進入一個房間的門,是一個入口.函數做用域
主要提供函數變量的訪問,找不到一個變量就會去上一個做用域找,而以後所提到的原型鏈是一個對象的原型鏈是在這個對象的內部找屬性找不到的時候就會去查找.(本身在看書的時候不當心弄混了)數組

function foo(a) { 
    var b = 2;
    // 一些代碼
    function bar() { 
        // ...
    }
    var c = 3;
}複製代碼

隱藏內部實現

在下面的代碼當中doSomethingElse的調用並非最安全的,由於其餘函數均可以調用安全

function doSomething(a) {
    b = a + doSomethingElse( a * 2 );
    console.log( b * 3 );
}
function doSomethingElse(a) { 
    return a - 1;
}
var b;
doSomething( 2 ); // 15複製代碼

而下面的函數則是比較安全的bash

function doSomething(a) { 
    function doSomethingElse(a) {
        return a - 1; 
    }
    var b;
    b = a + doSomethingElse( a * 2 );
    console.log( b * 3 );
}
doSomething( 2 ); // 15複製代碼

因此function(){}用來隱藏代碼解決衝突(這是由於js在es5當中只有函數做用域並無塊做用域)

函數做用域

js當中爲了可以模仿塊級做用域,邊有人想到了用函數做用域模仿的概念.先來看看下面代碼

//並不理想
function foo() { 
    var a = 3; 
    console.log( a ); 
} 
foo(); 
//方法1:
(function foo(){ 
    console.log( a ); // 3 
})();複製代碼

雖然這種技術能夠解決一些問題,可是它並不理想,由於會致使一些額外的問題。首先,必須聲明一個具名函數 foo(),意味着 foo 這個名稱自己「污染」了所在做用域(在這個 例子中是全局做用域)。其次,必須顯式地經過函數名foo()調用這個函數才能運行其中的代碼。然而使用了自執行函數之後欺騙編譯器對於經過()或者+-*等等欺騙了編譯器的檢查(後面會提到)
因此忽略了function的聲明語句.而這個語句的結果值就是這個函數調用之後就會執行

匿名函數和當即執行函數還有函數的聲明和函數表達式

編寫帶有名字的函數便於理解

setTimeout( function timeoutHandler() { 
    // <-- 快看,我有名字了! 
    console.log( "I waited 1 second!" );
}, 1000 );複製代碼

function 和 var 編譯時存在函數的提高,也就是說var 和 function會優先的被編譯器識別交給做用域。而後引擎在訪問代碼的時候就可以查詢到編譯器交過來的變量了
下面說的方法實際上是經過一些其餘的表達式干擾編譯器的判斷,讓編譯器認爲這並非一個聲明,對於函數的表達式和函數的聲明還有當即執行函數能夠看看這兩個博主的文章看看我還有我
1.編譯器在編譯代碼的時候發現一行代碼若是第一個出現的是funtion則會被理解爲函數的聲明語句(var也是而let的機制可能就不一樣),編譯器就會自動把它叫給做用域.函數的聲明是不會有結果值的
2.當一個有function的函數的聲明加入其餘的東西時(例如括號+或者-等)編譯器會把他認爲是一個非聲明的語句,而這些語句是須要引擎來執行的
3.當引擎執行代碼的時候會發現這裏面藏着一個非聲明的語句因而就執行他這時候是有結果值的,因此能夠對他進行調用

下面的代碼就是例子(感覺一下js的黑暗吧)

//下面對能夠對返回的結果值進行調用,括號的位置並不影響由於function(){}被做爲表達式執行完之後會就會返回函數因此兩個都行
( function foo(){} )()//ƒ foo(){}
( function foo(){} () )//ƒ foo(){}
//我本身又寫了一下感覺邪惡吧
(function(){return (a)=>{ console.log(a); return (b)=>{console.log(b)}}})()(1)(2)
//下面沒有返回的結果值就不能夠因此報錯
function foo(){}()
//因此你能夠依靠this和做用域來實現let,若是沒有聲明就會出錯
function foo(){console.log(a); (function(){this.a = 10})(); console.log(a); }
function foo(){console.log(a); let a = 10; console.log(a); }複製代碼

塊做用域

{}沒法建立塊做用域由於js並不支持塊做用域,可是try{}catch{}卻能夠,和function(){
}我認爲他們建立的實際上是函數做用域,其實他們一直是在用函數做用域模擬塊做用域

第四章:提高

函數優先的原則

看下面的代碼

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

上面的代碼會執行1和下面的代碼是等價的,這說明函數的聲明是要比var提早的,我認爲可能編譯器在發現有function生命的時候會把var替換掉

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

第五章: 閉包

什麼是閉包

其實我把閉包想象爲一個被保存的做用域,而實現方式一般使用function(){}建立這樣一個函數做用域的方式(固然也有其餘的方式)

function foo() { 
    var a = 2;
    function bar() { 
        console.log( a );
    }
    return bar; 
}
var baz = foo();
baz(); // 2 —— 朋友,這就是閉包的效果。複製代碼

看起來你並不以爲這有什麼牛逼的地方,可是其實js當中閉包是十分經常使用的功能(好比全部的回調函數其實都是閉包)

//當你把閉包的返回進入另外一個函數內部的時候,你就能夠在另外一個函數內部訪問他的變量!!!!!!!
function foo() { 
    var a=2;
    function baz() { 
        console.log( a ); // 2
    }
    bar( baz ); 
}
function bar(fn) {
    fn(); // 媽媽快看呀,這就是閉包!
}
//經過做用域訪問的方式進行傳遞閉包
var fn; 
function foo() {
    var a=2;
    function baz() { 
        console.log( a );
    }   
    fn = baz; // 將 baz 分配給全局變量 
}
function bar() {
    fn(); // 媽媽快看呀,這就是閉包!
}
foo();
bar(); // 2複製代碼

做者也告訴咱們不只僅如此,閉包之所用重要是由於 在定時器、事件監聽器、 Ajax請求、跨窗口通訊、Web Workers或者任何其餘的異步(或者同步)任務中,只要使用了回調函數,本質都是在使用閉包!

function wait() {
    let a = 1;
    function test(){
        console.log(this.a)
        console.log(a)
    }
    return test;
}
var a = 2;
wait()();
// 定時器
function wait(message) {
    //這就是閉包
    function timer() {
        console.log( message );
    }
    //下面的就能夠理解爲引擎會在1s內調用一個函數,而這個函數就是閉包,他會訪問message的做用域
    setTimeout( timer, 1000 ); 
}
wait( "Hello, closure!" );
//事件監聽器
function setupBot(name, selector) {
$( selector ).click( function activator() {
console.log( "Activating: " + name ); });
}
     setupBot( "Closure Bot 1", "#bot_1" );
     setupBot( "Closure Bot 2", "#bot_2" );
//觸發的activator函數也能夠看作是一個閉包複製代碼

循環和閉包

先看看代碼

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

其實以上的輸出結果並不會是1,2,3,4,5.反而回是6,6,6,6,6.這是由於setTimeout閉包並非當即執行的,而是延遲執行的.因此第一步會先把for循環走完,當延遲執行的函數從新回到這個做用域的時候,這裏的變量已經面目全非了,因此爲了可以維護閉包調用的做用域咱們會纔去一些措施(我記得大搜車的筆試題就有這個)

//這樣是不行的,雖然咱們確實建立了一個供閉包未來回頭查看的做用域,可是這個做用域裏面什麼都沒有
for (var i=1; i<=5; i++) { 
    (function() { 
        setTimeout( function timer() { console.log( i );}, i*1000 );})();
}
//因此像下面這樣的纔可以運行,由於這裏面維護的做用域就再也不是空了,固然也是由於這裏面是一個值變量
for (var i=1; i<=5; i++) { 
(function() {
    var j = i;
    setTimeout( function timer() {
                 console.log( j );
             }, j*1000 );
})(); }複製代碼

模塊

在前端方面最先的模塊機制的實現其實就是閉包,開頭我說一般使用function(){}來維持一個特定的做用域,而下面的返回object的對象將各個維持特定做用域的function(){}組合起來,也可以實現閉包.

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複製代碼

首先,CoolModule() 只是一個函數,必需要經過調用它來建立一個模塊實例。若是不執行外部函數,內部做用域和閉包都沒法被建立。
其次,CoolModule()返回一個用對象字面量語法{ key: value, ... }來表示的對象。這個返回的對象中含有對內部函數而不是內部數據變量的引用。咱們須要保持內部數據變量是隱藏且私有的狀態。能夠將這個對象類型的返回值看做本質上是模塊的公共 API。
從模塊中返回一個實際的對象並非必須的,也能夠直接返回一個內部函數。jQuery 就是一個很好的例子。jQuery 和 $ 標識符就是 juery 模塊的公共 API但它們自己都是函數(使用jq的時候實際上是調用了他的構造函數創造了一個jq的節點)
這樣就實現了訪問API中的方法可是卻又不會使變量污染,可是你必須使用它而後本身賦值一個變量
閉包的造成必須有兩個條件:1.必須有像上面一CoolModule()同樣的封閉函數,也就是閉包所能保留的做用域範圍.2.封閉函數至少要返回一個函數去做爲探測這個做用域的閉包

現代和將來的模塊機制

看看下面代碼

//這裏的模塊定義就像上面的那樣返回的是由閉包函數組成的對象
var MyModules = (
    function Manager() {
    var modules = {};
    //這個函數是將建立定義,模塊的名字,和制定本身所依賴的模塊當你須要依賴其餘模塊的時候就會在這裏進行加載,傳入實現函數進行加載,最後是這個模塊的實現
    function define(name, deps, impl) { 
        for (var i=0; i<deps.length; i++) {
            deps[i] = modules[deps[i]];
        }
        modules[name] = impl.apply( impl, deps );
    }
    function get(name) { return modules[name];}
    return {
        define: define,
        get: get 
    }
})();
//首先定義一個本身的模塊bar,用來分裝一個說你好的方式
MyModules.define( "bar", [], function() { 
    function hello(who) {return "Let me introduce: " + who; }
    return {
    hello: hello
};});
//foo依賴於
MyModules.define( "foo", ["bar"], function(bar) {
    var hungry = "hippo";
    function awesome(){
        console.log(bar.hello(hungry).toUpperCase())
    }
    return {
        awesome: awesome
    }; 
});
var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(bar.hello( "hippo" )); // Let me introduce: hippo 
foo.awesome(); // LET ME INTRODUCE: HIPPO複製代碼

實際上在foo中獲得的閉包bar閉包和 var bar = MyModules.get( "bar" );獲得的閉包是同樣的

將來的模塊機制

  1. 在es6中會把一個文件當作一個模塊,我我的理解就是用一個(function 文件名(導入的其餘文件){})將整個文件代碼括起來
  2. es6的模塊是比較穩定的,在以前的模塊機制用函數來分裝模塊會致使只有在引擎執行代碼的時候纔會知道爲何錯,可是es6的模塊機制會被編譯器識別也就會在執行知道將會有什麼錯誤.
  3. 這裏對應的應該是require和import的區別由於在webstorm編寫代碼的時候,require即使是路徑寫的不對也不會在webstorm出現錯誤,可是使用import導入的時候若是不存在webstorm會提示你沒辦法導入,我想着就是webstorm後臺爲你編譯進行提示錯誤吧
  4. 還有一點好處是若是b塊裏面用了c,當b導入到a中c也就會本身導入
  5. module和import的區別

    import 能夠將一個模塊中的一個或多個 API 導入到當前做用域中,並分別綁定在一個變量 上(在咱們的例子裏是 hello)。module 會將整個模塊的 API 導入並綁定到一個變量上(在 咱們的例子裏是 foo 和 bar)。export 會將當前模塊的一個標識符(變量、函數)導出爲公 共 API。這些操做能夠在模塊定義中根據須要使用任意屢次。

附錄的內容

動態做用域

經過前面的學習咱們知道靜態做用域也就是詞法做用域,也就是詞法做用域是由編譯器提早執行代碼的時候構造出來的一個做用域,我以爲他必定是採用樹進行存儲的.而動態的做用域實際上更多的是指的this指針,也就是說在引擎執行代碼的過程當中進行變化的.(大部分的做用域應該是詞法做用域,可是不免的要使用一些在執行過程當中變化的做用域)

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

上面的代碼當中foo()做用域中沒有a變量,也就是說要執行RHS引用(固然也沒有console變量他會執行LHS引用)因此會找到2

塊做用域

以前說過js中是沒有塊級做用域的可是這其實並非一個正常的編程語言的行爲,因此模擬塊級做用域是很是重要的.其實在一些語法中就已經有了塊級做用域
好比with 和 catch

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

this詞法

這裏主要提到了箭頭函數的用法,好比說

var obj = {
    id:"awesome",
    cool:function coolFn(){
        console.log(this.id);
    }
}
var id = "not awesome"
obj.cool();//"awesome"
setTimeout(obj.cool,100);//"not awesome"複製代碼

obj.cool()當然是隱式綁定可是當放在函數當中的時候其實這個隱式綁定會被斷開由於他把這個函數
的指針賦給了setTimeout的參數變量因此調用的時候實際上是cool()這種方式。除了文章中提到的self保存住this的方法,就是使用箭頭函數的綁定能夠寫成這個樣子

var obj = {
    count: 0,
    cool: function coolFn() {
        if (this.count < 1) {
            setTimeout( () => {  // 箭頭函數是什麼鬼東西?
            this.count++;
            console.log( "awesome?" );
            }, 100 );
        }
    }
};
obj.cool(); // "awesome?"複製代碼

箭頭函數的筆記在後面還會詳細的學習記錄下。

遺留問題

  1. 最後我仍是沒有弄懂,附錄中動態做用域的問題,做者也說了動態做用域關心的是這個調用的位置而不是聲明的位置,因此若是按照做者的動態做用域的觀點會輸出this但是
    做者本身又否認了說this的實現原理並非一個純粹的動態做用域。那他究竟是個什麼?
    function foo(){
     console.log(a)//2
    }
    function bar(){
     var a = 3;
     foo(); 
    }
    var a = 2;
    bar();複製代碼
相關文章
相關標籤/搜索