【JavaScript】(附面試題)深刻理解做用域、做用域鏈和閉包

引言

JavaScript中有做用域、做用域鏈和閉包。咱們最開始可能以爲知道這些的定義就算懂了(剛入門時的我也是這樣),可是當深刻了解的時候,發現本身知道的只是皮毛。因此,這篇文章將詳細講解做用域、做用域鏈和閉包。html

咱們先借助一道題,瞭解一下做用域、做用域鏈和閉包的造成過程~前端

let x = 1;
function A(y){
   let x = 2;
   function B(z){
       console.log(x+y+z);
   }
   return B;
}
let C = A(2);
C(3);
複製代碼

對於上面的這張解答圖,有以下解釋:web

  • 當建立一個函數時,會建立一個堆,同時初始化當前函數做用域,做用域([[Scope]])爲所在上下文中的變量對象VO/AO面試

  • 當執行一個函數時,會建立新的執行上下文 -> 初始化this指向 -> 初始化做用域鏈([[ScopeChain]]) -> 建立AO變量對象存儲變量bash

  • EC(A)執行上下文中,堆被全局變量C所佔用,不能出棧銷燬,此時就造成了閉包。閉包

  • 圖裏面紅線所表明的就是做用域鏈,在一個做用域中,它所須要的變量在當前做用域中沒有,就會一層一層向上查找。函數

這樣簡單的一個題,引出了做用域、做用域鏈、閉包的概念,下面本篇文章將正式對它們進行講解。學習

1、做用域

做用域([[Scope]])就是變量與函數的可訪問範圍,即做用域控制着變量與函數的可見性和生命週期。ui

1.1 詞法做用域與動態做用域

1.1.1 詞法做用域

詞法做用域也是靜態做用域,在JavaScript中採用的就是詞法做用域。詞法做用域就是定義在詞法階段的做用域。換句話說,詞法做用域是由你在寫代碼時將變量和塊做用域寫在哪裏決定的。所以當詞法分析器處理代碼時會保持做用域不變(大部分狀況是這樣)this

下面一個例子幫助理解:

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar();
複製代碼

假如上述例子採用了詞法做用域,那麼它的執行過程就是:

首先執行bar()函數,在bar()函數中執行foo()函數,foo()函數中輸出value的值。它首先會查找當前做用域中是否有value,若是沒有,則會向外一層查找,則最後輸出了1

1.1.2 動態做用域

動態做用域便是與詞法做用域相反的。咱們仍是以上面的例子爲例:

假如上述例子採用動態做用域:

它依然會像採用詞法做用域的形式執行函數,惟一不同的地方在於:在執行foo()函數時,他不會向外一層查找value,而是從調用的函數做用域中查找,因此最後的結果輸出爲2

1.2 全局做用域

在代碼任何地方都能訪問到的對象擁有全局做用域,通常擁有全局做用域有如下三種情形

1.2.1 最外層函數和在最外層函數外面定義的變量

var globalValue = `global value`;

function checkGlobal() {
    var localValue = `local value`;
    console.log(localValue);                // "local value"
    console.log(globalValue);               // "global value"
}

console.log(globalValue);                   // "global value"
console.log(checkGlobal);                   // "global value"
console.log(localValue);                    // "Uncaught ReferenceError: localValue is not defined"
複製代碼

在上面的例子中,globalValue就是一個全局變量,不管在哪都能訪問,而localValue是一個局部變量,只能在函數內部訪問

1.2.2 全部未定義直接賦值的變量

function checkGlobal() {
    var localValue = 'local value';
    globalValue = 'global value';
    console.log(localValue, globalValue);    // 'local value' 'globalValue'
}

console.log(globalValue);                    // 'globalValue'
console.log(localValue);                     // "Uncaught ReferenceError: localValue is not defined"
複製代碼

1.2.3 全部window對象的屬性

1.3 函數做用域

函數做用域,就是指聲明在函數內部的變量,它正好和全局做用域相反。內層做用域能夠訪問到外層做用域,而外層做用域不能訪問到內層做用域。

function check() {
    var localValue = 'local value';
    console.log(localValue);          // 'local value'
}
 
console.log(localValue);              // "Uncaught ReferenceError: localValue is not defined"
複製代碼

1.4 塊級做用域

塊級做用域可經過letconst聲明,聲明後的變量再指定塊級做用域外沒法被訪問。

1.4.1 塊級做用域被建立的狀況

  • 在一個函數內部

  • 在一個代碼塊內部

1.4.2 塊級做用域的特色

  • 聲明的變量不會提高到代碼塊頂部

let或者const聲明的變量不會被提高到當前做用域頂部。

function check(bool) {
    if(bool) {
        let result = 1;
        console.log(result);
    }
    console.log(result);
}

check(true);   // 1 Uncaught ReferenceError: result is not defined
check(false);  // Uncaught ReferenceError: result is not defined
複製代碼

若是想要訪問到result,須要本身手動將變量提高到當前做用域頂部。像這樣

