什么是閉包?可以在自己定義的詞法作用域以外的地方執行,就是閉包。
```
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar;
}
var baz = foo();
baz(); //2
```
bar很明顯被執行了,但是它在自己定義的詞法作用域以外的地方執行了。
foo()執行后,期待foo()的整個內部作用域都被銷毀,看起來foo()的內容不會再使用,應當被回收。但是,閉包的神奇之處就在于可以阻止這件事情發生。事實上內部作用域依然存在,所以無法被回收。bar聲明的位置導致bar擁有涵蓋整個foo內部作用域的閉包,它一直在引用著該作用域。正是因為它無法被銷毀,才使得函數可以繼續訪問定義時的詞法作用域。
閉包的常見場景就是在一個函數內的回調函數:
```
function wait(message){
setTimeout(function(){
console.log(message);
}, 1000)
};
wait('hello')
//在1000毫秒后,匿名函數仍然能夠獲取到message的值,這是因為wait的內部作用域不會消失,匿名函數一直擁有對它的引用。
function setupBot(name, selector){
$(selector).click(function(){
console.log(name);
})
};
setupBot('qc', DIV);
//setupBot執行完后,函數并不會被回收,當你點擊的時候匿名函數仍然能獲得當初的name。
```
#### 循環與閉包
```
for(var i = 0; i < 5; i++){
setTimeout(function(){console.log(i)}, 1000);
}
//結果很明顯是5個6,因為i是被共用的
```
如何解決?思路是每次循環時為i創建一個副本來保存當前i的值。立即執行函數會自動創建自己的作用域。
```
for(var i = 0; i < 5; i++){
(function(){
setTimeout(function(){
console.log(i)
}, 1000);
})();
}
```
然而這是不夠的,結果依舊是5個6。立即執行函數內部的作用域是空的,需要為作用域內添加點東西:
```
for(var i = 0; i < 5; i++){
(function(j){
var j = i;
setTimeout(function(){
console.log(j)
}, 1000);
})();
}
//也可以寫成
for(var i = 0; i < 5; i++){
(function(j){
setTimeout(function(){
console.log(j)
}, 1000);
})(i);
}
```
現在好了,每一次迭代時都會通過立即執行函數創建內部作用域保存i的值,在循環結束后,閉包仍然能夠訪問內部作用域的值。
#### let
let擁有自動創建作用域的功能:
```
for(let i = 0; i < 5; i++){
setTimeout(function(){console.log(i)}, 1000);
}
```
問題解決!
#### 模塊
```
function module(){
var something = 'cool';
var another = [1, 2, 3];
function doSomething(){
console.log(something);
}
function doAnother(){
console.log(another);
}
return {
doSomething: doSomething,
doAnother: doAnother
}
}
var foo = module();
foo.doSomething();
foo.doAnother();
//函數必須執行,否則無法創建內部作用域和閉包
//該函數返回的是含有對內部函數引用的對象,而不是對內部數據變量引用的對象。
```
模塊模式需要具備兩個必要條件
1. 必須有外部的封閉函數,該函數必須至少被調用一次(每調用一次都會創建一個新的模塊)。
2. 封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有作用域中形成閉包,并且可以訪問或者修改私有的狀態。
由此可以看出來,一個擁有函數屬性的對象并不是真正的模塊。
由于module每執行一次都會創建新的模塊實例,當只需要一個實例的時候,可以使用立即執行函數來實現單例模式:
```
var foo = (function module(){
var something = 'cool';
var another = [1, 2, 3];
function doSomething(){
console.log(something);
}
function doAnother(){
console.log(another);
}
return {
doSomething: doSomething,
doAnother: doAnother
}
})()
foo.doSomething();
```
模塊的另一個強大的用法是可以修改模塊內的任意東西:
```
var foo = (function(id){
function change(){
api.identify = identify2;
}
function identify1(){
console.log(id);
}
function identify2(){
console.log(id.toUpperCase());
}
var api = {
change: change,
identify: identify1
}
return api;
})('foo');
foo.identify(); //foo
foo.change();
foo.identify(); //FOO
```
由于閉包的存在,整個模塊的作用域會一直存在著,api這個變量永遠不會銷毀。當調用change后,api的identify就由1替換成了2。
#### 現代模塊機制
請認真思考下面的代碼原理
```
var Module = (function(){
var modules = {};
function define(name, deps, impl){
for(var i = 0; i < deps.length; i++){
deps[i] = modules[deps[i]]
}
modules[name] = impl.apply(impl, deps);
}
function get(name){
return modules[name];
}
return {define: define, get: get};
})()
```
```
Module.define("bar", [], function(){
function hello(name){
return name;
}
return {hello: hello};
})
Module.define("foo", ["bar"], function(bar){
function hi(){
console.log('name is: ' + bar.hello('qc'));
}
return {hi: hi};
})
```
```
var bar = Module.get("bar");
var foo = Module.get("foo");
console.log(bar.hello('mescal')); // mescal
foo.hi(); // name is: qc
```
這種模塊機制是運行時執行的,它的api只有在運行時才被創建,我們可以在運行時修改api,參考identify那個例子。而ES6的模塊機制是基于文件的,它的api就是靜態的,因此錯誤檢查在編譯階段就執行了,而上述代碼的模塊機制的錯誤檢查在運行時才開始。
- 你不知道的JS上
- 第一部分 第三章 函數作用域和塊作用域
- 第一部分 第四章 提升
- 第一部分 第五章 閉包
- 第二部分 第一章 關于this
- 第二部分 第二章 this全面解析
- 第二部分 第三章 對象
- 第二部分 第五章 原型
- 第二部分 第六章 行為委托
- 你不知道的JS中
- 第一部分 第二章 值
- 第一部分 第三章 原生函數
- 第一部分 第四章 強制類型轉換
- 第一部分 第五章 語法
- 第二部分 第一章 異步
- 第二部分 第三章 Promise
- 第二部分 第四章 生成器
- 第二部分 第五章 性能
- 你不知道的JS下
- 第一部分 總結
- 第二部分 第二章 語法
- 第二部分 第三章 代碼組織
- 第二部分 第四章 Promise
- 第二部分 第五章 集合
- 第二部分 第六章 新增API
- 第二部分 第七章 元編程
- 第二部分 第八章 ES6之后