Skip to content

JavaScript 核心 ​

适用:前端基础知识查阅(含垃圾回收、DOM 事件);手写题统一放在 手写与布局。建议先走一遍 知识串联,再按章节由浅入深看。

什么是 JavaScript?为什么说它是单线程? ​

JS 是前端三剑客之一,和 HTML、CSS 一起组成网页的核心:HTML 负责结构,CSS 负责样式,JS 赋予网页可操作性(点击、请求、改内容)。

JS 是单线程语言:它在处理这件事的时候,不能同时去处理另一件事。当前正在跑的这些,就是同步任务,排着队、做完一件再做下一件。

同步 / 单线程的好处 ​

  • 执行顺序清楚,代码从上到下,好推理
  • 不会两段 JS 同时改同一个 DOM,避免抢数据、页面状态打架
  • 心智负担小,不用自己管锁、线程同步

同步 / 单线程的坏处 ​

一件事如果很慢(死循环、同步等网络),后面的点击、渲染全被堵住,页面卡死。单线程本身解决不了「又要等结果、又不能占着主线程」。

怎么解决:EventLoop(事件循环) ​

耗时的事先异步出去:请求、定时器交给浏览器其它能力去做,JS 继续跑后面的同步代码。做完了把回调排进队列,等调用栈空了再执行。这套调度叫 EventLoop,具体怎么转见 下一节。

🎤 口述精简版 ​

前端三剑客:HTML 结构、CSS 样式、JS 让页面能操作。JS 单线程,处理 A 的时候不能同时处理 B,这就是同步,好处是顺序清楚、不会一起改 DOM 打架;坏处是慢任务会卡死页面。解决靠 EventLoop:耗时的丢给浏览器异步做,做完再排队回来。具体怎么转见下一节。

什么是 EventLoop?宏任务和微任务怎么转? ​

JavaScript 是单线程语言,同一时间只能处理一件事。如果所有任务都同步执行,遇到网络请求、定时器这类耗时操作就会阻塞页面。为了解决单线程阻塞,浏览器宿主环境提供了事件循环(EventLoop),用来处理异步任务。

JS 的任务分为同步任务和异步任务,异步任务又分为宏任务和微任务。

  • 同步任务运行在调用栈(执行栈),栈是后进先出
  • 异步任务不会直接进入调用栈,会被放到对应的任务队列等待调度

整个 script 本身就是浏览器提供的第一个宏任务。事件循环一轮一轮执行:

  1. 从宏任务队列取出一个宏任务,推入主线程调用栈
  2. 执行这个宏任务时:遇到同步任务就直接在调用栈执行;遇到异步任务交给浏览器其他后台线程,完成后再把回调放入宏任务或微任务队列,不阻塞主线程
  3. 当前宏任务里所有同步代码执行完毕、调用栈清空之后,立刻清空全部微任务队列;微任务过程中新产生的微任务本轮一并执行
  4. 微任务全部完成后,浏览器进行 UI 渲染
  5. 再进入下一轮,取下一个宏任务,重复上面流程

一轮完整顺序:取一个宏任务 → 执行内部同步代码 → 清空全部微任务 → UI 渲染 → 下一个宏任务。

宏任务和微任务常见有哪些 ​

宏任务(macrotask)微任务(microtask)
整段 script、setTimeout / setIntervalPromise.then / catch / finally
I/O、UI 渲染相关queueMicrotask、MutationObserver
Node 还有 setImmediate

经典输出顺序:

js
console.log(1)
setTimeout(() => console.log(2))
Promise.resolve().then(() => console.log(3))
console.log(4)
// 1 4 3 2

1、4 是当前宏任务里的同步;3 是微任务;2 是下一个宏任务。

Node 端阶段更细(nextTick、setImmediate),浏览器 先把上面这套讲顺即可。见 Node EventLoop。

🎤 口述精简版 ​

JS 单线程,全同步会卡页面,所以浏览器用 EventLoop 处理异步。整段 script 是第一个宏任务,里面同步直接跑,异步进队列;同步完清空微任务,再 UI 渲染,再取下一个宏任务。所以 1、setTimeout、Promise.then、4 打印出来是 1 4 3 2。

1. 基础语法与数据类型 ​

var、let、const 的区别? ​

三种都是声明方式。 先垫一句 JS 怎么执行,再说作用域、let / const、var / let。追问再展开变量提升、TDZ、const 怎么赋值。

JS 怎么执行(给 TDZ 垫一层) ​

EventLoop 管的是异步怎么排队回来。当前这段同步代码本身是:进入一段代码(一个 script / 一个函数)时,先创建执行上下文,分两步——

  1. 先处理声明:扫一遍 var / let / const / 函数声明(这就是提升)
  2. 再一行一行执行:从第一行往下跑,跑到哪一行,哪一行才真正赋值

暂时性死区就是第 2 步还没走到 let / const 那一行:名字在第 1 步已经占坑了,但执行指针还没到,提前用就报错。执行上下文细讲见 作用域那一节。

