每日知识积累 Day 30

171 阅读16分钟

每日的知识积累,包括 四个 Leetcode 算法题,十个前端八股文题,四个英语表达积累。总计大概 130 天左右,希望大家多多支持!

1. 四个 Leetcode 题目

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

2.1 [5] 最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。


示例 1:

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:

输入:s = "cbbd"
输出:"bb"
 

提示:

1 <= s.length <= 1000
s 仅由数字和英文字母组成

尝试实现:

/**
 * @param {string} s
 * @return {string}
 */
var longestPalindrome = function (s) {
  const n = s.length;
  if (n < 2) return s;

  let rst = {
    start: null,
    end: null,
    count: 0,
  }

  function print() {
    return s.slice(rst.start, rst.end);
  }

  function isValid(_s) {
    let i = 0;
    let j = _s.length - 1;
    while (i < j) {
      if (_s[i] !== _s[j]) return false;
      i++;
      j--;
    }
    return true;
  }

  for (let i = 0; i < n; i++) {
    if (n - i - 1 < rst.count) return print();

    for (let j = i; j < n + 1; j++) {
      const cur = s.slice(i, j);
      if (isValid(cur)) {
        if (cur.length > rst.count) {
          rst = {
            start: i,
            end: j,
            count: cur.length,
          }
        }
      }
    }
  }

  return print();
};

我的思路:

  • 最简单朴素的想法,就是将所有的字串找出来,然后统计最长长度即可
  • 所以我们需要一个检验函数,检验是否输入的是回文串
  • 要做的就是不重不漏的检查每一个字串,但是可以做一些优化,那比如利用当前最长回文串的长度来减少遍历的次数

得分结果: 35.35% 39.39%

2.2 [647] 回文子串

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。


示例 1:

输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"
示例 2:

输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
 

提示:

1 <= s.length <= 1000
s 由小写英文字母组成

尝试完成:

/**
 * @param {string} s
 * @return {number}
 */
var countSubstrings = function (s) {
  const n = s.length;
  if (n === 1) return 1;
  function isValid(_s) {
    let i = 0;
    let j = _s.length - 1;

    while (i < j) {
      if (_s[i] !== _s[j]) return false;
      i++;
      j--;
    }
    return true;
  }

  let count = 0;
  for (let i = 0; i < n; i++) {
    for (let j = i; j < n; j++) {
      const cur = s.slice(i, j + 1); // .slice(a,a) 的结果是 ""
      if (isValid(cur)) {
        count++;
      };
    }
  }

  return count;
};

我的思路:

  1. 注意题目并没有让我们输出所有的回文串只是其个数,所以不要占用过多的额外空间
  2. 我们对每一个字串做到不重不漏即可,遍历依据是,依次遍历从某一位开始的所有子串

得分结果: 5.02% 69.14%

总结提升: 运行速度太慢了,看效率高一些的解法:

/**
 * @param {string} s
 * @return {number}
 */
var countSubstrings = function(s) {
  const strLen = s.length
  let numbeofHui = 0
  let dp = Array.from(Array(strLen),() => Array(strLen).fill(false))

  for(let j = 0 ; j < strLen ; j++){
    for(let i = 0 ; i <= j ; i++){
      if(s[i] === s[j]){
        if(j - i < 2){
            dp[i][j] = true
        } else {
            dp[i][j] = dp[i+1][j-1]
        }
        numbeofHui  += dp[i][j] ? 1 : 0
      }
    }
  }
  return numbeofHui
};

2.3 [7] 整数反转

给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。

如果反转后整数超过 32 位的有符号整数的范围 [−2^31,  2^31 − 1] ,就返回 0。

假设环境不允许存储 64 位整数(有符号或无符号)。
 

示例 1:

输入:x = 123
输出:321
示例 2:

输入:x = -123
输出:-321
示例 3:

输入:x = 120
输出:21
示例 4:

输入:x = 0
输出:0
 

提示:

-231 <= x <= 231 - 1

