完全搞懂閉包,柯里化,手寫代碼,金九銀十再也不丟分!

點擊上方藍字「 大前端技術沙龍 」關注我javascript

您的關注意義重大前端

 

原創@大前端技術沙龍java

 

這段時間我試着經過思惟導圖來總結知識點,主要關注的是一些相對重要或理解難度較高的內容。下面是同系列文章:程序員

若是您須要換個角度看閉包,請直接打開解讀閉包,此次從ECMAScript詞法環境,執行上下文提及算法

本文總結了javascript中函數的常見知識點,包含了基礎概念,閉包this指向問題,高階函數,柯里化等,手寫代碼那部分也是滿滿的乾貨,不管您是想複習準備面試,仍是想深刻了解原理,本文都應該有你想看的點,總之仍是值得一看的。編程

老規矩,先上思惟導圖。小程序

什麼是函數

通常來講,一個函數是能夠經過外部代碼調用的一個「子程序」(或在遞歸的狀況下由內部函數調用)。像程序自己同樣,一個函數由稱爲函數體的一系列語句組成。值能夠傳遞給一個函數,函數將返回一個值。微信小程序

函數首先是一個對象,而且在javascript中,函數是一等對象(first-class object)。函數能夠被執行(callable,擁有內部屬性[[Call]]),這是函數的本質特性。除此以外,函數能夠賦值給變量,也能夠做爲函數參數,還能夠做爲另外一個函數的返回值

函數基本概念

函數名

函數名是函數的標識,若是一個函數不是匿名函數,它應該被賦予函數名。

  • 函數命名須要符合 javascript標識符規則,必須以字母、下劃線_或美圓符$開始,後面能夠跟數字,字母,下劃線,美圓符。

  • 函數命名不能使用javascript保留字,保留字是javascript中具備特殊含義的標識符。

  • 函數命名應該語義化,儘可能採用動賓結構,小駝峯寫法,好比 getUserName()validateForm(), isValidMobilePhone()

  • 對於構造函數,咱們一般寫成大駝峯格式(由於構造函數與類的概念強關聯)。

下面是一些不成文的約定,不成文表明它沒必要遵照,可是咱們按照這樣的約定來執行,會讓開發變得更有效率。

  • __xxx__表明非標準的方法。

  • _xxx表明私有方法。

函數參數

形參

形參是函數定義時約定的參數列表,由一對圓括號()包裹。

在MDN上有看到,一個函數最多能夠有255個參數。

然而形參太多時,使用者老是容易在引用時出錯。因此對於數量較多的形參,通常推薦把全部參數做爲屬性或方法整合到一個對象中,各個參數做爲這個對象的屬性或方法來使用。舉個例子,微信小程序的提供的API基本上是這種調用形式。

wx.redirectTo(Object object)

調用示例以下:

wx.redirectTo({
  url'/article/detail?id=1',
  successfunction() {},
  failfunction() {}
})

形參的數量能夠由函數的length屬性得到,以下所示。

