Leetcode 30 天 JavaScript 挑战 - 06. 类

121 阅读6分钟

2694. 事件发射器

设计一个 EventEmitter 类。这个接口与 Node.js 或 DOM 的 Event Target 接口相似,但有一些差异。EventEmitter 应该允许订阅事件和触发事件。

你的 EventEmitter 类应该有以下两个方法:

  • subscribe - 这个方法接收两个参数:一个作为字符串的事件名和一个回调函数。当事件被触发时,这个回调函数将被调用。 一个事件应该能够有多个监听器。当触发带有多个回调函数的事件时,应按照订阅的顺序依次调用每个回调函数。应返回一个结果数组。你可以假设传递给 subscribe 的回调函数都不是引用相同的。 subscribe 方法还应返回一个对象,其中包含一个 unsubscribe 方法,使用户可以取消订阅。当调用 unsubscribe 方法时,回调函数应该从订阅列表中删除,并返回 undefined。
  • emit - 这个方法接收两个参数:一个作为字符串的事件名和一个可选的参数数组,这些参数将传递给回调函数。如果没有订阅给定事件的回调函数,则返回一个空数组。否则,按照它们被订阅的顺序返回所有回调函数调用的结果数组。

示例 1:

输入:
actions = ["EventEmitter", "emit", "subscribe", "subscribe", "emit"], 
values = [[], ["firstEvent", "function cb1() { return 5; }"],  ["firstEvent", "function cb1() { return 5; }"], ["firstEvent"]]
输出:[[],["emitted",[]],["subscribed"],["subscribed"],["emitted",[5,6]]]
解释:
const emitter = new EventEmitter();
emitter.emit("firstEvent"); // [], 还没有订阅任何回调函数
emitter.subscribe("firstEvent", function cb1() { return 5; });
emitter.subscribe("firstEvent", function cb2() { return 6; });
emitter.emit("firstEvent"); // [5, 6], 返回 cb1 和 cb2 的输出

示例 2:

输入:
actions = ["EventEmitter", "subscribe", "emit", "emit"], 
values = [[], ["firstEvent", "function cb1(...args) { return args.join(','); }"], ["firstEvent", [1,2,3]], ["firstEvent", [3,4,6]]]
输出:[[],["subscribed"],["emitted",["1,2,3"]],["emitted",["3,4,6"]]]
解释:注意 emit 方法应该能够接受一个可选的参数数组。

