JavaScript 面试 100 题及参考答案
💡 本文整理了 100 道 JavaScript 经典面试题,并提供了参考答案和原理解析。题目涵盖了 基础语法、核心概念、异步编程、浏览器原理 等前端面试的重要知识点。掌握这些内容将助你从容应对大多数 JavaScript 面试。
📘 一、基础与数据类型
JavaScript 有哪些数据类型?如何准确判断
null
和Array
的类型?- 答案:JavaScript 有 8种 基本数据类型:
- 基本类型(Primitive):
undefined
、null
、boolean
、number
、string
、symbol
(ES6)、bigint
(ES2020) - 引用类型(Reference):
Object
(包含Array
,Function
,Date
等)
- 基本类型(Primitive):
- 判断
null
:使用Object.prototype.toString.call(null)
(返回"[object Null]"
) - 判断
Array
:Array.isArray(arr)
(推荐)Object.prototype.toString.call(arr) === "[object Array]"
- 答案:JavaScript 有 8种 基本数据类型:
typeof null
返回什么?为什么?- 答案:返回
"object"
。这是 JavaScript 早期实现的一个历史遗留 Bug。在 JavaScript 最初的版本中,值是由一个表示类型的标签和实际数据值表示的,对象的标签是0
,而null
被表示为空指针(通常是0x00
),因此null
的标签也被误判为0
,所以typeof
返回"object"
。
- 答案:返回
==
和===
有什么区别?- 答案:
==
是抽象相等,会进行类型转换再比较值;===
是严格相等,不会进行类型转换,要求值和类型都相同。 - 例如:
'5' == 5 // true
(字符串'5'
转数字后相等),而'5' === 5 // false
(类型不同)。
- 答案:
undefined
和null
有什么区别?- 答案:
undefined
表示变量已声明但未赋值,或函数未明确返回值时的默认返回值。null
表示一个空值或对象指针为空,通常由程序员主动赋值来表示“无”的状态。
typeof undefined
为'undefined'
,typeof null
为'object'
。
- 答案:
什么是 JavaScript 中的“假值”(Falsy)?列举所有。
- 答案:在布尔值上下文中会被转换为
false
的值称为假值。包括:false
、0
、-0
、""
(空字符串)、null
、undefined
、NaN
、BigInt
中的0n
。
- 答案:在布尔值上下文中会被转换为
如何将字符串转换为数字?有哪些方法?
- 答案:
Number('123')
→ 123 (严格转换,失败返回NaN
)parseInt('123px', 10)
→ 123 (解析整数,可指定进制)parseFloat('12.34')
→ 12.34 (解析浮点数)+'123'
→ 123 (一元正号运算符,等同于Number()
)Math.floor()
,Math.ceil()
,Math.round()
等(通常先转换再运算)
- 答案:
isNaN()
和Number.isNaN()
有什么区别?- 答案:
isNaN(value)
:会先将value
转换为数值,再判断是否是NaN
。isNaN('abc') // true
Number.isNaN(value)
:不会转换,仅当value
严格等于NaN
时才返回true
。Number.isNaN('abc') // false
。
- 答案:
为什么
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
处理高精度计算。
- 使用
- 答案:因为 JavaScript 中的数字遵循 IEEE 754 标准的 64 位双精度浮点数表示法。
var
,let
,const
有什么区别?- 答案:
特性 var
let
const
作用域 函数作用域 块级作用域 块级作用域 变量提升 是(初始化为 undefined
)是(但存在“暂时性死区”) 是(但存在“暂时性死区”) 重复声明 允许 不允许 不允许 值能否改变 - 能 不能(但对象属性可修改)
- 答案:
什么是变量提升(Hoisting)?
- 答案:JavaScript 引擎在解释代码时,会将变量和函数的声明提升到其所在作用域的顶部。需要注意的是:
var
声明的变量提升时初始化为undefined
。let
和const
也存在提升,但在声明之前访问会抛出ReferenceError
(称为“暂时性死区”,Temporal Dead Zone, TDZ)。- 函数声明整体会被提升,而函数表达式(如
var fn = function(){}
)则按变量提升规则处理。
- 答案:JavaScript 引擎在解释代码时,会将变量和函数的声明提升到其所在作用域的顶部。需要注意的是:
什么是“暂时性死区”(Temporal Dead Zone)?
- 答案:在代码块内,用
let
或const
声明的变量,在声明之前不能被访问或使用,这段从块开始到声明完成的区域称为暂时性死区。这是为了减少运行时错误,使变量管理更加规范。
- 答案:在代码块内,用
如何判断一个变量是否为数组?
- 答案:
Array.isArray(arr)
(最可靠、推荐)Object.prototype.toString.call(arr) === '[object Array]'
(兼容性好)arr instanceof Array
(在单一全局执行环境下有效,跨 frame 或 iframe 时会失效)
- 答案:
如何实现数组去重?
- 答案:
- 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], [])
- ES6 Set:
- 答案:
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
- 答案:
for-in
,for-of
以及forEach
有什么区别?- 答案:
for-in
:用于遍历对象的可枚举属性(包括原型链上的)。遍历数组时通常不推荐,因为可能遍历到非数字键。for-of
(ES6):用于遍历可迭代对象(Array, Map, Set, String, arguments 等)的值。forEach
:是 Array 的方法,用于遍历数组的每个元素。无法使用break
或return
中断循环。
- 答案:
如何实现数组的扁平化(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);
- ES2019 flat():
- 答案:
Math.round(-2.5)
返回什么?为什么?- 答案:返回
-2
。Math.round()
采用“四舍六入五成双”规则(或称“银行家舍入法”)。.5
时,会舍入到最接近的偶数。-2.5
处于-2
和-3
中间,-2
是偶数,所以返回-2
。
- 答案:返回
NaN === NaN
的结果是什么?为什么?- 答案:
false
。根据 IEEE 754 标准,NaN
代表一个非数字值,它不等于任何值,包括它自己。判断一个值是否为NaN
,应使用Number.isNaN(value)
或Object.is(value, NaN)
。
- 答案:
'5' - true
的结果是什么?- 答案:
4
。-
运算符会尝试将操作数转换为数字。'5'
转数字为5
,true
转数字为1
,所以5 - 1 = 4
。
- 答案:
如何安全地访问一个对象的深层属性(如
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 及之前)。
- 可选链操作符(Optional Chaining) (ES2020):
- 答案:
🔨 二、函数与作用域
什么是闭包(Closure)?举例说明其用途和可能的问题。
- 答案:闭包是指能够访问另一个函数作用域中变量的函数。它是由函数和声明该函数的词法环境组合而成。
- 用途:
- 创建私有变量和方法(模块模式)。
- 实现函数柯里化和偏应用。
- 在异步回调中保存状态(例如循环中的
setTimeout
)。
- 示例(计数器):
function createCounter() { let count = 0; // 私有变量 return function() { return ++count; }; } const counter = createCounter(); console.log(counter()); // 1 console.log(counter()); // 2
- 可能的问题:如果不慎,闭包可能导致内存泄漏,因为外部函数的变量会一直被内部函数引用,无法被垃圾回收。
什么是立即调用函数表达式(IIFE)?它有什么用途?
- 答案:IIFE 是定义后立即执行的函数。
(function() { // 代码块 })();
- 用途:
- 创建一个独立的作用域,避免污染全局命名空间。
- 封装私有变量,这些变量在函数外部无法访问。
- 在模块化规范出现之前,它是一种重要的模块化手段。
- 答案:IIFE 是定义后立即执行的函数。
箭头函数和普通函数(使用
function
关键字声明的函数)有什么区别?- 答案:
特性 箭头函数 普通函数 this
指向继承自定义时的外层词法作用域的 this
由调用方式决定(动态绑定) 能否作为构造函数 否(使用 new
调用会报错)是 arguments
对象没有,需使用 rest 参数 ( ...args
)有 prototype
属性没有 有 生成器函数 ( function*
)不能使用 yield
可以
- 答案:
解释 JavaScript 中的作用域(Scope)和作用域链(Scope Chain)。
- 答案:
- 作用域:规定了变量和函数的可访问范围。主要有:全局作用域、函数作用域、块级作用域(由
let
/const
和{}
产生)。 - 作用域链:当访问一个变量时,JavaScript 引擎会首先在当前作用域查找。如果没找到,会沿着外层作用域逐层向上查找,直到全局作用域。这种链式关系称为作用域链。它是在函数定义时就确定的(词法作用域/静态作用域)。
- 作用域:规定了变量和函数的可访问范围。主要有:全局作用域、函数作用域、块级作用域(由
- 答案:
解释 JavaScript 中的执行上下文(Execution Context)和调用栈(Call Stack)。
- 答案:
- 执行上下文:是 JavaScript 代码执行时的环境,包含了变量、函数、参数等信息。主要分为:
- 全局执行上下文:最外层的环境。
- 函数执行上下文:每次函数调用都会创建一个新的。
- Eval 执行上下文(较少使用)。
- 调用栈:是一种 LIFO(后进先出)的栈结构,用于管理执行上下文的创建和销毁。当函数被调用时,其执行上下文被推入栈;当函数执行完毕,其上下文从栈中弹出。
- 执行上下文:是 JavaScript 代码执行时的环境,包含了变量、函数、参数等信息。主要分为:
- 答案:
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!
- 答案:三者都用于显式地设置函数内部的
什么是函数柯里化(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
什么是纯函数(Pure Function)?它有什么好处?
- 答案:纯函数是指满足以下两个条件的函数:
- 相同的输入,永远会得到相同的输出(确定性)。
- 在执行过程中没有任何可观察的副作用(不改变外部状态,不修改传入参数,无 I/O 操作等)。
- 好处:可缓存(Memoization)、易于测试、无竞争条件、易于推理和重构。是函数式编程的核心概念。
- 答案:纯函数是指满足以下两个条件的函数:
什么是函数的重载(Overload)?JavaScript 中有函数重载吗?
- 答案:函数重载是指在同一作用域内定义多个同名函数,但它们的参数类型或数量不同。JavaScript 本身并不支持传统意义上的函数重载,因为后定义的函数会覆盖先定义的。
- 模拟重载:通常通过在函数内部检查
arguments
对象或 rest 参数的长度和类型来实现不同行为。function overloaded() { if (arguments.length === 1) { // 处理一个参数的情况 } else if (arguments.length === 2) { // 处理两个参数的情况 } }
解释尾调用(Tail Call)和尾调用优化(Tail Call Optimization, TCO)。
- 答案:
- 尾调用:指一个函数的最后一步是调用另一个函数。
return func(x);
是尾调用,而return func(x) + 1;
不是。 - 尾调用优化:在严格模式下,如果满足尾调用条件,引擎会复用当前函数的调用栈帧来执行尾调用函数,而不是新建一个。这样可以避免调用栈无限增长(栈溢出),特别适用于递归。
- 尾调用:指一个函数的最后一步是调用另一个函数。
- 示例(未优化 vs 优化):注意:虽然 ES6 规范定义了 TCO,但并非所有 JavaScript 引擎都实现了它。
// 未优化:栈帧会持续增长 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); // 最后一步是函数调用 }
- 答案:
什么是“副作用”(Side Effect)?列举几个有副作用的操作。
- 答案:函数副作用是指函数在运行过程中,修改了外部状态或与外部进行了可观察的交互。例如:
- 修改全局变量或外部对象的属性。
- 调用
console.log
或alert
。 - 发起 HTTP 请求(AJAX/Fetch)。
- 操作 DOM。
- 读取或写入文件(Node.js)。
- 答案:函数副作用是指函数在运行过程中,修改了外部状态或与外部进行了可观察的交互。例如:
arguments
对象是什么?它是一个数组吗?- 答案:
arguments
是一个类数组对象(Array-like),它包含了函数被调用时传入的所有参数。它不是真正的数组,没有数组的方法(如forEach
,map
)。 - 转换为数组:
Array.from(arguments)
[...arguments]
(ES6 展开运算符)
- 注意:在箭头函数中,
arguments
指向的是外层函数的arguments
。箭头函数内部应使用 rest 参数 (...args
) 来获取参数。
- 答案:
Rest 参数(
...
)和arguments
对象有什么区别?- 答案:
特性 Rest 参数 ( ...args
)arguments
对象类型 真正的数组 类数组对象 包含所有参数? 只包含没有对应形参的实参 包含所有实参 箭头函数中可用 是 否(指向外层函数的 arguments
)数组方法 可直接使用 ( forEach
,map
)需先转换为数组
- 答案:
什么是“一等公民”(First-class Citizen)?为什么说函数在 JavaScript 中是一等公民?
- 答案:如果一个编程实体(如函数)可以像其他变量一样被赋值给变量、作为参数传递、作为返回值返回,那么它就被称为“一等公民”。在 JavaScript 中,函数满足所有这些条件,因此是一等公民。这是支持函数式编程的基础。
什么是高阶函数(Higher-order Function)?
- 答案:高阶函数是操作函数的函数。它至少满足以下一个条件:
- 接受一个或多个函数作为参数(例如
Array.prototype.map
,filter
,setTimeout
)。 - 返回一个新的函数(例如
Function.prototype.bind
)。
- 接受一个或多个函数作为参数(例如
- 答案:高阶函数是操作函数的函数。它至少满足以下一个条件:
⏳ 三、异步编程
JavaScript 是单线程还是多线程?如何实现异步操作?
- 答案:JavaScript 是单线程的。这意味着它只有一个主线程(Main Thread)来执行代码。为了避免阻塞,它通过事件循环(Event Loop) 和任务队列(Task Queue) 机制来实现异步操作。异步任务(如定时器、网络请求)会被放入任务队列,等待主线程的同步任务执行完毕后,事件循环会从任务队列中取出异步任务执行。
解释事件循环(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
等。
- 宏任务(MacroTask):包括
- 区别与执行顺序:
- 执行一个宏任务(从宏任务队列中获取)。
- 执行过程中遇到微任务,将其添加到微任务队列。
- 当前宏任务执行完毕后,立即依次执行所有微任务队列中的微任务。
- 微任务清空后,进行 UI 渲染(如果需要)。
- 开始下一个宏任务(从宏任务队列中获取)。
- 示例代码分析输出顺序:
console.log('1'); // 同步任务 setTimeout(() => { console.log('2'); // 宏任务 }, 0); Promise.resolve().then(() => { console.log('3'); // 微任务 }); console.log('4'); // 同步任务 // 输出顺序: 1 -> 4 -> 3 -> 2
- 答案:事件循环是 JavaScript 处理异步任务的机制。它持续检查调用栈是否为空,如果为空,就从任务队列中取出任务执行。
setTimeout(fn, 0)
有什么含义?它是立即执行吗?- 答案:
setTimeout(fn, 0)
并不意味着立即执行。它的含义是:指定函数fn
在当前所有同步任务和微任务执行完毕后,且尽可能早地被添加到宏任务队列中执行。它用于将任务推迟到下一个宏任务中执行,常用于调整执行顺序或避免阻塞。
- 答案:
什么是 Promise?它有哪些状态?
- 答案:Promise 是一个对象,用于表示一个异步操作的最终完成(或失败)及其结果值。
- 三种状态:
- pending(待定):初始状态。
- fulfilled(已兑现):意味着操作成功完成。通过
resolve(value)
进入此状态。 - rejected(已拒绝):意味着操作失败。通过
reject(reason)
进入此状态。
- 特点:状态一旦改变(从
pending
变为fulfilled
或rejected
),就不能再变。
手写一个符合 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)); } } }
- 答案:这是一个简化的实现,展示了核心逻辑:
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
。
- 答案:
如何中断或取消一个 Promise 链?
- 答案:Promise 本身无法被直接取消。但可以通过以下方式模拟中断:
- 返回一个永不清除的 Promise:在链中的某个
then
或catch
中,返回一个永远处于pending
状态的 Promise,这样后续的then
将不会被执行。promise .then(() => { // 条件中断 if (someCondition) { return new Promise(() => {}); // 永不清除,中断链 } return continueWork(); }) .then(() => { // 这个 then 不会执行如果被中断了 });
- 使用
AbortController
(配合fetch
等):对于特定的异步操作如fetch
,可以使用AbortController
来中止请求,这会导致 Promise 拒绝。
- 返回一个永不清除的 Promise:在链中的某个
- 答案:Promise 本身无法被直接取消。但可以通过以下方式模拟中断:
async/await
的本质是什么?它的优势是什么?- 答案:
async/await
是 Generator 和 Promise 的语法糖,它使得异步代码的写法更像同步代码,提高了代码的可读性和可维护性。 - 本质:
async
函数总是返回一个 Promise 对象。await
后面可以跟一个 Promise 对象或任何值。如果是 Promise,它会等待 Promise 解析;如果不是,会被Promise.resolve()
包装。
- 优势:
- 代码清晰:避免了回调地狱和复杂的 Promise 链。
- 错误处理:可以使用传统的
try/catch
来捕获异步错误,而不是.catch()
。 - 调试方便:错误堆栈更清晰。
- 答案:
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(); // 自动执行
- 答案:
在
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]; }
- 答案:多个
setTimeout
和setInterval
有什么问题?如何实现一个精确的定时器?- 答案:
- 问题:它们指定的延迟时间只是最少延迟时间,而不是精确时间。因为事件循环机制,如果主线程上有长时间运行的同步任务或微任务,定时器回调会被推迟执行。
- 实现精确定时器:可以通过计算实际延迟与预期延迟的差值,动态调整下一次的延迟时间。但请注意,在 JavaScript 中实现高精度定时非常困难且不推荐,通常用于动画的
requestAnimationFrame
是更好的选择。
- 答案:
什么是回调地狱(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:使异步代码看起来像同步代码,彻底解决嵌套问题。
- 模块化:将回调函数拆分为独立的命名函数。
- 使用 Promise:通过
- 答案:回调地狱是指多个异步操作嵌套调用,导致代码形成金字塔形状,难以阅读和维护。
process.nextTick
和setImmediate
有什么区别?(Node.js 环境)- 答案:(此题为 Node.js 特定)
process.nextTick(callback)
:将callback
添加到 当前事件循环阶段结束后、下一个阶段开始前 的“nextTick 队列”。这个队列的优先级高于微任务队列。不推荐滥用,因为它会阻塞事件循环。setImmediate(callback)
:将callback
添加到当前事件循环的 Check 阶段的队列中,在 I/O 回调之后执行。它的优先级类似于setTimeout(fn, 0)
。
- 答案:(此题为 Node.js 特定)
requestAnimationFrame
属于宏任务还是微任务?- 答案:
requestAnimationFrame
的执行时机与浏览器渲染相关,通常被认为是一种特殊的宏任务。它的回调函数会在浏览器下一次重绘(repaint)之前执行,通常频率为每秒 60 次(16.7ms/帧)。它的优先级高于普通的宏任务(如setTimeout
)。
- 答案:
如何实现一个简单的
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();
- 答案:
🔗 四、对象与原型
创建对象有哪几种方式?
- 答案:
- 对象字面量:
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');
- 对象字面量:
- 答案:
Object.create(null)
和{}
创建的对象有什么区别?- 答案:
Object.create(null)
创建一个没有原型的对象。它不继承Object.prototype
的任何方法(如toString
,hasOwnProperty
)。{}
(对象字面量)创建的对象继承自Object.prototype
,拥有所有对象的基础方法。
- 用途:
Object.create(null)
常用于创建纯粹的“字典”或“映射”,避免原型上的属性被意外覆盖或遍历到。
- 答案:
什么是原型(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 的方法)
- 答案:每个 JavaScript 对象(除
如何实现继承?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'
- ES5 继承(以组合继承为例):
- 区别:ES6 的
class
和extends
是语法糖,本质上还是基于原型链,但写法更简洁,更易于理解。
- 答案:
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
- 答案:
如何实现一个深拷贝(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
和循环引用的对象。
- 答案:深拷贝会创建一个新对象,并递归地拷贝源对象的所有属性,完全独立于原对象。
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 (原对象被修改了!)
- 答案:
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 属性)。
- 答案:
如何阻止对象扩展、密封对象、冻结对象?
- 答案:
Object.preventExtensions(obj)
:阻止对象添加新属性。Object.seal(obj)
:在preventExtensions
的基础上,同时将所有现有属性标记为configurable: false
(不可删除、不可重新配置)。Object.freeze(obj)
:在seal
的基础上,同时将所有现有属性标记为writable: false
(不可修改)。这是最高级别的不可变性。
- 检查方法:
Object.isExtensible(obj)
Object.isSealed(obj)
Object.isFrozen(obj)
- 答案:
new
操作符背后做了什么?- 答案:
- 创建一个空的简单 JavaScript 对象
{}
。 - 将这个空对象的
[[Prototype]]
(__proto__
)链接到构造函数的prototype
对象。 - 将步骤1新创建的对象作为
this
上下文,执行构造函数。 - 如果构造函数没有显式返回一个对象,则返回
this
(即新创建的对象)。
- 创建一个空的简单 JavaScript 对象
- 答案:
🌐 五、DOM 与 BOM
什么是 DOM?什么是虚拟 DOM(Virtual DOM)?
- 答案:
- DOM (Document Object Model):是浏览器提供的、用于操作 HTML 和 XML 文档的编程接口。它将文档解析为一个由节点和对象组成的树形结构,允许脚本动态地访问和更新文档的内容、结构和样式。
- 虚拟 DOM:是一个轻量级的 JavaScript 对象,它是真实 DOM 的抽象。当应用状态发生变化时,先在虚拟 DOM 上进行操作和比较(Diff算法),然后高效地将最小变化批量更新到真实 DOM,减少直接操作真实 DOM 带来的性能开销。React、Vue 等框架都使用了此概念。
- 答案:
document.load
和document.DOMContentLoaded
事件有什么区别?- 答案:
DOMContentLoaded
:当初始的 HTML 文档被完全加载和解析完成(不包括样式表、图片等外部资源)时触发。意味着 DOM 树已经构建完成。load
:当整个页面(包括所有依赖资源,如样式表、图片、iframe)已完全加载时触发。
- 答案:
事件流(Event Flow)是什么?事件冒泡和事件捕获有什么区别?
- 答案:事件流描述了事件在 DOM 结构中传播的顺序。
- 事件捕获(Capturing Phase):事件从
window
向下传播到目标元素。 - 目标阶段(Target Phase):事件到达目标元素。
- 事件冒泡(Bubbling Phase):事件从目标元素向上传播回
window
。
- 事件捕获(Capturing Phase):事件从
- 区别:传播方向相反。可以使用
addEventListener
的第三个参数来指定在哪个阶段处理事件:true
(捕获)或false
(冒泡,默认)。
- 答案:事件流描述了事件在 DOM 结构中传播的顺序。
什么是事件委托(Event Delegation)?它有什么好处?
- 答案:事件委托是利用事件冒泡机制,将事件监听器绑定在父元素上,而不是每个子元素上。通过
event.target
来判断事件的实际触发元素。 - 好处:
- 减少内存消耗:只需一个事件监听器管理多个子元素。
- 动态元素处理:对后来动态添加的子元素同样有效,无需重新绑定事件。
- 答案:事件委托是利用事件冒泡机制,将事件监听器绑定在父元素上,而不是每个子元素上。通过
如何阻止事件冒泡和默认行为?
- 答案:
- 阻止事件冒泡:
event.stopPropagation()
- 阻止默认行为:
event.preventDefault()
(如表单提交、链接跳转) - 停止所有监听器:
event.stopImmediatePropagation()
(阻止同一事件的其他监听器被调用)
- 阻止事件冒泡:
- 答案:
addEventListener
的参数有哪些?- 答案:
element.addEventListener(eventType, listener, { capture: false, // 是否在捕获阶段触发 once: false, // 是否只触发一次后自动移除 passive: false // 表示 listener 永远不会调用 preventDefault() });
passive: true
通常用于触摸和滚轮事件,以提高滚动性能。
- 答案:
window
和document
对象有什么区别?- 答案:
window
:是浏览器窗口的全局对象,代表一个浏览器窗口或标签页。所有全局变量和函数都是window
对象的属性。它包含document
,location
,history
,navigator
等子对象。document
:是window
的一个属性,代表当前加载的 HTML 文档。它是 DOM 树的入口点,提供了操作页面内容的方法和属性。
- 答案:
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。
- 答案:
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)
:替换当前历史记录,不刷新页面。
- 答案:
Cookie、LocalStorage、SessionStorage、IndexedDB 有什么区别?
- 答案:
特性 Cookie LocalStorage SessionStorage IndexedDB 容量 ~4KB ~5MB 或更多 ~5MB 或更多 大量(通常占可用磁盘空间的50%) 生命周期 可设置过期时间 永久,除非手动删除 标签页关闭后失效 永久,除非手动删除 与服务端交互 每次 HTTP 请求都会自动携带在 Header 中,影响性能 不参与 不参与 不参与 存储类型 字符串 字符串(需序列化对象) 字符串(需序列化对象) 多种类型(对象、文件等) API 易用性 复杂 简单 ( setItem
,getItem
)简单 ( setItem
,getItem
)复杂(异步 API)
- 答案:
🆕 六、ES6+ 与新特性
let
,const
和var
的区别已经在第9题阐述。- 答案:参见第9题。
模板字符串(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
- 字符串插值:使用
- 答案:
箭头函数(Arrow Functions)的特点和注意事项已在第23题阐述。
- 答案:参见第23题。
解构赋值(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!
- 数组解构:
- 答案:
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
- Rest 参数 (
- 答案:
Set
,Map
,WeakSet
,WeakMap
有什么区别?- 答案:
Set
:成员值唯一、无序的集合。Map
:键值对的集合,键可以是任何类型(对象、函数等)。WeakSet
:成员只能是对象的 Set。对象是弱引用,不计入垃圾回收机制。无法遍历,没有size
属性。WeakMap
:键只能是对象的 Map。键是弱引用。无法遍历,没有size
属性。
- 用途:
Set
:数组去重、存储唯一值。Map
:需要用非字符串作为键的字典。WeakSet/WeakMap
:主要用于存储与对象关联的元数据,而不会阻止对象被垃圾回收(例如 DOM 元素关联的数据)。
- 答案:
Symbol
数据类型有什么作用?- 答案:
Symbol
是 ES6 引入的一种新的原始数据类型,表示独一无二的值。 - 主要用途:
- 作为唯一的对象属性名:防止属性名冲突。
const mySymbol = Symbol('myKey'); const obj = {}; obj[mySymbol] = 'secret value';
- 定义对象的内部方法:如
Symbol.iterator
(定义迭代器)、Symbol.toStringTag
等。
- 作为唯一的对象属性名:防止属性名冲突。
- 答案:
什么是迭代器(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
- 迭代器:是一个对象,它实现了一个
- 答案:
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
配合使用,提供默认行为。
- 答案:
ES Module 和 CommonJS 有什么区别?
- 答案:
特性 ES Module (ESM) CommonJS (CJS) 加载方式 静态(编译时) 动态(运行时) 输出 值的引用(只读) 值的拷贝 导入 import { foo } from 'module'
const { foo } = require('module')
导出 export
,export default
module.exports
,exports
顶层 this
undefined
指向当前模块的 exports
循环依赖处理 更优(动态引用) 可能存在问题(值可能未完全初始化) 主要环境 浏览器和现代 Node.js Node.js - Node.js 中支持:在
package.json
中设置"type": "module"
可使用 ESM,文件扩展名为.mjs
也表示 ESM。
- 答案:
🛠️ 七、编码与设计
什么是防抖(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); }; }
- 答案:
什么是函数式编程?柯里化和组合(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
- 答案:
什么是 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 的存在。
- MVC (Model-View-Controller):
- 答案:这些都是前端架构模式,旨在分离UI、数据和逻辑。
前端模块化的发展历程是怎样的?(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:语言层面支持的模块化方案,静态化,编译时加载。
- 答案:
如何实现一个简单的 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);
- 答案:发布-订阅模式是一种对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
如何错误处理?
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')
捕获。
- 答案:
什么是 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
进行。
如何优化 JavaScript 性能?
- 答案:
- 减少 DOM 操作:批量操作、使用文档片段
DocumentFragment
。 - 事件委托:减少事件监听器的数量。
- 防抖和节流:控制高频事件的执行频率。
- 避免全局变量:减少命名冲突和内存占用。
- 使用 Web Worker:将复杂计算任务移出主线程。
- 代码拆分和懒加载:减少初始加载体积。
- 使用性能分析工具:Chrome DevTools 的 Performance 和 Memory 面板。
- 减少 DOM 操作:批量操作、使用文档片段
- 答案:
什么是内存泄漏?如何避免?如何检测?
- 答案:内存泄漏是指不再需要的内存没有被垃圾回收机制回收。
- 常见原因:
- 意外的全局变量。
- 被遗忘的定时器或回调函数。
- 脱离 DOM 的引用(保存了对 DOM 元素的引用,即使已从 DOM 树移除)。
- 闭包持有外部变量(如果闭包本身不需要持续存在)。
- 避免:
- 使用严格模式
'use strict'
避免意外全局变量。 - 及时清除定时器和事件监听器。
- 在删除 DOM 元素前,确保没有其他地方引用它。
- 使用严格模式
- 检测:使用 Chrome DevTools 的 Memory 面板录制堆内存快照,比较快照查找分离的 DOM 树或持续增长的对象。
如何实现大文件断点续传?
- 答案:核心是利用
Blob.slice()
方法将文件切片,以及记录上传状态。- 前端:
- 使用
input[type="file"]
获取文件对象File
(继承自Blob
)。 - 用
Blob.prototype.slice
将文件切成多个 chunks(切片)。 - 为每个 chunk 计算 hash(如使用 SparkMD5),用于标识和服务端校验。
- 记录已上传和未上传的 chunks。
- 上传 chunks 时,可并发控制(如 3-5 个同时上传)。
- 如果上传失败,重试特定 chunk。
- 使用
- 服务端:
- 接收 chunk 和其 hash、index 等信息。
- 将 chunk 临时存储。
- 所有 chunks 上传完成后,根据索引顺序合并成完整文件。
- 验证整体文件的 hash。
- 前端:
- 答案:核心是利用
🧪 八、综合与场景
实现一个
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));
- 答案:
实现一个函数,将深度嵌套的对象展平。
- 答案:
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 }
- 答案:
实现一个简单的路由(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
- 答案:
实现一个函数,比较两个对象是否相等(深度比较)。
- 答案:
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);
- 答案:
实现一个函数,比较两个对象是否相等(深度比较)。 * 答案:深度比较需要递归地比较两个对象的所有自身属性和嵌套属性。
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
)。
实现一个函数柯里化工具函数。 * 答案:柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下参数且返回结果的新函数的技术。
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
实现一个数组去重函数,要求支持多种数据类型和去重策略。 * 答案:
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, {}, {}]
注意:对象和数组的去重是基于引用而不是值。如果需要基于对象内容的去重,需要更复杂的逻辑。
实现一个函数,解析 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; }
实现一个简单的 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!"
实现一个函数,限制 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) });