每日知识积累 Day 9

304 阅读9分钟

每日的知识积累,包括 1 个 Ts 类型体操,两个 Leetcode 算法题,三个前端八股文题,四个英语表达积累。

1. 一个类型体操

类型体操题目集合 Readonly 2

实现一个通用 MyReadonly2<T, K>,它带有两种类型的参数 T 和 K。

K 指定应设置为 Readonly 的 T 的属性集。如果未提供 K,则应使所有属性都变为只读,就像普通的 Readonly一样。

例如:

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

const todo: MyReadonly2<Todo, "title" | "description"> = {
  title: "Hey",
  description: "foobar",
  completed: false,
};

todo.title = "Hello"; // Error: cannot reassign a readonly property
todo.description = "barFoo"; // Error: cannot reassign a readonly property
todo.completed = true; // OK

分析

很容易想到就是将原来的键分成两组,一组是 K 中的,一组不是 K 中的,关键在于如何将这分开的两部分合起来:使用 &.

尝试写出

type MyExclude<T, U> = T extends U ? never : T;

type MyReadonly2<T extends {}, K extends keyof T> = {
  readonly [P2 in K]: T[P2];
} & {
  [P1 in MyExclude<keyof T, K>]: T[P1];
};

简写方式

我们可以使用断言的方式筛选 P in keyof T 中的键:

type MyReadonly2<T extends {}, K extends keyof T> = {
  readonly [P2 in K]: T[P2];
} & {
  [P1 in keyof T as P1 extends K ? never : P1]: T[P1];
};

测试用例

type MyReadonly2<T extends {}, K extends keyof T> = {
  readonly [P2 in K]: T[P2];
} & {
  [P1 in MyExclude<keyof T, K>]: T[P1];
};

type C = MyReadonly2<Todo, "title">;

/*
  type C = {
      readonly title: string;
  } & {
      description: string;
      completed: boolean;
  }
*/

参考答案

type MyReadonly2<
  T,
  K extends keyof T = keyof T,
  O extends keyof T = keyof T
> = { readonly [P in keyof T]: T[P] } & {
  [P in O extends K ? never : O]: T[P];
};

经验总结

  1. 使用 & 链接多个 {} 类型;
  2. 使用 as 做范围限制,而不只是使用 extends.

2. 两个 Leetcode 题目

刷题的顺序参考这篇文章 LeeCode 刷题顺序

2.1 [189] 轮转数组

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。


示例 1:

输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
示例 2:

输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]


提示:

1 <= nums.length <= 105
-231 <= nums[i] <= 231 - 1
0 <= k <= 105


进阶:

尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。
你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?

尝试实现:

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {void} Do not return anything, modify nums in-place instead.
 */
var rotate = function (nums, k) {
  const rst = [];
  const n = nums.length;
  for (let i = 0; i < n; i++) {
    rst[(i + k) % n] = nums[i];
  }
  for (let i = 0; i < n; i++) {
    nums[i] = rst[i];
  }
  return nums;
};
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {void} Do not return anything, modify nums in-place instead.
 */
var rotate = function (nums, k) {
  const n = nums.length;
  k = k % n;
  if (k === 0) return nums;

  for (let i = 0; i < n; i++) {
    const _i = (i + k) % n;
    const num = nums[i]; // 当前值
    const target = nums[_i];

    if (typeof num === "string") {
      // 提取源数据,还原数字
      const [a, b] = num.split("&");
      nums[i] = parseInt(a);

      if (typeof target === "undefined") {
        nums[_i] = parseInt(b);
      } else {
        nums[_i] = parseInt(b) + "&" + target;
      }
    } else {
      // 未被遍历过
      if (typeof target === "undefined") {
        nums[_i] = nums[i];
      } else {
        nums[_i] = nums[i] + "&" + target;
      }
      nums[i] = undefined;
    }
  }

  return nums;
};