对比点varletconst
作用域函数作用域块级作用域块级作用域
顶层是否挂 window会不会不会
变量提升会,初始化为 undefined会,但处于 TDZ会,但处于 TDZ
重复声明允许不允许不允许
重新赋值允许允许不允许(绑定不可变)
暂时性死区无有有

作用域和 window ​

  • var:函数作用域。写在函数里,整个函数都能用;写在 script 顶层,会变成全局变量,并且挂到 window 上(window.a === a)。
  • let / const:块级作用域({}、if、for)。顶层声明也能在全局访问,但不会成为 window 的属性。

变量提升 ​

就是上面第 1 步:var a 被登记到当前作用域,并初始化成 undefined,所以后面一行一行执行时,声明前访问不报错、值是 undefined。let / const 也会在第 1 步占坑,但不会初始化,执行还没到那一行就访问,走 TDZ。函数声明整体提升,见下一题 函数写法。

js
console.log(a) // undefined
var a = 1

console.log(b) // ReferenceError
let b = 1

暂时性死区(TDZ) ​

代码是一行一行往下执行的。从进入作用域到执行指针走到 let / const 那一行之前,这个名字处于「不可用」状态,读、写都会 ReferenceError。不是没声明,是第 1 步已经登记了,第 2 步还没执行到赋值那一行。

const 怎么赋值 ​

  • 声明时必须赋值:const x; 直接语法错误,const x = 1 才行。
  • 不能换绑:后面不能 x = 2,也不能把对象换成另一个。
  • 可以改内容:const 锁的是绑定,不是对象内部。obj.n = 2、arr.push(1) 都可以。真要整份锁死再用 Object.freeze。
js
const obj = { n: 1 }
obj.n = 2   // 可以,改的是内容
obj = {}    // 报错,换绑不行

💡 学习建议:开口先垫「先进执行上下文:先处理声明,再一行一行执行」。主线再按「作用域 → let/const 能不能再赋值 → var 挂 window、能重复声明、声明前是 undefined」。追问 TDZ 时扣「执行还没走到那一行」。

🎤 口述精简版 ​

先进执行上下文:先处理声明,再从第一行往下执行。三种都是声明方式。先说作用域:var 函数级,let/const 块级。let 是变量能再赋值,const 是常量不能换绑。var 顶层会挂到 window,let 能访问但不挂 window;var 能重复声明、声明前是 undefined,let 重复声明不行、声明前直接抛错。TDZ 就是一行一行执行时还没走到 let/const 那一行。

函数有哪几种写法? ​

按五种说:函数声明、函数表达式、箭头函数、new Function、对象方法 / class 方法。

  1. 函数声明(函数提升):function fn() {}。整份函数会提升,写在后面也能先调用。
js
fn() // 能跑
function fn() { return 1 }
  1. 函数表达式(变量接收函数):把函数赋给一个名字,const fn = function () {}。赋上之后才能调,没有函数提升。
js
const fn = function () { return 1 }
fn()
  1. 箭头函数(ES6):const fn = () => {},本质也是函数表达式。没有自己的 this / arguments,不能 new。this 看定义时外层,细讲见 普通函数 vs 箭头函数。

  2. new Function 构造函数(极少用):用字符串在运行时拼出一个函数。不形成闭包,始终走全局作用域,类似 eval,业务里几乎不用。

js
const add = new Function('a', 'b', 'return a + b')
add(1, 2) // 3
  1. 对象方法 / class 类方法:写在对象或 class 里的函数。对象里 obj.say() 的 this 看谁调用;class 里的方法挂在原型上大家共享,constructor 相当于以前的构造函数,必须 new。语法清单见 面向对象 和 class 核心点,底层见 原型。
js
const obj = {
  say() { return this.name },
}
class A {
  constructor(x) { this.x = x }
  say() { return this.x }   // A.prototype.say
}

💡 学习建议:开口按 1 到 5 报名单。重点对比声明能先调、表达式要赋上再调;箭头函数没有自己的 this;new Function 一句带过;class 方法就是挂原型上的函数。

🎤 口述精简版 ​

函数常见五种写法。函数声明会提升,写在后面也能先调用。函数表达式是变量接收函数,赋上才能调。箭头函数是 ES6 的表达式写法,没有自己的 this,也不能 new。new Function 用字符串造函数,极少用。对象方法和 class 方法是挂在对象或原型上的函数,class 必须 new,底层还是原型。

JS 怎么做面向对象? ​

别先背封装 / 继承 / 多态。你已经会写一个对象,也会原型链。面向对象就是:做一套模具,用 new 批量做出实例。JS 的模具底层就是原型,class 只是把「函数 + prototype」写成更好看的样子。

模具长什么样

js
class Dog {
  constructor(name) {
    this.name = name // new 出来的这条狗自己的数据
  }
  bark() {
    return this.name + ':旺旺' // 方法挂在 Dog.prototype,所有狗共享
  }
}
const a = new Dog('旺财')
const b = new Dog('来福')
a.bark() // 旺财:旺旺
a.bark === b.bark // true,方法同一份

