Appearance
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 本身就是浏览器提供的第一个宏任务。事件循环一轮一轮执行:
- 从宏任务队列取出一个宏任务,推入主线程调用栈
- 执行这个宏任务时:遇到同步任务就直接在调用栈执行;遇到异步任务交给浏览器其他后台线程,完成后再把回调放入宏任务或微任务队列,不阻塞主线程
- 当前宏任务里所有同步代码执行完毕、调用栈清空之后,立刻清空全部微任务队列;微任务过程中新产生的微任务本轮一并执行
- 微任务全部完成后,浏览器进行 UI 渲染
- 再进入下一轮,取下一个宏任务,重复上面流程
一轮完整顺序:取一个宏任务 → 执行内部同步代码 → 清空全部微任务 → UI 渲染 → 下一个宏任务。
宏任务和微任务常见有哪些
| 宏任务(macrotask) | 微任务(microtask) |
|---|---|
整段 script、setTimeout / setInterval | Promise.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 21、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 / 一个函数)时,先创建执行上下文,分两步——
- 先处理声明:扫一遍
var/let/const/ 函数声明(这就是提升) - 再一行一行执行:从第一行往下跑,跑到哪一行,哪一行才真正赋值
暂时性死区就是第 2 步还没走到 let / const 那一行:名字在第 1 步已经占坑了,但执行指针还没到,提前用就报错。执行上下文细讲见 作用域那一节。
| 对比点 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
顶层是否挂 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 方法。
- 函数声明(函数提升):
function fn() {}。整份函数会提升,写在后面也能先调用。
js
fn() // 能跑
function fn() { return 1 }- 函数表达式(变量接收函数):把函数赋给一个名字,
const fn = function () {}。赋上之后才能调,没有函数提升。
js
const fn = function () { return 1 }
fn()箭头函数(ES6):
const fn = () => {},本质也是函数表达式。没有自己的this/arguments,不能new。this 看定义时外层,细讲见 普通函数 vs 箭头函数。new Function构造函数(极少用):用字符串在运行时拼出一个函数。不形成闭包,始终走全局作用域,类似eval,业务里几乎不用。
js
const add = new Function('a', 'b', 'return a + b')
add(1, 2) // 3- 对象方法 / 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 / symbol | object / 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
undefined | null | |
|---|---|---|
| 含义 | 未定义、未赋值 | 空对象指针,有意「没有」 |
| 常见来源 | 变量声明未赋、缺属性、函数无 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:
下面是第 4、5 块速查。class 清单仍在本节。
面向对象(class / 继承)核心点
五种函数写法见 函数写法,面向对象怎么答见 面向对象。下面是 class 语法速查。
- class 与 constructor:
class A { constructor(x) { this.x = x } } - 继承 extends + super:
class B extends A { constructor() { super() } },super()必须在this前调用 - static 静态成员:挂在类上、不挂在实例(
A.foo()),常用于工具方法 - 私有字段
#(ES2022):#secret,外部访问直接报错,才是真正的私有 - getter / setter:拦截属性的读 / 写,可做校验或计算属性
- 实例方法在 prototype 上:所有实例共享同一份方法,不在每个实例各存一份
- 判断实例:
obj instanceof A或A.prototype.isPrototypeOf(obj) - JS 没有接口 / 抽象类(语言层),靠 TS 补;"多态"靠鸭子类型(长得像就能用)
- class 是语法糖:底层仍是原型链,
class A {}≈ 构造函数 +prototype - 封装约定:没有
#的时代用_name约定"私有"(其实外部仍能访问)
🎤 口述精简版
JS 面向对象用 class 写、底层还是原型;继承靠 extends + super,静态方法挂类上、实例方法挂原型;真正的私有字段用 #(ES2022),之前靠 _ 约定。
解构赋值(常用写法清单)
- 数组解构:
const [a, b] = arr - 对象解构:
const { name } = obj - 重命名:
const { name: userName } = obj - 默认值:
const { x = 1 } = obj(属性不存在 / 为 undefined 时才生效) - 剩余项:
const [first, ...rest] = arr - 嵌套解构:
const { user: { id } } = data - 参数解构:
function f({ a, b = 2 } = {}) {}—— 解构 props / options 最常用 - 交换变量:
[a, b] = [b, a] - 跳过项:
const [a, , c] = arr
ES6+ 数组操作(必会方法)
- map(fn):映射成新数组(一一对应,不改原数组)
- filter(fn):过滤出满足条件的元素
- reduce(fn, init):聚合成一个值(求和 / 分组 / 扁平 / 计数)
- forEach(fn):遍历,无返回值(不能 break,要中断用 some/every 或 for...of)
- find / findIndex:找第一个符合的元素 / 下标(找不到返回 undefined / -1)
- some / every:是否有至少一个 / 是否全部符合
- includes(val):是否包含某值(替代
indexOf > -1) - flat(depth) / flatMap:多层扁平化 / 映射后扁平
- sort(compareFn):排序(原地修改原数组;比较函数返回正/负/0)
- Array.from / Array.of:类数组 / Set → 数组、创建数组;
slice(不改动)vssplice(改动原数组、可删插)
字符串操作(常用 API)
- includes / startsWith / endsWith:是否包含 / 前缀 / 后缀
- slice(start, end):截取子串(不改动原串);
substring类似但自动处理负/颠倒参数 - split / join:字符串 → 数组 / 数组 → 字符串(互逆,经典如
str.split('').reverse().join('')翻转) - trim / trimStart / trimEnd:去首尾 / 左 / 右空白
- replace / replaceAll:替换首个 / 替换全部(全局用
/g或replaceAll) - repeat(n):重复 n 次
- padStart / padEnd:补位(如
'1'.padStart(2, '0')→'01') - toUpperCase / toLowerCase:大小写转换
- charAt(i) / [i] / charCodeAt(i):取第 i 个字符 / 取字符编码
- 模板字符串(反引号):
${}插值、可换行、可写表达式,替代字符串拼接
对象操作(常用写法)
- 属性简写:
{ name }等于{ name: name };方法简写{ say() {} } - 展开 / 合并:
{ ...obj, age: 18 }(浅拷贝,后者覆盖前者) - Object.assign(target, ...src):也是浅合并,会改
target - Object.keys / values / entries:键名 / 键值 /
[键, 值]数组 - 计算属性名:
{ [key]: val } - 可选链 / 空值合并(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),函数声明整个提上来、表达式不提」。用下面两个例子对比跑,立刻就懂:
jsconsole.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:jsfor (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 extends | ES6 语法糖,底层仍是原型 |
🎤 口述精简版
老写法里组合继承最常见,寄生组合更优;现在直接 class Child extends Parent。
new 关键字的执行过程?
- 创建空对象
- 链接原型:
obj.__proto__ = Ctor.prototype - 绑定 this 执行构造函数
- 若构造函数返回对象则用该对象,否则返回这个新对象
🎤 口述精简版
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 树上走一圈,分三阶段:
- 捕获:从
window→document→ 外层父节点,一路往里走到目标 - 目标:真正被点的那个节点
- 冒泡:从目标再往外走,回到外层,一直到
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 停默认行为。委托就是利用冒泡,把子元素的事件绑到父级统一处理,省监听,动态节点也能点到。