閉包
前言
現在去面試前端開發的崗位,如果你面對的面試官也是個前端,并且不是太水的話,你有很大的機率被問到JavaScript的閉包。
什麼是閉包
什麼是閉包,百度、Google之後,你可能會搜尋很多答案...
《JavaScript進階程式設計》這樣描述
閉包是指有權通路另一個函數作用域中的變量的函數
《JavaScript權威指南》這樣描述
從技術的角度講,所有的JavaScript函數都是閉包:它們都是對象,它們都關聯到作用域鍊
《你不知道的JavaScript》這樣描述
當函數可以記住并通路所在的詞法作用域時,就産生了閉包,即使函數是目前詞法作用域之外執行
最認可的當屬《你不知道的JavaScript》,前面兩種說話都沒有錯。
但閉包應該是基于詞法作用域書寫代碼時産生的自然結果,是一種現象!你也不用為了利用閉包而特意的建立,因為閉包的在你的代碼中随處可見,隻是你還不知道當時你寫的那一段代碼其實就産生了閉包
講解閉包
上面已經說到,當函數可以記住并通路所在的詞法作用域時,就産生了閉包,即使函數是在目前詞法作用域之外執行。
看段代碼
function fn1(){
var name='小馬哥'
function fn2(){
console.log(name);
}
fn2();
}
fn1();
如果是根據《JavaScript進階程式設計》和《JavaScript權威指南》來說,上面的代碼已經産生閉包了。fn2通路到了fn1的變量,滿足了條件“有權通路另一個函數作用域中的變量的函數”,fn2本身是個函數,是以滿足了條件“所有的JavaScript函數都是閉包”。
這的确是閉包,但是這種方式定義的閉包不太好觀察。
再看一段代碼:
function fn1(){
var name='小馬哥'
function fn2(){
console.log(name);
}
return fn2;
}
var fn3 = fn1();
fn3();
這樣就清晰地展示了閉包:
- fn2的詞法作用域能通路fn1的作用域
- 将fn2當做一個值傳回
- fn1執行後,将fn2的引用指派給fn3
- 執行fn3,輸出了變量name
我們知道通過引用關系,fn3就是fn2函數本身。執行fn3能正常輸出name,這不就是fn2能記住并通路它所在的詞法作用域,并且fn2函數的運作還是在目前詞法作用域之外
正常來說,當fn1函數執行完畢之後,其作用域是會被銷毀的,然後垃圾回收器會釋放那段記憶體空間。而閉包卻很神奇的将fn1的作用域存活了下來,fn2依然持有該作用域的引用,這個引用就是閉包。
總結:某個函數在定義時的詞法作用域之外的地方被調用,閉包可以使該函數極限通路定義時的詞法作用域。
注意:對函數值的傳遞可以通過其他的方式,并不一定隻有傳回該函數這一條路,比如可以用回調函數:
function fn1() {
var name = '小馬哥';
function fn2() {
console.log(name);
}
fn3(fn2);
}
function fn3(fn) {
fn();
}
fn1();
本例中,将内部函數fn2傳遞給fn3,當它在fn3中被運作時,它是可以通路到name變量的。
是以無論通過哪種方式将内部的函數傳遞到所在的詞法作用域以外,它都會持有對原始作用域的引用,無論在何處執行這個函數都會使用閉包。
再次解釋閉包
以上的例子會讓人覺得有點學院派了,但是閉包絕不僅僅是一個無用的概念,你寫過的代碼當中肯定有閉包的身影,比如類似如下的代碼:
function waitSomeTime(msg,time){
setTimeout(function(){
console.log(msg);
},time);
}
waitSomeTime('hello',1000);
定時器中有一個匿名函數,該匿名函數就有涵蓋waitSomeTime函數作用域的閉包,是以當1秒之後,該匿名函數能輸出msg。
另一個很經典的例子就是for循環中使用定時器延遲列印的問題:
for (var i = 1; i <= 10; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
我們預期的結果為1~10,但卻輸出10此11。這是因為setTimeout中的匿名函數執行的時候,for循環都已經結束了,for循環結束的條件是i大于10,是以輸出10此11。
原因:i是聲明在全局作用域中的,定時器中的匿名函數也是執行在全局作用域中,那當時是每次都輸出11
原因知道了,解決起來就簡單了,我們可以讓i在每次疊代的時候,都産生一個私有的作用域,在這個私有的作用域中儲存目前i的值
for (var i = 1; i <= 10; i++) {
(function () {
var j = i;
setTimeout(function () {
console.log(j);
}, 1000);
})();
}
這樣就達到我們的預期了呀,讓我們用一種比較優雅的寫法改造一些,将每次疊代的i作為實參傳遞給自執行函數,自執行函數中用變量去接收:
for (var i = 1; i <= 10; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 1000);
})(i);
}
閉包的應用
- setTimeout
//原生的setTimeout傳遞的第一個函數不能帶參數
setTimeout(function(param){
alert(param)
},1000)
//通過閉包可以實作傳參效果
function func(param){
return function(){
alert(param)
}
}
var f1 = func(1);
setTimeout(f1,1000);
- 閉包的應用比較典型的是定義子產品和封裝變量,我們将操作函數暴露給外部,而細節隐藏在子產品内容
function module(){
var arr = []; //私有變量
function add(val){
if(typeof val ==='number'){
arr.push(val);
}
}
function get(index){
if(index < arr.length){
return arr[index];
}else {
return null
}
}
return {
add:add,
get:get
}
}
var mod1 = module();
mod1.add(1);
mod1.add(2);
mod1.add('xxx');
console.log(mod1.get(2));
- 緩存
function getNewValue(key) {
var obj = {
name:'張三'
}
return obj[key]
}
var CacheCount = (function () {
var cache = {};
return {
getCache: function (key) {
if (key in cache) { // 如果結果在緩存中
console.log(cache);
return cache[key]; // 直接傳回緩存中的對象
}
var newValue = getNewValue(key); // 外部方法,擷取緩存
cache[key] = newValue; // 更新緩存
return newValue;
}
};
})();
console.log(CacheCount.getCache("name"));
console.log(CacheCount.getCache("name"));
JavaSript的進階函數
回調函數
function createDiv(cb){
let oDiv = document.createElement('div');
document.body.appendChild(oDiv);
if(typeof cb === 'function'){
cb(oDiv);
}
}
createDiv(function(oDiv){
oDiv.style.color = 'red';
});
這個例子中,有一個createDiv這個函數,這個函數負責建立一個div并添加到頁面中,但是之後要再怎麼操作這個div,createDiv這個函數就不知道,是以把權限交給調用createDiv函數的人,讓調用者決定接下來的操作,就通過回調的方式将div給調用者。
這是展現出了抽象,既然不知道div接下來的操作,那麼就直接給調用者,讓調用者去實作
抽象就是隐藏更具體的實作細節,從更高的層次看待我們要解決的問題。
數組中周遊
在程式設計的時候,并不是所有功能都是現成的,比如上面例子中,可以建立好幾個div,對每個div的處理都可能不一樣,需要對未知的操作做抽象,預留操作的入口,作為一名程式員,我們需要具備這種在恰當的時候将代碼抽象的思想
接下來看一下ES5中提供的幾個數組操作方法,可以更深入的了解抽象的思想,ES5之前周遊數組的方式是:
var arr = [1, 2, 3, 4, 5];
for (var i = 0; i < arr.length; i++) {
var item = arr[i];
console.log(item);
}
仔細看一下,這段代碼中用for,然後按順序取值,有沒有覺得如此操作有些不夠優雅,為出現錯誤留下了隐患,比如把length寫錯了,一不小心複用了i。既然這樣,能不能抽取一個函數出來呢?最重要的一點,我們要的隻是數組中的每一個值,然後操作這個值,那麼就可以把周遊的過程隐藏起來:
function forEach(arr, callback) {
for (var i = 0; i < arr.length; i++) {
var item = arr[i];
callback(item);
}
}
forEach(arr, function (item) {
console.log(item);
});
以上的forEach方法就将周遊的細節隐藏起來的了,把使用者想要操作的item傳回出來,在callback還可以将i、arr本身傳回:
callback(item, i, arr)
。
JS原生提供的forEach方法就是這樣的:
arr.forEach(function (item) {
console.log(item);
});
跟forEach同族的方法還有map、some、every等。思想都是一樣的,通過這種抽象的方式可以讓使用者更友善,同僚又讓代碼變得更加清晰。
抽象是一種很重要的思想,讓可以讓代碼變得更加優雅,并且操作起來更友善。在高階函數中也是使用了抽象的思想,是以學習高階函數得先了解抽象的思想。
高階函數
什麼是高階函數
至少滿足以下條件中的一個,就是高階函數
- 将其他函數作為參數傳遞
- 将函數作為傳回值
簡單來說,就是一個函數可以操作其他函數,将其他函數作為參數或将函數作為傳回值。我相信,寫過JS代碼的同學對這個概念都是很容易了解的,因為在JS中函數就是一個普通的值,可以被傳遞,可以被傳回。
參數可以被傳遞,可以被傳回
函數作為參數傳遞
函數作為參數傳遞就是我們上面提到的回調函數,回調函數在異步請求中用的非常多,使用者想要在請求成功後利用請求回來的資料做一些操作,但是又不知道請求什麼時候結束。
用jQuery來發一個Ajax請求
function getDetailData(sub_category_id, callback) {
$.ajax(`https://www.luffycity.com/api/v1/courses/?sub_category=${sub_category_id}&ordering=`, function (res) {
if (typeof callback === 'function') {
callback(res);
}
});
}
getDetailData('1', function (res) {
// do some thing
});
類似Ajax這種操作非常适合用回調去做,當一個函數裡不适合執行一些具體的操作,或者說不知道要怎麼操作時,可以将相應的資料傳遞給另一個函數,讓另一個函數來執行,而這個函數就是傳遞進來的回調函數。
另一個典型的例子就是數組排序
函數作為值傳回
在判斷資料類型的時候最常用的是typeof,但是typeof有一定的局限性,比如:
console.log(typeof []);//object
console.log(typeof {});//object
判斷數組和對象都是輸出object,如果想要更細緻的判斷應該要使用Object.prototype.toString
console.log(Object.prototype.toString.call([])); // 輸出[object Array]
console.log(Object.prototype.toString.call({})); // 輸出[object Object]
于是,我們可以寫出判斷對象、數組、數字的方法
function isObject(obj){
return Object.prototype.toString.call(obj) === '[object Object]';
}
function isArray(arr) {
return Object.prototype.toString.call(arr) === '[object Array]';
}
function isNumber(number) {
return Object.prototype.toString.call(number) === '[object Number]';
}
我們發現這三個方法太像了,可以做一些抽取:
function isType(type) {
return function (obj) {
return Object.prototype.toString.call(obj) === '[object ' + type + ']';
}
}
var isArray = isType('Array');
console.log(isArray([1,2]));
這個isType方法就是高階函數,該函數傳回了一個函數,并且利用閉包,将代碼變得優雅。