const emitter = new EventEmitter();
emitter.subscribe("firstEvent, function cb1(...args) { return args.join(','); });
emitter.emit("firstEvent", [1, 2, 3]); // ["1,2,3"]
emitter.emit("firstEvent", [3, 4, 6]); // ["3,4,6"]

示例 3:

输入:
actions = ["EventEmitter", "subscribe", "emit", "unsubscribe", "emit"], 
values = [[], ["firstEvent", "(...args) => args.join(',')"], ["firstEvent", [1,2,3]], [0], ["firstEvent", [4,5,6]]]
输出:[[],["subscribed"],["emitted",["1,2,3"]],["unsubscribed",0],["emitted",[]]]
解释:
const emitter = new EventEmitter();
const sub = emitter.subscribe("firstEvent", (...args) => args.join(','));
emitter.emit("firstEvent", [1, 2, 3]); // ["1,2,3"]
sub.unsubscribe(); // undefined
emitter.emit("firstEvent", [4, 5, 6]); // [], 没有订阅者

示例 4:

输入:
actions = ["EventEmitter", "subscribe", "subscribe", "unsubscribe", "emit"], 
values = [[], ["firstEvent", "x => x + 1"], ["firstEvent", "x => x + 2"], [0], ["firstEvent", [5]]]
输出:[[],["subscribed"],["emitted",["1,2,3"]],["unsubscribed",0],["emitted",[7]]]
解释:
const emitter = new EventEmitter();
const sub1 = emitter.subscribe("firstEvent", x => x + 1);
const sub2 = emitter.subscribe("firstEvent", x => x + 2);
sub1.unsubscribe(); // undefined
emitter.emit("firstEvent", [5]); // [7]

提示:

  • 1 <= actions.length <= 10
  • values.length === actions.length
  • 所有测试用例都是有效的。例如,你不需要处理取消一个不存在的订阅的情况。
  • 只有 4 种不同的操作:EventEmitteremitsubscribe 和 unsubscribe 。 EventEmitter 操作没有参数。
  • emit 操作接收 1 或 2 个参数。第一个参数是要触发的事件名,第二个参数传递给回调函数。
  • subscribe 操作接收 2 个参数,第一个是事件名,第二个是回调函数。
  • unsubscribe 操作接收一个参数,即之前进行订阅的顺序(从 0 开始)。

思路一(Map + 数组)

这题是一道很考验代码基本能力的题。

因为需要记忆事件的订阅,所以还是直接用 Map。同时因为一个事件名可以绑定多个回调函数,所以可以想到,Map 的 key 为事件名,value 为回调函数列表。

唯一的难点是 unsubscibe 函数,当 subscribe 之后,返回的对象可以调用 unsubscribe 来取消订阅。这里妥妥的使用了闭包。在取消订阅的时候,将事件名下对应的需要取消订阅的回调函数剔除即可。

代码

type Callback = (...args: any[]) => any;
type Subscription = {
  unsubscribe: () => void;
};

class EventEmitter {
  private eventMap: Map<string, Callback[]>;

  constructor() {
    // 初始化 Map,用于存储事件的订阅信息
    this.eventMap = new Map();
  }

  subscribe(eventName: string, callback: Callback): Subscription {
    // 查找 Map 里是否已存在
    if (this.eventMap.has(eventName)) {
      // 存在:将 callback 推入已有数组
      this.eventMap.set(eventName, [
        ...this.eventMap.get(eventName)!,
        callback,
      ]);
    } else {
      // 不存在:初始化数组
      this.eventMap.set(eventName, [callback]);
    }

    return {
      unsubscribe: () => {
        // 订阅时候 eventName 和 callback 作为闭包存储了,不会丢失
        // 所以取消订阅只要把 callback 筛除即可
        this.eventMap.set(
          eventName,
          this.eventMap.get(eventName)!.filter((fn) => fn !== callback)
        );
      },
    };
  }

  emit(eventName: string, args: any[] = []): any[] {
    if (this.eventMap.has(eventName)) {
      // 存在给定事件的回调函数,用 map 方法遍历执行返回即可
      return this.eventMap.get(eventName)!.map((callback) => callback(...args));
    } else {
      // 不存在给定事件的回调函数,返回空数组
      return [];
    }
  }
}

/**
 * const emitter = new EventEmitter();
 *
 * // Subscribe to the onClick event with onClickCallback
 * function onClickCallback() { return 99 }
 * const sub = emitter.subscribe('onClick', onClickCallback);
 *
 * emitter.emit('onClick'); // [99]
 * sub.unsubscribe(); // undefined
 * emitter.emit('onClick'); // []
 */

思路二(Map + Set)

用 Set 来代替数组,可以进一步简化代码。

代码二

type Callback = (...args: any[]) => any;
type Subscription = {
  unsubscribe: () => void;
};

class EventEmitter {
  private eventMap: Map<string, Set<Callback>>;

  constructor() {
    // 初始化 Map,用于存储事件的订阅信息
    this.eventMap = new Map();
  }

  subscribe(eventName: string, callback: Callback): Subscription {
    // 查找 Map 里是否已存在
    if (this.eventMap.has(eventName)) {
      // 存在:将 callback 加入 Set
      this.eventMap.set(eventName, this.eventMap.get(eventName)!.add(callback));
    } else {
      // 不存在:初始化 Set
      this.eventMap.set(eventName, new Set([callback]));
    }

    return {
      unsubscribe: () => {
        // 订阅时候 eventName 和 callback 作为闭包存储了,不会丢失
        // 所以取消订阅只要把 callback 删除即可
        this.eventMap.get(eventName)!.delete(callback);
      },
    };
  }

  emit(eventName: string, args: any[] = []): any[] {
    if (this.eventMap.has(eventName)) {
      // 存在给定事件的回调函数,用 map 方法遍历执行返回即可
      return [...this.eventMap.get(eventName)!].map((callback) =>
        callback(...args)
      );
    } else {
      // 不存在给定事件的回调函数,返回空数组
      return [];
    }
  }
}

/**
 * const emitter = new EventEmitter();
 *
 * // Subscribe to the onClick event with onClickCallback
 * function onClickCallback() { return 99 }
 * const sub = emitter.subscribe('onClick', onClickCallback);
 *
 * emitter.emit('onClick'); // [99]
 * sub.unsubscribe(); // undefined
 * emitter.emit('onClick'); // []
 */

2695. 包装数组

建一个名为 ArrayWrapper 的类,它在其构造函数中接受一个整数数组作为参数。该类应具有以下两个特性:

当使用 + 运算符将两个该类的实例相加时,结果值为两个数组中所有元素的总和。

当在实例上调用 String() 函数时,它将返回一个由逗号分隔的括在方括号中的字符串。例如,[1,2,3] 。

示例 1:

输入:nums = [[1,2],[3,4]], operation = "Add"
输出:10
解释:
const obj1 = new ArrayWrapper([1,2]);
const obj2 = new ArrayWrapper([3,4]);
obj1 + obj2; // 10

示例 2:

输入:nums = [[23,98,42,70]], operation = "String"
输出:"[23,98,42,70]"
解释:
const obj = new ArrayWrapper([23,98,42,70]);
String(obj); // "[23,98,42,70]"

示例 3:

输入:nums = [[],[]], operation = "Add"
输出:0
解释:
const obj1 = new ArrayWrapper([]);
const obj2 = new ArrayWrapper([]);
obj1 + obj2; // 0

提示:

  • 0 <= nums.length <= 1000
  • 0 <= nums[i] <= 1000
  • 注意:nums 是传递给构造函数的数组。

思路

这个问题引入了 JavaScript 编程中一个有趣的方面:修改 JavaScript 的加法运算符(+) String() 函数的标准行为,以模仿其他编程语言中的行为。

在 JavaScript 中,toString() 方法返回表示对象的字符串。当需要将对象显示为字符串时(例如在字符串连接操作中),JavaScript 会自动调用此方法。

JavaScript 中的 valueOf() 方法是一个内置函数,它返回指定对象的原始值。默认情况下,所有继承自 Object 的对象都继承了 valueOf() 方法。此方法可用于 NumberBooleanObjectString 和 Date 对象。

所以这题就是需要重载 valueOf()toString()方法,达到题目要求即可。

代码

class ArrayWrapper {
  private nums: number[];

  constructor(nums: number[]) {
    this.nums = nums;
  }

  valueOf(): number {
    return this.nums.reduce((sum, num) => sum + num, 0);
  }

  toString(): string {
    return `[${this.nums.join(',')}]`;
  }
};

/**
 * const obj1 = new ArrayWrapper([1,2]);
 * const obj2 = new ArrayWrapper([3,4]);
 * obj1 + obj2; // 10
 * String(obj1); // "[1,2]"
 * String(obj2); // "[3,4]"
 */

2726. 使用方法链的计算器

设计一个类 Calculator 。该类应提供加法、减法、乘法、除法和乘方等数学运算功能。同时,它还应支持连续操作的方法链式调用。Calculator 类的构造函数应接受一个数字作为 result 的初始值。

你的 Calculator 类应包含以下方法:

  • add - 将给定的数字 value 与 result 相加,并返回更新后的 Calculator 对象。
  • subtract - 从 result 中减去给定的数字 value ,并返回更新后的 Calculator 对象。
  • multiply - 将 result 乘以给定的数字 value ,并返回更新后的 Calculator 对象。
  • divide - 将 result 除以给定的数字 value ,并返回更新后的 Calculator 对象。如果传入的值为 0 ,则抛出错误 "Division by zero is not allowed" 。
  • power - 计算 result 的幂,指数为给定的数字 value ,并返回更新后的 Calculator 对象。(result = result ^ value )
  • getResult - 返回 result 的值。

结果与实际结果相差在 10^-5 范围内的解被认为是正确的。

思路

这题并不复杂,因为是链式调用,所以重点是每次调用后需要返回 this(对象实例本身)。

代码

class Calculator {
  private value: number;

  constructor(value: number) {
    this.value = value;
  }

  add(value: number): Calculator {
    this.value += value;
    return this;
  }

  subtract(value: number): Calculator {
    this.value -= value;
    return this;
  }

  multiply(value: number): Calculator {
    this.value *= value;
    return this;
  }

  divide(value: number): Calculator {
    if (value === 0) {
      throw new Error("Division by zero is not allowed");
    }
    this.value /= value;
    return this;
  }

  power(value: number): Calculator {
    this.value **= value;
    return this;
  }

  getResult(): number {
    return this.value;
  }
}

2675. 将对象数组转换为矩阵

编写一个函数,将对象数组 arr 转换为矩阵 m 。

arr 是一个由对象组成的数组或一个数组。数组中的每个项都可以包含深层嵌套的子数组和子对象。它还可以包含数字、字符串、布尔值和空值。

矩阵 m 的第一行应该是列名。如果没有嵌套,列名是对象中的唯一键。如果存在嵌套,列名是对象中相应路径,以点号 "." 分隔。

剩余的每一行对应 arr 中的一个对象。矩阵中的每个值对应对象中的一个值。如果给定对象在给定列中没有值,则应该包含空字符串 "" 。

矩阵中的列应按 字典序升序 排列。

示例 1:

输入:
arr = [  {"b": 1, "a": 2},  {"b": 3, "a": 4}]
输出:
[  ["a", "b"],
  [2, 1],
  [4, 3]
]

解释:
两个对象中有两个唯一的列名:"a"和"b"。 
"a"对应[2, 4]。 
"b"对应[1, 3]

示例 2:

输入:
arr = [
  {"a": 1, "b": 2},
  {"c": 3, "d": 4},
  {}
]
输出:
[
  ["a", "b", "c", "d"],
  [1, 2, "", ""],
  ["", "", 3, 4],
  ["", "", "", ""]
]

解释:
有四个唯一的列名:"a""b""c""d"。 
 第一个对象具有与"a""b"关联的值。 
第二个对象具有与"c""d"关联的值。 
第三个对象没有键,因此只是一行空字符串。

示例 3:

输入:
arr = [  {"a": {"b": 1, "c": 2}},  {"a": {"b": 3, "d": 4}}]
输出:
[  ["a.b", "a.c", "a.d"],
  [1, 2, ""],
  [3, "", 4]
]

解释:
在这个例子中,对象是嵌套的。键表示每个值的完整路径,路径之间用句点分隔。 
有三个路径:"a.b"、"a.c"、"a.d"。

示例 4:

输入:
arr = [
  [{"a": null}],
  [{"b": true}],
  [{"c": "x"}]
]
输出: 
[
  ["0.a", "0.b", "0.c"],
  [null, "", ""],
  ["", true, ""],
  ["", "", "x"]
]

解释:
数组也被视为具有索引为键的对象。 
每个数组只有一个元素,所以键是"0.a""0.b""0.c"

示例 5:

输入:
arr = [
  {},
  {},
  {},
]
输出:
[
  [],
  [],
  [],
  []
]

解释:
没有键,所以每一行都是一个空数组。

思路

这题的唯一难点是处理起来很复杂,容易小地方写错导致跑不起来。

根据题意,这题的解题步骤是:

  1. 对 arr 先进行第一次遍历,将所有的 key 都收集起来
  2. 根据 key 数组,排序后创建 map,用于收集数据
  3. 对 arr 进行第二次遍历,将所有的值收集到 map 中
  4. 将 map 结果转化成题目要求的格式

代码

type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };

function jsonToMatrix(arr: JSONValue[]): JSONValue[][] {
  const isObjectOrArray = (value: JSONValue) =>
    typeof value === 'object' && value != null;
  const columnSet = new Set<string>();
  // 用于收集对象内的所有 key
  const initializeColumnSet = (elem: JSONValue, prefix = '') => {
    for (const key of Object.keys(elem)) {
      const value = elem[key];
      if (isObjectOrArray(value)) {
        initializeColumnSet(value, prefix + key + '.');
      } else {
        columnSet.add(prefix + key);
      }
    }
  };
  // 第一次遍历:遍历所有元素,获得 arr 所拥有的 key
  arr.forEach((elem) => initializeColumnSet(elem));

  // 所有数据结果的 map
  const resultMap = new Map<string, JSONValue[]>();
  // 排序得到列名数组
  const titles = [...columnSet].sort();
  // 初始化 map
  for (const column of titles) {
    resultMap.set(column, []);
  }

  // 用于存放没有被使用到的 key
  let remainedTitles: Set<string>;

  // 用于递归遍历对象,将元素值放入 map
  const generateMap = (elem: JSONValue, prefix = '') => {
    for (const key of Object.keys(elem)) {
      const value = elem[key];
      if (isObjectOrArray(value)) {
        generateMap(value, prefix + key + '.');
      } else if (resultMap.has(prefix + key)) {
        resultMap.set(prefix + key, [...resultMap.get(prefix + key), value]);
        remainedTitles.delete(prefix + key);
      }
    }
  };
  // 第二次遍历:遍历 arr 内的所有对象,将元素值放入 map
  arr.forEach((elem) => {
    remainedTitles = new Set(titles);
    generateMap(elem);
    // 填充空值
    remainedTitles.forEach((title) => {
      resultMap.set(title, [...resultMap.get(title), '']);
    });
  });

  const res: JSONValue[][] = [titles];
  const columnResults = [...resultMap.values()];
  // 将 map 的值放入 res
  for (let i = 0; i < arr.length; i++) {
    res.push(columnResults.map((columnResult) => columnResult[i]));
  }
  return res;
}

