大名鼎鼎的作用域和閉包,面試經常會問到。閉包(closure)是Javascript語言的一個難點,也是它的特色。
宣告
理解閉包,先理解函式的執行過程。
程式碼在執行的過程中會有一個預解析的過程,也就是在程式碼的執行過程中,會先將程式碼讀取到記憶體中,檢查其是否有錯誤,然後將所有宣告在此進行標記,讓js解析器知道有這樣的一個名字,後面使用時便不會出現未定義的錯誤,這個標記的過程就是提升。
- 變數的宣告
var num;沒有與之對應的資料,僅僅是讓js解析器知道,你定義了一個num的變數。 - 函式的宣告
function foo(){};一個獨立的結構,沒有任何語句。首先是將函式名進行提升,讓js解析器知道有一個foo函式,接著是將函式名與函式體連線起來,注意這裡並不執行函式體。
程式碼演示:
var num = 10;
function foo(){
console.log(num);
}
foo();
預解析的過程(變數提升,函式提升):
var num;
function foo(){
console.log(num)
}
num = 10;
foo();
程式碼執行時,首先會執行 num = 10; 然後執行foo(),進行函式體,列印出num的值。此時的num訪問到的是全域性中定義的num,所以num的值為10.
作用域
以上程式碼涉及到作用域的問題,所謂的域,表示的是範圍,所以作用域表示的是作用範圍,也就是一個名字在什麼地方可以使用,在什麼地方不可使用。
1、詞法作用域
在js中,採用的是詞法作用域,詞法作用域是指在編寫程式碼的過程中體現出來的作用範圍,一旦程式碼寫好了,不用執行,作用範圍就確定好了。
Javascript的作用域無非就是兩種:全域性變數和區域性變數。
2、詞法作用域的規則
- 函式允許訪問函式外的資料
- 整個程式碼結構中只有函式可限定作用域
- 作用域規則首先使用提升規則分析
- 若當前作用域中有名字了,就不考慮外面的名字
3、作用域鏈
只有函式可以構成作用域結構。只要存在程式碼,就至少有一個作用域,即全域性作用域。凡是程式碼有函式,那麼這個函式就構成一個作用域,如果函式中還有函式,那麼在這個作用域中就又誕生一個作用域,那麼將這樣的所有作用域列出來,就可以有一個:函式內指向函式外的鏈式結構。
作用域鏈變數訪問規則:看變數在當前作用域中,是否有變數的定義與賦值,如果有,則直接使用;如果沒有,則到外面的作用域中檢視,如果有,則停止查詢,使用外面一層作用域中定義的變數或值,如果沒有,則繼續往外查詢,直到最外層的全域性,如果全域性也沒有定義,則會報錯: xx is not defined。
閉包
什麼是閉包
閉包,是一個具有封閉功能與包裹功能的一個結構或空間。在js中,函式可以構成閉包。因為函式在當前的作用域中是一個封閉的結構,具有封閉性;同時根據作用域規則,只允許函式內部訪問外部的資料,而外部無法訪問函式內部的資料,即函式具有封閉的對外不公開的特性,就像把一個東西包裹起來一樣,因此函式可以構成閉包。
有點難理解,簡單來說,就是能夠讀取其他函式內部變數的函式,再簡潔一點就是:定義在一個函式內部的函式。
閉包的基本結構
因為閉包不允許外界直接訪問,所以只能間接訪問函式內部的資料,獲得函式內部資料的使用權。
1、寫一個函式,函式內定義一個新函式,返回新函式,用新函式獲得函式內部的資料
function foo(){
var num = 123;
function func(){
return num;
}
return func;
}
var f = foo();
var res1 = f();
var res2 = f();
// 此時,foo只呼叫了一次,不會再記憶體中重新建立一個函式,而通過f,可以訪問並獲取num的值,那麼呼叫f,即可獲得一樣的num值
改良:
function foo(){
var num = 123;
return function(){
return num;
}
}
var f = foo();
var res1 = f();
var res2 = f();
再改良:
var f = (function foo(){
var num = 123;
return function (){
return num;
}
})();
var res1 = f();
var res2 = f();
2、寫一個函式,函式內定義一個物件,物件繫結一個或多個函式(方法),返回物件,利用物件的方法訪問函式內部的資料
function func(){
var num1 = Math.random();
var num2 = Math.random();
return {
num1: function(){
return num1;
},
num2: function(){
return num2;
}
}
}
var p = func();
console.log(p.num1());
console.log(p.num1());// 這兩個訪問到的是同一個隨機數
閉包的基本用法
如上面程式碼演示的那樣,閉包可以通過返回函式來間接訪問到函式內的資料,這樣,閉包可以實現具有私有訪問空間的函式,保護私有的資料。另一方面,可以幫助其他物件讀取到函式內部的變數
閉包的效能問題
函式定義的變數會在函式執行結束後自動回收,但是因為閉包結構引出的資料經常會被外界所引用,這些資料將不會被回收,因此過多的閉包會消耗記憶體資源,影響效能。所以要謹慎使用閉包,可以在使用閉包時,如果不再使用某些變數了,一定要賦值一個null。
在ES6中,提出來物件代理概念,在代理層運算元據而不是直接操作原資料。