每日知识积累 Day 6

200 阅读9分钟

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

1. 一个类型体操

类型体操题目集合

Deep Readonly

实现一个通用的 DeepReadonly<T>,它将对象的每个参数及其子对象递归地设为只读。

您可以假设在此挑战中我们仅处理对象。数组,函数,类等都无需考虑。但是,您仍然可以通过覆盖尽可能多的不同案例来挑战自己。

例如

type X = {
  x: {
    a: 1;
    b: "hi";
  };
  y: "hey";
};

type Expected = {
  readonly x: {
    readonly a: 1;
    readonly b: "hi";
  };
  readonly y: "hey";
};

type Todo = DeepReadonly<X>; // should be same as `Expected`

分析

这也是递归,只不过对象从 Tuple 变成了 Object 所以终止条件会有所区别。

尝试写出

type DeepReadonly<X extends Object> = {
  readonly [K in keyof X]: X[K] extends object ? DeepReadonly<X[K]> : X[K];
};

测试用例

type Todo = DeepReadonly<{
  x: {
    a: 1;
    b: "hi";
  };
  y: "hey";
}>;

参考答案

type DeepReadonly<T extends {}> = {
  readonly [P in keyof T]: T[P] extends { [K: string]: {} } | any[]
    ? DeepReadonly<T[P]>
    : T[P];
};

经验总结

  1. Object 应该换成 {}
  2. object 应该换成 { [K: string]: {} } | any[]
  3. {} 的键组成的联合类型可以使用 keyof 运算符得到

在 TypeScript 中,Object 类型和 object 类型通常可以互换使用,但它们在语义上有一些细微的差别:

  1. Object 类型:指的是所有对象类型的父类型,包括任何具有构造函数的自定义对象、内置对象(如 DateRegExpArray 等),以及原始包装对象(如 StringNumberBoolean 包装对象)。Object 类型在 TypeScript 中通常用来表示一个值至少是一个对象。

    let obj: Object;
    obj = new Date(); // 正确
    obj = {}; // 正确
    obj = 42; // 错误,因为 42 不是对象
    
  2. object 类型:在 TypeScript 中,使用小写的 object 可以表示非原始类型的所有对象。这意味着它排除了原始类型(如 stringnumberboolean 等),只包括由 {} 创建的普通对象和所有自定义对象类型。

    let obj: object;
    obj = {}; // 正确
    obj = { name: "Alice" }; // 正确
    obj = 42; // 错误,因为 42 是原始类型
    

在实际使用中,如果你想要确保一个变量只能是对象而不能是原始类型,使用 object 类型是一个更明确的选择。如果你想要包含所有类型的对象,包括原始包装对象,那么使用 Object 类型可能更合适。

请注意,TypeScript 的类型系统是结构化的,这意味着只要两个类型的形状相同,它们就被认为是兼容的。因此,Objectobject 在很多情况下可以互换使用,但它们在类型守卫和类型排除时的行为可能会有所不同。

2. 两个 Leetcode 题目

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

2.1 [453] 最小操作次数使数组相等

给你一个长度为 n 的整数数组,每次操作将会使 n - 1 个元素增加 1 。返回让数组所有元素相等的最小操作次数。

示例 1:

输入:nums = [1,2,3]

输出:3

解释:只需要 3 次操作(注意每次操作会增加两个元素的值): [1,2,3] => [2,3,3] => [3,4,3] => [4,4,4]

示例 2:

输入:nums = [1,1,1]

输出:0

提示:

n == nums.length

1 <= nums.length <= 105

-109 <= nums[i] <= 109

答案保证符合 32-bit 整数

尝试实现:

/**
 * @param {number[]} nums
 * @return {number}
 */
var minMoves = function (nums) {
  const n = nums.length;
  if (n === 1) return 0;

  let sum = 0;
  let min = nums[0];

  for (let i = 0; i < n; i++) {
    if (min > nums[i]) {
      sum += (min - nums[i]) * i;
      min = nums[i];
    } else {
      sum += nums[i] - min;
    }
  }
  return sum;
};

