Published on

前端面试系列二

Authors
  • avatar
    Name
    Tszkong Cheng
    Twitter

一、分享展示之前学过的一段比较满意或者认为有亮点的代码进行讲解思路

二、原型和原型链

一句话总结:

原型是对象的“继承模版”,原型链是对象查找属性时的“链式路径”

问题回答要点
什么是原型?对象的内部 [[Prototype]] 属性,指向另一个对象
什么是原型链?是对象查找属性时形成的一条链路,从当前对象沿着原型不断向上查找。
__proto__prototype 的区别?__proto__ 是实例的原型;prototype 是构造函数的原型对象
原型链的终点?null, 即 Object.prototype.__proto__ === null
如何判断属性来自对象本身?使用 Object.hasOwnProperty('prop')
instanceof 如何工作?判断构造函数的 prototype 是否存在于实例的原型链中

三、作用域和作用域链

作用域是 JavaScript 在定义变量时所决定的变量可访问范围。

类型说明
全局作用域在任何函数体外部声明的变量
函数作用域在函数内部定义的变量,只能在函数内用
块级作用域let/const 在花括号中的范围

作用域链:当访问一个变量时,JavaScript 引擎会从当前作用域向上查找父作用域,直到找到该变量或到达全局作用域。

问题回答要点
什么是作用域?变量可以被访问的区域范围
什么是作用域链?查找变量时,逐层往外查找的链条
JS 有哪些作用域?全局作用域、函数作用域、块级作用域
块级作用域和函数作用域的区别?块级用 let/const 声明,函数作用域用 var
闭包和作用域的关系?闭包会持久化外层作用域的引用,形成私有变量

四、闭包

闭包是函数“记住”它创建时所处的作用域,即使它在当前作用域外执行。 当一个函数在其定义的作用域外被调用,但它依然能访问定义时的作用域中的变量,就形成了闭包。

应用场景示例或说明
封装私有变量如上例中的 count
模拟块级作用域var 环境下隔离变量
函数柯里化function add(a){ return b => a+b }
防抖 / 节流等高阶函数需要保存计时器等中间状态
工具函数缓存如 memoization
问题说明
内存泄漏闭包会持久化外部变量,注意清理引用
调试复杂多层嵌套闭包调试困难,推荐使用命名函数
生命周期延长被引用变量无法被 GC(垃圾回收)

递归是闭包吗?

  • 递归和闭包是两种概念,递归是函数的一种实现方式,闭包是一种作用域机制,它们可以结合,但递归本身不是闭包

五、事件循环(Event Loop)

JavaScript 是单线程语言,为了能够同时处理异步任务,需要有一个机制来处理任务,这个机制就是事件循环。
事件循环的核心逻辑是:

不断从任务队列中取出任务,放入主线程执行。

主要流程如下:

  1. 执行主线程中的同步任务(Script 脚本)。
  2. 执行完后,开始进入事件循环。
  3. 检查是否有微任务队列(Microtask Queue)中的任务,有则全部执行。
  4. 然后从宏任务队列(Macrotask Queue)中取出一个任务执行。
  5. 重复步骤 3~4,直到宏任务队列为空。

2. 宏任务(Macrotask)

宏任务(Macrotask)指的都是执行环境为浏览器的异步任务,比如:

  • setTimeout
  • setInterval
  • setImmediate(Node.js)
  • I/O
  • requestAnimationFrame

3. 微任务(Microtask)

微任务(Microtask)指的都是执行环境为 Promise 的异步任务,比如:

  • Promise.then / catch / finally
  • queueMicrotask(原生 API)

4. 举例子说明执行顺序

console.log('1');

setTimeout(() => {
  console.log('2');
  Promise.resolve().then(() => {
    console.log('3');
  });
}, 0);

Promise.resolve().then(() => {
  console.log('4');
  setTimeout(() => {
    console.log('5');
  }, 0);
}).then(() => {
    console.log('6');
})

