在寻找前端实习的过程中,我们会发现,面试除了考察算法题之外,手写题同样也是高频考点。尤其是在准备中大厂前端面试时,手写能力几乎是必不可少的一部分。这篇文章将围绕几道经典高频手写题展开,包括手写深拷贝、实现寄生组合式继承以及数组扁平化,帮助大家从原理到底层实现进行系统理解。
1.手写深拷贝
深拷贝是指创建一个与原对象完全独立的新对象,不仅复制对象第一层的属性,还会递归复制内部嵌套的对象、数组等引用类型。这样修改新对象时,不会影响原对象。它的核心意义在于“彻底断开引用关系”,常用于状态管理、数据备份以及避免对象共享带来的副作用等场景。
手写深拷贝的核心是递归复制对象中的每一层数据,而不是只复制引用地址。实现时需要先判断数据类型:基础类型直接返回,引用类型则继续递归创建新的对象或数组,并逐层复制属性。同时,为了处理循环引用问题,通常会配合WeakMap记录已经拷贝过的对象,避免无限递归。本质上,手写深拷贝就是“递归复制结构 + 断开引用关系”。
function deepClone(obj, map = new WeakMap()) { // 1. 基本类型直接返回 if (obj === null || typeof obj !== 'object') { return obj } // 2. 处理循环引用 if (map.has(obj)) { return map.get(obj) } // 3. 创建新对象/数组 const newObj = Array.isArray(obj) ? [] : {} // 4. 缓存当前对象 map.set(obj, newObj) // 5. 递归拷贝 for (const key in obj) { // 过滤原型链属性 if (obj.hasOwnProperty(key)) { newObj[key] = deepClone(obj[key], map) } } return newObj } /* 这段 deepClone 首先判断如果是基本类型或者 null,就直接返回; 如果是对象,就先通过 WeakMap 判断是否已经拷贝过,用来解决循环引用问题。 然后根据原数据是数组还是对象创建对应的新容器,并立刻把原对象和新对象的映射关系存到 WeakMap 中。 最后遍历对象自身属性,对每个属性递归调用 deepClone,实现真正的深层复制。这样拷贝出来的新对象和原对象在嵌套引用上也是相互独立的。 deepClone 中使用 WeakMap 而不是 Map,主要是为了避免内存泄漏。 因为 Map 对 key 是强引用,即使外部对象已经不再使用,只要 Map 中还保存着这个对象作为 key,垃圾回收器就无法释放它。 而 WeakMap 对 key 是弱引用,不会阻止垃圾回收,更适合用来做 deepClone 这种临时缓存的循环引用处理。并且 WeakMap 的 key 只能是对象,也正符合 deepClone 的使用场景。 */2.手写寄生组合式继承
寄生组合式继承是 JavaScript 中一种比较经典且高效的继承方案,它结合了“原型链继承”和“借用构造函数继承”的优点。实现时,子类通过Parent.call(this)继承父类实例属性,避免属性共享问题;同时通过Object.create(Parent.prototype)继承父类原型方法,从而避免多次调用父类构造函数带来的性能浪费。本质上,它实现了“实例属性独立 + 原型方法复用”,也是 ES6class extends出现前最推荐的继承方式之一。
手写寄生组合式继承的核心是组合“构造函数继承”和“原型继承”两种方式:先通过Parent.call(this)让子类继承父类的实例属性和方法,再通过Object.create(Parent.prototype)创建一个以父类原型为基础的新对象赋值给子类原型,从而实现原型方法复用,并修正constructor指向。这样既避免了引用属性共享的问题,又减少了父类构造函数的重复调用,是 ES5 中比较完善的一种继承实现方案。
function Parent(name) { this.name = name this.colors = ['red', 'blue'] } Parent.prototype.sayName = function () { console.log(this.name) } function Child(name, age) { // 1. 继承父类实例属性 Parent.call(this, name) this.age = age } // 2. 继承父类原型方法 Child.prototype = Object.create(Parent.prototype) // 3. 修正 constructor 指向 Child.prototype.constructor = Child Child.prototype.sayAge = function () { console.log(this.age) }3.手写数组扁平化
数组扁平化是指将多层嵌套的数组转换成一个一维数组的过程,例如把[1, [2, [3, 4]]]转换为[1, 2, 3, 4]。它的核心思想是“递归展开嵌套结构”,在前端开发中常用于数据处理、树形结构转换以及接口数据整理等场景。实现方式通常包括递归、reduce、扩展运算符以及 ES6 的flat()方法等。
手写数组扁平化的核心是递归遍历数组中的每一项:如果当前元素是数组,就继续递归展开;如果是普通元素,就直接放入结果数组中,最终将多层嵌套结构转换成一维数组。实现时常见的方法有递归、reduce、栈结构以及扩展运算符等,本质上就是“遍历嵌套结构并不断展开”。
function flatten(arr, depth = 1) { if (depth <= 0) { return arr.slice() } const res = [] for (const item of arr) { if (Array.isArray(item)) { res.push(...flatten(item, depth - 1)) } else { res.push(item) } } return res }