每日的知识积累,包括 四个 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;
};
我的思路:
- 注意题目并没有让我们输出所有的回文串只是其个数,所以不要占用过多的额外空间
- 我们对每一个字串做到不重不漏即可,遍历依据是,依次遍历从某一位开始的所有子串
得分结果: 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%
总结提升:
- 只能写成
(-2)**31,这里不能省略括号。 - BigInt 可以与普通 number 直接比大小。
- 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 优劣对比
- Proxy 的优势如下:
- Proxy 可以直接监听对象而非属性;
- Proxy 可以直接监听数组的变化;
- Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
- Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
- Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
- 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动态添加getter和setter的功能所调用的方法)
5、虚拟 DOM 的优缺点
- 优点:
- 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
- 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
- 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。
- 缺点: 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
6、虚拟 DOM 实现原理
虚拟 DOM 的实现原理主要包括以下 3 部分:
- 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
- diff 算法 — 比较两棵虚拟 DOM 树的差异;
- 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 框架踩过最大的坑是什么?怎么解决的
- 就地复用的坑。
- proxy 导致 Upload 组件上传的时候 FormDate 无法二进制化的问题。
4. 五句英语积累
- That way
- The plane departs at 5:30 PM -- five thirty
- The plane is delayed
- They arrived yesterday
- Turn around