Leetcode 30 天 JavaScript 挑战 - 05. JSON

123 阅读18分钟

2727. 判断对象是否为空

给定一个对象或数组,判断它是否为空。

  • 一个空对象不包含任何键值对。
  • 一个空数组不包含任何元素。

你可以假设对象或数组是通过 JSON.parse 解析得到的。

示例 1:

输入:obj = {"x": 5, "y": 42}
输出:false
解释:这个对象有两个键值对,所以它不为空。

示例 2:

输入:obj = {}
输出:true
解释:这个对象没有任何键值对,所以它为空。

示例 3:

输入:obj = [null, false, 0]
输出:false
解释:这个数组有 3 个元素,所以它不为空。

提示:

  • obj 是一个有效的 JSON 对象或数组
  • 2 <= JSON.stringify(obj).length <= 10^5

你可以在 O(1) 时间复杂度内解决这个问题吗?

思路一

使用 Object.keys方法检查对象或者数组的键的长度,看这长度是否为 0,就可以判断对象或者数组是否为空了。

Object.keys的时间复杂度和空间复杂度都为 O(n),因为需要遍历对象/数组里每一个键值对,且返回的数组长度和对象的大小正相关。

代码一

type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };
type Obj = Record<string, JSONValue> | JSONValue[]

function isEmpty(obj: Obj): boolean {
  return Object.keys(obj).length === 0;
};

思路二(O(1) 解法:判断是否有可迭代内容)

题目倡导我们用O(1)的时间复杂度来解决问题,下面是一种方法:

可以使用 for 循环迭代器来检查是否有内容可迭代,如果有,表示对象不为空,如果没有可迭代的内容,表示对象为空。这样除了时间复杂度为O(1)之外,空间复杂度也为O(1)

代码二

type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };
type Obj = Record<string, JSONValue> | JSONValue[]

function isEmpty(obj: Obj): boolean {
  for (let _ in obj) {
    // 如果走进了迭代器,说明有内容,直接判定为不为空并返回
    return false;
  }
  // 没有走进迭代器,直接走到了下边,说明无内容
  return true;
};

2677. 分块数组

给定一个数组 arr 和一个块大小 size ,返回一个 分块 的数组。分块 的数组包含了 arr 中的原始元素,但是每个子数组的长度都是 size 。如果 arr.length 不能被 size 整除,那么最后一个子数组的长度可能小于 size 。

你可以假设该数组是 JSON.parse 的输出结果。换句话说,它是有效的JSON。

请你在不使用 lodash 的函数 _.chunk 的情况下解决这个问题。

示例 1:

输入:arr = [1,2,3,4,5], size = 1
输出:[[1],[2],[3],[4],[5]]
解释:数组 arr 被分割成了每个只有一个元素的子数组。

示例 2:

输入:arr = [1,9,6,3,2], size = 3
输出:[[1,9,6],[3,2]]
解释:数组 arr 被分割成了每个有三个元素的子数组。然而,第二个子数组只有两个元素。

示例 3:

输入:arr = [8,5,3,2,6], size = 6
输出:[[8,5,3,2,6]]
解释:size 大于 arr.length ,因此所有元素都在第一个子数组中。

示例 4:

输入:arr = [], size = 1
输出:[]
解释:没有元素需要分块,因此返回一个空数组。

提示:

  • arr 是一个有效的 JSON 数组
  • 2 <= JSON.stringify(arr).length <= 10``5
  • 1 <= size <= arr.length + 1

思路一(常规解法)

简单题,怎么搞都能很简单返回结果,重点还是在性能上。

少次循环 + 少做修改数组的操作,可以让性能达到最好。

代码一

type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };
type Obj = Record<string, JSONValue> | Array<JSONValue>;

function chunk(arr: Obj[], size: number): Obj[][] {
  const result = [];
  // 需要注意,每次循环,下标移动 size 长度
  for (let i = 0; i < arr.length; i += size) {
    // 一次性把 chunk 推到 result 中
    result.push(arr.slice(i, i + size));
  }
  return result;
};

因为还需要 result 存储结果,所以空间复杂度为O(n)

思路二(高阶解法)

如果能一口气返回结果,那肯定是最好的。Array.from函数除了可以把想要转换成数组的类数组或可迭代对象转化成数组,还可以很方便的产出批量处理的数组。

developer.mozilla.org/zh-CN/docs/…

示例:

