每日知识积累 Day 13

187 阅读8分钟

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

1. 一个类型体操

类型体操题目集合 Absolute

实现一个接收 string, number 或 bigInt 类型参数的 Absolute 类型,返回一个正数字符串。

例如:

type Test = -100;
type Result = Absolute<Test>; // expected to be "100"

分析

注意仔细审题,传入的是 number 类型的,输出的却是 string 类型的,将 number 变成 string 可以通过模板字符串实现,将符号视为一个普通字符即可。

尝试写出

type Absolute<T extends number> = `${T}` extends `-${infer K}` ? K : `${T}`;

测试用例

type Test = -100;
type Test2 = 100;
type Result = Absolute<Test>; // "100"
type Result2 = Absolute<Test2>; // "100"

参考答案

type Absolute<T extends number | string | bigint> = T extends `${
  | "+"
  | "-"}${infer R}`
  ? R
  : T extends string
  ? T
  : Absolute<`${T}`>;

经验总结

参考答案考虑的更加详细,特别是:

T extends `${'+' | '-'}${infer R}`

写的非常的好。

2. 两个 Leetcode 题目

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

2.1 [289] 生命游戏

根据 百度百科 , 生命游戏 ,简称为 生命 ,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。

给定一个包含 m × n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态: 1 即为 活细胞 (live),或 0 即为 死细胞 (dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:

如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
如果死细胞周围正好有三个活细胞,则该位置死细胞复活;
下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。给你 m x n 网格面板 board 的当前状态,返回下一个状态。


示例 1:


输入:board = [[0,1,0],[0,0,1],[1,1,1],[0,0,0]]
输出:[[0,0,0],[1,0,1],[0,1,1],[0,1,0]]

0 1 0
0 0 1
1 1 1
0 0 0
-----
0 0 0
1 0 1
0 1 1
0 1 0


示例 2:


输入:board = [[1,1],[1,0]]
输出:[[1,1],[1,1]]

1 1
1 0
---
1 1
1 1

提示:

m == board.length
n == board[i].length
1 <= m, n <= 25
board[i][j] 为 0 或 1


进阶:

你可以使用原地算法解决本题吗?请注意,面板上所有格子需要同时被更新:你不能先更新某些格子,然后使用它们的更新后的值再更新其他格子。
本题中,我们使用二维数组来表示面板。原则上,面板是无限的,但当活细胞侵占了面板边界时会造成问题。你将如何解决这些问题?

尝试实现:

/**
 * @param {number[][]} board
 * @return {void} Do not return anything, modify board in-place instead.
 */
var gameOfLife = function (board) {
  const r = board.length;
  const c = board[0].length;

  function getPrevious(val) {
    return Math.floor(val + 0.5);
  }

  function getCurrent(val) {
    return Math.abs(Math.floor(val));
  }

  for (let i = 0; i < r; i++) {
    for (let j = 0; j < c; j++) {
      const cur = board[i][j];
      let live = 0;
      // right
      if (board[i] && getPrevious(board[i][j + 1]) === 1) {
        live += 1;
      }
      // left
      if (board[i] && getPrevious(board[i][j - 1]) === 1) {
        live += 1;
      }
      // top
      if (board[i - 1] && getPrevious(board[i - 1][j]) === 1) {
        live += 1;
      }
      // bottom
      if (board[i + 1] && getPrevious(board[i + 1][j]) === 1) {
        live += 1;
      }
      // topright
      if (board[i - 1] && getPrevious(board[i - 1][j + 1]) === 1) {
        live += 1;
      }
      // topleft
      if (board[i - 1] && getPrevious(board[i - 1][j - 1]) === 1) {
        live += 1;
      }
      // bottomright
      if (board[i + 1] && getPrevious(board[i + 1][j + 1]) === 1) {
        live += 1;
      }
      // bottomleft
      if (board[i + 1] && getPrevious(board[i + 1][j - 1]) === 1) {
        live += 1;
      }

      // case 1
      if (live < 2) {
        if (cur === 1) {
          // 原活现死
          board[i][j] = 0.5;
        } else {
          // 原死现死
          board[i][j] = 0;
        }
      } else if (live === 2 || live === 3) {
        if (cur === 1) {
          // 原活现活
          board[i][j] = 1;
        } else {
          if (live === 3) {
            // 原死现活
            board[i][j] = -0.5;
          }
        }
      } else if (live > 3) {
        if (cur === 1) {
          // 原活现死
          board[i][j] = 0.5;
        } else {
          // 原死现死
          board[i][j] = 0;
        }
      }
    }
  }
  for (let i = 0; i < r; i++) {
    for (let j = 0; j < c; j++) {
      const cur = board[i][j];
      board[i][j] = getCurrent(cur);
    }
  }
  return board;
};

我的思路:

  • 快速搞清楚想考什么也非常重要;这道题绝对不是在考多次迭代之后的收敛结果
  • 它实际考察的内容还是矩阵元素改变之后如何还原出原来的值
  • 从一个状态到另一个状态,有四种可能 1->1 1->0 0->0 0->1 因此我们需要做的就是构造两个函数,能够根据当前值还原出状态改变之后和之前的值
  • 改变之后的值用来更新状态,改变之前的值用来影响周围细胞
  • 我们构造 getPrevious round,此时 1->1 :1, 1->0: 0.5, 0->0: 0, 0->1: -0.5
  • 我们构造 getCurrent 它向下取整之后取绝对值。

得分结果: 32.49% 58.71%

总结提升:

  1. 快速的理解考点然后识别出坑点。
  2. 不使用笔而是在脑子里面将整个过程不重不漏的演示一遍很重要。
  3. 设置合适的调试位置也很重要。

2.2 [303] 区域和检索 - 数组不可变

给定一个整数数组  nums,处理以下类型的多个查询:

计算索引 left 和 right (包含 left 和 right)之间的 nums 元素的 和 ,其中 left <= right
实现 NumArray 类:

NumArray(int[] nums) 使用数组 nums 初始化对象
int sumRange(int i, int j) 返回数组 nums 中索引 left 和 right 之间的元素的 总和 ,包含 left 和 right 两点(也就是 nums[left] + nums[left + 1] + ... + nums[right] )


示例 1:

输入:
["NumArray", "sumRange", "sumRange", "sumRange"]
[[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
输出:
[null, 1, -1, -3]

解释:
NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)
numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1))
numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))