2700. 两个对象之间的差异

请你编写一个函数,它接收两个深度嵌套的对象或数组 obj1 和 obj2 ,并返回一个新对象表示它们之间差异。

该函数应该比较这两个对象的属性,并识别任何变化。返回的对象应仅包含从 obj1 到 obj2 的值不同的键。

对于每个变化的键,值应表示为一个数组 [obj1 value, obj2 value] 。不存在于一个对象中但存在于另一个对象中的键不应包含在返回的对象中。在比较两个数组时,数组的索引被视为它们的键。最终结果应是一个深度嵌套的对象,其中每个叶子的值都是一个差异数组。

你可以假设这两个对象都是 JSON.parse 的输出结果。

示例 1:

输入: 
obj1 = {}
obj2 = {
  "a": 1, 
  "b": 2
}
输出:{}
解释:obj1没有进行任何修改。obj2中出现了新的键 "a""b" ,但添加或删除的键应该被忽略。

示例 2:

输入:
obj1 = {
  "a": 1,
  "v": 3,
  "x": [],
  "z": {
    "a": null
  }
}
obj2 = {
  "a": 2,
  "v": 4,
  "x": [],
  "z": {
    "a": 2
  }
}
输出:
{
  "a": [1, 2],
  "v": [3, 4],
  "z": {
    "a": [null, 2]
  }
}
解释:键 "a""v""z" 都有变化。"a"1 变为 2"v"3 变为 4"z" 的子对象 "a"null 变为 2

