由淺入深、逐個擊破 30SecondsOfCode 中函數系列所有源碼片段,帶你領略源碼之美。 ...
前言
由淺入深、逐個擊破 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.debug
API 和函數的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)
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 源碼刨析系列會挑選一些源碼片段去解析,而不是針對某一分類了。
本篇文章涉及了我的一些思考,希望能對你有幫助。
轉載文章請註明作者和出處 一個壞掉的番茄,請勿用於任何商業用途。