我們不背誦 API关面,只實(shí)現(xiàn) API

有不少剛?cè)胄械耐瑢W(xué)跟我說(shuō):“JavaScript 很多 API 記不清楚怎么辦坦袍?數(shù)組的這方法、那方法總是傻傻分不清楚等太,該如何是好捂齐?操作 DOM 的方式今天記,明天忘缩抡,真讓人奔潰奠宜!

甚至有的開發(fā)者在討論面試時(shí),總向我抱怨:“面試官總愛糾結(jié) API 的使用瞻想,甚至 jQuery 某些方法的參數(shù)順序都需要讓我說(shuō)清楚压真!

我認(rèn)為,對(duì)于反復(fù)使用的方法蘑险,所有人都要做到“機(jī)械記憶”滴肿,能夠反手寫出。一些貌似永遠(yuǎn)記不清的 API 只是因?yàn)橛玫貌粔蚨喽选?/p>

在做面試官時(shí)佃迄,我從來(lái)不強(qiáng)求開發(fā)者準(zhǔn)確無(wú)誤地“背誦” API泼差。相反,我喜歡從另外一個(gè)角度來(lái)考察面試者:“既然記不清使用方法呵俏,那么我告訴你它的使用方法堆缘,你來(lái)實(shí)現(xiàn)一個(gè)吧!”實(shí)現(xiàn)一個(gè) API普碎,除了可以考察面試者對(duì)這個(gè) API 的理解吼肥,更能體現(xiàn)開發(fā)者的編程思維和代碼能力。對(duì)于積極上進(jìn)的前端工程師随常,模仿并實(shí)現(xiàn)一些經(jīng)典方法潜沦,應(yīng)該是“家常便飯”肩榕,這是比較基本的要求扣典。

本小節(jié),我根據(jù)了解的面試題目和作為面試官的經(jīng)歷脑慧,挑了幾個(gè)典型的 API枣察,通過(guò)對(duì)其不同程度争占,不同方式的實(shí)現(xiàn),來(lái)覆蓋 JavaScript 中的部分知識(shí)點(diǎn)和編程要領(lǐng)序目。通過(guò)學(xué)習(xí)本節(jié)內(nèi)容臂痕,期待你不僅能領(lǐng)會(huì)代碼奧義,更應(yīng)該學(xué)習(xí)舉一反三的方法猿涨。

API 主題的相關(guān)知識(shí)點(diǎn)如下:

目錄

jQuery offset 實(shí)現(xiàn)

這個(gè)話題演變自今日頭條某部門面試題握童。當(dāng)時(shí)面試官提問:“如何獲取文檔中任意一個(gè)元素距離文檔 document 頂部的距離?”

熟悉 jQuery 的同學(xué)應(yīng)該對(duì) offset 方法并不陌生叛赚,它返回或設(shè)置匹配元素相對(duì)于文檔的偏移(位置)澡绩。這個(gè)方法返回的對(duì)象包含兩個(gè)整型屬性:topleft稽揭,以像素計(jì)。如果可以使用 jQuery肥卡, 我們可以直接調(diào)取該 API 獲得結(jié)果溪掀。但是,如果用原生 JavaScript 實(shí)現(xiàn)步鉴,也就是說(shuō)手動(dòng)實(shí)現(xiàn) jQuery offset 方法揪胃,該如何著手呢?

主要有兩種思路:

  • 通過(guò)遞歸實(shí)現(xiàn)
  • 通過(guò) getBoundingClientRect API 實(shí)現(xiàn)

遞歸實(shí)現(xiàn)方案

我們通過(guò)遍歷目標(biāo)元素氛琢、目標(biāo)元素的父節(jié)點(diǎn)喊递、父節(jié)點(diǎn)的父節(jié)點(diǎn)......依次溯源,并累加這些遍歷過(guò)的節(jié)點(diǎn)相對(duì)于其最近祖先節(jié)點(diǎn)(且 position 屬性非 static)的偏移量艺沼,向上直到 document册舞,累加即可得到結(jié)果。

其中障般,我們需要使用 JavaScript 的 offsetTop 來(lái)訪問一個(gè) DOM 節(jié)點(diǎn)上邊框相對(duì)離其本身最近调鲸、且 position 值為非 static 的祖先元素的垂直偏移量。具體實(shí)現(xiàn)為:

