什麼是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里扮演的重要角色。