摘要:接下來,我們換一種思路,用一個相對較新的來實現(xiàn)方法。從這道題目看出,相比考察死記硬背,這樣的實現(xiàn)更有意義。對數(shù)組的操作我們不能陌生,其中方法更要做到駕輕就熟。最后,我們再看下社區(qū)上著名的和的實現(xiàn)。
有不少剛?cè)胄械耐瑢W(xué)跟我說:“JavaScript 很多 API 記不清楚怎么辦?數(shù)組的這方法、那方法總是傻傻分不清楚,該如何是好?操作 DOM 的方式今天記,明天忘,真讓人奔潰!”
甚至有的開發(fā)者在討論面試時,總向我抱怨:“面試官總愛糾結(jié) API 的使用,甚至 jQuery 某些方法的參數(shù)順序都需要讓我說清楚!”
我認(rèn)為,對于反復(fù)使用的方法,所有人都要做到“機(jī)械記憶”,能夠反手寫出。一些貌似永遠(yuǎn)記不清的 API 只是因為用得不夠多而已。
在做面試官時,我從來不強(qiáng)求開發(fā)者準(zhǔn)確無誤地“背誦” API。相反,我喜歡從另外一個角度來考察面試者:“既然記不清使用方法,那么我告訴你它的使用方法,你來實現(xiàn)一個吧!”實現(xiàn)一個 API,除了可以考察面試者對這個 API 的理解,更能體現(xiàn)開發(fā)者的編程思維和代碼能力。對于積極上進(jìn)的前端工程師,模仿并實現(xiàn)一些經(jīng)典方法,應(yīng)該是“家常便飯”,這是比較基本的要求。
本小節(jié),我根據(jù)了解的面試題目和作為面試官的經(jīng)歷,挑了幾個典型的 API,通過對其不同程度,不同方式的實現(xiàn),來覆蓋 JavaScript 中的部分知識點和編程要領(lǐng)。通過學(xué)習(xí)本節(jié)內(nèi)容,期待你不僅能領(lǐng)會代碼奧義,更應(yīng)該學(xué)習(xí)舉一反三的方法。
API 主題的相關(guān)知識點如下:
jQuery offset 實現(xiàn)這個話題演變自今日頭條某部門面試題。當(dāng)時面試官提問:“如何獲取文檔中任意一個元素距離文檔 document 頂部的距離?”
熟悉 jQuery 的同學(xué)應(yīng)該對 offset 方法并不陌生,它返回或設(shè)置匹配元素相對于文檔的偏移(位置)。這個方法返回的對象包含兩個整型屬性:top 和 left,以像素計。如果可以使用 jQuery, 我們可以直接調(diào)取該 API 獲得結(jié)果。但是,如果用原生 JavaScript 實現(xiàn),也就是說手動實現(xiàn) jQuery offset 方法,該如何著手呢?
主要有兩種思路:
通過遞歸實現(xiàn)
通過 getBoundingClientRect API 實現(xiàn)
遞歸實現(xiàn)方案我們通過遍歷目標(biāo)元素、目標(biāo)元素的父節(jié)點、父節(jié)點的父節(jié)點......依次溯源,并累加這些遍歷過的節(jié)點相對于其最近祖先節(jié)點(且 position 屬性非 static)的偏移量,向上直到 document,累加即可得到結(jié)果。
其中,我們需要使用 JavaScript 的 offsetTop 來訪問一個 DOM 節(jié)點上邊框相對離其本身最近、且 position 值為非 static 的祖先元素的垂直偏移量。具體實現(xiàn)為:
const offset = ele => { let result = { top: 0, left: 0 } // 當(dāng)前 DOM 節(jié)點的 display === "none" 時, 直接返回 {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 }
上述代碼并不難理解,使用遞歸實現(xiàn)。如果節(jié)點 node.nodeType 類型不是 Element(1),則跳出;如果相關(guān)節(jié)點的 position 屬性為 static,則不計入計算,進(jìn)入下一個節(jié)點(其父節(jié)點)的遞歸。如果相關(guān)屬性的 display 屬性為 none,則應(yīng)該直接返回 0 作為結(jié)果。
這個實現(xiàn)很好地考察了開發(fā)者對于遞歸的初級應(yīng)用、以及對 JavaScript 方法的掌握程度。
接下來,我們換一種思路,用一個相對較新的 API: getBoundingClientRect 來實現(xiàn) jQuery offset 方法。
getBoundingClientRect 方法getBoundingClientRect 方法用來描述一個元素的具體位置,這個位置的下面四個屬性都是相對于視口左上角的位置而言的。對某一節(jié)點執(zhí)行該方法,它的返回值是一個 DOMRect 類型的對象。這個對象表示一個矩形盒子,它含有:left、top、right 和 bottom 等只讀屬性。
請參考實現(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é)點的 display === "none" 時,直接返回 {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é)點的一個屬性,它返回當(dāng)前節(jié)點的頂層的 document 對象。ownerDocument 是文檔,documentElement 是根節(jié)點。事實上,ownerDocument 下含 2 個節(jié)點:
documentElement
docElement.clientTop,clientTop 是一個元素頂部邊框的寬度,不包括頂部外邊距或內(nèi)邊距。
除此之外,該方法實現(xiàn)就是簡單的幾何運(yùn)算,邊界 case 和兼容性處理,也并不難理解。
從這道題目看出,相比考察“死記硬背” API,這樣的實現(xiàn)更有意義。站在面試官的角度,我往往會給面試者(開發(fā)者)提供相關(guān)的方法提示,以引導(dǎo)其給出最后的方案實現(xiàn)。
數(shù)組 reduce 方法的相關(guān)實現(xiàn)數(shù)組方法非常重要:因為數(shù)組就是數(shù)據(jù),數(shù)據(jù)就是狀態(tài),狀態(tài)反應(yīng)著視圖。對數(shù)組的操作我們不能陌生,其中 reduce 方法更要做到駕輕就熟。我認(rèn)為這個方法很好地體現(xiàn)了“函數(shù)式”理念,也是當(dāng)前非常熱門的考察點之一。
我們知道 reduce 方法是 ES5 引入的,reduce 英文解釋翻譯過來為“減少,縮小,使還原,使變?nèi)酢保琈DN 對該方法直述為:
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.
它的使用語法:
arr.reduce(callback[, initialValue])
這里我們簡要介紹一下。
reduce 第一個參數(shù) callback 是核心,它對數(shù)組的每一項進(jìn)行“疊加加工”,其最后一次返回值將作為 reduce 方法的最終返回值。 它包含 4 個參數(shù):
previousValue 表示“上一次” callback 函數(shù)的返回值
currentValue 數(shù)組遍歷中正在處理的元素
currentIndex 可選,表示 currentValue 在數(shù)組中對應(yīng)的索引。如果提供了 initialValue,則起始索引號為 0,否則為 1
array 可選,調(diào)用 reduce() 的數(shù)組
initialValue 可選,作為第一次調(diào)用 callback 時的第一個參數(shù)。如果沒有提供 initialValue,那么數(shù)組中的第一個元素將作為 callback 的第一個參數(shù)。
reduce 實現(xiàn) runPromiseInSequence我們看它的一個典型應(yīng)用:按順序運(yùn)行 Promise:
const runPromiseInSequence = (array, value) => array.reduce( (promiseChain, currentFunction) => promiseChain.then(currentFunction), Promise.resolve(value) )
runPromiseInSequence 方法將會被一個每一項都返回一個 Promise 的數(shù)組調(diào)用,并且依次執(zhí)行數(shù)組中的每一個 Promise,請讀者仔細(xì)體會。如果覺得晦澀,可以參考示例:
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 實現(xiàn) pipereduce 的另外一個典型應(yīng)用可以參考函數(shù)式方法 pipe 的實現(xiàn):pipe(f, g, h) 是一個 curry 化函數(shù),它返回一個新的函數(shù),這個新的函數(shù)將會完成 (...args) => h(g(f(...args))) 的調(diào)用。即 pipe 方法返回的函數(shù)會接收一個參數(shù),這個參數(shù)傳遞給 pipe 方法第一個參數(shù),以供其調(diào)用。
const pipe = (...functions) => input => functions.reduce( (acc, fn) => fn(acc), input )
仔細(xì)體會 runPromiseInSequence 和 pipe 這兩個方法,它們都是 reduce 應(yīng)用的典型場景。
實現(xiàn)一個 reduce那么我們該如何實現(xiàn)一個 reduce 呢?參考來自 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 作為初始值,并通過 while 循環(huán),依次累加計算出 value 結(jié)果并輸出。但是相比 MDN 上述實現(xiàn),我個人更喜歡的實現(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 來代替 while 實現(xiàn)結(jié)果的累加,它們本質(zhì)上是相同的。
我也同樣看了下 ES5-shim 里的 pollyfill,跟上述思路完全一致。唯一的區(qū)別在于:我用了 forEach 迭代而 ES5-shim 使用的是簡單的 for 循環(huán)。實際上,如果“杠精”一些,我們會指出數(shù)組的 forEach 方法也是 ES5 新增的。因此,用 ES5 的一個 API(forEach),去實現(xiàn)另外一個 ES5 的 API(reduce),這并沒什么實際意義——這里的 pollyfill 就是在不兼容 ES5 的情況下,模擬的降級方案。此處不多做追究,因為根本目的還是希望讀者對 reduce 有一個全面透徹的了解。
通過 Koa only 模塊源碼認(rèn)識 reduce通過了解并實現(xiàn) reduce 方法,我們對它已經(jīng)有了比較深入的認(rèn)識。最后,我們再來看一個 reduce 使用示例——通過 Koa 源碼的 only 模塊,加深印象:
var o = { a: "a", b: "b", c: "c" } only(o, ["a","b"]) // {a: "a", b: "b"}
該方法返回一個經(jīng)過指定篩選屬性的新對象。
?
only 模塊實現(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 及其衍生場景有很多值得我們玩味、探究的地方。舉一反三,活學(xué)活用是技術(shù)進(jìn)階的關(guān)鍵。
compose 實現(xiàn)的幾種方案函數(shù)式理念——這一古老的概念如今在前端領(lǐng)域“遍地開花”。函數(shù)式很多思想都值得借鑒,其中一個細(xì)節(jié):compose 因為其巧妙的設(shè)計而被廣泛運(yùn)用。對于它的實現(xiàn),從面向過程式到函數(shù)式實現(xiàn),風(fēng)格迥異,值得我們探究。在面試當(dāng)中,也經(jīng)常有面試官要求實現(xiàn) compose 方法,我們先看什么是 compose。
compose 其實和前面提到的 pipe 一樣,就是執(zhí)行一連串不定長度的任務(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)鍵點:
compose 的參數(shù)是函數(shù)數(shù)組,返回的也是一個函數(shù)
compose 的參數(shù)是任意長度的,所有的參數(shù)都是函數(shù),執(zhí)行方向是自右向左的,因此初始函數(shù)一定放到參數(shù)的最右面
compose 執(zhí)行后返回的函數(shù)可以接收參數(shù),這個參數(shù)將作為初始函數(shù)的參數(shù),所以初始函數(shù)的參數(shù)是多元的,初始函數(shù)的返回結(jié)果將作為下一個函數(shù)的參數(shù),以此類推。因此除了初始函數(shù)之外,其他函數(shù)的接收值是一元的。
我們發(fā)現(xiàn),實際上,compose 和 pipe 的差別只在于調(diào)用順序的不同:
// compose fn1(fn2(fn3(fn4(args)))) // pipe fn4(fn3(fn2(fn1(args))))
即然跟我們先前實現(xiàn)的 pipe 方法如出一轍,那么還有什么好深入分析的呢?請繼續(xù)閱讀,看看還能玩出什么花兒來。
compose 最簡單的實現(xiàn)是面向過程的:
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)鍵是用到了閉包,使用閉包變量儲存結(jié)果 result 和函數(shù)數(shù)組長度以及遍歷索引,并利用遞歸思想,進(jìn)行結(jié)果的累加計算。整體實現(xiàn)符合正常的面向過程思維,不難理解。
聰明的同學(xué)可能也會意識到,利用上文所講的 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())
通過前面的學(xué)習(xí),結(jié)合 call、apply 方法,這樣的實現(xiàn)并不難理解。
我們繼續(xù)開拓思路,“既然涉及串聯(lián)和流程控制”,那么我們還可以使用 Promise 實現(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))) }
這種實現(xiàn)利用了 Promise 特性:首先通過 Promise.resolve(init.apply(null, arg)) 啟動邏輯,啟動一個 resolve 值為最后一個函數(shù)接收參數(shù)后的返回值,依次執(zhí)行函數(shù)。因為 promise.then() 仍然返回一個 Promise 類型值,所以 reduce 完全可以按照 Promise 實例執(zhí)行下去。
既然能夠使用 Promise 實現(xiàn),那么 generator 當(dāng)然應(yīng)該也可以實現(xiàn)。這里給大家留一個思考題,感興趣的同學(xué)可以嘗試,歡迎在評論區(qū)討論。
最后,我們再看下社區(qū)上著名的 lodash 和 Redux 的實現(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 版本更像我們的第一種實現(xiàn)方式,理解起來也更容易。
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ù)式概念確實有些抽象,需要開發(fā)者仔細(xì)琢磨,并動手調(diào)試。一旦頓悟,必然會感受到其中的優(yōu)雅和簡潔。
apply、bind 進(jìn)階實現(xiàn)面試中關(guān)于 this 綁定的相關(guān)話題如今已經(jīng)“泛濫”,同時對 bind 方法的實現(xiàn),社區(qū)上也有相關(guān)討論。但是很多內(nèi)容尚不系統(tǒng),且存在一些瑕疵。這里簡單摘錄我 2017 年年初寫的文章 從一道面試題,到“我可能看了假源碼” 來遞進(jìn)討論。在《一網(wǎng)打盡 this》一課,我們介紹過對 bind 的實現(xiàn),這里我們進(jìn)一步展開。
此處不再贅述 bind 函數(shù)的使用,尚不清楚的讀者可以自行補(bǔ)充一下基礎(chǔ)知識。我們先來看一個初級實現(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 分。
先簡要解讀一下:
基本原理是使用 apply 進(jìn)行模擬 bind。函數(shù)體內(nèi)的 this 就是需要綁定 this 的函數(shù),或者說是原函數(shù)。最后使用 apply 來進(jìn)行參數(shù)(context)綁定,并返回。
與此同時,將第一個參數(shù)(context)以外的其他參數(shù),作為提供給原函數(shù)的預(yù)設(shè)參數(shù),這也是基本的“ curry 化”基礎(chǔ)。
上述實現(xiàn)方式,我們返回的參數(shù)列表里包含:argsArray.slice(1),它的問題在于存在預(yù)置參數(shù)功能丟失的現(xiàn)象。
想象我們返回的綁定函數(shù)中,如果想實現(xiàn)預(yù)設(shè)傳參(就像 bind 所實現(xiàn)的那樣),就面臨尷尬的局面。真正實現(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 要綁定在實例上。也就是說,new 的操作符要高于 bind 綁定,兼容這種情況的實現(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)為這樣就完了,其實我會告訴你說,高潮才剛要上演。曾經(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 的實現(xiàn)到底在”搞什么鬼“呢?你可能不知道,其實每個函數(shù)都有 length 屬性。對,就像數(shù)組和字符串那樣。函數(shù)的 length 屬性,用于表示函數(shù)的形參個數(shù)。更重要的是函數(shù)的 length 屬性值是不可重寫的。我寫了個測試代碼來證明:
function test (){} test.length // 輸出 0 test.hasOwnProperty("length") // 輸出 true Object.getOwnPropertyDescriptor("test", "length") // 輸出: // configurable: false, // enumerable: false, // value: 4, // writable: false
說到這里,那就好解釋了:es5-shim 是為了最大限度地進(jìn)行兼容,包括對返回函數(shù) length 屬性的還原。而如果按照我們之前實現(xiàn)的那種方式,length 值始終為零。因此,既然不能修改 length 的屬性值,那么在初始化時賦值總可以吧!于是我們可通過 eval 和 new Function 的方式動態(tài)定義函數(shù)。但是出于安全考慮,在某些瀏覽器中使用 eval 或者 Function() 構(gòu)造函數(shù)都會拋出異常。然而巧合的是,這些無法兼容的瀏覽器基本上都實現(xiàn)了 bind 函數(shù),這些異常又不會被觸發(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; }
對比過幾版的 polyfill 實現(xiàn),對于 bind 應(yīng)該有了比較深刻的認(rèn)識。這一系列實現(xiàn)有效地考察了很重要的知識點:比如 this 的指向、JavaScript 閉包、原型與原型鏈,設(shè)計程序上的邊界 case 和兼容性考慮經(jīng)驗等硬素質(zhì)。
一道更好的面試題最后,現(xiàn)如今在很多面試中,面試官都會以“實現(xiàn) bind”作為題目。如果是我,現(xiàn)在可能會規(guī)避這個很容易“應(yīng)試”的題目,而是別出心裁,讓面試者實現(xiàn)一個 “call/apply”。我們往往用 call/apply 模擬實現(xiàn) bind,而直接實現(xiàn) call/apply 也算簡單:
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ì)心的讀者會發(fā)現(xiàn),這里存在一個問題:如果 targetObject 對象本身就存在 targetFnKey 這樣的屬性,那么在使用 applyFn 函數(shù)時,原有的 targetFnKey 屬性值就會被覆蓋,之后被刪除。解決方案可以使用 ES6 Sybmol() 來保證鍵的唯一性;另一種解決方案是用 Math.random() 實現(xiàn)獨一無二的 key,這里我們不再贅述。
實現(xiàn)這些 API 帶來的啟示這些 API 的實現(xiàn)并不算復(fù)雜,卻能恰如其分地考驗開發(fā)者的 JavaScript 基礎(chǔ)。基礎(chǔ)是地基,是探究更深入內(nèi)容的鑰匙,是進(jìn)階之路上最重要的一環(huán),需要每個開發(fā)者重視。在前端技術(shù)快速發(fā)展迭代的今天,在“前端市場是否飽和”,“前端求職火爆異常”,“前端入門簡單,錢多人傻”等眾說紛紜的浮躁環(huán)境下,對基礎(chǔ)內(nèi)功的修煉就顯得尤為重要。這也是你在前端路上能走多遠(yuǎn)、走多久的關(guān)鍵。
從面試的角度看,面試題歸根結(jié)底是對基礎(chǔ)的考察,只有對基礎(chǔ)爛熟于胸,才能具備突破面試的基本條件。
分享交流本篇文章出自我的課程:前端開發(fā)核心知識進(jìn)階 當(dāng)中的一篇基礎(chǔ)部分章節(jié)。
感興趣的讀者可以:
PC 端點擊了解更多《前端開發(fā)核心知識進(jìn)階》
移動端點擊了解更多:
大綱內(nèi)容:
Happy coding!
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/109825.html
摘要:接下來,我們換一種思路,用一個相對較新的來實現(xiàn)方法。從這道題目看出,相比考察死記硬背,這樣的實現(xiàn)更有意義。對數(shù)組的操作我們不能陌生,其中方法更要做到駕輕就熟。最后,我們再看下社區(qū)上著名的和的實現(xiàn)。 有不少剛?cè)胄械耐瑢W(xué)跟我說:JavaScript 很多 API 記不清楚怎么辦?數(shù)組的這方法、那方法總是傻傻分不清楚,該如何是好?操作 DOM 的方式今天記,明天忘,真讓人奔潰! 甚至有的開發(fā)...
摘要:聲明語句是可選部分如果存在需要放在文檔的第一行所謂的文檔聲明就是告訴解析器當(dāng)前文檔格式版本號以及編碼格式。所有的元素都必須是成對閉合標(biāo)簽非閉合標(biāo)簽是非法的,解析器將報錯,不無正常解析標(biāo)簽對大小寫敏感必須頭尾標(biāo)簽一致。 前言 一直想系統(tǒng)性的學(xué)XML,就沒時間學(xué),今晚抽出幾個小時時間學(xué)完了XML。過幾天再過來看看,背一背應(yīng)該就差不多,記得東西較多,沒什么難理解的。 XML數(shù)據(jù)傳輸格式 第一...
摘要:聲明語句是可選部分如果存在需要放在文檔的第一行所謂的文檔聲明就是告訴解析器當(dāng)前文檔格式版本號以及編碼格式。所有的元素都必須是成對閉合標(biāo)簽非閉合標(biāo)簽是非法的,解析器將報錯,不無正常解析標(biāo)簽對大小寫敏感必須頭尾標(biāo)簽一致。 前言 一直想系統(tǒng)性的學(xué)XML,就沒時間學(xué),今晚抽出幾個小時時間學(xué)完了XML。過幾天再過來看看,背一背應(yīng)該就差不多,記得東西較多,沒什么難理解的。 XML數(shù)據(jù)傳輸格式 第一...
摘要:在項目中,為滿足以上要求,我們將大量的參數(shù)配置在或文件中,通過注解,我們可以方便的獲取這些參數(shù)值使用配置模塊假設(shè)我們正在搭建一個發(fā)送郵件的模塊。這使得在不影響其他模塊的情況下重構(gòu)一個模塊中的屬性變得容易。 在編寫項目代碼時,我們要求更靈活的配置,更好的模塊化整合。在 Spring Boot 項目中,為滿足以上要求,我們將大量的參數(shù)配置在 application.properties 或...
閱讀 3723·2021-10-13 09:39
閱讀 3789·2021-09-24 09:48
閱讀 1189·2021-09-01 10:30
閱讀 2526·2019-08-30 15:55
閱讀 1773·2019-08-29 16:39
閱讀 2296·2019-08-26 13:55
閱讀 3050·2019-08-26 12:23
閱讀 1633·2019-08-26 11:59