// 从字符串构建数组
console.log(Array.from('foo'));
// Expected output: Array ["f", "o", "o"]

// 从 Set 构建数组
const set = new Set(["foo", "bar", "baz", "foo"]);
Array.from(set);
// [ "foo", "bar", "baz" ]

// 从 Map 构建数组
const map = new Map([
  [1, 2],
  [2, 4],
  [4, 8],
]);
Array.from(map);
// [[1, 2], [2, 4], [4, 8]]

// 根据 DOM 元素的属性创建一个数组
const images = document.querySelectorAll("img");
const sources = Array.from(images, (image) => image.src);
const insecureSources = sources.filter((link) => link.startsWith("http://"));

// ======================================================

// 使用箭头函数作为映射函数去操作多个元素
Array.from([1, 2, 3], (x) => x + x);
// [2, 4, 6]

// 生成一个数字序列。因为数组在每个位置都使用 `undefined` 初始化,下面的 `v` 值将是 `undefined`
Array.from({ length: 5 }, (v, i) => i);
// [0, 1, 2, 3, 4]

代码二

type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };
type Obj = Record<string, JSONValue> | Array<JSONValue>;

function chunk(arr: Obj[], size: number): Obj[][] {
  // 定义一个数组,长度计算如下,需要进位保证所有元素有地方放
  return Array.from({ length: Math.ceil(arr.length / size) }, (_, index) =>
    // 数组里每个元素的内容
    arr.slice(size * index, size * index + size)
  );
}

2619. 数组原型对象的最后一个元素

请你编写一段代码实现一个数组方法,使任何数组都可以调用 array.last() 方法,这个方法将返回数组最后一个元素。如果数组中没有元素,则返回 -1 。

你可以假设数组是 JSON.parse 的输出结果。

示例 1 :

输入:nums = [null, {}, 3]
输出:3
解释:调用 nums.last() 后返回最后一个元素: 3。

示例 2 :

输入:nums = []
输出:-1
解释:因为此数组没有元素,所以应该返回 -1。

提示:

  • arr 是一个有效的 JSON 数组
  • 0 <= arr.length <= 1000

思路

这题没什么难点,主要是注意,数组原型上的 this 指的是数组本身。

代码

declare global {
  interface Array<T> {
    last(): T | -1;
  }
}

Array.prototype.last = function () {
  // 判断数组长度,有元素就返回最后一个元素,否则返回 -1
  return this.length > 0 ? this[this.length - 1] : -1;
};

/**
 * const arr = [1, 2, 3];
 * arr.last(); // 3
 */

export { };

2631. 分组

请你编写一段可应用于所有数组的代码,使任何数组调用 array. groupBy(fn) 方法时,它返回对该数组 分组后 的结果。

数组 分组 是一个对象,其中的每个键都是 fn(arr[i]) 的输出的一个数组,该数组中含有原数组中具有该键的所有项。

提供的回调函数 fn 将接受数组中的项并返回一个字符串类型的键。

每个值列表的顺序应该与元素在数组中出现的顺序相同。任何顺序的键都是可以接受的。

请在不使用 lodash 的 _.groupBy 函数的前提下解决这个问题。

示例 1:

输入:
array = [
  {"id":"1"},
  {"id":"1"},
  {"id":"2"}
], 
fn = function (item) { 
  return item.id; 
}
输出:
{ 
  "1": [{"id": "1"}, {"id": "1"}],   
  "2": [{"id": "2"}] 
}
解释:
输出来自函数 array.groupBy(fn)。
分组选择方法是从数组中的每个项中获取 "id" 。
有两个 "id"1 的对象。所以将这两个对象都放在第一个数组中。
有一个 "id"2 的对象。所以该对象被放到第二个数组中。

示例 2:

输入:
array = [
  [1, 2, 3],
  [1, 3, 5],
  [1, 5, 9]
]
fn = function (list) { 
  return String(list[0]); 
}
输出:
{ 
  "1": [[1, 2, 3], [1, 3, 5], [1, 5, 9]] 
}
解释:
数组可以是任何类型的。在本例中,分组选择方法是将键定义为数组中的第一个元素。
所有数组的第一个元素都是1,所以它们被组合在一起。
{
  "1": [[1, 2, 3], [1, 3, 5], [1, 5, 9]]
}

示例 3:

输出:
array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
fn = function (n) { 
  return String(n > 5);
}
输入:
{
  "true": [6, 7, 8, 9, 10],
  "false": [1, 2, 3, 4, 5]
}
解释:
分组选择方法是根据每个数字是否大于 5 来分割数组。

提示:

  • 0 <= array.length <= 105
  • fn 返回一个字符串

思路一

这题可以发现,返回的数据为,key 为 fn 的执行的可能结果。所以使用遍历的方法去执行,依次填入即可。

代码一

declare global {
  interface Array<T> {
    groupBy(fn: (item: T) => string): Record<string, T[]>
  }
}

Array.prototype.groupBy = function <T>(fn: (item: T) => string) {
  const result: Record<string, T[]> = {};
  for (const item of this) {
    // 执行 fn 获得 key
    const key = fn(item);
    if (result[key]) {
      // 如果 result 中已经存在 key,直接塞进去
      result[key].push(item);
    } else {
      // 否则初始化键值对
      result[key] = [item];
    }
  }
  return result;
}

/**
 * [1,2,3].groupBy(String) // {"1":[1],"2":[2],"3":[3]}
 */

思路二(reduce 写法)

所有遍历返回的写法,都可以用 reduce,就是有可读性问题。

代码二

declare global {
  interface Array<T> {
    groupBy(fn: (item: T) => string): Record<string, T[]>
  }
}

Array.prototype.groupBy = function <T>(fn: (item: T) => string) {
  return this.reduce((result: Record<string, T[]>, item: T) => {
    const key = fn(item);
    if (result[key]) {
      result[key].push(item);
    } else {
      result[key] = [item];
    }
    // 记得每次 reduce 遍历都要 return 结果
    return result;
  }, {});
}

/**
 * [1,2,3].groupBy(String) // {"1":[1],"2":[2],"3":[3]}
 */

2724. 排序方式

给定一个数组 arr 和一个函数 fn,返回一个排序后的数组 sortedArr。你可以假设 fn 只返回数字,并且这些数字决定了 sortedArr 的排序顺序。sortedArr 必须按照 fn 的输出值 升序 排序。

你可以假设对于给定的数组,fn 不会返回重复的数字。

示例 1:

输入:arr = [5, 4, 1, 2, 3], fn = (x) => x
输出:[1, 2, 3, 4, 5]
解释:fn 只是返回传入的数字,因此数组按升序排序。

示例 2:

输入:arr = [{"x": 1}, {"x": 0}, {"x": -1}], fn = (d) => d.x
输出:[{"x": -1}, {"x": 0}, {"x": 1}]
解释:fn 返回 "x" 键的值,因此数组根据该值排序。

示例 3:

输入:arr = [[3, 4], [5, 2], [10, 1]], fn = (x) => x[1]
输出:[[10, 1], [5, 2], [3, 4]]
解释:数组按照索引为 1 处的数字升序排序。

提示:

  • arr 是一个有效的 JSON 数组
  • fn 是一个函数,返回一个数字
  • 1 <= arr.length <= 5 * 105

思路一(API 偷懒写法)

Array.prototype.sort()就地对数组的元数进行排序,并返回对相同数组的应用。默认排序是将元素转换为字符串,然后按照它们的 UTF-16 码元值升序排序。

如果想要 immutable 返回一个新的已排序数组,使用 toSorted

例子:

const months = ['March', 'Jan', 'Feb', 'Dec'];
months.sort();
console.log(months);
// Expected output: Array ["Dec", "Feb", "Jan", "March"]

const array1 = [1, 30, 4, 21, 100000];
array1.sort();
console.log(array1);
// Expected output: Array [1, 100000, 21, 30, 4]

可以传入一个定义排序顺序的函数,规则如下

function compareFn(a, b) {
  if (根据排序标准,a 小于 b) {
    return -1;
  }
  if (根据排序标准,a 大于 b) {
    return 1;
  }
  // a 一定等于 b
  return 0;
}

例子:

const numericStringArray = ["80", "9", "700"];
numericStringArray.sort(compareNumbers); // ['9', '80', '700']

function compareNumbers(a, b) {
  return a - b;
}

numericStringArray.sort(compareNumbers); // ['9', '80', '700']

代码

type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };
type Fn = (value: JSONValue) => number

function sortBy(arr: JSONValue[], fn: Fn): JSONValue[] {
  return arr.sort((a, b) => fn(a) - fn(b));
};

思路二(手撕一个快排)

