每日知识积累 Day 27

9 阅读10分钟

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

1. 一个类型体操

类型体操题目集合 OmitByType

从 T 中选出一组无法分配给 U 的属性

示例:

type OmitBoolean = OmitByType<
  {
    name: string;
    count: number;
    isReadonly: boolean;
    isEnable: boolean;
  },
  boolean
>; // { name: string; count: number }

分析

这个是对 Object 进行遍历。然后在遍历的时候使用 as 对得到的结果进行限制。

尝试写出

type OmitByType<T extends {}, K> = {
  [P in keyof T as T[P] extends K ? P : never]: T[P];
};

测试用例

type OmitBoolean = OmitByType<
  {
    name: string;
    count: number;
    isReadonly: boolean;
    isEnable: boolean;
  },
  boolean
>; // { name: string; count: number }

参考答案

type OmitByType<T, U> = { [K in keyof T as T[K] extends U ? never : K]: T[K] };

经验总结

in keyof as 的搭配。

另外一个类型体操

类型体操题目集合

Mutable

实现一个通用的类型 Mutable,使类型 T 的全部属性可变(非只读)。

interface Todo {
  readonly title: string;
  readonly description: string;
  readonly completed: boolean;
}
type MutableTodo = Mutable<Todo>; // { title: string; description: string; completed: boolean; }

分析

在 Ts 中,我们可以使用 -readonly 来消除原来的只读属性。所以这本质上还是对 object 的遍历。

尝试写出

type Mutable<T extends {}> = {
  -readonly [K in keyof T]: T[K];
};

测试用例

type MutableTodo = Mutable<Todo>; // { title: string; description: string; completed: boolean; }

参考答案

type Mutable<T extends object> = { -readonly [K in keyof T]: T[K] };

经验总结

  1. -readonly

2. 两个 Leetcode 题目

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

2.1 [522] 最长特殊序列 Ⅱ

给定字符串列表 strs ,返回其中 最长的特殊序列 的长度。如果最长特殊序列不存在,返回 -1 。

特殊序列 定义如下:该序列为某字符串 独有的子序列(即不能是其他字符串的子序列)。

 s 的 子序列可以通过删去字符串 s 中的某些字符实现。

例如,"abc" 是 "aebdc" 的子序列,因为您可以删除"aebdc"中的下划线字符来得到 "abc" 。"aebdc"的子序列还包括"aebdc"、 "aeb" 和 "" (空字符串)。


示例 1:

输入: strs = ["aba","cdc","eae"]
输出: 3
示例 2:

输入: strs = ["aaa","aaa","aa"]
输出: -1


提示:

2 <= strs.length <= 50
1 <= strs[i].length <= 10
strs[i] 只包含小写英文字母

尝试实现:

/**
 * @param {string} s
 * @param {string} t
 * @return {boolean}
 */
var isSubsequence = function (s, t) {
  let sIndex = 0;
  let tIndex = 0;

  while (sIndex < s.length && tIndex < t.length) {
    if (s[sIndex] === t[tIndex]) {
      sIndex++;
    }
    tIndex++;
  }

  return sIndex === s.length;
};

/**
 * @param {string[]} strs
 * @return {number}
 */
var findLUSlength = function (strs) {
  for (let i = 0; i < strs.length - 1; i++) {
    const a = strs[i];

    for (let j = i + 1; j < strs.length; j++) {
      const b = strs[j];
      if (a === b) {
        strs[i] = "";
        strs[j] = "";
        continue;
      }

      if (isSubsequence(a, b)) {
        strs[i] = "";
      }

      if (isSubsequence(b, a)) {
        strs[j] = "";
      }
    }
  }

  let max = 0;

  strs.forEach((v) => {
    max = Math.max(max, v.length);
  });

  return max === 0 ? -1 : max;
};

我的思路:

  • 如果 a 是 b 的子序列,则 a 就失去意义,可以将其信息从原数组中擦除。

得分结果: 21.62% 16.54%

总结提升:

  1. 使用双指针方法来提升效率。

2.2 [66] 加一

给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。

最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。

你可以假设除了整数 0 之外,这个整数不会以零开头。



示例 1:

输入:digits = [1,2,3]
输出:[1,2,4]
解释:输入数组表示数字 123。
示例 2:

输入:digits = [4,3,2,1]
输出:[4,3,2,2]
解释:输入数组表示数字 4321。
示例 3:

输入:digits = [0]
输出:[1]


提示:

1 <= digits.length <= 100
0 <= digits[i] <= 9

尝试完成:

/**
 * @param {number[]} digits
 * @return {number[]}
 */
var plusOne = function (digits) {
  function exec(_d, _p) {
    if (_p === -1) {
      _d.unshift(1);
      return;
    }
    if (_d[_p] === 9) {
      _d[_p] = 0;
      exec(_d, _p - 1);
      return;
    }
    _d[_p] = _d[_p] + 1;
    return;
  }

  const n = digits.length;
  exec(digits, n - 1);

  return digits;
};