const offset = ele => {
    let result = {
        top: 0,
        left: 0
    }

    // 當(dāng)前 DOM 節(jié)點(diǎn)的 display === 'none' 時(shí), 直接返回 {top: 0, left: 0}
    if (window.getComputedStyle(ele)['display'] === 'none') {
        return result
    }
 
    let position

    const getOffset = (node, init) => {
        if (node.nodeType !== 1) {
            return
        }

        position = window.getComputedStyle(node)['position']
 
        if (typeof(init) === 'undefined' && position === 'static') {
            getOffset(node.parentNode)
            return
        }

        result.top = node.offsetTop + result.top - node.scrollTop
        result.left = node.offsetLeft + result.left - node.scrollLeft
 
        if (position === 'fixed') {
            return
        }
 
        getOffset(node.parentNode)
    }
 
    getOffset(ele, true)
 
    return result
}

上述代碼并不難理解挽荡,使用遞歸實(shí)現(xiàn)藐石。如果節(jié)點(diǎn) node.nodeType 類型不是 Element(1),則跳出定拟;如果相關(guān)節(jié)點(diǎn)的 position 屬性為 static于微,則不計(jì)入計(jì)算,進(jìn)入下一個(gè)節(jié)點(diǎn)(其父節(jié)點(diǎn))的遞歸青自。如果相關(guān)屬性的 display 屬性為 none株依,則應(yīng)該直接返回 0 作為結(jié)果。

這個(gè)實(shí)現(xiàn)很好地考察了開發(fā)者對(duì)于遞歸的初級(jí)應(yīng)用延窜、以及對(duì) JavaScript 方法的掌握程度恋腕。

接下來(lái),我們換一種思路逆瑞,用一個(gè)相對(duì)較新的 API: getBoundingClientRect 來(lái)實(shí)現(xiàn) jQuery offset 方法荠藤。

getBoundingClientRect 方法

getBoundingClientRect 方法用來(lái)描述一個(gè)元素的具體位置,這個(gè)位置的下面四個(gè)屬性都是相對(duì)于視口左上角的位置而言的获高。對(duì)某一節(jié)點(diǎn)執(zhí)行該方法哈肖,它的返回值是一個(gè) DOMRect 類型的對(duì)象。這個(gè)對(duì)象表示一個(gè)矩形盒子念秧,它含有:left淤井、toprightbottom 等只讀屬性。

圖示

請(qǐng)參考實(shí)現(xiàn):

const offset = ele => {
    let result = {
        top: 0,
        left: 0
    }
    // 當(dāng)前為 IE11 以下庄吼,直接返回 {top: 0, left: 0}
    if (!ele.getClientRects().length) {
        return result
    }

    // 當(dāng)前 DOM 節(jié)點(diǎn)的 display === 'none' 時(shí)缎除,直接返回 {top: 0, left: 0}
    if (window.getComputedStyle(ele)['display'] === 'none') {
        return result
    }

    result = ele.getBoundingClientRect()
    var docElement = ele.ownerDocument.documentElement

    return {
        top: result.top + window.pageYOffset - docElement.clientTop,
        left: result.left + window.pageXOffset - docElement.clientLeft
    }
}

需要注意的細(xì)節(jié)有:

  • node.ownerDocument.documentElement 的用法可能大家比較陌生严就,ownerDocument 是 DOM 節(jié)點(diǎn)的一個(gè)屬性总寻,它返回當(dāng)前節(jié)點(diǎn)的頂層的 document 對(duì)象。ownerDocument 是文檔梢为,documentElement 是根節(jié)點(diǎn)渐行。事實(shí)上,ownerDocument 下含 2 個(gè)節(jié)點(diǎn):

    • <!DocType>
    • documentElement

    docElement.clientTop铸董,clientTop 是一個(gè)元素頂部邊框的寬度祟印,不包括頂部外邊距或內(nèi)邊距。

  • 除此之外粟害,該方法實(shí)現(xiàn)就是簡(jiǎn)單的幾何運(yùn)算蕴忆,邊界 case 和兼容性處理,也并不難理解悲幅。

從這道題目看出套鹅,相比考察“死記硬背” API,這樣的實(shí)現(xiàn)更有意義汰具。站在面試官的角度卓鹿,我往往會(huì)給面試者(開發(fā)者)提供相關(guān)的方法提示,以引導(dǎo)其給出最后的方案實(shí)現(xiàn)留荔。

數(shù)組 reduce 方法的相關(guān)實(shí)現(xiàn)

