前言
由淺入深、逐個擊破 30SecondsOfCode 中函式系列所有原始碼片段,帶你領略原始碼之美,
本系列是對名庫 30SecondsOfCode 的深入刨析,
本篇是其中的函式篇,可以在極短的時間內培養你的函式式思維,
內容根據原始碼的難易等級進行排版,目錄如下:
- 新手級
- 普通級
- 專家級
正文
新手級
checkProp
const checkProp = (predicate, prop) => obj => !!predicate(obj[prop]);
const lengthIs4 = checkProp(l => l === 4, 'length');
lengthIs4([]); // false
lengthIs4([1, 2, 3, 4]); // true
lengthIs4(new Set([1, 2, 3, 4])); // false (Set uses Size, not length)
const session = { user: {} };
const validUserSession = checkProp(u => u.active && !u.disabled, 'user');
validUserSession(session); // false
session.user.active = true;
validUserSession(session); // true
const noLength = checkProp(l => l === undefined, 'length');
noLength([]); // false
noLength({}); // true
noLength(new Set()); // true
作用:檢查引數是否存在給定的屬性,
決議:給定一個檢查函式,和所需檢查的屬性名,回傳一個函式,可通過呼叫 回傳的函式,去判定 傳入的物件引數是否符合檢查函式,
functionName
const functionName = fn => (console.debug(fn.name), fn);
functionName(Math.max); // max (logged in debug channel of console)
作用:列印函式名,
決議:使用console.debugAPI 和函式的name屬性,把 函式型別引數的名字 列印到控制臺的debug channel中,
negate
const negate = func => (...args) => !func(...args);
[1, 2, 3, 4, 5, 6].filter(negate(n => n % 2 === 0)); // [ 1, 3, 5 ]
作用:反轉 謂詞函式(回傳型別為布爾的函式)的回傳結果,
決議:假設有一謂詞函式為func = args => bool,我們想要反轉其結果,便可對它的呼叫方式進行進一步的抽象,把反轉結果的邏輯放置抽象中,
在本函式中,只需要一個 邏輯非運算子!func(...args),
而擴展運算子...是對引數的抽象,代表的是傳入的所有引數,我們要將所有引數一個不差地傳遞,不可破環 謂詞函式的“純潔性”,
unary
const unary = fn => val => fn(val);
['6', '8', '10'].map(unary(parseInt)); // [6, 8, 10]
作用:引數函式呼叫時 只接受 引數函式的第一個引數,忽略其他引數,
決議:包裝一個函式,并不做任何處理:wrap = fn => (...args) => fn(...args)
很顯然,如果想對傳入的引數進行處理,只需對args動刀,而本例直接使用了單獨的一個變數,忽略了其他引數,
普通級
ary
const ary = (fn, n) => (...args) => fn(...args.slice(0, n));
const firstTwoMax = ary(Math.max, 2);
[[2, 6, 'a'], [6, 4, 8], [10]].map(x => firstTwoMax(...x)); // [6, 6, 1
作用:引數函式呼叫時 只接受 引數函式的前 n 個引數,忽略其他引數,
決議:和上列邏輯如出一轍,只不過處理引數的邏輯換成了...args.slice(0, n),只要前n個,
attempt
const attempt = (fn, ...args) => {
try {
return fn(...args);
} catch (e) {
return e instanceof Error ? e : new Error(e);
}
};
var elements = attempt(function(selector) {
return document.querySelectorAll(selector);
}, '>_>');
if (elements instanceof Error) elements = []; // elements = []
作用:對 引數函式 進行例外捕獲,如果有例外則拋出,
決議:對 引數函式 進行進一步封裝,本例封裝的邏輯是try catch,即捕獲引數函式的例外,
很久之前,我看到過一個關于java8的 attempt 片段,里面還增加了重試邏輯,
js 實作代碼如下:
const attempt = (fn, ...args, count, bound) => {
try {
return fn(...args);
} catch (e) {
if(count == bound){
return e instanceof Error ? e : new Error(e);
}
return attempt(fn, ...args, count + 1, bound)
}
};
bind
const bind = (fn, context, ...boundArgs) => (...args) => fn.apply(context, [...boundArgs, ...args]);
function greet(greeting, punctuation) {
return greeting + ' ' + this.user + punctuation;
}
const freddy = { user: 'fred' };
const freddyBound = bind(greet, freddy);
console.log(freddyBound('hi', '!')); // 'hi fred!'
作用:原生API-bind的另一種實作,
fn.bind(context,...args) => bind(fn,context,...args)
MDN 關于 bind 的解釋:
bind()方法創建一個新的函式,在bind()被呼叫時,這個新函式的this被指定為bind()的第一個引數,而其余引數將作為新函式的引數,供呼叫時使用,
決議:首先,使用了apply將給定的 背景關系引數 應用于 引數函式,
其次,利用 apply 只接受陣列作為引數的規定,將最初傳入的引數,和后續傳入的引數按順序合并在一個陣列中傳遞進去,
bindKey
const bindKey = (context, fn, ...boundArgs) => (...args) =>
context[fn].apply(context, [...boundArgs, ...args]);
const freddy = {
user: 'fred',
greet: function(greeting, punctuation) {
return greeting + ' ' + this.user + punctuation;
}
};
const freddyBound = bindKey(freddy, 'greet');
console.log(freddyBound('hi', '!')); // 'hi fred!'
作用:把上列中的fn換成了context[fn],
決議:我們原來的 引數函式 變成了一個 背景關系引數的一個屬性,而將這個屬性依附于背景關系物件就成了一個函式context[fn],
可以說,這個一個呼叫方式特殊的bind,
call
const call = (key, ...args) => context => context[key](...args);
Promise.resolve([1, 2, 3])
.then(call('map', x => 2 * x))
.then(console.log); // [ 2, 4, 6 ]
const map = call.bind(null, 'map');
Promise.resolve([1, 2, 3])
.then(map(x => 2 * x))
.then(console.log); // [ 2, 4, 6 ]
作用:動態改變函式執行的背景關系,
決議:給定一個屬性引數,再給定一組呼叫引數,回傳一個接受背景關系物件的函式,并最終組合呼叫,
其實這里面暗含了一個約束,很顯然,context[key]必須是一個函式,
這個片段本質是對背景關系的抽象,舉個例子:
const filterMen = call('filter', person => person.sex === 'man')
filterMen([{sex:'woman',...},{sex:'man',...},...])
// 如果有其他 背景關系物件,本例中也就是陣列 需要相同的 邏輯過濾呢?
chainAsync
const chainAsync = fns => {
let curr = 0;
const last = fns[fns.length - 1];
const next = () => {
const fn = fns[curr++];
fn === last ? fn() : fn(next);
};
next();
};
chainAsync([
next => {
console.log('0 seconds');
setTimeout(next, 1000);
},
next => {
console.log('1 second');
setTimeout(next, 1000);
},
() => {
console.log('2 second');
}
]);
作用:將 函式陣列轉換為有決策權的鏈式函式呼叫,
我為什么稱之有決策權的鏈式函式呼叫呢?
因為每個函式都會接受一個next方法引數,它代表的就是呼叫鏈中的下一個函式,所以什么時候呼叫下一個函式,要不要呼叫,決策權在你,
決議:其實這個片段很簡單,
首先,fns 型別一個函式陣列,其中除了最后一個函式都有隱含的約束,可以選擇接受 next 引數,
而 next 引數的含義就是呼叫鏈中的下一個函式,說白了 就是陣列中的下一個成員,
而最后一個函式是無參函式,
片段中復雜點在于:利用閉包存盤了兩個關鍵變數,
第一個是 呼叫鏈中的函式游標:curr;第二個是結束標志,最后一個函式:last,
每次鏈式向下呼叫前,都會進行一些邏輯處理:
const next = () => {
const fn = fns[curr++];
fn === last ? fn() : fn(next);
};
先取出當前游標所在函式,再把游標指向下一個函式,
然后,判斷是否是最后一個函式,是則直接呼叫,結束;反之,傳入 next 呼叫,
如果,你是一個后端開發者,可以把其理解為中間件的作業模式,
collectInto
const collectInto = fn => (...args) => fn(args);
const Pall = collectInto(Promise.all.bind(Promise));
let p1 = Promise.resolve(1);
let p2 = Promise.resolve(2);
let p3 = new Promise(resolve => setTimeout(resolve, 2000, 3));
Pall(p1, p2, p3).then(console.log); // [1, 2, 3] (after about 2 seconds)
作用:將接受陣列的函式更改為接受可變引數,
分析:利用了擴展運算子的性質,...args代表的是所有引陣列成的陣列,然后將這陣列傳遞進去呼叫,
可別小看了這一片段,呼叫方式的改變會決定很多上層邏輯,
平常我們大概率都會,建立一個陣列,收集所需的異步函式,
在本例中,很明顯的看到 從引數為陣列型別的約束 中解放了出來,
compose
const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));
const substract3 = x => x - 3;
const add5 = x => x + 5;
const multiply = (x, y) => x * y;
const multiplyAndAdd5AndSubstract3 = compose(
substract3,
add5,
multiply
);
multiplyAndAdd5AndSubstract3(5, 2); // 12
作用:將傳入的多個[異步]函式以組合的方式 呼叫,
先將引數傳入最后一個[異步]函式,然后將得到的結果,傳入倒數第二個[異步]函式,以此類推,
compose可以說是函式式編程的經典片段,
它的具體意義可以說是邏輯分層,像洋蔥一樣,一層一層地處理資料,
決議:fns 代表的是 傳入的多個函式 組成的陣列,
利用reduce方法實作函式的“洋蔥”包裹,
因為這種邏輯語意表示效果不好,就直接上上面例子的代碼流程了,
reduce 第一次回圈:
f: substract3;
g: add5;
回傳結果:(...args) => substract3(add5(...args));
reduce 第二次回圈:
f: (...args) => substract3(add5(...args));
g: multiply;
回傳結果:
(...args1) => ((...args2) => substract3(add5(...args2)))(multiply(...args1))
優化后:
(...args) => substract3(add5(multiply(...args)));
回圈下去,以此類推...
最后的回傳的形式:
(...args) => 第一個函式(第二個函式(第三個函式(...最后一個函式(...args))))
PS: 說實話,我并不喜歡 compose,在上例中就可以很明顯的看到缺點,
把很多函陣列合起來,第一是缺少語意化,與之對應的例子就是 Promise 的 then 呼叫鏈,語意鮮明;
第二是無法添加函式與函式之間的抽象邏輯,只能一次寫好,
第三是各個函式之間存在隱含的引數約束,很可怕的,
composeRight
const composeRight = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args)));
const add = (x, y) => x + y;
const square = x => x * x;
const substract3 = x => x - 3;
const addAndSquare = composeRight(add, square,substract3);
addAndSquareAndSubstract3(1, 2); // 6
作用:將傳入的多個[異步]函式以組合的方式 呼叫,
先將引數傳入第一個[異步]函式,然后將得到的結果,傳入第二個[異步]函式,以此類推,
converge
const converge = (converger, fns) => (...args) => converger(...fns.map(fn => fn.apply(null, args)));
const average = converge((a, b) => a / b, [
arr => arr.reduce((a, v) => a + v, 0),
arr => arr.length
]);
average([1, 2, 3, 4, 5, 6, 7]); // 4
作用:將 函式陣列的回傳結果 傳遞到converger函式,進一步處理,可用作分析統計,
決議: 使用map 和apply將引數資料傳遞給每個處理函式,并將處理后的結果交給converger函式,
curry
const curry = (fn, arity = fn.length, ...args) =>
arity <= args.length ? fn(...args) : curry.bind(null, fn, arity, ...args);
curry(Math.pow)(2)(10); // 1024
curry(Math.min, 3)(10)(50)(2); // 2
作用:函式柯里化,
柯里化不管在是函式式思維的理解,還是現實面試中,都非常的重要,
維基百科上 柯里化的解釋:
把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,并且回傳接受余下的引數而且回傳結果的新函式
決議:這個bind用得真是神了,借助它積累每次傳進來的引數,等到引數足夠時,再呼叫,
debounce
const debounce = (fn, ms = 0) => {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), ms);
};
};
window.addEventListener(
'resize',
debounce(() => {
console.log(window.innerWidth);
console.log(window.innerHeight);
}, 250)
); // Will log the window dimensions at most every 250ms
作用:函式防抖,
什么是防抖和節流?有什么區別?如何實作? 一文中關于防抖解釋:
觸發高頻事件后n秒內函式只會執行一次,如果n秒內高頻事件再次被觸發,則重新計算時間,
同樣,防抖也是面試必考的點,
決議: 傳入需防抖的函式,和防抖的時間間隔,回傳一個已防抖化的函式,
主要借助setTimeout和function + apply保存背景關系完成,
每次呼叫函式前,都執行一遍clearTimeout,保證重新計算呼叫時間,
無論是呼叫多么頻繁的函式都會在指定時間的間隔后只運行一次,
defer
const defer = (fn, ...args) => setTimeout(fn, 1, ...args);
// Example A:
defer(console.log, 'a'), console.log('b'); // logs 'b' then 'a'
// Example B:
document.querySelector('#someElement').innerHTML = 'Hello';
longRunningFunction(); // Browser will not update the HTML until this has finished
defer(longRunningFunction); // Browser will update the HTML then run the function
作用:推遲呼叫函式,直到清除當前呼叫堆疊,
可適用于推遲 cpu 密集型計算,以免阻塞渲染引擎作業,
分析:使用setTimeout(超時時間為1ms)將 函式引數 添加到瀏覽器事件佇列末尾,
因為 JavaScript 是單執行緒執行,先是主執行緒執行完畢,然后在讀取事件佇列中的代碼執行,
如果主執行緒有運行時間太長的函式,會阻塞頁面渲染,所以將其放置到事件佇列,
delay
const delay = (fn, wait, ...args) => setTimeout(fn, wait, ...args);
delay(
function(text) {
console.log(text);
},
1000,
'later'
); // Logs 'later' after one second.
作用:延遲函式執行,
是的,它和defer非常像,但使用場景卻是不一樣,
defer 的目的是將占據主執行緒時間長的函式推遲到事件佇列,
而 delay 只是字面意思,延遲執行,
決議:對 setTimeout 進行語意化封裝,
flip
const flip = fn => (first, ...rest) => fn(...rest, first);
let a = { name: 'John Smith' };
let b = {};
const mergeFrom = flip(Object.assign);
let mergePerson = mergeFrom.bind(null, a);
mergePerson(b); // == b
b = {};
Object.assign(b, a); // == b
作用:對 引數函式 的輸入資料進行進一步處理,將資料的第一個引數與其余引數位置對調,
決議:主要利用 擴展運算子的性質,對引數的位置進行調整,
如果你不了解這一語言特性,可參考阮一峰老師的ES6入門,
hz
const hz = (fn, iterations = 100) => {
const before = performance.now();
for (let i = 0; i < iterations; i++) fn();
return (1000 * iterations) / (performance.now() - before);
};
// 10,000 element array
const numbers = Array(10000)
.fill()
.map((_, i) => i);
// Test functions with the same goal: sum up the elements in the array
const sumReduce = () => numbers.reduce((acc, n) => acc + n, 0);
const sumForLoop = () => {
let sum = 0;
for (let i = 0; i < numbers.length; i++) sum += numbers[i];
return sum;
};
// `sumForLoop` is nearly 10 times faster
Math.round(hz(sumReduce)); // 572
Math.round(hz(sumForLoop)); // 4784
作用:回傳函式每秒執行一次的次數,
hz是赫茲的單位(頻率的單位)定義為每秒一個周期,
決議:通過兩次使用performance.now獲取iterations次迭代前后的毫秒差,
然后將毫秒轉換為秒并除以經過的時間,可以得到每秒的函式執行次數,
PS: 此處,并沒有太好的個人理解,翻譯自官方,
once
const once = fn => {
let called = false;
return function(...args) {
if (called) return;
called = true;
return fn.apply(this, args);
};
};
const startApp = function(event) {
console.log(this, event); // document.body, MouseEvent
};
document.body.addEventListener('click', once(startApp)); // only runs `startApp` once upon click
作用:確保一個函式只被呼叫一次,
分析:因為 JavaScript 是單執行緒執行環境,不需要考慮并發環境,直接一個內部變數存到閉包中,每次呼叫前判斷,并在第一次呼叫時,修改其值,讓后續呼叫全部失效,
給你看一下 Go 的 once,官方是通過atomic庫實作的:
package sync
import (
"sync/atomic"
)
type Once struct {
m Mutex
done uint32
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 1 {
return
}
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
over
const over = (...fns) => (...args) => fns.map(fn => fn.apply(null, args));
const minMax = over(Math.min, Math.max);
minMax(1, 2, 3, 4, 5); // [1,5]
作用:利用函式陣列,對接下來的輸入資料進行處理,得到每個函式處理后的結果陣列,
決議:使用map和apply將輸入的資料傳遞到每個函式中進行處理,
overArgs
const overArgs = (fn, transforms) => (...args) => fn(...args.map((val, i) => transforms[i](val)));
const square = n => n * n;
const double = n => n * 2;
const fn = overArgs((x, y) => [x, y], [square, double]);
fn(9, 3); // [81, 6]
作用:利用 transforms 函式陣列,分別處理相應位置的輸入資料,并把結果傳遞進給定函式,
決議:transforms 函式陣列 和引數必須位置對應,這個約束有點強啊,
partial
const partial = (fn, ...partials) => (...args) => fn(...partials, ...args);
const greet = (greeting, name) => greeting + ' ' + name + '!';
const greetHello = partial(greet, 'Hello');
greetHello('John'); // 'Hello John!'
作用:將呼叫函式的資料分為兩次輸入,并按正序呼叫,
決議:兩次使用擴展運算子(...),保存不同時期的資料,最后呼叫,
partialRight
const partialRight = (fn, ...partials) => (...args) => fn(...args, ...partials);
const greet = (greeting, name) => greeting + ' ' + name + '!';
const greetJohn = partialRight(greet, 'John');
greetJohn('Hello'); // 'Hello John!'
作用:將呼叫函式的資料分為兩次輸入,并按反序呼叫,
決議:兩次使用擴展運算子(...),保存不同時期的資料,最后呼叫,
pipeAsyncFunctions
const pipeAsyncFunctions = (...fns) => arg => fns.reduce((p, f) => p.then(f), Promise.resolve(arg));
const sum = pipeAsyncFunctions(
x => x + 1,
x => new Promise(resolve => setTimeout(() => resolve(x + 2), 1000)),
x => x + 3,
async x => (await x) + 4
);
(async () => {
console.log(await sum(5)); // 15 (after one second)
})();
作用:將傳入的多個[異步]函式按照正序 依次呼叫,
決議:結合reduce和Promise.then,將資料按照正序傳遞到每個[異步]函式,進行處理,處理的結果又傳給下一個[異步]函式,以此類推,
promisify
const promisify = func => (...args) =>
new Promise((resolve, reject) =>
func(...args, (err, result) => (err ? reject(err) : resolve(result)))
);
const delay = promisify((d, cb) => setTimeout(cb, d));
delay(2000).then(() => console.log('Hi!')); // // Promise resolves after 2s
作用:將回呼函式改為Promise方式處理結果,
在 Node8+ ,你可以使用util.promisify
決議:首先接受給定的回呼函式,然后直接在 Promise 中呼叫該函式,
因為回呼函式的結果按照規范永遠是最后一個引數,我們只需要在函式呼叫時,把最后一個引數換成 Promise 的方式,即:如果回呼函式出現錯誤則 reject,反之 resolve,
注意:被 promisify 的函式必須接受回呼引數且后續會呼叫,
rearg
const rearg = (fn, indexes) => (...args) => fn(...indexes.map(i => args[i]));
var rearged = rearg(
function(a, b, c) {
return [a, b, c];
},
[2, 0, 1]
);
rearged('b', 'c', 'a'); // ['a', 'b', 'c']
作用:根據指定的索引重新排列傳入的引數,
決議:利用map結合擴展運算子,重新排列傳入的引數,并將轉換后的引數傳遞給fn,
runPromisesInSeries
const runPromisesInSeries = ps => ps.reduce((p, next) => p.then(next), Promise.resolve());
const delay = d => new Promise(r => setTimeout(r, d));
runPromisesInSeries([() => delay(1000), () => delay(2000)]);
// Executes each promise sequentially, taking a total of 3 seconds to complete
作用:按照正序 運行給定的多個回傳型別為 Promise 函式,
決議:使用reduce創建一個Promise鏈,每次運行完一個傳入的 Promise,都會回傳最外部的Promise.then,從而進行下一次呼叫,
sleep
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
async function sleepyWork() {
console.log("I'm going to sleep for 1 second.");
await sleep(1000);
console.log('I woke up after 1 second.');
}
作用: 延遲異步函式的執行,
決議:創建一個接受毫秒數的函式,并結合setTimeout,在給定的毫秒數后,回傳一個resolve狀態的Promise,
使用場景:利用異步函式的“同步”機制(await),使其在異步函式中達到“睡眠”的效果,
spreadOver
const spreadOver = fn => argsArr => fn(...argsArr);
const arrayMax = spreadOver(Math.max);
arrayMax([1, 2, 3]); // 3
作用:將接受可變引數的函式更改為接受陣列,
如果你認真讀了文章,就會發現這是collectInto函式的反模式,
分析:利用了擴展運算子的性質,將傳遞進來的陣列解構再交給處理函式,
times
const times = (n, fn, context = undefined) => {
let i = 0;
while (fn.call(context, i) !== false && ++i < n) {}
};
var output = '';
times(5, i => (output += i));
console.log(output); // 01234
作用:將給定的函式,迭代執行n次,
分析:使用Function.call迭代呼叫給定的函式,并把迭代的次數傳進函式第一個引數,
如果函式回傳 false 可提前退出,
uncurry
const uncurry = (fn, n = 1) => (...args) => {
const next = acc => args => args.reduce((x, y) => x(y), acc);
if (n > args.length) throw new RangeError('Arguments too few!');
return next(fn)(args.slice(0, n));
};
const add = x => y => z => x + y + z;
const uncurriedAdd = uncurry(add, 3);
uncurriedAdd(1, 2, 3); // 6
作用:函式反柯里化,
柯里化是將接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,并且回傳接受余下的引數而且回傳結果的新函式,
而反柯里化就是將多個接受引數的層層函式,鋪平,
決議:反柯里化的關鍵代碼在于 args.reduce((x, y) => x(y), acc),
在上例中,
args: [1,2,3]
acc: x => y => z => x + y + z
第一次回圈:
x:x => y => z => x + y + z
y:1
回傳結果:y => z => 1 + y + z
第二次回圈:
x: y => z => 1 + y + z
y: 2
回傳結果:z => 1 + 2 + z
最后一次回圈的結果,即 1 + 2 +3
可以看出,每次一回圈,都會利用閉包”填充”一個所需變數,
回傳的結果分為兩種情況:
一是 一個保留了 n 個前置引數的函式,
二是層疊函式中最后一個函式的回傳結果,
值得一提的是,在原始碼中使用了slice(0,n)保留適當數量的引數,
如果提供的引數的個數小于給定的決議長度,就會拋出錯誤,
unfold
const unfold = (fn, seed) => {
let result = [],
val = [null, seed];
while ((val = fn(val[1]))) result.push(val[0]);
return result;
};
var f = n => (n > 50 ? false : [-n, n + 10]);
unfold(f, 10); // [-10, -20, -30, -40, -50]
作用:使用種子值以及特殊的資料存盤與迭代方式構建一個陣列,
決議: 我為什么說資料存盤與迭代方式很特殊呢?
迭代的變數與結果值,保存在同一陣列里,用01下標區分,
而迭代的函式,也需要滿足這一規范,回傳同樣的陣列[value,nextSeed],保證下一次迭代,或者回傳false終止程序,
when
const when = (pred, whenTrue) => x => (pred(x) ? whenTrue(x) : x);
const doubleEvenNumbers = when(x => x % 2 === 0, x => x * 2);
doubleEvenNumbers(2); // 4
doubleEvenNumbers(1); // 1
作用:根據pred函式測驗給定資料,如結果為真,則執行whenTrue函式;反之,回傳資料,
決議: 我喜歡語意化的封裝,可大幅提升代碼的可讀性,減少邏輯負擔,
專家級
memoize
const memoize = fn => {
const cache = new Map();
const cached = function(val) {
return cache.has(val) ? cache.get(val) : cache.set(val, fn.call(this, val)) && cache.get(val);
};
cached.cache = cache;
return cached;
};
// See the `anagrams` snippet.
const anagramsCached = memoize(anagrams);
anagramsCached('javascript'); // takes a long time
anagramsCached('javascript'); // returns virtually instantly since it's now cached
console.log(anagramsCached.cache); // The cached anagrams map
作用:為給定的函式添加快取功能,
決議: 通過實體化一個新的Map物件來創建一個空的快取,
并對函式的呼叫進一步的封裝,如果呼叫時,傳入了一個之前已經傳遞過的引數,將從快取中直接回傳結果,執行時間為O(1);如果是首次傳遞,則需運行函式,將得到結果快取,并回傳,
其實,我們還可以借助這個片段,看到一絲 JavaScript 語法的殘缺,
到目前為止,一個社區公認的私有屬性語法都沒有,TC39 一直提議用#號,并闡述了很多原因、宣告,
哎,說白了,就是 JavaScript 從一開始設計的失誤,到現在已經無法挽回了,
throttle
const throttle = (fn, wait) => {
let inThrottle, lastFn, lastTime;
return function() {
const context = this,
args = arguments;
if (!inThrottle) {
fn.apply(context, args);
lastTime = Date.now();
inThrottle = true;
} else {
clearTimeout(lastFn);
lastFn = setTimeout(function() {
if (Date.now() - lastTime >= wait) {
fn.apply(context, args);
lastTime = Date.now();
}
}, Math.max(wait - (Date.now() - lastTime), 0));
}
};
};
window.addEventListener(
'resize',
throttle(function(evt) {
console.log(window.innerWidth);
console.log(window.innerHeight);
}, 250)
); // Will log the window dimensions at most every 250ms
作用: 函式節流,
什么是防抖和節流?有什么區別?如何實作? 一文中關于防抖解釋:
高頻事件觸發,但在 n 秒內只會執行一次,所以節流會稀釋函式的執行頻率,
同樣,節流也是面試必考的點,
決議:第一次執行時,立即執行給定函式,保存當前的時間,并設定標記變數,
標記變數主要用于判斷是否第一次呼叫,如果是第一次則立刻運行,
反之不是第一次運行,過了等待的毫秒后才可繼續運行,
主要邏輯是每次運行前先清除上一個的定時器,然后計算出上一次運行的時間與給定的運行間隔所差的毫秒數,并利用其資料新建一個定時器運行,
定時器里的函式除了呼叫給定函式,還會更新上一次運行的時間變數,
節流的實作,網上的文章有很多版本,但多少都有點瑕疵,
結束語
呼,花了很長的時間,終于搞定了這篇文章,
以后的 30s 原始碼刨析系列會挑選一些原始碼片段去決議,而不是針對某一分類了,
本篇文章涉及了我的一些思考,希望能對你有幫助,
轉載文章請注明作者和出處 一個壞掉的番茄,請勿用于任何商業用途,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/145749.html
標籤:JavaScript
上一篇:帶你學習ES5中新增的方法
