主頁 > 企業開發 > 30s原始碼刨析系列之函式篇

30s原始碼刨析系列之函式篇

2020-10-01 16:38:32 企業開發

前言

由淺入深、逐個擊破 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 原始碼刨析系列會挑選一些原始碼片段去決議,而不是針對某一分類了,

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

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/145749.html

標籤:JavaScript

上一篇:帶你學習ES5中新增的方法

下一篇:Ajax-解決跨域請求(jsonp、cors)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • IEEE1588PTP在數字化變電站時鐘同步方面的應用

    IEEE1588ptp在數字化變電站時鐘同步方面的應用 京準電子科技官微——ahjzsz 一、電力系統時間同步基本概況 隨著對IEC 61850標準研究的不斷深入,國內外學者提出基于IEC61850通信標準體系建設數字化變電站的發展思路。數字化變電站與常規變電站的顯著區別在于程序層傳統的電流/電壓互 ......

    uj5u.com 2020-09-10 03:51:52 more
  • HTTP request smuggling CL.TE

    CL.TE 簡介 前端通過Content-Length處理請求,通過反向代理或者負載均衡將請求轉發到后端,后端Transfer-Encoding優先級較高,以TE處理請求造成安全問題。 檢測 發送如下資料包 POST / HTTP/1.1 Host: ac391f7e1e9af821806e890 ......

    uj5u.com 2020-09-10 03:52:11 more
  • 網路滲透資料大全單——漏洞庫篇

    網路滲透資料大全單——漏洞庫篇漏洞庫 NVD ——美國國家漏洞庫 →http://nvd.nist.gov/。 CERT ——美國國家應急回應中心 →https://www.us-cert.gov/ OSVDB ——開源漏洞庫 →http://osvdb.org Bugtraq ——賽門鐵克 →ht ......

    uj5u.com 2020-09-10 03:52:15 more
  • 京準講述NTP時鐘服務器應用及原理

    京準講述NTP時鐘服務器應用及原理京準講述NTP時鐘服務器應用及原理 安徽京準電子科技官微——ahjzsz 北斗授時原理 授時是指接識訓通過某種方式獲得本地時間與北斗標準時間的鐘差,然后調整本地時鐘使時差控制在一定的精度范圍內。 衛星導航系統通常由三部分組成:導航授時衛星、地面檢測校正維護系統和用戶 ......

    uj5u.com 2020-09-10 03:52:25 more
  • 利用北斗衛星系統設計NTP網路時間服務器

    利用北斗衛星系統設計NTP網路時間服務器 利用北斗衛星系統設計NTP網路時間服務器 安徽京準電子科技官微——ahjzsz 概述 NTP網路時間服務器是一款支持NTP和SNTP網路時間同步協議,高精度、大容量、高品質的高科技時鐘產品。 NTP網路時間服務器設備采用冗余架構設計,高精度時鐘直接來源于北斗 ......

    uj5u.com 2020-09-10 03:52:35 more
  • 詳細解讀電力系統各種對時方式

    詳細解讀電力系統各種對時方式 詳細解讀電力系統各種對時方式 安徽京準電子科技官微——ahjzsz,更多資料請添加VX 衛星同步時鐘是我京準公司開發研制的應用衛星授時時技術的標準時間顯示和發送的裝置,該裝置以M國全球定位系統(GLOBAL POSITIONING SYSTEM,縮寫為GPS)或者我國北 ......

    uj5u.com 2020-09-10 03:52:45 more
  • 如何保證外包團隊接入企業內網安全

    不管企業規模的大小,只要企業想省錢,那么企業的某些服務就一定會采用外包的形式,然而看似美好又經濟的策略,其實也有不好的一面。下面我通過安全的角度來聊聊使用外包團的安全隱患問題。 先看看什么服務會使用外包的,最常見的就是話務/客服這種需要大量重復性、無技術性的服務,或者是一些銷售外包、特殊的職能外包等 ......

    uj5u.com 2020-09-10 03:52:57 more
  • PHP漏洞之【整型數字型SQL注入】

    0x01 什么是SQL注入 SQL是一種注入攻擊,通過前端帶入后端資料庫進行惡意的SQL陳述句查詢。 0x02 SQL整型注入原理 SQL注入一般發生在動態網站URL地址里,當然也會發生在其它地發,如登錄框等等也會存在注入,只要是和資料庫打交道的地方都有可能存在。 如這里http://192.168. ......

    uj5u.com 2020-09-10 03:55:40 more
  • [GXYCTF2019]禁止套娃

    git泄露獲取原始碼 使用GET傳參,引數為exp 經過三層過濾執行 第一層過濾偽協議,第二層過濾帶引數的函式,第三層過濾一些函式 preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'] (?R)參考當前正則運算式,相當于匹配函式里的引數 因此傳遞 ......

    uj5u.com 2020-09-10 03:56:07 more
  • 等保2.0實施流程

    流程 結論 ......

    uj5u.com 2020-09-10 03:56:16 more
