淺談JavaScript閉包與柯里化函數(shù)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容