前言

由浅入深、逐个击破 30SecondsOfCode 中函数系列所有源码片段,带你领略源码之美。

本系列是对名库 30SecondsOfCode 的深入刨析。

本篇是其中的函数篇,可以在极短的时间内培养你的函数式思维。

内容根据源码的难易等级进行排版,目录如下:

  1. 新手级
  2. 普通级
  3. 专家级

正文

新手级

checkProp

  1. const checkProp = (predicate, prop) => obj => !!predicate(obj[prop]);
  2. const lengthIs4 = checkProp(l => l === 4, 'length');
  3. lengthIs4([]); // false
  4. lengthIs4([1, 2, 3, 4]); // true
  5. lengthIs4(new Set([1, 2, 3, 4])); // false (Set uses Size, not length)
  6. const session = { user: {} };
  7. const validUserSession = checkProp(u => u.active && !u.disabled, 'user');
  8. validUserSession(session); // false
  9. session.user.active = true;
  10. validUserSession(session); // true
  11. const noLength = checkProp(l => l === undefined, 'length');
  12. noLength([]); // false
  13. noLength({}); // true
  14. noLength(new Set()); // true

作用:检查参数是否存在给定的属性。

解析:给定一个检查函数,和所需检查的属性名,返回一个函数。可通过调用 返回的函数,去判定 传入的对象参数是否符合检查函数。

functionName

  1. const functionName = fn => (console.debug(fn.name), fn);
  2. functionName(Math.max); // max (logged in debug channel of console)

作用:打印函数名。

解析:使用console.debugAPI 和函数的name属性,把 函数类型参数的名字 打印到控制台的debug channel中。

negate

  1. const negate = func => (...args) => !func(...args);
  2. [1, 2, 3, 4, 5, 6].filter(negate(n => n % 2 === 0)); // [ 1, 3, 5 ]

作用:反转 谓词函数(返回类型为布尔的函数)的返回结果。

解析:假设有一谓词函数为func = args => bool,我们想要反转其结果,便可对它的调用方式进行进一步的抽象,把反转结果的逻辑放置抽象中。

在本函数中,只需要一个 逻辑非运算符!func(...args)

而扩展运算符...是对参数的抽象,代表的是传入的所有参数,我们要将所有参数一个不差地传递,不可破环 谓词函数的“纯洁性”。

unary

  1. const unary = fn => val => fn(val);
  2. ['6', '8', '10'].map(unary(parseInt)); // [6, 8, 10]

作用:参数函数调用时 只接受 参数函数的第一个参数,忽略其他参数。

解析:包装一个函数,并不做任何处理:wrap = fn => (...args) => fn(...args)

很显然,如果想对传入的参数进行处理,只需对args动刀,而本例直接使用了单独的一个变量,忽略了其他参数。

普通级

ary

  1. const ary = (fn, n) => (...args) => fn(...args.slice(0, n));
  2. const firstTwoMax = ary(Math.max, 2);
  3. [[2, 6, 'a'], [6, 4, 8], [10]].map(x => firstTwoMax(...x)); // [6, 6, 1

作用:参数函数调用时 只接受 参数函数的前 n 个参数,忽略其他参数。

解析:和上列逻辑如出一辙,只不过处理参数的逻辑换成了...args.slice(0, n),只要前n个。

attempt

  1. const attempt = (fn, ...args) => {
  2. try {
  3. return fn(...args);
  4. } catch (e) {
  5. return e instanceof Error ? e : new Error(e);
  6. }
  7. };
  8. var elements = attempt(function(selector) {
  9. return document.querySelectorAll(selector);
  10. }, '>_>');
  11. if (elements instanceof Error) elements = []; // elements = []

作用:对 参数函数 进行异常捕获,如果有异常则抛出。

解析:对 参数函数 进行进一步封装,本例封装的逻辑是try catch,即捕获参数函数的异常。

很久之前,我看到过一个关于java8的 attempt 片段,里面还增加了重试逻辑。

js 实现代码如下:

  1. const attempt = (fn, ...args, count, bound) => {
  2. try {
  3. return fn(...args);
  4. } catch (e) {
  5. if(count == bound){
  6. return e instanceof Error ? e : new Error(e);
  7. }
  8. return attempt(fn, ...args, count + 1, bound)
  9. }
  10. };

