xDocxDoc
AI
前端
后端
iOS
Android
Flutter
AI
前端
后端
iOS
Android
Flutter
  • JavaScript 100 题

    • JavaScript 面试 100 题及参考答案
  • HTML+CSS 50题

    • 前端面试之 HTML+CSS 50题及参考答案
  • Vue 50题

    • 前端面试之 Vue 50题及参考答案
  • React 50题

    • 前端面试之 React 50题及参考答案
  • 前端工程化50题

    • 前端面试之工程化50题及参考答案
  • 实践案例50题

    • 前端面试之实践案例50题及参考答案

JavaScript 面试 100 题及参考答案

💡 本文整理了 100 道 JavaScript 经典面试题,并提供了参考答案和原理解析。题目涵盖了 基础语法、核心概念、异步编程、浏览器原理 等前端面试的重要知识点。掌握这些内容将助你从容应对大多数 JavaScript 面试。

📘 一、基础与数据类型

  1. JavaScript 有哪些数据类型?如何准确判断 null 和 Array 的类型?

    • 答案:JavaScript 有 8种 基本数据类型:
      • 基本类型(Primitive):undefined、null、boolean、number、string、symbol (ES6)、bigint (ES2020)
      • 引用类型(Reference):Object(包含 Array, Function, Date 等)
    • 判断 null:使用 Object.prototype.toString.call(null) (返回 "[object Null]")
    • 判断 Array:
      • Array.isArray(arr) (推荐)
      • Object.prototype.toString.call(arr) === "[object Array]"
  2. typeof null 返回什么?为什么?

    • 答案:返回 "object"。这是 JavaScript 早期实现的一个历史遗留 Bug。在 JavaScript 最初的版本中,值是由一个表示类型的标签和实际数据值表示的,对象的标签是 0,而 null 被表示为空指针(通常是 0x00),因此 null 的标签也被误判为 0,所以 typeof 返回 "object"。
  3. == 和 === 有什么区别?

    • 答案:== 是抽象相等,会进行类型转换再比较值;=== 是严格相等,不会进行类型转换,要求值和类型都相同。
    • 例如:'5' == 5 // true (字符串 '5' 转数字后相等),而 '5' === 5 // false (类型不同)。
  4. undefined 和 null 有什么区别?

    • 答案:
      • undefined 表示变量已声明但未赋值,或函数未明确返回值时的默认返回值。
      • null 表示一个空值或对象指针为空,通常由程序员主动赋值来表示“无”的状态。
    • typeof undefined 为 'undefined',typeof null 为 'object'。
  5. 什么是 JavaScript 中的“假值”(Falsy)?列举所有。

    • 答案:在布尔值上下文中会被转换为 false 的值称为假值。包括:false、0、-0、""(空字符串)、null、undefined、NaN、BigInt 中的 0n。
  6. 如何将字符串转换为数字?有哪些方法?

    • 答案:
      • Number('123') → 123 (严格转换,失败返回 NaN)
      • parseInt('123px', 10) → 123 (解析整数,可指定进制)
      • parseFloat('12.34') → 12.34 (解析浮点数)
      • +'123' → 123 (一元正号运算符,等同于 Number())
      • Math.floor(), Math.ceil(), Math.round() 等(通常先转换再运算)
  7. isNaN() 和 Number.isNaN() 有什么区别?

    • 答案:
      • isNaN(value):会先将 value 转换为数值,再判断是否是 NaN。isNaN('abc') // true
      • Number.isNaN(value):不会转换,仅当 value 严格等于 NaN 时才返回 true。Number.isNaN('abc') // false。
  8. 为什么 0.1 + 0.2 !== 0.3?如何解决?

    • 答案:因为 JavaScript 中的数字遵循 IEEE 754 标准的 64 位双精度浮点数表示法。0.1 和 0.2 在二进制中是无限循环小数,计算时会产生精度损失。
    • 解决:
      • 使用 Number.EPSILON:Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON
      • 转换为整数计算后再转换:(0.1 * 10 + 0.2 * 10) / 10 === 0.3
      • 使用第三方库如 decimal.js 处理高精度计算。
  9. var, let, const 有什么区别?

    • 答案:
      特性varletconst
      作用域函数作用域块级作用域块级作用域
      变量提升是(初始化为undefined)是(但存在“暂时性死区”)是(但存在“暂时性死区”)
      重复声明允许不允许不允许
      值能否改变-能不能(但对象属性可修改)
  10. 什么是变量提升(Hoisting)?

    • 答案:JavaScript 引擎在解释代码时,会将变量和函数的声明提升到其所在作用域的顶部。需要注意的是:
      • var 声明的变量提升时初始化为 undefined。
      • let 和 const 也存在提升,但在声明之前访问会抛出 ReferenceError(称为“暂时性死区”,Temporal Dead Zone, TDZ)。
      • 函数声明整体会被提升,而函数表达式(如 var fn = function(){})则按变量提升规则处理。
  11. 什么是“暂时性死区”(Temporal Dead Zone)?

    • 答案:在代码块内,用 let 或 const 声明的变量,在声明之前不能被访问或使用,这段从块开始到声明完成的区域称为暂时性死区。这是为了减少运行时错误,使变量管理更加规范。
  12. 如何判断一个变量是否为数组?

    • 答案:
      • Array.isArray(arr) (最可靠、推荐)
      • Object.prototype.toString.call(arr) === '[object Array]' (兼容性好)
      • arr instanceof Array (在单一全局执行环境下有效,跨 frame 或 iframe 时会失效)
  13. 如何实现数组去重?

    • 答案:
      • ES6 Set:[...new Set(array)] 或 Array.from(new Set(array))
      • Filter + indexOf:array.filter((item, index) => array.indexOf(item) === index)
      • Reduce:array.reduce((unique, item) => unique.includes(item) ? unique : [...unique, item], [])
  14. map, filter, reduce 的作用是什么?

    • 答案:
      • map():遍历数组,对每个元素执行回调函数,返回由结果组成的新数组。
      • filter():遍历数组,过滤出使回调函数返回 true 的元素,组成新数组。
      • reduce():遍历数组,将每个元素依次传入回调函数,最终累积为一个单一值。
    • 示例:
      // map
      [1, 2, 3].map(x => x * 2); // [2, 4, 6]
      // filter
      [1, 2, 3, 4].filter(x => x % 2 === 0); // [2, 4]
      // reduce
      [1, 2, 3].reduce((sum, curr) => sum + curr, 0); // 6
  15. for-in, for-of 以及 forEach 有什么区别?

    • 答案:
      • for-in:用于遍历对象的可枚举属性(包括原型链上的)。遍历数组时通常不推荐,因为可能遍历到非数字键。
      • for-of (ES6):用于遍历可迭代对象(Array, Map, Set, String, arguments 等)的值。
      • forEach:是 Array 的方法,用于遍历数组的每个元素。无法使用 break 或 return 中断循环。
  16. 如何实现数组的扁平化(Flatten)?

    • 答案:
      • ES2019 flat():arr.flat(Infinity) (指定深度或 Infinity 无限级)
      • Reduce + Concat + 递归:
        function flattenDeep(arr) {
          return arr.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), []);
        }
      • toString (仅适用于数字或字符串数组):arr.toString().split(',').map(Number);
  17. Math.round(-2.5) 返回什么?为什么?

    • 答案:返回 -2。Math.round() 采用“四舍六入五成双”规则(或称“银行家舍入法”)。.5 时,会舍入到最接近的偶数。-2.5 处于 -2 和 -3 中间,-2 是偶数,所以返回 -2。
  18. NaN === NaN 的结果是什么?为什么?

    • 答案:false。根据 IEEE 754 标准,NaN 代表一个非数字值,它不等于任何值,包括它自己。判断一个值是否为 NaN,应使用 Number.isNaN(value) 或 Object.is(value, NaN)。
  19. '5' - true 的结果是什么?

    • 答案:4。- 运算符会尝试将操作数转换为数字。'5' 转数字为 5,true 转数字为 1,所以 5 - 1 = 4。
  20. 如何安全地访问一个对象的深层属性(如 obj.a.b.c)?

    • 答案:
      • 可选链操作符(Optional Chaining) (ES2020):obj?.a?.b?.c (如果中间某个属性为 null 或 undefined,则返回 undefined 而非报错)。
      • 逻辑与短路:obj && obj.a && obj.a.b && obj.a.b.c (ES6 及之前)。