我的思路:

  1. 首先如果使用辅助数组来做的话,这道题基本没有难度;题目本意是在 nums 上调整顺序
  2. 假如 k = 3, 当 i = 0 的时候,nums[0] 要到 nums[3] 位置,但是 nums[3] 怎么办
  3. 我们跳出 number 类型,使用不同类型表示是否遍历或者是否修改过
  4. 获取当前的元素,如果此元素为 number 类型的,则检查其后 k 位置,如果 k 位置元素为 undefined 则直接赋值;如果为 string 类型

得分结果: 5.06% 5.00%

总结提升:

  1. 对于这种环状题目,需要取余以实现最小的完成时间并且防止潜在的溢出错误。例如本题中,我们可以先确保 k<n 以及当 k=0 或者 n=1 的时候直接输出。
  2. 将数组每个元素向后移动 i 个元素可写成 [...nums, ...nums].slice(n - i, 2 * n - i)

2.2 [396] 旋转函数

给定一个长度为 n 的整数数组 nums 。

假设 arrk 是数组 nums 顺时针旋转 k 个位置后的数组,我们定义 nums 的 旋转函数  F 为:

F(k) = 0 * arrk[0] + 1 * arrk[1] + ... + (n - 1) * arrk[n - 1]
返回 F(0), F(1), ..., F(n-1)中的最大值 。

生成的测试用例让答案符合 32 位 整数。


示例 1:

输入: nums = [4,3,2,6]
输出: 26
解释:
F(0) = (0 * 4) + (1 * 3) + (2 * 2) + (3 * 6) = 0 + 3 + 4 + 18 = 25
F(1) = (0 * 6) + (1 * 4) + (2 * 3) + (3 * 2) = 0 + 4 + 6 + 6 = 16
F(2) = (0 * 2) + (1 * 6) + (2 * 4) + (3 * 3) = 0 + 6 + 8 + 9 = 23
F(3) = (0 * 3) + (1 * 2) + (2 * 6) + (3 * 4) = 0 + 2 + 12 + 12 = 26
所以 F(0), F(1), F(2), F(3) 中的最大值是 F(3) = 26 。
示例 2:

输入: nums = [100]
输出: 0


提示:

n == nums.length
1 <= n <= 105
-100 <= nums[i] <= 100

尝试完成:

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxRotateFunction = function (nums) {
  const sum = _.sum(nums);
  function calc(arr) {
    let sum = 0;
    for (let i = 0; i < arr.length; i++) {
      sum += i * arr[i];
    }
    return sum;
  }

  const n = nums.length;
  let f = calc(nums);
  let max = f;

  if (n < 2) return f;

  for (let i = 1; i < n; i++) {
    f += sum - n * nums[n - i];
    max = max < f ? f : max;
  }

  return max;
};

我的思路:

  1. 这个题不能使用穷举法,会超时的;
  2. 说到底这是一个算法题,并不只是编程题,所以当我们要进行很多超时操作的时候,我应该尝试使用迭代的算法,从一个耗时操作得到另外一个,而不是从头算起。
  3. 假如 i 为 1, 当我表示倒数第一个元素的时候,我不应该使用 nums[-i] 而是应该使用 nums[n-i]

得分结果: 44.44% 66.67%