bind

  1. const bind = (fn, context, ...boundArgs) => (...args) => fn.apply(context, [...boundArgs, ...args]);
  2. function greet(greeting, punctuation) {
  3. return greeting + ' ' + this.user + punctuation;
  4. }
  5. const freddy = { user: 'fred' };
  6. const freddyBound = bind(greet, freddy);
  7. 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

  1. const bindKey = (context, fn, ...boundArgs) => (...args) =>
  2. context[fn].apply(context, [...boundArgs, ...args]);
  3. const freddy = {
  4. user: 'fred',
  5. greet: function(greeting, punctuation) {
  6. return greeting + ' ' + this.user + punctuation;
  7. }
  8. };
  9. const freddyBound = bindKey(freddy, 'greet');
  10. console.log(freddyBound('hi', '!')); // 'hi fred!'

作用:把上列中的fn换成了context[fn]

解析:我们原来的 参数函数 变成了一个 上下文参数的一个属性,而将这个属性依附于上下文对象就成了一个函数context[fn]

可以说,这个一个调用方式特殊的bind

call

  1. const call = (key, ...args) => context => context[key](...args);
  2. Promise.resolve([1, 2, 3])
  3. .then(call('map', x => 2 * x))
  4. .then(console.log); // [ 2, 4, 6 ]
  5. const map = call.bind(null, 'map');
  6. Promise.resolve([1, 2, 3])
  7. .then(map(x => 2 * x))
  8. .then(console.log); // [ 2, 4, 6 ]

作用:动态改变函数执行的上下文。

解析:给定一个属性参数,再给定一组调用参数,返回一个接受上下文对象的函数,并最终组合调用。

其实这里面暗含了一个约束,很显然,context[key]必须是一个函数。

这个片段本质是对上下文的抽象。举个例子:

  1. const filterMen = call('filter', person => person.sex === 'man')
  2. filterMen([{sex:'woman',...},{sex:'man',...},...])
  3. // 如果有其他 上下文对象,本例中也就是数组 需要相同的 逻辑过滤呢?

chainAsync

  1. const chainAsync = fns => {
  2. let curr = 0;
  3. const last = fns[fns.length - 1];
  4. const next = () => {
  5. const fn = fns[curr++];
  6. fn === last ? fn() : fn(next);
  7. };
  8. next();
  9. };
  10. chainAsync([
  11. next => {
  12. console.log('0 seconds');
  13. setTimeout(next, 1000);
  14. },
  15. next => {
  16. console.log('1 second');
  17. setTimeout(next, 1000);
  18. },
  19. () => {
  20. console.log('2 second');
  21. }
  22. ]);

作用:将 函数数组转换为有决策权的链式函数调用。

我为什么称之有决策权的链式函数调用呢?

因为每个函数都会接受一个next方法参数,它代表的就是调用链中的下一个函数,所以什么时候调用下一个函数,要不要调用,决策权在你。

解析:其实这个片段很简单。

首先,fns 类型一个函数数组,其中除了最后一个函数都有隐含的约束,可以选择接受 next 参数。

而 next 参数的含义就是调用链中的下一个函数,说白了 就是数组中的下一个成员。

而最后一个函数是无参函数。

片段中复杂点在于:利用闭包存储了两个关键变量。

第一个是 调用链中的函数游标:curr;第二个是结束标志,最后一个函数:last

每次链式向下调用前,都会进行一些逻辑处理:

  1. const next = () => {
  2. const fn = fns[curr++];
  3. fn === last ? fn() : fn(next);
  4. };

先取出当前游标所在函数,再把游标指向下一个函数。

然后,判断是否是最后一个函数,是则直接调用,结束;反之,传入 next 调用。

如果,你是一个后端开发者,可以把其理解为中间件的工作模式。

collectInto

  1. const collectInto = fn => (...args) => fn(args);
  2. const Pall = collectInto(Promise.all.bind(Promise));
  3. let p1 = Promise.resolve(1);
  4. let p2 = Promise.resolve(2);
  5. let p3 = new Promise(resolve => setTimeout(resolve, 2000, 3));
  6. Pall(p1, p2, p3).then(console.log); // [1, 2, 3] (after about 2 seconds)

作用:将接受数组的函数更改为接受可变参数。

分析:利用了扩展运算符的性质,...args代表的是所有参数组成的数组,然后将这数组传递进去调用。

可别小看了这一片段,调用方式的改变会决定很多上层逻辑。

