javascript從範圍鏈談閉包_javascript技巧

來源:互聯網
上載者:User

神馬是閉包
關於閉包的概念,是婆說婆有理。

閉包是指有權訪問另外一個函數範圍中的變數的函數
這概念有點繞,拆分一下。從概念上說,閉包有兩個特點:

  • 1、函數
  • 2、能訪問另外一個函數範圍中的變數

在ES 6之前,Javascript只有函數範圍的概念,沒有塊級範圍(但catch捕獲的異常 只能在catch塊中訪問)的概念(IIFE可以建立局部範圍)。每個函數範圍都是封閉的,即外部是訪問不到函數範圍中的變數。

function getName() { var name = "美女的名字"; console.log(name);  //"美女的名字"}function displayName() { console.log(name); //報錯}

但是為了得到美女的名字,不死心的單身汪把代碼改成了這樣:

function getName() { var name = "美女的名字"; function displayName() { console.log(name);  } return displayName;}var 美女 = getName(); 美女() //"美女的名字"

這下,美女是一個閉包了,單身汪想怎麼玩就怎麼玩了。(但並不推薦單身汪用中文做變數名的寫法,大家不要學)。

關於閉包呢,還想再說三點:
1、閉包可以訪問當前函數以外的變數

function getOuter(){ var date = '815'; function getDate(str){ console.log(str + date); //訪問外部的date } return getDate('今天是:'); //"今天是:815"}getOuter();

getDate是一個閉包,該函數執行時,會形成一個範圍A,A中並沒有定義變數date,但它能在父一級範圍中找到該變數的定義。

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); //815count(816); //816

範圍鏈
為毛閉包就能訪問外部函數的變數呢?這就要說說Javascript中的範圍鏈了。
Javascript中有一個執行環境(execution context)的概念,它定義了變數或函數有權訪問的其它資料,決定了他們各自的行為。每個執行環境都有一個與之關聯的變數對象,環境中定義的所有變數和函數都儲存在這個對象中。你可以把它當做Javascript的一個普通對象,但是你只能修改它的屬性,卻不能引用它。

變數對象也是有父範圍的。當訪問一個變數時,解譯器會首先在當前範圍尋找標示符,如果沒有找到,就去父範圍找,直到找到該變數的標示符或者不再存在父範圍了,這就是範圍鏈。

範圍鏈和原型繼承有點類似,但又有點小區別:如果去尋找一個普通對象的屬性時,在當前對象和其原型中都找不到時,會返回undefined;但尋找的屬性在範圍鏈中不存在的話就會拋出ReferenceError。

範圍鏈的頂端是全域對象。對於全域環境中的代碼,範圍鏈只包含一個元素:全域對象。所以,在全域環境中定義變數的時候,它們就會被定義到全域對象中。當函數被調用的時候,範圍鏈就會包含多個範圍對象。

  • 全域環境

關於範圍鏈講得略多(紅皮書上有關於範圍及執行環境的詳細解釋),看一個簡單地例子:

// my_script.js"use strict";var foo = 1;var bar = 2;

在全域環境中,建立了兩個簡單地變數。如前面所說,此時變數對象是全域對象。

  • Non-nested functions

改動一下代碼,建立一個沒有函數嵌套的函數:

"use strict";var foo = 1;var bar = 2;function myFunc() { //-- define local-to-function variables var a = 1; var b = 2; var foo = 3; console.log("inside myFunc");}console.log("outside");//-- and then, call it:myFunc();

當myFunc被定義的時候,myFunc的標識符(identifier)就被加到了當前的範圍對象中(在這裡就是全域對象),並且這個標識符所引用的是一個函數對象(function object)。函數對象中所包含的是函數的原始碼以及其他的屬性。其中一個我們所關心的屬性就是內部屬性[[scope]]。[[scope]]所指向的就是當前的範圍對象。也就是指的就是函數的標識符被建立的時候,我們所能夠直接存取的那個範圍對象(在這裡就是全域對象)。

比較重要的一點是:myFunc所引用的函數對象,其本身不僅僅含有函數的代碼,並且還含有指向其被建立的時候的範圍對象。

當myFunc函數被調用的時候,一個新的範圍對象被建立了。新的範圍對象中包含myFunc函數所定義的本地變數,以及其參數(arguments)。這個新的範圍對象的父範圍對象就是在運行myFunc時我們所能直接存取的那個範圍對象。

  • Nested functions

如前面所說,當函數返回沒有被引用的時候,就會被記憶體回收行程回收。但是對於閉包(函數嵌套是形成閉包的一種簡單方式)呢,即使外部函數返回了,函數對象仍會引用它被建立時的範圍對象。

"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()); // 返回 100myCounter.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()被調用時,新的範圍對象會被建立,並且該範圍對象的父範圍對象會是當前可以直接存取的範圍對象。

當執行到return counter;時,在get()所在的範圍並沒有找到對應的標示符,就會沿著範圍鏈往上找,直到找到變數counter,然後返回該變數,調用increment(5)則會更有意思。當單獨調用increment(5)時,參數value會存貯在當前的範圍對象。函數要訪問value,能馬上在當前範圍找到該變數。但是當函數要訪問counter時,並沒有找到,於是沿著範圍鏈向上尋找,在createCounter(100)的範圍找到了對應的標示符,increment()就會修改counter的值。除此之外,沒有其他方式來修改這個變數。閉包的強大也在於此,能夠存貯私人資料。

Similar function objects, different scope objects
對於上面的counter樣本,再說點擴充的事。看代碼:

//myScript.js"use strict";function createCounter(initial) { /* ... see the code from previous example ... */}//-- create counter objectsvar myCounter1 = createCounter(100);var myCounter2 = createCounter(200);

myCounter1 和 myCounter2建立之後,關係圖是醬紫的:

在上面的例子中,myCounter1.increment和myCounter2.increment的函數對象擁有著一樣的代碼以及一樣的屬性值(name,length等等),但是它們的[[scope]]指向的是不一樣的範圍對象。

這才有了下面的結果:

var a, b;a = myCounter1.get(); // a 等於 100b = myCounter2.get(); // b 等於 200myCounter1.increment(1);myCounter1.increment(2);myCounter2.increment(5);a = myCounter1.get(); // a 等於 103b = myCounter2.get(); // b 等於 205

範圍和this
範圍會儲存變數,但this並不是範圍的一部分,它取決於函數調用時的方式。關於this指向的總結,可以看這篇文章:JavaScript面試問題:事件委託和this

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.