logo

面试必知:最全手写JS题解析与实战指南

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

简介:本文汇总了面试中最常见的手写JS题,涵盖数据类型、函数、原型链、异步处理等核心知识点,通过详细解析和代码示例帮助开发者系统掌握手写JS题的解题思路,提升面试通过率。

面试中最全的手写JS题解析

在前端开发面试中,手写JavaScript代码题是考察候选人基础能力的核心环节。这类题目不仅能检验开发者对语言特性的理解,还能体现其编码规范和问题拆解能力。本文将系统梳理面试中最常出现的手写JS题,从基础到进阶逐层解析,帮助读者构建完整的知识体系。

一、数据类型与基础操作

1. 类型判断:实现typeof的增强版

原生typeof存在局限性(如无法区分ArrayObject),面试常要求实现更精准的类型判断函数。

  1. function getType(value) {
  2. const typeStr = Object.prototype.toString.call(value);
  3. return typeStr.match(/\[object (.*?)\]/)[1].toLowerCase();
  4. }
  5. // 测试用例
  6. getType([]); // "array"
  7. getType(null); // "null"
  8. getType(/regex/); // "regexp"

关键点

  • 使用Object.prototype.toString获取标准类型标识
  • 通过正则提取实际类型名称
  • 覆盖所有原始类型和引用类型

2. 深拷贝实现

深拷贝是面试高频题,需处理循环引用、特殊对象(如Date、RegExp)等边界情况。

  1. function deepClone(obj, hash = new WeakMap()) {
  2. if (obj === null || typeof obj !== 'object') return obj;
  3. // 处理循环引用
  4. if (hash.has(obj)) return hash.get(obj);
  5. let clone;
  6. if (obj instanceof Date) clone = new Date(obj);
  7. else if (obj instanceof RegExp) clone = new RegExp(obj);
  8. else {
  9. clone = Array.isArray(obj) ? [] : {};
  10. hash.set(obj, clone);
  11. for (let key in obj) {
  12. if (obj.hasOwnProperty(key)) {
  13. clone[key] = deepClone(obj[key], hash);
  14. }
  15. }
  16. }
  17. return clone;
  18. }

优化方向

  • 使用WeakMap避免内存泄漏
  • 特殊对象单独处理
  • 递归实现时注意栈溢出风险

二、函数与高阶编程

1. 柯里化(Currying)实现

柯里化要求将多参数函数转换为单参数函数的嵌套调用。

  1. function curry(fn) {
  2. return function curried(...args) {
  3. if (args.length >= fn.length) {
  4. return fn.apply(this, args);
  5. } else {
  6. return function(...args2) {
  7. return curried.apply(this, args.concat(args2));
  8. }
  9. }
  10. }
  11. }
  12. // 使用示例
  13. const sum = curry((a, b, c) => a + b + c);
  14. sum(1)(2)(3); // 6

应用场景

  • 参数复用
  • 延迟执行
  • 函数组合

2. 防抖与节流

这两个函数是性能优化的经典手段,需清晰区分实现差异。

  1. // 防抖:事件触发后等待n秒再执行
  2. function debounce(fn, delay) {
  3. let timer;
  4. return function(...args) {
  5. clearTimeout(timer);
  6. timer = setTimeout(() => fn.apply(this, args), delay);
  7. }
  8. }
  9. // 节流:固定时间间隔执行一次
  10. function throttle(fn, interval) {
  11. let lastTime = 0;
  12. return function(...args) {
  13. const now = Date.now();
  14. if (now - lastTime >= interval) {
  15. fn.apply(this, args);
  16. lastTime = now;
  17. }
  18. }
  19. }

选择依据

  • 防抖适合输入框联想等场景
  • 节流适合滚动事件、鼠标移动等高频事件

三、原型链与继承

1. 实现new操作符

需理解构造函数执行过程:创建对象、关联原型、执行构造方法、返回对象。

  1. function myNew(constructor, ...args) {
  2. const obj = Object.create(constructor.prototype);
  3. const result = constructor.apply(obj, args);
  4. return result instanceof Object ? result : obj;
  5. }
  6. // 测试
  7. function Person(name) {
  8. this.name = name;
  9. }
  10. const p = myNew(Person, 'Tom');
  11. console.log(p.name); // "Tom"

执行步骤

  1. 创建空对象并关联原型
  2. 调用构造函数,绑定this
  3. 处理构造函数返回值

2. 多种继承方式实现

需掌握原型链继承、构造函数继承、组合继承等模式的差异。

  1. // 组合继承示例
  2. function Parent(name) {
  3. this.name = name;
  4. this.colors = ['red'];
  5. }
  6. Parent.prototype.sayName = function() {
  7. console.log(this.name);
  8. }
  9. function Child(name, age) {
  10. Parent.call(this, name); // 构造函数继承
  11. this.age = age;
  12. }
  13. Child.prototype = new Parent(); // 原型链继承
  14. Child.prototype.constructor = Child;
  15. const child1 = new Child('Tom', 18);
  16. child1.colors.push('blue');
  17. console.log(child1.colors); // ["red", "blue"]
  18. const child2 = new Child('Jerry', 20);
  19. console.log(child2.colors); // ["red"]