平常我们大概率都会,建立一个数组,收集所需的异步函数。

在本例中,很明显的看到 从参数为数组类型的约束 中解放了出来。

compose

  1. const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));
  2. const substract3 = x => x - 3;
  3. const add5 = x => x + 5;
  4. const multiply = (x, y) => x * y;
  5. const multiplyAndAdd5AndSubstract3 = compose(
  6. substract3,
  7. add5,
  8. multiply
  9. );
  10. multiplyAndAdd5AndSubstract3(5, 2); // 12

作用:将传入的多个[异步]函数以组合的方式 调用。

先将参数传入最后一个[异步]函数,然后将得到的结果,传入倒数第二个[异步]函数,以此类推。

compose可以说是函数式编程的经典片段。

它的具体意义可以说是逻辑分层。像洋葱一样,一层一层地处理数据。

解析:fns 代表的是 传入的多个函数 组成的数组。

利用reduce方法实现函数的“洋葱”包裹。

因为这种逻辑语义表示效果不好,就直接上上面例子的代码流程了。

  1. reduce 第一次循环:
  2. f: substract3;
  3. g: add5;
  4. 返回结果:(...args) => substract3(add5(...args));
  5. reduce 第二次循环:
  6. f: (...args) => substract3(add5(...args));
  7. g: multiply;
  8. 返回结果:
  9. (...args1) => ((...args2) => substract3(add5(...args2)))(multiply(...args1))
  10. 优化后:
  11. (...args) => substract3(add5(multiply(...args)));
  12. 循环下去,以此类推...
  13. 最后的返回的形式:
  14. (...args) => 第一个函数(第二个函数(第三个函数(...最后一个函数(...args))))

PS: 说实话,我并不喜欢 compose,在上例中就可以很明显的看到缺点。

把很多函数组合起来,第一是缺少语义化,与之对应的例子就是 Promise 的 then 调用链,语义鲜明;

第二是无法添加函数与函数之间的抽象逻辑,只能一次写好。

第三是各个函数之间存在隐含的参数约束,很可怕的。

composeRight

  1. const composeRight = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args)));
  2. const add = (x, y) => x + y;
  3. const square = x => x * x;
  4. const substract3 = x => x - 3;
  5. const addAndSquare = composeRight(add, square,substract3);
  6. addAndSquareAndSubstract3(1, 2); // 6

作用:将传入的多个[异步]函数以组合的方式 调用。

先将参数传入第一个[异步]函数,然后将得到的结果,传入第二个[异步]函数,以此类推。

converge

  1. const converge = (converger, fns) => (...args) => converger(...fns.map(fn => fn.apply(null, args)));
  2. const average = converge((a, b) => a / b, [
  3. arr => arr.reduce((a, v) => a + v, 0),
  4. arr => arr.length
  5. ]);
  6. average([1, 2, 3, 4, 5, 6, 7]); // 4

作用:将 函数数组的返回结果 传递到converger函数,进一步处理,可用作分析统计。

解析: 使用mapapply将参数数据传递给每个处理函数,并将处理后的结果交给converger函数。

curry

  1. const curry = (fn, arity = fn.length, ...args) =>
  2. arity <= args.length ? fn(...args) : curry.bind(null, fn, arity, ...args);
  3. curry(Math.pow)(2)(10); // 1024
  4. curry(Math.min, 3)(10)(50)(2); // 2

作用:函数柯里化。

柯里化不管在是函数式思维的理解,还是现实面试中,都非常的重要。

维基百科上 柯里化的解释

把接受多个参数函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数

解析:这个bind用得真是神了,借助它积累每次传进来的参数,等到参数足够时,再调用。

debounce

  1. const debounce = (fn, ms = 0) => {
  2. let timeoutId;
  3. return function(...args) {
  4. clearTimeout(timeoutId);
  5. timeoutId = setTimeout(() => fn.apply(this, args), ms);
  6. };
  7. };
  8. window.addEventListener(
  9. 'resize',
  10. debounce(() => {
  11. console.log(window.innerWidth);
  12. console.log(window.innerHeight);
  13. }, 250)
  14. ); // Will log the window dimensions at most every 250ms

作用:函数防抖。

什么是防抖和节流?有什么区别?如何实现? 一文中关于防抖解释:

触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间。

同样,防抖也是面试必考的点。

