什麼是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
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...