javascript忍者祕籍-第五章 閉包和作用域

smiler2018 發表於 2018-12-04

5.1 理解閉包

閉包允許函式訪問並操作函式外部的變數,只要變數或函式存在於宣告函式時的作用域內,閉包就可以訪問這些變數和函式

//全域性閉包 不明顯
var outerValue = "ninja";
function outerFunction(){
    outerValue === "ninja";  //true
}
outerFunction();
複製程式碼
//閉包例子
var outerValue = "samurai";
var later;

function outerFunction(){
    var innerValue = "ninja";
    
    function innerFunction(){
        outerValue === 'samurai';  //true
        innerValue === 'ninja';    //true
    }
    later = innerFunction();
}
outerFunction();
later();
複製程式碼

當在外部函式中宣告內部函式時,不僅定義了函式的宣告,還建立了一個閉包

該閉包不僅包含了函式的宣告,還包含了 函式宣告時該作用域中的所有變數

閉包建立了被定義時的作用域內的變數和函式的安全氣泡

閉包圖片

每一個通過閉包訪問變數的函式 都具有一個作用域鏈,作用域鏈包含閉包的全部資訊。

:star: 閉包所有的資訊都儲存在記憶體中,直到 JavaScript 引擎確保這些資訊不再使用或頁面解除安裝時,才會清理。

5.2 使用閉包

封裝私有變數

私有變數 是對外部隱藏的物件屬性

function Ninja(){
    var feints = 0;
    this.getFeints = function(){
        return feints;
    }
    this.feint = function(){
        feints++;
    }
}

var ninja1 = new Ninja();
ninja1.feint();

ninja1.feints  //無法直接從外部訪問屬性
ninja1.getFeints() == 1  //true 可以通過方法來訪問

var ninja2 = new Ninja();  //建立一個新的物件例項,新物件例項作為上下文 this指向新的物件例項
ninja2.getFeints() == 0  //true 新例項有自己的私有變數
複製程式碼

:zap: 在構造器中隱藏變數,使其在外部作用域中不可訪問,但是可在閉包內部進行訪問

  • 通過變數 ninja , 物件例項是可見的
  • 因為 feint 方法在閉包內部,因此可以訪問變數 feints
  • 在閉包外部,無法訪問變數 feints

閉包

回撥函式

//在 interval 的回撥函式中使用閉包
//回撥函式中 this 指向變了
function animateIt(elementId){
    var elem = document.getElementById(elementId);
    var tick = 0;
    var timer = setInterval(function(){
        if(tick < 100){
            elem.style.left = elem.style.top = tick + 'px';
            tick++;
        }else{
            clearInterval(timer);
            tick === 100;  //true
        }
    },10);
}
animateIt("box1");

//通過在函式內部定義變數,並基於閉包,使得在計時器的回撥函式中可以訪問這些變數,每個動畫都能夠獲得屬於自己的"氣泡"中的私有變數
複製程式碼

閉包內的函式 不僅可以在閉包建立時可以訪問這些變數,而且可以在閉包函式執行時,更改這些變數的值。

閉包不是在建立的那一時刻的快照,而是一個真實的狀態封裝,只要閉包存在,就可以對變數進行修改。

模擬三個閉包

5.3 執行上下文跟蹤程式碼

JavaScript 有兩種型別的程式碼:一種是全域性程式碼,一種是函式程式碼

上下文分為兩種:全域性執行上下文和函式執行上下文

全域性執行上下文只有一個,當 JavaScript 程式開始執行時就已經建立了全域性上下文;而函式執行上下文在每次 呼叫 函式時,就會建立一個新的。

JavaScript 是單執行緒的:一旦發生函式呼叫,當前的執行上下文必須停止執行,並建立新的函式執行上下文來執行函式。當函式執行完成後,將函式執行上下文銷燬,並重新回到發生呼叫時的執行上下文中。

?的活塞

//建立執行上下文
function skulk(ninja){
    report(ninja + " skulking");
}
function report(message){
    console.log(message);
}
skulk("Kuma");
skulk("Yoshi");
複製程式碼

image-20181101114022478

可以通過 JavaScript 偵錯程式中檢視,在 JavaScript 偵錯程式中可以看到對應的呼叫棧(call stack)。

呼叫棧

5.4 使用詞法環境跟蹤變數的作用域

詞法環境 是 JavaScript 引擎內部用來跟蹤識別符號與特定變數之間的對映關係

詞法環境 是 JavaScript 作用域的內部實現機制,稱為 作用域 (scopes)

程式碼巢狀

詞法環境主要基於程式碼巢狀,通過程式碼巢狀可以實現程式碼結構包含另一程式碼結構

在作用域範圍內,每次執行程式碼時,程式碼結構都獲得與之關聯的詞法環境。

內部程式碼結構可以訪問外部程式碼結構中定義的變數。

程式碼巢狀與詞法環境

每個執行上下文都有一個與之關聯的詞法環境,詞法環境中包含了在上下文中定義的識別符號對映表。=> 作用域鏈