🔨 二、函数与作用域

  1. 什么是闭包(Closure)?举例说明其用途和可能的问题。

    • 答案:闭包是指能够访问另一个函数作用域中变量的函数。它是由函数和声明该函数的词法环境组合而成。
    • 用途:
      • 创建私有变量和方法(模块模式)。
      • 实现函数柯里化和偏应用。
      • 在异步回调中保存状态(例如循环中的 setTimeout)。
    • 示例(计数器):
      function createCounter() {
        let count = 0; // 私有变量
        return function() {
          return ++count;
        };
      }
      const counter = createCounter();
      console.log(counter()); // 1
      console.log(counter()); // 2
    • 可能的问题:如果不慎,闭包可能导致内存泄漏,因为外部函数的变量会一直被内部函数引用,无法被垃圾回收。
  2. 什么是立即调用函数表达式(IIFE)?它有什么用途?

    • 答案:IIFE 是定义后立即执行的函数。
      (function() {
        // 代码块
      })();
    • 用途:
      • 创建一个独立的作用域,避免污染全局命名空间。
      • 封装私有变量,这些变量在函数外部无法访问。
      • 在模块化规范出现之前,它是一种重要的模块化手段。
  3. 箭头函数和普通函数(使用 function 关键字声明的函数)有什么区别?

    • 答案:
      特性箭头函数普通函数
      this 指向继承自定义时的外层词法作用域的 this由调用方式决定(动态绑定)
      能否作为构造函数否(使用 new 调用会报错)是
      arguments 对象没有,需使用 rest 参数 (...args)有
      prototype 属性没有有
      生成器函数 (function*)不能使用 yield可以
  4. 解释 JavaScript 中的作用域(Scope)和作用域链(Scope Chain)。

    • 答案:
      • 作用域:规定了变量和函数的可访问范围。主要有:全局作用域、函数作用域、块级作用域(由 let/const 和 {} 产生)。
      • 作用域链:当访问一个变量时,JavaScript 引擎会首先在当前作用域查找。如果没找到,会沿着外层作用域逐层向上查找,直到全局作用域。这种链式关系称为作用域链。它是在函数定义时就确定的(词法作用域/静态作用域)。
  5. 解释 JavaScript 中的执行上下文(Execution Context)和调用栈(Call Stack)。

    • 答案:
      • 执行上下文:是 JavaScript 代码执行时的环境,包含了变量、函数、参数等信息。主要分为:
        • 全局执行上下文:最外层的环境。
        • 函数执行上下文:每次函数调用都会创建一个新的。
        • Eval 执行上下文(较少使用)。
      • 调用栈:是一种 LIFO(后进先出)的栈结构,用于管理执行上下文的创建和销毁。当函数被调用时,其执行上下文被推入栈;当函数执行完毕,其上下文从栈中弹出。
  6. call, apply, bind 的区别是什么?

    • 答案:三者都用于显式地设置函数内部的 this 指向。
      • func.call(thisArg, arg1, arg2, ...):立即调用函数,参数以逗号分隔的列表传递。
      • func.apply(thisArg, [argsArray]):立即调用函数,参数以数组形式传递。
      • func.bind(thisArg, arg1, arg2, ...):不会立即调用,而是返回一个绑定了 this 和部分参数(柯里化)的新函数。
    • 示例:
      const person = { name: 'Alice' };
      function greet(greeting) { console.log(`${greeting}, ${this.name}!`); }
      
      greet.call(person, 'Hello'); // Hello, Alice!
      greet.apply(person, ['Hi']); // Hi, Alice!
      const boundGreet = greet.bind(person, 'Hey');
      boundGreet(); // Hey, Alice!
  7. 什么是函数柯里化(Currying)?实现一个 add 函数使得 add(1)(2)(3)() 返回 6。

    • 答案:柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下参数且返回结果的新函数的技术。
    • 实现:
      function add(a) {
        return function(b) {
          if (b === undefined) return a;
          return add(a + b);
        };
      }
      console.log(add(1)(2)(3)()); // 6
      或者使用更简洁的写法(需要已知参数个数):
      const add = a => b => c => a + b + c;
      console.log(add(1)(2)(3)); // 6
  8. 什么是纯函数(Pure Function)?它有什么好处?

    • 答案:纯函数是指满足以下两个条件的函数:
      1. 相同的输入,永远会得到相同的输出(确定性)。
      2. 在执行过程中没有任何可观察的副作用(不改变外部状态,不修改传入参数,无 I/O 操作等)。
    • 好处:可缓存(Memoization)、易于测试、无竞争条件、易于推理和重构。是函数式编程的核心概念。
  9. 什么是函数的重载(Overload)?JavaScript 中有函数重载吗?

    • 答案:函数重载是指在同一作用域内定义多个同名函数,但它们的参数类型或数量不同。JavaScript 本身并不支持传统意义上的函数重载,因为后定义的函数会覆盖先定义的。
    • 模拟重载:通常通过在函数内部检查 arguments 对象或 rest 参数的长度和类型来实现不同行为。
      function overloaded() {
        if (arguments.length === 1) {
          // 处理一个参数的情况
        } else if (arguments.length === 2) {
          // 处理两个参数的情况
        }
      }
  10. 解释尾调用(Tail Call)和尾调用优化(Tail Call Optimization, TCO)。

    • 答案:
      • 尾调用:指一个函数的最后一步是调用另一个函数。return func(x); 是尾调用,而 return func(x) + 1; 不是。
      • 尾调用优化:在严格模式下,如果满足尾调用条件,引擎会复用当前函数的调用栈帧来执行尾调用函数,而不是新建一个。这样可以避免调用栈无限增长(栈溢出),特别适用于递归。
    • 示例(未优化 vs 优化):
      // 未优化:栈帧会持续增长
      function factorial(n) {
        if (n === 1) return 1;
        return n * factorial(n - 1); // 最后一步是乘法,不是纯函数调用
      }
      // 优化后:可被 TCO
      function factorial(n, total = 1) {
        if (n === 1) return total;
        return factorial(n - 1, n * total); // 最后一步是函数调用
      }
      注意:虽然 ES6 规范定义了 TCO,但并非所有 JavaScript 引擎都实现了它。
  11. 什么是“副作用”(Side Effect)?列举几个有副作用的操作。

    • 答案:函数副作用是指函数在运行过程中,修改了外部状态或与外部进行了可观察的交互。例如:
      • 修改全局变量或外部对象的属性。
      • 调用 console.log 或 alert。
      • 发起 HTTP 请求(AJAX/Fetch)。
      • 操作 DOM。
      • 读取或写入文件(Node.js)。
  12. arguments 对象是什么?它是一个数组吗?

    • 答案:arguments 是一个类数组对象(Array-like),它包含了函数被调用时传入的所有参数。它不是真正的数组,没有数组的方法(如 forEach, map)。
    • 转换为数组:
      • Array.from(arguments)
      • [...arguments] (ES6 展开运算符)
    • 注意:在箭头函数中,arguments 指向的是外层函数的 arguments。箭头函数内部应使用 rest 参数 (...args) 来获取参数。
  13. Rest 参数(...)和 arguments 对象有什么区别?

    • 答案:
      特性Rest 参数 (...args)arguments 对象
      类型真正的数组类数组对象
      包含所有参数?只包含没有对应形参的实参包含所有实参
      箭头函数中可用是否(指向外层函数的 arguments)
      数组方法可直接使用 (forEach, map)需先转换为数组
  14. 什么是“一等公民”(First-class Citizen)?为什么说函数在 JavaScript 中是一等公民?

    • 答案:如果一个编程实体(如函数)可以像其他变量一样被赋值给变量、作为参数传递、作为返回值返回,那么它就被称为“一等公民”。在 JavaScript 中,函数满足所有这些条件,因此是一等公民。这是支持函数式编程的基础。
  15. 什么是高阶函数(Higher-order Function)?

    • 答案:高阶函数是操作函数的函数。它至少满足以下一个条件:
      • 接受一个或多个函数作为参数(例如 Array.prototype.map, filter, setTimeout)。
      • 返回一个新的函数(例如 Function.prototype.bind)。

