在JavaScript
中有做用域、做用域鏈和閉包。咱們最開始可能以爲知道這些的定義就算懂了(剛入門時的我也是這樣),可是當深刻了解的時候,發現本身知道的只是皮毛。因此,這篇文章將詳細講解做用域、做用域鏈和閉包。html
咱們先借助一道題,瞭解一下做用域、做用域鏈和閉包的造成過程~前端
let x = 1;
function A(y){
let x = 2;
function B(z){
console.log(x+y+z);
}
return B;
}
let C = A(2);
C(3);
複製代碼
對於上面的這張解答圖,有以下解釋:web
當建立一個函數時,會建立一個堆,同時初始化當前函數做用域,做用域([[Scope]
])爲所在上下文中的變量對象VO/AO
。面試
當執行一個函數時,會建立新的執行上下文 -> 初始化this
指向 -> 初始化做用域鏈([[ScopeChain]]
) -> 建立AO
變量對象存儲變量bash
在EC(A)
執行上下文中,堆被全局變量C
所佔用,不能出棧銷燬,此時就造成了閉包。閉包
圖裏面紅線所表明的就是做用域鏈,在一個做用域中,它所須要的變量在當前做用域中沒有,就會一層一層向上查找。函數
這樣簡單的一個題,引出了做用域、做用域鏈、閉包的概念,下面本篇文章將正式對它們進行講解。學習
做用域([[Scope]
])就是變量與函數的可訪問範圍,即做用域控制着變量與函數的可見性和生命週期。ui
詞法做用域也是靜態做用域,在JavaScript
中採用的就是詞法做用域。詞法做用域就是定義在詞法階段的做用域。換句話說,詞法做用域是由你在寫代碼時將變量和塊做用域寫在哪裏決定的。所以當詞法分析器處理代碼時會保持做用域不變(大部分狀況是這樣)this
下面一個例子幫助理解:
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
複製代碼
假如上述例子採用了詞法做用域,那麼它的執行過程就是:
首先執行bar()
函數,在bar()
函數中執行foo()
函數,foo()
函數中輸出value
的值。它首先會查找當前做用域中是否有value
,若是沒有,則會向外一層查找,則最後輸出了1
動態做用域便是與詞法做用域相反的。咱們仍是以上面的例子爲例:
假如上述例子採用動態做用域:
它依然會像採用詞法做用域的形式執行函數,惟一不同的地方在於:在執行foo()
函數時,他不會向外一層查找value
,而是從調用的函數做用域中查找,因此最後的結果輸出爲2
。
在代碼任何地方都能訪問到的對象擁有全局做用域,通常擁有全局做用域有如下三種情形
var globalValue = `global value`;
function checkGlobal() {
var localValue = `local value`;
console.log(localValue); // "local value"
console.log(globalValue); // "global value"
}
console.log(globalValue); // "global value"
console.log(checkGlobal); // "global value"
console.log(localValue); // "Uncaught ReferenceError: localValue is not defined"
複製代碼
在上面的例子中,globalValue
就是一個全局變量,不管在哪都能訪問,而localValue
是一個局部變量,只能在函數內部訪問
function checkGlobal() {
var localValue = 'local value';
globalValue = 'global value';
console.log(localValue, globalValue); // 'local value' 'globalValue'
}
console.log(globalValue); // 'globalValue'
console.log(localValue); // "Uncaught ReferenceError: localValue is not defined"
複製代碼
函數做用域,就是指聲明在函數內部的變量,它正好和全局做用域相反。內層做用域能夠訪問到外層做用域,而外層做用域不能訪問到內層做用域。
function check() {
var localValue = 'local value';
console.log(localValue); // 'local value'
}
console.log(localValue); // "Uncaught ReferenceError: localValue is not defined"
複製代碼
塊級做用域可經過let
和const
聲明,聲明後的變量再指定塊級做用域外沒法被訪問。
在一個函數內部
在一個代碼塊內部
let
或者const
聲明的變量不會被提高到當前做用域頂部。
function check(bool) {
if(bool) {
let result = 1;
console.log(result);
}
console.log(result);
}
check(true); // 1 Uncaught ReferenceError: result is not defined
check(false); // Uncaught ReferenceError: result is not defined
複製代碼
若是想要訪問到result
,須要本身手動將變量提高到當前做用域頂部。像這樣
function check(bool) {
let result = null;
if(bool){
result = 1
}
console.log(result);
}
....
複製代碼
在同層級的做用域內,已經聲明過的變量,不能夠再次聲明。
// 1.同層級做用域
var bool = true;
let bool = false; // Uncaught SyntaxError: Identifier 'bool' has already been declared
// 不一樣層級做用域
var bool = true;
function check() {
let bool = false; // 這裏不會報錯
// .....
}
複製代碼
在for
循環中聲明的變量僅在循環內部使用。
for(let i = 0; i < 1; i++) {
console.log(i); // 0
}
console.log(i); // Uncaught ReferenceError: i is not defined
複製代碼
可是循環內部又是一個單獨的做用域
for(let i = 0; i < 2; i++) {
let i = 'hello';
console.log(i); // 'hello' 'hello'
}
複製代碼
當所須要的變量在所在的做用域中查找不到的時候,它會一層一層向上查找,直到找到全局做用域尚未找到的時候,就會放棄查找。這種一層一層的關係,就是做用域鏈。
var a = 1;
function check() {
return function() {
console.log(a); // 當前做用域內找不到a,會向上一層一層查找,最後找到了全局下的a,輸出結果爲1
console.log(b); // 同理,因此輸出"Uncaught ReferenceError: b is not defined"
}
}
var func = check(); // 此時返回匿名函數
func(); // 執行匿名函數
複製代碼
當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包。即便函數是在當前詞法做用域以外執行。
咱們來看一段代碼
function foo() {
var a = 1;
return function() {
console.log(a);
}
}
var bar = foo();
bar();
複製代碼
foo()
函數的執行結果返回給bar
,而此時因爲變量a
還在使用,於是沒有被銷燬,而後執行bar()
函數。這樣,咱們就能在外部做用域訪問到函數內部做用域的變量。這個就是閉包。
閉包的造成條件:
函數嵌套
內部函數引用外部函數的局部變量
能夠讀取函數內部的變量
可使變量的值長期保存在內存中,生命週期比較長。
可用來實現JS
模塊(JQuery
庫等)
JS
模塊是具備特定功能的JS
文件,將全部的數據和功能都封裝在一個函數內部(私有的),只向外暴露一個包含多個方法的對象或函數,模塊的使用者,只須要經過模塊暴露的對象調用方法來實現對應的功能。
(function() {
var a = 1;
function test() {
return a;
}
window.module = {a, test}; // 向外暴露
})()
複製代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="./1.js"> </script>
<title>Document</title>
</head>
<body>
<script>
console.log(module.a); // 1
console.log(module.test()); // 1
</script>
</body>
</html>
複製代碼
每一個函數都是閉包,函數可以記住本身定義時所處的做用域,函數走到了哪,定義時的做用域就到了哪。
內存泄漏
內存泄漏就是一個對象在你不須要它的時候仍然存在。因此不能濫用閉包。當咱們使用完閉包後,應該將引用變量置爲null
。
function outer(){
var num = 0;
return function add(){
num++;
console.log(num);
};
}
var func1 = outer();
func1(); // 1
func1(); // 2 [沒有被釋放,一直被佔用]
var func2 = outer();
func2(); // 1 [從新引用函數時,閉包是新的]
func2(); // 2
複製代碼
如今要求實現點擊第幾個button
就輸出幾
先寫一個html
<button>1</button>
<button>2</button>
複製代碼
再來寫JS
let buttons = document.getElementsByTagName('button');
for(var i = 0; i < buttons.length; i++) {
buttons[i].onclick = function() {
console.log(i + 1);
}
}
複製代碼
第一次咱們可能會寫出這樣的代碼,可是咱們會發現,這個代碼存在問題,不管我點第幾個按鈕,都會輸出3
。這是由於此時的i
是全局變量,當執行點擊事件時,i
已經變成了3
咱們能夠用兩種方式解決這個問題
let
聲明將上述代碼中的var
改成let
for(var i = 0; i < buttons.length; i++) {
(function(k){
buttons[k].onclick = function() {
console.log(k + 1);
}
})(i)
}
複製代碼
let x = 5;
function fn(x) {
return function(y) {
console.log(y + (++x));
}
}
let f = fn(6);
f(7);
console.log(x);
複製代碼
本篇文章主要講解了關於做用域、做用域鏈和閉包的知識,若是以爲對你有幫助,能夠給本篇文章點個贊呀~若是有哪裏不對的地方,還請你們指出來,咱們共同窗習、共同進步~
最後,分享一下個人公衆號「web前端日記」,歡迎你們前來關注~