new Dog('旺财') 还是原型那四步:建空对象 → __proto__ 指到 Dog.prototype → 当 this 跑 constructor → 返回这个对象。所以数据在实例上,方法在原型上。必须 new。a instanceof Dog 为 true。

子模具接着用爸的:extends + super

js
class Cat extends Dog {
  constructor(name, color) {
    super(name)      // 先跑爸的 constructor,必须写在 this 前
    this.color = color
  }
  bark() {
    return this.name + ':喵喵' // 覆盖爸的叫法
  }
}
const c = new Cat('橘坐', '橘色')
c.bark() // 橘坐:喵喵
c instanceof Dog // true,原型链上有 Dog.prototype

找 bark:c → Cat.prototype(有猫的 bark,就用这个)→ Dog.prototype → Object.prototype → null。

同样叫 bark,各叫各的

js
function makeSound(animal) {
  return animal.bark() // 有 bark 就能调,不管是狗是猫
}
makeSound(a) // 旺财:旺旺
makeSound(c) // 橘坐:喵喵

这就是 里的三句话:封装 = 数据和调用收在模具里;继承 = extends + super;多态 = 同样方法名各写各的。语法清单见 class 核心点,查找过程见 原型。

🎤 口述精简版 ​

面向对象就是先做模具再 new 出实例。JS 的模具底层是原型,class 只是写法好看。constructor 里 this.xxx 是这个实例自己的数据;class 里写的方法挂在 prototype 上大家共享。子类要用爸的能力就 extends,constructor 里先 super 再写自己的。不同类可以都有一个 bark,各写各的。必须 new。

JS有哪些数据类型? ​

基本类型 ​

string、number、boolean、null、undefined、symbol

特点:按值访问,赋值是拷贝值本身。

补充:ES2020 又加了 bigint, 若问「现在有几种」可以说 7 种;经典口径仍常答上面 6 种。

引用类型 ​

常见就记两类:

类型说明
object普通对象、数组、日期、正则等都属于对象
function函数也是对象,但 typeof 会单独返回 'function'

特点:按引用访问,赋值拷贝的是地址,多个变量可能指向同一块堆内存。

js
typeof 'a'        // 'string'
typeof 1          // 'number'
typeof true       // 'boolean'
typeof undefined  // 'undefined'
typeof null       // 'object'  ← 历史坑
typeof Symbol()   // 'symbol'
typeof {}         // 'object'
typeof []         // 'object'
typeof function(){} // 'function'

基本类型 vs 引用类型(区别) ​

对比点基本类型引用类型
存什么变量里直接存的是值变量里存的是地址(指向堆里的对象)
赋值拷贝一份值,互不影响拷贝地址,多个变量可能指向同一对象
比较=== 比的是值是否一样=== 比的是是不是同一个地址
可否扩展属性一般不能当对象用(包装对象另说)可以随意加属性、方法
典型成员string / number / boolean / null / undefined / symbolobject / function(数组等也是 object)
js
// 基本类型:互不影响
let a = 1
let b = a
b = 2
console.log(a) // 1

// 引用类型:改的是同一块数据
let obj1 = { name: 'tom' }
let obj2 = obj1
obj2.name = 'jerry'
console.log(obj1.name) // 'jerry'

一句话:基本类型「各拿各的值」;引用类型「可能共看一份对象」。

🎤 口述精简版 ​

原始类型六个:string、number、布尔、null、undefined、symbol。引用类型主要是 object 和 function——函数本质也是对象,只是 typeof 能单独识别。

区别上:基本类型按值拷贝、比的是值;引用类型变量存地址,赋值是拷地址,改一个可能另一个也变,比较也是看是不是同一个引用。

隐式转换、比较与类型判断 ​

常把这几块一起问:引擎会不会偷偷转类型、两个值怎么比、怎么判断是什么类型、instanceof 和原型链什么关系。先说隐式转换和 == / ===,再说比较类型、比较原型。

隐式类型转换 ​

引擎自动把类型转成运算需要的类型,没有你手写 Number() / String()。

常见场景:

  • == 比较(会转换;所以多用 ===)
  • +:有字符串就偏向拼接;否则偏向数字相加
  • 关系比较、if / && / || 转布尔
  • 对象转原始值:走 valueOf / toString

== 和 === ​

  • == 会先隐式类型转换再比较;=== 不转换,类型和值都要相等。
  • 基本类型 === 比的是值;引用类型 === 比的是是不是同一个地址(和数据类型那题同一套)。
  • 业务里默认用 ===,需要转换就显式写 Number() / String(),别依赖 == 的规则表。

💡 学习建议:== 的转换表很反人类(比如 [] == ![] 是 true), 和实战都不要死记表,记住「用 === 避开」就够了。

假值(布尔转换) ​

在 if、&&、|| 里会隐式转布尔。下面都是 falsy(假值):

false、0、-0、0n、''、null、undefined、NaN

非空字符串、非 0 数字、对象(含 []、{})是真值。空数组空对象别当成假。

比较类型:typeof / instanceof / toString.call ​