尝试完成:

/**
 * @param {number} x
 * @return {number}
 */
var reverse = function (x) {
  function isExceed(bigInt) {
    if (bigInt > 2 ** 31 - 1 || bigInt < (-2) ** 31) return 0;
    return Number(bigInt);
  }
  const strX = x + '';
  if (strX[0] === '-') {

    return isExceed(BigInt('-'.concat(strX.slice(1).split("").reverse().join(""))));
  } else {
    return isExceed(BigInt(strX.split("").reverse().join("")));
  }
};

我的思路:

  • 涉及到大数,我们一般使用 BigInt.
  • 注意处理负号,并且题目没有说的话,不用考虑 '+' 符号。

得分结果: 81.61% 76.60%

总结提升:

  1. 只能写成 (-2)**31 ,这里不能省略括号。
  2. BigInt 可以与普通 number 直接比大小。
  3. BigInt 转成 number 使用 Number(bigInt) 即可。

2.4 [9] 回文数

给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。

回文数
是指正序(从左向右)和倒序(从右向左)读都是一样的整数。

例如,121 是回文,而 123 不是。
 

示例 1:

输入:x = 121
输出:true
示例 2:

输入:x = -121
输出:false
解释:从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。
示例 3:

输入:x = 10
输出:false
解释:从右向左读, 为 01 。因此它不是一个回文数。
 

提示:

-231 <= x <= 231 - 1
 

进阶:你能不将整数转为字符串来解决这个问题吗?

尝试完成:

/**
 * @param {number} x
 * @return {boolean}
 */
var isPalindrome = function(x) {
  if(x < 0) return false;
  const strX = x + "";
  return strX === strX.split("").reverse().join("");
};

我的思路:

  • 如果是负数则可以直接返回 false, 然后使用数组的 reverse 反转比较即可。

得分结果: 13.22% 6.93%

总结提升: 效率不是很高,高效率需要使用位运算。

2.5 [479] 最大回文串乘积

给定一个整数 n ,返回 可表示为两个 n 位整数乘积的 最大回文整数 。因为答案可能非常大,所以返回它对 1337 取余 。

 

示例 1:

输入:n = 2
输出:987
解释:99 x 91 = 9009, 9009 % 1337 = 987
示例 2:

输入:n = 1
输出:9
 

提示:

1 <= n <= 8

尝试完成:

/**
 * @param {number} n
 * @return {number}
 */
var _largestPalindrome = function (n) {
  function isValid(num) {
    return (num + '') === (num + "").split("").reverse().join("");
  }

  let rst = 0;

  for (let i = 10 ** n - 1; i > 10 ** (n - 1); i--) {
    for (let j = i; j > 10 ** (n - 1); j--) {
      const cur = i * j;
      if (cur < rst) continue;
      if (isValid(cur)) {
        rst = rst < cur ? cur : rst;
      }
    }
  }

  return rst % 1337;
};


// 然后上面的解法肯定是超时的,这样的话,我们只需要在本地计算出 1-8 就可以了。
console.log(_largestPalindrome(1)) // 9 
console.log(_largestPalindrome(2)) // 987
console.log(_largestPalindrome(3)) // 123
console.log(_largestPalindrome(4)) // 597
console.log(_largestPalindrome(5)) // 677
console.log(_largestPalindrome(6)) // 1218
console.log(_largestPalindrome(7)) // 877
console.log(_largestPalindrome(8)) // 475

/**
 * @param {number} n
 * @return {number}
 */
var largestPalindrome = function (n) {
  return {
    1: 9,
    2: 987,
    3: 123,
    4: 597,
    5: 677,
    6: 1218,
    7: 877,
    8: 475
  }[n]
}

我的思路:

  • 这个题的结果是可枚举的,因此我们先算出来这些值,然后打表就可以了

得分结果: 100% 100%

总结提升: 玩笑归玩笑,看看大佬如何解答:

/**
 * @param {number} n
 * @return {number}
 */