數(shù)組方法非常重要:因?yàn)閿?shù)組就是數(shù)據(jù)吟孙,數(shù)據(jù)就是狀態(tài),狀態(tài)反應(yīng)著視圖聚蝶。對(duì)數(shù)組的操作我們不能陌生杰妓,其中 reduce 方法更要做到駕輕就熟。我認(rèn)為這個(gè)方法很好地體現(xiàn)了“函數(shù)式”理念碘勉,也是當(dāng)前非常熱門的考察點(diǎn)之一巷挥。

我們知道 reduce 方法是 ES5 引入的,reduce 英文解釋翻譯過(guò)來(lái)為“減少恰聘,縮小句各,使還原,使變?nèi)酢鼻邕叮琈DN 對(duì)該方法直述為:

The reduce method applies a function against an accumulator and each value of the array (from left-to-right) to reduce it to a single value.

它的使用語(yǔ)法:

arr.reduce(callback[, initialValue])

這里我們簡(jiǎn)要介紹一下凿宾。

  • reduce 第一個(gè)參數(shù) callback 是核心,它對(duì)數(shù)組的每一項(xiàng)進(jìn)行“疊加加工”兼蕊,其最后一次返回值將作為 reduce 方法的最終返回值初厚。 它包含 4 個(gè)參數(shù):
    • previousValue 表示“上一次” callback 函數(shù)的返回值
    • currentValue 數(shù)組遍歷中正在處理的元素
    • currentIndex 可選,表示 currentValue 在數(shù)組中對(duì)應(yīng)的索引。如果提供了 initialValue产禾,則起始索引號(hào)為 0排作,否則為 1
    • array 可選,調(diào)用 reduce() 的數(shù)組
  • initialValue 可選亚情,作為第一次調(diào)用 callback 時(shí)的第一個(gè)參數(shù)妄痪。如果沒有提供 initialValue,那么數(shù)組中的第一個(gè)元素將作為 callback 的第一個(gè)參數(shù)楞件。

reduce 實(shí)現(xiàn) runPromiseInSequence

我們看它的一個(gè)典型應(yīng)用:按順序運(yùn)行 Promise:

const runPromiseInSequence = (array, value) => array.reduce(
    (promiseChain, currentFunction) => promiseChain.then(currentFunction),
    Promise.resolve(value)
)

runPromiseInSequence 方法將會(huì)被一個(gè)每一項(xiàng)都返回一個(gè) Promise 的數(shù)組調(diào)用衫生,并且依次執(zhí)行數(shù)組中的每一個(gè) Promise,請(qǐng)讀者仔細(xì)體會(huì)土浸。如果覺得晦澀罪针,可以參考示例:

const f1 = () => new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('p1 running')
        resolve(1)
    }, 1000)
})

const f2 = () => new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('p2 running')
        resolve(2)
    }, 1000)
})


const array = [f1, f2]

const runPromiseInSequence = (array, value) => array.reduce(
    (promiseChain, currentFunction) => promiseChain.then(currentFunction),
    Promise.resolve(value)
)

runPromiseInSequence(array, 'init')

執(zhí)行結(jié)果如下圖:

代碼執(zhí)行

reduce 實(shí)現(xiàn) pipe

reduce 的另外一個(gè)典型應(yīng)用可以參考函數(shù)式方法 pipe 的實(shí)現(xiàn):pipe(f, g, h) 是一個(gè) curry 化函數(shù),它返回一個(gè)新的函數(shù)黄伊,這個(gè)新的函數(shù)將會(huì)完成 (...args) => h(g(f(...args))) 的調(diào)用泪酱。即 pipe 方法返回的函數(shù)會(huì)接收一個(gè)參數(shù),這個(gè)參數(shù)傳遞給 pipe 方法第一個(gè)參數(shù)还最,以供其調(diào)用墓阀。

const pipe = (...functions) => input => functions.reduce(
    (acc, fn) => fn(acc),
    input
)

仔細(xì)體會(huì) runPromiseInSequencepipe 這兩個(gè)方法,它們都是 reduce 應(yīng)用的典型場(chǎng)景憋活。

實(shí)現(xiàn)一個(gè) reduce

那么我們?cè)撊绾螌?shí)現(xiàn)一個(gè) reduce 呢岂津?參考來(lái)自 MDN 的 polyfill:

