每日的知识积累,包括 1 个 Ts 类型体操,两个 Leetcode 算法题,三个前端八股文题,四个英语表达积累。
1. 一个类型体操
类型体操题目集合 Readonly 2
实现一个通用 MyReadonly2<T, K>,它带有两种类型的参数 T 和 K。
K 指定应设置为 Readonly 的 T 的属性集。如果未提供 K,则应使所有属性都变为只读,就像普通的 Readonly一样。
例如:
interface Todo {
title: string;
description: string;
completed: boolean;
}
const todo: MyReadonly2<Todo, "title" | "description"> = {
title: "Hey",
description: "foobar",
completed: false,
};
todo.title = "Hello"; // Error: cannot reassign a readonly property
todo.description = "barFoo"; // Error: cannot reassign a readonly property
todo.completed = true; // OK
分析
很容易想到就是将原来的键分成两组,一组是 K 中的,一组不是 K 中的,关键在于如何将这分开的两部分合起来:使用 &.
尝试写出
type MyExclude<T, U> = T extends U ? never : T;
type MyReadonly2<T extends {}, K extends keyof T> = {
readonly [P2 in K]: T[P2];
} & {
[P1 in MyExclude<keyof T, K>]: T[P1];
};
简写方式
我们可以使用断言的方式筛选 P in keyof T 中的键:
type MyReadonly2<T extends {}, K extends keyof T> = {
readonly [P2 in K]: T[P2];
} & {
[P1 in keyof T as P1 extends K ? never : P1]: T[P1];
};
测试用例
type MyReadonly2<T extends {}, K extends keyof T> = {
readonly [P2 in K]: T[P2];
} & {
[P1 in MyExclude<keyof T, K>]: T[P1];
};
type C = MyReadonly2<Todo, "title">;
/*
type C = {
readonly title: string;
} & {
description: string;
completed: boolean;
}
*/
参考答案
type MyReadonly2<
T,
K extends keyof T = keyof T,
O extends keyof T = keyof T
> = { readonly [P in keyof T]: T[P] } & {
[P in O extends K ? never : O]: T[P];
};
经验总结
- 使用 & 链接多个 {} 类型;
- 使用 as 做范围限制,而不只是使用 extends.
2. 两个 Leetcode 题目
刷题的顺序参考这篇文章 LeeCode 刷题顺序
2.1 [189] 轮转数组
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]
提示:
1 <= nums.length <= 105
-231 <= nums[i] <= 231 - 1
0 <= k <= 105
进阶:
尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。
你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?
尝试实现:
/**
* @param {number[]} nums
* @param {number} k
* @return {void} Do not return anything, modify nums in-place instead.
*/
var rotate = function (nums, k) {
const rst = [];
const n = nums.length;
for (let i = 0; i < n; i++) {
rst[(i + k) % n] = nums[i];
}
for (let i = 0; i < n; i++) {
nums[i] = rst[i];
}
return nums;
};
/**
* @param {number[]} nums
* @param {number} k
* @return {void} Do not return anything, modify nums in-place instead.
*/
var rotate = function (nums, k) {
const n = nums.length;
k = k % n;
if (k === 0) return nums;
for (let i = 0; i < n; i++) {
const _i = (i + k) % n;
const num = nums[i]; // 当前值
const target = nums[_i];
if (typeof num === "string") {
// 提取源数据,还原数字
const [a, b] = num.split("&");
nums[i] = parseInt(a);
if (typeof target === "undefined") {
nums[_i] = parseInt(b);
} else {
nums[_i] = parseInt(b) + "&" + target;
}
} else {
// 未被遍历过
if (typeof target === "undefined") {
nums[_i] = nums[i];
} else {
nums[_i] = nums[i] + "&" + target;
}
nums[i] = undefined;
}
}
return nums;
};
我的思路:
- 首先如果使用辅助数组来做的话,这道题基本没有难度;题目本意是在 nums 上调整顺序
- 假如 k = 3, 当 i = 0 的时候,nums[0] 要到 nums[3] 位置,但是 nums[3] 怎么办
- 我们跳出 number 类型,使用不同类型表示是否遍历或者是否修改过
- 获取当前的元素,如果此元素为 number 类型的,则检查其后 k 位置,如果 k 位置元素为 undefined 则直接赋值;如果为 string 类型
得分结果: 5.06% 5.00%
总结提升:
- 对于这种环状题目,需要取余以实现最小的完成时间并且防止潜在的溢出错误。例如本题中,我们可以先确保
k<n以及当 k=0 或者 n=1 的时候直接输出。 - 将数组每个元素向后移动 i 个元素可写成
[...nums, ...nums].slice(n - i, 2 * n - i)
2.2 [396] 旋转函数
给定一个长度为 n 的整数数组 nums 。
假设 arrk 是数组 nums 顺时针旋转 k 个位置后的数组,我们定义 nums 的 旋转函数 F 为:
F(k) = 0 * arrk[0] + 1 * arrk[1] + ... + (n - 1) * arrk[n - 1]
返回 F(0), F(1), ..., F(n-1)中的最大值 。
生成的测试用例让答案符合 32 位 整数。
示例 1:
输入: nums = [4,3,2,6]
输出: 26
解释:
F(0) = (0 * 4) + (1 * 3) + (2 * 2) + (3 * 6) = 0 + 3 + 4 + 18 = 25
F(1) = (0 * 6) + (1 * 4) + (2 * 3) + (3 * 2) = 0 + 4 + 6 + 6 = 16
F(2) = (0 * 2) + (1 * 6) + (2 * 4) + (3 * 3) = 0 + 6 + 8 + 9 = 23
F(3) = (0 * 3) + (1 * 2) + (2 * 6) + (3 * 4) = 0 + 2 + 12 + 12 = 26
所以 F(0), F(1), F(2), F(3) 中的最大值是 F(3) = 26 。
示例 2:
输入: nums = [100]
输出: 0
提示:
n == nums.length
1 <= n <= 105
-100 <= nums[i] <= 100
尝试完成:
/**
* @param {number[]} nums
* @return {number}
*/
var maxRotateFunction = function (nums) {
const sum = _.sum(nums);
function calc(arr) {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += i * arr[i];
}
return sum;
}
const n = nums.length;
let f = calc(nums);
let max = f;
if (n < 2) return f;
for (let i = 1; i < n; i++) {
f += sum - n * nums[n - i];
max = max < f ? f : max;
}
return max;
};
我的思路:
- 这个题不能使用穷举法,会超时的;
- 说到底这是一个算法题,并不只是编程题,所以当我们要进行很多超时操作的时候,我应该尝试使用迭代的算法,从一个耗时操作得到另外一个,而不是从头算起。
- 假如 i 为 1, 当我表示倒数第一个元素的时候,我不应该使用 nums[-i] 而是应该使用 nums[n-i]
得分结果: 44.44% 66.67%
3. 三个前端题目
- 对比操作符 ||, && 和 ??
- 共同点:
- 这三个操作符都具有短路的特性
??可以看成是||的收紧版本,||对 js 中的假值短路生效,而??只对undefined和null生效(也就是说console.log(0 ?? 20)的执行结果为 0 而不是 20)
- 不同点:
x||y的特性是,x 为假值(或者 x 表达式的返回值为假值)的时候,返回 y 的值(或者 y 表达式的返回值);但是 x 为真值(或者 x 表达式的返回值为真值)的时候,不会执行 y 或者 y 表达式,就好像||y不存在一样,直接返回 x(或者 x 表达式的返回值)x&&y的特性是,x 为真值(或者 x 表达式的返回值为真值)的时候,返回 y 的值(或者 y 表达式的返回值);但是 x 为假值(或者 x 表达式的返回值为真值)的时候,不会执行 y 或者 y 表达式,就好像||y不存在一样,直接返回 x(或者 x 表达式的返回值)
- 对比 ==, === 和 Object.is,并手写 Object.is 这个就不用找相同点和不同点了,这三个不能说一模一样吧,只能说是完全不同了,所以分开来单独说:
a == b的判断机制是:-
- 如果 a 和 b 的类型相同(这里指的是 typeof 作用之后的返回值相同),那么对
非引用值直接进行比较原始值;而对引用值比较的是内存地址;
- 如果 a 和 b 的类型相同(这里指的是 typeof 作用之后的返回值相同),那么对
-
- 如果 a 和 b 的类型不同,则首先判断是否其中有一个是
引用类型的,如果两个都不是引用类型,则直接返回 false;如果有一个是,则根据 js 设计规格将两者强制转成同一个非引用类型进行比较。
- 如果 a 和 b 的类型不同,则首先判断是否其中有一个是
-
a === b的判断机制是:-
- 如果 a 和 b 的类型不相同,则直接返回 false
-
- 如果 a 和 b 都是
引用类型的,则比较内存地址
- 如果 a 和 b 都是
-
- 如果 a 和 b 都是
非引用类型的,则比较原始值
- 如果 a 和 b 都是
-
Object.is(a,b)的判断机制是:NaN 等于自己,0 不等于自己-
- 检查是否符合两种特例,如果不符合,则直接返回
a===b的结果
- 检查是否符合两种特例,如果不符合,则直接返回
-
- 特例一:a 和 b 都是 NaN,此时应该返回 true
-
- 特例二:a 和 b 都是 0,只不过一个为正一个为负,此时需要返回 false
-
手写Object.is之前需要补充一个知识点:1 / Infinity === 0;的结果是 true;因此 Object.is(0, 1/Infinity) 也为 true.
根据 Object.is 的判断机制,容易写出其实现:
function myObjectIs(a, b) {
if (Number.isNaN(a) && Number.isNaN(b)) return true;
if (a === 0 && b === 0 && 1 / a !== 1 / b) return false;
return a === b;
}
有一个坑,那就是需要使用Number.isNaN而不是isNaN!
- 什么是 js 中的包装类型,又如何反包装?
对于 js 中的一个变量 x 而言,如果
typeof x的返回值是 C(= "number" | "string" | "boolean"),这意味着 x 是非引用类型。那么,按照道理来说,x 是没有属性和方法的。但是作为使用者,仍然可以通过"abc".length获得字符串的长度,或者使用123.toString(16)将 number 转成字符串格式。
之所以能够这样,在于 js 中存在着的包装类型。包装类型实际上是一种机制,在使用者做上述操作的时候,js 会自动将"number" | "string" | "boolean"包装成对象,即所谓的包装对象。
这个过程可以表示为:let _x = new C(x);其中,x 是"number" | "string" | "boolean"类型的,C 为构造函数Number | String | Boolean.
对于包装对象有如下特征:
x == _x // true 除过NaNx === _x // false_x.valueOf() === x // true 除过NaN
可以看到,使用包装对象的valueOf方法可以反包装,得到原始值(又称为是 Primitive Value)
补充:一般认为 js 中有六种primitive value
- undefined
- null
- string
- number
- boolean
- symbol
- 至于 bigint, 2020 年之后才出现的,也算是 primitive value
4.英语造句
-
I need to brainstorm more ideas about the problem you just mentioned. Let's start brainstorming some solutions to this online bug.
-
Through reviewing the current situation, you can do better next time. I plan to review the guidelines of my new job.
-
I can't identify the cause of this bug, it's too difficult to find.
-
Every day, I have so many new features to implement. You must implement the aforementioned functionality today.
-
Here's the data provided by another colleague, pleae analyse it and give me the outcome as soon as possible. Analyse the current situation and tell me how bad it is.
-
You must approach your tough times, otherwise you can't make any progress. Sometimes, I need bravery to approach the reality.
-
Tomorrow is the deadline, we must finalise this deal then.