一、初级
1. 数组去重
原理:Set 数据结构特性:元素唯一,自动去重;通过扩展运算符转回数组。
面试话术:利用ES6 Set不允许重复值的特性快速去重,代码简洁、性能高,适合普通数组去重,缺点是无法去重引用类型。
// ES6 Set(最简)
function uniqueArr(arr) {
return […new Set(arr)]
}
// 测试
console.log(uniqueArr([1,2,2,3,3,4])) // [1,2,3,4]
2.数组扁平化(实现flat)
原理:递归遍历数组,遇到数组继续递归,普通值直接存入,实现多维数组转一维。
面试话术:通过递归遍历数组,判断元素是否为数组,递归拆解,最终合并为一维数组,实现数组扁平化。
function flatArr(arr) {
let res = []
arr.forEach(item => {
if (Array.isArray(item)) {
res = res.concat(flatArr(item))
} else {
res.push(item)
}
})
return res
}
// 测试
console.log(flatArr([1,[2,[3,4]],5])) // [1,2,3,4,5]
3.简易深拷贝(初级)
原理:递归遍历对象/数组属性,基础类型直接赋值,引用类型递归拷贝,彻底断绝引用关系。
缺点:不支持正则、日期、循环引用,仅适合基础面试场景。
function deepClone(obj) {
// 基础类型直接返回
if (typeof obj !== 'object' || obj === null) return obj
// 判断数组/对象
let newObj = Array.isArray(obj) ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepClone(obj[key])
}
}
return newObj
}
4.手写防抖 Debounce
原理:短时间多次触发,清空上一次任务,重新计时,只执行最后一次触发的回调。
使用场景:搜索框输入、窗口resize、表单输入校验。
function debounce(fn, delay = 300) {
let timer = null
return function(…args) {
// 每次触发清空上一次定时器
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay)
}
}
5.手写节流 Throttle
原理:固定时间内只执行一次,锁住定时器,未到期不重复执行。
使用场景:滚动监听、鼠标移动、按钮频繁点击。
function throttle(fn, delay = 300) {
let timer = null
return function(…args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay)
}
}
}
6.手写new
原理:1. 创建空实例对象;2. 挂载构造函数原型;3. 执行构造函数绑定this;4. 判断返回值。
function myNew(Fn, …args) {
// 1. 创建空对象
let obj = {}
// 2. 绑定原型
obj.__proto__ = Fn.prototype
// 3. 执行构造函数,绑定this
let res = Fn.apply(obj, args)
// 4. 如果构造函数返回对象,返回该对象,否则返回新建obj
return res instanceof Object ? res : obj
}
7.手写call
原理:把函数挂载到指定上下文对象上执行,执行后删除临时属性,实现this绑定。
Function.prototype.myCall = function(context, …args) {
// 处理严格模式 + 基本类型包装
context = context === null || context === undefined ? window : Object(context)
// 唯一Symbol,防止属性名冲突
const tempKey = Symbol('tempFn')
// 挂载原函数
context[tempKey] = this
// 执行
const result = context[tempKey](…args)
// 删除临时属性
delete context[tempKey]
return result
}
8.手写apply
区别:call传参列表,apply传数组参数。
Function.prototype.myApply = function(context, args = []) {
// 严格模式处理 null/undefined,其余转为包装对象
context = context == null ? window : Object(context)
// 唯一Symbol避免属性覆盖
const uniqueKey = Symbol('apply_temp_fn')
context[uniqueKey] = this
// 确保args是数组,防止传入非数组报错
const result = context[uniqueKey](…Array.from(args))
delete context[uniqueKey]
return result
}
9.手写bind
原理:bind返回新函数,不会立即执行,可累积传参,永久绑定this。
Function.prototype.myBind = function (context, …args1) {
// 原函数
const originFn = this
// 处理上下文:null/undefined指向全局,基本类型转为包装对象
context = context == null ? window : Object(context)
// 空函数做中转,实现原型继承,避免修改原函数原型
const emptyFn = function () {}
emptyFn.prototype = originFn.prototype
// 绑定后的最终函数
const boundFn = function (…args2) {
// 判断是否是 new 调用:this 是不是 boundFn 的实例
const isNewCall = this instanceof boundFn
// new调用:this指向新实例;普通调用:绑定传入的context
const execCtx = isNewCall ? this : context
// 合并两次参数
const allArgs = […args1, …args2]
return originFn.apply(execCtx, allArgs)
}
// 修正原型,继承原函数原型
boundFn.prototype = new emptyFn()
return boundFn
}
二、中级
1.完整深拷贝(支持正则/日期)
核心:使用WeakMap解决循环引用,兼容Date、RegExp、对象、数组。
function deepClone(obj, map = new WeakMap()) {
// 原始类型直接返回
if (typeof obj !== 'object' || obj === null) return obj
// 循环引用处理
if (map.has(obj)) return map.get(obj)
// 日期、正则
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags)
// Map / Set 处理
if (obj instanceof Map) {
const newMap = new Map()
map.set(obj, newMap)
obj.forEach((v, k) => newMap.set(k, deepClone(v, map)))
return newMap
}
if (obj instanceof Set) {
const newSet = new Set()
map.set(obj, newSet)
obj.forEach(v => newSet.add(deepClone(v, map)))
return newSet
}
// 数组/对象初始化
const newObj = Array.isArray(obj) ? [] : {}
map.set(obj, newObj)
// 遍历所有自有key:包含Symbol、不可枚举属性
Reflect.ownKeys(obj).forEach(key => {
newObj[key] = deepClone(obj[key], map)
})
return newObj
}
- 为什么用 WeakMap 不用 Map? WeakMap 的 key 是弱引用,原对象销毁后可被 GC 回收,不会内存泄漏;Map 是强引用。
- 循环引用不处理会怎样? 递归无限调用,栈溢出 Maximum call stack size exceeded。
- JSON.parse (JSON.stringify ()) 缺点 无法拷贝 Date、RegExp、函数、Symbol、循环引用、undefined。
2.手写简易Promise
原理:三种状态不可逆,pending状态缓存回调,状态变更后批量执行回调,实现异步任务调度。
class MyPromise {
constructor(executor) {
this.status = 'pending'
this.value = null
this.reason = null
this.resolveCb = []
this.rejectCb = []
const resolve = (val) => {
if (this.status === 'pending') {
this.status = 'fulfilled'
this.value = val
this.resolveCb.forEach(fn => fn())
}
}
const reject = (err) => {
if (this.status === 'pending') {
this.status = 'rejected'
this.reason = err
this.rejectCb.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch (err) {
reject(err)
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }
if (this.status === 'fulfilled') {
onFulfilled(this.value)
}
if (this.status === 'rejected') {
onRejected(this.reason)
}
if (this.status === 'pending') {
this.resolveCb.push(() => onFulfilled(this.value))
this.rejectCb.push(() => onRejected(this.reason))
}
}
}
3.手写Promise.all
特性:全部成功才返回结果数组,一个失败直接失败。
Promise.myAll = function(promiseArr) {
return new Promise((resolve, reject) => {
let count = 0
let resArr = []
promiseArr.forEach((item, index) => {
Promise.resolve(item).then(res => {
count++
resArr[index] = res
if (count === promiseArr.length) {
resolve(resArr)
}
}).catch(err => {
reject(err)
})
})
})
}
4.手写发布订阅模式(EventBus)
用途:Vue非父子组件通信、全局事件总线。
class EventBus {
constructor() {
// 存储事件:{事件名: [回调数组]}
this.event = {}
}
// 订阅
on(name, fn) {
if (!this.event[name]) this.event[name] = []
this.event[name].push(fn)
}
// 发布
emit(name, …args) {
if (this.event[name]) {
this.event[name].forEach(fn => fn(…args))
}
}
// 取消订阅
off(name) {
delete this.event[name]
}
}
5.手写简易AJAX
function myAjax(url, method = 'GET', data = {}) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
// 参数拼接
if (method.toUpperCase() === 'GET') {
const params = new URLSearchParams(data).toString()
url += '?' + params
}
xhr.open(method, url, true)
// 设置请求头
if (method.toUpperCase() === 'POST') {
xhr.setRequestHeader('Content-Type', 'application/json')
}
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText))
} else {
reject(xhr.statusText)
}
}
xhr.onerror = reject
// 发送数据
xhr.send(method === 'POST' ? JSON.stringify(data) : null)
})
}
三、高级
1. Vue2 简易响应式原理(Object.defineProperty)
核心原理:通过Object.defineProperty劫持对象的get/set,获取数据收集依赖,修改数据触发视图更新。
缺陷:无法监听数组下标、长度变化,无法监听新增/删除属性(需$set)。
// 数据劫持
function observe(obj) {
if (!obj || typeof obj !== 'object') return
for (let key in obj) {
let val = obj[key]
// 递归劫持
observe(val)
Object.defineProperty(obj, key, {
get() {
console.log('获取数据:', val)
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
observe(newVal) // 新值继续劫持
console.log('更新视图:', newVal)
}
})
}
}
// 使用
let data = { name: 'vue', list: [1,2,3] }
observe(data)
2.手写虚拟DOM h函数 + 简单diff
2.1生成VNode
// h函数:生成虚拟节点
function h(tag, props, children) {
return { tag, props, children }
}
2.2 虚拟DOM转真实DOM
function mount(vnode, container) {
const el = document.createElement(vnode.tag)
// 设置属性
for (let key in vnode.props) {
el.setAttribute(key, vnode.props[key])
}
// 设置子节点
if (typeof vnode.children === 'string') {
el.textContent = vnode.children
} else {
vnode.children.forEach(child => mount(child, el))
}
container.appendChild(el)
// 保存真实DOM
vnode.el = el
}
2.3 简单diff算法
function diff(n1, n2) {
const el = n1.el
// 标签不同直接替换
if (n1.tag !== n2.tag) {
mount(n2, el.parentNode)
el.parentNode.removeChild(el)
return
}
// 文本更新
if (typeof n2.children === 'string') {
el.textContent = n2.children
return
}
// 简单子节点同位置对比
n2.children.forEach((child, index) => {
diff(n1.children[index], child)
})
}
3. 手写Vue nextTick(微任务优先级)
原理:维护回调队列,优先微任务执行,异步批量更新DOM,避免多次DOM操作损耗性能。
let timerFunc
// 优先微任务
if (Promise.resolve) {
timerFunc = () => Promise.resolve().then(flushCallbacks)
} else if (MutationObserver) {
timerFunc = () => {
const observer = new MutationObserver(flushCallbacks)
observer.observe(document.body, { childList: true })
}
} else {
// 降级宏任务
timerFunc = () => setTimeout(flushCallbacks, 0)
}
let callbacks = []
function flushCallbacks() {
callbacks.forEach(fn => fn())
callbacks = []
}
function nextTick(cb) {
callbacks.push(cb)
timerFunc()
}
4. 手写简易VueRouter(hash模式)
class VueRouter {
constructor(options) {
this.routes = options.routes
this.routeMap = {}
// 路由映射
this.routes.forEach(item => {
this.routeMap[item.path] = item.component
})
// 监听hash变化
window.addEventListener('hashchange', () => this.render())
window.addEventListener('load', () => this.render())
}
render() {
// 获取当前hash
const hash = location.hash.slice(1) || '/'
// 匹配组件
document.getElementById('router-view').innerHTML = this.routeMap[hash]
}
}
5.手写防抖(带取消、立即执行)
function debounce(fn, delay, immediate = false) {
let timer = null
const deb = function(…args) {
if (timer) clearTimeout(timer)
// 立即执行
if (immediate && !timer) {
fn.apply(this, args)
}
timer = setTimeout(() => {
// 延迟执行
if (!immediate) fn.apply(this, args)
timer = null
}, delay)
}
// 取消防抖
deb.cancel = () => clearTimeout(timer)
return deb
}
6.手写完整版节流
/**
* 节流函数
* @param {Function} fn 要节流的函数
* @param {number} delay 节流间隔毫秒
* @param {Object} options { leading: 是否首次立即执行,trailing: 结束是否兜底执行 }
* @returns 节流函数,挂载 cancel 方法可手动取消
*/
function throttle(fn, delay = 300, options = {}) {
let lastTime = 0; // 上一次执行时间戳
let timer = null; // 兜底定时器
const { leading = true, trailing = true } = options;
const throttled = function (…args) {
// 保留调用上下文
const ctx = this;
const now = Date.now();
// 不开启首次立即执行,初始化lastTime为当前时间,跳过第一次执行
if (!leading && lastTime === 0) {
lastTime = now;
}
// 距离上一次执行剩余等待时间
const remainTime = delay – (now – lastTime);
// 剩余时间<=0:达到间隔,立即执行
if (remainTime <= 0) {
// 清除可能存在的尾部兜底定时器
if (timer) {
clearTimeout(timer);
timer = null;
}
lastTime = now;
fn.apply(ctx, args);
} else if (trailing && !timer) {
// 时间不足,开启定时器,最后一次触发兜底执行一次
timer = setTimeout(() => {
lastTime = Date.now();
timer = null;
fn.apply(ctx, args);
}, remainTime);
}
};
// 手动取消节流:清空定时器、重置标记
throttled.cancel = function () {
clearTimeout(timer);
timer = null;
lastTime = 0;
};
return throttled;
}
四、CSS手写
1. 垂直水平居中(万能flex)
.box {
display: flex;
justify-content: center;
align-items: center;
}
2.一行文本省略号
.text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
3. 多行文本省略号
.text {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
4.CSS三角形
.triangle {
width: 0;
height: 0;
border: 50px solid transparent;
border-top-color: red;
}