var largestPalindrome = function(n) {
  if (n === 1) {
      return 9;
  }
  const upper = 10 ** n - 1;
  for (let left = upper; left > upper / 10; left--) {
      let right = String(left).split('').reverse().join('');
      let p = BigInt(String(left) + right)    //得到回文数
      let x = BigInt(upper);
      while (x * x >= p) {
          if (p % x === BigInt(0)) { // x 是 p 的因子
              return p % BigInt(1337);
          }
          x--;
      }
  }
};

看不懂,数学太多了。

3. 十个 vue 面试题

1、Vue 是如何实现数据双向绑定的

Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下所示:

  • 输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。

  • Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化。

  • 其中,View 变化更新 Data ,可以通过事件监听的方式来实现,所以 Vue 的数据双向绑定的工作主要是如何根据 Data 变化更新 View。 Vue 主要通过以下 4 个步骤来实现数据双向绑定的:

    • 实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。

    • 实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。

    • 实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。

    • 实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。

2、Vue 框架怎么实现对象和数组的监听

如果被问到 Vue 怎么实现数据双向绑定,大家肯定都会回答 通过 Object.defineProperty() 对数据进行劫持,但是 Object.defineProperty() 只能对属性进行数据劫持,不能对整个对象进行劫持,同理无法对数组进行劫持,但是我们在使用 Vue 框架中都知道,Vue 能检测到对象和数组(部分方法的操作)的变化,那它是怎么实现的呢?我们查看相关代码如下:

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])  // observe 功能为监测数据的变化
    }
  }

  /**
   * 对属性进行递归遍历
   */
  let childOb = !shallow && observe(val) // observe 功能为监测数据的变化

通过以上 Vue 源码部分查看,我们就能知道 Vue 框架是通过遍历数组 和递归遍历对象,从而达到利用 Object.defineProperty() 也能对对象和数组(部分方法的操作)进行监听。

3、Proxy 与 Object.defineProperty 优劣对比

  1. Proxy 的优势如下:
  • Proxy 可以直接监听对象而非属性;
  • Proxy 可以直接监听数组的变化;
  • Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
  • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
  • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
  1. Object.defineProperty 的优势如下:

兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题, 而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。

4、Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题

受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value) 来实现为对象添加响应式属性,那框架本身是如何实现的呢? 我们查看对应的 Vue 源码:vue/src/core/instance/index.js

export function set (target: Array<any> | Object, key: any, val: any): any {
  // target 为数组  
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
    target.length = Math.max(target.length, key)
    // 利用数组的splice变异方法触发响应式  
    target.splice(key, 1, val)
    return val
  }
  // key 已经存在,直接修改属性值  
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  // target 本身就不是响应式数据, 直接赋值
  if (!ob) {
    target[key] = val
    return val
  }
  // 对属性进行响应式处理
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

我们阅读以上源码可知,vm.$set 的实现原理是:

  • 如果目标是数组,直接使用数组的 splice 方法触发相应式;
  • 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 gettersetter 的功能所调用的方法)

5、虚拟 DOM 的优缺点

  1. 优点:
  • 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
  • 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
  • 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。
  1. 缺点: 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。

6、虚拟 DOM 实现原理

虚拟 DOM 的实现原理主要包括以下 3 部分:

  1. 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
  2. diff 算法 — 比较两棵虚拟 DOM 树的差异;
  3. pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。

7、Vue 中的 key 有什么作用

key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速。Vue 的 diff 过程可以概括为:

  • oldCh 和 newCh 各有两个头尾的变量 oldStartIndex、oldEndIndex 和 newStartIndex、newEndIndex,它们会新节点和旧节点会进行两两对比,即:
  • 一共有 4 种比较方式:newStartIndex 和 oldStartIndex 、newEndIndex 和 oldEndIndex 、newStartIndex 和 oldEndIndex 、newEndIndex 和 oldStartIndex
  • 如果以上 4 种比较都没匹配,如果设置了 key,就会用 key 再进行比较,在比较的过程中,遍历会往中间靠,一旦 StartIdx > EndIdx 表明 oldCh 和 newCh 至少有一个已经遍历完了,就会结束比较。