最新发布
  • 使用Django Rest framework搭建Blog

    在前面的Blog例子中我們使用的是GraphQL, 雖然GraphQL的使用處于上升趨勢,但是Rest API還是使用的更廣泛一些. 所以還是決定回到傳統的rest api framework上來, Django rest framework的官網上給了一個很好用的QuickStart, 我參考Qu ......

    uj5u.com 2023-04-20 08:17:54 more
  • 記錄-new Date() 我忍你很久了!

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 大家平時在開發的時候有沒被new Date()折磨過?就是它的諸多怪異的設定讓你每每用的時候,都可能不小心踩坑。造成程式意外出錯,卻一下子找不到問題出處,那叫一個煩透了…… 下面,我就列舉它的“四宗罪”及應用思考 可惡的四宗罪 1. Sa ......

    uj5u.com 2023-04-20 08:17:47 more
  • 使用Vue.js實作文字跑馬燈效果

    實作文字跑馬燈效果,首先用到 substring()截取 和 setInterval計時器 clearInterval()清除計時器 效果如下: 實作代碼如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta ......

    uj5u.com 2023-04-20 08:12:31 more
  • JavaScript 運算子

    JavaScript 運算子/運算子 在 JavaScript 中,有一些運算子可以使代碼更簡潔、易讀和高效。以下是一些常見的運算子: 1、可選鏈運算子(optional chaining operator) ?.是可選鏈運算子(optional chaining operator)。?. 可選鏈操 ......

    uj5u.com 2023-04-20 08:02:25 more
  • CSS—相對單位rem

    一、概述 rem是一個相對長度單位,它的單位長度取決于根標簽html的字體尺寸。rem即root em的意思,中文翻譯為根em。瀏覽器的文本尺寸一般默認為16px,即默認情況下: 1rem = 16px rem布局原理:根據CSS媒體查詢功能,更改根標簽的字體尺寸,實作rem單位隨螢屏尺寸的變化,如 ......

    uj5u.com 2023-04-20 08:02:21 more
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 08:01:50 more
  • 如何在 vue3 中使用 jsx/tsx?

    我們都知道,通常情況下我們使用 vue 大多都是用的 SFC(Signle File Component)單檔案組件模式,即一個組件就是一個檔案,但其實 Vue 也是支持使用 JSX 來撰寫組件的。這里不討論 SFC 和 JSX 的好壞,這個仁者見仁智者見智。本篇文章旨在帶領大家快速了解和使用 Vu ......

    uj5u.com 2023-04-20 08:01:37 more
  • 【Vue2.x原始碼系列06】計算屬性computed原理

    本章目標:計算屬性是如何實作的?計算屬性快取原理以及洋蔥模型的應用?在初始化Vue實體時,我們會給每個計算屬性都創建一個對應watcher,我們稱之為計算屬性watcher ......

    uj5u.com 2023-04-20 08:01:31 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:01:10 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:00:32 more