什麼是JavaScript閉包?

来源:http://www.cnblogs.com/wwhhq/archive/2017/12/21/8078598.html
-Advertisement-
Play Games

什麼是JavaScript閉包? 本文轉載自: "眾成翻譯" 譯者: "Mcbai" 鏈接: "http://www.zcfy.cc/article/4639" 原文: "https://medium.freecodecamp.org/whats a javascript closure in pl ...


什麼是JavaScript閉包?

本文轉載自:眾成翻譯
譯者:Mcbai
鏈接:http://www.zcfy.cc/article/4639
原文:https://medium.freecodecamp.org/whats-a-javascript-closure-in-plain-english-please-6a1fc1d2ff1c

JavaScript閉包就如同汽車的功能——不同的位置都有對應那輛車的不同組件。

JavaScript中的每一個函數都構成一個閉包,這也是JavaScript最酷的特點之一。因為沒有閉包的話,實現像回調函數或者事件句柄這樣的公共結構就會很困難。

不管你什麼時候定義了一個函數,你都創建了一個閉包。然後當你執行這些函數時,他們的閉包能夠讓他們訪問他們作用域內的數據。

這有點像生產一輛帶有一些像start, accelerate, decelerate之類功能的汽車。司機每次操縱他們的車時執行這些功能。定義這些函數的閉包就像汽車一樣,並且‘閉合’了需要操作的變數。

讓我們拿accelerate函數做一個簡單的類比,當汽車被製造的時候,函數也就被定義了:

function accelerate(force) {
  // Is the car started?
  // Do we have fuel?
  // Are we in traction control mode?
  // Many other checks...
  // If all good, burn more fuel depending on 
  // the force variable (how hard we’re pressing the gas pedal)
}

每次司機踩下油門,這個方法就被執行。註意這個函數需要訪問很多變數才能執行,包括它自己的force變數。但是更重要的是,它需要自己作用域外被其它汽車功能控制的變數。這就是accelerate函數的閉包(我們從汽車本身獲得到的)的用處。

以下是accelerate函數的閉包對加速函數所作出的承諾:

> 好的accelerate,當你執行時,你可以訪問你的_force_變數,你可以訪問_isCarStarted_變數,也可以訪問_fuelLevel_變數和_isTractionControlOn_變數。 你也可以控制我們發送給引擎的_currentFuelSupply_變數。

請註意,閉包不會為這些變數賦予acceleration函數確切的值,而是允許在accelerate函數執行時訪問這些值。

閉包與函數作用域密切相關,因此理解這些作用域如何工作將有助於理解閉包。 簡而言之,瞭解作用域最重要的就是瞭解當你執行一個函數時,一個私有函數作用域被創建並用於執行該函數的過程。

然後當你內部函數開始執行函數時,這些函數作用域就會形成嵌套。

當你定義一個函數時就創建了一個閉包,而不是當你執行它的時候。然後,每當你執行這個函數,其已經定義的閉包使它可以訪問所有對它可用的函數作用域。

在某種程度上,你可以認為作用域是臨時的(全局作用域除外),而把閉包是永久的。

一個chrome調試工具展示的閉包。

想要真正瞭解閉包好它在JavaScript里扮演的角色,你首先需要明白幾個簡單的JavaScript函數和作用域的概念。

在我們開始之前,註意我已經創建了一個交互實驗,你可以在這裡查看。

1 — 按引用分配函數

當你把一個函數賦值給一個變數,就像這樣:

function sayHello() {
  console.log("hello");
};

var func = sayHello;

你正在給變數func賦予一個sayHello的引用,而不是複製。這使得func僅僅是sayHello的一個別名,你在這個別名上做的任何事,其實都是在原來的函數上操作的。比如:

func.answer = 42;

console.log(sayHello.answer); // prints 42

屬性的answer是直接在func上設置的,然後使用sayHello進行讀取,這依然是有效的。

你還可以通過執行func別名來執行sayHello:

func() // prints "hello"

2 — 作用域有生命周期

當你調用一個函數時,在執行該函數期間創建一個作用域,函數執行完畢,作用域消失。

當你第二次調用該函數時,在第二個執行期間創建一個新的不同的作用域,當函數執行完畢,第二個作用域也隨之消失。

function printA() {
  console.log(answer);
  var answer = 1;
};
printA(); // this creates a scope which gets discarded right after
printA(); // this creates a new different scope which also gets discarded right after;