⏳ 三、异步编程

  1. JavaScript 是单线程还是多线程?如何实现异步操作?

    • 答案:JavaScript 是单线程的。这意味着它只有一个主线程(Main Thread)来执行代码。为了避免阻塞,它通过事件循环(Event Loop) 和任务队列(Task Queue) 机制来实现异步操作。异步任务(如定时器、网络请求)会被放入任务队列,等待主线程的同步任务执行完毕后,事件循环会从任务队列中取出异步任务执行。
  2. 解释事件循环(Event Loop)模型。宏任务和微任务有什么区别?

    • 答案:事件循环是 JavaScript 处理异步任务的机制。它持续检查调用栈是否为空,如果为空,就从任务队列中取出任务执行。
      • 宏任务(MacroTask):包括 setTimeout, setInterval, setImmediate (Node.js), I/O, UI rendering, 网络请求(AJAX)等。
      • 微任务(MicroTask):包括 Promise.then(), Promise.catch(), Promise.finally(), process.nextTick (Node.js), MutationObserver 等。
    • 区别与执行顺序:
      1. 执行一个宏任务(从宏任务队列中获取)。
      2. 执行过程中遇到微任务,将其添加到微任务队列。
      3. 当前宏任务执行完毕后,立即依次执行所有微任务队列中的微任务。
      4. 微任务清空后,进行 UI 渲染(如果需要)。
      5. 开始下一个宏任务(从宏任务队列中获取)。
    • 示例代码分析输出顺序:
      console.log('1'); // 同步任务
      
      setTimeout(() => {
        console.log('2'); // 宏任务
      }, 0);
      
      Promise.resolve().then(() => {
        console.log('3'); // 微任务
      });
      
      console.log('4'); // 同步任务
      // 输出顺序: 1 -> 4 -> 3 -> 2
  3. setTimeout(fn, 0) 有什么含义?它是立即执行吗?

    • 答案:setTimeout(fn, 0) 并不意味着立即执行。它的含义是:指定函数 fn 在当前所有同步任务和微任务执行完毕后,且尽可能早地被添加到宏任务队列中执行。它用于将任务推迟到下一个宏任务中执行,常用于调整执行顺序或避免阻塞。
  4. 什么是 Promise?它有哪些状态?

    • 答案:Promise 是一个对象,用于表示一个异步操作的最终完成(或失败)及其结果值。
    • 三种状态:
      • pending(待定):初始状态。
      • fulfilled(已兑现):意味着操作成功完成。通过 resolve(value) 进入此状态。
      • rejected(已拒绝):意味着操作失败。通过 reject(reason) 进入此状态。
    • 特点:状态一旦改变(从 pending 变为 fulfilled 或 rejected),就不能再变。
  5. 手写一个符合 Promises/A+ 规范的 Promise。

    • 答案:这是一个简化的实现,展示了核心逻辑:
      class MyPromise {
        constructor(executor) {
          this.state = 'pending';
          this.value = undefined;
          this.reason = undefined;
          this.onFulfilledCallbacks = [];
          this.onRejectedCallbacks = [];
      
          const resolve = (value) => {
            if (this.state === 'pending') {
              this.state = 'fulfilled';
              this.value = value;
              this.onFulfilledCallbacks.forEach(fn => fn());
            }
          };
      
          const reject = (reason) => {
            if (this.state === 'pending') {
              this.state = 'rejected';
              this.reason = reason;
              this.onRejectedCallbacks.forEach(fn => fn());
            }
          };
      
          try {
            executor(resolve, reject);
          } catch (err) {
            reject(err);
          }
        }
      
        then(onFulfilled, onRejected) {
          if (this.state === 'fulfilled') {
            onFulfilled(this.value);
          } else if (this.state === 'rejected') {
            onRejected(this.reason);
          } else if (this.state === 'pending') {
            this.onFulfilledCallbacks.push(() => onFulfilled(this.value));
            this.onRejectedCallbacks.push(() => onRejected(this.reason));
          }
        }
      }
  6. Promise.all(), Promise.race(), Promise.allSettled(), Promise.any() 有什么区别?

    • 答案:
      • Promise.all(iterable):所有 Promise 都成功时,返回一个成功状态的结果数组;如果有一个失败,则立即返回失败的原因。
      • Promise.race(iterable):返回第一个 settled(无论成功或失败)的 Promise 的结果。
      • Promise.allSettled(iterable) (ES2020):等待所有 Promise 都 settled(无论成功或失败),返回一个对象数组,每个对象描述每个 Promise 的结果。
      • Promise.any(iterable) (ES2021):等待第一个成功的 Promise。如果所有都失败,则返回一个 AggregateError。
  7. 如何中断或取消一个 Promise 链?

    • 答案:Promise 本身无法被直接取消。但可以通过以下方式模拟中断:
      1. 返回一个永不清除的 Promise:在链中的某个 then 或 catch 中,返回一个永远处于 pending 状态的 Promise,这样后续的 then 将不会被执行。
        promise
          .then(() => {
            // 条件中断
            if (someCondition) {
              return new Promise(() => {}); // 永不清除,中断链
            }
            return continueWork();
          })
          .then(() => {
            // 这个 then 不会执行如果被中断了
          });
      2. 使用 AbortController (配合 fetch 等):对于特定的异步操作如 fetch,可以使用 AbortController 来中止请求,这会导致 Promise 拒绝。
  8. async/await 的本质是什么?它的优势是什么?

    • 答案:async/await 是 Generator 和 Promise 的语法糖,它使得异步代码的写法更像同步代码,提高了代码的可读性和可维护性。
    • 本质:
      • async 函数总是返回一个 Promise 对象。
      • await 后面可以跟一个 Promise 对象或任何值。如果是 Promise,它会等待 Promise 解析;如果不是,会被 Promise.resolve() 包装。
    • 优势:
      • 代码清晰:避免了回调地狱和复杂的 Promise 链。
      • 错误处理:可以使用传统的 try/catch 来捕获异步错误,而不是 .catch()。
      • 调试方便:错误堆栈更清晰。
  9. async/await 和 Generator 函数有什么关系?

    • 答案:async/await 可以看作是 Generator 函数的语法糖和自动化执行器。
      • async 函数相当于 Generator 函数的 *。
      • await 命令相当于 yield 命令。
      • async 函数自带执行器,不需要像 co 模块那样的工具来自动执行 Generator。
    • 示例对比:
      // Generator + co
      const co = require('co');
      function* fetchData() {
        const data = yield fetch('/api');
        return data;
      }
      co(fetchData);
      
      // async/await
      async function fetchData() {
        const data = await fetch('/api');
        return data;
      }
      fetchData(); // 自动执行
  10. 在 async 函数中,多个 await 是并行还是串行?如何实现并行?

    • 答案:多个 await 默认是串行执行,即一个等待完成后再执行下一个。
    • 实现并行:使用 Promise.all() 来同时触发多个异步操作。
    • 示例:
      // 串行 (慢)
      async function serial() {
        const result1 = await fetch(url1);
        const result2 = await fetch(url2);
        return [result1, result2];
      }
      
      // 并行 (快)
      async function parallel() {
        const [result1, result2] = await Promise.all([fetch(url1), fetch(url2)]);
        return [result1, result2];
      }
  11. setTimeout 和 setInterval 有什么问题?如何实现一个精确的定时器?

    • 答案:
      • 问题:它们指定的延迟时间只是最少延迟时间,而不是精确时间。因为事件循环机制,如果主线程上有长时间运行的同步任务或微任务,定时器回调会被推迟执行。
      • 实现精确定时器:可以通过计算实际延迟与预期延迟的差值,动态调整下一次的延迟时间。但请注意,在 JavaScript 中实现高精度定时非常困难且不推荐,通常用于动画的 requestAnimationFrame 是更好的选择。
  12. 什么是回调地狱(Callback Hell)?如何解决?

    • 答案:回调地狱是指多个异步操作嵌套调用,导致代码形成金字塔形状,难以阅读和维护。
      // 回调地狱示例
      asyncFunc1(function(err, result1) {
        if (err) { /* handle error */ }
        asyncFunc2(result1, function(err, result2) {
          if (err) { /* handle error */ }
          asyncFunc3(result2, function(err, result3) {
            if (err) { /* handle error */ }
            // ... more nesting
          });
        });
      });
    • 解决方案:
      • 使用 Promise:通过 .then() 的链式调用扁平化代码。
      • 使用 async/await:使异步代码看起来像同步代码,彻底解决嵌套问题。
      • 模块化:将回调函数拆分为独立的命名函数。
  13. process.nextTick 和 setImmediate 有什么区别?(Node.js 环境)

    • 答案:(此题为 Node.js 特定)
      • process.nextTick(callback):将 callback 添加到 当前事件循环阶段结束后、下一个阶段开始前 的“nextTick 队列”。这个队列的优先级高于微任务队列。不推荐滥用,因为它会阻塞事件循环。
      • setImmediate(callback):将 callback 添加到当前事件循环的 Check 阶段的队列中,在 I/O 回调之后执行。它的优先级类似于 setTimeout(fn, 0)。
  14. requestAnimationFrame 属于宏任务还是微任务?

    • 答案:requestAnimationFrame 的执行时机与浏览器渲染相关,通常被认为是一种特殊的宏任务。它的回调函数会在浏览器下一次重绘(repaint)之前执行,通常频率为每秒 60 次(16.7ms/帧)。它的优先级高于普通的宏任务(如 setTimeout)。
  15. 如何实现一个简单的 sleep 函数?

    • 答案:
      // 使用 Promise 和 setTimeout
      function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
      }
      
      // 用法 (在 async 函数中)
      async function demo() {
        console.log('开始等待...');
        await sleep(2000); // 等待 2 秒
        console.log('2秒后');
      }
      demo();