方法作用注意
typeof判断值类型typeof null === 'object';数组、对象都是 'object'
instanceof看原型链上有没有某构造函数[] instanceof Array;跨 iframe 可能不准
Object.prototype.toString.call最稳的类型字符串[object Array]、[object Null] 等

场景:简单原始值用 typeof;要认数组/实例用 instanceof 或 Array.isArray;要精细区分用 toString.call。

typeof null === 'object' 是早期设计 bug:null 用「空对象指针」表示,typeof 按类型标签当成对象了。当历史梗记一句即可。

比较原型:instanceof 沿原型链 ​

instanceof 不是看 typeof 字符串,是沿着左边对象的原型链往上走,看右边构造函数的 prototype 在不在这条链上。

js
[] instanceof Array   // true,Array.prototype 在 [] 的原型链上
[] instanceof Object  // true,再往上还有 Object.prototype

跨 iframe 两边的 Array 不是同一个构造函数,instanceof 会不准,认数组更稳用 Array.isArray。原理手写见 instanceof,原型链见 原型。

null 和 undefined ​

undefinednull
含义未定义、未赋值空对象指针,有意「没有」
常见来源变量声明未赋、缺属性、函数无 return开发者主动赋空
typeof'undefined''object'(历史坑)
==null == undefined 为 true(特例)同上
===不相等不相等

🎤 口述精简版 ​

隐式转换就是引擎自己转类型,最坑是 == 和 +,业务比较用 ===。比较分两块:比相不相等,== 先转再比、=== 硬比;比是什么类型,typeof 看基本类型但 null 会骗你,instanceof 沿原型链找右边的 prototype,最准常用 Object.prototype.toString.call。假值记 0、空串、null、undefined、NaN;空数组空对象反而是真。undefined 是还没有,null 是你主动说空。

2. ES6 基础常用(class / 解构 / 数组与字符串) ​

问「ES6 有哪些新特性」,按块报,不必背完 API:

  1. let / const(块级、TDZ)→ 变量声明
  2. 箭头函数(没有自己的 this)→ 箭头函数
  3. class → 面向对象
  4. 解构赋值、模板字符串、扩展运算符 ...
  5. 数组 / 字符串 / 对象一批新方法

下面是第 4、5 块速查。class 清单仍在本节。

面向对象(class / 继承)核心点 ​

五种函数写法见 函数写法,面向对象怎么答见 面向对象。下面是 class 语法速查。

  1. class 与 constructor:class A { constructor(x) { this.x = x } }
  2. 继承 extends + super:class B extends A { constructor() { super() } },super() 必须在 this 前调用
  3. static 静态成员:挂在类上、不挂在实例(A.foo()),常用于工具方法
  4. 私有字段 #(ES2022):#secret,外部访问直接报错,才是真正的私有
  5. getter / setter:拦截属性的读 / 写,可做校验或计算属性
  6. 实例方法在 prototype 上:所有实例共享同一份方法,不在每个实例各存一份
  7. 判断实例:obj instanceof A 或 A.prototype.isPrototypeOf(obj)
  8. JS 没有接口 / 抽象类(语言层),靠 TS 补;"多态"靠鸭子类型(长得像就能用)
  9. class 是语法糖:底层仍是原型链,class A {} ≈ 构造函数 + prototype
  10. 封装约定:没有 # 的时代用 _name 约定"私有"(其实外部仍能访问)

🎤 口述精简版 ​

JS 面向对象用 class 写、底层还是原型;继承靠 extends + super,静态方法挂类上、实例方法挂原型;真正的私有字段用 #(ES2022),之前靠 _ 约定。

解构赋值(常用写法清单) ​

  1. 数组解构:const [a, b] = arr
  2. 对象解构:const { name } = obj
  3. 重命名:const { name: userName } = obj
  4. 默认值:const { x = 1 } = obj(属性不存在 / 为 undefined 时才生效)
  5. 剩余项:const [first, ...rest] = arr
  6. 嵌套解构:const { user: { id } } = data
  7. 参数解构:function f({ a, b = 2 } = {}) {} —— 解构 props / options 最常用
  8. 交换变量:[a, b] = [b, a]
  9. 跳过项:const [a, , c] = arr

ES6+ 数组操作(必会方法) ​

  1. map(fn):映射成新数组(一一对应,不改原数组)
  2. filter(fn):过滤出满足条件的元素
  3. reduce(fn, init):聚合成一个值(求和 / 分组 / 扁平 / 计数)
  4. forEach(fn):遍历,无返回值(不能 break,要中断用 some/every 或 for...of)
  5. find / findIndex:找第一个符合的元素 / 下标(找不到返回 undefined / -1)
  6. some / every:是否有至少一个 / 是否全部符合
  7. includes(val):是否包含某值(替代 indexOf > -1)
  8. flat(depth) / flatMap:多层扁平化 / 映射后扁平
  9. sort(compareFn):排序(原地修改原数组;比较函数返回正/负/0)
  10. Array.from / Array.of:类数组 / Set → 数组、创建数组;slice(不改动)vs splice(改动原数组、可删插)

