【前端面试】JS篇
作用域与作用域链
1. 什么是作用域?
- 作用域就是变量或函数生效的范围。
- 在 JS 中,每个函数都有自己的作用域。
- 通常有三种作用域:全局作用域、函数作用域、块级作用域。
- ES6 之后,
let和const引入了块级作用域。
2. 什么是作用域链?
当我们在函数里访问变量时,JS 引擎会先在当前作用域查找,找不到就去上一层作用域,一直查到全局。 这条从里到外的 “查找路径” 就是作用域链。
3. JS 的作用域是怎么确定的?
JS 使用的是词法作用域,作用域在定义函数时就确定,不会因为调用位置而改变。
4. 函数执行时是怎么查找变量的?
每个函数执行时会创建一个“执行上下文”,其中保存了当前作用域中的变量。
当访问变量时,JS 引擎会先看当前上下文有没有,没有就沿着作用域链往外查找,直到全局。
所以变量查找是从内向外。
5. 闭包和作用域链有什么关系?
闭包本质上就是作用域链的延伸。当一个内部函数在外部被引用时,它会“保留”创建时的作用域链,从而可以访问外层函数的变量。所以闭包其实是作用域链被延长到函数外部的结果。
6. 块级作用域和函数作用域的区别是什么?
函数作用域是由 function 定义的,每次调用都会生成新的作用域实例。
而块级作用域是由 {} 定义的,比如 if、for 或用 let、const 声明的变量。
块级作用域在 ES6 才引入,用于解决变量提升带来的问题。
7. 变量提升和作用域的关系是什么?
在作用域创建阶段,JS 会先扫描声明,把函数声明和 var 声明提升到作用域顶部。
所以虽然变量能提前访问,但值是 undefined,这是因为声明提升但赋值没提升。
而 let 和 const 不会被初始化,存在“暂时性死区”。
8. 典型代码例子
var a = 1;
function outer() {var b = 2;function inner() {var c = 3;console.log(a, b, c);}inner();
}
outer();// 当执行 inner() 时,能访问到 a、b、c 三个变量
// [inner作用域] → [outer作用域] → [全局作用域]
变量提升
1. 什么是变量提升?
变量提升是指 在代码执行前,JS 引擎会先扫描当前作用域,把变量和函数声明提升到作用域顶部,但不会提升赋值。
2. var、let、const 的区别?
- var 有函数作用域、会变量提升;
- let 和 const 有块级作用域,也会被提升,但存在暂时性死区,不能在声明前使用;
- const 声明的变量必须初始化且不能重新赋值。
3. 什么是暂时性死区(TDZ)?
暂时性死区是指在块级作用域中,从作用域开始到变量声明语句执行前的这段时间内,该变量不可访问。
4. 函数声明和函数表达式的提升区别?
函数声明会被整体提升,包括函数体;
函数表达式只会提升变量名,不会提升函数体。
sayHi(); // ✅ OK
function sayHi() { console.log('hi'); }sayHello(); // ❌ TypeError: sayHello is not a function
var sayHello = function() { console.log('hello'); };
5. 典型陷阱题(输出题)
// 题目一
console.log(a);
var a = 1;
console.log(b);
let b = 2;// undefined
// ReferceError
// 输出解释:var 声明的变量会被提升并初始化为 undefined,let / const 会被提升但存在暂时性死区(TDZ),在声明前访问会报错。// 题目二
var a = 1;
{console.log(a);let a = 2;
}// ReferenceError(TDZ)
// 输出解释:因为在块作用域中,let a 会在编译阶段就“屏蔽”外层同名变量。在 TDZ 期间,任何对 a 的访问都会抛错,不会沿作用域链查找外层的 a。
闭包
1. 什么是闭包?
闭包就是函数记住它定义时的外层作用域。
当内部函数被返回或传给外部使用时,它还能访问定义它时的外层变量。
2. 为什么闭包可以访问外层变量?
因为 JS 函数在创建时会形成作用域链,内部函数持有对外层作用域的引用,即使外层函数执行完,变量也不会被销毁。
3. 闭包有什么用?
- 封装私有变量(模块化、计数器)
- 保存状态(异步回调、事件处理)
- 实现函数式技巧(防抖、节流、柯里化)
4. 举个闭包的例子
封装私有变量的计数器
function counter() {let count = 0;return function() {count++;return count;};
}
const c = counter();
c(); // 1
c(); // 2
5. 闭包会造成内存泄漏吗?
闭包本身不会自动泄漏内存,但它会让外层变量长时间驻留在内存中。如果引用了不再需要的对象,且闭包还在被使用,这部分内存就无法回收。使用完闭包后,可手动清空引用(比如置 null),或者确保不保留不必要的闭包引用。
6. 闭包和作用域链有什么关系?
闭包其实就是作用域链延长到函数外部的结果。
内部函数持有外层作用域引用,这条链让函数在外部依然能访问外层变量。
7. 闭包在循环里常见什么问题?
所有闭包共享同一个循环变量,导致闭包内部访问的值都是最后一次循环的结果。
for (var i = 0; i < 3; i++) {setTimeout(() => console.log(i), 0);
}
// 输出:3, 3, 3
for (let i = 0; i < 3; i++) {setTimeout(() => console.log(i), 0);
}
// 输出:1, 2, 3
this 指向
1. 什么是 this
this 是函数执行时的上下文对象,指向函数运行时所属的对象,而不是函数定义时所属的对象。
2. this 是在什么时候绑定的?
this 的绑定是在函数执行时动态确定的,不同调用方式会导致不同指向。
3. 四种基本调用方式 this 指向
- 普通函数调用 → 指向全局对象(浏览器中是 window,严格模式下是 undefined)
- 对象方法调用 → 指向调用该方法的对象
- 构造函数调用(new) → 指向新创建的实例对象
- 显式绑定(call / apply / bind) → 指向传入的对象
4. 箭头函数的 this 怎么绑定?
箭头函数没有自己的 this,它会继承定义时外层作用域的 this。
5. 在事件处理函数里,this 指向谁?
普通 DOM 事件处理函数中,this 指向触发事件的元素。
如果使用箭头函数,则继承定义时的外层 this。
6. bind、call、apply 的区别?
- call: 立即执行函数,并指定 this,参数逐个传入
- apply: 立即执行函数,并指定 this,参数以数组形式传入
- bind: 返回一个新函数,绑定指定 this,不立即执行
7. bind 可以再改变 this 吗?
bind 一旦绑定 this,就不可再修改,哪怕使用 call/apply 也不会改变绑定的 this。
原型与原型链