🔗 四、对象与原型

  1. 创建对象有哪几种方式?

    • 答案:
      • 对象字面量:const obj = {};
      • new Object():const obj = new Object();
      • 构造函数:
        function Person(name) { this.name = name; }
        const person = new Person('John');
      • Object.create():const obj = Object.create(proto);
      • 类 (ES6):class Person { constructor(name) { this.name = name; } } const person = new Person('John');
  2. Object.create(null) 和 {} 创建的对象有什么区别?

    • 答案:
      • Object.create(null) 创建一个没有原型的对象。它不继承 Object.prototype 的任何方法(如 toString, hasOwnProperty)。
      • {}(对象字面量)创建的对象继承自 Object.prototype,拥有所有对象的基础方法。
    • 用途:Object.create(null) 常用于创建纯粹的“字典”或“映射”,避免原型上的属性被意外覆盖或遍历到。
  3. 什么是原型(Prototype)?什么是原型链(Prototype Chain)?

    • 答案:每个 JavaScript 对象(除 null 外)都有一个内部属性 [[Prototype]](可通过 __proto__ 或 Object.getPrototypeOf() 访问),它指向另一个对象,这个对象就是它的原型。
      • 原型链:当访问一个对象的属性时,如果该对象自身没有这个属性,就会沿着它的 [[Prototype]] 向上查找,直到找到该属性或到达链的尽头(null)。这种链式结构就是原型链。
    • 示例:
      const parent = { name: 'Parent' };
      const child = Object.create(parent);
      child.age = 10;
      
      console.log(child.age); // 10 (自身属性)
      console.log(child.name); // 'Parent' (通过原型链找到)
      console.log(child.toString); // ƒ toString() (通过原型链找到 Object.prototype 的方法)
  4. 如何实现继承?ES5 和 ES6 的继承方式有什么区别?

    • 答案:
      • ES5 继承(以组合继承为例):
        function Parent(name) { this.name = name; }
        Parent.prototype.sayName = function() { console.log(this.name); };
        
        function Child(name, age) {
          Parent.call(this, name); // 继承属性
          this.age = age;
        }
        Child.prototype = Object.create(Parent.prototype); // 继承方法
        Child.prototype.constructor = Child; // 修复 constructor 指向
        
        const child = new Child('Jack', 12);
        child.sayName(); // 'Jack'
      • ES6 继承:
        class Parent {
          constructor(name) { this.name = name; }
          sayName() { console.log(this.name); }
        }
        
        class Child extends Parent {
          constructor(name, age) {
            super(name); // 调用父类的 constructor
            this.age = age;
          }
        }
        
        const child = new Child('Jack', 12);
        child.sayName(); // 'Jack'
    • 区别:ES6 的 class 和 extends 是语法糖,本质上还是基于原型链,但写法更简洁,更易于理解。
  5. instanceof 运算符的原理是什么?如何手动实现一个 instanceof?

    • 答案:obj instanceof Constructor 的原理是:检查 Constructor.prototype 是否出现在 obj 的原型链上。
    • 手动实现:
      function myInstanceof(obj, Constructor) {
        let proto = Object.getPrototypeOf(obj); // 获取 obj 的 [[Prototype]]
        while (proto !== null) {
          if (proto === Constructor.prototype) return true;
          proto = Object.getPrototypeOf(proto); // 继续向上查找
        }
        return false;
      }
      
      console.log(myInstanceof([], Array)); // true
      console.log(myInstanceof([], Object)); // true
  6. 如何实现一个深拷贝(Deep Clone)函数?

    • 答案:深拷贝会创建一个新对象,并递归地拷贝源对象的所有属性,完全独立于原对象。
      function deepClone(obj, hash = new WeakMap()) {
        if (obj === null || typeof obj !== 'object') return obj; // 基础类型直接返回
        if (obj instanceof Date) return new Date(obj); // 处理 Date
        if (obj instanceof RegExp) return new RegExp(obj); // 处理 RegExp
        if (hash.has(obj)) return hash.get(obj); // 解决循环引用
      
        const cloneObj = new obj.constructor(); // 创建一个同类型的新对象
        hash.set(obj, cloneObj);
      
        for (let key in obj) {
          if (obj.hasOwnProperty(key)) {
            cloneObj[key] = deepClone(obj[key], hash); // 递归拷贝
          }
        }
        return cloneObj;
      }
      
      // 使用
      const obj = { a: 1, b: { c: 2 } };
      const clonedObj = deepClone(obj);
      clonedObj.b.c = 3;
      console.log(obj.b.c); // 2 (原对象未被修改)
    • 简单方法(有局限):JSON.parse(JSON.stringify(obj))。此法无法拷贝函数、undefined、Symbol 和循环引用的对象。
  7. Object.assign() 是深拷贝还是浅拷贝?

    • 答案:Object.assign() 实现的是浅拷贝。它只会将源对象自身的可枚举属性拷贝到目标对象。如果源对象的属性值是一个对象,那么拷贝的是这个对象的引用。
      const obj = { a: 1, b: { c: 2 } };
      const shallowCopy = Object.assign({}, obj);
      shallowCopy.b.c = 3;
      console.log(obj.b.c); // 3 (原对象被修改了!)
  8. for...in, Object.keys(), Object.getOwnPropertyNames() 有什么区别?

    • 答案:
      • for...in:遍历对象自身和原型链上的可枚举属性(不包括 Symbol 属性)。
      • Object.keys(obj):返回一个数组,包含对象自身的所有可枚举属性的键名(不包括 Symbol 属性)。
      • Object.getOwnPropertyNames(obj):返回一个数组,包含对象自身的所有属性(包括不可枚举属性,但不包括 Symbol 属性)的键名。
      • Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有 Symbol 属性的键名。
      • Reflect.ownKeys(obj):返回一个数组,包含对象自身的所有键名(包括可枚举、不可枚举、Symbol 属性)。
  9. 如何阻止对象扩展、密封对象、冻结对象?

    • 答案:
      • Object.preventExtensions(obj):阻止对象添加新属性。
      • Object.seal(obj):在 preventExtensions 的基础上,同时将所有现有属性标记为 configurable: false(不可删除、不可重新配置)。
      • Object.freeze(obj):在 seal 的基础上,同时将所有现有属性标记为 writable: false(不可修改)。这是最高级别的不可变性。
    • 检查方法:
      • Object.isExtensible(obj)
      • Object.isSealed(obj)
      • Object.isFrozen(obj)
  10. new 操作符背后做了什么?

    • 答案:
      1. 创建一个空的简单 JavaScript 对象 {}。
      2. 将这个空对象的 [[Prototype]](__proto__)链接到构造函数的 prototype 对象。
      3. 将步骤1新创建的对象作为 this 上下文,执行构造函数。
      4. 如果构造函数没有显式返回一个对象,则返回 this(即新创建的对象)。

