1. 程式人生 > >【進階2-1期】深入淺出圖解作用域鏈和閉包

【進階2-1期】深入淺出圖解作用域鏈和閉包

這是我在公眾號(高階前端進階)看到的文章,現在做筆記 https://github.com/yygmind/blog/issues/17

紅寶書(p178)上對於閉包的定義:閉包是指有權訪問另外一個函式作用域中的變數的函式
關鍵在於下面兩點:

  • 是一個函式
  • 能訪問另外一個函式作用域中的變數

對於閉包有下面三個特性:

      • 1、閉包可以訪問當前函式以外的變數
function getOuter(){
  var date = '815';
  function getDate(str){
    console.log(str + date);  //訪問外部的date
  }
  return getDate('今天是:'); //"今天是:815"
}
getOuter();
  • 2、即使外部函式已經返回,閉包仍能訪問外部函式定義的變數
function getOuter(){
  var date = '815';
  function getDate(str){
    console.log(str + date);  //訪問外部的date
  }
  return getDate;     //外部函式返回
}
var today = getOuter();
today('今天是:');   //"今天是:815"
today('明天不是:');   //"明天不是:815"
  • 3、閉包可以更新外部變數的值
function updateCount(){
  var count = 0;
  function getCount(val){
    count = val;
    console.log(count);
  }
  return getCount;     //外部函式返回
}
var count = updateCount();
count(815); //815
count(816); //816

作用域鏈

Javascript中有一個執行上下文(execution context)的概念,它定義了變數或函式有權訪問的其它資料,決定了他們各自的行為。每個執行環境都有一個與之關聯的變數物件,環境中定義的所有變數和函式都儲存在這個物件中。

詳情檢視 【進階1-2期】JavaScript深入之執行上下文棧和變數物件

作用域鏈:當訪問一個變數時,直譯器會首先在當前作用域查詢標示符,如果沒有找到,就去父作用域找,直到找到該變數的標示符或者不在父作用域中,這就是作用域鏈。

作用域鏈和原型繼承查詢時的區別:如果去查詢一個普通物件的屬性,但是在當前物件和其原型中都找不到時,會返回undefined;但查詢的屬性在作用域鏈中不存在的話就會丟擲ReferenceError。

作用域鏈的頂端是全域性物件,在全域性環境中定義的變數就會繫結到全域性物件中。

全域性環境

無巢狀的函式
// my_script.js
"use strict";

var foo = 1;
var bar = 2;

function myFunc() {
  
  var a = 1;
  var b = 2;
  var foo = 3;
  console.log("inside myFunc");
  
}

console.log("outside");
myFunc();

定義時:當myFunc被定義的時候,myFunc的識別符號(identifier)就被加到了全域性物件中,這個識別符號所引用的是一個函式物件(myFunc function object)。

內部屬性[[scope]]指向當前的作用域物件,也就是函式的識別符號被建立的時候,我們所能夠直接訪問的那個作用域物件(即全域性物件)。

myFunc所引用的函式物件,其本身不僅僅含有函式的程式碼,並且還含有指向其被建立的時候的作用域物件。

呼叫時:當myFunc函式被呼叫的時候,一個新的作用域物件被建立了。新的作用域物件中包含myFunc函式所定義的本地變數,以及其引數(arguments)。這個新的作用域物件的父作用域物件就是在執行myFunc時能直接訪問的那個作用域物件(即全域性物件)。

有巢狀的函式

當函式返回沒有被引用的時候,就會被垃圾回收器回收。但是對於閉包,即使外部函式返回了,函式物件仍會引用它被建立時的作用域物件。

"use strict";
function createCounter(initial) {
  var counter = initial;
  
  function increment(value) {
    counter += value;
  }
  
  function get() {
    return counter;
  }
  
  return {
    increment: increment,
    get: get
  };
}

var myCounter = createCounter(100);
console.log(myCounter.get());   // 返回 100

myCounter.increment(5);
console.log(myCounter.get());   // 返回 105

當呼叫 createCounter(100) 時,內嵌函式increment和get都有指向createCounter(100) scope的引用。假設createCounter(100)沒有任何返回值,那麼createCounter(100) scope不再被引用,於是就可以被垃圾回收。

但是createCounter(100)實際上是有返回值的,並且返回值被儲存在了myCounter中,所以物件之間的引用關係如下圖:

即使createCounter(100)已經返回,但是其作用域仍在,並且只能被行內函數訪問。可以通過呼叫myCounter.increment() 或 myCounter.get()來直接訪問createCounter(100)的作用域。

當myCounter.increment() 或 myCounter.get()被呼叫時,新的作用域物件會被建立,並且該作用域物件的父作用域物件會是當前可以直接訪問的作用域物件。

呼叫get()時,當執行到return counter時,在get()所在的作用域並沒有找到對應的標示符,就會沿著作用域鏈往上找,直到找到變數counter,然後返回該變數。

單獨呼叫increment(5)時,引數value儲存在當前的作用域物件。當函式要訪問counter時,沒有找到,於是沿著作用域鏈向上查詢,在createCounter(100)的作用域找到了對應的標示符,increment()就會修改counter的值。除此之外,沒有其他方式來修改這個變數。閉包的強大也在於此,能夠存貯私有資料。

建立兩個函式:myCounter1myCounter2

//my_script.js
"use strict";
function createCounter(initial) {
  /* ... see the code from previous example ... */
}

//-- create counter objects
var myCounter1 = createCounter(100);
var myCounter2 = createCounter(200);

關係圖如下