我的思路: 实话实说,我没想清楚,需要逆向思维,下面是标准答案。

因为只需要找出让数组所有元素相等的最小操作次数,所以我们不需要考虑数组中各个元素的绝对大小,即不需要真正算出数组中所有元素相等时的元素值,只需要考虑数组中元素相对大小的变化即可。

因此,每次操作既可以理解为使 n−1 个元素增加 1,也可以理解使 1 个元素减少 1。显然,后者更利于我们的计算。

于是,要计算让数组中所有元素相等的操作数,我们只需要计算将数组中所有元素都减少到数组中元素最小值所需的操作数

完整链接:leetcode.cn/problems/mi…

得分结果: 68.97% 68.96%

2.2 [665] 非递减数列

给你一个长度为 n 的整数数组 nums ,请你判断在 最多 改变 1 个元素的情况下,该数组能否变成一个非递减数列。

我们是这样定义一个非递减数列的: 对于数组中任意的 i (0 <= i <= n-2),总满足 nums[i] <= nums[i + 1]。

示例 1:

输入: nums = [4,2,3]
输出: true
解释: 你可以通过把第一个 4 变成 1 来使得它成为一个非递减数列。

示例 2:

输入: nums = [4,2,1]
输出: false
解释: 你不能在只改变一个元素的情况下将其变为非递减数列。

提示:

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

尝试完成:

/**
 * @param {number[]} nums
 * @return {boolean}
 */
var checkPossibility = function (nums) {
  function isNotDescend(a, b) {
    const arr = nums.slice(a, b);
    const length = arr.length;
    if (length === 1) return true;
    for (let i = 1; i < length; i++) {
      if (arr[i] < arr[i - 1]) return false;
    }
    return true;
  }

  const n = nums.length;
  for (let j = 0; j < n; j++) {
    if (j === 0) {
      if (isNotDescend(1, n)) return true;
    }
    if (j === n - 1) {
      if (isNotDescend(0, n - 1)) return true;
    }

    if (nums[j + 1] < nums[j - 1]) continue;

    const _a = isNotDescend(0, j);
    if (!_a) return false;
    const _b = isNotDescend(j + 1, n); // slice(j, -1) 会把最后一个元素去掉;要么就是 slice(j) 要么就是 slice(j, n)
    if (_a && _b) return true;
  }
  return false;
};

我的思路:

  1. 遍历数组中的每一个元素,修改这个元素使原数组非递减的条件是,这个元素左边非递减;右边非递减,并且右边第一个数大于等于左边最后一个数
  2. 如果有这样的数返回 true 否则返回 false
  3. 考虑边界条件,对于第一个元素,只要右边非递减即可;对于最后一个元素,只要左边非递减即可。

得分结果: 23.19% 5.80%

总结提升:

  1. slice(j, -1) 会把最后一个元素去掉;要么就是 slice(j) 要么就是 slice(j, n)
  2. slice 返回的就是切片,并且不会影响原数组
  3. 使用 slice 的时候尽量不要用简写,如果去掉第一个数:slice(1,n) 去掉最后一个数就是 slice(0, n-1)

3. 三个前端题目

  1. js 中,将其他类型值隐式转变成 number 类型的机制是什么?

js 中的数据类型有八种,接下来对着八种逐一进行分析:

  • null: 隐式转换成 0
  • undefined: 隐式转换成 NaN
  • string: ①0 ②8,10,16 进制对应数 ③NaN
  • number: 原样返回
  • boolean: true 1, false 0
  • symbol: 报错
  • bigint: 报错
  • object: 内部机制

总结一下就是:对于 null, undefined, string, boolean 来说相当于显式调用 Number()进行强转;对于 bigint 和 symbol 来说会报错(但是 Number()对 bigint 不会报错,但对 symbol 仍然报错);对于 object 会触发内部机制。