没写过快排的最好看下其他相关的博客。快排的思路是:

  1. 先在数组中取任意值(这里就选第一个元素),声明为 midValue,并定义 leftArray 和 rightArray;
  2. 遍历 midValue 以外剩余的元素,如果小于 midValue 就放在 leftArray,其他情况放在 rightArray;
  3. 将数组按照 leftArray, midValue, rightArray 的顺序摆放,这就粗略地进行了一次排序;
  4. 其中 leftArray 和 rightArray 还需要继续以上面的方式去排序(递归),直到数组的元素数量小于等于 1 位置,设为递归的终止条件(因为分无可分了,肯定是排序完了)。

代码二

type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };
type Fn = (value: JSONValue) => number

const quickSort = (arr: JSONValue[], fn: Fn): JSONValue[] => {
  // 递归终止点
  if (arr.length <= 1) return arr;
  // 变量声明阶段
  const midVal = fn(arr[0]);
  const leftArr = [];
  const rightArr = [];
  // 遍历其余元素,放入对应的数组中
  for (const item of arr.slice(1)) {
    if (fn(item) < midVal) {
      leftArr.push(item);
    } else {
      rightArr.push(item);
    }
  }
  // 按大小排放,leftArr 和 rightArr 继续递归
  return [...quickSort(leftArr, fn), arr[0], ...quickSort(rightArr, fn)];
}

function sortBy(arr: JSONValue[], fn: Fn): JSONValue[] {
  return quickSort(arr, fn);
};

性能肯定不如原生,但写出来还是爽的。

722. 根据 ID 合并两个数组

现给定两个数组 arr1 和 arr2 ,返回一个新的数组 joinedArray 。两个输入数组中的每个对象都包含一个 id 字段。joinedArray 是一个通过 id 将 arr1 和 arr2 连接而成的数组。joinedArray 的长度应为唯一值 id 的长度。返回的数组应按 id 升序 排序。

如果一个 id 存在于一个数组中但不存在于另一个数组中,则该对象应包含在结果数组中且不进行修改。

如果两个对象共享一个 id ,则它们的属性应进行合并:

  • 如果一个键只存在于一个对象中,则该键值对应该包含在对象中。
  • 如果一个键在两个对象中都包含,则 arr2 中的值应覆盖 arr1 中的值。

示例 1:

输入:
arr1 = [    {"id": 1, "x": 1},    {"id": 2, "x": 9}], 
arr2 = [    {"id": 3, "x": 5}]
输出:
[    {"id": 1, "x": 1},    {"id": 2, "x": 9},    {"id": 3, "x": 5}]
解释:没有共同的 id,因此将 arr1 与 arr2 简单地连接起来。

示例 2:

输入:
arr1 = [
    {"id": 1, "x": 2, "y": 3},
    {"id": 2, "x": 3, "y": 6}
], 
arr2 = [
    {"id": 2, "x": 10, "y": 20},
    {"id": 3, "x": 0, "y": 0}
]
输出:
[
    {"id": 1, "x": 2, "y": 3},
    {"id": 2, "x": 10, "y": 20},
    {"id": 3, "x": 0, "y": 0}
]
解释:id 为 1 和 id 为 3 的对象在结果数组中保持不变。id 为 2 的两个对象合并在一起。arr2 中的键覆盖 arr1 中的值。

示例 3:

输入:
arr1 = [
    {"id": 1, "b": {"b": 94},"v": [4, 3], "y": 48}
]
arr2 = [
    {"id": 1, "b": {"c": 84}, "v": [1, 3]}
]
输出: [
    {"id": 1, "b": {"c": 84}, "v": [1, 3], "y": 48}
]
解释:具有 id 为 1 的对象合并在一起。对于键 "b""v" ,使用 arr2 中的值。由于键 "y" 只存在于 arr1 中,因此取 arr1 的值。

提示:

  • arr1 和 arr2 都是有效的 JSON 数组
  • 在 arr1 和 arr2 中都有唯一的键值 id
  • 2 <= JSON.stringify(arr1).length <= 106
  • 2 <= JSON.stringify(arr2).length <= 106

思路

其实就是需要判断一层,在重复 id 的情况下,需要合并两个对象的情况。所以需要把第一个对象的键值对给记忆下来,然后在第二个对象进行添加的时候,判断是否有重复 id。

记录这种事情,使用 Map 最合适了。下面的代码是 Map 数据结构的解法。