字符串操作(常用 API) ​

  1. includes / startsWith / endsWith:是否包含 / 前缀 / 后缀
  2. slice(start, end):截取子串(不改动原串);substring 类似但自动处理负/颠倒参数
  3. split / join:字符串 → 数组 / 数组 → 字符串(互逆,经典如 str.split('').reverse().join('') 翻转)
  4. trim / trimStart / trimEnd:去首尾 / 左 / 右空白
  5. replace / replaceAll:替换首个 / 替换全部(全局用 /g 或 replaceAll)
  6. repeat(n):重复 n 次
  7. padStart / padEnd:补位(如 '1'.padStart(2, '0') → '01')
  8. toUpperCase / toLowerCase:大小写转换
  9. charAt(i) / [i] / charCodeAt(i):取第 i 个字符 / 取字符编码
  10. 模板字符串(反引号):${} 插值、可换行、可写表达式,替代字符串拼接

对象操作(常用写法) ​

  1. 属性简写:{ name } 等于 { name: name };方法简写 { say() {} }
  2. 展开 / 合并:{ ...obj, age: 18 }(浅拷贝,后者覆盖前者)
  3. Object.assign(target, ...src):也是浅合并,会改 target
  4. Object.keys / values / entries:键名 / 键值 / [键, 值] 数组
  5. 计算属性名:{ [key]: val }
  6. 可选链 / 空值合并(ES2020,常和 ES6 一起被问):obj?.a、a ?? 1(只有 null / undefined 才走右边)

扩展运算符数组同样能用:[...arr] 浅拷贝、[...a, ...b] 拼接;函数剩余参数 function f(...args) {}。

💡 学习建议:这一章当「速查表」用,不用背。 先按块报名单,再被追问某个 API 时查下面清单。去重 / 扁平 / 分组 / 字符串翻转,优先想 map / filter / reduce / slice 和 split / join / reverse。

🎤 口述精简版 ​

ES6 就是给 JS 补了一堆更好写的语法。先说变量,以前用 var,现在用 let 和 const,它们是块级作用域。然后再说函数,箭头函数没有自己的 this,定义的时候外层是谁就是谁。class 看起来像面向对象,底层还是原型。然后就是解构赋值,数组按位置拆,对象按名字拆。模板字符串用反引号,能插值也能换行。三个点是展开运算符,拷贝合并是浅拷贝。数组最常用 map、filter、reduce,字符串常用 includes,对象常用 keys 和展开。

3. 作用域、闭包与 this ​

什么是执行上下文、作用域、作用域链? ​

概念白话
执行上下文代码跑起来时的「环境」:变量、this、作用域链等(全局 / 函数 / eval)
作用域变量能被访问的范围(ES6 有全局、函数、块级)
作用域链找变量时,从当前往外一层层找的链表

执行上下文创建时会生成作用域链;函数定义时已确定能访问哪些外层变量(词法作用域)。

🎤 口述精简版 ​

执行上下文是运行环境;作用域是变量可见范围;作用域链是由内向外找变量的路径。JS 是词法作用域,看代码写在哪,不看从哪调用。

什么是变量提升、函数提升(Hoisting)? ​

三种声明怎么比,见 var / let / const。函数五种写法见 函数写法。JS 在执行前会先「编译」:把 var 声明和函数声明提到作用域顶部。

  • 变量提升:var a 被提到顶部并初始化为 undefined(所以声明前访问是 undefined 不报错);let/const 也提升但处于 TDZ,提前访问直接报错。
  • 函数提升:函数声明整体提升,调用可以写在声明之前;函数表达式(const fn = () => {})按变量规则提升,不会整体提升。
  • 优先级:函数提升先于变量提升。

💡 学习建议:提升最容易和「闭包」「this」混在一起考。一个判断口诀:「var 提声明变 undefined,let 提了但锁住(TDZ),函数声明整个提上来、表达式不提」。用下面两个例子对比跑,立刻就懂:

js
console.log(fn)      // 函数体(整体提升了)
function fn() {}
console.log(fn2)     // ReferenceError(TDZ)
const fn2 = () => {}

🎤 口述精简版 ​

JS 运行前先提升声明:var 提到顶部变 undefined;函数声明整体提升可先调用;let/const 提升但 TDZ 锁住。函数提升优先级高于变量。

什么是闭包?闭包的作用、优缺点和使用场景? ​

闭包:函数「记住」并访问它外层作用域变量的能力——即使外层函数已经返回。

最简例子:

js
function outer() {
  let count = 0
  return function inner() {
    count++
    return count
  }
}
const fn = outer()
fn() // 1
fn() // 2  ← count 没被回收,被 inner 记着

inner 能持续访问 outer 里的 count,这就是闭包——count 被「封闭」在 inner 的作用域里,外面改不了、但它自己一直能用。