所以 Vue 中 key 的作用是:

  • key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速
    • 更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
    • 更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快,源码如下:
function createKeyToOldIdx (children, beginIdx, endIdx) {
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}

8、你有对 Vue 项目进行哪些优化

(1)代码层面的优化

  • v-if 和 v-show 区分使用场景
  • computed 和 watch 区分使用场景
  • v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
  • 长列表性能优化
  • 事件的销毁
  • 图片资源懒加载
  • 路由懒加载
  • 第三方插件的按需引入
  • 优化无限列表性能
  • 服务端渲染 SSR or 预渲染

(2)Webpack 层面的优化

  • Webpack 对图片进行压缩
  • 减少 ES6 转为 ES5 的冗余代码
  • 提取公共代码
  • 模板预编译
  • 提取组件的 CSS
  • 优化 SourceMap
  • 构建结果输出分析
  • Vue 项目的编译优化

(3)基础的 Web 技术的优化

  • 开启 gzip 压缩
  • 浏览器缓存
  • CDN 的使用
  • 使用 Chrome Performance 查找性能瓶颈

9、对于即将到来的 vue3.0 特性你有什么了解的吗

Vue 3.0 正走在发布的路上,Vue 3.0 的目标是让 Vue 核心变得更小、更快、更强大,因此 Vue 3.0 增加以下这些新特性:

(1)监测机制的改变

3.0 将带来基于代理 Proxy 的 observer 实现,提供全语言覆盖的反应性跟踪。这消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:

  • 只能监测属性,不能监测对象
  • 检测属性的添加和删除;
  • 检测数组索引和长度的变更;
  • 支持 Map、Set、WeakMap 和 WeakSet。

新的 observer 还提供了以下特性:

  • 用于创建 observable 的公开 API。这为中小规模场景提供了简单轻量级的跨组件状态管理解决方案。
  • 默认采用惰性观察。在 2.x 中,不管反应式数据有多大,都会在启动时被观察到。如果你的数据集很大,这可能会在应用启动时带来明显的开销。- 在 3.x 中,只观察用于渲染应用程序最初可见部分的数据。
  • 更精确的变更通知。在 2.x 中,通过 Vue.set 强制添加新属性将导致依赖于该对象的 watcher 收到变更通知。在 3.x 中,只有依赖于特定属性的 watcher 才会收到通知。
  • 不可变的 observable:我们可以创建值的“不可变”版本(即使是嵌套属性),除非系统在内部暂时将其“解禁”。这个机制可用于冻结 prop 传递或 Vuex 状态树以外的变化。
  • 更好的调试功能:我们可以使用新的 renderTracked 和 renderTriggered 钩子精确地跟踪组件在什么时候以及为什么重新渲染。

(2)模板

  • 模板方面没有大的变更,只改了作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。
  • 同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。

(3)对象式的组件声明方式

  • vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易。

  • 此外,vue 的源码也改用了 TypeScript 来写。其实当代码的功能复杂之后,必须有一个静态类型系统来做一些辅助管理。现在 vue3.0 也全面改用 TypeScript 来重写了,更是使得对外暴露的 api 更容易结合 TypeScript。静态类型系统对于复杂代码的维护确实很有必要。

(4)其它方面的更改

vue3.0 的改变是全面的,上面只涉及到主要的 3 个方面,还有一些其他的更改:

  • 支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。
  • 支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。
  • 基于 treeshaking 优化,提供了更多的内置功能。

10、说说你使用 Vue 框架踩过最大的坑是什么?怎么解决的

  1. 就地复用的坑。
  2. proxy 导致 Upload 组件上传的时候 FormDate 无法二进制化的问题。

4. 五句英语积累

  1. That way
  2. The plane departs at 5:30 PM -- five thirty
  3. The plane is delayed
  4. They arrived yesterday
  5. Turn around