30s源碼刨析系列之函數篇

前言

由淺入深、逐個擊破 30SecondsOfCode 中函數系列全部源碼片斷,帶你領略源碼之美。javascript

本系列是對名庫 30SecondsOfCode 的深刻刨析。java

本篇是其中的函數篇,能夠在極短的時間內培養你的函數式思惟。git

內容根據源碼的難易等級進行排版,目錄以下:es6

  1. 新手級
  2. 普通級
  3. 專家級

正文

新手級

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

做用:檢查參數是否存在給定的屬性。github

解析:給定一個檢查函數,和所需檢查的屬性名,返回一個函數。可經過調用 返回的函數,去斷定 傳入的對象參數是否符合檢查函數。面試

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,咱們想要反轉其結果,即可對它的調用方式進行進一步的抽象,把反轉結果的邏輯放置抽象中。promise

在本函數中,只須要一個 邏輯非運算符!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函數,進一步處理,可用做分析統計。

解析: 使用mapapply將參數數據傳遞給每一個處理函數,並將處理後的結果交給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秒內高頻事件再次被觸發,則從新計算時間。

一樣,防抖也是面試必考的點。

解析: 傳入需防抖的函數,和防抖的時間間隔,返回一個已防抖化的函數。

主要藉助setTimeoutfunction + 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]

做用:利用函數數組,對接下來的輸入數據進行處理,獲得每一個函數處理後的結果數組。

解析:使用mapapply將輸入的數據傳遞到每一個函數中進行處理。

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)
})();

做用:將傳入的多個[異步]函數按照正序 依次調用。

解析:結合reducePromise.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 源碼刨析系列會挑選一些源碼片斷去解析,而不是針對某一分類了。

本篇文章涉及了個人一些思考,但願能對你有幫助。

轉載文章請註明做者和出處 一個壞掉的番茄,請勿用於任何商業用途。

相關文章
相關標籤/搜索