前端面试(常见八股+手撕代码)
前端面试:常见八股与手撕代码指南(出于自用的原因整理,基本涵盖各个方面)
本文系统梳理了前端面试中的经典八股题,涵盖JavaScript、框架原理、浏览器机制等核心内容,同时提供手撕代码的典型例题与解析。通过理论与实践结合,帮助读者快速查漏补缺,高效备战面试。
一、常见八股文
1、基础部分
2、进阶部分
(1)JS
(2)Vue+性能+原理
二、部分高频手写
这个目录从左侧目录栏跳转是正确的,上面的不知道为啥有点问题
一、常见八股文
1、基础部分
每个HTML文件里开头都有Doctype声明,它告诉浏览器该文档使用哪种HTML版本进行解析。
1、JS原始数据类型有哪些?引用数据类型有哪些?
原始数据类型:number、string、boolean、null、undefined、(es6新增)symbol、bigInt
引用数据类型:Object、Array、function、date、正则(RegExp)、(es6新增)Map、Set
原始数据类型存储位置在栈(stack)中,直接传递值
引用数据类型存储位置在堆(heap)中,传递引用地址
2、==和===有哪些区别?
===判断两者类型和值是否相同
==两者类型相同的话先比较值,类型不同的话先进行类型转换为Number再比较
3、隐式转换和对象转原始类型
所有类型都能转换成Boolean值,除了false、0、""(空字符串)、null、undefined、NaN这6个个为false,其余所有值(包括[]、{}、"0")都为ture
+运算
若有一个是string另一个操作数会隐式转换成string,最后进行字符串拼接
若没有string,所有操作数都会隐式转换成number,再执行数值加法(undefined->NaN)
除加法外的算数运算(-、*、/、%)全部强制隐式转换为Number或相同类型,再执行数值运算
逻辑运算(&&、||、!):隐式转换为Boolean值,再执行逻辑判断,!先将值转换为Boolean再取反;&&从左到右遇到假值返回假值,否则返回最后一个真值;||从左到右遇到真值返回真值,否则返回最后一个假值
[] == ![] 结果为 true(解析:![]→false,[]→0,false→0,0==0);
2 == true 结果为false(true→1,2≠1)
对象转原始类型时,JS 引擎会根据转换目标类型(hint) 触发不同的方法,优先级从高到低为:Symbol.toPrimitive → valueOf → toString(Symbol.toPrimitive是ES6之后给对象加的快捷方式,需要自己定义)
-
默认的 valueOf 和 toString 行为 原生对象(如 {}、[]、Date)有默认实现:
-
valueOf:默认返回对象本身(非原始类型);
-
toString():
-
普通对象 {} → "[object object]";
-
数组 [] → ""(空字符串);
-
日期对象 new Date() → 日期字符串(如 "Wed Aug 27 2025...")。
-
-
typeof是否能正确判断类型?Instanceof能正确判断对象的原理是什么
typeof对于原始数据类型除了null都可以显示正确的类型(null会显示为object),对于对象来说除了函数都会显示object。
typeof判断未声明变量不会报错,会显示undefined
用'=== null'来判断是否是null,用Array.isArray()来判断是否是数组,返回值是布尔值
Instanceof通过原型链来判断数据类型,可以用于判断引用数据类型
Object.prototype.toString.call()可以判断所有数据类型,返回值是数据类型
Typeof返回值是数据类型,instanceof返回值是布尔值
// 实现Instanceof(原型链向上查找)
Function myInstanceof(left,right){
let proto = Object.getPrototypeOf(left)
while(ture){
if(proto === null) return false
if(proto === right.prototype) return ture
proto = Object.getPrototypeOf(proto)
}
}
数组常用方法
可迭代方法:forEach、map、filter、reduce
修改原数组:push、pop、shift、unshift、splice、sort、reverse
返回新数组:map、filter、reduce、slice、concat

返回新数组