示例 3:

输入:
obj1 = {
  "a": 5, 
  "v": 6, 
  "z": [1, 2, 4, [2, 5, 7]]
}
obj2 = {
  "a": 5, 
  "v": 7, 
  "z": [1, 2, 3, [1]]
}
输出:
{
  "v": [6, 7],
  "z": {
    "2": [4, 3],
    "3": {
      "0": [2, 1]
    }
  }
}
解释:在 obj1 和 obj2 中,键 "v" 和 "z" 的值不同。"a" 被忽略,因为值没有变化。在键 "z" 中,有一个嵌套的数组。数组被视为对象,其中索引被视为键。数组发生了两处变化:z[2] 和 z[3][0]。z[0] 和 z[1] 没有变化,因此没有包含在结果中。z[3][1] 和 z[3][2] 被删除,因此也没有包含在结果中。

示例 4:

输入:
obj1 = {
  "a": {"b": 1}, 
}
obj2 = {
  "a": [5],
}
输出:
{
  "a": [{"b": 1}, [5]]
}
解释:键 "a" 在两个对象中都存在。但由于两个相关值具有不同的类型,所以它们被放置在差异数组中。

示例 5:

输入:
obj1 = {
  "a": [1, 2, {}], 
  "b": false
}
obj2 = {   
  "b": false,
  "a": [1, 2, {}]
}
输出:
{}
解释:除了键的顺序不同之外,两个对象是相同的,因此返回一个空对象。