function check(bool) {
    let result = null;
    if(bool){
        result = 1
    }
    console.log(result);
}
....
複製代碼
  • 禁止重複聲明

在同層級的做用域內,已經聲明過的變量,不能夠再次聲明。

// 1.同層級做用域
var bool = true;
let bool = false;    // Uncaught SyntaxError: Identifier 'bool' has already been declared

// 不一樣層級做用域
var bool = true;
function check() {
    let bool = false;   // 這裏不會報錯
    // .....
} 

複製代碼
  • 在循環中的使用

for循環中聲明的變量僅在循環內部使用。

for(let i = 0; i < 1; i++) {
    console.log(i);    // 0
}
console.log(i);        // Uncaught ReferenceError: i is not defined
複製代碼

可是循環內部又是一個單獨的做用域

for(let i = 0; i < 2; i++) {
    let i = 'hello';
    console.log(i);    // 'hello' 'hello' 
}
複製代碼

2、做用域鏈

2.1 做用域鏈的定義及造成

當所須要的變量在所在的做用域中查找不到的時候,它會一層一層向上查找,直到找到全局做用域尚未找到的時候,就會放棄查找。這種一層一層的關係,就是做用域鏈。

var a = 1;

function check() {
    return function() {
        console.log(a); // 當前做用域內找不到a,會向上一層一層查找,最後找到了全局下的a,輸出結果爲1
        console.log(b); // 同理,因此輸出"Uncaught ReferenceError: b is not defined"
    }
}

var func = check();   // 此時返回匿名函數
func();               // 執行匿名函數
複製代碼

3、閉包

3.1 閉包的定義

當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包。即便函數是在當前詞法做用域以外執行。

3.2 閉包的造成

咱們來看一段代碼

function foo() {
    var a = 1;
    return function() {
        console.log(a);
    }
}

var bar = foo();
bar();
複製代碼

foo()函數的執行結果返回給bar,而此時因爲變量a還在使用,於是沒有被銷燬,而後執行bar()函數。這樣,咱們就能在外部做用域訪問到函數內部做用域的變量。這個就是閉包。

閉包的造成條件:

  • 函數嵌套

  • 內部函數引用外部函數的局部變量

3.3 閉包的做用

  • 能夠讀取函數內部的變量

  • 可使變量的值長期保存在內存中,生命週期比較長。

  • 可用來實現JS模塊(JQuery庫等)

JS模塊是具備特定功能的JS文件,將全部的數據和功能都封裝在一個函數內部(私有的),只向外暴露一個包含多個方法的對象或函數,模塊的使用者,只須要經過模塊暴露的對象調用方法來實現對應的功能。

(function() {
    var a = 1;
    
    function test() {
        return a;
    }
    
    window.module = {a, test};  // 向外暴露
})()
複製代碼
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="./1.js"> </script>
    <title>Document</title>
</head>
<body>
    <script>
        console.log(module.a);          // 1
        console.log(module.test());     // 1
    </script>
</body>
</html>
複製代碼

3.4 閉包的特性

  • 每一個函數都是閉包,函數可以記住本身定義時所處的做用域,函數走到了哪,定義時的做用域就到了哪。

  • 內存泄漏

內存泄漏就是一個對象在你不須要它的時候仍然存在。因此不能濫用閉包。當咱們使用完閉包後,應該將引用變量置爲null

function outer(){
    var num = 0;
    return function add(){
        num++;
        console.log(num);
    };
 }
var func1 = outer();
func1();              // 1
func1();              // 2  [沒有被釋放,一直被佔用]
var func2 = outer();
func2();              // 1  [從新引用函數時,閉包是新的]
func2();              // 2
複製代碼

3.5 閉包的應用

如今要求實現點擊第幾個button就輸出幾

先寫一個html

<button>1</button>
<button>2</button>
複製代碼

再來寫JS

let buttons = document.getElementsByTagName('button');

for(var i = 0; i < buttons.length; i++) {
    buttons[i].onclick = function() {
        console.log(i + 1);
    }
}
複製代碼

第一次咱們可能會寫出這樣的代碼,可是咱們會發現,這個代碼存在問題,不管我點第幾個按鈕,都會輸出3。這是由於此時的i是全局變量,當執行點擊事件時,i已經變成了3

咱們能夠用兩種方式解決這個問題

  • let聲明

將上述代碼中的var改成let

  • 閉包
for(var i = 0; i < buttons.length; i++) {
    (function(k){
        buttons[k].onclick = function() {
            console.log(k + 1);
        }
    })(i)
}
複製代碼

面試題

let x = 5;
function fn(x) {
    return function(y) {
        console.log(y + (++x));
    }
}
let f = fn(6);
f(7);   
console.log(x);
複製代碼

總結

本篇文章主要講解了關於做用域、做用域鏈和閉包的知識,若是以爲對你有幫助,能夠給本篇文章點個贊呀~若是有哪裏不對的地方,還請你們指出來,咱們共同窗習、共同進步~

最後,分享一下個人公衆號「web前端日記」,歡迎你們前來關注~

相關文章
相關標籤/搜索