在特定的執行上下文中,我們的程式不僅直接訪問詞法環境中定義的區域性變數,而且還會訪問外部環境中定義的變數。

無論何時建立函式,都會建立一個與之相關聯的詞法環境,並儲存在名為 [[Environment]] 的內部屬性上。兩個中括號表示是內部屬性。

函式都有詞法環境

var ninja = "Muneyoshi";

function skulk(){
    var action = "Skulking";
    
    function report(){
        var intro = "Aha!";
        assert(intro === "Aha!","Local");
        assert(action === "Skulking","Outer");
        assert(ninja === "Muneyoshi","Global");
    }
    report();
}
skulk();
複製程式碼

詞法環境

:question: 為什麼不直接跟蹤整個執行上下文搜尋識別符號,而是通過詞法環境來跟蹤呢?

JavaScript 函式可以作為任意物件進行傳遞,定義函式時的環境與呼叫函式的環境往往是不同的(閉包)

:zap: 無論何時呼叫函式,都會建立一個新的執行環境,被推入執行上下文棧。此外,還會建立一個與之關聯的詞法環境。外部環境與新建的詞法環境,JavaScript 引擎將呼叫函式的內建[[Environment]]屬性與建立時的環境進行關聯。

5.5 理解 JavaScript 的變數型別

3個關鍵字定義變數:var let const

不同之處:可變性、詞法環境

vs 不可變

const 不可變,let var 可變

宣告時需要初始化,一旦宣告完成之後,其值不可更改。 => 指向不可更改

const firstConst = "samurai";
firstConst = "ninja";  //報錯

const secondConst = {};
secondConst.weapon = "wakizashi";  //不報錯

const thirdConst = [];
thirdConst.push("Yoshi");  //不報錯
複製程式碼

如果 const 的值是 靜態變數,則不允許重新賦值;如果 const 的值是物件或者是陣列型別,則可以對其增加新元素,但是不能重寫。其實不可變的是引用,而不是值。

vs 詞法環境

var 一組,let 和 const 一組

關鍵字 var :變數是在距離最近的 函式內部 或是在 全域性詞法環境 中定義的。忽略塊級作用域

var 宣告的變數實際上總是在 距離最近的函式內 或 全域性詞法環境中 註冊的,不關注塊級作用域

let 與 const 直接在最近的詞法環境中定義變數(可以是塊級作用域內迴圈內函式內或全域性環境內)。我們可以用 let 和 const 定義 塊級別、函式級別、全域性級別的變數。

var三種詞法環境

let const詞法環境

詞法環境註冊識別符號

詞法作用域又叫靜態作用域,因為js的作用域在詞法解析階段就確定了

動態作用域:區別於靜態作用域,即在函式呼叫時才決定作用域

JavaScript 程式碼的執行 分兩個階段:

一旦建立了新的詞法環境,就會執行第一階段。在第一階段,沒有執行程式碼,而是JavaScript引擎會訪問並註冊在當前詞法環境中所宣告的變數和函式。變數和函式宣告提升

第二階段的執行取決於變數的型別(let var const 函式宣告)以及環境型別(全域性環境、函式環境或塊級作用域)

1.如果是建立一個函式環境,那麼建立形參及函式引數的預設值。如果是非函式環境,將跳過此步驟。

2.如果是建立全域性或函式環境,就掃描當前程式碼進行函式宣告(不會掃描其他函式的函式體),但是不會掃描函式表示式或箭頭函式。對於所找到的函式宣告,將建立函式,並繫結到當前環境與函式名相同的識別符號上。若該識別符號已經存在,那麼該識別符號的值將被重寫。如果是塊級作用域,將跳過此步驟。

3.掃描當前程式碼進行變數宣告。在函式或全域性環境中,找到所有當前函式以及其他函式之外通過 var 宣告的變數,並找到所有在其他函式或程式碼塊之外通過 let 或 const 定義的變數。在塊級環境中,僅查詢當前塊中通過 let 或 const 定義的變數。對於所查詢到的變數,若該識別符號不存在,進行註冊並將其初始化為 undefined。若該識別符號已經存在,將保留其值。

註冊識別符號的過程

若函式是函式宣告進行定義的,則可以在函式宣告之前訪問函式。

若函式是函式表示式或箭頭函式進行定義的,則不會在之前訪問到函式


變數的宣告會提升至函式頂部,函式的宣告會提升至全域性程式碼頂部。

其實,變數和函式的宣告並沒有實際發生移動,只是在程式碼執行之前,先在詞法環境中進行註冊。

5.6 閉包的工作原理

閉包可以訪問建立函式時所在作用域內的全部變數

//通過函式訪問私有變數,而不通過物件訪問
function Ninja(){
    var feints = 0;
    this.getFeints = function(){
        return feints;
    }
    this.feint = function(){
        feints++;
    };
}
var ninja1 = new Ninja();
ninja1.feint();

var imposter = {};
imposter.getFeints = ninja1.getFeints;
imposter.getFeints() == 1   //true
複製程式碼

JavaScript 中沒有真正的私有物件屬性,但是可以通過閉包實現一種可接受的“私有”變數的方案