代码

type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };
type ArrayType = { "id": number } & Record<string, JSONValue>;

function join(arr1: ArrayType[], arr2: ArrayType[]): ArrayType[] {
  // 开辟空间用户存储已记录的对象
  const map = new Map<number, { "id": number } & Record<string, JSONValue>>();
  // 将 arr1 中的数据录入到 map 里
  for (const obj of arr1) {
    map.set(obj.id, obj);
  }
  // 遍历 arr2
  for (const obj of arr2) {
    if (map.has(obj.id)) {
      // 如果 map 中已经存在重复 id,合并两对象
      map.set(obj.id, { ...map.get(obj.id), ...obj });
    } else {
      // 不存在重复 id,直接存入 map
      map.set(obj.id, obj);
    }
  }
  // map.values 是 MapIterator,需要转化为数组
  // 同时进行升序排序
  return [...map.values()].sort((a, b) => a.id - b.id);
};

2625. 扁平化嵌套数组

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

多维数组 是一种包含整数或其他 多维数组 的递归数据结构。

数组 扁平化 是对数组的一种操作,定义是将原数组部分或全部子数组删除,并替换为该子数组中的实际元素。只有当嵌套的数组深度大于  时,才应该执行扁平化操作。第一层数组中元素的深度被认为是 0。

请在没有使用内置方法的前提下解决这个问题。

示例 1:

输入
arr = [1, 2, 3, [4, 5, 6], [7, 8, [9, 10, 11], 12], [13, 14, 15]]
n = 0
输出
[1, 2, 3, [4, 5, 6], [7, 8, [9, 10, 11], 12], [13, 14, 15]]

解释
传递深度 n=0 的多维数组将始终得到原始数组。这是因为 子数组(0) 的最小可能的深度不小于 n=0 。因此,任何子数组都不应该被平面化。

示例 2:

输入
arr = [1, 2, 3, [4, 5, 6], [7, 8, [9, 10, 11], 12], [13, 14, 15]]
n = 1
输出
[1, 2, 3, 4, 5, 6, 7, 8, [9, 10, 11], 12, 13, 14, 15]

解释
以 4 、7 和 13 开头的子数组都被扁平化了,这是因为它们的深度为 0 , 而 0 小于 1 。然而 [9,10,11] 其深度为 1 ,所以未被扁平化。

示例 3:

输入
arr = [[1, 2, 3], [4, 5, 6], [7, 8, [9, 10, 11], 12], [13, 14, 15]]
n = 2
输出
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

解释
所有子数组的最大深度都为 1 。因此,它们都被扁平化了。

思路一(常规写法)

最容易想到的写法是,根据输入的层数去做循环,每层循环里初始化一个数组,将铺平后的元素放进数组里,处理完后返回给下一次循环使用。

时间复杂度为 O(n * m),即深度n和数组元素数量m的乘积。

空间复杂度为 O(m),因为使用了数组去存储数组元素。

代码一

type MultiDimensionalArray = (number | MultiDimensionalArray)[];

var flat = function (arr: MultiDimensionalArray, n: number): MultiDimensionalArray {
  // 判断数组中是否还有嵌套数组的标记
  let hasArrayElement = true;
  // 循环条件:在扁平化深度的循环轮数内,且数组中还嵌套数组
  for (let i = 0; i < n && hasArrayElement; i++) {
    // 改标记为 false
    hasArrayElement = false;
    // 存储扁平化一层后的数组
    const flatedArr = [];
    for (const item of arr) {
      // 遍历元素,判断元素是否为数组
      if (Array.isArray(item)) {
        // 元素是数组,标记为数组中有嵌套数组,同时扁平化该元素后推入 flatedArr
        hasArrayElement = true;
        flatedArr.push(...item);
      } else {
        // 普通元素,直接推进 flatedArr
        flatedArr.push(item);
      }
    }
    // 赋值给 arr,提供给下次循环使用或作为最终结果
    arr = [...flatedArr];
  }
  return arr;
};

思路二(递归写法)

思路二利用了 Array.prototype.concat() 可以简便地扁平化数组的特性。concat 接收若干个数组

示例:

const letters = ["a", "b", "c"];
const alphaNumeric = letters.concat(1, [2, 3]);
console.log(alphaNumeric);
// results in ['a', 'b', 'c', 1, 2, 3]

[1, 2].concat([3, 4, [5, 6]], [7], 8)
// [1, 2, 3, 4, [5, 6], 7, 8]

