我打算面一家,就总结一家的面经,这样可以自己巩固并且分享出来希望对大家有所帮助,答案如果有问题的大家可以在评论区指出
A公司(7-9k)
面试题览
- es6新增特性
- const let var 区别,可以重复声明吗
- 原型链
- 如何实现深拷贝
- 数组方法
- 哪些会改变原数组
- 遍历数组方法中断循环
- 闭包
- 事件循环微任务先执行还是宏任务先执行
- Vue生命周期
- 父子组件生命周期
- Diff算法
- 响应式原理
- Uni动态tabbar
es6新增特性
- let和const关键字
- 解构赋值
- 箭头函数
- 模板字符串:使用反引号``代替双引号创建字符串
- 扩展运算符
- 新增了一些字符串和数组方法
- Symbol
- 迭代器(Iterator)
- 生成器
- Promise
const let var 区别,可以重复声明吗
(1)块级作用域: 块作用域由 { }包括,let 和 const 具有块级作用域,var 不存在块级作用域。块级作用域解决了 ES5 中的两个问题:
- 内层变量可能覆盖外层变量
- 用来计数的循环变量泄露为全局变量
(2)变量提升: var 存在变量提升,let 和 const 不存在变量提升,即在变量只能在声明之后使用,否在会报错。
(3)给全局添加属性: 浏览器的全局对象是 window,Node 的全局对象是 global。var 声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是 let 和 const 不会。
(4)重复声明: var 声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const 和 let 不允许重复声明变量。
(5)暂时性死区: 在使用 let、const 命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用 var 声明的变量不存在暂时性死区。
(6)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而 const 声明变量必须设置初始值。
(7)指针指向: let 和 const 都是 ES6 新增的用于创建变量的语法。 let 创建的变量是可以更改指针指向(可以重新赋值)。但 const 声明的变量是不允许改变指针的指向。
| 区别 | var | let | const |
|---|---|---|---|
| 是否有块级作用域 | × | ✔️ | ✔️ |
| 是否存在变量提升 | ✔️ | × | × |
| 是否添加全局属性 | ✔️ | × | × |
| 能否重复声明变量 | ✔️ | × | × |
| 是否存在暂时性死区 | × | ✔️ | ✔️ |
| 是否必须设置初始值 | × | × | ✔️ |
| 能否改变指针指向 | ✔️ | ✔️ | × |
原型链
构造函数通过prototype指向原型对象,原型对象通过constructor指回构造函数,构造函数实例化出来的实例通过proto指向原型对象。实例本身没有的属性和方法会沿着原型链朝上找,直到Object的原型对象,再没的话就么得了,原型链最终指到null。
如何实现深拷贝
深拷贝的原理核心是遍历原对象,然后一个一个复制到新的对象上,如果遇到引用类型的,然后再递归掉自己,继续去遍历,直到最后都是基本数据类型,就停止复制了,返回这个新对象,就是克隆后的对象了。
网上看到有很多版本的深拷贝,需要考虑的东西要考虑的东西其实很多,比如有的没有实现除了数组和对象外的引用数据类型赋值(正则,日期)、有的克隆后的对象没有继承原对象的原型链等等。
以下是一个相对全面的深克隆实现:
function deepClone(obj, cache = new WeakMap()) {
const type = Object.prototype.toString.call(obj).slice(8, -1) // 获取obj的数据类型
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null) // 判断是否为复杂数据类型(typeof判断null为'object',判断function为'function',所以这俩要特殊判断下)
// 基本数据类型直接返回
if (!isComplexDataType) {
return obj
}
// 缓存中有直接返回
if (cache.has(obj)) {
return cache.get(obj)
}
if (type === 'Date') {
return new Date(obj)
}
if (type === 'RegExp') {
return new RegExp(obj.source, obj.flags)
}
let allDesc = Object.getOwnPropertyDescriptors(obj) // 获取obj的所有属性描述符
let clonedObj = Object.create(Object.getPrototypeOf(obj), allDesc) // getPrototypeOf-获取对象的原型 create-创建一个新对象并继承原型链(第一个参数为原型链,第二个参数为属性描述符)
// 缓存原对象和可隆后的对象(都是引用地址)
cache.set(obj, clonedObj)
// 遍历原对象并将属性和值赋给克隆后的对象
for (let key of Reflect.ownKeys(obj)) { // 针对能够遍历对象的不可枚举属性以及 Symbol 类型,使用 Reflect.ownKeys 方法
clonedObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], cache) : obj[key] // 如果还是引用类型直接递归调用deepClone,否则直接赋值
}
return clonedObj
}
数组方法
- join()
- push()和pop()
- shift() 和 unshift()
- sort()
- reverse()
- concat()
- slice()
- splice()
- indexOf()和 lastIndexOf() (ES5新增)
- forEach() (ES5新增)
- map() (ES5新增)
- filter() (ES5新增)
- every() (ES5新增)
- some() (ES5新增)
- reduce()和 reduceRight() (ES5新增)
哪些会改变原数组
- push(): 将一个或多个元素添加到数组的末尾,并返回修改后的数组。
- pop(): 移除并返回数组的最后一个元素。
- shift(): 移除并返回数组的第一个元素。
- unshift(): 在数组的开头添加一个或多个元素,并返回修改后的数组。
- sort(): 对数组进行原地排序(修改原数组)。
- reverse(): 反转数组中元素的顺序,修改原数组。
- splice(): 从数组中添加/删除元素,可以修改原数组。
- forEach(): 对数组中的每个元素执行提供的函数,不会返回新数组,但是可以在遍历过程中修改原数组。
遍历数组方法中断循环
-
中断for循环
使用
break语句来中断for循环的执行。当break语句被执行时,循环将立即终止,并且程序将继续执行循环之后的代码。for (let i = 0; i < 10; i++) { if (i === 5) { break; // 当 i 等于 5 时中断循环 } console.log(i); } -
中断forEach
使用异常处理来中断循环。在回调函数中,可以抛出一个自定义异常,然后在外部使用
try...catch语句来捕获异常,从而实现中断循环的效果。const array = [1, 2, 3, 4, 5]; try { array.forEach((element) => { if (element === 3) { throw 'StopIteration'; // 抛出异常来中断循环 } console.log(element); }); } catch (e) { if (e !== 'StopIteration') { throw e; // 重新抛出其他异常 } }闭包
闭包可以使内部函数访问到外部函数的作用域。
function makeFunc() { var name = "Mozilla"; return function displayName() { alert(name); } } var myFunc = makeFunc(); myFunc();我的理解是,正常来说函数执行完,函数里的变量会被内存回收掉,但是上面这个例子中,因为makeFunc函数返回的函数中访问了name(根据作用域链displayName是可以访问他之外的变量的),js不知道你啥时候会调用displayName这个函数,所以会一直保持对name的引用。所以,即使这个name这个变量是在函数中的,但他不会被回收,我们调用myFunc的时候依旧可以访问到name。
闭包的优点
- 访问其他函数内部变量
- 变量长期驻扎在内存中,不会被内存回收机制回收,即延长变量的生命周期;
- 避免定义全局变量所造成的污染
但是,因为他会被一直引用,不会被内存回收,所以他有内存泄漏的问题,要在退出函数之前,将不使用的局部变量赋值为null,以避免内存泄漏。
事件循环微任务先执行还是宏任务先执行
当然是微任务先执行,宏任务又分为交互队列和延时队列,这两队列不同浏览器的优先级是不一样的,之后又跟面试官扯了扯为什么需要事件循环之类的,自己朝开的说,会给面试官意想不到的好印象。
Vue的生命周期
创建前后,挂载前后,更新前后,销毁前后。
还问了第一次加载页面会执行哪些生命周期,包括mounted之前的。
父子组件生命周期
整体思路就是先要把子的生命周期执行完才能执行完父的。
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
更新是一样的:
父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
销毁也是,就不写了。
Diff算法
在新老虚拟 DOM 对比时:
首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
如果为相同节点,进行 patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的 children 没有子节点,将旧的子节点移除)
比较如果都有子节点,则进行 updateChildren,判断如何对这些新 老节点的子节点进行操作(diff 核心)。
匹配时,找到相同的子节点,递归比较子节点
在 diff 中,只对同层的子节点进行比较,放弃跨级的节点比较,使 得时间复杂从 O(n 3)降低值 O(n),也就是说,只有当新旧 children 都为多个子节点时才需要用核心的 Diff 算法进行同层级比较。
响应式原理
Vue2中
-
针对对象
将data中的属性使用Object.defineProperty进行包装,每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
所以想要响应式要在data中先声明,给对象添加属性不可以直接添加,需要使用$set。
-
直接用索引改值和直接改数组长度都不是响应式的,也得用$set。
vue 出于性能的考虑,没有用 Object.defineProperty 去监听数组,而是通过覆盖数组的原型的方法,对常用的七种方法(push/pop/shift/unshift/splice/sort/reserve)进行了变异,以此来实现对数组的监听。
Uni动态tabbar
当时说的是封装一个tabbar组件,然后通过后端返的值来判断展示哪些tab。