關于 JS 閉包看這一篇就夠了
- 關于 JS 閉包看這一篇就夠了
- 1. LHS 和 RHS 查詢
- 2. 作用域
- 2.1 作用域分類
- 2.2 作用域鏈
- 2.3 詞法作用域
- 2.4 欺騙詞法作用域
- 2.5 塊級作用域
- 2.6 模塊化
- 3. 閉包
- 3.1 什么是閉包
- 3.2 閉包的作用
- 3.3 閉包經典使用場景
- 3.3.1 return 回一個函式
- 3.3.2 IIFE(自執行函式)
- 3.3.3 回圈賦值
- 3.3.4 回呼函式
- 3.3.5 節流防抖
- 3.3.6 柯里化實作
- 參考
關于 JS 閉包看這一篇就夠了
今天看完了《你不知道的Javascript 上卷》的閉包,來總結一下,
1. LHS 和 RHS 查詢
LHS (Left-hand Side) 和 RHS (Right-hand Side) ,是在代碼執行階段 JS 引擎操作變數的兩種方式,字面理解就是當變數出現在賦值操作左側時進行LHS查詢,出現在右側時進行RHS查詢,更準確的來說,LHS是為了找到變數的容器本身從而可以進行賦值,而RHS則是獲取某個變數的值,
例如:
console.log(a);
其中對a的參考就是一個RHS參考,因為這里沒有給a賦任何值,而是獲取它的值從而將它傳遞給console.log,
a = 2;
顯然這里對a的參考是LHS參考,因為這里并不需要獲取值,只是為了將2賦值給a這個變數,
現在我們已經知道在代碼執行階段 JS 引擎操作變數這兩種方式,那么這兩種方式會如何去找到變數呢?
2. 作用域
簡單來說,作用域 指程式中定義變數的區域,它決定了當前執行代碼對變數的訪問權限,
2.1 作用域分類
作用域包括:
- 全域作用域:程式的最外層作用域
- 函式作用域:函式定義時會被創建
- 塊級作用域:
ES6新增的let、const特性
例如:
var name = '夏安'; // 全域作用域
function func() { //
var name = '..夏安..'; // 函式作用域
console.log(name);
}
if (true) {
let name = '夏安...'; // 塊級作用域
console.log(name);
}
2.2 作用域鏈
但幾個作用域進行了嵌套,這邊現成了作用域鏈,
LHS和RHS查詢都會在當前執行作用域中開始,如果它們沒有找到所對應的識別符號,就會沿作用域向外層作用域查找,直到抵達全域作用域再停止,
不成功的RHs參考會導致拋出ReferenceError,不成功的LHS參考會導致自動隱式地創建一個全域變數(非嚴格模式下),或者拋出ReferenceError例外(嚴格模式下),
例如:
function func(b) {
console.log(a + b); // 3
console.log(c); // ReferenceError: c is not defined
}
var a = 1;
func(2);
上述栗子中,對b進行RHS參考,在func函式內部作用域中無法找到,但可以在上級作用域(全域作用域)中找到,而c在整個作用域鏈中都沒有找到,所以拋出了ReferenceError例外,
2.3 詞法作用域
作用域共有兩種主要的作業模型,第一種是最為普遍的,被大多數編程語言所采用的詞法作用域,也可以被叫做 靜態作用域,另一種則稱為動態作用域(如Bash腳本),
無論函式在哪里被呼叫,也無論它如何被呼叫,它的詞法作用域都只由函式被宣告時所處的位置決定,
我們來看下面這個栗子:
function func() {
console.log(a);
}
function func2() {
var a = 1;
}
var a = 2;
func(); // 2
在函式func作用域李沒有找到變數a,向外層全域作用域找,而不會在函式func2作用域里找,
詞法作用域查找只會查找一級識別符號,比如a,b等,如果代碼中參考了obj.name,詞法作用域查找只會試圖查找obj識別符號,找到這個變數后,物件屬性訪問規則會接管對name屬性的訪問,
2.4 欺騙詞法作用域
Javascript中有兩種機制可以欺騙詞法作用域,,分別是eval和with,但欺騙詞法作用域會導致性能下降,所以不建議使用,
下面我們以eval為例簡單介紹一下:
function func(str) {
eval(str);
console.log(a);
}
var a = 1;
func('var a = 2;'); // 2
eval的引數var a = 2;被當作本來就在那里的代碼執行,在函式func作用域里創建了一個變數a,從而遮蔽了外層全域作用域里的變數a
2.5 塊級作用域
什么是塊級作用域呢?簡單來說,花括號內 {...} 的區域就是塊級作用域區域,
很多語言本身都是支持塊級作用域的,Javascript 中大部分情況下,只有兩種作用域型別:全域作用域 和 函式作用域,
if (true) {
var a = 1;
}
console.log(a); // 1
運行后會發現,結果還是 1,花括號內定義并賦值的 a 變數跑到全域了,這足以說明,Javascript 不是原生支持塊級作用域的,
但是 ES6 標準提出了使用 let 和 const 代替 var 關鍵字,來“創建塊級作用域”,也就是說,上述代碼改成如下方式,塊級作用域是有效的:
if (true) {
let a = 1;
}
console.log(a); // ReferenceError
2.6 模塊化
作用域的一個常見運用場景之一,就是 模塊化,由于原生Javascript不支持模塊化,在正式的模塊化方案出來之前,開發者為了解決這類問題想到了使用函式作用域來創建模塊的方法,
// module1.js
(function () {
var a = 1;
console.log(a);
})();
// module2.js
(function () {
var a = 2;
console.log(a);
})();
上面的代碼中,構建了 module1 和 module2 兩個代表模塊的不同檔案,立即呼叫函式運算式(Immediately Invoked Function Expression 簡寫 IIFE),兩個函式內分別定義了一個同名變數 a ,由于函式作用域的隔離性質,這兩個變數被保存在不同的作用域中(不嵌套),JS 引擎在執行這兩個函式時會去不同的作用域中讀取,并且外部作用域無法訪問到函式內部的 a 變數,這樣一來就巧妙地解決了 全域作用域污染 和 變數名沖突 的問題,并且,由于函式的包裹寫法,這種方式看起來封裝性好多了,
3. 閉包
3.1 什么是閉包
關于什么是閉包,說法很多:
在 JS 忍者秘籍(P90)中對閉包的定義:閉包允許函式訪問并操作函式外部的變數,
紅寶書上對于閉包的定義:閉包是指有權訪問另外一個函式作用域中的變數的函式,
MDN 對閉包的定義為:一個函式和對其周圍狀態(lexical environment,詞法環境)的參考捆綁在一起(或者說函式被參考包圍),這樣的組合就是閉包(closure),也就是說,閉包讓你可以在一個內層函式中訪問到其外層函式的作用域,在 JavaScript 中,每當創建一個函式,閉包就會在函式創建的同時被創建出來,
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2
函式bar()的詞法作用域能夠訪問foo()的內部作用域,然后我們將bar()函式本身當作一個值型別進行傳遞,在這個例子中,我們將 bar 所參考的函式物件本身當作回傳值,
在foo()執行后,其回傳值(也就是內部的 bar()函式)賦值給變數baz并呼叫 baz(),實際上只是通過不同的識別符號參考呼叫了內部的函式bar(),
bar()顯然可以被正常執行,但是在這個例子中,它在自己定義的詞法作用域以外的地方執行,
在 foo()執行后,通常會期待foo()的整個內部作用域都被銷毀,因為我們知道引擎有垃圾回收器用來釋放不再使用的記憶體空間,由于看上去 foo()的內容不會再被使用,所以很自然地會考慮對其進行回收,
而閉包的“神奇”之處正是可以阻止這件事情的發生,事實上內部作用域依然存在,因此沒有被回收,誰在使用這個內部作用域?原來是bar()本身在使用,
拜bar()所宣告的位置所賜,它擁有涵蓋foo()內部作用域的閉包,使得該作用域能夠一直存活,以供 bar()在之后任何時間進行參考,
bar()依然持有對該作用域的參考,而這個參考就叫作閉包,
3.2 閉包的作用
- 保護函式的私有變數不受外部的干擾,形成不銷毀的堆疊記憶體,
- 保存,把一些函式內的值保存下來,閉包可以實作方法和屬性的私有化
3.3 閉包經典使用場景
下面舉例一些典型的閉包場景:
3.3.1 return 回一個函式
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2
3.3.2 IIFE(自執行函式)
(function (a) {
console.log(a);
})(1)
3.3.3 回圈賦值
for(var i = 0; i<10; i++){
(function(j){
setTimeout(function(){
console.log(j)
}, 1000)
})(i)
}
因為存在閉包的原因上面能依次輸出1~10,閉包形成了10個互不干擾的私有作用域,將外層的自執行函式去掉后就不存在外部作用域的參考了,輸出的結果就是連續的 10,為什么會連續輸出10,因為 JS 是單執行緒的遇到異步的代碼不會先執行(會入堆疊),等到同步的代碼執行完
i++到 10時,異步代碼才開始執行此時的i=10輸出的都是 10,
3.3.4 回呼函式
setTimeout(function(){
console.log(j)
}, 1000)
3.3.5 節流防抖
// 節流
function throttle(fn, timeout) {
let timer = null
return function (...arg) {
if(timer) return
timer = setTimeout(() => {
fn.apply(this, arg)
timer = null
}, timeout)
}
}
// 防抖
function debounce(fn, timeout){
let timer = null
return function(...arg){
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arg)
}, timeout)
}
}
3.3.6 柯里化實作
function curry(fn, len = fn.length) {
return _curry(fn, len)
}
function _curry(fn, len, ...arg) {
return function (...params) {
let _arg = [...arg, ...params]
if (_arg.length >= len) {
return fn.apply(this, _arg)
} else {
return _curry.call(this, fn, len, ..._arg)
}
}
}
let fn = curry(function (a, b, c, d, e) {
console.log(a + b + c + d + e)
})
fn(1, 2, 3, 4, 5) // 15
fn(1, 2)(3, 4, 5)
fn(1, 2)(3)(4)(5)
fn(1)(2)(3)(4)(5)
最后,看下面這道題檢驗一下自己吧:
var result = [];
var a = 3;
var total = 0;
function foo(a) {
for (var i = 0; i < 3; i++) {
result[i] = function () {
total += i * a;
console.log(total);
}
}
}
foo(1);
result[0](); // 3
result[1](); // 6
result[2](); // 9
參考
- JS 閉包經典使用場景和含閉包必刷題 - 掘金 (juejin.cn)
- 閉包 - JavaScript | MDN (mozilla.org)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/413462.html
標籤:其他
上一篇:Js基本包裝型別(含原理)
下一篇:Vue2學習筆記