解析: 传入需防抖的函数,和防抖的时间间隔,返回一个已防抖化的函数。

主要借助setTimeoutfunction + apply保存上下文完成。

每次调用函数前,都执行一遍clearTimeout,保证重新计算调用时间。

无论是调用多么频繁的函数都会在指定时间的间隔后只运行一次。

defer

  1. const defer = (fn, ...args) => setTimeout(fn, 1, ...args);
  2. // Example A:
  3. defer(console.log, 'a'), console.log('b'); // logs 'b' then 'a'
  4. // Example B:
  5. document.querySelector('#someElement').innerHTML = 'Hello';
  6. longRunningFunction(); // Browser will not update the HTML until this has finished
  7. defer(longRunningFunction); // Browser will update the HTML then run the function

作用:推迟调用函数,直到清除当前调用堆栈。

可适用于推迟 cpu 密集型计算,以免阻塞渲染引擎工作。

分析:使用setTimeout(超时时间为1ms)将 函数参数 添加到浏览器事件队列末尾。

因为 JavaScript 是单线程执行,先是主线程执行完毕,然后在读取事件队列中的代码执行。

如果主线程有运行时间太长的函数,会阻塞页面渲染,所以将其放置到事件队列。

delay

  1. const delay = (fn, wait, ...args) => setTimeout(fn, wait, ...args);
  2. delay(
  3. function(text) {
  4. console.log(text);
  5. },
  6. 1000,
  7. 'later'
  8. ); // Logs 'later' after one second.

作用:延迟函数执行。

是的,它和defer非常像,但使用场景却是不一样。

defer 的目的是将占据主线程时间长的函数推迟到事件队列。

而 delay 只是字面意思,延迟执行。

解析:对 setTimeout 进行语义化封装。

flip

  1. const flip = fn => (first, ...rest) => fn(...rest, first);
  2. let a = { name: 'John Smith' };
  3. let b = {};
  4. const mergeFrom = flip(Object.assign);
  5. let mergePerson = mergeFrom.bind(null, a);
  6. mergePerson(b); // == b
  7. b = {};
  8. Object.assign(b, a); // == b

作用:对 参数函数 的输入数据进行进一步处理,将数据的第一个参数与其余参数位置对调。

解析:主要利用 扩展运算符的性质,对参数的位置进行调整。

如果你不了解这一语言特性,可参考阮一峰老师的ES6入门

hz

  1. const hz = (fn, iterations = 100) => {
  2. const before = performance.now();
  3. for (let i = 0; i < iterations; i++) fn();
  4. return (1000 * iterations) / (performance.now() - before);
  5. };
  6. // 10,000 element array
  7. const numbers = Array(10000)
  8. .fill()
  9. .map((_, i) => i);
  10. // Test functions with the same goal: sum up the elements in the array
  11. const sumReduce = () => numbers.reduce((acc, n) => acc + n, 0);
  12. const sumForLoop = () => {
  13. let sum = 0;
  14. for (let i = 0; i < numbers.length; i++) sum += numbers[i];
  15. return sum;
  16. };
  17. // `sumForLoop` is nearly 10 times faster
  18. Math.round(hz(sumReduce)); // 572
  19. Math.round(hz(sumForLoop)); // 4784

作用:返回函数每秒执行一次的次数。

hz是赫兹的单位(频率的单位)定义为每秒一个周期。

解析:通过两次使用performance.now获取iterations次迭代前后的毫秒差。

然后将毫秒转换为秒并除以经过的时间,可以得到每秒的函数执行次数。

PS: 此处,并没有太好的个人理解,翻译自官方

once

  1. const once = fn => {
  2. let called = false;
  3. return function(...args) {
  4. if (called) return;
  5. called = true;
  6. return fn.apply(this, args);
  7. };
  8. };
  9. const startApp = function(event) {
  10. console.log(this, event); // document.body, MouseEvent
  11. };
  12. document.body.addEventListener('click', once(startApp)); // only runs `startApp` once upon click

作用:确保一个函数只被调用一次。

分析:因为 JavaScript 是单线程执行环境,不需要考虑并发环境,直接一个内部变量存到闭包中,每次调用前判断,并在第一次调用时,修改其值,让后续调用全部失效。

