1.閉包的概念
????在對(duì)作用域,作用域鏈的概念進(jìn)行討論時(shí)我們知道,一般情況下定義在函數(shù)內(nèi)部的變量在函數(shù)外部是不可訪問(wèn)的。但某些時(shí)候有又確實(shí)有這樣的需求,這時(shí)就會(huì)用到閉包。閉包,就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。這就是閉包的概念。通過(guò)閉包我們可以在一個(gè)函數(shù)內(nèi)部訪問(wèn)另一個(gè)函數(shù)內(nèi)部的變量。
2.閉包的形式
下面介紹閉包的形式,也就是訪問(wèn)函數(shù)內(nèi)部變量的常見(jiàn)手段。
1 函數(shù)返回值為函數(shù)
function foo (){
let name = 'xiaom'
return function (){
return name
}
}
const bar = foo()
console.log(bar())
// 全局變量bar獲取到了局部作用域foo的內(nèi)部變量,這是最常見(jiàn)的形式
2 內(nèi)部函數(shù)賦給外部變量
let num;
function foo() {
const _num = 18;
function bar() {
return _num;
}
num = bar;
}
foo();
console.log(num()); // 18
3 通過(guò)立即執(zhí)行函數(shù)行成獨(dú)立作用域,保存變量(es6之后使用let,const替代)。
下面是一個(gè)經(jīng)典例子。
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
// 上述代碼的預(yù)期輸出結(jié)果是每隔一秒按順序輸出12345。
// 根據(jù)前面討論過(guò)的事件循環(huán)機(jī)制,定時(shí)器任務(wù)會(huì)在同步任務(wù)執(zhí)行完畢后再執(zhí)行。因此此時(shí)的i已經(jīng)變成6 會(huì)直接輸出5個(gè)6。
// 解決這一問(wèn)題的關(guān)鍵就是每次循環(huán)形成一個(gè)獨(dú)立作用域,這樣定時(shí)器中的操作執(zhí)行時(shí)會(huì)訪問(wèn)對(duì)應(yīng)作用域的變量。
for (var i = 1; i <= 5; i++) {
// 包一層立即執(zhí)行函數(shù) 并傳入i 由于內(nèi)部操作用到了i 因此會(huì)形成閉包
(function (i) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
})(i)
閉包函數(shù)的其他形式大多是以上形式的變體。
3.閉包的優(yōu)缺點(diǎn)
通過(guò)上述例子可以總結(jié)出閉包的幾大優(yōu)點(diǎn)
- 1.外部可以訪問(wèn)函數(shù)內(nèi)部變量。
- 2.讓函數(shù)內(nèi)部變量一直保留在內(nèi)存中。
function fn1(){
let count = 0;
function fn2 (){
count++
return count
}
return fn2
}
let result1 = fn1()
console.log(result1()) // 1
console.log(result1()) // 2
//通常來(lái)講,函數(shù)執(zhí)行完畢后,函數(shù)連同它內(nèi)部的變量會(huì)被一同銷(xiāo)毀。
//由于函數(shù)fn內(nèi)部變量count被外部引用,因此fn執(zhí)行完畢后,其內(nèi)部變量count不會(huì)被銷(xiāo)毀。因此過(guò)度使用閉包會(huì)造成內(nèi)存消耗。
- 3.形成獨(dú)立作用域。
????顯然,通過(guò)上述第二點(diǎn)也能看出,由于閉包會(huì)使函數(shù)內(nèi)部變量一直保存在內(nèi)存中,造成內(nèi)存消耗,因此過(guò)度使用會(huì)造成頁(yè)面性能問(wèn)題。解決方法是及時(shí)刪除不使用的局部變量。
4.閉包的應(yīng)用—柯里化函數(shù)
下面介紹閉包的一個(gè)典型應(yīng)用:柯里化函數(shù)。介紹柯里化之前需要先了解高階函數(shù)的概念。
高階函數(shù),是對(duì)其他函數(shù)進(jìn)行操作的函數(shù),可以將它們作為參數(shù)或返回它們。
通俗的講,滿足以下條件之一的函數(shù)就是高階函數(shù):
- 接受參數(shù)為函數(shù)
- 返回值為函數(shù)
下面是一個(gè)簡(jiǎn)單例子,利用高階函數(shù)為傳入的函數(shù)綁定this指向。他同時(shí)滿足上述兩個(gè)條件。
function foo() {
console.log(this.name);
}
const obj = {
name: "xiaom",
};
const obj1 = {
name: "xiaoh"
}
function bindThis(fn, obj) {
return fn.bind(obj);
}
bindThis(foo, obj)(); //xiaom
bindThis(foo, obj1)(); //xiaoh
柯里化就是一種特殊的高階函數(shù),下面介紹柯里化函數(shù)的概念。
定義:柯里化(Currying)是把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)(最初函數(shù)的第一個(gè)參數(shù))的函數(shù),并且返回接受余下的參數(shù)且返回結(jié)果的新函數(shù)的技術(shù)。
直接看定義非常晦澀難懂,我們把上述例子稍加改造。
function foo() {
console.log(this.name);
}
const obj = {
name: "xiaom",
};
const obj1 = {
name: "xiaoh"
}
function curryingFn(fn) {
return (obj) => {
return fn.bind(obj)
}
}
const newFoo = curryingFn(foo)
newFoo(obj)() // xiaom
newFoo(obj1)() // xiaoh
????上述操作就是把原先接受兩個(gè)參數(shù)的函數(shù)變成先接受一個(gè)參數(shù),再返回一個(gè)函數(shù)去處理剩余的參數(shù)。可以看到,柯里化函數(shù)的形式恰好符合閉包函數(shù)的第一種形式。而柯里化函數(shù)的優(yōu)勢(shì)就是參數(shù)復(fù)用。試想,就上述例子而言,當(dāng)我們需要多次改變fn的指向時(shí)就無(wú)需每次都傳入fn,只需傳入需要綁定的對(duì)象即可。
下面再介紹幾個(gè)利用柯里化函數(shù)進(jìn)行參數(shù)復(fù)用的典型例子。
- 正則判斷
如下,封裝一個(gè)正則判斷的函數(shù),傳入正則表達(dá)式和目標(biāo)字符串,返回判斷結(jié)果。沒(méi)有問(wèn)題。
function check(targetString, reg) {
return reg.test(targetString);
}
check(/^1[34578]\d{9}$/, '14900000088');
check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com');
當(dāng)業(yè)務(wù)需求只是判斷手機(jī)號(hào)或判斷郵箱時(shí),仍需要每次都傳入相應(yīng)的正則這就很低效,因此可以使用柯里化函數(shù)再次進(jìn)行封裝。
function curring(reg) {
return (str) => {
return reg.test(str);
};
}
var checkPhone = curring(/^1[34578]\d{9}$/);
var checkEmail = curring(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
console.log(checkPhone("183888888")); // false
console.log(checkPhone("17654239819")); // true
console.log(checkEmail("exy@163.com")); // true
- 判斷html標(biāo)簽
????我們知道Vue中的自定義組件在模板中是可以用html標(biāo)簽的形式書(shū)寫(xiě)的。那么vue碰到一個(gè)標(biāo)簽,如何知道他是html標(biāo)簽還是自定義組件呢?常規(guī)的思路是,html標(biāo)簽類型就那幾十種。將其存入數(shù)組中,每碰到一個(gè)標(biāo)簽就判斷其是否在該數(shù)組中即可。但這種方式缺點(diǎn)也很明顯,每次都要循環(huán)數(shù)組,非常消耗性能。我們可以將數(shù)組結(jié)構(gòu)轉(zhuǎn)為字典。
let set = {};
tags.forEach( key => set[ key ] = true )
進(jìn)一步優(yōu)化,將該標(biāo)簽集合以參數(shù)的形式傳入。這樣就封裝了一個(gè)通用函數(shù),它可以判斷某個(gè)元素是否在指定集合中。
let tags = "div,p,a,img,ul,li".split(",");
function makeMap(keys) {
let set = {};
tags.forEach((key) => (set[key] = true));
return function (tagName) {
return !!set[tagName.toLowerCase()];
};
}
let isHTMLTag = makeMap(tags);
console.log(isHTMLTag('Menu')) // false
console.log(isHTMLTag('div')) // true
- 自定義封裝bind方法
相較于call和apply,bind是永久性的改變this指向,相當(dāng)于復(fù)用了使用call/apply時(shí)傳入的目標(biāo)對(duì)象,因此可以使用柯里化函數(shù)封裝。
function foo(action) {
console.log(this.name + action);
}
const obj1 = {
name: "小明",
};
foo.__proto__.myBind = function (obj) {
const fun = this;
return function (args) {
fun.call(obj, args);
};
};
const newFoo = foo.myBind(obj1);
newFoo("跑步"); // 小明跑步
????柯里化函數(shù)的優(yōu)勢(shì)遠(yuǎn)不止這些。我們重點(diǎn)要理解柯里化函數(shù)的設(shè)計(jì)思想及其應(yīng)用場(chǎng)景。在實(shí)際業(yè)務(wù)中遇到一些固定的操作,需要復(fù)用的數(shù)據(jù),或?yàn)楹瘮?shù)擴(kuò)展功能時(shí),就可以考慮使用柯里化函數(shù)。柯里化的更多優(yōu)勢(shì)還需再實(shí)際編碼中進(jìn)行體會(huì)。
參考鏈接:http://m.itdecent.cn/p/5e1899fe7d6b