第5章:作用域閉包
到底什么是閉包
- 本章講解
閉包(Closures),它與作用域工作原理息息相關(guān)。 - 首先我用自己總結(jié)的三句話,簡單說明什么是閉包:
- (1)首先我們要知道,變量的查找 規(guī)則 是由內(nèi)到外的;
- (2)所以 子函數(shù)可以訪問外部作用域 的變量;
- (3)如果 把子函數(shù)賦值給外部變量 時,此時外部變量就 擁有 了可以 訪問封閉數(shù)據(jù)包的能力 ;
- 一個簡單的閉包示例
function foo(){
var a = 2;
function bar(){
console.log(a); //2
}
return bar;
}
var baz = foo();
- 分析上述代碼:
-
bar函數(shù)能訪問外部foo函數(shù)的作用域,將bar傳遞給外部變量baz來執(zhí)行; - 此時
bar函數(shù)在原來定義的詞法作用域之外執(zhí)行,同時持有foo函數(shù)作用域的引用,這就叫作閉包。
-
- 并且通過閉包的執(zhí)行方式,
foo函數(shù)在執(zhí)行后,其作用域不會被立即銷毀(畢竟bar函數(shù)還要用的啊)
閉包暴露的方式不止一種
- 閉包函數(shù)除了可以直接賦值給外部變量,也可以通過執(zhí)行外部函數(shù),將閉包函數(shù)以參數(shù)傳遞的方式暴露出去。
var foo(){
var a = 2;
//閉包函數(shù)
function baz(){
console.log(a);; //2
}
//執(zhí)行外部函數(shù),將閉包函數(shù)通過參數(shù)的方式傳遞進去
bar(baz)
}
var bar(fn){
fn();
}
閉包是最熟悉的陌生人
- 雖然閉包比較隱晦,但它絕不僅僅是一個好玩的玩具而已,在我們的代碼中到處都有它的身影。
- 比如常見的定時函數(shù)
setTimeout():
function wait(message){
setTimeout(function timer(){
console.log(message);
},1000)
}
上述代碼等價于:
//全局的setTimout函數(shù)準備就緒
var setTimout = function(invokeFn){}
function wait(message){
//timer內(nèi)部函數(shù)擁有對mesage的訪問權(quán)
var timer = function(){
console.log(message);
}
//執(zhí)行setTimeout()函數(shù),并將timer以參數(shù)方式傳遞進去
setTimout(timer);
}
- 是不是有似曾相似的感覺?內(nèi)部函數(shù)
timer()具有外部函數(shù)wait()作用域中的message變量的引用,它就是閉包。 - 除了定時函數(shù)之外,jQuery代碼也普遍的在使用閉包,不信你看下面的代碼:
//參數(shù)傳遞一個name字符串,選擇器字符串
function setupBot(name,selector){
//通過選擇器字符串初始化成jQuery對象
//綁定點擊事件
$(selector).click(function activator(){
//打印外部函數(shù)的name
console.log('Activating:' + name);
})
}
setupBot('Closure Bot 1','#bot_1');
setupBot('Closure Bot 2','#bot_2');
- 其實無論何時何地,只要將函數(shù)當(dāng)做參數(shù)進行傳遞,就有閉包的應(yīng)用。
- 什么定時器、事件監(jiān)聽函數(shù)、Ajax請求回調(diào)函數(shù)、跨窗口通信、Web Workers等代碼中,都普遍應(yīng)用到了閉包。
- 它每天與我們擦肩而過,就好像那個最熟悉的陌生人。
閉包解決了什么問題
1. 用閉包造塊級作用域
- 我們先看問題: 我只是想依次輸出循環(huán)的
i(1 ~ 5)
for(var i=1;i<=5;i++){
setTimtou(function timer(){
console.log(i);
},0)
}
- 見鬼了,然而輸出的結(jié)果卻是五次6,這是為什么呢?
- 其根本原因是,定時器的回調(diào)函數(shù)永遠在循環(huán)結(jié)束后才執(zhí)行。
- 那你可能會想,哦,那我豈不是永遠都不能在
for循環(huán)中用定時器了?JavaScript真垃圾!誒誒,先別慌,我們來分析一下。 - 我們不是預(yù)期循環(huán)的每個迭代中,都有一個
i的副本,然后輸出它嗎?通過閉包就能實現(xiàn)。
for(var i=1;i<=5;i++){
(function(index){
setTimtou(function timer(){
console.log(index);
},0)
})(i);
}
- 我們通過IIFE構(gòu)造了一個塊級作用域?qū)?code>i存了起來。
- 提到塊級作用域,其實ES6的語法里還有一種更便捷的解決方式——
let聲明
for(let i=1;i<=5;i++){
setTimtou(function timer(){
console.log(i);
},0)
}
2. 用閉包造模塊
function CoolModule(){
var something = 'cool';
var another = [1, 2, 3];
function doSomething(){
console.log(something);
}
function doAnother(){
console.log(another.join(","));
}
//對外暴露內(nèi)部函數(shù)
return {
doSomething : doSomething,
doAnother : doAnother
}
}
- 上述代碼演示了JavaScript模塊暴露。通過調(diào)用
CoolModule函數(shù)創(chuàng)建一個模塊實例,CoolModule返回的對象中包含內(nèi)部函數(shù)的引用,就相當(dāng)于模塊的公共API。 - 當(dāng)然上述代碼可以任意調(diào)用多次,重復(fù)返回新的模塊實例,我們可以改成單例模式:
//通過一個IIFE函數(shù)來包裝
var foo = (function CoolModule(){
var something = 'cool';
var another = [1, 2, 3];
function doSomething(){
console.log(something);
}
function doAnother(){
console.log(another.join(","));
}
//對外暴露內(nèi)部函數(shù)
return {
doSomething : doSomething,
doAnother : doAnother
}
})();
foo.doSomething(); //cool
foo.doAnother(); //1,2,3
- 模塊的公共API不僅可以是內(nèi)部私有變量的訪問,也可以是修改私有變量的方法:
var foo = (function CoolModule(id){
var moduleId = id;
function showId(){
console.log(moduleId);
}
function uppcaseId(){
moduleId = moduleId.toUpperCase();
}
return{
showId : showId,
uppcaseId : uppcaseId
}
})('fooModule');
foo.showId();
foo.uppcaseId();
foo.showId();
- 其實大多數(shù)模塊管理機制本質(zhì)是也是通過類似的方式來實現(xiàn)的,我們來嘗試寫一個簡版的模塊管理器:
/**
* 定義牛批哄哄的超級模塊管理器
*/
var SuperModules = (function(){
//所有的模塊集合,以name作為key
var moduleMap = {};
function define(name,deps,impl){
//獲取依賴
for(var i=0;i<deps.length;i++){
deps[i] = moduleMap[deps[i]]
}
//執(zhí)行引入的模塊,并以deps作為參數(shù)
moduleMap[name] = impl.apply(impl,deps);
}
function get(name){
reutrn moduleMap[name];
}
//暴露公共API
return{
define : define,
get : get
}
})()
/**
* 先定義一個bar模塊
* 沒有依賴,impl是一個執(zhí)行函數(shù)
*/
SuperModules.define('bar',[],function(){
function hello(name){
return 'let me introduce:' + name;
}
return {
hello : hello
}
})
/**
* 再定義一個foo模塊
*/
SuperModules.difine('foo',['bar'],function(bar){
var hungry = 'hippo';
function awesome(){
console.log(bar.hello(hungry).toUpperCase());
}
return {
awesome : awesome
}
})
/**
* 調(diào)用測試
*/
var bar = SuperModules.get('bar');
var foo = SuperModules.get('foo');
//let me introduce: hippo
console.log(bar.hello('hippo'));
//let me introduce: HIPPO
foo.awesome();
- 是不是看起來比較復(fù)雜……不過不用擔(dān)心,ES6以及添加了模塊的語法支持!
- ES6會將每個js文件當(dāng)做獨立的模塊來處理,每個模塊可以通過
import關(guān)鍵字導(dǎo)入依賴的模塊,或者通過export關(guān)鍵字導(dǎo)出API。你需要做的,只是擁抱ES6! bar.js
function hello(name){
return 'let me introduce:' + name;
}
export hello;
foo.js
import hello from 'bar';
var hungry = 'hippo';
function awesome(){
console.log(bar.hello(hungry).toUpperCase());
}
export awesome;
baz.js
module foo from 'foo';
module bar from 'bar';
//let me introduce: hippo
console.log(bar.hello('hippo'));
//let me introduce: HIPPO
foo.awesome();
小結(jié)
- 內(nèi)部函數(shù)可以訪問外部函數(shù)的作用域,如果將內(nèi)部函數(shù)暴露給外部變量時,或者說內(nèi)部函數(shù)在定義的詞法作用域之外執(zhí)行時,就產(chǎn)生了閉包。
- 閉包是個非常強大的工具,可以用來實現(xiàn)模塊模式。
- 模塊的特征:
- 為創(chuàng)建內(nèi)部作用域而調(diào)用一個包裝函數(shù);
- 包裝函數(shù)的返回值必須至少包含一個對內(nèi)部函數(shù)的引用,這樣就會創(chuàng)建涵蓋整個包裝函數(shù)內(nèi)部作用域的閉包;