JavaScript 14 天编程挑战

67 阅读6分钟

在 leetcode 看到 JavaScript 14 天编程挑战 的题目。

这里分享下自己的做题思路,期望同学面对同类型问题时,有所启发。

下面是以解决问题依赖的知识做的分类。

闭包

闭包中的变量是封闭的,对外暴露的接口相对可控,有利于储存一些私有的内容。

涉及的题目

计数器

计数器 函数最初返回 n,每次调用它时返回前一个值加 1 的值 ( n ,  n + 1 ,  n + 2 ,等等)。

思路

  1. 利用闭包去储存 计数
  2. return 的函数修改闭包中的 计数 ,并返回出去

代码

var createCounter = function (n) {
  /* 闭包区域 */
  let preN = n;
  return function () {
    return preN++; // return 是优先返回 preN 再做 ++
  };
};

记忆函数

记忆函数 是一个对于相同的输入永远不会被调用两次的函数。相反,它将返回一个缓存值。

思路

  1. 使用闭包去储存之前的调用结果
  2. 使用args.join(',') 计算出需要缓存的key

代码

function memoize(fn) {
  /* 闭包区域 */
  const cacheMap = new Map();
  const getCacheKey = (...args) => args.join(",");
  return function (...args) {
    const key = getCacheKey(...args);
    return cacheMap.has(key)
      ? cacheMap.get(key)
      : cacheMap.set(key, fn(...args)).get(key);
  };
}

基本数组转换

对于数组数据的转换,已经提供了很多 API。例如mapfilter等。使用 循环即可模拟这些方法。

其它

分享一些关于 callback 使用技巧,避免手动传递参数。

filter 使用为例。

// 有些同学在这里将 callback 的参数做了二次传递,可以简化成另一种方式
[].filter((v) => Boolean(v))

// 简化版
[].filter(Boolean)

Reduce

根据 reduce 自身的特点。只要是 下一步依赖上一步 的场景,就都可以尝试使用reduce

例如累加、斐波那契数列等。中间件模式,也可以使用reduce实现。

涉及的题目

复合函数

请你编写一个函数,它接收一个函数数组 [f1, f2, f3,…, fn] ,并返回一个新的函数 fn ,它是函数数组的 复合函数 。

[f(x), g(x), h(x)] 的 复合函数 为 fn(x) = f(g(h(x))) 。

思路

  1. 上一个函数的返回结果传递给下一个函数,可以直接使用reduceRight

代码

var compose = function (functions) {
  return function (x) {
    return functions.reduceRight((acc, fn) => fn(acc), x);
  };
};

分块数组

给定一个数组 arr 和一个块大小 size ,返回一个 分块 的数组。分块的数组包含了 arr 中的原始元素,但是每个子数组的长度都是 size 。

思路

  1. 判断分块数组最后一个元素是否小于给定的 size,
    1. 如果小于则 push item 到分块数组最后的数组中
    2. 如果大于等于则 push [item] 到分块数组中

代码

var chunk = function (arr, size) {
  if (!arr.length) return [];

  return arr.reduce(
    (acc, item) => {
      acc[acc.length - 1].length < size
        ? acc[acc.length - 1].push(item)
        : acc.push([item]);
      return acc;
    },
    [[]]
  );
};

Promise

Promise 提供了6 种静态方法去满足不同场景。

涉及的题目

Promise 对象池

请你编写一个异步函数 promisePool ,它接收一个异步函数数组 functions 和 池限制 n。它应该返回一个 promise 对象,当所有输入函数都执行完毕后,promise 对象就执行完毕。

思路

下面是具体思路:

  1. 声明两个数组,分别用于储存正在执行的函数 execPools 和 等待执行的函数 waitPools
  2. 取出 waitPools 的函数,并用 warperFunction 包裹一下,添加到 execPools
  3. 在函数执行完时,将执行完的函数,从 execPools 剔除,执行步骤 2
  4. execPoolsexecPools 都为空时,则表示所有任务都执行结束

warperFunction 使用了 装饰器模式。 它适用于对原有方法的增强,在原函数增加了调整储存 执行中 和 待执行 函数的数组的逻辑。

代码

var promisePool = async function (functions, n) {
  return new Promise((res) => {
    let waitPools = [...functions];
    let execPools = [];

    const isFinish = () => execPools.length === 0 && waitPools.length === 0;

    const removeFinishFunction = (func) => {
      execPools = execPools.filter((v) => v !== func);
    };

    const warperFunction = (fn) => {
      const newFunction = () => {
        fn().then(() => {
          removeFinishFunction(newFunction);
          isFinish() ? res() : execNext();
        });
      };
      return newFunction;
    };

    const execNext = () => {
      execPools.push(warperFunction(waitPools.shift()));
    };

    const init = () => {
      let i = 0;
      while (i < n && waitPools.length) {
        execNext();
        i++;
      }
    };

    init();
  });
};