setTimeout(() => {
  console.log('7');
}, 0);

console.log('8');

执行顺序为: 1 8 4 6 2 3 7 5

六、深拷贝和浅拷贝

  • 浅拷贝是复制内存中的地址,拷贝前后的对象,因为引用类型共享同一块内存,修改会相互影响。
  • 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址。
  • 原始数据类型(string、number、boolean、undefined、null)是值类型,拷贝时直接复制值本身,没有引用关系,不存在“深浅“之分。

七、Vue3 的特性

分类新特性名称简要说明
🌟 核心特性Composition API组合式 API,增强逻辑复用和组织能力
<script setup> 语法糖更简洁的组合式写法,自动引入变量、无需显式 return
Fragment一个组件可以返回多个根节点
Teleport可将子组件渲染到 DOM 的其他位置
Suspense支持异步组件的加载占位符
⚙️ 性能优化更快的虚拟 DOM 重写比 Vue 2 更快、更节省内存
Tree-shaking 支持未用 API 不会打包进最终代码
编译优化模板编译更智能、更高效
📦 响应系统Proxy-based 响应式系统Proxy 替代 Object.defineProperty,支持更多类型(如数组、Map、Set)
shallowRefreadonly提供更细粒度的响应式控制
🧩 组件系统更好的类型推导(TS 支持)Vue 3 为 TypeScript 提供了一流的支持
多个 v-model 支持同一组件支持多个 v-model 绑定
自定义事件变更this.$emit 的使用方式变更,更清晰
🛠 其他特性新的生命周期名beforeUnmount / unmounted 替代旧的 destroyed 系列
自定义指令的钩子变更生命周期钩子名称变更为更统一的形式
emits 选项显式声明组件可触发的事件,提高代码可维护性

Vue2 vs Vue3 自定义指令对比

对比项Vue 2Vue 3
注册方式Vue.directive() 全局注册 / 局部注册同样支持 app.directive() 全局注册 / 局部注册
生命周期钩子名称bindinsertedupdatecomponentUpdatedunbind统一为 createdbeforeMountmountedbeforeUpdateupdatedbeforeUnmountunmounted
指令函数结构可拆成多个钩子函数推荐使用对象形式或函数形式(都支持)
支持函数式简写✅ 支持✅ 同样支持
vnodeoldVnode默认可访问仍可访问,但结构略有不同

八、watch 和 computed 的区别

两个都可以监听数据的变化

✅ 最核心区别

特性computed 计算属性watch 侦听器(观察者)
是否有返回值✅ 有,必须返回值(return❌ 没有返回值,只做副作用(如调用函数)
是否支持缓存✅ 支持,依赖不变则不会重新计算❌ 不缓存,每次依赖变化就触发
适合场景用于基于响应式状态的派生数据(展示、模板)用于异步操作或副作用处理(API 调用、DOM 操作)
是否响应式✅ 是响应式数据,可直接绑定在模板中❌ 不是响应式数据,不能直接用于模板
何时触发用作计算值,依赖变化才会重新计算依赖变化就会执行处理函数

九、v-for 和 v-if 可以在同一元素上使用吗?

可以但不推荐,因为在同一元素上使用会导致渲染逻辑混乱和性能问题。
在 Vue2 中,v-for 的优先级更高,在 Vue3 中,v-if 的优先级更高。
在 Vue3 中,v-if 做了提升优化,去除了没有必要的计算,同时也带来了一个无法取到 v-for 遍历的 item 问题。

优化

  1. 不要把 v-if 和 v-for 放在同一元素上,带来性能问题(每次渲染都会先循环再进行条件判断)。
  2. 可以在外层嵌套 template 元素进行 v-if 判断,在内部进行 v-for 循环。
  3. 如果条件出现在循环内部,可以通过计算属性 computed 提前过滤掉那些不需要显示的项。
前端面试系列二 | Chengtszkong's Blog