2、进阶部分
(1)JS
深拷贝和浅拷贝
为什么要拷贝? 修改副本时不影响原数据,创建一个完全独立的数据快照
浅拷贝速度快,开销小;深拷贝速度慢,开销大;按需使用
浅拷贝:仅复制对象的表层属性,如果是基本数据类型会复制它的值,引用数据类型会复制它的地址(共享引用,修改一方会影响另一方)
const shallowCopy = Object.assign({},original)
const shallowCopy = {...original}
深拷贝:完全复制对象的所有层级结构,无论是基本数据类型还是引用数据类型都会递归复制(新旧对象完全独立,修改一方不会影响另一方)
// 首选* 专为深拷贝设计,无法拷贝function,erro对象和dom节点
structuredClone()
// 数据简单可选* 忽略undefined、Symbol()、function,Date()对象会变成字符串,无法处理循环引用
JSON.parse(JSON.stringify())
// 拷贝函数
lodash.cloneDeep()
手写深拷贝
// (1)处理边界:如果是null或者非object类型直接返回
// (2)处理循环引用:用WeakMap存储已拷贝过的对象,避免死循环
// (3)创建新容器:判断是数组还是对象,创建对应的容器[ ]或{ }
// (4)递归拷贝:遍历原始对象的属性,递归调用deepClone赋值给新容器
Function deepClone(obj,hash=new WeakMap()){
if(typeof obj !== 'object' || obj === null) return obj
if(hash.has(obj)) return hash.get(obj)
let cloneObj = Array.isArray(obj) ? [] : {}
hash.set(obj,cloneObj)
for(let key in obj){
//确保只拷贝对象自身的属性,排除原型链继承的属性
if(Object.prototype.hasOwnProperty.call(obj,key)){
cloneObj[key] = deepClone(obj[key],hash)
}
}
return cloneObj
}
JS中this指向
this的指向由调用方式决定,在运行时绑定(优先级new -> 显示绑定(call,apply,bind)->隐式绑定(指向调用对象)->默认(window))
箭头函数的this由定义时的词法作用域决定,且不可更改,是解决回调函数this丢失的最佳方法
3、什么是闭包?有什么作用?
闭包是指有权访问另一个函数作用域中变量的函数,核心作用是实现数据私有封装,避免全局变量污染,可以延长变量的生命周期、实现私有变量、封装模块等。但是容易造成内存泄露,需手动释放引用。
内存泄露
(1)意外的全局变量(2)定时器未正确关闭(3)事件监听未正确销毁(4)闭包
js中创建对象的方式有:
(1)字面量方式:通过{}定义对象,适合定义单个对象
(2)构造函数模式:使用new构造函数()创造对象,适合批量创造结构相同的对象
(3)原型模式:将方法定义在构造函数的prototype属性上,所有实例共享方法
(4)组合使用构造函数和原型模式:构造函数用于定义实例属性,原型用于定义共享方法
(5)const child = Object.create(parent)通过指定原型创建新对象,适合基于已有对象扩展新对象
(6)工厂模式:使用一个函数来封装对象的创建过程,返回一个新对象,适合创建结构复杂的对象
js中继承的六种方式:
(1)原型链继承:子类原型指向父类实例,子类实例通过原型链访问父类属性/方法(缺:所有子类共用一个实例,修改一个全部都会变)
(2)构造函数继承:子类中通过call/apply调用父类构造函数,实现属性继承(缺:只能继承父类属性不能继承父类原型上的方法)
(3)组合继承(原型链+构造函数):结合上面两者方法又继承属性又继承方法(缺:父类构造函数被调用两次)
(4)寄生组合继承(优化组合继承):通过Object.create()避免被两次调用
(5)原型式继承:通过Object.create()让新对象继承已有对象的属性,适合简单的对象继承
(6)寄生式继承:在原型式继承基础上封装一个函数增强对象增加新方法(新增方法无法共享,每个实例都是独立的,类似于工厂模式)
联系:js中对象的创建方式和继承方式非常相像,但是目标和侧重点不一样。对象创建的核心是”生成新对象并初始化自身属性“,继承的核心是”让新对象复用已有对象的属性“
什么是提升?什么是暂时性死区?var、let及const区别?
函数提升优于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部。
var存在变量提升,导致在变量声明之前可以访问到,而let和const因为暂时性死区的原因不能在声明前使用。
Var在全局作用域下声明变量会导致变量挂载在window上,其他两者不会。
Let和const作用基本一致,但是const声明的变量不能够再赋值。
高阶函数和柯里化应用场景
高阶函数:接收函数作为参数或者返回值为函数
柯里化:单参数函数序列转换
核心应用:代码复用、函数组合、延迟与配置
// 柯里化函数:将多参数函数转换为可分步传递参数的函数
function curry(fn){
return function curried(...args){
// 已传递参数数量 >= 原函数所需参数数量,直接执行原函数
if(args.length >= fn.length){
return fn.apply(this,args)
}else{ //参数不足,返回新柯里化函数,继续收集参数
return function(...nextArgs){
return curried.apply(this,args.concat(nextArgs))
}
}
}
}
防抖和节流
防抖:时间范围内再次触发重新计时,事件停止后执行
function debounce(fn,wait){
let timer = null
return function(...args){
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this,args)
timer = null
},wait)
}
}
节流:范围内再次触发不予理会,固定频率执行
定时器模式:基于延迟期控制(适用于"延迟处理",用户输入框输入)
function throttle(fn,wait=500){
let flag = true
return function(...args) {
if(!flag) return
flag = false
setTimeout(() => {
fn.apply(this,args)
flag = true
},wait)
}
}
时间戳模式:基于上次执行时间计算(适用于需要"实时响应"的场景,scroll)
function throttle(fn,wait=500){
let lastTime = 0
return function(...args){
const now = Date.now()
if(now - lastTime >= wait){
fn.apply(this,args)
lastTime = now
}
}
}
IIFE立即执行函数
定义后立即执行的函数:(function(){})(),主要是为了创建私有作用域,避免全局污染。es6之后因为模块化export/import和let/const的普及使用率下降
for (var i=0;i<5;i++){
(function(j){
setTimeout(() => {
console.log(j)
},10*j)
})(i)
}
纯函数:给定相同输入 -> 确定输出,且无副作用(不改变原数组、状态改变)
bind、call和apply
1、修改this指向(箭头函数不绑定自己的this,会绑定上下文的this)
(1)bind会产生新的函数,(把对象和函数绑定死后,产生新的函数)
(2)call和apply不会产生新的函数,只是在调用时,绑定一下而已。
(3)call和apply的区别,第一个参数都是要绑定的this,apply第二个参数是数组(是函数的所有参数),call把apply的第二个参数单列出来。
fn.call()接收的是多个参数的形式,如: fn.call(this,参数1,参数2,参数3,,,)
fn.apply()接收的是一个参数数组,如: fn.apply(this, [参数1,参数2,参数3,,])
他们都会直接指向调用,
fn.bind(this, args) 的不会执行调用,需要这样做, let fun = fn
// 手写bind
Function.prototype.myBind(context,...args){
// 边界校验:确保调用该方法的必须是函数
if(typeof this !== 'function') throw new TypeError('error')
const self = this //保存原函数
// 定义最终返回的绑定函数fbound
const fbound = function(...newArgs){
const allArgs = args.concat(newArgs) //合并参数
// this是否的bound实例(区分new调用和普通调用)
return this instanceof fbound
? new self(...allArgs) // new调用,原函数作为构造函数
: self.apply(context,allArgs) // 普通调用,绑定context
}
// 原型链维护
fbound.prototype = Object.create(self.prototype)
fbound.prototype.constructor = self
return fbound
}
// 手写call
Function.prototype.myCall(context,...args){
if(typeof this !== 'function') throw new TypeError('error')
context = context || window
if(typeof context !=='object' || context === null){
context = new Object(context)
}
const fn = Symbol('fn')
context[fn] = this
const result = context[fn](...args)
delete context[fn]
return result
}
// 手写apply
Function.prototype.myApply(context,args){
if(typeof this !== 'function') throw new TypeError('Error')
context = context || window // 若context为null和undefined,this指向全局
// 若context为原始数据类则自动转换为对象
if(typeof context !== 'object' || context === null){
context = new Object(context)
}
const fn = Symbol('fn')
context[fn] = this
const result = context[fn](...(args ?? []))
delete context[fn]
return result
}
函数组合:链式连接函数,提高可读性、复用性
(从右至左执行)示例:const processedValue = compose(函数1,函数2,函数3,......)
函数记忆化:缓存结果、提升计算性能、减少资源(调用一次后缓存,后续不需要重新计算(只缓存了调用的那一次数值的结果))
注:只对纯函数使用记忆化,对有副作用的函数记忆化可能导致行为异常
示例:const memorizedFibonacci = memoize(fibonacci)
定时器:是异步调度的工具
setInterval会导致任务堆积,所以一般用递归setTimeout
setTimeout(callback,delay,...args):delay后执行一次这个回调函数
setInterval(callback,delay,...args):每隔delay后重复执行回调函数
(2)Vue+性能+原理
Vue中computed和watch的区别?
Computed:依赖缓存,基于响应式依赖进行缓存,多次调用不会重复计算。
Watch:深度监听,监听到值的变化就会执行回调,适合处理异步操作和复杂逻辑的监听。
Vue中v-show和v-if的区别
v-show通过CSS的display属性控制元素的显示与隐藏,而v-if则是条件渲染,会销毁和重建元素。v-show适用于频繁切换显示状态的场景,而v-if适用于条件较少改变的场景
Promise是什么?解决了什么问题?
Vue3相比Vue2有哪些改进?
-
composition-api更灵活地组织逻辑代码(2)响应式系统升级为Proxy更强大性能更好。(3)更好的Ts支持(4)体积更小、运行更快
如何做页面性能优化?
(1)减少http请求数量(合并js/css)(2)使用懒加载只加载图片资源和组件(3)使用CDN(内容分发网络)加速,它将网页资源分布到全球多个服务器节点使用户能从离自己最近的服务器获取资源(4)利用浏览器缓存或服务器缓存,延迟加载其他部分(5)首屏优化、骨架屏(6)减少DOM操作,避免重绘重排(7)启用服务端Gzip压缩
为什么要使用模块化,都有哪几种方式可以实现模块化,各有什么特点?
解决命名冲突、提高复用性、提高代码可维护性
JS的垃圾回收机制
Js的垃圾回收是引擎自动执行的机制,核心目的是释放程序中不再使用的内存,避免内存泄漏。常见的垃圾回收算法有引用计数、标记清除和增量标记。引用计数通过记录对象被引用的次数来管理内存,但无法处理循环引用(两个对象互相引用);标记清除通过从根对象出发标记存活对象并清除未标记对象来释放内存,但执行标记清除过程中会停止整个应用程序;增量标记算法则是改进标记清除法将整个垃圾回收过程分段执行,减少了应用程序卡顿的时间。开发过程中要注意避免内存泄露,及时清除事件监听器和定时器、避免意外全局变量等不再使用的对象手动置为null等。
什么是执行栈?
执行栈的核心是“管理执行上下文的生命周期”,遵循“先进后出”的原则,栈顶上下文优先执行,执行完立即出栈。函数调用会触发新的入栈。函数执行完出栈,是函数嵌套调用按顺序执行的根本原因。最终输出顺序完全由执行栈的入栈/出栈顺序决定。
异步代码执行顺序?
解释一下什么是Even Loop(事件循环)?
js是单线程语言,主线程只能同步执行一个任务,所以js通过事件循环机制协调同步与异步任务之间的执行顺序。异步代码执行顺序由执行栈、微任务队列和宏任务队列决定
执行顺序:1、同步 -> 2、所有微任务 -> 3、一个宏任务(循环检查2)
执行栈:优先处理同步任务,执行完出栈,栈空时调用事件循环。
微任务队列:存放优先级最高的异步任务,执行完同步任务后清空推入执行栈中。(promise.then/catch/finally,async/await,queueMicrotask().)
宏任务队列:存放优先级较低的异步任务,微任务队列清空后取一个执行。(执行完后又去检查微任务队列)(setTimeout/setInterval,dom事件(click)、I/O操作完成回调、UI渲染(绘制)、fetch网络请求、setImmediate(Node.js环境))
注:promise.resolve是同步任务
Promise:是异步操作的机制,提高异步代码的可读性和可维护性,避免回调地狱
状态:pending(进行中)、fulfilled(已成功)、rejected(已失败),状态只能从pending变成fulfilled或rejected,且一旦改变就不可逆转,成功时会有一个结果值,失败会有一个失败原因(错误对象)
构造函数:new Promise(executor函数),executor接收两个参数resolve(成功时调用)、reject(失败时调用)
实例方法:.then(onFulfilled,onReject)总是返回一个新的Promise;(成功fulfilled)
.catch(onReject)是.then的语法糖,专用于捕获promise失败情况(rejected)
.finally()无论promise是fulfilled还是rejected都会执行
class MyPromise{
constructor(target){
this.status = 'pending'
this.value = undefined
this.reason = undefined
this.onResolvedCallbacks = []
this.onRejectedCallbacks = []
const resolve = (value) => {
if(this.status === 'pending'){
this.status = 'fulfilled'
this.value = value
this.onResolvedCallbacks.forEach(fn => fn())
}
}
const reject = (reason) => {
if(this.status === 'pending'){
this.status = 'rejected'
this.reason = reason
this.onRejectedCallback.forEach(fn => fn())
}
}
target(resolve,reject)
}
then(onFulfilled,onRejected){
if(this.status === 'fulfilled'){
onFulfilled(this.value)
}else if(this.status === 'rejected'){
onRejected(this.reason)
}else{
this.onFulfilledCallback.push(() => {
onFulfilled(this.value)
})
this.onRejectedCallback.push(() => {
onRejected(this.reason)
})
}
}
}
如何解决Promise回调地狱(Callback Hell)问题?
回调地狱的本质是异步操作串行化导致的代码嵌套,解决的核心思路是将嵌套转化为线性或并行。可以通过使用Promise链式调用(then/catch)或async/await语法来避免多层回调嵌套。实际开发中首选async/await因为可读性最好,同时使用promise.all进行并发请求。
async/await = Promises + Generators的语法糖
可以提高异步代码的可读性简化错误处理,核心机制是暂停当前async函数,将控制器还给事件循环,等待Promise决议
注:不阻塞主线程,await只能在async中使用,并发需要使用Promise.all
Promise.all、allSettled、race、any
Promise.all() :接收一个 Promise 数组,只有当所有 Promise 都成功 resolved 时,它才会返回成功的结果。
但只要有一个 Promise 被 reject,Promise.all 就会立即触发 catch 回调,并返回那个被 reject 的值。
function myPromiseAll(promise){
if(!Array.isArray(promise)) return Promise.reject(new Error('error'))
return new Promise((resolve,reject) => {
if(promise.length === 0){
resolve([])
return
}
const results = []
let counter = 0
for(let i=0;i {
results[i] = res
counter++
if(counter === promise.length){
resolve(results)
}
}).catch(err => {
reject(err)
})
}
})
}
Promise.allSettle():等待所有promise执行完毕,无论成功或失败提供所有结果信息
Promise.race():第一个变状态(成功/失败)的Promise决定结果
function myRace(promise){
if(!Array.isArray(promise)) return Promise.reject(new Error('error'))
return new Promise((resolve,reject) => {
promise.forEach(promise => {
Promise.resolve(promise).then(res => {
resolve(res)
},err => {
reject(error)
})
})
})
}
function myAll(promise){
if(!Array.isArray(promise)) return Promise.reject(new Error('error'))
return new Promise((resolve,reject) => {
if(promise.length === 0){
resolve([])
return
}
const results = []
let counter = 0
for(let i=0;i {
results[i] = res
counter++
if(counter === promise.length){
return results
}
}).catch(error => {
reject(error)
})
}
})
}
function myRace(promise){
if(!Array.isArray(promise)) return Promise.reject(new Error('error'))
return new Promise((resolve,reject) => {
promise.forEach(promise => {
Promise.resolve(promise).then(res => {
resolve()
},err => {
reject(error)
})
})
})
}
Promise.any():只关心第一个成功的,返回成功值,只有所有失败才拒绝,返回聚合错误
// 成功
const promise1 = Promise.resolve(3)
const promise2 = 42 // 非 Promise会立即 resolve
const promise3 = new Promise((resolve,reject) => {
setTimeout(resolve,100,'foo')
})
Promise.all([promise1,promise2,promise3])
.then((values) => {
console.log(values) // Array [3,42,'foo']
})
// 失败
const promise4 = Promise.resolve(10)
const promise5 = Promise.reject('Error occurred')
const promise6 = new Promise((resolve,reject) => {
setTimeout(resolve,500,'slow')
})
Promise.all([promise4,promise5,promise6])
.catch((erro) => {
console.error(error)
// 立即输出Error occurred,不会等待promise6完成
})
手写Ajax请求
function myAjax(url){
// 创建一个XHR对象
return new Promise((resolve,reject) => {
const xhr = new XMLHttpRequest()
// 指定请求类型,请求url,和是否异步
xhr.open('GET',url,true)
xhr.onreadystatechange = function(){
// 数据已就绪
if(xhr.readyState === 4){
if(xhr.status === 200){
resolve(JSON.stringify(xhr.responseText))
}else{
reject('error')
}
}
}
xhr.send(null)
})
}
new的原理是什么?通过new的方式创建对象和通过字面量创建有什么区别?
new通过四步创建,(1)创建一个空对象(2)为空对象建立原型链关联(对象原型指向构造函数的原型对象)(3)为空对象绑定this(4)返回值为object类型,否则返回全新对象newObj
区别:(1)原型链不同:new创造的对象原型指向构造函数的prototype,共享构造函数原型上的属性方法。字面量创建的对象原型指向object.prototype。(2)使用场景不同:字面量适合创建简单的、一次性对象,new适合创造多个结构相似的对象,便于代码复用。(3)初始化方式不同:new创建时自动调用构造函数,在构造函数中统一初始化属性,字面量则需手动为每个属性赋值。
Function myNew(fn,...args){
const newObj = Object.create(fn.prototype)
const res = fn.apply(newObj,args)
const isObject = typeof res === 'object' && res !== null
const isFunction = typeof res === 'function'
if(isObject || isFunction){
return res
}
return newObj
}
Object.create()可以创建没有原型的对象
Function myCreate(proto){
const fn = function{
fn.prototype = proto
fn.prototype.constructor = fn
return new fn()
}
}
Js常见继承机制:解决代码复用和层级结构
继承方式:原型链、构造函数、组合、寄生组合
传统最优解是寄生组合方式,es6中 class、extend是最推荐的方式
为什么0.1+0.2!=0.3?
因为js采用双精度浮点数存储数值,而0.1和0.2转二进制是无限循环小数,存储的时候保留其近似值,相加后就不完全相等0.3,可以采用转整数再除回去方式或者parseFloat或者toFixed()固定精度(tofixd返回是字符串要转成数字)
事件的触发过程是怎么样的?知道什么是事件代理吗?
事件触发分为三个阶段:1、捕获阶段:事件从window向下传播到目标元素的父级。2、目标阶段:事件到达目标元素本身。3、冒泡阶段:事件从目标元素向上传播回window。
事件代理(事件委托):利用事件冒泡机制将子元素的事件绑在父元素上,通过event.target判断具体触发事件的子元素并处理,可以减少绑定数量优化内存。
什么是跨域?为什么浏览器要使用同源策略?你有几种方法可以解决跨域问题?了解预检请求吗?
跨域:当一个请求的协议、域名、端口中任意一个与当前页面不同就是跨域。
同源策略:是浏览器的核心安全机制,限制不同源的脚本之间的交互,是为了防止恶意网站窃取用户数据
方法:1、CORS(跨域资源共享):服务器端设置响应头(Access-Control-Allow-Origin:*),允许指定源的跨域请求。