可以看到,concat 中的数组在拼接后,都扁平化了一层。所以依据这个思路,对数组内仍然是数组的元素再进行扁平化,直到达到目标的深度。

时间复杂度:O(n * m),其中 k 表示每个层级平均嵌套数组的数量,n 是最大深度级别。

空间复杂度:O(n),空间复杂度由深度级别 n 决定,因为 n 决定了递归的深度。需要为调用堆栈上的 n 次递归调用分配空间。

代码二

type MultiDimensionalArray = (number | MultiDimensionalArray)[];

var flat = function (arr: MultiDimensionalArray, n: number): MultiDimensionalArray {
  // 递归终止条件一:已经不需要扁平化了(深度为 0)
  if (n === 0) return arr;
  return [].concat(
    // 递归终止条件二:当元素不是数组元素时
    // 数组元素继续执行函数递归(注意深度 -1)
    ...arr.map((item) => (Array.isArray(item) ? flat(item, n - 1) : item))
  );
};

2705. 精简对象

现给定一个对象或数组 ,返回一个 精简对象 。精简对象 与原始对象相同,只是将包含 假 值的键移除。该操作适用于对象及其嵌套对象。数组被视为索引作为键的对象。当 Boolean(value) 返回 false 时,值被视为 假 值。

你可以假设 obj是 JSON.parse的输出结果。换句话说,它是有效的 JSON。

示例 1:

输入:obj = [null, 0, false, 1]
输出:[1]
解释:数组中的所有假值已被移除。

示例 2:

输入:obj = {"a": null, "b": [false, 1]}
输出:{"b": [1]}
解释:obj["a"] 和 obj["b"][0] 包含假值,因此被移除。

示例 3:

输入:obj = [null, 0, 5, [0], [false, 16]]
输出:[5, [], [16]]
解释:obj[0], obj[1], obj[3][0], 和 obj[4][0] 包含假值,因此被移除。

思路一(深度优先搜索)

这题很明显,是需要用递归写法去写的。

  1. 在遍历数组/对象的时候,发现遍历的值 value 是对象或数组,继续递归执行函数;如果不是数组也不是对象,直接返回本身。
  2. 在递归堆栈出来的时候,如果拿到的元素是假值,就直接舍去。

代码一

type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };
type Obj = Record<string, JSONValue> | Array<JSONValue>;

function compactObject(obj: any): JSONValue {
  // 递归终止点: 如果不是 object(包括对象和数组),返回本身
  // typeof null === 'object',要注意
  // typeof obj === 'object' 的情况包括数组和对象
  if (obj == null || typeof obj !== 'object') return obj;

  // 在 obj 是数组的情况
  if (Array.isArray(obj)) {
    // 初始化数组,用于存放过滤“假”值后的数组
    const newArr: any[] = [];
    for (const item of obj) {
      // 遍历数组元素,并将元素进行递归
      const value = compactObject(item);
      // 如果是真值,才把真值推进数组
      if (value) {
        newArr.push(value);
      }
    }
    return newArr;
  } else {
    // 在 obj 是对象的情况
    // 初始化对象,用于存放过滤“假”值后的对象
    const newObj: Record<string, JSONValue> = {};
    for (const key in obj) {
      // 遍历对象的 value,并将 value 进行递归
      const value = compactObject(obj[key]);
      // 如果是真值,才把真值放进对象里
      if (value) {
        newObj[key] = value;
      }
    }
    return newObj;
  }
}

思路二(函数式写法,很秀的写法)

上面的写法多少有点别扭,感觉没怎么使用 API 很难受。下面是网友的写法,可读性提高的同时,代码量少了很多。

代码二

type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };
type Obj = Record<string, JSONValue> | Array<JSONValue>;

function compactObject(obj: any): JSONValue {
  if (Array.isArray(obj)) {
    // 将所有假值过滤掉后,对所有剩余元素进行下一层递归
    return obj.filter((item) => item).map(compactObject);
  } else if (obj && typeof obj === 'object') {
    return Object.fromEntries(
      // 将对象转化为 entries 后,进行过滤和下一层递归的操作
      Object.entries(obj)
        .filter(([_, value]) => value)
        .map(([key, value]) => [key, compactObject(value)])
    );
  } else {
    // 递归终止点:obj 不是数组也不是对象时,返回自身
    return obj;
  }
}