🌐 五、DOM 与 BOM

  1. 什么是 DOM?什么是虚拟 DOM(Virtual DOM)?

    • 答案:
      • DOM (Document Object Model):是浏览器提供的、用于操作 HTML 和 XML 文档的编程接口。它将文档解析为一个由节点和对象组成的树形结构,允许脚本动态地访问和更新文档的内容、结构和样式。
      • 虚拟 DOM:是一个轻量级的 JavaScript 对象,它是真实 DOM 的抽象。当应用状态发生变化时,先在虚拟 DOM 上进行操作和比较(Diff算法),然后高效地将最小变化批量更新到真实 DOM,减少直接操作真实 DOM 带来的性能开销。React、Vue 等框架都使用了此概念。
  2. document.load 和 document.DOMContentLoaded 事件有什么区别?

    • 答案:
      • DOMContentLoaded:当初始的 HTML 文档被完全加载和解析完成(不包括样式表、图片等外部资源)时触发。意味着 DOM 树已经构建完成。
      • load:当整个页面(包括所有依赖资源,如样式表、图片、iframe)已完全加载时触发。
  3. 事件流(Event Flow)是什么?事件冒泡和事件捕获有什么区别?

    • 答案:事件流描述了事件在 DOM 结构中传播的顺序。
      • 事件捕获(Capturing Phase):事件从 window 向下传播到目标元素。
      • 目标阶段(Target Phase):事件到达目标元素。
      • 事件冒泡(Bubbling Phase):事件从目标元素向上传播回 window。
    • 区别:传播方向相反。可以使用 addEventListener 的第三个参数来指定在哪个阶段处理事件:true(捕获)或 false(冒泡,默认)。
  4. 什么是事件委托(Event Delegation)?它有什么好处?

    • 答案:事件委托是利用事件冒泡机制,将事件监听器绑定在父元素上,而不是每个子元素上。通过 event.target 来判断事件的实际触发元素。
    • 好处:
      • 减少内存消耗:只需一个事件监听器管理多个子元素。
      • 动态元素处理:对后来动态添加的子元素同样有效,无需重新绑定事件。
  5. 如何阻止事件冒泡和默认行为?

    • 答案:
      • 阻止事件冒泡:event.stopPropagation()
      • 阻止默认行为:event.preventDefault()(如表单提交、链接跳转)
      • 停止所有监听器:event.stopImmediatePropagation()(阻止同一事件的其他监听器被调用)
  6. addEventListener 的参数有哪些?

    • 答案:
      element.addEventListener(eventType, listener, {
        capture: false, // 是否在捕获阶段触发
        once: false,    // 是否只触发一次后自动移除
        passive: false // 表示 listener 永远不会调用 preventDefault()
      });
      passive: true 通常用于触摸和滚轮事件,以提高滚动性能。
  7. window 和 document 对象有什么区别?

    • 答案:
      • window:是浏览器窗口的全局对象,代表一个浏览器窗口或标签页。所有全局变量和函数都是 window 对象的属性。它包含 document, location, history, navigator 等子对象。
      • document:是 window 的一个属性,代表当前加载的 HTML 文档。它是 DOM 树的入口点,提供了操作页面内容的方法和属性。
  8. location 对象包含哪些常用属性?如何解析 URL?

    • 答案:location 对象包含当前页面的 URL 信息。
      • location.href:完整的 URL。
      • location.protocol:协议(如 https:)。
      • location.host:主机名和端口。
      • location.hostname:主机名。
      • location.port:端口号。
      • location.pathname:路径部分。
      • location.search:查询字符串(? 之后的部分)。
      • location.hash:片段标识符(# 之后的部分)。
    • 解析 URL:可以直接访问这些属性,也可以使用 new URL(urlString) API。
  9. navigator 对象和 history 对象有什么用?

    • 答案:
      • navigator:提供关于浏览器和操作系统的信息。
        • navigator.userAgent:浏览器用户代理字符串。
        • navigator.platform:操作系统平台。
        • navigator.language:用户首选语言。
      • history:操作浏览器的会话历史(前进、后退)。
        • history.back():后退一页。
        • history.forward():前进一页。
        • history.go(n):前进或后退 n 页。
        • history.pushState(state, title, url):添加一条历史记录,不刷新页面。
        • history.replaceState(state, title, url):替换当前历史记录,不刷新页面。
  10. Cookie、LocalStorage、SessionStorage、IndexedDB 有什么区别?

    • 答案:
      特性CookieLocalStorageSessionStorageIndexedDB
      容量~4KB~5MB 或更多~5MB 或更多大量(通常占可用磁盘空间的50%)
      生命周期可设置过期时间永久,除非手动删除标签页关闭后失效永久,除非手动删除
      与服务端交互每次 HTTP 请求都会自动携带在 Header 中,影响性能不参与不参与不参与
      存储类型字符串字符串(需序列化对象)字符串(需序列化对象)多种类型(对象、文件等)
      API 易用性复杂简单 (setItem, getItem)简单 (setItem, getItem)复杂(异步 API)

🆕 六、ES6+ 与新特性

  1. let, const 和 var 的区别已经在第9题阐述。

    • 答案:参见第9题。
  2. 模板字符串(Template Literals)有什么优势?

    • 答案:
      • 字符串插值:使用 ${expression} 语法嵌入变量或表达式,更简洁。
        const name = 'World';
        console.log(`Hello, ${name}!`); // Hello, World!
      • 多行字符串:可以直接换行,无需使用 \n 或字符串连接符 +。
        const multiLine = `
          This is
          a multi-line
          string.
        `;
      • 标签模板(Tagged Templates):可以用函数解析模板字符串。
        function tag(strings, ...values) {
          console.log(strings); // ["Hello ", "!"]
          console.log(values); // ["World"]
          return "Processed string";
        }
        const result = tag`Hello ${'World'}!`;
        console.log(result); // Processed string
  3. 箭头函数(Arrow Functions)的特点和注意事项已在第23题阐述。

    • 答案:参见第23题。
  4. 解构赋值(Destructuring Assignment)有哪些用法?

    • 答案:
      • 数组解构:
        const [a, b, ...rest] = [1, 2, 3, 4];
        console.log(a); // 1
        console.log(b); // 2
        console.log(rest); // [3, 4]
      • 对象解构:
        const { name, age: years } = { name: 'Alice', age: 25 };
        console.log(name); // Alice
        console.log(years); // 25 (重命名)
      • 函数参数解构:
        function greet({ name, greeting = 'Hello' }) {
          console.log(`${greeting}, ${name}!`);
        }
        greet({ name: 'Bob' }); // Hello, Bob!
  5. Rest 参数和 Spread 语法有什么区别?

    • 答案:
      • Rest 参数 (...rest):用于函数定义中,将多余的参数收集到一个数组中。
        function sum(a, b, ...others) { return a + b + others.reduce((s, n) => s + n, 0); }
        sum(1, 2, 3, 4); // 10
      • Spread 语法 (...array):用于函数调用或数组字面量中,将一个可迭代对象(如数组)展开。
        const arr1 = [1, 2];
        const arr2 = [3, 4];
        const merged = [...arr1, ...arr2]; // [1, 2, 3, 4]
        Math.max(...arr1); // 2
  6. Set, Map, WeakSet, WeakMap 有什么区别?

    • 答案:
      • Set:成员值唯一、无序的集合。
      • Map:键值对的集合,键可以是任何类型(对象、函数等)。
      • WeakSet:成员只能是对象的 Set。对象是弱引用,不计入垃圾回收机制。无法遍历,没有 size 属性。
      • WeakMap:键只能是对象的 Map。键是弱引用。无法遍历,没有 size 属性。
    • 用途:
      • Set:数组去重、存储唯一值。
      • Map:需要用非字符串作为键的字典。
      • WeakSet/WeakMap:主要用于存储与对象关联的元数据,而不会阻止对象被垃圾回收(例如 DOM 元素关联的数据)。
  7. Symbol 数据类型有什么作用?

    • 答案:Symbol 是 ES6 引入的一种新的原始数据类型,表示独一无二的值。
    • 主要用途:
      • 作为唯一的对象属性名:防止属性名冲突。
        const mySymbol = Symbol('myKey');
        const obj = {};
        obj[mySymbol] = 'secret value';
      • 定义对象的内部方法:如 Symbol.iterator(定义迭代器)、Symbol.toStringTag 等。
  8. 什么是迭代器(Iterator)和生成器(Generator)?

    • 答案:
      • 迭代器:是一个对象,它实现了一个 next() 方法,该方法返回 { value: any, done: boolean }。实现了 [Symbol.iterator] 方法的对象是可迭代的(如 Array, Map, Set)。
      • 生成器:是一种特殊的函数,使用 function* 定义。调用生成器函数返回一个生成器对象(也是迭代器)。通过 yield 关键字可以暂停和恢复函数的执行。
        function* idGenerator() {
          let id = 1;
          while (true) {
            yield id++;
          }
        }
        const gen = idGenerator();
        console.log(gen.next().value); // 1
        console.log(gen.next().value); // 2
  9. Proxy 和 Reflect 对象有什么用?

    • 答案:
      • Proxy:用于创建一个对象的代理,从而可以拦截和自定义对象的基本操作(如属性查找、赋值、枚举、函数调用等)。
        const target = {};
        const handler = {
          get(obj, prop) {
            return prop in obj ? obj[prop] : 'Default Value';
          }
        };
        const proxy = new Proxy(target, handler);
        proxy.foo = 'bar';
        console.log(proxy.foo); // bar
        console.log(proxy.unknown); // Default Value
      • Reflect:提供了一个拦截 JavaScript 操作的方法的集合。这些方法与 Proxy 的拦截器方法一一对应。通常与 Proxy 配合使用,提供默认行为。
  10. ES Module 和 CommonJS 有什么区别?

    • 答案:
      特性ES Module (ESM)CommonJS (CJS)
      加载方式静态(编译时)动态(运行时)
      输出值的引用(只读)值的拷贝
      导入import { foo } from 'module'const { foo } = require('module')
      导出export, export defaultmodule.exports, exports
      顶层 thisundefined指向当前模块的 exports
      循环依赖处理更优(动态引用)可能存在问题(值可能未完全初始化)
      主要环境浏览器和现代 Node.jsNode.js
    • Node.js 中支持:在 package.json 中设置 "type": "module" 可使用 ESM,文件扩展名为 .mjs 也表示 ESM。

🛠️ 七、编码与设计

  1. 什么是防抖(Debounce)和节流(Throttle)?它们的应用场景是什么?

    • 答案:
      • 防抖:高频事件触发后,在指定的时间间隔内只执行最后一次。如果在这段时间内事件再次被触发,则重新计时。
        • 应用:搜索框输入联想、窗口 resize 结束后的操作。
      • 节流:高频事件触发后,在指定的时间间隔内只执行第一次。确保函数每隔一段时间执行一次。
        • 应用:滚动加载、按钮频繁点击、鼠标移动。
    • 简单实现:
      // 防抖
      function debounce(fn, delay) {
        let timer;
        return function(...args) {
          clearTimeout(timer);
          timer = setTimeout(() => fn.apply(this, args), delay);
        };
      }
      
      // 节流
      function throttle(fn, delay) {
        let canRun = true;
        return function(...args) {
          if (!canRun) return;
          canRun = false;
          setTimeout(() => {
            fn.apply(this, args);
            canRun = true;
          }, delay);
        };
      }
  2. 什么是函数式编程?柯里化和组合(Compose)是什么?

    • 答案:
      • 函数式编程 (FP):是一种编程范式,主要思想是将计算过程视为数学函数的求值,避免状态变化和可变数据。核心概念包括:纯函数、不可变性、高阶函数、柯里化、函数组合等。
      • 柯里化 (Currying):参见第27题。
      • 组合 (Compose):将多个函数组合成一个新函数,数据从右向左流经这些函数。
        const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
        const add1 = x => x + 1;
        const double = x => x * 2;
        const addThenDouble = compose(double, add1);
        console.log(addThenDouble(5)); // (5 + 1) * 2 = 12
  3. 什么是 MVC、MVP、MVVM 模式?

    • 答案:这些都是前端架构模式,旨在分离UI、数据和逻辑。
      • MVC (Model-View-Controller):
        • Model:数据和业务逻辑。
        • View:用户界面。
        • Controller:接收用户输入,更新 Model 和 View。
        • 通信:View -> Controller -> Model -> View。
      • MVP (Model-View-Presenter):
        • Presenter 取代了 Controller。它包含 UI 业务逻辑,View 通过接口与 Presenter 交互。
        • View 和 Model 完全解耦。
      • MVVM (Model-View-ViewModel):
        • ViewModel 是 View 的抽象,它暴露了 View 需要的命令和状态。利用数据绑定自动同步 View 和 ViewModel。
        • ViewModel 不知道 View 的存在。
  4. 前端模块化的发展历程是怎样的?(IIFE -> CommonJS -> AMD -> CMD -> UMD -> ES6 Module)

    • 答案:
      • IIFE:使用立即执行函数创建私有作用域,是最早的模块化手段。
      • CommonJS:主要用于 Node.js,同步加载模块,使用 require 和 module.exports。
      • AMD (Asynchronous Module Definition):RequireJS 推广,浏览器端异步加载模块,使用 define 和 require。
      • CMD (Common Module Definition):Sea.js 推广,理念是“就近依赖”,异步加载。
      • UMD (Universal Module Definition):兼容 AMD、CommonJS 和全局变量的模式。
      • ES6 Module:语言层面支持的模块化方案,静态化,编译时加载。
  5. 如何实现一个简单的 Pub/Sub(发布-订阅)模式?

    • 答案:发布-订阅模式是一种对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
      class EventBus {
        constructor() {
          this.events = {};
        }
        on(event, callback) {
          if (!this.events[event]) this.events[event] = [];
          this.events[event].push(callback);
        }
        off(event, callback) {
          if (!this.events[event]) return;
          this.events[event] = this.events[event].filter(cb => cb !== callback);
        }
        emit(event, ...args) {
          if (!this.events[event]) return;
          this.events[event].forEach(callback => callback(...args));
        }
      }
      
      // 使用
      const bus = new EventBus();
      const callback = (data) => console.log('Event received:', data);
      bus.on('message', callback);
      bus.emit('message', 'Hello World!'); // Event received: Hello World!
      bus.off('message', callback);
  6. 如何错误处理?window.onerror 和 window.addEventListener('error') 有什么区别?

    • 答案:
      • Try-Catch:捕获同步代码错误。
      • Promise.catch():捕获 Promise 链中的错误。
      • Async/Await 中的 Try-Catch:捕获 async 函数中的错误。
      • 全局错误捕获:
        • window.onerror:是一个全局事件处理器,能捕获运行时错误和脚本错误。无法捕获网络请求错误和 Promise 拒绝。
        • window.addEventListener('error'):也能捕获运行时错误,但可以捕获资源(如 <img>, <script>)加载失败的错误。事件对象会包含更多信息。
      • 未处理的 Promise 拒绝:使用 window.addEventListener('unhandledrejection') 捕获。
  7. 什么是 Web Worker?如何使用?

    • 答案:Web Worker 允许 JavaScript 代码在后台线程中运行,而不阻塞主线程。
    • 使用:
      // 主线程 main.js
      const worker = new Worker('worker.js');
      worker.postMessage('Hello Worker!'); // 发送消息给 worker
      worker.onmessage = (e) => {
        console.log('Message from worker:', e.data);
      };
      
      // worker.js
      self.onmessage = (e) => {
        console.log('Message from main:', e.data);
        self.postMessage('Hello Main!');
      };
    • 限制:Worker 中无法直接操作 DOM,也无法使用 window 对象的大部分属性和方法。通信通过 postMessage 进行。
  8. 如何优化 JavaScript 性能?

    • 答案:
      • 减少 DOM 操作:批量操作、使用文档片段 DocumentFragment。
      • 事件委托:减少事件监听器的数量。
      • 防抖和节流:控制高频事件的执行频率。
      • 避免全局变量:减少命名冲突和内存占用。
      • 使用 Web Worker:将复杂计算任务移出主线程。
      • 代码拆分和懒加载:减少初始加载体积。
      • 使用性能分析工具:Chrome DevTools 的 Performance 和 Memory 面板。
  9. 什么是内存泄漏?如何避免?如何检测?

    • 答案:内存泄漏是指不再需要的内存没有被垃圾回收机制回收。
    • 常见原因:
      • 意外的全局变量。
      • 被遗忘的定时器或回调函数。
      • 脱离 DOM 的引用(保存了对 DOM 元素的引用,即使已从 DOM 树移除)。
      • 闭包持有外部变量(如果闭包本身不需要持续存在)。
    • 避免:
      • 使用严格模式 'use strict' 避免意外全局变量。
      • 及时清除定时器和事件监听器。
      • 在删除 DOM 元素前,确保没有其他地方引用它。
    • 检测:使用 Chrome DevTools 的 Memory 面板录制堆内存快照,比较快照查找分离的 DOM 树或持续增长的对象。
  10. 如何实现大文件断点续传?

    • 答案:核心是利用 Blob.slice() 方法将文件切片,以及记录上传状态。
      1. 前端:
        • 使用 input[type="file"] 获取文件对象 File(继承自 Blob)。
        • 用 Blob.prototype.slice 将文件切成多个 chunks(切片)。
        • 为每个 chunk 计算 hash(如使用 SparkMD5),用于标识和服务端校验。
        • 记录已上传和未上传的 chunks。
        • 上传 chunks 时,可并发控制(如 3-5 个同时上传)。
        • 如果上传失败,重试特定 chunk。
      2. 服务端:
        • 接收 chunk 和其 hash、index 等信息。
        • 将 chunk 临时存储。
        • 所有 chunks 上传完成后,根据索引顺序合并成完整文件。
        • 验证整体文件的 hash。

🧪 八、综合与场景

  1. 实现一个 Promise.all。

    • 答案:
      Promise.myAll = function(promises) {
        return new Promise((resolve, reject) => {
          if (!Array.isArray(promises)) return reject(new TypeError('Argument must be an array'));
          const results = [];
          let count = 0;
          promises.forEach((promise, index) => {
            Promise.resolve(promise) // 将可能不是Promise的值转为Promise
              .then(value => {
                results[index] = value;
                count++;
                if (count === promises.length) resolve(results);
              })
              .catch(reject); // 任何一个失败,立即reject
          });
        });
      };
      
      // 测试
      const p1 = Promise.resolve(1);
      const p2 = new Promise((resolve) => setTimeout(() => resolve(2), 100));
      const p3 = 3; // 非Promise值
      
      Promise.myAll([p1, p2, p3])
        .then(values => console.log(values)) // [1, 2, 3]
        .catch(err => console.error(err));
  2. 实现一个函数,将深度嵌套的对象展平。

    • 答案:
      function flattenObject(obj, prefix = '', result = {}) {
        for (let key in obj) {
          if (obj.hasOwnProperty(key)) {
            const pre = prefix ? `${prefix}.` : '';
            const value = obj[key];
            if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
              flattenObject(value, pre + key, result);
            } else {
              result[pre + key] = value;
            }
          }
        }
        return result;
      }
      
      const nested = { a: 1, b: { c: 2, d: { e: 3 } } };
      console.log(flattenObject(nested)); // { a: 1, 'b.c': 2, 'b.d.e': 3 }
  3. 实现一个简单的路由(Hash 模式)。

    • 答案:
      class HashRouter {
        constructor() {
          this.routes = {};
          window.addEventListener('hashchange', () => this.handleRouteChange());
        }
        on(path, callback) {
          this.routes[path] = callback;
        }
        handleRouteChange() {
          const hash = window.location.hash.slice(1) || '/';
          const callback = this.routes[hash];
          if (callback) callback();
        }
        navigate(path) {
          window.location.hash = path;
        }
      }
      
      // 使用
      const router = new HashRouter();
      router.on('/', () => console.log('Home Page'));
      router.on('/about', () => console.log('About Page'));
      router.navigate('/about'); // URL 变为 #/about, 输出 About Page
  4. 实现一个函数,比较两个对象是否相等(深度比较)。

    • 答案:
      function isEqual(obj1, obj2) {
        if (obj1 === obj2) return true;
        if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) return false;
        const keys1 = Object.keys(obj1);
        const keys2 = Object.keys(obj2);
        if (keys1.length !== keys2.length) return false;
        for (let key of keys1) {
          if (!keys2.includes(key) || !isEqual(obj1[key], obj2[key])) return false;
        }
        return true;
      }
      
      const objA = { a: 1, b: { c: 2 } };
      const objB = { a: 1, b: { c: 2 } };
      console.log(isEqual(objA, objB);
  5. 实现一个函数,比较两个对象是否相等(深度比较)。 * 答案:深度比较需要递归地比较两个对象的所有自身属性和嵌套属性。

    function isDeepEqual(obj1, obj2) {
      // 处理基本类型和 null
      if (obj1 === obj2) return true;
      if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
        return obj1 === obj2;
      }
    
      // 处理特殊对象类型
      if (obj1 instanceof Date && obj2 instanceof Date) return obj1.getTime() === obj2.getTime();
      if (obj1 instanceof RegExp && obj2 instanceof RegExp) return obj1.toString() === obj2.toString();
    
      // 获取对象的键
      const keys1 = Object.keys(obj1);
      const keys2 = Object.keys(obj2);
    
      // 比较键的数量
      if (keys1.length !== keys2.length) return false;
    
      // 递归比较每个键的值
      for (let key of keys1) {
        if (!keys2.includes(key) || !isDeepEqual(obj1[key], obj2[key])) {
          return false;
        }
      }
      return true;
    }
    
    // 使用
    const objA = { a: 1, b: { c: 2 } };
    const objB = { a: 1, b: { c: 2 } };
    console.log(isDeepEqual(objA, objB)); // true