作用:数据私有(外部无法直接改 count)、柯里化、函数工厂、回调里保住变量。 缺点:被引用的外部变量不会被回收,乱用可能内存泄漏;不需要时及时解除引用(如 fn = null)。 场景:模块封装、防抖节流内部状态、Vue/React 里保留上下文。

💡 学习建议(这块很多人觉得抽象):别从定义背,从「背包」类比理解——inner 出门时把 count 装进背包带走了,外面改不了、但它自己一直能用。最容易考的坑是循环 + var:

js
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0) // 打印 3 3 3
}

因为 var 没有块级作用域,3 个回调共用同一个 i;改成 let i 或包一层闭包就打印 0 1 2。把这个循环例子亲手跑一遍,闭包基本就通了。

🎤 口述精简版 ​

闭包就是里面的函数还能用外面函数的变量,哪怕外面的函数已经执行完了。比如计数器,outer 返回 inner,inner 每次调用 count 还能加一,因为 count 被 inner 记着。好处是数据私有、保存状态。坏处是被记着的变量回收不了,乱用可能内存泄漏,用完要把引用断掉。和垃圾回收、内存泄漏怎么接,见 闭包 / GC。

普通函数和箭头函数有什么区别? ​

五种写法里的箭头函数,常单独追问和普通函数差在哪。核心是 this,再补 new、arguments。

普通函数箭头函数
this谁调用指向谁,可被 call / apply / bind 改没有自己的 this,看定义时外层,改不了
new可以不能当构造函数
arguments有没有,用剩余参数 ...args
prototype有没有

对象里如果写成箭头方法,this 不是这个对象,是外层(经常是 window / undefined),这是常见坑。适合用在回调里保住外层 this,比如 setTimeout、数组 map。

call / apply / bind 见下一节。五种写法总览见 函数写法。

🎤 口述精简版 ​

普通函数 this 看谁调用谁,能被 call 改,也能 new。箭头函数没有自己的 this,定义的时候就定死了,call 也改不了,也不能 new,没有 arguments。

call、apply、bind 的区别?各自使用场景? ​

三者都能改普通函数的 this(箭头函数除外)。不单独当一题背,会写就行。

参数形式是否立刻执行
call参数列表 fn.call(thisArg, a, b)立刻
apply数组 fn.apply(thisArg, [a, b])立刻
bind同 call 可预设参数返回新函数,稍后执行
js
const obj = { name: 'tom' }
function say(from) {
  console.log(this.name, from)
}
say.call(obj, 'call')             // tom call
say.apply(obj, ['apply'])         // tom apply
const fn = say.bind(obj, 'bind')
fn()                              // tom bind

场景:借方法、指定回调 this、bind 预设参数。箭头函数对这三下都没反应。手写见 手写与布局。

🎤 口述精简版 ​

call、apply 立刻调,区别是参数打散还是数组;bind 返回绑好 this 的新函数。箭头函数改不了。

4. 原型与原型链 ​

什么是原型、原型对象、原型链? ​

先分清三个词,再画三角关系。追问再展开 instanceof、new、继承、class 是语法糖。

词挂在谁身上干什么
prototype函数原型对象,给实例当模板;方法挂这里,实例共享
__proto__对象指向它的原型,一般等于构造函数的 prototype(标准写法是 Object.getPrototypeOf)
原型链—自身没有某个属性,就沿 __proto__ 一层层往上找,直到 null

三角关系

js
function Person(name) {
  this.name = name
}
Person.prototype.say = function () {
  return this.name
}
const p = new Person('tom')

p.__proto__ === Person.prototype              // true
Person.prototype.constructor === Person       // true
Person.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null           // 链的尽头
p.say() // 自身没有 say,沿着原型链找到 Person.prototype.say

访问属性:先看实例自己 → 构造函数 prototype → Object.prototype → null。找不到才是 undefined。

方法挂原型上的好处:所有实例共享一份,不在每个对象上各存一份。

💡 学习建议:别死背 __proto__ 这个名字(有的环境不推荐直接用), 说「实例的原型指向构造函数的 prototype」即可。能把 p → Person.prototype → Object.prototype → null 画出来,这题就过了。

🎤 口述精简版 ​

当我们声明一个函数的时候,它身上有一个 prototype,也就是原型对象,给后面 new 出来的实例当模板,方法挂在这个 prototype 上大家共享。然后就是对象的 proto,也就是原型,它指向这个模板——后面 new 出来的对象的 proto 等于函数的 prototype。原型链的话就是当我们找某个属性的时候,先看自己有没有,没有就顺着 proto 往上找,找到就用,没有就继续找,找到 Object.prototype,如果还没有再往上就是 null。这整个查找的链路就是原型链。

instanceof 的底层实现原理? ​

类型判断、== / === 见 隐式转换与比较。这里只说原理:检测构造函数的 prototype 是否出现在对象的原型链上。

js
function myInstanceof(obj, Ctor) {
  let proto = Object.getPrototypeOf(obj)
  const prototype = Ctor.prototype
  while (proto) {
    if (proto === prototype) return true
    proto = Object.getPrototypeOf(proto)
  }
  return false
}

🎤 口述精简版 ​

