【前端面试】HTML篇
【前端面试】CSS篇
【前端面试】JS篇
【前端面试】Vue篇
【前端面试】Git篇
【前端面试】前端性能优化篇
【前端面试】手写题篇
【前端面试】浏览器&网络篇
【前端面试】前端工程化篇

作用域与作用域链

1. 什么是作用域?

  • 作用域就是变量或函数生效的范围。
  • 在 JS 中,每个函数都有自己的作用域。
  • 通常有三种作用域:全局作用域、函数作用域、块级作用域。
  • ES6 之后,letconst 引入了块级作用域。

2. 什么是作用域链?

当我们在函数里访问变量时,JS 引擎会先在当前作用域查找,找不到就去上一层作用域,一直查到全局。 这条从里到外的 “查找路径” 就是作用域链。

3. JS 的作用域是怎么确定的?

JS 使用的是词法作用域,作用域在定义函数时就确定,不会因为调用位置而改变。

4. 函数执行时是怎么查找变量的?

每个函数执行时会创建一个“执行上下文”,其中保存了当前作用域中的变量。
当访问变量时,JS 引擎会先看当前上下文有没有,没有就沿着作用域链往外查找,直到全局。
所以变量查找是从内向外

5. 闭包和作用域链有什么关系?

闭包本质上就是作用域链的延伸。当一个内部函数在外部被引用时,它会“保留”创建时的作用域链,从而可以访问外层函数的变量。所以闭包其实是作用域链被延长到函数外部的结果

6. 块级作用域和函数作用域的区别是什么?

函数作用域是由 function 定义的,每次调用都会生成新的作用域实例。
而块级作用域是由 {} 定义的,比如 iffor 或用 letconst 声明的变量。
块级作用域在 ES6 才引入,用于解决变量提升带来的问题。

7. 变量提升和作用域的关系是什么?

