每日知识积累 Day 26

116 阅读10分钟

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

1. 一个类型体操

类型体操题目集合 TupleToNestedObject

给定一个只包含字符串类型的元组类型T和一个类型 U, 递归地构建一个对象.

type a = TupleToNestedObject<['a'], string>; // {a: string}
type b = TupleToNestedObject<['a', 'b'], number>; // {a: {b: number}}
type c = TupleToNestedObject<[], boolean>; // boolean. if the tuple is empty, just return the U type

分析

这个本质上也是遍历数组,然后出口就是数组长度等于 0 的时候。

尝试写出

type TupleToNestedObject<T extends any[], K> = {
    0: T extends [infer A] ? (
        {[X in A as PropertyKey]: K}
    ) : never,
    1: T extends [infer A, ...infer B] ? (
        {[X in A as PropertyKey]: TupleToNestedObject<B, K>}
    ) : K,
}[
    T["length"] extends 1 ? 0 : 1
]

测试用例

type a = TupleToNestedObject<['a'], string>; // {a: string}
type b = TupleToNestedObject<['a', 'b'], number>; // {a: {b: number}}
type c = TupleToNestedObject<[], boolean>; // boolean

参考答案

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;

经验总结

这道题的卡点在于,如何将元组中提取出来的元素类型作为 Object 的键使用,在上面的代码中,如果 infer A 是当前元组中的第一个元素,那么要想让其作为 Object 的 key 则必须写成:[X in A as PropertyKey]: any 这里用到 in 和 as 两个操作符,千万不能写成 [A]: any 或者 [X in keyof A]: any

另外一个类型体操

类型体操题目集合

ObjectEntries

实现 Object.entries 的类型版本

interface Model {
    name: string;
    age: number;
    locations: string[] | null;
}
type modelEntries = ObjectEntries<Model>; // ['name', string] | ['age', number] | ['locations', string[] | null];

分析

利用联合类型的自动组装特性,只要完成一个,则 Ts 自动遍历完成剩余部分。

尝试写出

type ObjectEntries<M extends {}, P extends keyof M  = keyof M> = P extends P ? [P, M[P]] : never

测试用例

type modelEntries = ObjectEntries<Model>; // ['name', string] | ['age', number] | ['locations', string[] | null];

参考答案

type ObjectEntries<
    T,
    R extends keyof T = keyof T,
    RequiredT = { [K in keyof T]-?: T[K] }
> = R extends keyof RequiredT
    ? [R, [RequiredT[R]] extends [never] ? undefined : RequiredT[R]]
    : never;

经验总结

  1. 如果想要表达 infer P extends keyof M 这样的意思,那么正确的做法应该是:
type C<M extends {}, P extends keyof M = keyof M> = P ...

也就是说,P 的定义是在左边而不是右边。 2. 如果想要明确的告诉 ts 在某个地方联合类型使用其当前值而无需探索所有可能,则写成:P extends P. 3. 答案中的 -?: 是什么意思呢?

2. 两个 Leetcode 题目

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

2.1 [481] 神奇字符串

神奇字符串 s 仅由 '1' 和 '2' 组成,并需要遵守下面的规则:

神奇字符串 s 的神奇之处在于,串联字符串中 '1' 和 '2' 的连续出现次数可以生成该字符串。
s 的前几个元素是 s = "1221121221221121122……" 。如果将 s 中连续的若干 1 和 2 进行分组,可以得到 "1 22 11 2 1 22 1 22 11 2 11 22 ......" 。每组中 1 或者 2 的出现次数分别是 "1 2 2 1 1 2 1 2 2 1 2 2 ......" 。上面的出现次数正是 s 自身。

给你一个整数 n ,返回在神奇字符串 s 的前 n 个数字中 1 的数目。

 

示例 1:

输入:n = 6
输出:3
解释:神奇字符串 s 的前 6 个元素是 “122112”,它包含三个 1,因此返回 3 。 
示例 2:

输入:n = 1
输出:1
 

提示:

1 <= n <= 105

尝试实现:

/**
 * @param {number} n
 * @return {number}
 */
var magicalString = function(n) {
    if(n===1 || n===2) return 1;
    let s = "12";

    while(s.length < n) {
        let newS = "";
        for(let i = 0; i < s.length; i++) {
            const cur = s[i];
            const num = i % 2 === 0 ? 1 : 2;
            newS += (num+"").repeat(cur);
        }

        s = newS;
    }

    return s.slice(0,n).split("").filter(v=>v==='1').length;
};

我的思路:

  • 首先不管思路好不好,也不论复杂度的问题,首先先把问题解决了再说
  • 根据题目的意思,这样的神奇字符串在 n 确定的时候是确定的
  • 所以我们需要找到神奇字符串的前 n 个数,然后统计 1 的个数
  • 通过递归算出神奇字符串的指定几位