我的思路: 额外考虑进位以及类似 [9,9,9] 这种会增加最前面 1 的情况。

得分结果: 29.42% 41.65%

2.3 [67] 二进制求和

给你两个二进制字符串 a 和 b ,以二进制字符串的形式返回它们的和。



示例 1:

输入:a = "11", b = "1"
输出:"100"
示例 2:

输入:a = "1010", b = "1011"
输出:"10101"


提示:

1 <= a.length, b.length <= 104
a 和 b 仅由字符 '0' 或 '1' 组成
字符串如果不是 "0" ,就不含前导零

尝试完成:

/**
 * @param {string} a
 * @param {string} b
 * @return {string}
 */
var addBinary = function (a, b) {
  return (BigInt("0b" + a) + BigInt("0b" + b)).toString(2);
};

我的思路:

  1. 在 js 中字符串数字减 0 变成数字这一点对于各个进制都是成立的。
  2. 使用 toString 将数字变成字符串可以是常见的任意进制。
  3. BigInt('0b10') 是可行的,并且没有精度损失。

得分结果: 10.89% 90.85%

总结提升:

  1. '0b11100' - 0 这种操作在数字很大的时候会产生误差,所以使用 BigInt('0b11100') 替换。
  2. BigInt 的包装类型也可以使用 toString 方法转成任意进制的字符串并且没有损失。

2.4 [415] 字符串相加

给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。

你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。



示例 1:

输入:num1 = "11", num2 = "123"
输出:"134"
示例 2:

输入:num1 = "456", num2 = "77"
输出:"533"
示例 3:

输入:num1 = "0", num2 = "0"
输出:"0"




提示:

1 <= num1.length, num2.length <= 104
num1 和num2 都只包含数字 0-9
num1 和num2 都不包含任何前导零

尝试完成:

/**
 * @param {string} num1
 * @param {string} num2
 * @return {string}
 */
var addStrings = function (num1, num2) {
  const rst = [];
  const n1 = num1.split("");
  const n2 = num2.split("");
  const c1 = n1.length;
  const c2 = n2.length;

  function add(rst, i, p) {
    console.log(i);

    const _a = (n1[c1 - 1 - i] ?? "0") - 0 + ((n2[c2 - 1 - i] ?? "0") - 0) + p;
    rst.unshift(_a % 10);
    if (i === Math.max(c1, c2)) return;
    if (_a > 9) {
      add(rst, i + 1, 1);
    } else {
      if (i === Math.max(c1, c2) - 1) return;
      add(rst, i + 1, 0);
    }
    return;
  }

  add(rst, 0, 0);

  return rst.join("");
};

我的思路:

  1. 那就不写这样的答案了 BigInt(num1)+BigInt(num2) + ""
  2. 构造递归定点更新辅助函数 add 处理好边界条件即可。

总结提升: 这是 AI 答案:

/**
 * @param {string} num1
 * @param {string} num2
 * @return {string}
 */
var addStrings = function (num1, num2) {
  const n1 = num1.split("");
  const n2 = num2.split("");
  const maxLength = Math.max(n1.length, n2.length);
  let carry = 0;
  let result = [];

  for (let i = 0; i < maxLength || carry; i++) {
    const digit1 = i < n1.length ? parseInt(n1[n1.length - 1 - i], 10) : 0;
    const digit2 = i < n2.length ? parseInt(n2[n2.length - 1 - i], 10) : 0;
    const sum = digit1 + digit2 + carry;
    result.unshift(sum % 10);
    carry = Math.floor(sum / 10);
  }

  return result.join("");
};

得分结果: 5.07% 5.07%

3. 六个 vue2.x 面试题

  1. vue 中组件 data 为什么是 return ⼀个对象的函数,⽽不是直接是个对象?

如果将 data 定义为对象,这就表示所有的组件实例共⽤了⼀份 data 数据,因此,⽆论在哪个组件实例中修改了 data,都会影响到所有的组件实例。