注意:这个简易实现可能无法处理循环引用、Symbol属性等复杂情况。生产环境建议使用成熟的库(如 lodash.isEqual)。

  1. 实现一个函数柯里化工具函数。 * 答案:柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下参数且返回结果的新函数的技术。

    function curry(fn) {
      return function curried(...args) {
        if (args.length >= fn.length) {
          return fn.apply(this, args);
        } else {
          return function(...args2) {
            return curried.apply(this, args.concat(args2));
          };
        }
      };
    }
    
    // 使用
    function sum(a, b, c) {
      return a + b + c;
    }
    const curriedSum = curry(sum);
    console.log(curriedSum(1)(2)(3)); // 6
    console.log(curriedSum(1, 2)(3)); // 6
  2. 实现一个数组去重函数,要求支持多种数据类型和去重策略。 * 答案:

    function uniqueArray(arr) {
      // 使用 Set 和 展开运算符
      return [...new Set(arr)];
    }
    
    // 或者使用 filter
    function uniqueArray2(arr) {
      return arr.filter((item, index) => arr.indexOf(item) === index);
    }
    
    // 使用
    const arr = [1, 2, 2, '2', '2', null, null, undefined, undefined, {}, {}];
    console.log(uniqueArray(arr)); // [1, 2, '2', null, undefined, {}, {}]