instanceof 就是沿着对象原型链,看有没有等于右边的 prototype。

JS 如何实现继承?列举常用继承方式 ​

方式要点
原型链继承子 prototype = 父实例;引用类型共享有问题
构造函数继承Parent.call(this);借不到原型方法
组合继承两者结合,较常用(会调两次父构造)
寄生组合Object.create(Parent.prototype) + 构造窃取,较优
class extendsES6 语法糖,底层仍是原型

🎤 口述精简版 ​

老写法里组合继承最常见,寄生组合更优;现在直接 class Child extends Parent。

new 关键字的执行过程? ​

  1. 创建空对象
  2. 链接原型:obj.__proto__ = Ctor.prototype
  3. 绑定 this 执行构造函数
  4. 若构造函数返回对象则用该对象,否则返回这个新对象

🎤 口述精简版 ​

new 四步:建对象、挂原型、当 this 执行构造、返回对象(构造返回对象则优先用返回值)。

手写练习

new 手写实现见 手写与布局。

5. 异步与事件循环 ​

浏览器 EventLoop、宏微任务已提前到章首:什么是 EventLoop。下面从 Promise 往下。

同步和异步的区别?为什么需要异步? ​

见 EventLoop · 同步和异步。单线程下请求、定时器必须异步,否则卡 UI。

宏任务和微任务有哪些?执行优先级是什么? ​

见 EventLoop · 宏任务和微任务。定时器是宏任务,Promise.then 是微任务;每轮先清空微任务,再进下一个宏任务。

简述浏览器 EventLoop 执行流程 ​

见 什么是 EventLoop。同步 → 清空微任务 →(渲染)→ 一个宏任务 → 再清空微任务,一直转。

Node 端 EventLoop 和浏览器 EventLoop 的区别? ​

浏览器:宏/微任务模型较直观(上述)。

Node(libuv)有多个阶段:timers、poll、check 等;setImmediate 与 setTimeout(0) 顺序和阶段有关;微任务(Promise)在各阶段切换间也会清空。旧版 process.nextTick 优先级高于 Promise 微任务。

够用版:都有宏微任务;Node 阶段更细,还有 nextTick / setImmediate 这些浏览器没有的概念。

🎤 口述精简版 ​

浏览器就记宏微任务循环。Node 多了阶段模型,还有 nextTick、setImmediate,细节比浏览器绕。

Promise 的状态、核心特点、常用 API 有哪些? ​

状态:pending → fulfilled / rejected,一变就定,不可逆。

特点:解决回调地狱;链式 then;错误可向下传递。

常用:then、catch、finally、Promise.resolve/reject、all / race / allSettled / any。

🎤 口述精简版 ​

Promise 三态,成功失败只定一次。常用 then/catch,组合用 all、race、allSettled。

Promise.all、Promise.race、Promise.allSettled 的区别和使用场景? ​

API行为场景
all全成功才成功;一个失败就失败并行都要成功
race谁先结束用谁的结果超时控制、赛跑
allSettled等全部结束,给每人成功/失败结果都要结果,允许部分失败

🎤 口述精简版 ​

all 要全成;race 取最快;allSettled 全跑完都要报告。

async/await 的原理和优缺点? ​

async 函数返回 Promise;await 等 Promise 完成,后面代码相当于放进 then。可理解为生成器 + Promise 的语法糖(引擎层优化实现)。

优点:同步写法写异步,可读性好。
缺点:滥用串行 await 会变慢(可独立的应用 Promise.all);错误要用 try/catch 管。

🎤 口述精简版 ​

async/await 是 Promise 的语法糖,用同步样子写异步。能并行的别傻等串行。

async/await 如何捕获异常? ​

js
async function load() {
  try {
    const data = await fetchData()
    return data
  } catch (e) {
    console.error(e)
  }
}

// 或
load().catch(handleError)

await 的 Promise reject 会抛成异常,用 try/catch 或外层 .catch。

🎤 口述精简版 ​

await 失败会抛错,函数里 try/catch,或者调用方 .catch。

手写练习

简易 Promise 手写见 手写与布局。

手写练习

防抖节流、深浅拷贝、去重、reduce、flat 见 手写与布局 · JS 工具函数。

6. 模块化:ESM 与 CommonJS 的区别? ​

你用 Vite + Vue3,工程里全是 ESM,这块实际天天用只是没意识到。

对比点ESM (import/export)CommonJS (require/module)
加载时机编译时静态分析运行时加载
导出本质值的只读引用(绑定)值的拷贝
能否动态静态,顶层才能 import可动态 require()
Tree-shaking支持(静态可分析)难
运行环境浏览器原生、现代打包器Node 传统

关键点:ESM 导出的是「活的引用」,模块内改了外部也能看到;CommonJS require 拿到的是当时值的拷贝,之后原模块改了不影响你。

💡 学习建议:记忆锚点——「ESM 是静态的、引用式、能摇树;CommonJS 是运行时、拷贝式、能动态 require」。 常追问「tree-shaking 为什么依赖 ESM」——答案就在这张表:只有静态的 import 才能被打包器在编译期分析出哪些没用到、从而删掉。

