每日的知识积累,包括 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] };
经验总结
-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%
总结提升:
- 使用双指针方法来提升效率。
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);
};
我的思路:
- 在 js 中字符串数字减 0 变成数字这一点对于各个进制都是成立的。
- 使用 toString 将数字变成字符串可以是常见的任意进制。
BigInt('0b10')
是可行的,并且没有精度损失。
得分结果: 10.89% 90.85%
总结提升:
'0b11100' - 0
这种操作在数字很大的时候会产生误差,所以使用BigInt('0b11100')
替换。- 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("");
};
我的思路:
- 那就不写这样的答案了 BigInt(num1)+BigInt(num2) + ""
- 构造递归定点更新辅助函数 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 面试题
- vue 中组件 data 为什么是 return ⼀个对象的函数,⽽不是直接是个对象?
如果将 data 定义为对象,这就表示所有的组件实例共⽤了⼀份 data 数据,因此,⽆论在哪个组件实例中修改了 data,都会影响到所有的组件实例。
组件中的 data 写成⼀个函数,数据以函数返回值形式定义,这样每复⽤⼀次组件,就会返回⼀份新的 data,类似于给每个组件实例创建⼀个私有的数据空间,让各个组件实例维护各⾃的数据。⽽单纯的写成对象形式,就使得所有组件实例共⽤了⼀份 data,就会造成⼀个变了全都会变的结果。
- 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 ⽅法,完成响应更新。
- Vue 的响应式原理
- Vue 的响应式是通过 Object.defineProperty 对数据进⾏劫持,并结合观察者模式实现。
- Vue 利⽤ Object.defineProperty 创建⼀个 observe 来劫持监听所有的属性,把这些属性全部转为 getter 和 setter 。
- Vue 中每个组件实例都会对应⼀个 watcher 实例,它会在组件渲染的过程中把使⽤过的数据属性通过 getter 收集为依赖。之后当依赖项的 setter 触发时,会通知 watcher ,从⽽使它关联的组件重新渲染。
- 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 ⽅案
-
Vue2.0 中如何检测数组变化? Vue 的 Observer 对数组做了单独的处理,对数组的⽅法进⾏编译,并赋值给数组属性的 proto属性上,因为原型链的机制,找到对应的⽅法就不会继续往上找了。编译⽅法中会对⼀些会增加索引的⽅法( push , unshift , splice )进⾏⼿动 observe。
-
nextTick 是做什么⽤的,其原理是什么?
能回答清楚这道问题的前提,是清楚 EventLoop 过程。
- 在下次 DOM 更新循环结束后执⾏延迟回调,在修改数据之后⽴即使⽤ nextTick 来获取更新后的 DOM。
- nextTick 对于 micro task 的实现,会先检测是否⽀持 Promise ,不⽀持的话,直接指向 macrotask,⽽ macro task 的实现,优先检测是否⽀持 setImmediate (⾼版本 IE 和 Etage ⽀持),不⽀持的再去检测是否⽀持 MessageChannel,如果仍不⽀持,最终降级为 setTimeout 0;
- 默认的情况,会先以 micro task ⽅式执⾏,因为 micro task 可以在⼀次 tick 中全部执⾏完毕,在⼀些有重绘和动画的场景有更好的性能。
- 但是由于 micro task 优先级较⾼,在某些情况下,可能会在事件冒泡过程中触发,导致⼀些问题,所以有些地⽅会强制使⽤ macro task (如 v-on )。
注意:之所以将 nextTick 的回调函数放⼊到数组中⼀次性执⾏,⽽不是直接在 nextTick 中执⾏回调函数,是为了保证在同⼀个 tick 内多次执⾏了 nextTcik ,不会开启多个异步任务,⽽是把这些异步任务都压成⼀个同步任务,在下⼀个 tick 内执⾏完毕。
4. 五句英语积累
- How do I get to the American Embassy? 我怎么去美国领事馆
- How long does it take by car?
- How long does it take to get to Shanghai?
- How long is the flight?
- I want to ask you a question.