• 作者:老汪软件技巧
  • 发表时间:2024-09-16 07:02
  • 浏览量:

每日的知识积累,包括 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 TupleToNestedObjectextends 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}
    ) : 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
            : [
                  ...FlattenDepthDepth, [any, ...Acc]>,
                  ...FlattenDepthDepth, Acc>
              ]
        : [L, ...FlattenDepthDepth, 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 ObjectEntriesextends {}, 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;

经验总结如果想要表达 infer P extends keyof M 这样的意思,那么正确的做法应该是:

type Cextends {}, 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;
};

我的思路:

得分结果: 40% 30%

总结提升:

有点反向思维的意思。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%

总结提升:

字符串没有 find 或者 findIndex 的方法,所以需要先转换。截断使用的是 .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] ?? "";
};

我的思路:

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

得分结果: 6.50% 5.69%

总结提升:

字符串没有 find 或者 findIndex 的方法,所以需要先转换。截断使用的是 .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);
};

我的思路:

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

得分结果: 97.72% 6.00%

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

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

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

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

vue-router hash 模式和 history 模式有什么区别?vue-router hash 模式和 history 模式是如何实现的?

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

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

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

vue3.0 相对于 vue2.x 有哪些变化?你能讲⼀讲MVVM吗?

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

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

4. 五句英语积累Follow me.Go straight ahead.Have you arrived?Have you been to Thailand?How do I get there? 我该怎么那儿?