给你看一下 Go 的 once,官方是通过atomic库实现的:

  1. package sync
  2. import (
  3. "sync/atomic"
  4. )
  5. type Once struct {
  6. m Mutex
  7. done uint32
  8. }
  9. func (o *Once) Do(f func()) {
  10. if atomic.LoadUint32(&o.done) == 1 {
  11. return
  12. }
  13. o.m.Lock()
  14. defer o.m.Unlock()
  15. if o.done == 0 {
  16. defer atomic.StoreUint32(&o.done, 1)
  17. f()
  18. }
  19. }

over

  1. const over = (...fns) => (...args) => fns.map(fn => fn.apply(null, args));
  2. const minMax = over(Math.min, Math.max);
  3. minMax(1, 2, 3, 4, 5); // [1,5]

作用:利用函数数组,对接下来的输入数据进行处理,得到每个函数处理后的结果数组。

解析:使用mapapply将输入的数据传递到每个函数中进行处理。

overArgs

  1. const overArgs = (fn, transforms) => (...args) => fn(...args.map((val, i) => transforms[i](val)));
  2. const square = n => n * n;
  3. const double = n => n * 2;
  4. const fn = overArgs((x, y) => [x, y], [square, double]);
  5. fn(9, 3); // [81, 6]

作用:利用 transforms 函数数组,分别处理相应位置的输入数据,并把结果传递进给定函数。

解析:transforms 函数数组 和参数必须位置对应,这个约束有点强啊。

partial

  1. const partial = (fn, ...partials) => (...args) => fn(...partials, ...args);
  2. const greet = (greeting, name) => greeting + ' ' + name + '!';
  3. const greetHello = partial(greet, 'Hello');
  4. greetHello('John'); // 'Hello John!'

作用:将调用函数的数据分为两次输入,并按正序调用。

解析:两次使用扩展运算符(...),保存不同时期的数据,最后调用。

partialRight

  1. const partialRight = (fn, ...partials) => (...args) => fn(...args, ...partials);
  2. const greet = (greeting, name) => greeting + ' ' + name + '!';
  3. const greetJohn = partialRight(greet, 'John');
  4. greetJohn('Hello'); // 'Hello John!'

作用:将调用函数的数据分为两次输入,并按反序调用。

解析:两次使用扩展运算符(...),保存不同时期的数据,最后调用。

pipeAsyncFunctions

  1. const pipeAsyncFunctions = (...fns) => arg => fns.reduce((p, f) => p.then(f), Promise.resolve(arg));
  2. const sum = pipeAsyncFunctions(
  3. x => x + 1,
  4. x => new Promise(resolve => setTimeout(() => resolve(x + 2), 1000)),
  5. x => x + 3,
  6. async x => (await x) + 4
  7. );
  8. (async () => {
  9. console.log(await sum(5)); // 15 (after one second)
  10. })();

作用:将传入的多个[异步]函数按照正序 依次调用。

解析:结合reducePromise.then,将数据按照正序传递到每个[异步]函数,进行处理,处理的结果又传给下一个[异步]函数,以此类推。

promisify

  1. const promisify = func => (...args) =>
  2. new Promise((resolve, reject) =>
  3. func(...args, (err, result) => (err ? reject(err) : resolve(result)))
  4. );
  5. const delay = promisify((d, cb) => setTimeout(cb, d));
  6. delay(2000).then(() => console.log('Hi!')); // // Promise resolves after 2s

作用:将回调函数改为Promise方式处理结果。

在 Node8+ ,你可以使用util.promisify

解析:首先接受给定的回调函数,然后直接在 Promise 中调用该函数。

因为回调函数的结果按照规范永远是最后一个参数,我们只需要在函数调用时,把最后一个参数换成 Promise 的方式,即:如果回调函数出现错误则 reject,反之 resolve。

注意:被 promisify 的函数必须接受回调参数且后续会调用。

rearg

  1. const rearg = (fn, indexes) => (...args) => fn(...indexes.map(i => args[i]));
  2. var rearged = rearg(
  3. function(a, b, c) {
  4. return [a, b, c];
  5. },
  6. [2, 0, 1]
  7. );
  8. rearged('b', 'c', 'a'); // ['a', 'b', 'c']

作用:根据指定的索引重新排列传入的参数。

解析:利用map结合扩展运算符,重新排列传入的参数,并将转换后的参数传递给fn。