3. 三个前端题目

  1. 对比操作符 ||, && 和 ??
  • 共同点:
    • 这三个操作符都具有短路的特性
    • ??可以看成是||收紧版本||对 js 中的假值短路生效,而??只对undefinednull生效(也就是说console.log(0 ?? 20)的执行结果为 0 而不是 20)
  • 不同点:
    • x||y的特性是,x 为假值(或者 x 表达式的返回值为假值)的时候,返回 y 的值(或者 y 表达式的返回值);但是 x 为真值(或者 x 表达式的返回值为真值)的时候,不会执行 y 或者 y 表达式,就好像||y不存在一样,直接返回 x(或者 x 表达式的返回值)
    • x&&y的特性是,x 为真值(或者 x 表达式的返回值为真值)的时候,返回 y 的值(或者 y 表达式的返回值);但是 x 为假值(或者 x 表达式的返回值为真值)的时候,不会执行 y 或者 y 表达式,就好像||y不存在一样,直接返回 x(或者 x 表达式的返回值)
  1. 对比 ==, === 和 Object.is,并手写 Object.is 这个就不用找相同点和不同点了,这三个不能说一模一样吧,只能说是完全不同了,所以分开来单独说:
  • a == b的判断机制是:
      1. 如果 a 和 b 的类型相同(这里指的是 typeof 作用之后的返回值相同),那么对非引用值直接进行比较原始值;而对引用值比较的是内存地址;
      1. 如果 a 和 b 的类型不同,则首先判断是否其中有一个是引用类型的,如果两个都不是引用类型,则直接返回 false;如果有一个是,则根据 js 设计规格将两者强制转成同一个非引用类型进行比较。
  • a === b的判断机制是:
      1. 如果 a 和 b 的类型不相同,则直接返回 false
      1. 如果 a 和 b 都是引用类型的,则比较内存地址
      1. 如果 a 和 b 都是非引用类型的,则比较原始值
  • Object.is(a,b)的判断机制是:NaN 等于自己,0 不等于自己
      1. 检查是否符合两种特例,如果不符合,则直接返回a===b的结果
      1. 特例一:a 和 b 都是 NaN,此时应该返回 true
      1. 特例二:a 和 b 都是 0,只不过一个为正一个为负,此时需要返回 false

手写Object.is之前需要补充一个知识点:1 / Infinity === 0;的结果是 true;因此 Object.is(0, 1/Infinity) 也为 true.

根据 Object.is 的判断机制,容易写出其实现:

function myObjectIs(a, b) {
  if (Number.isNaN(a) && Number.isNaN(b)) return true;
  if (a === 0 && b === 0 && 1 / a !== 1 / b) return false;
  return a === b;
}

有一个坑,那就是需要使用Number.isNaN而不是isNaN!

  1. 什么是 js 中的包装类型,又如何反包装? 对于 js 中的一个变量 x 而言,如果typeof x的返回值是 C(= "number" | "string" | "boolean"),这意味着 x 是非引用类型。那么,按照道理来说,x 是没有属性和方法的。但是作为使用者,仍然可以通过"abc".length获得字符串的长度,或者使用123.toString(16)将 number 转成字符串格式。

之所以能够这样,在于 js 中存在着的包装类型。包装类型实际上是一种机制,在使用者做上述操作的时候,js 会自动将"number" | "string" | "boolean"包装成对象,即所谓的包装对象。

这个过程可以表示为:let _x = new C(x);其中,x 是"number" | "string" | "boolean"类型的,C 为构造函数Number | String | Boolean.

对于包装对象有如下特征:

  • x == _x // true 除过NaN
  • x === _x // false
  • _x.valueOf() === x // true 除过NaN

可以看到,使用包装对象的valueOf方法可以反包装,得到原始值(又称为是 Primitive Value)

补充:一般认为 js 中有六种primitive value

  • undefined
  • null
  • string
  • number
  • boolean
  • symbol
  • 至于 bigint, 2020 年之后才出现的,也算是 primitive value

4.英语造句

  1. I need to brainstorm more ideas about the problem you just mentioned. Let's start brainstorming some solutions to this online bug.

  2. Through reviewing the current situation, you can do better next time. I plan to review the guidelines of my new job.

  3. I can't identify the cause of this bug, it's too difficult to find.

  4. Every day, I have so many new features to implement. You must implement the aforementioned functionality today.

  5. Here's the data provided by another colleague, pleae analyse it and give me the outcome as soon as possible. Analyse the current situation and tell me how bad it is.

  6. You must approach your tough times, otherwise you can't make any progress. Sometimes, I need bravery to approach the reality.

  7. Tomorrow is the deadline, we must finalise this deal then.