其它

看到其他同学的题解,非常精简,这里也分享下。

var promisePool = async function (functions, n) {
  return Promise.all(
    Array.from({
      length: n,
    }).map(async () => {
      while (functions.length > 0) {
        await functions.shift()();
      }
    })
  );
};

哈希表

哈希表一般用于储存关联关系,方便查找。

涉及的题目

递归 + 回溯

递归一般用于重复处理相同的场景。

回溯就是在递归前记录一下操作,递归后再还原一下操作。 扁平化嵌套数组 就用到了回溯。

涉及的题目

将对象转换为 JSON 字符串

现给定一个对象,返回该对象的有效 JSON 字符串。你可以假设这个对象只包括字符串、整数、数组、对象、布尔值和 null。返回的字符串不能包含额外的空格。键的返回顺序应该与 Object.keys() 的顺序相同。

思路

  1. 使用原生的JSON.stringify转化一下对象,分析转换后字符串的结构
    1. 怎么拼接不同的变量
    2. 怎么拼接逗号
  2. 分别处理不同数据类型
    1. 直接拼接基础数据类型
    2. 循环 ArrayObject 中的元素,重复(递归)第 2 步操作

代码

var jsonStringify = function (object) {
  const stringify = (string, data) => {
    const type = Object.prototype.toString
      .call(data)
      .match(/\[object (.+)\]/)[1];

    switch (type) {
      case "Array":
        return `[${data.reduce((acc, cur) => {
          return `${acc}${acc === "" ? "" : ","}${stringify("", cur)}`;
        }, string)}]`;
      case "Object":
        return `{${Object.keys(data).reduce((acc, key) => {
          return `${acc}${acc === "" ? "" : ","}"${key}":${stringify(
            "",
            data[key]
          )}`;
        }, string)}}`;
      case "String":
        return `"${data}"`;
      default:
        return String(data);
    }
  };

  return stringify("", object);
};

扁平化嵌套数组

请你编写一个函数,它接收一个 多维数组 arr 和它的深度 n ,并返回该数组的 扁平化 后的结果。

思路

  1. 循环处理数组中的元素
  2. 记录当前递归的深度
    1. 碰到非数组元素,则直接添加到展开的数组中
    2. 碰到数组,如果深度小于等于 n, 则重复(递归)第 1 步操作,否则直接添加到展开的数组中

有一个核心问题,就是递归处理时,层级到了第几层,就用到了回溯。下面是回溯的模板代码

递归前 ==> 记录信息
recursion() // 递归处理
递归后 ==> 回撤信息

代码

var flat = function (arr, n) {
  if (n === 0) return arr;

  const result = [];
  let dep = 1; // 记录深度

  const flatArray = (source) => {
    let i = 0;
    const len = source.length;

    while (i < len) {
      if (Array.isArray(source[i]) && dep <= n) {
        dep++; // 记录信息
        flatArray(source[i]);
        dep--; // 回撤信息
      } else {
        result.push(source[i]);
      }
      i++;
    }
  };

  flatArray(arr);

  return result;
};

原型链

原型链主要应用的场景是继承。JavaScript 对象有一个指向一个原型对象的链。更多 MDN

涉及的题目

检查是否是类的对象实例

请你编写一个函数,检查给定的对象是否是给定类或超类的实例。

思路

  1. 判断当前对象的 __proto__ 属性, 和类的 prototype 是否是同一个对象
  2. 基于原型链的特性,如果当前对比的结果不是同一个对象,我们可以基于原型链的查找规则(类似 xxx.__proto__.__proto__.__proto__),继续向上查找对比

代码

var checkIfInstanceOf = function (obj, classFunction) {
  while (obj != null) {
    if (obj.__proto__ === classFunction?.prototype) return true;

    obj = obj.__proto__;
  }

  return false;
};

生成器

初次调用生成器函数不执行任何代码,而是返回一种称为 Generator 的迭代器。

通过调用生成器的下一个方法消耗值时,Generator 函数将执行,直到遇到 yield 关键字。

斐波那契数列介绍。形如[0, 1, 1, 2, 3 ,5, 8]

涉及的题目

嵌套数组生成器

现给定一个整数的 多维数组 ,请你返回一个生成器对象,按照 中序遍历 的顺序逐个生成整数。中序遍历 是从左到右遍历每个数组。

思路

  1. 循环处理数组中的元素
    1. 碰到非数组元素,则在元素前添加 yield(用于暂停和恢复生成器函数) 关键字
    2. 碰到数组,则在递归处理当前数组前添加 yield* (用于委托给另一个 generator 或可迭代对象)关键字

代码

var inorderTraversal = function* (arr) {
  let i = 0;
  const len = arr.length;

  while (i < len) {
    Array.isArray(arr[i]) ? yield* inorderTraversal(arr[i]) : yield arr[i];
    i++;
  }
};