组件中的 data 写成⼀个函数,数据以函数返回值形式定义,这样每复⽤⼀次组件,就会返回⼀份新的 data,类似于给每个组件实例创建⼀个私有的数据空间,让各个组件实例维护各⾃的数据。⽽单纯的写成对象形式,就使得所有组件实例共⽤了⼀份 data,就会造成⼀个变了全都会变的结果。

  1. Vue 中的 computed 是如何实现的 流程总结如下:
  • 当组件初始化的时候, computed 和 data 会分别建⽴各⾃的响应系统, Observer 遍历 data 中每个属性设置 get/set 数据拦截
  • 初始化 computed 会调⽤ initComputed 函数
  • 注册⼀个 watcher 实例,并在内实例化⼀个 Dep 消息订阅器⽤作后续收集依赖(⽐如渲染函数的 watcher 或者其他观察该计算属性变化的 watcher )
  • 调⽤计算属性时会触发其 Object.defineProperty 的 get 访问器函数
  • 调⽤ watcher.depend() ⽅法向⾃身的消息订阅器 dep 的 subs 中添加其他属性的 watcher
  • 调⽤ watcher 的 evaluate ⽅法(进⽽调⽤ watcher 的 get ⽅法)让⾃身成为其他 watcher 的消息订阅器的订阅者,⾸先将 watcher 赋给 Dep.target ,然后执⾏ getter 求值函数,当访问求值函数⾥⾯的属性(⽐如来⾃ data 、 props 或其他 computed )时,会同样触发它们的 get 访问器函数从⽽将该计算属性的 watcher 添加到求值函数中属性的 watcher 的消息订阅器 dep 中,当这些操作完成,最后关闭 Dep.target 赋为 null 并返回求值函数结果。
  • 当某个属性发⽣变化,触发 set 拦截函数,然后调⽤⾃身消息订阅器 dep 的 notify ⽅法,遍历当前 dep 中保存着所有订阅者 wathcer 的 subs 数组,并逐个调⽤ watcher 的 update ⽅法,完成响应更新。
  1. Vue 的响应式原理
  • Vue 的响应式是通过 Object.defineProperty 对数据进⾏劫持,并结合观察者模式实现。
  • Vue 利⽤ Object.defineProperty 创建⼀个 observe 来劫持监听所有的属性,把这些属性全部转为 getter 和 setter 。
  • Vue 中每个组件实例都会对应⼀个 watcher 实例,它会在组件渲染的过程中把使⽤过的数据属性通过 getter 收集为依赖。之后当依赖项的 setter 触发时,会通知 watcher ,从⽽使它关联的组件重新渲染。
  1. Object.defineProperty 有哪些缺点?
  • Object.defineProperty 只能劫持对象的属性,⽽ Proxy 是直接代理对象由于 Object.defineProperty 只能对属性进⾏劫持,需要遍历对象的每个属性。⽽ Proxy 可以直接代理对象。

  • Object.defineProperty 对新增属性需要⼿动进⾏ Observe , 由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新 增属性再使⽤ Object.defineProperty 进⾏劫持。 也正是因为这个原因,使⽤ Vue 给 data 中的数组或对象新增属性时,需要使⽤ vm.$set 才能保证新增的属性也是响应式的。

  • Proxy ⽀持 13 种拦截操作,这是 defineProperty 所不具有的。

  • 新标准性能红利 Proxy 作为新标准,⻓远来看,JS 引擎会继续优化 Proxy ,但 getter 和 setter 基本不会再有针对性优化。

  • Proxy 兼容性差 ⽬前并没有⼀个完整⽀持 Proxy 所有拦截⽅法的 Polyfill ⽅案

  1. Vue2.0 中如何检测数组变化? Vue 的 Observer 对数组做了单独的处理,对数组的⽅法进⾏编译,并赋值给数组属性的 proto属性上,因为原型链的机制,找到对应的⽅法就不会继续往上找了。编译⽅法中会对⼀些会增加索引的⽅法( push , unshift , splice )进⾏⼿动 observe。

  2. nextTick 是做什么⽤的,其原理是什么?

能回答清楚这道问题的前提,是清楚 EventLoop 过程。

  1. 在下次 DOM 更新循环结束后执⾏延迟回调,在修改数据之后⽴即使⽤ nextTick 来获取更新后的 DOM。
  2. nextTick 对于 micro task 的实现,会先检测是否⽀持 Promise ,不⽀持的话,直接指向 macrotask,⽽ macro task 的实现,优先检测是否⽀持 setImmediate (⾼版本 IE 和 Etage ⽀持),不⽀持的再去检测是否⽀持 MessageChannel,如果仍不⽀持,最终降级为 setTimeout 0;
  3. 默认的情况,会先以 micro task ⽅式执⾏,因为 micro task 可以在⼀次 tick 中全部执⾏完毕,在⼀些有重绘和动画的场景有更好的性能。
  4. 但是由于 micro task 优先级较⾼,在某些情况下,可能会在事件冒泡过程中触发,导致⼀些问题,所以有些地⽅会强制使⽤ macro task (如 v-on )。

注意:之所以将 nextTick 的回调函数放⼊到数组中⼀次性执⾏,⽽不是直接在 nextTick 中执⾏回调函数,是为了保证在同⼀个 tick 内多次执⾏了 nextTcik ,不会开启多个异步任务,⽽是把这些异步任务都压成⼀个同步任务,在下⼀个 tick 内执⾏完毕。

4. 五句英语积累

  1. How do I get to the American Embassy? 我怎么去美国领事馆
  2. How long does it take by car?
  3. How long does it take to get to Shanghai?
  4. How long is the flight?
  5. I want to ask you a question.