模式对比
| 继承方式 | 优点 | 缺点 |
|————————|—————————————|—————————————|
| 原型链继承 | 实现简单 | 共享引用属性 |
| 构造函数继承 | 可传参、不共享属性 | 无法继承原型方法 |
| 组合继承 | 结合两者优点 | 调用两次父构造函数 |

四、异步编程

1. 实现Promise

需完整实现then的链式调用、状态变更、异步决议等特性。

  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 : value => value;
  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. // 辅助函数:处理thenable对象和循环引用
  77. function resolvePromise(promise2, x, resolve, reject) {
  78. if (promise2 === x) return reject(new TypeError('Chaining cycle detected'));
  79. let called = false;
  80. if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
  81. try {
  82. const then = x.then;
  83. if (typeof then === 'function') {
  84. then.call(
  85. x,
  86. y => {
  87. if (called) return;
  88. called = true;
  89. resolvePromise(promise2, y, resolve, reject);
  90. },
  91. r => {
  92. if (called) return;
  93. called = true;
  94. reject(r);
  95. }
  96. );
  97. } else {
  98. resolve(x);
  99. }
  100. } catch (e) {
  101. if (called) return;
  102. called = true;
  103. reject(e);
  104. }
  105. } else {
  106. resolve(x);
  107. }
  108. }

实现要点

  • 三种状态管理
  • 异步执行回调
  • 链式调用支持
  • 循环引用检测

2. 实现Async/Await

基于Generator函数实现简易版Async/Await。

  1. function asyncToGenerator(generatorFn) {
  2. return function(...args) {
  3. const gen = generatorFn.apply(this, args);
  4. return new Promise((resolve, reject) => {
  5. function step(key, arg) {
  6. let info;
  7. try {
  8. info = gen[key](arg);
  9. } catch (error) {
  10. return reject(error);
  11. }
  12. const { value, done } = info;
  13. if (done) {
  14. return resolve(value);
  15. }
  16. Promise.resolve(value).then(
  17. v => step('next', v),
  18. e => step('throw', e)
  19. );
  20. }
  21. step('next');
  22. });
  23. }
  24. }
  25. // 使用示例
  26. const asyncFunc = asyncToGenerator(function* () {
  27. const a = yield 1;
  28. const b = yield a + 2;
  29. return b + 3;
  30. });
  31. asyncFunc().then(console.log); // 6

工作原理

  1. 将Generator函数包装为Promise
  2. 通过step函数控制执行流程
  3. 自动处理yield表达式的异步决议

五、算法与数据结构

1. 实现扁平化数组

需处理多级嵌套和类型判断。

  1. function flatten(arr) {
  2. let result = [];
  3. for (let item of arr) {
  4. if (Array.isArray(item)) {
  5. result = result.concat(flatten(item));
  6. } else {
  7. result.push(item);
  8. }
  9. }
  10. return result;
  11. }
  12. // 优化版(支持深度参数)
  13. function flattenDeep(arr, depth = 1) {
  14. return depth > 0
  15. ? arr.reduce((prev, cur) =>
  16. prev.concat(Array.isArray(cur) ? flattenDeep(cur, depth - 1) : cur),
  17. [])
  18. : arr.slice();
  19. }

应用场景

  • 处理API返回的嵌套数据
  • 数据预处理

2. 实现LRU缓存

需结合Map数据结构和双向链表实现高效操作。

  1. class LRUCache {
  2. constructor(capacity) {
  3. this.capacity = capacity;
  4. this.cache = new Map();
  5. }
  6. get(key) {
  7. if (this.cache.has(key)) {
  8. const value = this.cache.get(key);
  9. this.cache.delete(key);
  10. this.cache.set(key, value); // 重新插入实现最近使用
  11. return value;
  12. }
  13. return -1;
  14. }
  15. put(key, value) {
  16. if (this.cache.has(key)) {
  17. this.cache.delete(key);
  18. } else if (this.cache.size >= this.capacity) {
  19. const oldestKey = this.cache.keys().next().value;
  20. this.cache.delete(oldestKey);
  21. }
  22. this.cache.set(key, value);
  23. }
  24. }

性能分析

  • getput操作时间复杂度均为O(1)
  • 利用Map保持插入顺序的特性

六、备考建议

  1. 系统梳理知识体系:按数据类型、函数、异步、算法等维度分类整理
  2. 刻意练习:每天手写3-5道典型题,注重代码规范和边界处理
  3. 理解原理:不仅要会写,更要明白为什么这样实现
  4. 模拟面试:找同伴进行模拟面试,训练表达能力和临场反应
  5. 查看源码:研究Lodash、Axios等库的实现,学习最佳实践

通过系统准备和针对性练习,开发者可以显著提升手写JS题的解题能力,在面试中展现出扎实的基本功和良好的编码素养。记住,面试官考察的不仅是代码的正确性,更是解决问题的思路和工程化思维。

相关文章推荐

发表评论