每日知识积累 Day 24

115 阅读7分钟

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

1. 一个类型体操

类型体操题目集合 FlatternDepth

递归将数组展开到指定的深度

示例:

type a = FlattenDepth<[1, 2, [3, 4], [[[5]]]], 2>; // [1, 2, 3, 4, [5]]. flattern 2 times
type b = FlattenDepth<[1, 2, [3, 4], [[[5]]]]>; // [1, 2, 3, 4, [[5]]]. Depth defaults to be 1

分析

与完全的展开不同,我们需要展开到指定的深度,也就是我们需要一个计数器,而通常我们使用元组类型的 length 作为计数器,并且使用 extends 判断计数器是否到点。

尝试写出

type Unfold<T extends any[]> = T extends [infer F, ...infer R] ? (
	F extends any[] ? [...F, ...Unfold<R>] : [F, ...Unfold<R>]
) : T;

type FlattenDepth<T extends any[], C extends number = 1, K extends any[] = []> = {
	0: T,
	1: FlattenDepth<Unfold<T>, C, [...K, unknown]>,
}[
	K["length"] extends C ? 0 : 1
]

测试用例

type a = FlattenDepth<[1, 2, [3, 4], [[[5]]]], 2>; // [1, 2, 3, 4, [5]]
type b = FlattenDepth<[1, 2, [3, 4], [[[5]]]]>; // [1, 2, 3, 4, [[5]]]

参考答案

type FlattenDepth<
    T extends any[],
    Depth extends number = 1,
    Acc extends any[] = []
> = T extends [infer L, ...infer R]
    ? L extends any[]
        ? Acc['length'] extends Depth
            ? T
            : [
                  ...FlattenDepth<L, Depth, [any, ...Acc]>,
                  ...FlattenDepth<R, Depth, Acc>
              ]
        : [L, ...FlattenDepth<R, Depth, Acc>]
    : T;

经验总结

  1. 可以先分开写,然后设法组装:
type FlattenDepth<T extends any[], C extends number = 1, K extends any[] = []> = {
	0: T,
	1: FlattenDepth<(T extends [infer F, ...infer R] ?
		(F extends any[] ? [...F, ...Unfold<R>] : [F, ...Unfold<R>])
		: T), C, [...K, unknown]>,
}[
	K["length"] extends C ? 0 : 1
]

另外一个类型体操

类型体操题目集合

FlipArguments

实现 lodash 中_.flip方法的类型版本

FlipArguments<T>类型接收泛型参数 T 并返回一个函数类型, 且此函数类型有和 T 相同的返回类型但其参数的顺序是倒过来的

type Flipped = FlipArguments<(arg0: string, arg1: number, arg2: boolean) => void>; // (arg0: boolean, arg1: number, arg2: string) => void

分析

在 ts 中,函数签名的形参名不重要,重要的是形参的类型;所以先提取 args 然后将其 reverse 即可。

尝试写出

type Reverse<T extends any[]> = T extends [infer F, ...infer R] ? [...Reverse<R>, F] : T;

type FlipArguments< T extends (...args: any[]) => any > = T extends (...args: infer A) => infer R ? (
	(...args: Reverse<A>) => R
) : never;

测试用例

type Flipped = FlipArguments<(arg0: string, arg1: number, arg2: boolean) => void>; // (arg0: boolean, arg1: number, arg2: string) => void

参考答案

type FlipArray<T extends any[]> = T extends [...infer L, infer R]
    ? [R, ...FlipArray<L>]
    : T;
type FlipArguments<T extends Function> = T extends (
    ...a: infer R
) => infer TReturn
    ? (...a: FlipArray<R>) => TReturn
    : never;

经验总结

  1. 构造的辅助函数被递归了,因此很难将它们写在一起去。

2. 两个 Leetcode 题目

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

2.1 [273] 整数转换英文表示

将非负整数 num 转换为其对应的英文表示。

 

示例 1:

输入:num = 123
输出:"One Hundred Twenty Three"
示例 2:

输入:num = 12345
输出:"Twelve Thousand Three Hundred Forty Five"
示例 3:

输入:num = 1234567
输出:"One Million Two Hundred Thirty Four Thousand Five Hundred Sixty Seven"
 

提示:

0 <= num <= 231 - 1

尝试实现:

/**
 * @param {number} num
 * @return {string}
 */