🎤 口述精简版 ​

ESM 是静态的、引用式、能摇树;CommonJS 是运行时、拷贝式、能动态 require。Vite 项目里默认就是 ESM。

7. 内存泄漏与垃圾回收 ​

闭包把外层变量记着,所以和垃圾回收、内存泄漏经常一起考。核心就一句话:还有引用就不回收。闭包本身见 闭包。

JS 垃圾回收机制是什么? ​

JS 内存由引擎自动回收,开发者不需要手动释放。核心直觉:还有引用 = 不回收。

主流算法:标记清除(Mark-Sweep) ​

从根对象(全局、调用栈等)出发,标记所有还能访问到的对象;没被标记的视为垃圾并回收。

缺点:回收后可能留下内存碎片。

已淘汰:引用计数 ​

记录每个对象被引用的次数,引用数为 0 时回收。

问题:循环引用时计数永远不为 0,容易内存泄漏,所以已被淘汰。

🎤 口述精简版 ​

JS 是自动垃圾回收,核心是还有引用就不回收。现在主流是标记清除:从全局、调用栈这些根出发,能碰到的留下,碰不到的回收。以前的引用计数搞不定循环引用,基本不用了。

什么是内存泄漏?常见场景有哪些?怎么排查? ​

内存泄漏:本该被回收的内存,因为还被引用着而一直释放不掉,积少成多会导致页面卡顿、崩溃。

常见场景:

  • 意外全局变量:函数内漏写 var/let 直接 a = 1,变成全局属性,永不回收。
  • 被遗忘的定时器 / 回调:setInterval 没 clear,回调里引用的大对象一直挂着。
  • 脱离 DOM 的引用:把 DOM 节点存进数组 / 对象,节点从页面删了但引用还在。
  • 事件监听未解绑:节点或窗口上的 listener 还在,连带把相关对象挂住。
  • 闭包持有大对象:闭包记着的外部变量一直不释放(见上节)。

解决也对着来:让对象不再被引用,GC 才能回收。

泄漏场景解决办法
意外全局变量开严格模式;别漏写 let/const
定时器未清除组件销毁 / 页面离开时 clearInterval / clearTimeout
DOM 引用未释放节点删掉后,把对应变量置 null,别再挂着引用
事件未解绑用完 removeEventListener;框架里在 onUnmounted / useEffect return 里清掉
闭包挂死数据闭包只保留必要变量;不用的大对象及时断开引用

排查:Chrome DevTools → Memory 面板拍堆快照(Heap Snapshot),对比操作前后的快照,看哪些对象数量只增不减;或用 Performance Monitor 看 JS Heap 曲线是否只涨不落。

💡 学习建议:这块你「工作中没用过」很正常——内存泄漏多出现在长生命周期页面(后台系统、富交互编辑器、SPA 路由不销毁)。学习路径:① 先建立「引用 = 不回收」的直觉;② 背熟上面几类场景( 基本就考这些,不用深究 V8 分代回收算法);③ 想动手就写个 setInterval 不清除,在 DevTools 里看内存曲线涨,再 clearInterval 看回落——一次就建立体感。

🎤 口述精简版 ​

内存泄漏就是该回收的还被引用挂着,一直放不掉。常见是意外全局、定时器没清、DOM 删了变量还挂着、事件没解绑、闭包里捏着大对象。解决就是对着把引用清掉。排查用 Chrome Memory 拍堆快照对比。

整道题怎么一口气说:先说闭包是里面的函数还能用外面的变量;再说 JS 自动回收,有引用就不回收;最后泄漏就是该回收的还挂着。见概念卡口述。

8. DOM 事件:冒泡、捕获、委托 ​

事件冒泡、捕获、委托? ​

点击里层节点时,事件不是只在这一点上响,会在整棵 DOM 树上走一圈,分三阶段:

  1. 捕获:从 window → document → 外层父节点,一路往里走到目标
  2. 目标:真正被点的那个节点
  3. 冒泡:从目标再往外走,回到外层,一直到 document / window

addEventListener(type, fn, useCapture) 第三个参数默认 false,监听冒泡;传 true 才在捕获阶段触发。

  • event.stopPropagation():拦住,后面的捕获 / 冒泡都不再走
  • event.preventDefault():不让浏览器做默认动作(链接跳转、表单提交)
  • 两件事不是一回事,别混

事件委托 ​

把子元素的点击统一绑在父级,等冒泡上来再处理。监听少,后来动态插进来的节点也能点到,列表很常用。

js
list.addEventListener('click', (e) => {
  const item = e.target.closest('li')
  if (!item || !list.contains(item)) return
  // 处理这一项
})

🎤 口述精简版 ​

点一下会先从外往里捕获,到目标,再从里往外冒泡。默认监听的是冒泡。stopPropagation 停传播,preventDefault 停默认行为。委托就是利用冒泡,把子元素的事件绑到父级统一处理,省监听,动态节点也能点到。