得分结果: 40% 30%

总结提升:

  1. 有点反向思维的意思。

2.2 [392] 判断子序列

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

进阶:

如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

致谢:

特别感谢 @pbrother 添加此问题并且创建所有测试用例。

 

示例 1:

输入:s = "abc", t = "ahbgdc"
输出:true
示例 2:

输入:s = "axc", t = "ahbgdc"
输出:false
 

提示:

0 <= s.length <= 100
0 <= t.length <= 10^4
两个字符串都只由小写字符组成。

尝试完成:

/**
 * @param {string} s
 * @param {string} t
 * @return {boolean}
 */
var isSubsequence = function(s, t) {
    for(let i = 0; i < s.length; i++) {
        const idx = t.split("").findIndex(
            v => v === s[i]
        );

        if(idx < 0 ) return false;

        t = t.slice(idx+1);
    }

    return true;
};

我的思路: 使用 slice 可以保证顺序。

得分结果: 76.24% 13.85%

总结提升:

  1. 字符串没有 find 或者 findIndex 的方法,所以需要先转换。
  2. 截断使用的是 .slice(idx+1) 而不是 .slice(idx).

2.3 [524] 通过删除字母匹配到字典里最长单词

给你一个字符串 s 和一个字符串数组 dictionary ,找出并返回 dictionary 中最长的字符串,该字符串可以通过删除 s 中的某些字符得到。

如果答案不止一个,返回长度最长且字母序最小的字符串。如果答案不存在,则返回空字符串。

 

示例 1:

输入:s = "abpcplea", dictionary = ["ale","apple","monkey","plea"]
输出:"apple"
示例 2:

输入:s = "abpcplea", dictionary = ["a","b","c"]
输出:"a"
 

提示:

1 <= s.length <= 1000
1 <= dictionary.length <= 1000
1 <= dictionary[i].length <= 1000
s 和 dictionary[i] 仅由小写英文字母组成

尝试完成:

/**
 * @param {string} s
 * @param {string} t
 * @return {boolean}
 */
var isSubsequence = function(s, t) {
    for(let i = 0; i < s.length; i++) {
        const idx = t.split("").findIndex(
            v => v === s[i]
        );

        if(idx < 0 ) return false;

        t = t.slice(idx+1);
    }

    return true;
};

/**
 * @param {string} s
 * @param {string[]} dictionary
 * @return {string}
 */
var findLongestWord = function(s, dictionary) {
    let rst = [];
    let max = 0;

    for(let i = 0; i < dictionary.length; i++) {
        const t = dictionary[i];
        if(isSubsequence(t,s)){
            if(t.length > max){
                rst = [t];
                max = t.length;
            } else if (t.length === max) {
                rst.push(t);
            }
        }
    }

    rst.sort();
    return rst[0] ?? "";
};

我的思路:

  1. 就是比上一个题目多了一个步骤而已。
  2. 字母序最小的字符串 指的就是使用 sort 排序之后的第一个元素。
  3. 要看清楚题目,不存在的时候返回空字符串,像这种边界条件要主动去找而不是说没有看见。

得分结果: 6.50% 5.69%

总结提升:

  1. 字符串没有 find 或者 findIndex 的方法,所以需要先转换。
  2. 截断使用的是 .slice(idx+1) 而不是 .slice(idx).
/**
 * @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} s
 * @param {string[]} dictionary
 * @return {string}
 */
var findLongestWord = function(s, dictionary) {
    let longestWord = "";

    for (let i = 0; i < dictionary.length; i++) {
        const t = dictionary[i];
        if (isSubsequence(t, s)) {
            if (t.length > longestWord.length) {
                longestWord = t;
            } else if (t.length === longestWord.length) {
                // 如果有多个最长单词,可以选择保留字典序最小的,或者根据需要进行其他处理
                longestWord = longestWord < t ? longestWord : t;
            }
        }
    }

    return longestWord;
};

2.4 [521] 最长特殊序列Ⅰ

给你两个字符串 a 和 b,请返回 这两个字符串中 最长的特殊序列  的长度。如果不存在,则返回 -1 。

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

字符串 s 的子序列是在从 s 中删除任意数量的字符后可以获得的字符串。

例如,"abc" 是 "aebdc" 的子序列,因为删除 "aebdc" 中斜体加粗的字符可以得到 "abc" 。 "aebdc" 的子序列还包括 "aebdc" 、 "aeb" 和 "" (空字符串)。
 

示例 1:

输入: a = "aba", b = "cdc"
输出: 3
解释: 最长特殊序列可为 "aba" (或 "cdc"),两者均为自身的子序列且不是对方的子序列。
示例 2:

输入:a = "aaa", b = "bbb"
输出:3
解释: 最长特殊序列是 "aaa" 和 "bbb" 。
示例 3:

输入:a = "aaa", b = "aaa"
输出:-1
解释: 字符串 a 的每个子序列也是字符串 b 的每个子序列。同样,字符串 b 的每个子序列也是字符串 a 的子序列。
 

提示:

1 <= a.length, b.length <= 100
a 和 b 由小写英文字母组成

尝试完成:

/**
 * @param {string} s
 * @param {string} t
 * @return {boolean}
 */
var isSubsequence = function (s, t) {
  for (let i = 0; i < s.length; i++) {
    const idx = t.split("").findIndex((v) => v === s[i]);

    if (idx < 0) return false;

    t = t.slice(idx + 1);
  }

  return true;
};

/**
 * @param {string} a
 * @param {string} b
 * @return {number}
 */
var findLUSlength = function (a, b) {
  const rst = [];
  if (!isSubsequence(a, b)) rst.push(a);
  if (!isSubsequence(b, a)) rst.push(b);
  console.log(rst);

  if (rst.length === 0) return -1;
  if (rst.length === 1) return rst[0].length;
  if (rst.length === 2) return Math.max(a.length, b.length);
};

我的思路:

  1. 首先搞明白,如果 a 是 b 的子序列,则 a 的任意子序列也是 b 的子序列,反过来也是一样的。
  2. 因此题目让找最长的,那么检查 a 和 b 相互是否为对方的子序列;如果都是则说明 a === b 此时返回 -1.
  3. 如果 a 是 b 子序列并且 b 不是 a 的,则返回 b 即可;反过来也是一样的。
  4. 如果 a, b 都不是对方子序列,则返回它们中间最长的那个。

得分结果: 97.72% 6.00%

3. 六个 vue2.x 面试题

  1. v-for 中 key 的作⽤是什么? key 是给每个 vnode 指定的唯⼀ id ,在同级的 vnode diff 过程中,可以根据 key 快速的对⽐,来判断是否为相同节点,并且利⽤ key 的唯⼀性可以⽣成 map 来更快的获取相应的节点。

另外指定 key 后,就不再采⽤“就地复⽤”策略了,可以保证渲染的准确性。

  1. 为什么 v-for 和 v-if 不建议⽤在⼀起

当 v-for 和 v-if 处于同⼀个节点时, v-for 的优先级⽐ v-if 更⾼,这意味着 v-if 将分别重复运⾏于每个 v-for 循环中。如果要遍历的数组很⼤,⽽真正要展示的数据很少时,这将造成很⼤的性能浪费。 这种场景建议使⽤ computed ,先对数据进⾏过滤。

  1. vue-router hash 模式和 history 模式有什么区别?
  • url 展示上,hash 模式有 "#",history 模式没有.
  • 刷新⻚⾯时,hash 模式可以正常加载到 hash 值对应的⻚⾯,⽽ history 没有处理的话,会返回404,⼀般需要后端将所有⻚⾯都配置重定向到⾸⻚路由。
  • 兼容性。hash 可以⽀持低版本浏览器和 IE
  1. vue-router hash 模式和 history 模式是如何实现的?
  • hash 模式:

后⾯ hash 值的变化,不会导致浏览器向服务器发出请求,浏览器不发出请求,就不会刷新⻚⾯。同时通过监听 hashchange 事件可以知道 hash 发⽣了哪些变化,然后根据 hash 变化来实现更新⻚⾯部分内容的操作。

  • history 模式:

history 模式的实现,主要是 HTML5 标准发布的两个 API, pushState 和 replaceState ,这两个 API 可以在改变 url,但是不会发送请求。这样就可以监听 url 变化来实现更新⻚⾯部分内容的操作。

所以,不向后端发送请求 是最关键的。

  1. vue3.0 相对于 vue2.x 有哪些变化?
  • 监测机制的改变(Object.defineProperty —> Proxy)
  • 模板
  • 对象式的组件声明⽅式 (class)
  • 使⽤ts
  • 其它⽅⾯的更改:⽀持⾃定义渲染器、 ⽀持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件、基于 treeshaking 优化,提供了更多的内置功能
  1. 你能讲⼀讲MVVM吗?

MVVM是 Model-View-ViewModel 缩写,也就是把 MVC 中的 Controller 演变成 ViewModel。

Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并⾃动将数据渲染到⻚⾯中,视图变化的时候会通知viewModel层更新数据。

4. 五句英语积累

  1. Follow me.
  2. Go straight ahead.
  3. Have you arrived?
  4. Have you been to Thailand?
  5. How do I get there? 我该怎么那儿?