思路

根据题意,函数逻辑如下:

  1. 如果 obj1 和 obj2 不是同一个类型,直接返回[obj1, obj2]

  2. 否则,两个类型相同

    1. 如果两个都是基础类型,值相同返回空对象,否则返回[obj1, obj2]
    2. 否则,说明 obj1 和 obj2 都是引用类型,需要进行递归,继续对里面的 value 进行比较,即objDiff(obj1[k], obj2[k])

代码

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

const isPrimitive = (value: any) => value == null || typeof value !== 'object';

const sameType = (value1: any, value2: any) =>
  Object.prototype.toString.call(value1) === Object.prototype.toString.call(value2);

function objDiff(obj1: Obj, obj2: Obj): Obj {
  if (!sameType(obj1, obj2)) {
    // 不是同一个类型,直接返回 diff
    return [obj1, obj2];
  } else {
    if (isPrimitive(obj1)) {
      // 基础类型比较,不相等返回 diff
      return obj1 === obj2 ? {} : [obj1, obj2];
    } else {
      const res = {};
      // 引用类型比较,需要递归比较引用类型的 value
      for (const key of Object.keys(obj1).filter((key1) => key1 in obj2)) {
        const diff = objDiff(obj1[key], obj2[key]);
        if (Object.keys(diff).length > 0) {
          res[key] = diff;
        }
      }
      return res;
    }
  }
}

2632. 柯里化

请你编写一个函数,它接收一个其他的函数,并返回该函数的 柯里化 后的形式。

柯里化 函数的定义是接受与原函数相同数量或更少数量的参数,并返回另一个 柯里化 后的函数或与原函数相同的值。

实际上,当你调用原函数,如 sum(1,2,3) 时,它将调用 柯里化 函数的某个形式,如 csum(1)(2)(3), csum(1)(2,3), csum(1,2)(3),或 csum(1,2,3) 。所有调用 柯里化 函数的方法都应该返回与原始函数相同的值。

示例 1:

输入:
fn = function sum(a, b, c) { return a + b + c; }
inputs = [[1],[2],[3]]
输出:6
解释:
执行的代码是:
const curriedSum = curry(fn);
curriedSum(1)(2)(3) === 6;
curriedSum(1)(2)(3) 应该返回像原函数 sum(1, 2, 3) 一样的值。

示例 2:

输入:
fn = function sum(a, b, c) { return a + b + c; }
inputs = [[1,2],[3]]]
输出:6
解释:
curriedSum(1, 2)(3) 应该返回像原函数 sum(1, 2, 3) 一样的值。

示例 3:

输入:
fn = function sum(a, b, c) { return a + b + c; }
inputs = [[],[],[1,2,3]]
输出:6
解释:
你应该能够以任何方式传递参数,包括一次性传递所有参数或完全不传递参数。
curriedSum()()(1, 2, 3) 应该返回像原函数 sum(1, 2, 3) 一样的值。

示例 4:

输入:
fn = function life() { return 42; }
inputs = [[]]
输出:42
解释:
柯里化一个没有接收参数,没做有效操作的函数。
curriedLife() === 42

思路

柯里化函数是一个很常见的函数,柯里化是指将一个多参数的函数转换为一系列只接受单一参数的函数序列。

这里只要注意的是,需要一个闭包来存柯里化的参数,达到长度后直接执行,否则仍然返回函数本身。

代码

function curry(fn: Function): Function {
  // 闭包,用于存储传递进来的参数
  let argArr: any[] = [];

  return function curried(...args: any[]) {
    argArr = [...argArr, ...args];
    // 如果传入的长度够了,直接执行
    if (fn.length <= argArr.length) {
      return fn(...argArr);
    } else {
      // 否则返回当前函数
      return curried;
    }
  }
};

/**
 * function sum(a, b) { return a + b; }
 * const csum = curry(sum);
 * csum(1)(2) // 3
 */

2628. 完全相等的 JSON 字符串

给定两个对象 o1 和 o2 ,请你检查它们是否 完全相等 。