在上面的示例中創建的這兩個作用域是不同的。這裡的變數answer在它們兩個之間完全是不共用的。

每個函數作用域都有一個生命周期。它們會被創建出來,然後又立刻被丟棄。惟一的例外是全局作用域,只要應用程式在運行,它就不會消失。

3 — 閉包跨越多個作用域

當你定義一個函數,也就創建了一個閉包

和作用域不同,閉包是當你定義一個函數時創建的,而不是你執行函數的時候。閉包在你執行完函數後也不會消失。

在定義了一個函數很久以後,你依然可以訪問閉包里的數據,即使它執行了也是一樣。

一個閉包包含所有定義好的函數可以訪問的書。這意味著定義函數的作用域,全局作用域和定義函數作用域之間嵌套的作用域,以及全局作用域本身。

var G = 'G';

// Define a function and create a closure
function functionA() {
  var A = 'A'

  // Define a function and create a closure
  function functionB() {
    var B = 'B'
    console.log(A, B, G);
  }

  functionB();  // prints A, B, G

  // functionB closure does not get discarded
  A = 42;
  functionB();  // prints 42, B, G
}

functionA();

當我們定義一個functionB所創建的閉包,允許我們訪問functionB的作用域,functionA的作用域以及全局作用域。

每次我們執行functionB,我們都可以通過先前創建好的閉包訪問變數B, A, 和 G。然而,閉包並不是複製了這些變數,而是引用它們。

例如,functionB的閉包被創建之後,變數A的值會在某些時候發生變化,當我們執行functionB之後,我們會看到新的值,而不是舊的值。functionB的第二個調用列印42、B、G,因為變數A的值被更改為42,閉包給我們提供了一個引用,而不是一個副本。

不要將閉包和作用域混淆

把閉包與作用域混淆是很常見的,所以讓我們確保不要這樣做。

// scope: global
var a = 1;
void function one() {
  // scope: one
  // closure: [one, global]
  var b = 2;

  void function two() {
    // scope: two
    // closure: [two, one, global]
    var c = 3;

    void function three() {
      // scope: three
      // closure: [three, two, one, global]
      var d = 4;
      console.log(a + b + c + d); // prints 10
    }();
  }();
}();

在上面的簡單例子中,我們定義並立即調用了三個函數,所以他們都創建了作用域和閉包。

函數one()的作用域就是它自己,它的閉包讓我們有訪問它和全局作用域的權利。

函數two()的作用域就是它自己,它的閉包讓我們有訪問它和函數one(),還有全局作用域的權利。

同樣,函數three()的閉包給我們訪問所有作用域的權力。這就是為什麼我們可以在函數three()中訪問所有變數的原因。

但是作用域和閉包的關係不總是如此。在不同作用域里定義和調用函數時,情況又會變得不一樣。讓我通過一個例子來解釋:

var v = 1;

var f1 = function () {
  console.log(v);
}

var f2 = function() {
  var v = 2;
  f1(); // Will this print 1 or 2?
};

f2();

你認為上面的例子中會列印1還是2?代碼很簡單,函數f1()列印v的值,是全局作用域的1。但是我們在有不同的值等於2的v的函數f2()里執行f1(),然後再執行f2()

這段代碼將會列印1還是2?

如果你想說2,那麼你將會感到驚訝,這段代碼實際上會列印1。原因是作用域和閉包並不相同。console.log方法會使用當我們定義f1()時所創建的f1()閉包,這意味著f1()的閉包值允許我們訪問f1()和全局的作用域。

我們執行f1()的地方的作用域並不會影響閉包。實際上,f1()的閉包並不會給我們訪問函數f2()作用域的權力。如果你刪除全局變數v,然後執行這段代碼,你將會得到錯誤消息:

var f1 = function () {
  console.log(v);
}

var f2 = function() {
  var v = 2;
  f1(); // ReferenceError: v is not defined
};

f2();

這對理解和記憶非常重要。

4 — 閉包有讀和寫的許可權

由於閉包給我們提供了在作用域中的變數的引用,所以意味著它們給我們的許可權包括讀和寫,而且不僅僅是讀。

看看這個例子:

function outer() {
  let a = 42;

  function inner() {
    a = 43;
  }

  inner();
  console.log(a);
}

outer();