function test(a, b, c{}
test.length; // 3

實參

實參是調用函數時傳入的,實參的值在函數執行前被肯定。

javascript在函數定義時並不會約定參數的數據類型。若是你指望函數調用時傳入正確的數據類型,你必須在函數體中對入參進行數據類型判斷。

function add(a, b{
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new Error("參數必須是數字類型")
  }
}

好在Typescript提供了數據類型檢查的能力,這必定程度上防止了意外狀況的發生。

實參的數量能夠經過函數中arguments對象的length屬性得到,以下所示。

實參數量不必定與形參數量一致。

function test(a, b, c{
  var argLength = arguments.length;
  return argLength;
}
test(12); // 2

默認參數

函數參數的默認值是undefined,若是你不傳入實參,那麼實際上在函數執行過程當中,相應參數的值是undefined

ES6也支持在函數聲明時設置參數的默認值。

function add(a, b = 2{
    return a + b;
}
add(1); // 3

在上面的add函數中,參數b被指定了默認值2。因此,即使你不傳第二個參數b,也能獲得一個預期的結果。

假設一個函數有多個參數,咱們但願不給中間的某個參數傳值,那麼這個參數值必須顯示地指定爲undefined,不然咱們指望傳給後面的參數的值會被傳到中間的這個參數。

function printUserInfo(name, age = 18, gender{
  console.log(`姓名:${name},年齡:${age},性別:${gender}`);
}
// 正確地使用
printUserInfo('Bob'undefined'male');
// 錯誤,'male'被錯誤地傳給了age參數
printUserInfo('Bob''male');

PS:注意,若是你但願使用參數的默認值,請必定傳undefined,而不是null

固然,咱們也能夠在函數體中判斷參數的數據類型,防止參數被誤用。

function printUserInfo(name, age = 18, gender{
  if (typeof arguments[1] === 'string') {
    age = 18;
    gender = arguments[1];
  }
  console.log(`姓名:${name},年齡:${age},性別:${gender}`);
}

printUserInfo('bob''male'); // 姓名:bob,年齡:18,性別:male

這樣一來,函數的邏輯也不會亂。

剩餘參數

剩餘參數語法容許咱們將一個不定數量的參數表示爲一個數組。

剩餘參數經過剩餘語法...將多個參數聚合成一個數組。

function add(a, ...args{
  return args.reduce((prev, curr) => {
    return prev + curr
  }, a)
}

剩餘參數和arguments對象之間的區別主要有三個:

  • 剩餘參數只包含那些沒有對應形參的實參,而 arguments對象包含了傳給函數的全部實參。

  • arguments對象不是一個真正的數組,而剩餘參數是真正的 Array實例,也就是說你可以在它上面直接使用全部的數組方法,好比 sortmapforEachpop。而 arguments須要借用 call來實現,好比 [].slice.call(arguments)

  • arguments對象還有一些附加的屬性(如 callee屬性)。

剩餘語法和展開運算符看起來很類似,然而從功能上來講,是徹底相反的。

剩餘語法(Rest syntax) 看起來和展開語法徹底相同,不一樣點在於, 剩餘參數用於解構數組和對象。從某種意義上說,剩餘語法與展開語法是相反的:展開語法將數組展開爲其中的各個元素,而剩餘語法則是將多個元素收集起來並「凝聚」爲單個元素。

arguments

函數的實際參數會被保存在一個類數組對象arguments中。

類數組(ArrayLike)對象具有一個非負的length屬性,而且能夠經過從0開始的索引去訪問元素,讓人看起來以爲就像是數組,好比NodeList,可是類數組默認沒有數組的那些內置方法,好比push, pop, forEach, map

咱們能夠試試,隨便找一個網站,在控制檯輸入:

var linkList = document.querySelectorAll('a')

會獲得一個NodeList,咱們也能夠經過數字下標去訪問其中的元素,好比linkList[0]

可是NodeList不是數組,它是類數組。

Array.isArray(linkList); // false

回到主題,arguments也是類數組,argumentslength實參的數量決定,而不是由形參的數量決定。

function add(a, b{
  console.log(arguments.length);
  return a + b;
}
add(1234);
// 這裏打印的是4,而不是2

arguments也是一個和嚴格模式有關聯的對象。

  • 非嚴格模式下, arguments裏的元素和函數參數都是指向同一個值的引用,對 arguments的修改,會直接影響函數參數。

function test(obj{
  arguments[0] = '傳入的實參是一個對象,可是被我變成字符串了'
  console.log(obj)
}
test({name'jack'})
// 這裏打印的是字符串,而不是對象
  • 嚴格模式下, arguments是函數參數的副本,對 arguments的修改不會影響函數參數。可是 arguments不能從新被賦值,關於這一點,我在 解讀閉包,此次從ECMAScript詞法環境,執行上下文提及這篇文章中解讀 不可變綁定時有提到。在嚴格模式下,也不能使用 arguments.callerarguments.callee,限制了對調用棧的檢測能力。

函數體

函數體(FunctionBody)是函數的主體,其中的函數代碼(function code)由一對花括號{}包裹。函數體能夠爲空,也能夠由任意條javascript語句組成。

函數的調用形式

大致來講,函數的調用形式分爲如下四種:

做爲普通函數

函數做爲普通函數被調用,這是函數調用的經常使用形式。

function add(a, b{
  return a + b;
}
add(); // 調用add函數

做爲普通函數調用時,若是在非嚴格模式下,函數執行時,this指向全局對象,對於瀏覽器而言則是window對象;若是在嚴格模式下,this的值則是undefined

做爲對象的方法

函數也能夠做爲對象的成員,這種狀況下,該函數一般被稱爲對象方法。當函數做爲對象的方法被調用時,this指向該對象,此時即可以經過this訪問對象的其餘成員變量或方法。

var counter = {
  num0,
  increasefunction() {
    this.num++;
  }
}
counter.increase();

做爲構造函數

函數配合new關鍵字使用時就成了構造函數。構造函數用於實例化對象,構造函數的執行過程大體以下:

  1. 首先建立一個新對象,這個新對象的 __proto__屬性指向構造函數的 prototype屬性。

  2. 此時構造函數的 this指向這個新對象。

  3. 執行構造函數中的代碼,通常是經過 this給新對象添加新的成員屬性或方法。

  4. 最後返回這個新對象。

實例化對象也能夠經過一些技巧來簡化,好比在構造函數中顯示地return另外一個對象,jQuery很巧妙地利用了這一點。具體分析詳見面試官真的會問:new的實現以及無new實例化。

經過call, apply調用

applycall是函數對象的原型方法,掛載於Function.prototype。利用這兩個方法,咱們能夠顯示地綁定一個this做爲調用上下文,同時也能夠設置函數調用時的參數。

applycall的區別在於:提供參數的形式不一樣,apply方法接受的是一個參數數組call方法接受的是參數列表

someFunc.call(obj, 123)
someFunc.apply(obj, [123])

注意,在非嚴格模式下使用call或者apply時,若是第一個參數被指定爲nullundefined,那麼函數執行時的this指向全局對象(瀏覽器環境中是window);若是第一個參數被指定爲原始值,該原始值會被包裝。這部份內容在下文中的手寫代碼會再次講到。

call是用來實現繼承的重要方法。在子類構造函數中,經過call來調用父類構造函數,以使對象實例得到來自父類構造函數的屬性或方法。

function Father() {
  this.nationality = 'Han';
};
Father.prototype.propA = '我是父類原型上的屬性';
function Child() {
  Father.call(this);
};
Child.prototype.propB = '我是子類原型上的屬性';
var child = new Child();
child.nationality; // "Han"

call, apply, bind

callapplybind均可以綁定this,區別在於:applycall是綁定this後直接調用該函數,而bind會返回一個新的函數,並不直接調用,能夠由程序員決定調用的時機。

bind的語法形式以下:

function.bind(thisArg[, arg1[, arg2[, ...]]]) 

bindarg1, arg2, ...是給新函數預置好的參數(預置參數是可選的)。固然新函數在執行時也能夠繼續追加參數。

手寫call, apply, bind

提到callapplybind老是沒法避免手寫代碼這個話題。手寫代碼不只僅是爲了應付面試,也是幫助咱們理清思路和深刻原理的一個好方法。手寫代碼必定不要抄襲,若是實在沒思路,能夠參考下別人的代碼整理出思路,再本身按照思路獨立寫一遍代碼,而後驗證看看有沒有缺陷,這樣纔能有所收穫,不然忘得很快,只能短期應付應付。

那麼如何才能順利地手寫代碼呢?首先是要清楚一段代碼的做用,能夠從官方對於它的定義和描述入手,同時還要注意一些特殊狀況下的處理。

就拿call來講,call是函數對象的原型方法,它的做用是綁定this和參數,並執行函數。調用形式以下:

function.call(thisArg, arg1, arg2, ...) 

那麼咱們慢慢來實現它,將咱們要實現的函數命名爲myCall。首先myCall是一個函數,接受的第一個參數thisArg是目標函數執行時的this的值,從第二個可選參數arg1開始的其餘參數將做爲目標函數執行時的實參。

這裏面有不少細節要考慮,我大體羅列了一下:

  1. 要考慮是否是嚴格模式。若是是非嚴格模式,對於 thisArg要特殊處理。

  2. 如何判斷嚴格模式?

  3. thisArg被處理後還要進行非空判斷,而後考慮是以方法的形式調用仍是以普通函數的形式調用。

  4. 目標函數做爲方法調用時,如何不覆蓋對象的原有屬性?

實現代碼以下,請仔細看我寫的註釋,這是主要的思路!

// 首先apply是Function.prototype上的一個方法
Function.prototype.myCall = function() {
  // 因爲目標函數的實參數量是不定的,這裏就不寫形參了
  // 實際上經過arugments對象,咱們能拿到全部實參
  // 第一個參數是綁定的this
  var thisArg = arguments[0];
  // 接着要判斷是否是嚴格模式
  var isStrict = (function(){return this === undefined}())
  if (!isStrict) {
    // 若是在非嚴格模式下,thisArg的值是null或undefined,須要將thisArg置爲全局對象
    if (thisArg === null || thisArg === undefined) {
      // 獲取全局對象時兼顧瀏覽器環境和Node環境
      thisArg = (function(){return this}())
    } else {
      // 若是是其餘原始值,須要經過構造函數包裝成對象
      var thisArgType = typeof thisArg
      if (thisArgType === 'number') {
       thisArg = new Number(thisArg)
      } else if (thisArgType === 'string') {
        thisArg = new String(thisArg)
      } else if (thisArgType === 'boolean') {
        thisArg = new Boolean(thisArg)
      }
    }
  }
  // 截取從索引1開始的剩餘參數
  var invokeParams = [...arguments].slice(1);
  // 接下來要調用目標函數,那麼如何獲取到目標函數呢?
  // 實際上this就是目標函數,由於myCall是做爲一個方法被調用的,this固然指向調用對象,而這個對象就是目標函數
  // 這裏作這麼一個賦值過程,是爲了讓語義更清晰一點
  var invokeFunc = this;
  // 此時若是thisArg對象仍然是null或undefined,那麼說明是在嚴格模式下,而且沒有指定第一個參數或者第一個參數的值自己就是null或undefined,此時將目標函數當成普通函數執行並返回其結果便可
  if (thisArg === null || thisArg === undefined) {
    return invokeFunc(...invokeParams)
  }
  // 不然,讓目標函數成爲thisArg對象的成員方法,而後調用它
  // 直觀上來看,能夠直接把目標函數賦值給對象屬性,好比func屬性,可是可能func屬性自己就存在於thisArg對象上
  // 因此,爲了防止覆蓋掉thisArg對象的原有屬性,必須建立一個惟一的屬性名,能夠用Symbol實現,若是環境不支持Symbol,能夠經過uuid算法來構造一個惟一值。
  var uniquePropName = Symbol(thisArg)
  thisArg[uniquePropName] = invokeFunc
  // 返回目標函數執行的結果
  return thisArg[uniquePropName](...invokeParams)
}

寫完又思考了一陣,我忽然發現有個地方考慮得有點多餘了。

// 若是在非嚴格模式下,thisArg的值是null或undefined,須要將thisArg置爲全局對象
if (thisArg === null || thisArg === undefined) {
  // 獲取全局對象時兼顧瀏覽器環境和Node環境
  thisArg = (function(){return this}())
} else {

其實這種狀況下不用處理thisArg,由於代碼執行到該函數後面部分,目標函數會被做爲普通函數執行,那麼this天然指向全局對象!因此這段代碼能夠刪除了!

接着來測試一下myCall是否可靠,我寫了一個簡單的例子:

function test(a, b{
  var args = [].slice.myCall(arguments)
  console.log(arguments, args)
}
test(12)

var obj = {
  name'jack'
};
var name = 'global';
function getName() {
  return this.name;
}
getName();
getName.myCall(obj);

我不敢保證我寫的這個myCall方法沒有bug,但也算是考慮了不少狀況了。就算是在面試過程當中,面試官主要關注的就是你的思路和考慮問題的全面性,若是寫到這個程度還不能讓面試官滿意,那也無能爲力了......

理解了手寫call以後,手寫apply也天然舉一反三,只要注意兩點便可。

  • myApply接受的第二個參數是數組形式。

  • 要考慮實際調用時不傳第二個參數或者第二個參數不是數組的狀況。

直接上代碼:

Function.prototype.myApply = function(thisArg, params{
  var isStrict = (function(){return this === undefined}())
  if (!isStrict) {
    var thisArgType = typeof thisArg
    if (thisArgType === 'number') {
     thisArg = new Number(thisArg)
    } else if (thisArgType === 'string') {
      thisArg = new String(thisArg)
    } else if (thisArgType === 'boolean') {
      thisArg = new Boolean(thisArg)
    }
  }
  var invokeFunc = this;
  // 處理第二個參數
  var invokeParams = Array.isArray(params) ? params : [];
  if (thisArg === null || thisArg === undefined) {
    return invokeFunc(...invokeParams)
  }
  var uniquePropName = Symbol(thisArg)
  thisArg[uniquePropName] = invokeFunc
  return thisArg[uniquePropName](...invokeParams)
}

用比較經常使用的Math.max來測試一下:

Math.max.myApply(null, [1248]);
// 結果是8

接下來就是手寫bind了,首先要明確,bindcall, apply的不一樣點在哪裏。

  • bind返回一個新的函數。

  • 這個新的函數能夠預置參數。

好的,按照思路開始寫代碼。

Function.prototype.myBind = function() {
  // 保存要綁定的this
  var boundThis = arguments[0];
  // 得到預置參數
  var boundParams = [].slice.call(arguments1);
  // 得到綁定的目標函數
  var boundTargetFunc = this;
  if (typeof boundTargetFunc !== 'function') {
    throw new Error('綁定的目標必須是函數')
  }
  // 返回一個新的函數
  return function() {
    // 獲取執行時傳入的參數
    var restParams = [].slice.call(arguments);
    // 合併參數
    var allParams = boundParams.concat(restParams)
    // 新函數被執行時,經過執行綁定的目標函數得到結果,並返回結果
    return boundTargetFunc.apply(boundThis, allParams)
  }
}

原本寫到這以爲已經結束了,可是翻到一些資料,都提到了手寫bind須要支持new調用。仔細一想也對,bind返回一個新的函數,這個函數被做爲構造函數使用也是頗有可能的。

我首先思考的是,能不能直接判斷一個函數是否是以構造函數的形式執行的呢?若是能判斷出來,那麼問題就相對簡單了。

因而我想到構造函數中很重要的一點,那就是在構造函數中,this指向對象實例。因此,我利用instanceof改了一版代碼出來。

Function.prototype.myBind = function() {
  var boundThis = arguments[0];
  var boundParams = [].slice.call(arguments1);
  var boundTargetFunc = this;
  if (typeof boundTargetFunc !== 'function') {
    throw new Error('綁定的目標必須是函數')
  }
  function fBound () {
    var restParams = [].slice.call(arguments);
    var allParams = boundParams.concat(restParams)
    // 經過instanceof判斷this是否是fBound的實例
    var isConstructor = this instanceof fBound;
    if (isConstructor) {
      // 若是是,說明是經過new調用的(這裏有bug,見下文),那麼只要把處理好的參數傳給綁定的目標函數,並經過new調用便可。
      return new boundTargetFunc(...allParams)
    } else {
      // 若是不是,說明不是經過new調用的
      return boundTargetFunc.apply(boundThis, allParams)
    }
  }
  return fBound
}

最後看了一下MDN提供的bind函數的polyfill,發現思路有點不同,因而我經過一個實例進行對比。

function test() {}
var fBoundNative = test.bind()
var obj1 = new fBoundNative()
var fBoundMy = test.myBind()
var obj2 = new fBoundMy()
var fBoundMDN = test.mdnBind()
var obj3 = new fBoundMDN()

我發現個人寫法看起來居然更像原生bind。瞬間懷疑本身,但一會兒卻沒找到很明顯的bug......

終於我仍是意識到了一個很大的問題,obj1fBoundNative的實例,obj3fBoundMDN的實例,但obj2不是fBoundMy的實例(實際上obj2test的實例)。

obj1 instanceof fBoundNative; // true
obj2 instanceof fBoundMy; // false
obj3 instanceof fBoundMDN; // true

存在這個問題麻煩就大了,假設我要在fBoundMy.prototype上繼續擴展原型屬性或方法,obj2是沒法繼承它們的。因此最直接有效的方法就是用繼承的方法來實現,雖然不能達到原生bind的效果,但已經夠用了。因而我參考MDN改了一版。

Function.prototype.myBind = function() {
  var boundTargetFunc = this;
  if (typeof boundTargetFunc !== 'function') {
    throw new Error('綁定的目標必須是函數')
  }
  var boundThis = arguments[0];
  var boundParams = [].slice.call(arguments1);
  function fBound () {
    var restParams = [].slice.call(arguments);
    var allParams = boundParams.concat(restParams)
    return boundTargetFunc.apply(this instanceof fBound ? this : boundThis, allParams)
  }
  fBound.prototype = Object.create(boundTargetFunc.prototype || Function.prototype)
  return fBound
}

這裏面最重要的兩點:處理好原型鏈關係,以及理解bind中構造實例的過程

  • 原型鏈處理

fBound.prototype = Object.create(boundTargetFunc.prototype || Function.prototype)

這一行代碼中用了一個||運算符,||的兩端充分考慮了myBind函數的兩種可能的調用方式。

  1. 常規的函數綁定

function test(name, age{
  this.name = name;
  this.age = age;
}
var bound1 = test.myBind('小明')
var obj1 = new bound1(18)

這種狀況把fBound.prototype的原型指向boundTargetFunc.prototype,徹底符合咱們的思惟。

  1. 直接使用Function.prototype.myBind

var bound2 = Function.prototype.myBind()
var obj2 = new bound2()

這至關於建立一個新的函數,綁定的目標函數是Function.prototype。這裏必然有朋友會問了,Function.prototype也是函數嗎?是的,請看!

typeof Function.prototype; // "function"

雖然我還不知道第二種調用方式存在的意義,可是存在即合理,既然存在,咱們就支持它。

  • 理解bind中構造實例的過程

首先要清楚new的執行過程,若是您還不清楚這一點,能夠看看我寫的這篇面試官真的會問:new的實現以及無new實例化。

仍是以前那句話,先要判斷是否是以構造函數的形式調用的。核心就是這:

this instanceof fBound

咱們用一個例子再來分析下new的過程。

function test(name, age{
  this.name = name;
  this.age = age;
}
var bound1 = test.myBind('小明')
var obj1 = new bound1(18)
obj1 instanceof bound1 // true
obj1 instanceof test // true
  1. 執行構造函數 bound1,其實是執行 myBind執行後返回的新函數 fBound。首先會建立一個新對象 obj1,而且 obj1的非標準屬性 __proto__指向 bound1.prototype,其實就是 myBind執行時聲明的 fBound.prototype,而 fBound.prototype的原型指向 test.prototype。因此到這裏,原型鏈就串起來了!

  2. 執行的構造函數中, this指向這個 obj1

  3. 執行構造函數,因爲 fBound是沒有實際內容的,執行構造函數本質上仍是要去執行綁定的那個目標函數,本例中也就是 test。所以若是是以構造函數形式調用,咱們就把實例對象做爲 this傳給 test.apply

  4. 經過執行 test,對象實例被掛載了 nameage屬性,一個嶄新的對象就出爐了!

最後附上Raynos大神寫的bind實現,我感受又受到了「暴擊」!有興趣鑽研bind終極奧義的朋友請點開連接查看源碼!

this指向問題

分析this的指向,首先要肯定當前執行代碼的環境。

全局環境中的this指向

全局環境中,this指向全局對象(視宿主環境而定,瀏覽器是window,Node是global)。

函數中的this指向

在上文中介紹函數的調用形式時已經比較詳細地說過this指向問題了,這裏再簡單總結一下。

函數中this的指向取決於函數的調用形式,在一些狀況下也受到嚴格模式的影響。

  • 做爲普通函數調用:嚴格模式下, this的值是 undefined,非嚴格模式下, this指向全局對象。

  • 做爲方法調用: this指向所屬對象。

  • 做爲構造函數調用: this指向實例化的對象。

  • 經過call, apply, bind調用:若是指定了第一個參數 thisArgthis的值就是 thisArg的值(若是是原始值,會包裝爲對象);若是不傳 thisArg,要判斷嚴格模式,嚴格模式下 thisundefined,非嚴格模式下 this指向全局對象。

函數聲明和函數表達式

撕了這麼久代碼,讓大腦休息一下子,先看點輕鬆點的內容。

函數聲明

函數聲明是獨立的函數語句

function test() {}

函數聲明存在提高(Hoisting)現象,如變量提高通常,對於同名的狀況,函數聲明優於變量聲明(前者覆蓋後者,我說的是聲明階段哦)。

函數表達式

函數表達式不是獨立的函數語句,常做爲表達式的一部分,好比賦值表達式。

函數表達式能夠是命名的,也能夠是匿名的。

// 命名函數表達式
var a = function test() {}
// 匿名函數表達式
var b = function () {}

匿名函數就是沒有函數名的函數,它不能單獨使用,只能做爲表達式的一部分使用。匿名函數常以IIFE(當即執行函數表達式)的形式使用。

(function(){console.log("我是一個IIFE")}())

閉包

關於閉包,我已經寫了一篇超詳細的文章去分析了,是我的原創總結的乾貨,建議直接打開解讀閉包,此次從ECMAScript詞法環境,執行上下文提及

PS:閱讀前,您應該對ECMAScript5的一些術語有一些簡單的瞭解,好比Lexical Environment, Execution Context等。

純函數

  • 純函數是具有冪等性(對於相同的參數,任什麼時候間執行純函數都將獲得一樣的結果),它不會引發反作用。

  • 純函數與外部的關聯應該都來源於函數參數。若是一個函數直接依賴了外部變量,那它就不是純函數,由於外部變量是可變的,那麼純函數的執行結果就不可控了。

// 純函數
function pure(a, b{
  return a + b;
}
// 非純函數
function impure(c{
  return c + d
}
var d = 10;
pure(12); // 3
impure(1); // 11
d = 20;
impure(1); // 21
pure(12); // 3

惰性函數

相信你們在兼容事件監聽時,都寫過這樣的代碼。

function addEvent(element, type, handler{
  if (window.addEventListener) {
    element.addEventListener(type, handler, false);
  } else if (window.attachEvent){
    element.attachEvent('on' + type, handler);
  } else {
    element['on' + type] = handler;
  }
}

仔細看下,咱們會發現,每次調用addEvent,都會作一次if-else的判斷,這樣的工做顯然是重複的。這個時候就用到惰性函數了。

惰性函數表示函數執行的分支只會在函數第一次調用的時候執行。後續咱們所使用的就是這個函數執行的結果。

利用惰性函數的思惟,咱們能夠改造下上述代碼。

function addEvent(element, type, handler{
  if (window.addEventListener) {
    addEvent = function(element, type, handler{
      element.addEventListener(type, handler, false);
    }
  } else if (window.attachEvent){
    addEvent = function(element, type, handler{
      element.attachEvent('on' + type, handler);
    }
  } else {
    addEvent = function(element, type, handler{
      element['on' + type] = handler;
    }
  }
  addEvent(type, element, fun);
}

這代碼看起來有點low,可是它確實減小了重複的判斷。在這種方式下,函數第一次執行時才肯定真正的值。

咱們還能夠利用IIFE提早肯定函數真正的值。

var addEvent = (function() {
  if (window.addEventListener) {
    return function(element, type, handler{
      element.addEventListener(type, handler, false);
    }
  } else if (window.attachEvent){
    return function(element, type, handler{
      element.attachEvent('on' + type, handler);
    }
  } else {
    return function(element, type, handler{
      element['on' + type] = handler;
    }
  }
}())

高階函數

函數在javascript中是一等公民,函數能夠做爲參數傳給其餘函數,這讓函數的使用充滿了各類可能性。

不如來看看維基百科中高階函數(High-Order Function)的定義:

在數學和計算機科學中,高階函數是至少知足下列一個條件的函數:

  1. 接受一個或多個函數做爲輸入

  2. 輸出一個函數

看到這,你們應該都意識到了,平時使用過不少高階函數。數組的一些高階函數使用得尤其頻繁。

[1234].forEach(function(item, index, arr{
  console.log(item, index, arr)
})
[1234].map(item => `小老弟${item}`)

能夠發現,傳入forEachmap的就是一個函數。咱們本身也能夠封裝一些複用的高階函數。

咱們知道Math.max能夠求出參數列表中最大的值。然而不少時候,咱們須要處理的數據並非1, 2, 3, 4這麼簡單,而是對象數組。

假設有這麼一個需求,存在一個數組,數組元素都是表示人的對象,咱們想從數組中選出年紀最大的人。

這個時候,就須要一個高階函數來完成。

/**  * 根據求值條件判斷數組中最大的項  * @param {Array} arr 數組  * @param {String|Function} iteratee 返回一個求值表達式,能夠根據對象屬性的值求出最大項,好比item.age。也能夠經過自定義函數返回求值表達式。  */
function maxBy(arr, iteratee{
    let values = [];
    if (typeof iteratee === 'string') {
        values = arr.map(item => item[iteratee]);
    } else if (typeof iteratee === 'function') {
        values = arr.map((item, index) => {
            return iteratee(item, index, arr);
        });
    }
    const maxOne = Math.max(...values);
    const maxIndex = values.findIndex(item => item === maxOne);
    return arr[maxIndex];
}

利用這個高階函數,咱們就能夠求出數組中年紀最大的那我的。

var list = [
  {name'小明'age18},
  {name'小紅'age19},
  {name'小李'age20}
]
// 根據age字段求出最大項,結果是小李。
var maxItem = maxBy(list, 'age');

咱們甚至能夠定義更復雜的求值規則,好比咱們須要根據一個字符串類型的屬性來斷定優先級。這個時候,就必須傳一個自定義的函數做爲參數了。

const list = [
  {name'小明'priority'middle'},
  {name'小紅'priority'low'},
  {name'小李'priority'high'}
]
const maxItem = maxBy(list, function(item{
  const { priority } = item
  const priorityValue = priority === 'low' ? 1 : priority === 'middle' ? 2 : priority === 'high' ? 3 : 0
  return priorityValue;
});

maxBy接受的參數最終都應該能轉化爲一個Math.max可度量的值,不然就沒有可比較性了。

要理解這樣的高階函數,咱們能夠認爲傳給高階函數的函數就是一箇中間件,它把數據預處理好了,而後再轉交給高階函數繼續運算

PS:寫完這句總結,忽然以爲挺有道理的,反手給本身一個贊!

柯里化

說柯里化以前,首先拋出一個疑問,如何實現一個add函數,使得這個add函數能夠靈活調用和傳參,支持下面的調用示例呢?

add(123// 6
add(1// 1
add(1)(2// 3
add(12)(3// 6
add(1)(2)(3// 6
add(1)(2)(3)(4// 10

要解答這樣的疑問,仍是要先明白什麼是柯里化。

在計算機科學中,柯里化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數且返回結果的新函數的技術。

這段解釋看着仍是挺懵逼的,不如舉個例子:

原本有這麼一個求和函數dynamicAdd(),接受任意個參數。

function dynamicAdd() {
  return [...arguments].reduce((prev, curr) => {
    return prev + curr
  }, 0)
}

如今須要經過柯里化把它變成一個新的函數,這個新的函數預置了第一個參數,而且能夠在調用時繼續傳入剩餘參數。

看到這,我以爲有點似曾相識,預置參數的特性與bind很相像。那麼咱們不如用bind的思路來實現。

function curry(fn, firstArg{
  // 返回一個新函數
  return function() {
    // 新函數調用時會繼續傳參
    var restArgs = [].slice.call(arguments)
    // 參數合併,經過apply調用原函數
    return fn.apply(this, [firstArg, ...restArgs])
  }
}

接着咱們經過一些例子來感覺一下柯里化。

// 柯里化,預置參數10
var add10 = curry(dynamicAdd, 10)
add10(5); // 15
// 柯里化,預置參數20
var add20 = curry(dynamicAdd, 20);
add20(5); // 25
// 也能夠對一個已經柯里化的函數add10繼續柯里化,此時預置參數10便可
var anotherAdd20 = curry(add10, 10);
anotherAdd20(5); // 25

能夠發現,柯里化是在一個函數的基礎上進行變換,獲得一個新的預置了參數的函數。最後在調用新函數時,實際上仍是會調用柯里化前的原函數。

而且柯里化獲得的新函數能夠繼續被柯里化,這看起來有點像俄羅斯套娃的感受。

實際使用時也會出現柯里化的變體,不侷限於只預置一個參數

function curry(fn{
  // 保存預置參數
  var presetArgs = [].slice.call(arguments1)
  // 返回一個新函數
  return function() {
    // 新函數調用時會繼續傳參
    var restArgs = [].slice.call(arguments)
    // 參數合併,經過apply調用原函數
    return fn.apply(this, [...presetArgs, ...restArgs])
  }
}

其實Function.protoype.bind就是一個柯里化的實現。不只如此,不少流行的庫都大量使用了柯里化的思想。

實際應用中,被柯里化的原函數的參數多是定長的,也多是不定長的。

參數定長的柯里化

假設存在一個原函數fnfn接受三個參數a, b, c,那麼函數fn最多被柯里化三次(有效地綁定參數算一次)。

function fn(a, b, c{
  return a + b + c
}
var c1 = curry(fn, 1);
var c2 = curry(c1, 2);
var c3 = curry(c2, 3);
c3(); // 6
// 再次柯里化也沒有意義,原函數只須要三個參數
var c4 = curry(c3, 4);
c4();

也就是說,咱們能夠經過柯里化緩存的參數數量,來判斷是否到達了執行時機。那麼咱們就獲得了一個柯里化的通用模式。

function curry(fn{
  // 獲取原函數的參數長度
  const argLen = fn.length;
  // 保存預置參數
  const presetArgs = [].slice.call(arguments1)
  // 返回一個新函數
  return function() {
    // 新函數調用時會繼續傳參
    const restArgs = [].slice.call(arguments)
    const allArgs = [...presetArgs, ...restArgs]
    if (allArgs.length >= argLen) {
      // 若是參數夠了,就執行原函數
      return fn.apply(this, allArgs)
    } else {
      // 不然繼續柯里化
      return curry.call(null, fn, ...allArgs)
    }
  }
}

這樣一來,咱們的寫法就能夠支持如下形式。

function fn(a, b, c{
  return a + b + c;
}
var curried = curry(fn);
curried(123); // 6
curried(12)(3); // 6
curried(1)(23); // 6
curried(1)(2)(3); // 6
curried(7)(8)(9); // 24

參數不定長的柯里化

解決了上面的問題,咱們不免會問本身,假設原函數的參數不定長呢,這種狀況如何柯里化?

首先,咱們須要理解參數不定長是指函數聲明時不約定具體的參數,而在函數體中經過arguments獲取實參,而後進行運算。就像下面這種。

function dynamicAdd() {
  return [...arguments].reduce((prev, curr) => {
    return prev + curr
  }, 0)
}

回到最開始的問題,怎麼支持下面的全部調用形式?

add(123// 6
add(1// 1
add(1)(2// 3
add(12)(3// 6
add(1)(2)(3// 6
add(1)(2)(3)(4// 10

思考了一陣,我發如今參數不定長的狀況下,要同時支持1~N次調用仍是挺難的。add(1)在一次調用後能夠直接返回一個值,但它也能夠做爲函數接着調用add(1)(2),甚至能夠繼續add(1)(2)(3)。那麼咱們實現add函數時,究竟是返回一個函數,仍是返回一個值呢?這讓人挺犯難的,我也不能預測這個函數將如何被調用啊。

並且咱們能夠拿上面的成果來驗證下:

curried(1)(2)(3)(4);

運行上面的代碼會報錯:Uncaught TypeError: curried(...)(...)(...) is not a function,由於執行到curried(1)(2)(3),結果就不是一個函數了,而是一個值,一個值固然是不能做爲函數繼續執行的。

因此若是要支持參數不定長的場景,已經柯里化的函數在執行完畢時不能返回一個值,只能返回一個函數;同時要讓JS引擎在解析獲得的這個結果時,能求出咱們預期的值。

你們看了這個可能仍是不懂,好,說人話!咱們實現的curry應該知足:

  1. curry處理,獲得一個新函數,這一點不變。

// curry是一個函數
var curried = curry(add);
  1. 新函數執行後仍然返回一個結果函數。

// curried10也是一個函數
var curried10 = curried(10);
var curried30 = curried10(20);
  1. 結果函數能夠被Javascript引擎解析,獲得一個預期的值。

curried10; // 10

好,關鍵點在於3,如何讓Javascript引擎按咱們的預期進行解析,這就回到Javascript基礎了。在解析一個函數的原始值時,會用到toString

咱們知道,console.log(fn)能夠把函數fn的源碼輸出,以下所示:

console.log(fn)
ƒ fn(a, b, c) {
  return a + b + c;
}

那麼咱們只要重寫toString,就能夠巧妙地實現咱們的需求了。

function curry(fn{
  // 保存預置參數
  const presetArgs = [].slice.call(arguments1)
  // 返回一個新函數
  function curried () {
    // 新函數調用時會繼續傳參
    const restArgs = [].slice.call(arguments)
    const allArgs = [...presetArgs, ...restArgs]
    return curry.call(null, fn, ...allArgs)
  }
  // 重寫toString
  curried.toString = function() {
    return fn.apply(null, presetArgs)
  }
  return curried;
}

這樣一來,魔性的add用法就都被支持了。

function dynamicAdd() {
  return [...arguments].reduce((prev, curr) => {
    return prev + curr
  }, 0)
}
var add = curry(dynamicAdd);
add(1)(2)(3)(4// 10
add(12)(34)(56// 21

至於爲何是重寫toString,而不是重寫valueOf,這裏留個懸念,你們能夠想想,也歡迎與我交流!

柯里化總結

柯里化是一種函數式編程思想,實際上在項目中可能用得少,或者說用得不深刻,可是若是你掌握了這種思想,也許在將來的某個時間點,你會用得上!

大概來講,柯里化有以下特色:

  • 簡潔代碼:柯里化應用在較複雜的場景中,有簡潔代碼,可讀性高的優勢。

  • 參數複用:公共的參數已經經過柯里化預置了。

  • 延遲執行:柯里化時只是返回一個預置參數的新函數,並無馬上執行,實際上在知足條件後纔會執行。

  • 管道式流水線編程:利於使用函數組裝管道式的流水線工序,不污染原函數。

小結

本文是筆者回顧函數知識點時總結的一篇很是詳細的文章。在理解一些晦澀的知識模塊時,我加入了一些我的解讀,相信對於想要深究細節的朋友會有一些幫助。若是您以爲這篇文章有所幫助,請無情地關注點贊支持一下吧!同時也歡迎加我微信laobaife一塊兒交流學習。

 

❤️ 愛心三連

  1. 若是您以爲這篇文章質量還不錯,請滑動至下方,點擊在看、分享 、點贊 ,讓更多人看到優質的文章。

  2. 長按下方二維碼識別並關注公衆號,您的鼓勵意義重大。

  3. 進入公衆號,在對話框內回覆「加羣」,進入技術交流羣,一塊兒交流學習成長。

 

 

 

本文分享自微信公衆號 - 大前端技術沙龍(is_coder)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索