var numberToWords = function (num) {
  if (num === 0) return 'Zero';
  if (num === 10) return 'Ten';
  const b = Math.floor(num / 10 ** 9);
  if (b) num -= b * 10 ** 9;
  const m = Math.floor((num) / 10 ** 6);
  if (m) num -= m * 10 ** 6;
  const t = Math.floor((num) / 10 ** 3);
  if (t) num -= t * 10 ** 3;
  const g = num;
  const red = {
    "Billion": b,
    "Million": m,
    "Thousand": t,
    "-1": g,
  }

  const digit = {
    0:"",
    1: "One",
    2: "Two",
    3: "Three",
    4: "Four",
    5: "Five",
    6: "Six",
    7: "Seven",
    8: "Eight",
    9: "Nine",
  }

  const digit2 = {
    0:"",
    1: "One",
    2: "Twenty",
    3: "Thirty",
    4: "Forty",
    5: "Fifty",
    6: "Sixty",
    7: "Seventy",
    8: "Eighty",
    9: "Ninety",
  }

  const digit3 = {
    10: "Ten",
    11: "Eleven",
    12: "Twelve",
    13: "Thirteen",
    14: "Fourteen",
    15: "Fifteen",
    16: "Sixteen",
    17: "Seventeen",
    18: "Eighteen",
    19: "Nineteen",
  }

  function getString(n) {
    let _r = "";
    const _b = Math.floor(n / 10 ** 2);

    if (_b) n -= _b * 10 ** 2;
    const _s = Math.floor(n / 10);
    if (_s) n -= _s * 10;
    const _g = n;

    if (_b) _r += ` ${digit[_b]} Hundred`;

    if (_s === 1) {
      _r += ` ${digit3[_s * 10 + _g]}`
    } else {
      if (_s) _r += ` ${digit2[_s]}`;
      if (_g) _r += ` ${digit[_g]}`;
    }


    return _r;
  }

  let rst = "";
  for (let i in red) {
    if (red[i] !== 0) {
      if (i == -1) {
        rst += getString(red[i]);
      } else {
        rst += getString(red[i]) + " " + i;
      }
    }


  }

  return rst.trim();
};

我的思路:

  1. 发音规则是三个数组为一组,最后加上单位即可
  2. 注意最大数范围是 2^31 - 1 在 Billion 范围之内
  3. [] hundred [] [单位]
  4. 首先将数组分成三个一组

得分结果: 12.50% 56.25%

总结提升:

  1. Ten 是易错点。
  2. 用下面的方法取出数位数字来:
  const b = Math.floor(num / 10 ** 9);
  if (b) num -= b * 10 ** 9;
  const m = Math.floor((num) / 10 ** 6);
  if (m) num -= m * 10 ** 6;
  const t = Math.floor((num) / 10 ** 3);
  if (t) num -= t * 10 ** 3;
  const g = num;

2.2 [165] 比较版本号

给你两个 版本号字符串 version1 和 version2 ,请你比较它们。版本号由被点 '.' 分开的修订号组成。修订号的值 是它 转换为整数 并忽略前导零。

比较版本号时,请按 从左到右的顺序 依次比较它们的修订号。如果其中一个版本字符串的修订号较少,则将缺失的修订号视为 0。

返回规则如下:

如果 version1 < version2 返回 -1,
如果 version1 > version2 返回 1,
除此之外返回 0。
 

示例 1:

输入:version1 = "1.2", version2 = "1.10"

输出:-1

解释:

version1 的第二个修订号为 "2",version2 的第二个修订号为 "10":2 < 10,所以 version1 < version2。

示例 2:

输入:version1 = "1.01", version2 = "1.001"

输出:0

解释:

忽略前导零,"01" 和 "001" 都代表相同的整数 "1"。

示例 3:

输入:version1 = "1.0", version2 = "1.0.0.0"

输出:0

解释:

version1 有更少的修订号,每个缺失的修订号按 "0" 处理。

 

提示:

1 <= version1.length, version2.length <= 500
version1 和 version2 仅包含数字和 '.'
version1 和 version2 都是 有效版本号
version1 和 version2 的所有修订号都可以存储在 32 位整数 中

尝试完成:

/**
 * @param {string} version1
 * @param {string} version2
 * @return {number}
 */