提示:

1 <= nums.length <= 104
-105 <= nums[i] <= 105
0 <= i <= j < nums.length
最多调用 104 次 sumRange 方法

尝试完成:

/**
 * @param {number[]} nums
 */
var NumArray = function (nums) {
  this.arr = nums;
};

/**
 * @param {number} left
 * @param {number} right
 * @return {number}
 */
NumArray.prototype.sumRange = function (left, right) {
  let sum = 0;
  for (let i = left; i < right + 1; i++) {
    sum += this.arr[i];
  }
  return sum;
};

我的思路:

  1. 是时候复习一下 new 的作用原理和 ES5 写 class 了。

得分结果: 13.11% 83.61%

总结提升:

  1. new 的本质
function myNew(constructor, args) {
  var obj = Object.create(constructor.prototype);
  var result = constructor.apply(obj, args);
  return typeof result === "object" && result !== null ? result : obj;
}
  1. 类语法糖的本质
class Engine {
  age: number;
  constructor(age: number) {
    this.age = age;
  }
  start() {
    console.log("Engine is starting...");
  }
}

对应的 ES5 写法:

"use strict";
class Engine {
  constructor(age) {
    this.age = age;
  }
  start() {
    console.log("Engine is starting...");
  }
}

3. 三个前端题目

  1. 对比普通函数和箭头函数
    1. 从形式的角度来看:箭头函数更加的简洁:
    • 无参数的情况使用括号代替之
    • 有一个参数的时候可以不使用括号
    • 可以省略大括号
    • 使用 void 表达式表示返回值为 undefined 的情况
let fn = () => void doSomething();
    1. 箭头函数没有自己的 this,使用 call apply bind 的时候也不会改变 this 的指向;本质上就是使用 a.b 格式调用箭头函数的时候不会将 this 传入运行时;所以和 this 相关的操作不生效(比无法改变 this 的指向更加贴切
    1. 箭头函数没有自己的原型对象,也就是没有 prototype 属性;而在实例化一个对象的时候必须要用到构造函数的原型对象,所以箭头函数充当构造函数也是非常的不合适的。
    1. 箭头函数上没有 arguments 属性,自然也就没有 length 和 callee 等
    1. 箭头函数不可以作为生成器,原因为:内部缺少[[Generator]]属性;这其实是一个伪命题,因为不应该将 function*看成是 function 的衍生物,它们只是长得比较像而已;应该这样理解:function function* 和箭头函数是三种不同的东西。

补充 js 中的几种作用域:

    1. 全局作用域
    1. 函数作用域
    1. eval 作用域
    1. with 作用域
    1. 模块作用域
    1. try catch 中的作用域
  1. 谈一谈对 rest 参数的理解
    1. rest 参数功能由扩展运算符实现;扩展运算符在此处的作用是将分离参数整合成为一个数组。而在 rest 之前,想要实现相同的功能只能通过Function.prototype.apply
    1. rest 参数的特点为:

    • 不计入函数参数长度
    • 只能放在最后,并且只能有一个
    1. 手动实现剩余函数的功能
function getRest(arguments, num) {
  const _arr = Array.prototype.slice.call(null, arguments);
  const _rest = _arr.splice(0, num);
  return [..._arr, _rest];
}
  1. 对比 map 和 object
    1. :map 上的键是有顺序的,顺序就是插入的顺序,并且可以是任何类型的;而 object 中的键是没有顺序的,并且只能是 string number symbol;此外 object 中还有多余的键
    1. 大小和可迭代性:map 可以通过封装好的接口直接进行迭代或者获取大小;但是 object 需要经过预先的处理才能显现相同的效果
    1. 性能:在频繁的增删键值对的场景下,map 的性能高于 object;这是因为 map 中做了优化;map 基于哈希表,object 中有哈希表的思想,但是不是通过哈希表实现的;map 使用的哈希表的容量比较大,所以占用的内存多一些,但是速度快,本质上是通过空间换时间。

总之:在场景简单,数据结构也简单的情况下优先使用 object 可以节省内存;而在复杂情况下使用 map 可以提高效率。

4.四句英语积累

  1. go on -- to start talking again after a pause or interruption
    1. [Please go on] - [I'm sorry for interrupting].
    2. [May I go on]?
  2. break something down -- separate sth into smaller parts so that it's easier to do or understand
    1. The process sounds quite complex. Could you [break it down for us] please?
    2. There's a lot involved in this task, so it might be better to [break it down into smaller tasks].