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

5.1 理解閉包

閉包容許函數訪問並操做函數外部的變量,只要變量或函數存在於聲明函數時的做用域內,閉包就能夠訪問這些變量和函數javascript

//全局閉包 不明顯
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();
複製代碼

當在外部函數中聲明內部函數時,不只定義了函數的聲明,還建立了一個閉包java

該閉包不只包含了函數的聲明,還包含了 函數聲明時該做用域中的全部變量數組

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

閉包圖片

每個經過閉包訪問變量的函數 都具備一個做用域鏈,做用域鏈包含閉包的所有信息。閉包

:star: 閉包全部的信息都存儲在內存中,直到 JavaScript 引擎確保這些信息再也不使用或頁面卸載時,纔會清理。函數

5.2 使用閉包

封裝私有變量

私有變量 是對外部隱藏的對象屬性post

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");

//經過在函數內部定義變量,並基於閉包,使得在計時器的回調函數中能夠訪問這些變量,每一個動畫都可以得到屬於本身的"氣泡"中的私有變量
複製代碼

閉包內的函數 不只能夠在閉包建立時能夠訪問這些變量,並且能夠在閉包函數執行時,更改這些變量的值。ui

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

模擬三個閉包

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 中沒有真正的私有對象屬性,可是能夠經過閉包實現一種可接受的「私有」變量的方案

相關文章
相關標籤/搜索