var compareVersion = function(version1, version2) {
    const arr1 = version1.split(".").map(v=>parseInt(v));
    const arr2 = version2.split(".").map(v=>parseInt(v));
    const n = Math.max(arr1.length,arr2.length);

    for(let i = 0; i < n; i++) {
        const _a = arr1[i] ?? 0;
        const _b = arr2[i] ?? 0;
    
        if(_a>_b) return 1;
        if(_a<_b) return -1;
    }

    return 0;
};

我的思路:

使用 dot 作为分隔符化成数组,然后逐个比较,如果是 undefined 则当成 0 对待即可。

得分结果: 78.50% 8.83%

3. 六个 vue2.x 面试题

  1. vue2.0组件通信⽅式有哪些?
  • ⽗⼦组件通信:

    • props 和 event、v-model、 .sync、 ref、 parentparent 和 children
  • 非⽗⼦组件通信:

    • attrattr 和 listeners、 provide 和 inject、eventbus、通过根实例$root访问、vuex、dispatch 和 brodcast
  1. v-model是如何实现双向绑定的?
  • vue 2.0

    • v-model 是⽤来在表单控件或者组件上创建双向绑定的,他的本质是 v-bind 和 v-on 的语法糖,在⼀个组件上使⽤ v-model ,默认会为组件绑定名为 value 的 prop 和名为 input 的事件。
  • Vue3.0

    • 在 3.x 中,⾃定义组件上的 v-model 相当于传递了 modelValue prop 并接收抛出的 update:modelValue 事件
  1. Vuex和单纯的全局对象有什么区别?
  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发⽣变化,那么相应的组件也会相应地得到⾼效更新。
  • 不能直接改变 store 中的状态。改变 store 中的状态的唯⼀途径就是显式地提交 (commit)mutation。这样使得我们可以⽅便地跟踪每⼀个状态的变化,从⽽让我们能够实现⼀些⼯具帮助我们更好地了解我们的应⽤。
  1. Vue 的⽗组件和⼦组件⽣命周期钩⼦执⾏顺序是什么?
  • 渲染过程:

    • ⽗组件挂载完成⼀定是等⼦组件都挂载完成后,才算是⽗组件挂载完,所以⽗组件的mounted在⼦组件mouted之后
    • ⽗beforeCreate -> ⽗created -> ⽗beforeMount -> ⼦beforeCreate -> ⼦created -> ⼦beforeMount -> ⼦mounted -> ⽗mounted
  • ⼦组件更新过程:

    • 影响到⽗组件: ⽗beforeUpdate -> ⼦beforeUpdate->⼦updated -> ⽗updted
    • 不影响⽗组件: ⼦beforeUpdate -> ⼦updated
  • ⽗组件更新过程:

    • 影响到⼦组件: ⽗beforeUpdate -> ⼦beforeUpdate->⼦updated -> ⽗updted
    • 不影响⼦组件: ⽗beforeUpdate -> ⽗updated
  • 销毁过程:

    • ⽗beforeDestroy -> ⼦beforeDestroy -> ⼦destroyed -> ⽗destroyed

看起来很多好像很难记忆,其实只要理解了,不管是哪种情况,都⼀定是⽗组件等待⼦组件完成后,才会执⾏⾃⼰对应完成的钩⼦,就可以很容易记住。

  1. v-show 和 v-if 有哪些区别?
  • v-if 会在切换过程中对条件块的事件监听器和⼦组件进⾏销毁和重建,如果初始条件是false,则什么都不做,直到条件第⼀次为true时才开始渲染模块。
  • v-show 只是基于css进⾏切换,不管初始条件是什么,都会渲染。

所以, v-if 切换的开销更⼤,⽽ v-show 初始化渲染开销更⼤,在需要频繁切换,或者切换的部分dom很复杂时,使⽤ v-show 更合适。渲染后很少切换的则使⽤ v-if 更合适。

  1. Vue 中 v-html 会导致什么问题 在⽹站上动态渲染任意 HTML,很容易导致 XSS 攻击。所以只能在可信内容上使⽤ v-html,且永远不能⽤于⽤户提交的内容上。

4. 五句英语积累

  1. At what time? 几点钟
  2. Boarding is at gate 23. 登机口在 23 号
  3. Can you help me?
  4. Does this role go to Paris? 这是去巴黎的路吗
  5. Do you know what this says? 你知道这是什么意思吗?