if (!Array.prototype.reduce) {
  Object.defineProperty(Array.prototype, 'reduce', {
    value: function(callback /*, initialValue*/) {
      if (this === null) {
        throw new TypeError( 'Array.prototype.reduce ' + 
          'called on null or undefined' )
      }
      if (typeof callback !== 'function') {
        throw new TypeError( callback +
          ' is not a function')
      }
    
      var o = Object(this)
    
      var len = o.length >>> 0
    
      var k = 0
      var value
    
      if (arguments.length >= 2) {
        value = arguments[1]
      } else {
        while (k < len && !(k in o)) {
          k++
        }
    
        if (k >= len) {
          throw new TypeError( 'Reduce of empty array ' +
            'with no initial value' )
        }
        value = o[k++]
      }
    
      while (k < len) {
        if (k in o) {
          value = callback(value, o[k], k, o)
        }
    
        k++
      }
    
      return value
    }
  })
}

上述代碼中使用了 value 作為初始值,并通過(guò) while 循環(huán)悦即,依次累加計(jì)算出 value 結(jié)果并輸出吮成。但是相比 MDN 上述實(shí)現(xiàn),我個(gè)人更喜歡的實(shí)現(xiàn)方案是:

Array.prototype.reduce = Array.prototype.reduce || function(func, initialValue) {
    var arr = this
    var base = typeof initialValue === 'undefined' ? arr[0] : initialValue
    var startPoint = typeof initialValue === 'undefined' ? 1 : 0
    arr.slice(startPoint)
        .forEach(function(val, index) {
            base = func(base, val, index + startPoint, arr)
        })
    return base
}

核心原理就是使用 forEach 來(lái)代替 while 實(shí)現(xiàn)結(jié)果的累加辜梳,它們本質(zhì)上是相同的粱甫。

我也同樣看了下 ES5-shim 里的 pollyfill,跟上述思路完全一致作瞄。唯一的區(qū)別在于:我用了 forEach 迭代而 ES5-shim 使用的是簡(jiǎn)單的 for 循環(huán)茶宵。實(shí)際上,如果“杠精”一些宗挥,我們會(huì)指出數(shù)組的 forEach 方法也是 ES5 新增的乌庶。因此,用 ES5 的一個(gè) API(forEach)契耿,去實(shí)現(xiàn)另外一個(gè) ES5 的 API(reduce)瞒大,這并沒什么實(shí)際意義——這里的 pollyfill 就是在不兼容 ES5 的情況下,模擬的降級(jí)方案搪桂。此處不多做追究透敌,因?yàn)楦灸康倪€是希望讀者對(duì) reduce 有一個(gè)全面透徹的了解。

通過(guò) Koa only 模塊源碼認(rèn)識(shí) reduce

通過(guò)了解并實(shí)現(xiàn) reduce 方法,我們對(duì)它已經(jīng)有了比較深入的認(rèn)識(shí)酗电。最后魄藕,我們?cè)賮?lái)看一個(gè) reduce 使用示例——通過(guò) Koa 源碼的 only 模塊,加深印象:

var o = {
    a: 'a',
    b: 'b',
    c: 'c'
}
only(o, ['a','b'])   // {a: 'a',  b: 'b'}

該方法返回一個(gè)經(jīng)過(guò)指定篩選屬性的新對(duì)象撵术。
?
only 模塊實(shí)現(xiàn):

var only = function(obj, keys){
    obj = obj || {}
    if ('string' == typeof keys) keys = keys.split(/ +/)
    return keys.reduce(function(ret, key) {
        if (null == obj[key]) return ret
        ret[key] = obj[key]
        return ret
    }, {})
}

小小的 reduce 及其衍生場(chǎng)景有很多值得我們玩味背率、探究的地方。舉一反三荷荤,活學(xué)活用是技術(shù)進(jìn)階的關(guān)鍵退渗。

compose 實(shí)現(xiàn)的幾種方案

函數(shù)式理念——這一古老的概念如今在前端領(lǐng)域“遍地開花”。函數(shù)式很多思想都值得借鑒蕴纳,其中一個(gè)細(xì)節(jié):compose 因?yàn)槠淝擅畹脑O(shè)計(jì)而被廣泛運(yùn)用。對(duì)于它的實(shí)現(xiàn)个粱,從面向過(guò)程式到函數(shù)式實(shí)現(xiàn)古毛,風(fēng)格迥異,值得我們探究都许。在面試當(dāng)中稻薇,也經(jīng)常有面試官要求實(shí)現(xiàn) compose 方法,我們先看什么是 compose胶征。

compose 其實(shí)和前面提到的 pipe 一樣塞椎,就是執(zhí)行一連串不定長(zhǎng)度的任務(wù)(方法),比如:

let funcs = [fn1, fn2, fn3, fn4]
let composeFunc = compose(...funcs)

執(zhí)行:

composeFunc(args)

就相當(dāng)于:

fn1(fn2(fn3(fn4(args))))

總結(jié)一下 compose 方法的關(guān)鍵點(diǎn):

  • compose 的參數(shù)是函數(shù)數(shù)組睛低,返回的也是一個(gè)函數(shù)
  • compose 的參數(shù)是任意長(zhǎng)度的案狠,所有的參數(shù)都是函數(shù),執(zhí)行方向是自右向左的钱雷,因此初始函數(shù)一定放到參數(shù)的最右面
  • compose 執(zhí)行后返回的函數(shù)可以接收參數(shù)骂铁,這個(gè)參數(shù)將作為初始函數(shù)的參數(shù),所以初始函數(shù)的參數(shù)是多元的罩抗,初始函數(shù)的返回結(jié)果將作為下一個(gè)函數(shù)的參數(shù)拉庵,以此類推。因此除了初始函數(shù)之外套蒂,其他函數(shù)的接收值是一元的钞支。

我們發(fā)現(xiàn),實(shí)際上操刀,composepipe 的差別只在于調(diào)用順序的不同:

// compose
fn1(fn2(fn3(fn4(args))))
    
// pipe
fn4(fn3(fn2(fn1(args))))

即然跟我們先前實(shí)現(xiàn)的 pipe 方法如出一轍烁挟,那么還有什么好深入分析的呢?請(qǐng)繼續(xù)閱讀馍刮,看看還能玩出什么花兒來(lái)信夫。

compose 最簡(jiǎn)單的實(shí)現(xiàn)是面向過(guò)程的:

const compose = function(...args) {
    let length = args.length
    let count = length - 1
    let result
    return function f1 (...arg1) {
        result = args[count].apply(this, arg1)
        if (count <= 0) {
            count = length - 1
            return result
        }
        count--
        return f1.call(null, result)
    }
}

這里的關(guān)鍵是用到了閉包,使用閉包變量?jī)?chǔ)存結(jié)果 result 和函數(shù)數(shù)組長(zhǎng)度以及遍歷索引,并利用遞歸思想静稻,進(jìn)行結(jié)果的累加計(jì)算警没。整體實(shí)現(xiàn)符合正常的面向過(guò)程思維,不難理解振湾。

聰明的同學(xué)可能也會(huì)意識(shí)到杀迹,利用上文所講的 reduce 方法,應(yīng)該能更函數(shù)式地解決問題:

const reduceFunc = (f, g) => (...arg) => g.call(this, f.apply(this, arg))
const compose = (...args) => args.reverse().reduce(reduceFunc, args.shift())

通過(guò)前面的學(xué)習(xí)押搪,結(jié)合 call树酪、apply 方法,這樣的實(shí)現(xiàn)并不難理解大州。

我們繼續(xù)開拓思路续语,“既然涉及串聯(lián)和流程控制”,那么我們還可以使用 Promise 實(shí)現(xiàn):

const compose = (...args) => {
    let init = args.pop()
    return (...arg) => 
    args.reverse().reduce((sequence, func) => 
      sequence.then(result => func.call(null, result))
    , Promise.resolve(init.apply(null, arg)))
}

這種實(shí)現(xiàn)利用了 Promise 特性:首先通過(guò) Promise.resolve(init.apply(null, arg)) 啟動(dòng)邏輯厦画,啟動(dòng)一個(gè) resolve 值為最后一個(gè)函數(shù)接收參數(shù)后的返回值疮茄,依次執(zhí)行函數(shù)。因?yàn)?promise.then() 仍然返回一個(gè) Promise 類型值根暑,所以 reduce 完全可以按照 Promise 實(shí)例執(zhí)行下去力试。

既然能夠使用 Promise 實(shí)現(xiàn),那么 generator 當(dāng)然應(yīng)該也可以實(shí)現(xiàn)排嫌。這里給大家留一個(gè)思考題畸裳,感興趣的同學(xué)可以嘗試,歡迎在評(píng)論區(qū)討論淳地。

最后怖糊,我們?cè)倏聪律鐓^(qū)上著名的 lodash 和 Redux 的實(shí)現(xiàn)。

lodash 版本

// lodash 版本
var compose = function(funcs) {
    var length = funcs.length
    var index = length
    while (index--) {
        if (typeof funcs[index] !== 'function') {
            throw new TypeError('Expected a function');
        }
    }
    return function(...args) {
        var index = 0
        var result = length ? funcs.reverse()[index].apply(this, args) : args[0]
        while (++index < length) {
            result = funcs[index].call(this, result)
        }
        return result
    }
}