runPromisesInSeries

  1. const runPromisesInSeries = ps => ps.reduce((p, next) => p.then(next), Promise.resolve());
  2. const delay = d => new Promise(r => setTimeout(r, d));
  3. runPromisesInSeries([() => delay(1000), () => delay(2000)]);
  4. // Executes each promise sequentially, taking a total of 3 seconds to complete

作用:按照正序 运行给定的多个返回类型为 Promise 函数。

解析:使用reduce创建一个Promise链,每次运行完一个传入的 Promise,都会返回最外部的Promise.then,从而进行下一次调用。

sleep

  1. const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
  2. async function sleepyWork() {
  3. console.log("I'm going to sleep for 1 second.");
  4. await sleep(1000);
  5. console.log('I woke up after 1 second.');
  6. }

作用: 延迟异步函数的执行。

解析:创建一个接受毫秒数的函数,并结合setTimeout,在给定的毫秒数后,返回一个resolve状态的Promise。

使用场景:利用异步函数的“同步”机制(await),使其在异步函数中达到“睡眠”的效果。

spreadOver

  1. const spreadOver = fn => argsArr => fn(...argsArr);
  2. const arrayMax = spreadOver(Math.max);
  3. arrayMax([1, 2, 3]); // 3

作用:将接受可变参数的函数更改为接受数组。

如果你认真读了文章,就会发现这是collectInto函数的反模式。

分析:利用了扩展运算符的性质,将传递进来的数组解构再交给处理函数。

times

  1. const times = (n, fn, context = undefined) => {
  2. let i = 0;
  3. while (fn.call(context, i) !== false && ++i < n) {}
  4. };
  5. var output = '';
  6. times(5, i => (output += i));
  7. console.log(output); // 01234

作用:将给定的函数,迭代执行n次。

分析:使用Function.call迭代调用给定的函数,并把迭代的次数传进函数第一个参数。

如果函数返回 false 可提前退出。

uncurry

  1. const uncurry = (fn, n = 1) => (...args) => {
  2. const next = acc => args => args.reduce((x, y) => x(y), acc);
  3. if (n > args.length) throw new RangeError('Arguments too few!');
  4. return next(fn)(args.slice(0, n));
  5. };
  6. const add = x => y => z => x + y + z;
  7. const uncurriedAdd = uncurry(add, 3);
  8. uncurriedAdd(1, 2, 3); // 6

作用:函数反柯里化。

柯里化是将接受多个参数函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数。

而反柯里化就是将多个接受参数的层层函数,铺平。

解析:反柯里化的关键代码在于 args.reduce((x, y) => x(y), acc)

  1. 在上例中,
  2. args: [1,2,3]
  3. acc: x => y => z => x + y + z
  4. 第一次循环:
  5. xx => y => z => x + y + z
  6. y1
  7. 返回结果:y => z => 1 + y + z
  8. 第二次循环:
  9. x: y => z => 1 + y + z
  10. y: 2
  11. 返回结果:z => 1 + 2 + z
  12. 最后一次循环的结果,即 1 + 2 +3

可以看出,每次一循环,都会利用闭包”填充”一个所需变量。

返回的结果分为两种情况:

一是 一个保留了 n 个前置参数的函数。

二是层叠函数中最后一个函数的返回结果。

值得一提的是,在源码中使用了slice(0,n)保留适当数量的参数。

如果提供的参数的个数小于给定的解析长度,就会抛出错误。

unfold

  1. const unfold = (fn, seed) => {
  2. let result = [],
  3. val = [null, seed];
  4. while ((val = fn(val[1]))) result.push(val[0]);
  5. return result;
  6. };
  7. var f = n => (n > 50 ? false : [-n, n + 10]);
  8. unfold(f, 10); // [-10, -20, -30, -40, -50]

作用:使用种子值以及特殊的数据存储与迭代方式构建一个数组。

解析: 我为什么说数据存储与迭代方式很特殊呢?

迭代的变量与结果值,保存在同一数组里,用01下标区分。

而迭代的函数,也需要满足这一规范,返回同样的数组[value,nextSeed],保证下一次迭代,或者返回false终止过程。

when

  1. const when = (pred, whenTrue) => x => (pred(x) ? whenTrue(x) : x);
  2. const doubleEvenNumbers = when(x => x % 2 === 0, x => x * 2);
  3. doubleEvenNumbers(2); // 4
  4. doubleEvenNumbers(1); // 1

作用:根据pred函数测试给定数据。如结果为真,则执行whenTrue函数;反之,返回数据。

