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

来源:https://www.cnblogs.com/jinma/archive/2020/03/01/12392094.html
-Advertisement-
Play Games

由淺入深、逐個擊破 30SecondsOfCode 中函數系列所有源碼片段,帶你領略源碼之美。 ...


前言

由淺入深、逐個擊破 30SecondsOfCode 中函數系列所有源碼片段,帶你領略源碼之美。

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

本篇是其中的函數篇,可以在極短的時間內培養你的函數式思維。

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

  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

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

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

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函數,進一步處理,可用作分析統計。

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

本篇文章涉及了我的一些思考,希望能對你有幫助。

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 介面目前可用:抖音、塊手、火山、皮皮蝦、皮皮搞笑、最右、小紅書、美拍、今日頭條、微博、秒拍、小咖秀、晃咖、微視、全民小視頻、百度(好看)視頻、全民K歌、唱吧、音悅台、WIDE、嗶哩嗶哩、AcFun、梨視頻、趣多拍、網易雲音樂、陌陌、56網、芒果TV、CCTV、BBC、TikTok、Vigo、YouT ...
  • 從選伺服器,功能變數名稱,到配置伺服器,安裝環境,打包上傳,pm2進程守護一條龍。 ...
  • 1、使用npm的預設配置初始化一個項目 nom init -y 2、安裝並使用當前目錄下的webpack,需要先卸載全局的webpack npm uninstall webpack web pack-cli -g 3、將webpack安裝在當前目錄下,好處是不同工程適配不同版本的webpack np ...
  • 數據蘊藏價值,但數據的價值需要用 IT 技術去發現、探索,可視化可以幫助人更好的去分析數據,信息的質量很大程度上依賴於其呈現方式。在數據分析上,熱力圖無疑是一種很好的方式。在很多行業中都有著廣泛的應用。最近剛好項目中需要用到 3D 熱力圖的效果展示。網上搜了相關資料,發現大多數是 2D 效果或者偽 ... ...
  • 自定義地址欄logo: 效果: 實現: 首先選一張自己中意的圖片,然後在該網站上:https://tool.lu/favicon/將圖片轉為ioc格式 然後將該格式的圖片保存後,再上傳到自己的博客的文件中: 本博客logo的地址:https://files.cnblogs.com/files/fan ...
  • 效果圖 ad.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>ad</title> <link rel="stylesheet" href="../css/base.css"> <link rel= ...
  • Map 數據結構 Map 對象保存鍵值對。任何值(對象或原始值) 都可以作為一個鍵或一個值。JavaScript的對象(Object),本質上是鍵值對的集合(Hash 結構),但是傳統上還是只能用字元串當作鍵,這給它的使用帶來了很大的限制。 Maps 和 Objects 的區別 1、一個 Objec ...
  • 一、JSONP的策略就是伺服器端可以動態生成JSON文件,把客戶端需要的數據放到這個文件中,讓客戶端通過script標簽的src屬性來請求這個文件。二、CORS無需客戶端做出任何變化,只是在被請求的服務端響應的時候添加一個響應頭Access-Control-Allow-Origin,表示這個資源是否... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...