lodash 版本更像我們的第一種實(shí)現(xiàn)方式薇芝,理解起來(lái)也更容易蓬抄。

Redux 版本

// Redux 版本
function compose(...funcs) {
    if (funcs.length === 0) {
        return arg => arg
    }
    
    if (funcs.length === 1) {
        return funcs[0]
    }
    
    return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

總之,還是充分利用了數(shù)組的 reduce 方法夯到。

函數(shù)式概念確實(shí)有些抽象嚷缭,需要開發(fā)者仔細(xì)琢磨,并動(dòng)手調(diào)試耍贾。一旦頓悟阅爽,必然會(huì)感受到其中的優(yōu)雅和簡(jiǎn)潔。

apply荐开、bind 進(jìn)階實(shí)現(xiàn)

面試中關(guān)于 this 綁定的相關(guān)話題如今已經(jīng)“泛濫”付翁,同時(shí)對(duì) bind 方法的實(shí)現(xiàn),社區(qū)上也有相關(guān)討論晃听。但是很多內(nèi)容尚不系統(tǒng)百侧,且存在一些瑕疵砰识。這里簡(jiǎn)單摘錄我 2017 年年初寫的文章 從一道面試題,到“我可能看了假源碼” 來(lái)遞進(jìn)討論佣渴。在《一網(wǎng)打盡 this》一課辫狼,我們介紹過(guò)對(duì) bind 的實(shí)現(xiàn),這里我們進(jìn)一步展開辛润。

此處不再贅述 bind 函數(shù)的使用膨处,尚不清楚的讀者可以自行補(bǔ)充一下基礎(chǔ)知識(shí)。我們先來(lái)看一個(gè)初級(jí)實(shí)現(xiàn)版本:

Function.prototype.bind = Function.prototype.bind || function (context) {
    var me = this;
    var argsArray = Array.prototype.slice.call(arguments);
    return function () {
        return me.apply(context, argsArray.slice(1))
    }
}

這是一般合格開發(fā)者提供的答案砂竖,如果面試者能寫到這里真椿,給他 60 分。

先簡(jiǎn)要解讀一下:

基本原理是使用 apply 進(jìn)行模擬 bind乎澄。函數(shù)體內(nèi)的 this 就是需要綁定 this 的函數(shù)突硝,或者說(shuō)是原函數(shù)。最后使用 apply 來(lái)進(jìn)行參數(shù)(context)綁定三圆,并返回狞换。

與此同時(shí),將第一個(gè)參數(shù)(context)以外的其他參數(shù)舟肉,作為提供給原函數(shù)的預(yù)設(shè)參數(shù),這也是基本的“ curry 化”基礎(chǔ)查库。

上述實(shí)現(xiàn)方式路媚,我們返回的參數(shù)列表里包含:argsArray.slice(1)它的問題在于存在預(yù)置參數(shù)功能丟失的現(xiàn)象樊销。

想象我們返回的綁定函數(shù)中整慎,如果想實(shí)現(xiàn)預(yù)設(shè)傳參(就像 bind 所實(shí)現(xiàn)的那樣),就面臨尷尬的局面围苫。真正實(shí)現(xiàn)“ curry 化”的“完美方式”是:

Function.prototype.bind = Function.prototype.bind || function (context) {
    var me = this;
    var args = Array.prototype.slice.call(arguments, 1);
    return function () {
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return me.apply(context, finalArgs);
    }
}

但繼續(xù)探究裤园,我們注意 bind 方法中:bind 返回的函數(shù)如果作為構(gòu)造函數(shù),搭配 new 關(guān)鍵字出現(xiàn)的話剂府,我們的綁定 this 就需要“被忽略”拧揽,this 要綁定在實(shí)例上。也就是說(shuō)腺占,new 的操作符要高于 bind 綁定淤袜,兼容這種情況的實(shí)現(xiàn):

Function.prototype.bind = Function.prototype.bind || function (context) {
    var me = this;
    var args = Array.prototype.slice.call(arguments, 1);
    var F = function () {};
    F.prototype = this.prototype;
    var bound = function () {
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return me.apply(this instanceof F ? this : context || this, finalArgs);
    }
    bound.prototype = new F();
    return bound;
}

如果你認(rèn)為這樣就完了,其實(shí)我會(huì)告訴你說(shuō)衰伯,高潮才剛要上演铡羡。曾經(jīng)的我也認(rèn)為上述方法已經(jīng)比較完美了,直到我看了 es5-shim 源碼(已適當(dāng)刪減):

function bind(that) {
    var target = this;
    if (!isCallable(target)) {
        throw new TypeError('Function.prototype.bind called on incompatible ' + target);
    }
    var args = array_slice.call(arguments, 1);
    var bound;
    var binder = function () {
        if (this instanceof bound) {
            var result = target.apply(
                this,
                array_concat.call(args, array_slice.call(arguments))
            );
            if ($Object(result) === result) {
                return result;
            }
            return this;
        } else {
            return target.apply(
                that,
                array_concat.call(args, array_slice.call(arguments))
            );
        }
    };
    var boundLength = max(0, target.length - args.length);
    var boundArgs = [];
    for (var i = 0; i < boundLength; i++) {
        array_push.call(boundArgs, '$' + i);
    }
    bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this, arguments); }')(binder);
    
    if (target.prototype) {
        Empty.prototype = target.prototype;
        bound.prototype = new Empty();
        Empty.prototype = null;
    }
    return bound;
}

