logo

面试必杀技:最全手写JS题精讲与实战指南

作者:JC2025.09.19 12:47浏览量:0

简介:本文整理了面试中高频出现的手写JS题目,涵盖基础语法、异步编程、原型链、闭包等核心知识点,提供详细解析与代码实现,助力开发者攻克技术面试难关。

面试中最全的手写JS题:从基础到进阶的实战指南

在前端技术面试中,手写JS代码是检验开发者基础功底的核心环节。无论是初级工程师还是资深开发者,都可能被要求现场实现某些经典算法或语言特性。本文将系统梳理面试中最常出现的手写JS题,涵盖基础语法、异步编程、原型链、闭包等核心知识点,并提供可运行的代码示例与深度解析。

一、基础语法与数据类型操作

1. 深拷贝实现

题目:实现一个函数,能够深度克隆任意JavaScript对象(包括循环引用)。

解析:深拷贝需要处理多种数据类型(Object、Array、Date、RegExp等),并解决循环引用导致的栈溢出问题。

  1. function deepClone(obj, hash = new WeakMap()) {
  2. // 处理基本类型和null/undefined
  3. if (obj === null || typeof obj !== 'object') return obj;
  4. // 处理循环引用
  5. if (hash.has(obj)) return hash.get(obj);
  6. // 处理特殊对象类型
  7. if (obj instanceof Date) return new Date(obj);
  8. if (obj instanceof RegExp) return new RegExp(obj);
  9. // 创建对应类型的空对象
  10. const cloneObj = Array.isArray(obj) ? [] : {};
  11. hash.set(obj, cloneObj);
  12. // 递归拷贝属性
  13. for (let key in obj) {
  14. if (obj.hasOwnProperty(key)) {
  15. cloneObj[key] = deepClone(obj[key], hash);
  16. }
  17. }
  18. // 处理Symbol属性
  19. const symbolKeys = Object.getOwnPropertySymbols(obj);
  20. for (let key of symbolKeys) {
  21. cloneObj[key] = deepClone(obj[key], hash);
  22. }
  23. return cloneObj;
  24. }

关键点

  • 使用WeakMap解决循环引用
  • 区分基本类型与引用类型
  • 特殊对象类型的单独处理
  • Symbol属性的拷贝

2. 类型判断函数

题目:实现一个比typeof更准确的类型判断函数。

解析:typeof无法区分Array、Date等对象类型,需要结合Object.prototype.toString。

  1. function getType(obj) {
  2. return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
  3. }
  4. // 测试
  5. console.log(getType([])); // "array"
  6. console.log(getType(new Date())); // "date"
  7. console.log(getType(/regex/)); // "regexp"

优势

  • 统一处理所有类型
  • 避免instanceof的跨窗口问题
  • 代码简洁高效

二、异步编程与事件循环

1. Promise实现

题目:手写一个简化版Promise,支持then方法链式调用。

