有不少剛?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è)整型屬性:top
和 left
稽揭,以像素計(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
淤井、top
、right
和 bottom
等只讀屬性。
請(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é)果如下圖:
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ì) runPromiseInSequence
和 pipe
這兩個(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í)際上操刀,compose
和 pipe
的差別只在于調(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ò) eval
和 new 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)擊了解更多:
大綱內(nèi)容:
Happy coding!