解析: 我喜欢语义化的封装,可大幅提升代码的可读性,减少逻辑负担。

专家级

memoize

  1. const memoize = fn => {
  2. const cache = new Map();
  3. const cached = function(val) {
  4. return cache.has(val) ? cache.get(val) : cache.set(val, fn.call(this, val)) && cache.get(val);
  5. };
  6. cached.cache = cache;
  7. return cached;
  8. };
  9. // See the `anagrams` snippet.
  10. const anagramsCached = memoize(anagrams);
  11. anagramsCached('javascript'); // takes a long time
  12. anagramsCached('javascript'); // returns virtually instantly since it's now cached
  13. console.log(anagramsCached.cache); // The cached anagrams map

作用:为给定的函数添加缓存功能。

解析: 通过实例化一个新的Map对象来创建一个空的缓存。

并对函数的调用进一步的封装,如果调用时,传入了一个之前已经传递过的参数,将从缓存中直接返回结果,执行时间为O(1);如果是首次传递,则需运行函数,将得到结果缓存,并返回。

其实,我们还可以借助这个片段,看到一丝 JavaScript 语法的残缺。

到目前为止,一个社区公认的私有属性语法都没有,TC39 一直提议用#号,并阐述了很多原因、声明。

哎,说白了,就是 JavaScript 从一开始设计的失误,到现在已经无法挽回了。

throttle

  1. const throttle = (fn, wait) => {
  2. let inThrottle, lastFn, lastTime;
  3. return function() {
  4. const context = this,
  5. args = arguments;
  6. if (!inThrottle) {
  7. fn.apply(context, args);
  8. lastTime = Date.now();
  9. inThrottle = true;
  10. } else {
  11. clearTimeout(lastFn);
  12. lastFn = setTimeout(function() {
  13. if (Date.now() - lastTime >= wait) {
  14. fn.apply(context, args);
  15. lastTime = Date.now();
  16. }
  17. }, Math.max(wait - (Date.now() - lastTime), 0));
  18. }
  19. };
  20. };
  21. window.addEventListener(
  22. 'resize',
  23. throttle(function(evt) {
  24. console.log(window.innerWidth);
  25. console.log(window.innerHeight);
  26. }, 250)
  27. ); // Will log the window dimensions at most every 250ms

作用: 函数节流。

什么是防抖和节流?有什么区别?如何实现? 一文中关于防抖解释:

高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率。

同样,节流也是面试必考的点。

解析:第一次执行时,立即执行给定函数,保存当前的时间,并设置标记变量。

标记变量主要用于判断是否第一次调用,如果是第一次则立刻运行。

反之不是第一次运行,过了等待的毫秒后才可继续运行。

主要逻辑是每次运行前先清除上一个的定时器,然后计算出上一次运行的时间与给定的运行间隔所差的毫秒数,并利用其数据新建一个定时器运行。

定时器里的函数除了调用给定函数,还会更新上一次运行的时间变量。

节流的实现,网上的文章有很多版本,但多少都有点瑕疵。


结束语

呼,花了很长的时间,终于搞定了这篇文章。

以后的 30s 源码刨析系列会挑选一些源码片段去解析,而不是针对某一分类了。

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

转载文章请注明作者和出处 一个坏掉的番茄,请勿用于任何商业用途。