解析:需要理解Promise的三种状态(pending/fulfilled/rejected)和微任务队列机制。

  1. class MyPromise {
  2. constructor(executor) {
  3. this.state = 'pending';
  4. this.value = undefined;
  5. this.reason = undefined;
  6. this.onFulfilledCallbacks = [];
  7. this.onRejectedCallbacks = [];
  8. const resolve = (value) => {
  9. if (this.state === 'pending') {
  10. this.state = 'fulfilled';
  11. this.value = value;
  12. this.onFulfilledCallbacks.forEach(fn => fn());
  13. }
  14. };
  15. const reject = (reason) => {
  16. if (this.state === 'pending') {
  17. this.state = 'rejected';
  18. this.reason = reason;
  19. this.onRejectedCallbacks.forEach(fn => fn());
  20. }
  21. };
  22. try {
  23. executor(resolve, reject);
  24. } catch (err) {
  25. reject(err);
  26. }
  27. }
  28. then(onFulfilled, onRejected) {
  29. onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
  30. onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };
  31. const promise2 = new MyPromise((resolve, reject) => {
  32. if (this.state === 'fulfilled') {
  33. setTimeout(() => {
  34. try {
  35. const x = onFulfilled(this.value);
  36. resolvePromise(promise2, x, resolve, reject);
  37. } catch (e) {
  38. reject(e);
  39. }
  40. }, 0);
  41. } else if (this.state === 'rejected') {
  42. setTimeout(() => {
  43. try {
  44. const x = onRejected(this.reason);
  45. resolvePromise(promise2, x, resolve, reject);
  46. } catch (e) {
  47. reject(e);
  48. }
  49. }, 0);
  50. } else {
  51. this.onFulfilledCallbacks.push(() => {
  52. setTimeout(() => {
  53. try {
  54. const x = onFulfilled(this.value);
  55. resolvePromise(promise2, x, resolve, reject);
  56. } catch (e) {
  57. reject(e);
  58. }
  59. }, 0);
  60. });
  61. this.onRejectedCallbacks.push(() => {
  62. setTimeout(() => {
  63. try {
  64. const x = onRejected(this.reason);
  65. resolvePromise(promise2, x, resolve, reject);
  66. } catch (e) {
  67. reject(e);
  68. }
  69. }, 0);
  70. });
  71. }
  72. });
  73. return promise2;
  74. }
  75. }
  76. function resolvePromise(promise2, x, resolve, reject) {
  77. if (promise2 === x) return reject(new TypeError('Chaining cycle detected'));
  78. let called = false;
  79. if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
  80. try {
  81. const then = x.then;
  82. if (typeof then === 'function') {
  83. then.call(x, y => {
  84. if (called) return;
  85. called = true;
  86. resolvePromise(promise2, y, resolve, reject);
  87. }, r => {
  88. if (called) return;
  89. called = true;
  90. reject(r);
  91. });
  92. } else {
  93. resolve(x);
  94. }
  95. } catch (e) {
  96. if (called) return;
  97. called = true;
  98. reject(e);
  99. }
  100. } else {
  101. resolve(x);
  102. }
  103. }

核心机制

  • 状态机管理
  • 异步调度(setTimeout模拟微任务)
  • 链式调用处理
  • 值穿透与Promise解析

2. Async/Await原理模拟

题目:用Generator函数模拟async/await的实现。

解析:理解async函数本质是Generator函数的语法糖。

  1. function asyncToGenerator(generatorFn) {
  2. return function() {
  3. const gen = generatorFn.apply(this, arguments);
  4. return new Promise((resolve, reject) => {
  5. function step(key, arg) {
  6. let record;
  7. try {
  8. record = gen[key](arg);
  9. } catch (err) {
  10. return reject(err);
  11. }
  12. const { value, done } = record;
  13. if (done) return resolve(value);
  14. return Promise.resolve(value).then(
  15. v => step('next', v),
  16. e => step('throw', e)
  17. );
  18. }
  19. step('next');
  20. });
  21. };
  22. }
  23. // 使用示例
  24. const fetchData = asyncToGenerator(function*(){
  25. const res1 = yield fetch('url1');
  26. const res2 = yield fetch('url2');
  27. return res2;
  28. });

三、原型链与继承

1. 实现new操作符

题目:手写一个函数模拟new操作符的行为。

解析:理解构造函数执行过程与原型链关系。

  1. function myNew(constructor, ...args) {
  2. // 1. 创建新对象并链接到原型
  3. const obj = Object.create(constructor.prototype);
  4. // 2. 执行构造函数并绑定this
  5. const result = constructor.apply(obj, args);
  6. // 3. 处理返回值
  7. return result instanceof Object ? result : obj;
  8. }
  9. // 测试
  10. function Person(name) {
  11. this.name = name;
  12. }
  13. Person.prototype.sayHi = function() {
  14. console.log(`Hi, ${this.name}`);
  15. };
  16. const p = myNew(Person, 'Tom');
  17. p.sayHi(); // "Hi, Tom"

执行步骤

  1. 创建空对象并设置proto指向构造函数prototype
  2. 调用构造函数,this指向新对象
  3. 返回新对象(若构造函数返回对象则返回该对象)

2. 继承实现

题目:实现ES5的类继承(组合寄生式继承)。

解析:最完善的继承方式,解决原型链污染和属性共享问题。

  1. function Parent(name) {
  2. this.name = name;
  3. this.colors = ['red', 'blue'];
  4. }
  5. Parent.prototype.sayName = function() {
  6. console.log(this.name);
  7. };
  8. function Child(name, age) {
  9. // 继承属性
  10. Parent.call(this, name);
  11. this.age = age;
  12. }
  13. // 继承方法
  14. Child.prototype = Object.create(Parent.prototype);
  15. Child.prototype.constructor = Child;
  16. Child.prototype.sayAge = function() {
  17. console.log(this.age);
  18. };
  19. // 测试
  20. const child1 = new Child('Tom', 5);
  21. child1.colors.push('black');
  22. console.log(child1.colors); // ["red", "blue", "black"]
  23. const child2 = new Child('Jerry', 6);
  24. console.log(child2.colors); // ["red", "blue"]