那么内部机制是什么呢?object 中有 primitive 机制和 defaultValue 机制,当将一个 object 隐式转成 number 类型的时候会按照一下流程:

    1. 查看 object 上是否实现了 valueof 方法,如果有则跳到 4,如果没有实现则;
    1. 查看 object 上是否实现了 toSting 方法,如果有则跳到 5,如果没有实现则;
    1. 返回 not a number, 但不是 NaN;
Object.is({}, NaN); // false
Number.isNaN({}); // false
isNaN({}); // true
    1. 假如 valueof 方法的返回值为 y,如果 typeof y 为"object",则直接报错;如果不为"object",则返回 Number(y);
    1. 假如 toSting 方法的返回值为 z,如果 typeof z 为"object",则直接报错;如果不为"object",则返回 Number(z);
  1. 手写一个函数,实现节流。 首先,节流的含义可以简单地理解成:在规定的时间内同一个函数不可以被触发超过一次。那么从其实现的功能出发,可以推出实现的策略为:
  • 建立一个闭包,在闭包中存储函数最后一次执行成功的时间戳lastExeTime
  • 然后在每一次尝试性执行此函数的额时候,获取当前的时间戳currentTime,并与最后一次执行成功的时间戳做差const gap = currentTime - lastExeTime
  • 如果 gap 的值大于规定值,则证明无需节流,执行函数,然后更新lastExeTime的值;
  • 如果 gap 的值小于规定值,则证明还在节流,这个时候什么都不需要做。
function throttle(fn, wait) {
  let lastExeTime = 0;
  return function (...args) {
    const currentTime = +new Date();
    const gap = currentTime - lastExeTime;
    if (gap > wait) {
      lastExeTime = currentTime;
      fn.apply(this, args);
    }
  };
}
  1. 手写一个函数,实现防抖。 首先,防抖的含义可以简单地理解成:在函数触发之前必须连续的等待规定的时间。这里需要重点理解连续的等待,所谓等待就是指规定的时间之后执行,而连续指的是在等待的过程中不能尝试去触发此函数,如果尝试了就打破了连续性,就需要重新计时。那么从其实现的功能出发,可以推出实现的策略为:
  • 建立一个闭包,在其中使用一个变量timer表示当前状态;
  • 如果 timer 的值为 undefined,表示此函数从来没有执行过;
  • 如果 timer 的值为 null,表示此刻无需防抖;
  • 如果 timer 的值为设置定时器的返回值,则表示此时需要防抖;
  • 根据 timer 的状态,引导待执行的函数做出不同的反应。
function debounce(fn, delay) {
  let timer = undefined;
  return function (...args) {
    if (!timer) {
      clearTimeout(timer);
    }

    timer = setTimeout(() => {
      timer = null;
      fn.apply(this.args);
    }, delay);
  };
}

可能你会觉得上面的实现过程中有一些代码是多余的;但是你先不要着急,因为按照最开始的策略就是应该这样实现。

接下来,加强上面的节流函数,加强的结果就是,使用者可以选择在 fn 执行之前连续等待规定的时间,或者先执行,然后再连续等待规定的时间;有一点不会变,那就是在连续等待的这段时间内需要防抖。

使用另外的语义化的参数immediate来表示使用者选择了哪种模式。

function debounce(fn, delay, immediate) {
  let timer = undefined;
  return function (...args) {
    if (!timer) {
      clearTimeout(timer);
    }

    if (!timer && immediate) {
      fn.apply(this.args);
    }

    timer = setTimeout(() => {
      timer = null;
      if (!immediate) fn.apply(this.args);
    }, delay);
  };
}

注意!两个判断条件分别是if (!timer && immediate)if(!immediate),是不一样的!

4.四句英语积累

  1. get back to someone -- give somebody more info than ealier
    • I'm sorry I don't have that infomation right now, but I will [get back to you] [by the end of the day]. [Would that be okay]?
    • Did Simon contact you about the offer? No. He said [he'd get back to me today], but [I still haven't heard from him].
  2. come up -- happen unexpectedlly
    • A lot of [problems came up] [in the first week of the project], but now [things are running more smoothly].
    • I'm sorry, but [something has just come up] and I need to [deal with it immediately].