30s源码刨析系列之函数篇的更多相关文章

  1. MapReduce源码刨析

    MapReduce编程刨析: Map map函数是对一些独立元素组成的概念列表(如单词计数中每行数据形成的列表)的每一个元素进行指定的操作(如把每行数据拆分成不同单词,并把每个单词计数为1),用户可以 ...

  2. Flask上下文管理及源码刨析

    基本流程概述 - 与django相比是两种不同的实现方式. - django/tornado是通过传参数形式实现 - 而flask是通过上下文管理, 两种都可以实现,只不实现的方式不一样罢了. - 上 ...

  3. HashMap源码刨析(面试必看)

    目录 1.Hash的计算规则? 2.HashMap是怎么形成环形链表的(即为什么不是线程安全)?(1.7中的问题) 3.JDK1.7和1.8的HashMap不同点? 4.HashMap和HashTab ...

  4. ConcurrentHashMap源码刨析(基于jdk1.7)

    看源码前我们必须先知道一下ConcurrentHashMap的基本结构.ConcurrentHashMap是采用分段锁来进行并发控制的. 其中有一个内部类为Segment类用来表示锁.而Segment ...

  5. Java 源码刨析 - String

    [String 是如何实现的?它有哪些重要的方法?] String 内部实际存储结构为 char 数组,源码如下: public final class String implements java. ...

  6. Java 源码刨析 - HashMap 底层实现原理是什么?JDK8 做了哪些优化?

    [基本结构] 在 JDK 1.7 中 HashMap 是以数组加链表的形式组成的: JDK 1.8 之后新增了红黑树的组成结构,当链表大于 8 并且容量大于 64 时,链表结构会转换成红黑树结构,它的 ...

  7. Java 源码刨析 - 线程的状态有哪些?它是如何工作的?

    线程(Thread)是并发编程的基础,也是程序执行的最小单元,它依托进程而存在. 一个进程中可以包含多个线程,多线程可以共享一块内存空间和一组系统资源,因此线程之间的切换更加节省资源.更加轻量化,也因 ...

  8. SSM-SpringMVC-04:SpringMVC深入浅出理解HandleMapping(源码刨析)

    ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 先从概念理解,从中央调度器,携带参数request,调度到HandleMapping处理器映射器,处理器映射器 ...

  9. form 源码刨析

    def clean_name(self) value = self.cleaned_data.get('name') if "金-瓶-梅" not in value: raise ...

随机推荐

  1. sqlite如何避免重复建表(获取已经存在的表)

    找到已经存在的所有表,手动判断是否需要建表 SELECT name FROM SQLITE_MASTER WHERE type='table'ORDER BY name" 建表时sqlite ...

  2. AI大火之下智能手机行业能适应这一风口吗?

    今年智能手机行业的变化,实在是让人摸不到头脑.一方面是智能手机厂商依然在拿出各种具有噱头的产品,仿佛整个市场还依然热火朝天.但在另一方面,智能手机出货量却出现大幅下滑.据中国信息通信研究院发布的数据显 ...

  3. MFC的cstring判断是否存在中文字符

    bool IsChinese(CString Cstr) { int nLen = Cstr.GetLength(); unsigned char ch1, ch2; for (int i = 0; ...

  4. 主效应|处理误差 |组间误差|处理效应|随机误差|组内误差|误差|效应分析|方差齐性检验|SSE|SSA|SST|MSE|MSA|F检验|关系系数|完全随机化设计|区组设计|析因分析

    8 什么是只考虑主效应的方差分析? 就是不考虑交互效应的方差分析,即认为因素之间是不相互影响的,就是无重复的方差分析.   什么是处理误差 (treatment error).组间误差(between ...

  5. Web Service概述 及 应用案例

    Web Service的定义  W3C组织对其的定义如下,它是一个软件系统,为了支持跨网络的机器间相互操作交互而设计.Web Service服务通常被定义为一组模块化的API,它们可以通过网络进行调用 ...

  6. 吴裕雄--天生自然python学习笔记:python文档操作自动生成菜单 Word 文件

    许多学校营养午餐的菜单是由教师来轮流制作 ,这是一个 比较烦锁的工作,如 果能自动用教师最熟悉的 Word 文件来生成一个菜单文件,使教师对生成的菜单稍作 修改即可使用,那将是一个不错的主意. 案例要 ...

  7. Linux把内存挂载成硬盘提高读写速度

    tmpfs是一种虚拟内存文件系统正如这个定义它最大的特点就是它的存储空间在VM里面,这里提一下VM(virtual memory),VM是由linux内核里面的vm子系统管理,现在大多数操作系统都采用 ...

  8. Anaconda 安装、TensorFlow 安装、pytorch 安装

    问题 1 假设已经安装完Anaconda Navigator,按照 网址:https://www.zhihu.com/question/55577586 的教程操作,设置镜像源地址,安装. 问题 2: ...

  9. 浅尝HTML5之canvas

    转自:http://segmentfault.com/a/1190000000661407/ HTML5新标签 HTML5新引入header,footer,article,section,aside和 ...

  10. 为什么前两年大热的VR创业突然冷了?

    不得不说,如果不是<头号玩家>在国内的热映,人们似乎都要忘记VR这个行业了.<头号玩家>中那些带有极强真实色彩的游戏,其实就是VR进化的目标,甚至是巅峰!而里面的角色佩戴的设备 ...