我們定義了一個inner()函數,創建了一個可以讓我們訪問變數a的閉包。我們可以讀寫這個變數,並且如我們我們真的改變了它的值,我們會改變outer()作用域里變數a的值。

這段代碼會列印43,因為我們用inner()函數的閉包改變了outer()函數的變數

這就是為什麼我們可以在任何地方改變全局變數。所有閉包都給我們提供了對所有全局變數的讀寫許可權。

5 — 閉包可以分享作用域

因為在定義函數時,閉包就給我們訪問嵌套作用域的權力,所以當我們在同一個作用域中定義多個函數時,這個作用域就被其中的閉包共用。由於這個原因,全局作用域總是被所有閉包共用。

function parent() {
  let a = 10;

  function double() {
    a = a+a;
   console.log(a);
  };

  function square() {
    a = a*a;
   console.log(a);
  }

  return { double, square }
}

let { double, square } = parent();

double(); // prints 20
square(); // prints 400
double(); // prints 800

在上面的例子中,我們有一個設置變數a的值為10的函數parent(),我們在函數parent()的作用域里定義了兩個函數,double()square()。定義函數double()square()時所創建的閉包共用函數double() 的作用域。

因為double()square()都會改變變數a,當我們執行最後3行代碼時,我們先把a相加(讓a = 20),然後把相加後的值相乘(讓a = 400),然後把相乘後的值相加(讓a = 800)。

最後一個測試

讓我們來測試到目前為止你對閉包的理解。在你執行下麵的代碼之前,先猜猜它會列印什麼:

let a = 1;

const function1 = function() {
  console.log(a);
  a = 2
}

a = 3;

const function2 = function() {
  console.log(a);
}

function1();
function2();

我希望得到正確答案並且希望這個簡單的概念能幫你正真理解函數閉包在JavaScript里扮演的重要角色。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在Excel中如果能夠將具有多級明細的數據進行分組顯示,可以清晰地展示數據表格的整體結構,使整個文檔具有一定層次感。根據需要設置顯示或者隱藏分類數據下的詳細信息,在便於數據查看、管理的同時也使文檔更具美觀性。那麼,在C#中如何來創建Excel數據的多級分組顯示呢?下麵將進行詳細闡述。方法中使用了免費 ...
  • Highcharts .NET allows developers to make charts using Highcharts API with the Microsoft .NET Framework (ASP.NET MVC 4+). ...
  • 個人資料庫幫助類:提供兩種訪問方式OleDb(需安裝Oracle客戶端)和 Oracle.ManagedDataAccess.Client(需Oracle.ManagedDataAccess.dll) 多說無益上代碼: #region Info/* ************************* ...
  • 在工作中經常使用WinForm來承載多個WCF服務,由於底層代碼無權查看,故自己研究寫了一個類實現同樣的功能並加入對單個WCS服務的控制,能夠選擇其中的一個會多個服務進行啟動,停止的控制,並且支持雙工通訊模式,廢話不多說直接上代碼: namespace Gaofajin.Net{ public cl ...
  • 生活工作中經常通過遠程式控制制某臺電腦,但是有些時候,我們想遠程某個電腦卻發現,電腦被斷電或者關機了,此時必須的跑去開個機; 由各種百度,博客發現可以通過幻包遠程進行喚醒:其電腦設置步驟此處省略,百度搜索遠程喚醒即可找到,當然網上也有很多工具可以遠程喚醒,由於樓主是學C#故發一下 遠程喚醒的C#代碼實現 ...
  • C#中批量處理數據,有時候因為一條記錄導致整個批量處理失敗。這時候肯能會導致數據不全等問題,這時候我們可以使用SqlTransaction來進行事務回滾,即是要麼全部成功要麼全部不成功。如下代碼 上面測試代碼,INSERT into t_student VALUES ('huage1','11',' ...
  • 我昨天寫了好多篇跟mysql有關的錯誤,但是在今天我發現好像所有的問題都是一個原因引起的: 版本問題,我原先使用的是MySql.Data.Entity.EF6(版本號:6.10.5),今天我換成6.9.10版本後上述的問題就有很少發生了。 建議大家,如果按照我上述的方案無法解決自己的問題,那麼大家把 ...
  • 本文轉自:https://stackoverflow.com/questions/44202478/lost-parameter-value-during-sql-trace-in-ef-core 問: I have implemented an approach for tracing SQL q ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...