西安三年经验面试面经分享

295 阅读9分钟

我打算面一家,就总结一家的面经,这样可以自己巩固并且分享出来希望对大家有所帮助,答案如果有问题的大家可以在评论区指出

A公司(7-9k)

面试题览

  • es6新增特性
  • const let var 区别,可以重复声明吗
  • 原型链
  • 如何实现深拷贝
  • 数组方法
  • 哪些会改变原数组
  • 遍历数组方法中断循环
  • 闭包
  • 事件循环微任务先执行还是宏任务先执行
  • Vue生命周期
  • 父子组件生命周期
  • Diff算法
  • 响应式原理
  • Uni动态tabbar

es6新增特性

  1. let和const关键字
  2. 解构赋值
  3. 箭头函数
  4. 模板字符串:使用反引号``代替双引号创建字符串
  5. 扩展运算符
  6. 新增了一些字符串和数组方法
  7. Symbol
  8. 迭代器(Iterator)
  9. 生成器
  10. Promise

const let var 区别,可以重复声明吗

(1)块级作用域: 块作用域由 { }包括,letconst 具有块级作用域,var 不存在块级作用域。块级作用域解决了 ES5 中的两个问题:

  • 内层变量可能覆盖外层变量
  • 用来计数的循环变量泄露为全局变量

(2)变量提升: var 存在变量提升,letconst 不存在变量提升,即在变量只能在声明之后使用,否在会报错。

(3)给全局添加属性: 浏览器的全局对象是 windowNode 的全局对象是 globalvar 声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是 letconst 不会。

(4)重复声明: var 声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。constlet 不允许重复声明变量。

(5)暂时性死区: 在使用 letconst 命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用 var 声明的变量不存在暂时性死区。

(6)初始值设置: 在变量声明时,varlet 可以不用设置初始值。而 const 声明变量必须设置初始值。

(7)指针指向: letconst 都是 ES6 新增的用于创建变量的语法。 let 创建的变量是可以更改指针指向(可以重新赋值)。但 const 声明的变量是不允许改变指针的指向。

区别varletconst
是否有块级作用域×✔️✔️
是否存在变量提升✔️××
是否添加全局属性✔️××
能否重复声明变量✔️××
是否存在暂时性死区×✔️✔️
是否必须设置初始值××✔️
能否改变指针指向✔️✔️×

原型链

构造函数通过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
    }

数组方法

  1. join()
  2. push()和pop()
  3. shift() 和 unshift()
  4. sort()
  5. reverse()
  6. concat()
  7. slice()
  8. splice()
  9. indexOf()和 lastIndexOf() (ES5新增)
  10. forEach() (ES5新增)
  11. map() (ES5新增)
  12. filter() (ES5新增)
  13. every() (ES5新增)
  14. some() (ES5新增)
  15. reduce()和 reduceRight() (ES5新增)

哪些会改变原数组

  1. push(): 将一个或多个元素添加到数组的末尾,并返回修改后的数组。
  2. pop(): 移除并返回数组的最后一个元素。
  3. shift(): 移除并返回数组的第一个元素。
  4. unshift(): 在数组的开头添加一个或多个元素,并返回修改后的数组。
  5. sort(): 对数组进行原地排序(修改原数组)。
  6. reverse(): 反转数组中元素的顺序,修改原数组。
  7. splice(): 从数组中添加/删除元素,可以修改原数组。
  8. forEach(): 对数组中的每个元素执行提供的函数,不会返回新数组,但是可以在遍历过程中修改原数组。

遍历数组方法中断循环

  1. 中断for循环

    使用 break 语句来中断 for 循环的执行。当 break 语句被执行时,循环将立即终止,并且程序将继续执行循环之后的代码。

    for (let i = 0; i < 10; i++) {
      if (i === 5) {
        break; // 当 i 等于 5 时中断循环
      }
      console.log(i);
    }
    
  2. 中断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中

  1. 针对对象

    将data中的属性使用Object.defineProperty进行包装,每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

    所以想要响应式要在data中先声明,给对象添加属性不可以直接添加,需要使用$set。

  2. 针对数组

    直接用索引改值和直接改数组长度都不是响应式的,也得用$set。

    vue 出于性能的考虑,没有用 Object.defineProperty 去监听数组,而是通过覆盖数组的原型的方法,对常用的七种方法(push/pop/shift/unshift/splice/sort/reserve)进行了变异,以此来实现对数组的监听。

Uni动态tabbar

当时说的是封装一个tabbar组件,然后通过后端返的值来判断展示哪些tab。

uniapp 动态tabbar

uniapp动态路由、动态tabbar实战方案 - DCloud问答