在作用域创建阶段,JS 会先扫描声明,把函数声明和 var 声明提升到作用域顶部。
所以虽然变量能提前访问,但值是 undefined,这是因为声明提升但赋值没提升。
letconst 不会被初始化,存在“暂时性死区”。

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 运算符用于创建一个新对象实例,并完成以下四步:

  1. 创建一个新对象
  2. 将新对象的proto指向构造函数的 prototype
  3. 将构造函数内部的 this 指向新对象,执行构造函数代码
  4. 如果构造函数返回的是对象类型,则返回该对象;否则返回新对象
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 时,执行顺序是:

  1. 先执行同步代码
  2. 再执行微任务队列(Promise.then)
  3. 最后执行下一轮宏任务(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 声明函数,保证函数返回一个 Promise
  • await 暂停函数执行,等待 Promise 完成,然后返回结果

2. async 函数返回的是什么?

总是返回一个 Promise,如果函数内部 return 一个普通值,等价于 Promise.resolve(value)

3. await 的本质是什么?

await的本质是暂停当前async函数的执行,等待右侧表达式(通常是 Promise)的状态变为fulfilledrejected,并将结果作为返回值,同时将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 完成后返回结果,任意失败则立即 reject
  • Promise.race([p1, p2]) → 谁先完成就返回谁的结果(成功或失败)

5. Promise.resolve / Promise.reject 有什么作用?

  • Promise.resolve(value) → 返回一个立即 resolve 的 Promise
  • Promise.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 用于创建唯一且不可枚举的标识符,常用于防冲突、私有化和自定义对象行为

  1. 避免对象属性名冲突
  2. 定义私有属性 / 内部方法
  3. 定义常量枚举值
  4. 作为唯一标识

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. 什么时候会发生隐式类型转换?

主要在四种场景:

  1. 字符串拼接(+
  2. 比较运算符(==
  3. 逻辑运算(&&||
  4. 模板字符串插值

9. 为什么 [] == ![]true

  • ![]false
  • [] 转为基本类型 ""
  • "" == false → 都转数字 0 → 相等。

10. 为什么 [] + {}{} + [] 结果不同?

  • [] + {}'[object Object]'(空数组转字符串 + 对象转字符串)
  • {} + []0(被当作代码块 + 数组转数字)

11. Number()parseInt()parseFloat() 有何区别?

  • Number():整体转数值(空字符串 →0,null→0undefined→NaN
  • parseInt():从左到右解析整数(遇到非数字停止)
  • parseFloat():解析浮点数。

防抖与节流

1. 什么是防抖(debounce)?

防抖是指在事件频繁触发时,只在最后一次触发后的一段时间才执行函数。
如果在等待时间内事件又触发,就重新计时。

常见应用:搜索框输入联想、窗口 resize、input 校验等。

2. 什么是节流(throttle)?

节流是指在高频触发事件中,让函数固定时间间隔内最多执行一次。

常见应用:滚动监听、鼠标移动、窗口拖拽等。

3. 防抖和节流为什么能优化性能?

因为它们减少了函数在短时间内的频繁执行,避免了高频 DOM 操作或计算,降低浏览器负担,提高页面流畅度。

4. 使用防抖/节流有哪些注意点?

  • 注意 this 和参数传递(需用箭头函数或 apply 绑定)。
  • 在 React/Vue 中使用要注意组件卸载时清除定时器。
  • 如果是防抖,建议配置“立即执行”选项(leading / trailing)。

执行上下文与调用栈

1. 什么是执行上下文?

执行上下文是 JS 代码在运行时的环境,每当函数被调用时,都会创建一个新的执行上下文,用来存储该函数的变量、作用域链、this 指向等信息。

2. 执行上下文有哪几种?

  • 全局执行上下文:页面加载时创建,只有一个。
  • 函数执行上下文:每次函数调用都会创建一个新的。
  • Eval 执行上下文:很少使用,一般忽略。

3. 执行上下文的创建过程是什么?

执行上下文创建分两步:

  1. 创建阶段:确定变量、函数声明、this,形成词法环境和变量环境。
  2. 执行阶段:执行代码,变量赋值、函数调用等正式运行。

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)——同步加载,用 requiremodule.exports
  • AMD(浏览器)——异步加载,用 definerequire
  • CMD(SeaJS)——按需加载,用 define(function(require, exports, module){})
  • ES Module (ESM) —— 原生模块化标准,用 importexport,支持静态分析和 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 模块加载机制

  1. 缓存优先:先检查 require.cache,若有缓存直接返回;
  2. 内置模块:如 fspath,优先级最高;
  3. 文件模块:按绝对路径 / 相对路径查找(补全 .js/.json/.node 后缀);
  4. 第三方模块:查找当前目录 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. 哪些事件不会冒泡?

blurfocusmouseentermouseleave 不会冒泡。

4. 事件捕获和事件冒泡的区别?

对比项 捕获(capture) 冒泡(bubble)
方向 从外到内 从内到外
监听方式 addEventListener(type, fn, true) 默认 false
默认执行阶段 不默认触发 默认触发
应用场景 事件委托前置判断、埋点 常规事件绑定、委托处理

5. 同一个元素上既注册捕获又注册冒泡,执行顺序?

执行顺序是:先捕获 → 后冒泡。

6. 什么是事件委托(事件代理)?

事件委托是利用事件冒泡,将子元素事件交由父元素监听和处理的机制。
比如列表点击,用父节点代理所有子项点击事件。

优点:

  • 减少事件绑定数量
  • 动态新增子元素仍能响应
  • 提高性能(尤其是大量节点)

7. 怎么阻止事件冒泡?怎么阻止默认行为?

event.stopPropagation() // 阻止冒泡
event.preventDefault()  // 阻止默认行为(如表单提交)

8. event.targetevent.currentTarget 的区别?

  • event.target触发事件的具体元素(事件源);
  • event.currentTarget当前正在处理事件的元素(即绑定事件监听的元素),不一定是父元素(可能是自身)。

9. 事件监听中 this 指向谁?

普通函数中,this 指向绑定事件的元素(等价于 event.currentTarget)。
若用箭头函数,则 this 指向定义时的上层作用域,不指向 DOM。

10. 实际开发中捕获阶段什么时候有用?

  • 全局埋点:在捕获阶段尽早监听点击/输入行为
  • 阻止某些冒泡逻辑:如 modal 遮罩层
  • 与第三方组件库冲突时,优先处理事件

11. addEventListener 默认是捕获还是冒泡?

默认是冒泡。

addEventListener 第三个参数默认为 false 代表事件冒泡,若为 true 则执行事件捕获行为。

深浅比较(== vs ===)

1. == 的比较过程是什么样的?

  1. 如果类型相同,直接比较值。
  2. 如果是 nullundefined,相等。
  3. 如果是数字和字符串,先把字符串转成数字。
  4. 如果是布尔值,先把布尔转成数字(true→1, false→0)。
  5. 如果是对象和基本类型,先对对象调用 valueOf()toString() 转成基本类型再比较。

2. 常见易错表达式及结果

表达式 结果 原因
null == undefined ✅ true 特殊规则
null === undefined ❌ false 类型不同
[] == ![] ✅ true ![]false0[]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 的区别

  1. 执行优先级:同步代码 → Promise.then/catch(微任务) → setTimeout(宏任务);
  2. 异步流程控制:setTimeout 是回调式异步,Promise 是链式异步(解决回调地狱),async/await 是同步写法的异步(更简洁);
  3. 错误处理: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 工作原理

  • 通过 XMLHttpRequestfetch 向服务器发送异步请求,不刷新页面即可获取数据。
    流程:创建对象 → open() → send() → 监听 readyState。
  • 流程图

在这里插入图片描述

数组的常用方法

不会改变原数组

方法 作用
map() 返回新数组,对每个元素执行回调
filter() 过滤符合条件的元素
reduce() 累积计算(如求和、扁平化)
concat() 合并数组
slice() 截取数组的一部分
find() / findIndex() 查找元素或下标
includes() 是否包含某值
join() 转字符串
flat() / flatMap() 扁平化数组

会改变原数组

方法 作用
push() / pop() 尾部增删
shift() / unshift() 头部增删
splice() 增删改指定位置元素
sort() 排序(默认按字符串)
reverse() 反转

对类数组对象的理解,如何转为数组

  • 类数组: 具有 length 和索引属性但无数组原型方法的对象(如 arguments、NodeList)。
  • 转换方法
  1. 通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
  1. 扩展运算符,需类数组实现迭代器
[...arr]
  1. 通过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arrayLike);
  1. 通过Array.from方法来实现转换
Array.from(arrayLike);

为什么 0.1 + 0.2 !== 0.3,如何让其相等

  • 原因:浮点数二进制精度问题导致存储误差
  • 解决方案
  1. 将其转为整数,再相加转回小数
  2. 使用 toFixed 方法,只保留一位小数点

substring 和 substr 的区别

  • substring(start, end):第二个参数是结束索引(不含)
  • substr(start, length):第二个参数是长度(已被弃用,是早期的非标准方法)

异步编程的发展历程

  1. 回调函数(Callback):最早的方式,容易产生“回调地狱”。
  2. Promise:提供链式调用和状态管理,解决回调嵌套问题。
  3. Generator + co:通过 yield 暂停异步流程,写同步风格的异步代码。
  4. async/await:Promise 的语法糖,写起来像同步,最直观。

JS 实现继承的七种方式

  • 原型链继承

    • 概念:子类的原型指向父类实例,继承父类属性和方法。
    • 优点:简单,实现原型方法共享,内存占用少。
    • 缺点
      • 不能向父类构造函数传参
      • 引用类型属性会被所有实例共享
      • 无法实现多继承
  • 借用构造函数(经典构造函数继承)

    • 概念:在子类构造函数中调用父类构造函数,用 callapply 绑定 this
    • 优点
      • 可以向父类构造函数传参
      • 每个实例都有自己的属性,不会共享引用类型
    • 缺点
      • 方法必须在构造函数中定义,无法复用(每个实例都创建一份)
      • 无法继承父类原型方法
  • 组合继承(原型链 + 构造函数) → 最常用

    • 概念:结合原型链和构造函数继承,既继承属性又继承方法。
    • 优点
      • 可以向父类构造函数传参
      • 方法共享在原型上,实例不会重复创建
      • 避免引用类型共享问题
    • 缺点
      • 调用了两次父类构造函数(一次给实例属性,一次给原型)
      • 相比寄生组合继承稍微低效
  • ES6 class extends

    • 概念:ES6 新语法,使用 extendssuper() 实现继承。
    • 优点
      • 语法清晰,面向对象风格明显
      • 自动处理原型链和构造函数调用
      • 方法天然在原型上共享
    • 缺点
      • 语法糖,底层仍是组合继承的原理
      • 不能完全解决多继承问题(需要 mixin)
  • 寄生组合继承 → 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() 的区别

  1. 返回值

    • map():会返回一个新数组(由回调函数的返回值组成)。
    • forEach()没有返回值(返回 undefined)。
  2. 用途区别

    • map():用于转换数组(需要结果)。
    • forEach():用于遍历执行操作(只做副作用,如打印、修改外部变量)。
  3. 是否可链式调用

    • map():返回新数组 → 可继续 .filter().reduce() 等链式操作。
    • forEach():返回 undefined不能链式调用
  4. 是否可中断循环

    • 两者都不能用 break / return 提前跳出。若需中断,用普通 for / for...of
  5. 性能差异

    • 基本相似,map()略慢(因需创建新数组)。

一句总结:
map() 用于生成新数组forEach() 用于遍历执行副作用,功能相似但用途不同。

set 和 weakSet 区别

特性 Set WeakSet
元素类型 任意值 只能是对象
是否可遍历 可遍历 不可遍历
是否阻止垃圾回收 会阻止 不阻止(弱引用)

map 和 weakMap 的区别

特性 Map WeakMap
键类型 任意类型 只能是对象
是否可遍历 可遍历 不可遍历
是否阻止垃圾回收 会阻止 不阻止(弱引用)

JS 中取整的方法

  • Math.floor():向下取整
  • Math.ceil():向上取整
  • Math.round():四舍五入
  • Math.trunc():去掉小数部分
  • 位运算:|0~~(只对 32 位有效)
  • parseInt():把字符串转整数(要注意非数字字符会截断)

ES6 新特性

  • 块级作用域letconst
  • 模板字符串`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)

  1. 作用
    • 浏览器提供的 动画渲染优化 API
    • 在下一次浏览器重绘之前执行回调函数
  2. 特点
    • 回调执行频率与屏幕刷新率同步(一般 60FPS)
    • 浏览器空闲时才执行,节省 CPU
    • 浏览器切换到后台标签页时暂停,减少无用计算
  3. 应用场景
    • 页面动画、Canvas / WebGL 渲染、滚动动画

RIC(requestIdleCallback)

  1. 作用
    • 浏览器提供的 空闲时间回调 API
    • 当主线程空闲时执行回调,不影响关键渲染
  2. 特点
    • 可指定 timeout ,保证在一定时间内执行
    • 优先级低,适合非关键任务
  3. 应用场景
    • 批量处理非紧急任务、预加载、日志上报

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