注意:对象和数组的去重是基于引用而不是值。如果需要基于对象内容的去重,需要更复杂的逻辑。

  1. 实现一个函数,解析 URL 的查询参数。 * 答案:

    function parseQueryString(url) {
      const queryString = url.split('?')[1] || '';
      const params = {};
      const pairs = queryString.split('&');
      for (const pair of pairs) {
        if (pair) {
          const [key, value] = pair.split('=');
          params[decodeURIComponent(key)] = decodeURIComponent(value || '');
        }
      }
      return params;
    }
    
    // 使用
    const url = 'https://example.com?name=John&age=30&city=New York';
    console.log(parseQueryString(url)); // { name: 'John', age: '30', city: 'New York' }

    现代浏览器:可以使用 URL 和 URLSearchParams API:

    function parseQueryStringModern(url) {
      const urlObj = new URL(url);
      const params = {};
      for (const [key, value] of urlObj.searchParams) {
        params[key] = value;
      }
      return params;
    }
  2. 实现一个简单的 Promise。 * 答案:这是一个简化的实现,展示了核心逻辑(状态、resolve、reject、then):

    class MyPromise {
      constructor(executor) {
        this.state = 'pending';
        this.value = undefined;
        this.reason = undefined;
        this.onFulfilledCallbacks = [];
        this.onRejectedCallbacks = [];
    
        const resolve = (value) => {
          if (this.state === 'pending') {
            this.state = 'fulfilled';
            this.value = value;
            this.onFulfilledCallbacks.forEach(fn => fn());
          }
        };
    
        const reject = (reason) => {
          if (this.state === 'pending') {
            this.state = 'rejected';
            this.reason = reason;
            this.onRejectedCallbacks.forEach(fn => fn());
          }
        };
    
        try {
          executor(resolve, reject);
        } catch (err) {
          reject(err);
        }
      }
    
      then(onFulfilled, onRejected) {
        if (this.state === 'fulfilled') {
          onFulfilled(this.value);
        } else if (this.state === 'rejected') {
          onRejected(this.reason);
        } else if (this.state === 'pending') {
          this.onFulfilledCallbacks.push(() => onFulfilled(this.value));
          this.onRejectedCallbacks.push(() => onRejected(this.reason));
        }
      }
    }
    
    // 使用
    const promise = new MyPromise((resolve, reject) => {
      setTimeout(() => resolve('Success!'), 1000);
    });
    promise.then(value => console.log(value)); // 1秒后输出 "Success!"
  3. 实现一个函数,限制 Promise 的并发数量。 * 答案:

    function limitConcurrency(promises, limit) {
      return new Promise((resolve) => {
        const results = [];
        let index = 0;
        let running = 0;
        let completed = 0;
    
        function runNext() {
          while (running < limit && index < promises.length) {
            const currentIndex = index++;
            running++;
            promises
              .then(result => {
                results[currentIndex] = result;
              })
              .catch(error => {
                results[currentIndex] = error;
              })
              .finally(() => {
                running--;
                completed++;
                if (completed === promises.length) {
                  resolve(results);
                } else {
                  runNext();
                }
              });
          }
        }
        runNext();
      });
    }
    
    // 使用
    const asyncTasks = [
      () => new Promise(resolve => setTimeout(() => resolve('Task 1'), 1000)),
      () => new Promise(resolve => setTimeout(() => resolve('Task 2'), 500)),
      () => new Promise(resolve => setTimeout(() => resolve('Task 3'), 800)),
      () => new Promise(resolve => setTimeout(() => resolve('Task 4'), 300)),
      () => new Promise(resolve => setTimeout(() => resolve('Task 5'), 1200))
    ];
    
    limitConcurrency(asyncTasks, 2).then(results => {
      console.log(results); // ['Task 1', 'Task 2', 'Task 3', 'Task 4', 'Task 5'] (按完成顺序,但并发数始终不超过2)
    });
最后更新: 2025/9/18 18:20