es5-shim 的實(shí)現(xiàn)到底在”搞什么鬼“呢意鲸?你可能不知道烦周,其實(shí)每個(gè)函數(shù)都有 length 屬性尽爆。對(duì),就像數(shù)組和字符串那樣读慎。函數(shù)的 length 屬性漱贱,用于表示函數(shù)的形參個(gè)數(shù)。更重要的是函數(shù)的 length 屬性值是不可重寫的贪壳。我寫了個(gè)測(cè)試代碼來(lái)證明:

function test (){}
test.length  // 輸出 0
test.hasOwnProperty('length')  // 輸出 true
Object.getOwnPropertyDescriptor('test', 'length') 
// 輸出:
// configurable: false, 
// enumerable: false,
// value: 4, 
// writable: false 

說(shuō)到這里饱亿,那就好解釋了:es5-shim 是為了最大限度地進(jìn)行兼容,包括對(duì)返回函數(shù) length 屬性的還原闰靴。而如果按照我們之前實(shí)現(xiàn)的那種方式彪笼,length 值始終為零。因此蚂且,既然不能修改 length 的屬性值配猫,那么在初始化時(shí)賦值總可以吧!于是我們可通過(guò) evalnew Function 的方式動(dòng)態(tài)定義函數(shù)杏死。但是出于安全考慮泵肄,在某些瀏覽器中使用 eval 或者 Function() 構(gòu)造函數(shù)都會(huì)拋出異常。然而巧合的是淑翼,這些無(wú)法兼容的瀏覽器基本上都實(shí)現(xiàn)了 bind 函數(shù)腐巢,這些異常又不會(huì)被觸發(fā)。上述代碼里玄括,重設(shè)綁定函數(shù)的 length 屬性:

var boundLength = max(0, target.length - args.length)

構(gòu)造函數(shù)調(diào)用情況冯丙,在 binder 中也有效兼容:

if (this instanceof bound) { 
    ... // 構(gòu)造函數(shù)調(diào)用情況
} else {
    ... // 正常方式調(diào)用
}
    
if (target.prototype) {
    Empty.prototype = target.prototype;
    bound.prototype = new Empty();
    // 進(jìn)行垃圾回收清理
    Empty.prototype = null;
}

對(duì)比過(guò)幾版的 polyfill 實(shí)現(xiàn),對(duì)于 bind 應(yīng)該有了比較深刻的認(rèn)識(shí)遭京。這一系列實(shí)現(xiàn)有效地考察了很重要的知識(shí)點(diǎn):比如 this 的指向胃惜、JavaScript 閉包、原型與原型鏈哪雕,設(shè)計(jì)程序上的邊界 case 和兼容性考慮經(jīng)驗(yàn)等硬素質(zhì)船殉。

一道更好的面試題

最后,現(xiàn)如今在很多面試中斯嚎,面試官都會(huì)以“實(shí)現(xiàn) bind”作為題目利虫。如果是我,現(xiàn)在可能會(huì)規(guī)避這個(gè)很容易“應(yīng)試”的題目孝扛,而是別出心裁列吼,讓面試者實(shí)現(xiàn)一個(gè) “call/apply”。我們往往用 call/apply 模擬實(shí)現(xiàn) bind苦始,而直接實(shí)現(xiàn) call/apply 也算簡(jiǎn)單:

Function.prototype.applyFn = function (targetObject, argsArray) {
    if(typeof argsArray === 'undefined' || argsArray === null) {
        argsArray = []
    }
    
    if(typeof targetObject === 'undefined' || targetObject === null){
        targetObject = this
    }
    
    targetObject = new Object(targetObject)
    
    const targetFnKey = 'targetFnKey'
    targetObject[targetFnKey] = this
    
    const result = targetObject[targetFnKey](...argsArray)
    delete targetObject[targetFnKey]
    return result
}

這樣的代碼不難理解寞钥,函數(shù)體內(nèi)的 this 指向了調(diào)用 applyFn 的函數(shù)。為了將該函數(shù)體內(nèi)的 this 綁定在 targetObject 上陌选,我們采用了隱式綁定的方法: targetObject[targetFnKey](...argsArray)理郑。

細(xì)心的讀者會(huì)發(fā)現(xiàn)蹄溉,這里存在一個(gè)問題:如果 targetObject 對(duì)象本身就存在 targetFnKey 這樣的屬性,那么在使用 applyFn 函數(shù)時(shí)您炉,原有的 targetFnKey 屬性值就會(huì)被覆蓋柒爵,之后被刪除。解決方案可以使用 ES6 Sybmol() 來(lái)保證鍵的唯一性赚爵;另一種解決方案是用 Math.random() 實(shí)現(xiàn)獨(dú)一無(wú)二的 key棉胀,這里我們不再贅述。

實(shí)現(xiàn)這些 API 帶來(lái)的啟示

這些 API 的實(shí)現(xiàn)并不算復(fù)雜冀膝,卻能恰如其分地考驗(yàn)開發(fā)者的 JavaScript 基礎(chǔ)唁奢。基礎(chǔ)是地基窝剖,是探究更深入內(nèi)容的鑰匙麻掸,是進(jìn)階之路上最重要的一環(huán),需要每個(gè)開發(fā)者重視赐纱。在前端技術(shù)快速發(fā)展迭代的今天脊奋,在“前端市場(chǎng)是否飽和”,“前端求職火爆異掣砻瑁”诚隙,“前端入門簡(jiǎn)單,錢多人傻”等眾說(shuō)紛紜的浮躁環(huán)境下起胰,對(duì)基礎(chǔ)內(nèi)功的修煉就顯得尤為重要最楷。這也是你在前端路上能走多遠(yuǎn)、走多久的關(guān)鍵待错。

從面試的角度看,面試題歸根結(jié)底是對(duì)基礎(chǔ)的考察烈评,只有對(duì)基礎(chǔ)爛熟于胸火俄,才能具備突破面試的基本條件。

分享交流

本篇文章出自我的課程:前端開發(fā)核心知識(shí)進(jìn)階 當(dāng)中的一篇基礎(chǔ)部分章節(jié)讲冠。

感興趣的讀者可以:

PC 端點(diǎn)擊了解更多《前端開發(fā)核心知識(shí)進(jìn)階》

移動(dòng)端點(diǎn)擊了解更多:

移動(dòng)端點(diǎn)擊了解更多《前端開發(fā)核心知識(shí)進(jìn)階

大綱內(nèi)容:

image

Happy coding!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瓜客,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子竿开,更是在濱河造成了極大的恐慌谱仪,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件否彩,死亡現(xiàn)場(chǎng)離奇詭異疯攒,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)列荔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門敬尺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)枚尼,“玉大人,你說(shuō)我怎么就攤上這事砂吞∈鸹校” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵蜻直,是天一觀的道長(zhǎng)盯质。 經(jīng)常有香客問我,道長(zhǎng)概而,這世上最難降的妖魔是什么呼巷? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮到腥,結(jié)果婚禮上朵逝,老公的妹妹穿的比我還像新娘。我一直安慰自己乡范,他們只是感情好配名,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著晋辆,像睡著了一般渠脉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瓶佳,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天芋膘,我揣著相機(jī)與錄音,去河邊找鬼霸饲。 笑死为朋,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的厚脉。 我是一名探鬼主播习寸,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼傻工!你這毒婦竟也來(lái)了霞溪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤中捆,失蹤者是張志新(化名)和其女友劉穎鸯匹,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體泄伪,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡殴蓬,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了臂容。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片科雳。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡根蟹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出糟秘,到底是詐尸還是另有隱情简逮,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布尿赚,位于F島的核電站散庶,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏凌净。R本人自食惡果不足惜悲龟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望冰寻。 院中可真熱鬧须教,春花似錦、人聲如沸斩芭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)划乖。三九已至贬养,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間琴庵,已是汗流浹背误算。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留迷殿,地道東北人儿礼。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像庆寺,于是被迫代替她去往敵國(guó)和親蜘犁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容