优势

  • 属性独立(通过Parent.call)
  • 方法共享(通过原型链)
  • 避免直接修改Child.prototype导致的污染

四、性能优化与算法

1. 防抖与节流

题目:实现防抖(debounce)和节流(throttle)函数。

解析:高频事件处理的核心优化手段。

  1. // 防抖:事件触发后等待n秒再执行,若期间再次触发则重新计时
  2. function debounce(fn, delay) {
  3. let timer = null;
  4. return function(...args) {
  5. if (timer) clearTimeout(timer);
  6. timer = setTimeout(() => {
  7. fn.apply(this, args);
  8. }, delay);
  9. };
  10. }
  11. // 节流:固定时间间隔内最多执行一次
  12. function throttle(fn, delay) {
  13. let lastTime = 0;
  14. return function(...args) {
  15. const now = Date.now();
  16. if (now - lastTime >= delay) {
  17. fn.apply(this, args);
  18. lastTime = now;
  19. }
  20. };
  21. }
  22. // 立即执行版节流
  23. function throttleImmediate(fn, delay) {
  24. let lastTime = 0;
  25. let timer = null;
  26. return function(...args) {
  27. const now = Date.now();
  28. const remaining = delay - (now - lastTime);
  29. if (remaining <= 0) {
  30. if (timer) {
  31. clearTimeout(timer);
  32. timer = null;
  33. }
  34. lastTime = now;
  35. fn.apply(this, args);
  36. } else if (!timer) {
  37. timer = setTimeout(() => {
  38. lastTime = Date.now();
  39. timer = null;
  40. fn.apply(this, args);
  41. }, remaining);
  42. }
  43. };
  44. }

应用场景

  • 防抖:搜索框输入、窗口resize
  • 节流:滚动事件、鼠标移动

2. 数组扁平化

题目:实现一个函数将多维数组扁平化为一维数组。

解析:考察递归与数组方法的使用。

  1. // 方法1:递归
  2. function flatten(arr) {
  3. let result = [];
  4. for (let item of arr) {
  5. if (Array.isArray(item)) {
  6. result = result.concat(flatten(item));
  7. } else {
  8. result.push(item);
  9. }
  10. }
  11. return result;
  12. }
  13. // 方法2:reduce + 递归
  14. function flatten(arr) {
  15. return arr.reduce((acc, val) => {
  16. return acc.concat(Array.isArray(val) ? flatten(val) : val);
  17. }, []);
  18. }
  19. // 方法3:ES6展开运算符(仅限一层)
  20. function flatten(arr) {
  21. while (arr.some(item => Array.isArray(item))) {
  22. arr = [].concat(...arr);
  23. }
  24. return arr;
  25. }
  26. // 方法4:Generator函数实现
  27. function* flattenGenerator(arr) {
  28. for (let item of arr) {
  29. if (Array.isArray(item)) {
  30. yield* flattenGenerator(item);
  31. } else {
  32. yield item;
  33. }
  34. }
  35. }
  36. // 测试
  37. const arr = [1, [2, [3, [4]], 5]];
  38. console.log(flatten(arr)); // [1, 2, 3, 4, 5]

五、面试准备建议

  1. 系统梳理知识体系:按照语言特性、异步编程、设计模式等维度分类整理
  2. 代码规范训练:注意变量命名、缩进、注释等细节
  3. 边界条件考虑:始终检查null/undefined、循环引用等特殊情况
  4. 性能优化意识:在实现中体现对时间复杂度、空间复杂度的考虑
  5. 沟通表达能力:面试时清晰阐述实现思路和考虑因素

结语

手写JS题考察的是开发者对语言本质的理解程度。本文整理的题目覆盖了前端面试中最核心的知识点,建议读者结合实际项目经验深入理解每个实现背后的原理。在准备面试时,不仅要能够写出正确代码,更要能解释清楚为什么这样实现,以及可能的优化方向。掌握这些基础,将帮助你在技术面试中脱颖而出。

相关文章推荐

发表评论