1. 什么是原型?
原型是一个对象,它允许其他对象从它那里继承属性和方法。
2. 原型和构造函数的 prototype 有什么关系?
构造函数的 prototype 指向将来实例的原型对象,实例通过 __proto__ 链接到构造函数的 prototype。
3. 什么是原型链?
原型链是对象通过 prototype 属性逐级连接的一条链,用于查找属性或方法。
当访问对象属性时,如果对象本身没有,会沿着原型链向上查找,直到 null 为止。
4. 原型链查找失败会发生什么?
如果沿链都找不到,访问结果返回 undefined;方法调用会报错。
5. New 的实现机制
new 运算符用于创建一个新对象实例,并完成以下四步:
- 创建一个新对象
- 将新对象的proto指向构造函数的
prototype - 将构造函数内部的
this指向新对象,执行构造函数代码 - 如果构造函数返回的是对象类型,则返回该对象;否则返回新对象
function myNew(Fn, ...args) {// 1. 创建新对象const obj = {};// 2. 关联原型obj.__proto__ = Fn.prototype;// 3. 执行构造函数const result = Fn.apply(obj, args);// 4. 返回对象return typeof result === 'object' && result !== null ? result : obj;
}
6. 原型链与作用域链有什么区别?
- 原型链用于查找对象的属性和方法,运行时动态查找
- 作用域链用于查找变量,编译阶段根据词法作用域确定
7. 构造函数和实例的原型关系?
每个实例通过 __proto__ 访问构造函数的 prototype 上的方法,实现方法共享。
8. 如何判断一个属性是实例自身的还是继承的?
obj.hasOwnProperty('prop')→ 判断是否为自身属性- 如果返回 false,说明该属性是通过原型链继承的
9. 修改实例的 __proto__ 会改变原型链
改变实例的原型链,属性查找路径随之改变,只影响当前实例,不影响构造函数或其他实例。
10. 原型方法共享导致引用类型属性被所有实例共享
原型上的引用类型属性被所有实例共享,修改会影响所有实例,生产中建议放到构造函数内部。
异步机制(事件循环)
1. 什么是事件循环?
JS 是单线程语言,通过事件循环机制实现异步执行。
当主线程执行同步代码时,异步任务(宏任务、微任务)会被放入任务队列,主线程空闲后依次执行。
2. JS 真的只有一个线程吗?
JS 只有一个主线程执行 JS 代码,但浏览器或 Node 内部可以多线程处理 I/O、定时器等,执行完毕后通过事件循环回到主线程。
3. JS 的任务队列分哪几类?
- 宏任务 :setTimeout、setInterval、setImmediate、I/O 等
- 微任务 :Promise.then / catch / finally、process.nextTick(Node)、MutationObserver
4. 宏任务和微任务有什么执行顺序?
每次事件循环中先执行一个宏任务,然后执行该宏任务产生的所有微任务,再进行下一轮宏任务。
5. 宏任务和微任务执行顺序的经典例子
当有同步代码 + Promise.then + setTimeout 时,执行顺序是:
- 先执行同步代码
- 再执行微任务队列(Promise.then)
- 最后执行下一轮宏任务(setTimeout)
6. async/await 与 Promise.then 的执行顺序?
await 后面的表达式会被封装成微任务,和 Promise.then 在同一队列中。
举例:在宏任务里执行 await 会产生微任务,执行顺序和 Promise.then 相同。
7. 事件循环中微任务可能阻塞宏任务吗?
会。如果微任务不断产生新微任务(如 Promise.then 中再生成 Promise.then),当前宏任务会一直被阻塞,下一轮宏任务永远无法执行。 这是微任务可能造成“饥饿宏任务”的问题。
异步编程模型(async/await)
1. async/await 是什么?
async/await 是基于 Promise 的语法糖,用于更直观地书写异步代码。
async声明函数,保证函数返回一个 Promiseawait暂停函数执行,等待 Promise 完成,然后返回结果
2. async 函数返回的是什么?
总是返回一个 Promise,如果函数内部 return 一个普通值,等价于 Promise.resolve(value)。
3. await 的本质是什么?
await的本质是暂停当前async函数的执行,等待右侧表达式(通常是 Promise)的状态变为fulfilled或rejected,并将结果作为返回值,同时将await之后的代码包装成微任务加入队列。
4. await 会阻塞主线程吗?
不会,async 函数暂停的是自身的执行(函数内部),主线程仍然可以继续处理其他任务。
5. 多个 await 的顺序问题
多个 await 是串行执行
Promise
1. 什么是 Promise?
Promise 是“用于处理异步操作的对象”,其核心是通过状态管理(pending → fulfilled/rejected)解决回调地狱问题。
2. Promise.then / catch 的执行时机?
Promise 的回调是微任务,当前宏任务执行完之后立即执行,优先于下一轮宏任务。
3. 多个 Promise.then 执行顺序?
链式调用的 then 会按注册顺序执行,前一个 then 的返回值会作为后一个 then 的参数。
4. Promise.all 和 Promise.race 的区别?
Promise.all([p1, p2])→ 所有 Promise 完成后返回结果,任意失败则立即 rejectPromise.race([p1, p2])→ 谁先完成就返回谁的结果(成功或失败)
5. Promise.resolve / Promise.reject 有什么作用?
Promise.resolve(value)→ 返回一个立即 resolve 的 PromisePromise.reject(reason)→ 返回一个立即 reject 的 Promise
可用于统一异步处理和链式调用
6. Promise 链式调用是怎么实现的?
每个 then 都会返回一个新的 Promise 对象,新 Promise 的状态由前一个 then 的回调返回值决定,从而实现链式传递。
7. then/catch 返回值为非 Promise 会发生什么?
会被自动包装成已 resolve 的 Promise,传给下一个 then。
深拷贝 / 浅拷贝
1. 什么是浅拷贝?
浅拷贝只复制对象的第一层属性,如果属性是引用类型(对象、数组),拷贝的只是引用地址。
改变原对象中的引用值,会影响拷贝对象。
2. JS 中有哪些浅拷贝方式?
Object.assign()- 展开运算符
{ ...obj } Array.prototype.slice()/concat()
都只会拷贝一层。
3. 什么是深拷贝?
深拷贝会递归复制对象的所有层级,使新对象完全独立于原对象。
修改原对象不会影响拷贝对象。
4. 深拷贝有哪些常见实现?
- 简单对象可用
JSON.parse(JSON.stringify(obj) - 复杂对象(包含函数、日期、循环引用等)需用递归或库(如 lodash.cloneDeep)
5. JSON.parse(JSON.stringify(obj)) 有什么缺点?
- 无法拷贝函数、undefined、Symbol
- 会丢失原型链
- 无法处理循环引用
- Date 会被转为字符串
6. 深拷贝与浅拷贝的根本区别?
浅拷贝复制引用地址(同一块堆内存),深拷贝创建新对象(不同内存空间)。
判断标准是修改原对象是否会影响拷贝对象。
7. lodash 的 cloneDeep 是如何实现的?
它通过递归遍历对象的每个属性,处理各种类型(对象、数组、Map、Set、Date 等),并用 WeakMap 解决循环引用。
类型判断与转换
1. JS 有哪些数据类型
- 基本数据类型(栈内存): Number、BigInt(es2020)、String、Boolean、Undefined、Null、Symbol(es6)
- 引用数据类型(堆内存): Object
2. Symbol 和 BigInt 解决了什么问题?
- Symbol:解决对象属性名冲突问题;
- BigInt:解决 Number 超过
2^53 - 1的精度丢失问题。
3. symbol 的使用场景
Symbol 用于创建唯一且不可枚举的标识符,常用于防冲突、私有化和自定义对象行为。
- 避免对象属性名冲突
- 定义私有属性 / 内部方法
- 定义常量枚举值
- 作为唯一标识
4. typeof 能判断哪些类型?有哪些局限?
typeof 可以判断基本类型(number、string、boolean、undefined、symbol、bigint)和 function。
但它无法区分 null 与对象,且对数组、对象、正则等都返回 'object'。
5. 为什么 typeof null === 'object'?
因为早期 JS 用低位标识类型,null 的二进制标识全为 0,被错误识别为 object 类型的标签,属于历史遗留问题。
6. 如何判断数组?
Array.isArray() 最准确,也可用 Object.prototype.toString.call(arr) === '[object Array]'。
7. instanceof 的原理是什么?
instanceof 运算符的原理是检查一个对象的原型链上是否存在指定构造函数的 prototype 属性。
8. 什么时候会发生隐式类型转换?
主要在四种场景:
- 字符串拼接(
+) - 比较运算符(
==) - 逻辑运算(
&&、||) - 模板字符串插值
9. 为什么 [] == ![] 为 true?
![]→false[]转为基本类型"""" == false→ 都转数字 0 → 相等。
10. 为什么 [] + {} 和 {} + [] 结果不同?
[] + {}→'[object Object]'(空数组转字符串 + 对象转字符串){} + []→0(被当作代码块 + 数组转数字)
11. Number()、parseInt()、parseFloat() 有何区别?
Number():整体转数值(空字符串 →0,null→0,undefined→NaN)parseInt():从左到右解析整数(遇到非数字停止)parseFloat():解析浮点数。
防抖与节流
1. 什么是防抖(debounce)?
防抖是指在事件频繁触发时,只在最后一次触发后的一段时间才执行函数。
如果在等待时间内事件又触发,就重新计时。
常见应用:搜索框输入联想、窗口 resize、input 校验等。
2. 什么是节流(throttle)?
节流是指在高频触发事件中,让函数固定时间间隔内最多执行一次。
常见应用:滚动监听、鼠标移动、窗口拖拽等。
3. 防抖和节流为什么能优化性能?
因为它们减少了函数在短时间内的频繁执行,避免了高频 DOM 操作或计算,降低浏览器负担,提高页面流畅度。
4. 使用防抖/节流有哪些注意点?
- 注意
this和参数传递(需用箭头函数或apply绑定)。 - 在 React/Vue 中使用要注意组件卸载时清除定时器。
- 如果是防抖,建议配置“立即执行”选项(leading / trailing)。
执行上下文与调用栈
1. 什么是执行上下文?
执行上下文是 JS 代码在运行时的环境,每当函数被调用时,都会创建一个新的执行上下文,用来存储该函数的变量、作用域链、this 指向等信息。
2. 执行上下文有哪几种?
- 全局执行上下文:页面加载时创建,只有一个。
- 函数执行上下文:每次函数调用都会创建一个新的。
- Eval 执行上下文:很少使用,一般忽略。
3. 执行上下文的创建过程是什么?
执行上下文创建分两步:
- 创建阶段:确定变量、函数声明、
this,形成词法环境和变量环境。 - 执行阶段:执行代码,变量赋值、函数调用等正式运行。
4. 什么是调用栈?
调用栈是 JS 引擎用来管理执行上下文的一种结构(栈结构:后进先出)。
每当函数被调用时,其上下文会被压入栈顶,执行完后再弹出。
5. 为什么会出现栈溢出
当函数无限递归或调用层级过深时,新的执行上下文不断入栈,而栈空间有限,最终导致栈溢出错误。
6. 执行上下文和作用域链的关系是什么?
执行上下文包含作用域链。
当查找变量时,会从当前上下文的词法环境开始,一层层往外查找父级词法环境,直到全局为止。
垃圾回收机制(GC)
1. 什么是垃圾回收?
垃圾回收是 JS 引擎自动释放不再被引用的内存的过程。
也就是说,当一个对象不再被任何引用指向时,它就会被判定为“可回收”,随后被 GC 机制清除。
2. JS 中常见的垃圾回收算法有哪些?
最常见的两种算法:
- 标记清除 → 主流算法
- 从根对象(如全局对象 window)出发,标记所有可达对象;未被标记的对象被回收。
- 引用计数 → 早期使用
- 记录每个对象被引用的次数,引用数为 0 时回收。
- 引用计数容易出现“循环引用”无法释放的问题,因为两个对象互相引用时,引用计数永远不为 0。
3. V8 引擎的垃圾回收机制是怎样的?
V8 采用 分代式垃圾回收:
- 新生代:存活时间短的小对象,使用 Scavenge 算法(复制 + 清理) 。
- 老生代:存活时间长的对象,使用 标记清除 + 标记整理 + 增量标记算法(是 V8 优化策略,把标记过程拆成多次小任务,避免阻塞主线程) 。
4. 什么是内存泄漏?
内存泄漏是指某些对象不再需要,但仍然被引用着,导致 GC 无法回收,从而浪费内存。
5. 常见的内存泄漏场景?
- 全局变量未释放
- 闭包使用不当
- DOM 元素引用未清除
- 定时器(
setInterval)未清除 - 缓存对象持续增长(如 Map、WeakMap 不当使用)
6. 如何排查内存泄漏?
- Chrome DevTools → Performance / Memory
- 录制 Heap Snapshot → 查看 retained size
- 观察内存曲线:
如果多次操作后内存不下降,就是泄漏。
7. 如何避免或优化 GC?
- 尽量减少全局变量
- 使用完 DOM 要及时移除引用
- 用
WeakMap/WeakSet存储临时引用 - 清除定时器和监听器
- 避免滥用闭包
8. 为什么 WeakMap 不会引发内存泄漏?
因为它的键是弱引用,键对象一旦不可达就会被 GC 自动回收。
模块化机制
1. 说说 JS 模块化的发展历程?
早期 JS 没有模块系统,只能靠全局变量和 IIFE(立即执行函数)来组织代码。
后面社区出现:
- CommonJS(Node.js)——同步加载,用
require、module.exports - AMD(浏览器)——异步加载,用
define、require - CMD(SeaJS)——按需加载,用
define(function(require, exports, module){}) - ES Module (ESM) —— 原生模块化标准,用
import、export,支持静态分析和 Tree Shaking
2. 为什么 CommonJS 不适合浏览器?
因为它是同步加载模块的,而浏览器加载脚本是网络请求,无法保证同步完成。
3. AMD 和 CMD 区别?
- AMD(RequireJS):依赖前置,提前加载所有依赖
- CMD(SeaJS):依赖就近,按需加载
4. CommonJS 与 ESModule 区别
| 对比点 | CommonJS | ESModule |
|---|---|---|
| 加载方式 | 同步 | 异步(编译阶段确定依赖) |
| 导出内容 | 值拷贝(导出时就确定值) | 值引用(实时绑定) |
| 运行时机 | 运行时加载 | 编译时静态分析 |
| 语法 | require / module.exports | import / export |
| Tree Shaking | 不支持 | 支持 |
| 环境 | Node.js | 浏览器 & Node.js (ESM 模式) |
5. 什么是 ESM“实时绑定”?
ESModule 导出的变量与原始模块的变量引用相同,原模块更新值时,导入方看到的也是最新的。
6. 为什么 ESM 能做 Tree Shaking?
因为它是静态结构,编译时就能知道哪些变量没被用到,可以在打包时剔除。
7. Node.js 模块加载机制
- 缓存优先:先检查
require.cache,若有缓存直接返回; - 内置模块:如
fs、path,优先级最高; - 文件模块:按绝对路径 / 相对路径查找(补全
.js/.json/.node后缀); - 第三方模块:查找当前目录
node_modules,若不存在则递归向上查找父目录的node_modules,直到根目录或找到模块。
8. 浏览器里如何使用 ES Module?
- 使用
<script type="module"> - 自动启用严格模式
- 默认延迟执行(类似
defer) - 只能通过 同源或 CORS 方式加载模块
9. import() 和 require 区别?
| 对比维度 | require | import() |
|---|---|---|
| 加载方式 | 同步(阻塞) | 异步(非阻塞,返回 Promise) |
| 加载时机 | 运行时 | 运行时(但路径需符合静态规则) |
| 返回值 | module.exports的值拷贝 | 模块命名空间对象(实时绑定) |
| 适用规范 | CommonJS | ESM |
| 典型场景 | Node.js 同步加载 | 按需加载、异步场景、浏览器 ESM |
事件模型与捕获冒泡
1. DOM 事件流的三个阶段是什么?
- 捕获阶段(从 window → 目标元素)
- 目标阶段(事件到达目标元素本身)
- 冒泡阶段(从目标元素 → window 反向传播)
2. 哪个阶段先触发?
捕获先于冒泡,但事件默认只在冒泡阶段触发(除非显式设置第三个参数为 true)。
3. 哪些事件不会冒泡?
如 blur、focus、mouseenter、mouseleave 不会冒泡。
4. 事件捕获和事件冒泡的区别?
| 对比项 | 捕获(capture) | 冒泡(bubble) |
|---|---|---|
| 方向 | 从外到内 | 从内到外 |
| 监听方式 | addEventListener(type, fn, true) | 默认 false |
| 默认执行阶段 | 不默认触发 | 默认触发 |
| 应用场景 | 事件委托前置判断、埋点 | 常规事件绑定、委托处理 |
5. 同一个元素上既注册捕获又注册冒泡,执行顺序?
执行顺序是:先捕获 → 后冒泡。
6. 什么是事件委托(事件代理)?
事件委托是利用事件冒泡,将子元素事件交由父元素监听和处理的机制。
比如列表点击,用父节点代理所有子项点击事件。
优点:
- 减少事件绑定数量
- 动态新增子元素仍能响应
- 提高性能(尤其是大量节点)
7. 怎么阻止事件冒泡?怎么阻止默认行为?
event.stopPropagation() // 阻止冒泡
event.preventDefault() // 阻止默认行为(如表单提交)
8. event.target 和 event.currentTarget 的区别?
event.target是触发事件的具体元素(事件源);event.currentTarget是当前正在处理事件的元素(即绑定事件监听的元素),不一定是父元素(可能是自身)。
9. 事件监听中 this 指向谁?
普通函数中,this 指向绑定事件的元素(等价于 event.currentTarget)。
若用箭头函数,则 this 指向定义时的上层作用域,不指向 DOM。
10. 实际开发中捕获阶段什么时候有用?
- 全局埋点:在捕获阶段尽早监听点击/输入行为
- 阻止某些冒泡逻辑:如 modal 遮罩层
- 与第三方组件库冲突时,优先处理事件
11. addEventListener 默认是捕获还是冒泡?
默认是冒泡。
addEventListener 第三个参数默认为 false 代表事件冒泡,若为 true 则执行事件捕获行为。
深浅比较(== vs ===)
1. == 的比较过程是什么样的?
- 如果类型相同,直接比较值。
- 如果是
null和undefined,相等。 - 如果是数字和字符串,先把字符串转成数字。
- 如果是布尔值,先把布尔转成数字(true→1, false→0)。
- 如果是对象和基本类型,先对对象调用
valueOf()或toString()转成基本类型再比较。
2. 常见易错表达式及结果
| 表达式 | 结果 | 原因 |
|---|---|---|
null == undefined | ✅ true | 特殊规则 |
null === undefined | ❌ false | 类型不同 |
[] == ![] | ✅ true | ![] → false → 0,[] → 0 |
[] == 0 | ✅ true | 转换后都是 0 |
[1] == 1 | ✅ true | [1] → '1' → 1 |
{} == {} | ❌ false | 引用类型不同地址 |
NaN == NaN | ❌ false | NaN 不等于任何值,包括自身 |
0 == false | ✅ true | false → 0 |
' \t\n' == 0 | ✅ true | 空白字符串转数字为 0 |
3. Object.is() 和 === 有什么区别?
两者几乎相同,但在这两个特殊情况不同:
Object.is(+0, -0)→ false(区分正负 0)Object.is(NaN, NaN)→ true(认为 NaN 相等)
setTimeout、Promise、Async/Await 的区别
- 执行优先级:同步代码 → Promise.then/catch(微任务) → setTimeout(宏任务);
- 异步流程控制:setTimeout 是回调式异步,Promise 是链式异步(解决回调地狱),async/await 是同步写法的异步(更简洁);
- 错误处理:setTimeout 回调错误需内部捕获,Promise 用 .catch (),async/await 用 try/catch。
for…in 和 for…of 用法
- for…in 遍历对象可枚举属性(包括原型链),for…in 遍历 “键名”
- for…of 遍历可迭代对象(数组、字符串、Map、Set 等),for…of 遍历 “键值”
追问: 数组用 for…in 有什么问题?
答: 会遍历索引字符串,甚至包括手动添加的属性,不推荐。
普通函数和箭头函数的区别
| 对比项 | 普通函数 | 箭头函数 |
|---|---|---|
| this 指向 | 调用时动态绑定(谁调用指向谁) | 定义时静态绑定外层作用域的 this |
| arguments | 有自己的 arguments | 没有,继承外层作用域的 arguments |
| prototype | 有 prototype | 没有 prototype |
| 构造函数 | 可作为构造函数使用(可 new) | 不能作为构造函数(不能 new) |
| 语法 | 相对冗长 | 更简洁,常用于回调函数 |
| 适用场景 | 普通方法、构造函数 | 回调、函数式编程场景 |
Ajax 工作原理
- 通过
XMLHttpRequest或fetch向服务器发送异步请求,不刷新页面即可获取数据。
流程:创建对象 → open() → send() → 监听 readyState。 - 流程图

数组的常用方法
不会改变原数组
| 方法 | 作用 |
|---|---|
map() | 返回新数组,对每个元素执行回调 |
filter() | 过滤符合条件的元素 |
reduce() | 累积计算(如求和、扁平化) |
concat() | 合并数组 |
slice() | 截取数组的一部分 |
find() / findIndex() | 查找元素或下标 |
includes() | 是否包含某值 |
join() | 转字符串 |
flat() / flatMap() | 扁平化数组 |
会改变原数组
| 方法 | 作用 |
|---|---|
push() / pop() | 尾部增删 |
shift() / unshift() | 头部增删 |
splice() | 增删改指定位置元素 |
sort() | 排序(默认按字符串) |
reverse() | 反转 |
对类数组对象的理解,如何转为数组
- 类数组: 具有
length和索引属性但无数组原型方法的对象(如 arguments、NodeList)。 - 转换方法:
- 通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
- 扩展运算符,需类数组实现迭代器
[...arr]
- 通过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arrayLike);
- 通过
Array.from方法来实现转换
Array.from(arrayLike);
为什么 0.1 + 0.2 !== 0.3,如何让其相等
- 原因:浮点数二进制精度问题导致存储误差
- 解决方案:
- 将其转为整数,再相加转回小数
- 使用 toFixed 方法,只保留一位小数点
substring 和 substr 的区别
substring(start, end):第二个参数是结束索引(不含)substr(start, length):第二个参数是长度(已被弃用,是早期的非标准方法)
异步编程的发展历程
- 回调函数(Callback):最早的方式,容易产生“回调地狱”。
- Promise:提供链式调用和状态管理,解决回调嵌套问题。
- Generator + co:通过
yield暂停异步流程,写同步风格的异步代码。 - async/await:Promise 的语法糖,写起来像同步,最直观。
JS 实现继承的七种方式
-
原型链继承
- 概念:子类的原型指向父类实例,继承父类属性和方法。
- 优点:简单,实现原型方法共享,内存占用少。
- 缺点:
- 不能向父类构造函数传参
- 引用类型属性会被所有实例共享
- 无法实现多继承
-
借用构造函数(经典构造函数继承)
- 概念:在子类构造函数中调用父类构造函数,用
call或apply绑定this。 - 优点:
- 可以向父类构造函数传参
- 每个实例都有自己的属性,不会共享引用类型
- 缺点:
- 方法必须在构造函数中定义,无法复用(每个实例都创建一份)
- 无法继承父类原型方法
- 概念:在子类构造函数中调用父类构造函数,用
-
组合继承(原型链 + 构造函数) → 最常用
- 概念:结合原型链和构造函数继承,既继承属性又继承方法。
- 优点:
- 可以向父类构造函数传参
- 方法共享在原型上,实例不会重复创建
- 避免引用类型共享问题
- 缺点:
- 调用了两次父类构造函数(一次给实例属性,一次给原型)
- 相比寄生组合继承稍微低效
-
ES6 class
extends- 概念:ES6 新语法,使用
extends和super()实现继承。 - 优点:
- 语法清晰,面向对象风格明显
- 自动处理原型链和构造函数调用
- 方法天然在原型上共享
- 缺点:
- 语法糖,底层仍是组合继承的原理
- 不能完全解决多继承问题(需要 mixin)
- 概念:ES6 新语法,使用
-
寄生组合继承 → ES5 最优方案
-
Object.create
-
复制继承(浅拷贝/深拷贝继承属性)
ajax、axios 和 fetch 的区别
| 特性 | Ajax(XHR) | Axios | Fetch |
|---|---|---|---|
| 底层 | XMLHttpRequest | 封装 XHR | 原生 Promise API |
| 返回值 | 回调 | Promise | Promise |
| 发送 JSON | 需手动序列化 | 自动序列化 | 手动 JSON.stringify |
| 浏览器兼容性 | 老旧浏览器支持 | 同 XHR | IE 不支持,需要 polyfill |
| 请求取消 | 需要手动 | 内置 cancel token | AbortController |
Set 和 Map 的区别
| 特性 | Set | Map |
|---|---|---|
| 数据结构 | 值的集合(不重复) | 键值对集合 |
| 键类型 | 只能存值本身,唯一 | 键可以是任意类型(对象、函数) |
| 遍历顺序 | 按插入顺序 | 按插入顺序 |
| 典型应用 | 去重数组 | 对象映射、缓存 |
Map 和 Object 的区别
| 特性 | Object | Map |
|---|---|---|
| 键类型 | 字符串或 Symbol | 任意类型(对象、函数等) |
| 默认键值对数量 | 不固定 | 可以直接通过 size 获取 |
| 遍历 | for…in(要过滤原型属性) | map.forEach / for…of |
| 性能 | 小量对象快 | 大量键值对快 |
map() 和 forEach() 的区别
-
返回值
map():会返回一个新数组(由回调函数的返回值组成)。forEach():没有返回值(返回undefined)。
-
用途区别
map():用于转换数组(需要结果)。forEach():用于遍历执行操作(只做副作用,如打印、修改外部变量)。
-
是否可链式调用
map():返回新数组 → 可继续.filter()、.reduce()等链式操作。forEach():返回undefined→ 不能链式调用。
-
是否可中断循环
- 两者都不能用
break/return提前跳出。若需中断,用普通for/for...of。
- 两者都不能用
-
性能差异
- 基本相似,
map()略慢(因需创建新数组)。
- 基本相似,
一句总结:
map() 用于生成新数组,forEach() 用于遍历执行副作用,功能相似但用途不同。
set 和 weakSet 区别
| 特性 | Set | WeakSet |
|---|---|---|
| 元素类型 | 任意值 | 只能是对象 |
| 是否可遍历 | 可遍历 | 不可遍历 |
| 是否阻止垃圾回收 | 会阻止 | 不阻止(弱引用) |
map 和 weakMap 的区别
| 特性 | Map | WeakMap |
|---|---|---|
| 键类型 | 任意类型 | 只能是对象 |
| 是否可遍历 | 可遍历 | 不可遍历 |
| 是否阻止垃圾回收 | 会阻止 | 不阻止(弱引用) |
JS 中取整的方法
Math.floor():向下取整Math.ceil():向上取整Math.round():四舍五入Math.trunc():去掉小数部分- 位运算:
|0、~~(只对 32 位有效) parseInt():把字符串转整数(要注意非数字字符会截断)
ES6 新特性
- 块级作用域:
let、const - 模板字符串:
`Hello ${name}` - 解构赋值:
const {a, b} = obj; - 箭头函数
- 默认参数
- 扩展运算符:
...(数组、对象、参数) - Promise
- 模块化:
import/export - Class 语法糖
- Symbol
设计模式
| 设计模式 | 概念 | 前端实例 |
|---|---|---|
| 单例模式 | 确保一个类只有一个实例,并提供全局访问 | 全局状态管理对象(Redux store、Vuex store)、全局配置对象 |
| 工厂模式 | 通过统一接口创建不同类型对象,隐藏具体实现 | 创建不同类型的图表、表单控件或组件 |
| 观察者模式 | 对象状态变化时,自动通知依赖它的所有对象 | Vue 响应式数据、数据绑定、跨组件事件监听 |
| 发布-订阅模式 | 事件驱动,发送者和接收者解耦,通过事件频道通信 | Node.js 的事件模块、浏览器自定义事件、组件间事件通信 |
| 装饰器模式 | 在不修改原对象的基础上,动态扩展对象功能 | React 高阶组件(HOC)、ES7 装饰器给类或方法增加功能 |
RAF 和 RIC 是什么?
RAF 用于动画,保证和浏览器刷新率同步;RIC 用于后台空闲任务,不阻塞渲染,二者都是浏览器性能优化手段。
RAF(requestAnimationFrame)
- 作用
- 浏览器提供的 动画渲染优化 API
- 在下一次浏览器重绘之前执行回调函数
- 特点
- 回调执行频率与屏幕刷新率同步(一般 60FPS)
- 浏览器空闲时才执行,节省 CPU
- 浏览器切换到后台标签页时暂停,减少无用计算
- 应用场景
- 页面动画、Canvas / WebGL 渲染、滚动动画
RIC(requestIdleCallback)
- 作用
- 浏览器提供的 空闲时间回调 API
- 当主线程空闲时执行回调,不影响关键渲染
- 特点
- 可指定
timeout,保证在一定时间内执行 - 优先级低,适合非关键任务
- 可指定
- 应用场景
- 批量处理非紧急任务、预加载、日志上报