对于两个 完全相等 的对象,必须满足以下条件:

  • 如果两个值都是原始类型,它们通过了 === 等式检查,则认为这两个值是 完全相等 的。
  • 如果两个值都是数组,在它们具有相同元素且顺序相同,并且每个元素在这些条件下也 完全相等 时,认为这两个值是 完全相等 的。
  • 如果两个值都是对象,在它们具有相同键,并且每个键关联的值在这些条件下也 完全相等 时,认为这两个值是 完全相等 的。

你可以假设这两个对象都是 JSON.parse 的输出。换句话说,它们是有效的 JSON 。

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

示例 1:

输入:o1 = {"x":1,"y":2}, o2 = {"x":1,"y":2}
输出:true
输入:键和值完全匹配。

示例 2:

输入:o1 = {"y":2,"x":1}, o2 = {"x":1,"y":2}
输出:true
解释:尽管键的顺序不同,但它们仍然完全匹配。

示例 3:

输入:o1 = {"x":null,"L":[1,2,3]}, o2 = {"x":null,"L":["1","2","3"]}
输出:false
解释:数字数组不同于字符串数组。

示例 4:

输入:o1 = true, o2 = false
输出:false
解释:true !== false

思路

这题也没啥难度,就是递归去比较。

  1. 类型不相等,返回 false

  2. 类型相等

    1. 是基础类型,严格比较得到结果
    2. 引用类型,比较所有 value,仅当所有 value 相等时才返回 true,否则返回 false。

代码

type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };

const sameType = (value1: any, value2: any) => Object.prototype.toString.call(value1) === Object.prototype.toString.call(value2);

const isPrimitive = (value: any) => (value == null) || (typeof value !== 'object');

function areDeeplyEqual(o1: JSONValue, o2: JSONValue): boolean {
  // 类型不同,直接返回 false
  if (!sameType(o1, o2)) {
    return false;
  } else {
    if (isPrimitive(o1)) {
      // 基础类型,直接严格比较就行
      return o1 === o2;
    } else {
      // 引用类型长度不同,直接 false
      if (Object.keys(o1).length !== Object.keys(o2).length) {
        return false;
      }
      for (const key of Object.keys(o1)) {
        // 递归比较引用类型中,所有 value 值
        if (!areDeeplyEqual(o1[key], o2[key])) {
          return false;
        }
      }
      // 比较结束,都是相等的,直接返回 true
      return true;
    }
  }
};

2633. 将对象转换为 JSON 字符串

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

请你在不使用内置方法 JSON.stringify 的前提下解决这个问题。

示例 1:

输入:object = {"y":1,"x":2}
输出:{"y":1,"x":2}
解释:
返回该对象的 JSON 表示形式。
注意,键的返回顺序应该与 Object.keys() 的顺序相同。

示例 2:

输入:object = {"a":"str","b":-12,"c":true,"d":null}
输出:{"a":"str","b":-12,"c":true,"d":null}
解释:
JSON 的基本类型是字符串、数字型、布尔值和 null

示例 3:

输入:object = {"key":{"a":1,"b":[{},null,"Hello"]}}
输出:{"key":{"a":1,"b":[{},null,"Hello"]}}
解释:
对象和数组可以包括其他对象和数组。

示例 4:

输入:object = true
输出:true
解释:
基本类型是有效的输入

思路

这题的并没有什么难度。也是需要判断是否是基础类型,如果是基础类型,就可以输出了,否则就是引用类型。

注:基础类型中,string 类型输出需要用双引号包裹,null 或 undefined 需要直接输出字符串,否则在后续处理的时候会被数据相关的函数处理为空而丢失。

引用类型有数组和对象两种,需要将里边的 value 进行递归处理。数组和对象的 stringify 的形态不一样,所以需要分开处理。

代码

type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };

const isPrimitive = (value: any) => value == null || typeof value !== 'object';

function jsonStringify(object: JSONValue): JSONValue {
  if (isPrimitive(object)) {
    // 是基础类型,根据类型处理后输出
    if (object == null) return `${object}`;
    if (typeof object === 'string') return `"${object}"`;
    return object;
  } else {
    if (Array.isArray(object)) {
      return `[${object.map((item) => jsonStringify(item)).join(',')}]`;
    } else {
      return `{${Object.entries(object)
        .map(([key, value]) => `"${key}":${jsonStringify(value)}`)
        .join(',')}}`;
    }
  }
}