以下题目是根据网上多份面经收集而来的,题目相同意味着被问的频率比较高,有问题欢迎留言讨论,喜欢可以点赞关注。
以下是 ES6+ 的高频面试题及详细解析,涵盖核心语法、原理和应用场景:
一、变量声明与作用域
- let/const 与 var 的区别
- 块级作用域:
let/const声明的变量仅在代码块内有效。 - 暂时性死区(TDZ):声明前访问变量会报错。
- 不可重复声明:同一作用域内不能重复声明。
if (true) { let a = 1; var b = 2; } console.log(a); // ReferenceError console.log(b); // 2 - 块级作用域:
二、箭头函数
- 特性
- 无
this,继承外层作用域的this - 不可作为构造函数
- 无
arguments对象 - this指向固定,不能通过call、apply或bind改变
- 不支持new.target和super
const obj = { name: 'Alice', greet: () => console.log(this.name) // 输出 undefined(this 指向 window) }; - 无
-
普通函数:
- 使用
arguments对象来处理不确定数量的参数。 arguments是一个类数组对象,不具备数组的方法。- 支持传统的函数声明和函数表达式。
- 使用
-
箭头函数:
- 使用剩余参数语法(
...args)来处理不确定数量的参数。 ...args是一个真正的数组,具备数组的方法。- 不支持
arguments对象,也不具备传统函数的this绑定行为。
- 使用剩余参数语法(
选择使用普通函数还是箭头函数取决于具体的需求,例如是否需要 arguments 对象或是否需要处理动态参数。箭头函数通常更简洁,但普通函数在处理动态参数时提供了更多的灵活性。
三、解构赋值
-
对象解构
const { name: userName, age = 18 } = { name: 'Bob' }; -
数组解构
经典场景——交换变量
let [a, b] = [1, 2]; [a, b] = [b, a]; // 交换变量
四、Promise 与异步
五、类与继承
- ES6 类继承
class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a noise`); } } class Dog extends Animal { constructor(name) { super(name); } speak() { console.log(`${this.name} barks`); } }
六、模块化
- ES Module vs CommonJS
- 静态导入(ESM):
import/export必须在顶层,支持 Tree Shaking。 - 动态导入:
const module = await import('./module.js');
- 静态导入(ESM):
七、新数据结构
- Map vs Object
- 键类型:
Map 支持任意类型键,Object 仅支持字符串/Symbol。 - 顺序:
Map 保留插入顺序。
const map = new Map(); map.set({}, 123); - 键类型:
八、Proxy 与 Reflect
- 实现数据响应式
const reactive = (obj) => new Proxy(obj, { get(target, key) { track(target, key); // 依赖收集 return Reflect.get(target, key); }, set(target, key, value) { trigger(target, key); // 触发更新 return Reflect.set(target, key, value); } });
九、ES2020+ 新特性 ⭐️
-
可选链操作符(?.)const name = user?.profile?.name; // 避免 Cannot read property 错误 -
空值合并运算符(??)const value = input ?? 'default'; // 仅在 null/undefined 时使用默认值
十、高频编码题
-
数组去重
const uniqueArr = [...new Set(arr)]; -
对象合并
const merged = { ...obj1, ...obj2 };
箭头函数应用场景
- 回调函数:简化代码结构。
- 数组方法:如 map 、 filter 、 reduce 等。
- 事件监听:避免 this 指向问题。
- 函数式编程:高阶函数、闭包等。
如果new一个箭头函数会怎么样?
箭头函数是ES6中的提出来的,它没有prototype,也没有自己的this指向,更不可以使用arguments参数,所以不能New一个箭头函数。
new操作符的实现步骤如下:
1、创建一个空的简单JavaScript对象(即{});
2、为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
3、将步骤1新创建的对象作为this的上下文 ;
4、如果该函数没有返回对象,则返回this。
所以,上面的第二、三步,箭头函数都是没有办法执行的。
箭头函数的this指向哪里
箭头函数的this指向定义箭头函数时所在的 “外层作用域的 this” ,且一旦绑定就无法改变(没有自己的this)。
简单说:
- 箭头函数本身不绑定 this,它会 “继承” 定义它时所处的最近的非箭头函数作用域的 this;
- 无论用
call/apply/bind,都无法修改箭头函数的this指向; - 若外层没有非箭头函数(比如在全局作用域定义箭头函数),则
this指向全局对象(浏览器是window,Node.js 是global,但严格模式下全局作用域的箭头函数this是undefined)。
举个例子:
const obj = {
name: 'obj',
fn1: function() { // 普通函数,this指向obj
const fn2 = () => {
console.log(this.name); // 箭头函数的this继承fn1的this → 输出obj
};
fn2();
}
};
obj.fn1(); // 输出obj
箭头函数与普通函数的区别
1、语法更加简洁、清晰
2、箭头函数不会创建自己的this(重要!!深入理解!!)
箭头函数不会创建自己的this,所以它没有自己的this,它只会从自己的作用域链的上一层继承this。
箭头函数没有自己的this,它会捕获自己在定义时(注意,是定义时,不是调用时)所处的外层执行环境的this,并继承这个this值。所以,箭头函数中this的指向在它被定义的时候就已经确定了,之后永远不会改变。
3、箭头函数继承而来的this指向永远不变(重要!!深入理解!!)
上面的例子,就完全可以说明箭头函数继承而来的this指向永远不变。对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。
4、.call()/.apply()/.bind()无法改变箭头函数中this的指向
.call()/.apply()/.bind()方法可以用来动态修改函数执行时this的指向,但由于箭头函数的this定义时就已经确定且永远不会改变。所以使用这些方法永远也改变不了箭头函数this的指向,虽然这么做代码不会报错。
5、箭头函数不能作为构造函数使用
我们先了解一下构造函数的new都做了些什么?简单来说,分为四步:
① JS内部首先会先生成一个对象; ② 再把函数中的this指向该对象; ③ 然后执行构造函数中的语句; ④ 最终返回该对象实例。
但是!!因为箭头函数没有自己的this,它的this其实是继承了外层执行环境中的this,且this指向永远不会随在哪里调用、被谁调用而改变,所以箭头函数不能作为构造函数使用,或者说构造函数不能定义成箭头函数,否则用new调用时会报错!
6、箭头函数没有自己的arguments
箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值。
7、箭头函数没有原型prototype
let sayHi = () => { console.log('Hello World !') }; console.log(sayHi.prototype); // undefined
8、箭头函数不能用作Generator函数,不能使用yeild关键字
箭头函数主要解决了以下几个问题
- 简化函数表达式:箭头函数提供了一种更简洁的函数定义方式,可以用更短的语法来定义函数,减少了冗余的代码。例如,使用箭头函数可以将一个函数表达式
function(x) { return x * x; }简化为(x) => x * x;。 - 简化this的指向:在传统的函数定义中,函数内部的
this指向的是调用该函数的对象。而在箭头函数中,this的指向是在定义函数时确定的,指向的是箭头函数所在的上下文。这解决了传统函数中this指向容易混淆的问题,使得代码更加易读和简洁。 - 消除了
arguments对象:在箭头函数中,不存在arguments对象,这是因为箭头函数没有自己的arguments,它继承了所在上下文的arguments。这样可以避免在传统函数中使用arguments对象时出现的一些问题,如无法使用arguments对象的一些方法,以及与命名参数的冲突等。
NaN 和NaN是不是全等
在 JavaScript 中,NaN 与 NaN 不全等 ,使用全等运算符 === 比较两个 NaN 时,结果为 false 。
这是因为根据 IEEE 754 浮点数标准规定,NaN 表示 “不是一个数” ,它与任何值(包括自身)都不相等。即使两个表达式都产生了 NaN ,它们也被视为不相等。例如:
console.log(NaN === NaN); // 输出 false
如果要判断一个值是否为 NaN ,不能使用 === 或 == ,而应使用 isNaN() 函数(在 ES6 及以后推荐使用 Number.isNaN() ,它不会对参数进行类型转换,判断更准确 ) ,示例如下:
console.log(isNaN(NaN)); // 输出 true
console.log(Number.isNaN(NaN)); // 输出 true
说一说var、let、const 之间的区别
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | ✅(值为 undefined) | ❌(TDZ) | ❌(TDZ) |
| 重复声明 | ✅ | ❌ | ❌ |
| 重新赋值 | ✅ | ✅ | ❌(引用类型属性可修改) |
| 初始化要求 | ❌ | ❌ | ✅ |
最佳实践:
- 默认使用
const,需重新赋值时改用let。 - 避免使用
var,以利用块级作用域和避免变量提升问题。
4. 初始化与修改
| 关键字 | 初始化要求 | 是否允许修改 |
|---|---|---|
const | ❗️ 必须初始化 | ❌ 不可重新赋值(引用类型可修改属性或元素)。 |
示例:
const PI; // SyntaxError: Missing initializer in const declaration
const obj = { a: 1 };
obj.a = 2; // ✅ 允许修改属性
obj = {}; // ❌ TypeError: Assignment to constant variable
5. 全局作用域下的行为
| 关键字 | 全局声明时的行为 |
|---|---|
var | 成为全局对象(如 window)的属性。 |
let | 不会成为全局对象的属性,存在于独立的全局词法环境。 |
const | 同 let。 |
示例:
var globalVar = 1;
console.log(window.globalVar); // 1
let globalLet = 2;
console.log(window.globalLet); // undefined
6. 典型应用场景
| 关键字 | 适用场景 |
|---|---|
let | 需要重新赋值的变量,如循环计数器、条件分支内的变量。 |
const | 推荐默认使用,用于常量、对象/数组引用(内容可修改但引用不变)。 |
示例:
// 循环中使用 let
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100); // 输出 0,1,2,3,4
}
ES6中let块作用域是怎么实现的
let的话,是
不会在栈内存里预分配内存空间,而且在栈内存分配变量时,做一个检查,如果已经有相同变量名存在就会报错。也就是暂时性死区,只要块级作用域内存在 let 命令,它所声明的变量就“绑定”( binding )这个区域,不再受外部的影响,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。
const的话,
也不会预分配内存空间,在栈内存分配变量时也会做同样的检查。不过const存储的变量是不可修改的,对于基本类型来说你无法修改定义的值,对于引用类型来说你无法修改栈内存里分配的指针,但是你可以修改指针指向的对象里面的属性
函数默认参数和剩余(rest)参数
默认值仅在参数未传递或传递undefined时生效。
function show({x=0,y=0}={}){
console.log(x,y);
}
show()
2. 函数参数默认已经定义了,不能再使用let,const声明
function show(a=18){
let a = 101; //错误
console.log(a);
}
show()
扩展运算符、Rest运算符:
...
展开数组
... :
[1,2,3,4] -> ... [1,2,3,4] -> 1,2,3,4,5
...:
1,2,3,4,5 -> ...1,2,3,4,5 -> [1,2,3,4,5]
剩余参数: 必须放到最后
rest参数和 arguments对象的区别
- 剩余参数只包含那些没有对应形参的实参(可以是参数的一部分),而 arguments 对象包含了传给函数的所有实参(是参数的全部)。
- arguments 对象不是一个真实的数组,而剩余参数是真实的 Array 实例。也就是说,能够在它上面直接使用所有的数组方法,比如 sort、map、forEach、pop。
- 如果想在 arguments 对象上使用数组方法,首先要将它转换为真实的数组,比如使用 [].slice.call (arguments)。
Object.keys()方法,获取对象的所有属性名或方法名
一、语法
Object.keys(obj)
参数:要返回其枚举自身属性的对象
返回值:一个表示给定对象的所有可枚举属性的字符串数组
二、处理对象,返回可枚举的属性数组
let person = {name:"张三",age:25,address:"深圳",getName:function(){}}
Object.keys(person) // ["name", "age", "address","getName"]
三、处理数组,返回索引值数组 字符串
let arr = [1,2,3,4,5,6]
Object.keys(arr) // ["0", "1", "2", "3", "4", "5"]
四、处理字符串,返回索引值数组
let str = "saasd字符串"
Object.keys(str) // ["0", "1", "2", "3", "4", "5", "6", "7"]
五、常用技巧
let person = {name:"张三",age:25,address:"深圳",getName:function(){}}
Object.keys(person).map((key)=>{
person[key] // 获取到属性对应的值,做一些处理
})
六、Object.values()和Object.keys()是相反的操作,把一个对象的值转换为数组
Object.assign ()原对象的属性和方法都合并到了目标对象
用途: 1. 复制一个对象 2. 合并参数
Object.assign(): 用来合并对象
let 新的对象 = Object.assign(目标对象, source1, srouce2....)
function ajax(options){ //用户传
let defaults={
type:'get',
header:
data:{}
....
};
let json = Object.assign({}, defaults, options);
.....
}
let json = { a: 3, b: 4 };
let json2 = { ...json };
console.log(json2 === json)//false
console.log(Object.assign({}, json) === json)//false
console.log(Object.assign(json) === json)//true
注意:
1.如果目标对象中的属性具有相同的键。后面的源对象的属性将类似地覆盖前面的源对象的属性
2.Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。
for...of 循环
一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的 Symbol.iterator方法。
for...of循环可以使用的范围包括数组、Set 和 Map 结构、类数组的对象(比如arguments对象、DOM NodeList 对象)、Generator 对象,以及字符串。
for...of 遍历 非 Iterator 的类数组对象
并不是所有类似数组的对象都具有 Iterator 接口,一个简便的解决方法,就是使用Array.from方法将其转为数组。
let arrayLike = { length: 2, 0: 'a', 1: 'b' };
// 报错
for (let x of arrayLike) {
console.log(x);
}
// 正确
for (let x of Array.from(arrayLike)) {
console.log(x);
}
for...of 遍历 对象
对于普通的对象,for...of结构不能直接使用,会报错。使用 for...in 可以遍历对象的键名。
let obj = {
a: 1,
b: 2,
c: 3
}
for (let e in obj) {
console.log(e); // 'a' 'b' 'c'
}
总之,for...in循环主要是为遍历对象而设计的,不适用于遍历数组。
一种解决方法是,使用Object.keys方法将对象的键名生成一个数组,然后再用 for...of 遍历这个数组。
for(let key of Object.keys(obj)) {
console.log(key + ': ' + obj[key]);
}
另一个方法是使用 Generator 函数将对象重新包装一下。
function* entries(obj) {
for(let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
for(let [key, value] of entries(obj)) {
console.log(key, '->', value);
}
for...of 可以与 break / continue / return 配合使用
for (var n of arr) {
if (n > 10) {
break;
}
console.log(n);
}
CommonJS 和 ES6 Module 的区别? ⭐️⭐️⭐️
| 对比维度 | CommonJS | ES6 Module(ESM) |
|---|---|---|
| 语法差异 | require 导入、module.exports/exports 导出 | import 导入、export 导出 |
| 加载机制 | 运行时加载(执行到 require 才加载,运行时确定依赖关系) | 编译时静态分析(编译阶段确定依赖) |
| 导出机制 | 导出「值拷贝」,后续修改不影响导入方 | 导出「只读绑定」,导入方同步更新 |
| 树摇支持 | 无树摇(无法剔除未使用代码) | 支持树摇(静态分析实现代码优化) |
| 运行环境 | Node.js 原生支持,浏览器需打包 | 浏览器 / Node.js 均原生支持(Node 需指定 type: module) |
JS引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,在根据引用到被加载的那个模块里面去取值。ES6模块是动态引用,并且不会缓存运行结果
ES6可以在编译时就完成模块加载,效率要比CommonJS模块的加载方式高。
set数据结构(可用于快速去重)
Set 能快速去重,核心原因是 内部用「哈希表(Hash Table)」存储数据,且天然不允许重复元素—— 这两个特性让去重操作的时间复杂度降到 O (n),比传统数组遍历去重(O (n²))快得多。
关键逻辑拆解:
- 存储结构:哈希表(核心提速点) Set 不会像数组那样按顺序存储元素,而是通过「哈希函数」把每个元素映射到哈希表的固定位置(类似字典的 “键”)。
- 查找元素时:不用遍历所有元素,直接通过哈希函数计算位置,一步定位(时间复杂度 O (1));
- 对比数组去重:数组要判断元素是否重复,得遍历已存元素逐一比对(O (n)),整体去重是 O (n²)。
- 天然去重规则:元素唯一Set 的核心设计就是 “不允许重复元素”,判断重复的规则和
===类似(仅 NaN 视为相等,这是特例):
- 当你调用
set.add(value)时,Set 会先通过哈希表快速查找该 value 是否已存在; - 若存在,直接忽略添加操作;若不存在,才把 value 存入哈希表。
简单对比:Set 去重 vs 数组去重
// Set 去重(O(n),快)
const arr = [1, 2, 2, 3, 3, 3];
const uniqueArr = [...new Set(arr)]; // [1,2,3]
// 数组遍历去重(O(n²),慢)
const uniqueArr2 = arr.filter((item, index) => {
return arr.indexOf(item) === index; // 每次 indexOf 都要遍历数组
});
总结
Set 快速去重的本质是:用哈希表实现 “O (1) 快速查找”,配合 “天然不重复” 的存储规则,遍历一次数据就能完成去重,效率远高于数组的遍历比对方案。
字符串新增方法
1、String.fromCodePoint()
2、String.raw()
3、codePointAt()
4、normalize()
5、includes(), startsWith(), endsWith()
6、repeat()
7、padStart(),padEnd()
8、trimStart(),trimEnd()
9、matchAll()
关于字符串一些东西:
字符串查找:
str.indexOf(要找的东西) 返回索引(位置) ,没找到返回-1
str.includes(要找的东西) 返回值 true/false
判断浏览器: includes
http://www.xxx.xx
字符串是否以谁开头:
str.startsWith(检测东西)
检测地址
字符串是否以谁结尾:
str.endsWith(检测东西)
.png
重复字符串:
str.repeat(次数);
填充字符串:
str.padStart(整个字符串长度, 填充东西) 往前填充
str.padEnd(整个字符串长度, 填充东西) 往后填充
str.padStart(str.length+padStr.length, padStr)
symbol应用
定义:
symbol 使用情况一般
let syml = Symbol('aaa')
用typeof检测出来数据类型: symbol
new Number(12)
new String()
new Array()
注意:
- Symbol 不能new
- Symbol() 返回是一个唯一值坊间传说, 做一个key,定义一些唯一或者私有一些东西
- symbol是一个单独数据类型,就叫 symbol, 基本类型
- 如果symbol作为key,用for in循环,出不来
- Symbol值作为对象属性时,不能使用点运算符。
- vue v-for key可以用Symbol
Symbol 是 ES6 新增的基本数据类型,核心特性是唯一性(即使描述相同,两个 Symbol 也不相等)和不可枚举性(默认不会被常规遍历方法捕获),这让它能解决很多传统字符串 / 数字作为标识时的痛点。以下是其核心应用场景,结合前端(尤其是 Vue/React)、工程化等实际开发场景说明:
一、作为对象的唯一属性名,避免属性冲突
这是 Symbol 最核心的场景。当多人协作 / 引入第三方库时,用字符串作为属性名易出现命名冲突,Symbol 可保证属性绝对唯一。
// 场景:第三方库对象 + 自定义属性,避免覆盖
const libObj = { name: '第三方对象' };
// 自定义 Symbol 属性,不会覆盖原有属性
const myKey = Symbol('desc');
libObj[myKey] = '自定义值';
// 即使第三方库也用了 "desc" 描述的 Symbol,也不会冲突
const libKey = Symbol('desc');
libObj[libKey] = '库的属性';
console.log(libObj[myKey]); // 自定义值(互不干扰)
Vue 中的典型应用:Vue3 中 provide/inject 用 Symbol 作为 key,避免不同组件间传值时 key 冲突(比如多个组件都用 'theme' 作为 key,Symbol 可保证唯一性):
<script setup>
import { provide, inject } from 'vue'
const THEME_KEY = Symbol('theme');
provide(THEME_KEY, 'dark'); // 唯一 key
const theme = inject(THEME_KEY); // 精准获取
</script>
二、定义常量 / 枚举,避免魔法值与常量冲突
用 Symbol 定义常量,可杜绝常量值重复导致的逻辑错误(字符串 / 数字常量易重复,Symbol 天然唯一)。
// 传统字符串常量:易冲突、易误改
const STATUS_SUCCESS = 'success';
const STATUS_ERROR = 'error';
// Symbol 常量:绝对唯一,无需担心值重复
const STATUS_SUCCESS = Symbol('success');
const STATUS_ERROR = Symbol('error');
// 枚举场景(替代数字/字符串枚举)
const Direction = {
LEFT: Symbol('left'),
RIGHT: Symbol('right'),
};
function move(dir) {
if (dir === Direction.LEFT) { /* ... */ }
}
三、模拟私有属性 / 方法(ES 私有字段提案前的方案)
Symbol 作为对象属性时,默认不会被 for...in、Object.keys()、JSON.stringify() 遍历到,可模拟 “私有属性”(非绝对私有,但能避免被常规操作访问)。
const privateMethod = Symbol('privateMethod');
class MyClass {
[privateMethod]() {
return '私有方法';
}
publicMethod() {
return this[privateMethod](); // 内部可访问
}
}
const instance = new MyClass();
console.log(instance.publicMethod()); // 私有方法
console.log(Object.keys(instance)); // [](无法遍历到 Symbol 属性)
console.log(instance[privateMethod]); // 外部需拿到 Symbol 才能访问,间接实现私有
注:ES2022 已支持
#定义真正的私有字段,但 Symbol 仍可用于 “弱私有” 场景(允许特定场景下外部通过 Symbol 访问)。
四、拓展内置对象 / 原生方法,避免覆盖原生属性
比如想给 Array、Object 扩展自定义方法,用 Symbol 作为方法名,不会覆盖原生方法。
// 扩展 Array,添加自定义遍历方法,避免覆盖 forEach
const forEachCustom = Symbol('forEachCustom');
Array.prototype[forEachCustom] = function (callback) {
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this);
}
};
const arr = [1,2,3];
arr[forEachCustom](item => console.log(item)); // 1 2 3
console.log(arr.forEach); // 原生 forEach(未被覆盖)
五、作为迭代器标识(实现可迭代接口)
ES6 中,对象实现 [Symbol.iterator] 方法即可成为可迭代对象,支持 for...of 遍历,这是 Symbol 的内置核心应用。
const iterableObj = {
data: [1,2,3],
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
return index < this.data.length
? { value: this.data[index++], done: false }
: { done: true };
}
};
}
};
// 支持 for...of 遍历
for (const item of iterableObj) {
console.log(item); // 1 2 3
}
常见内置 Symbol 接口:除了 Symbol.iterator,还有 Symbol.toStringTag(自定义对象 toString 结果)、Symbol.asyncIterator(异步迭代)等,用于扩展对象的原生行为。
六、Vue/React 中的特殊场景
-
Vue 组件的唯一 key:循环渲染时,用 Symbol 作为 key(替代索引 / 字符串),避免列表重排时的 key 冲突(尤其动态列表);
-
React 上下文(Context)标识:创建 Context 时用 Symbol 作为标识,避免不同 Context 重名:
const MyContext = React.createContext(Symbol('myContext'));
核心总结
Symbol 的核心价值是唯一性和不可枚举性,主要解决 “命名冲突” 和 “弱私有 / 扩展” 问题:
- 优先用于:对象唯一属性、常量枚举、跨模块协作的标识(如 Vue provide/inject);
- 次要场景:模拟私有属性、扩展内置对象、实现迭代器;
- 注意:Symbol 不能被隐式转换为字符串,需用
toString()或description属性获取描述,且无法被 JSON.stringify 序列化。
es7中新增加了什么
ES2016(es7)添加了两个小的特性来说明标准化过程:
1、Array.prototype.includes
注意:includes () 能够在数组中寻找 NaN
2、取幂运算符:“ ** ” 表示的是取幂运算。
比如,x**y 等价于 Math.pow (x, y)。var num = 3 ** 2 的运算结果为 9。
ES6转成ES5的常见例子
ES6 转 ES5 的核心是将 ES6 新增语法 / API 降级为 ES5 可兼容的写法,常见场景集中在**「语法糖转译」「API 垫片」两类**,以下是最常用的例子(基于 Babel 转译逻辑):
一、语法糖转译(核心场景)
1. 箭头函数 → 普通函数(绑定 this)
ES6 箭头函数的 this 绑定特性,转译为 ES5 时通过 var _this = this 保存上下文:
// ES6
const add = (a, b) => a + b;
const obj = { fn: () => console.log(this) };
// 转译后 ES5
var add = function add(a, b) {
return a + b;
};
var obj = {
fn: function fn() {
var _this = this; // 保存外层 this
return console.log(_this);
}
};
2. 模板字符串 → 字符串拼接
ES6 模板字符串(${})转译为 ES5 的 + 拼接:
// ES6
const name = "张三";
const str = `Hello, ${name}, 年龄 ${20 + 5}`;
// 转译后 ES5
var name = "张三";
var str = "Hello, " + name + ", 年龄 " + (20 + 5);
3. 解构赋值 → 手动取值
数组 / 对象解构,转译为 ES5 的索引 / 属性访问:
// ES6(对象解构)
const { name, age } = { name: "李四", age: 30 };
// ES6(数组解构)
const [a, b] = [1, 2];
// 转译后 ES5
var _ref = { name: "李四", age: 30 };
var name = _ref.name;
var age = _ref.age;
var _ref2 = [1, 2];
var a = _ref2[0];
var b = _ref2[1];
4. let/const → var(配合作用域处理)
ES6 的块级作用域(let/const)转译为 ES5 的 var,通过函数作用域模拟块级:
// ES6
if (true) {
let x = 1;
const y = 2;
}
console.log(x); // 报错(块级作用域)
// 转译后 ES5(IIFE 模拟块级作用域)
if (true) {
(function () {
var x = 1;
var y = 2; // const 转 var(无赋值保护,需依赖 eslint)
})();
}
console.log(x); // undefined(作用域隔离)
5. 类 Class → 构造函数
ES6 class 语法糖,转译为 ES5 原型链继承:
// ES6
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`Hi, ${this.name}`);
}
}
class Student extends Person {
constructor(name, grade) {
super(name);
this.grade = grade;
}
}
// 转译后 ES5
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function () {
console.log("Hi, " + this.name);
};
function Student(name, grade) {
Person.call(this, name); // 继承父类属性
this.grade = grade;
}
// 原型链继承
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
6. 扩展运算符(...)→ apply / 循环拼接
数组扩展 / 对象扩展,转译为 ES5 的 apply 或手动遍历:
// ES6(数组扩展)
const arr1 = [1, 2];
const arr2 = [...arr1, 3, 4];
// ES6(函数参数扩展)
function sum(...args) { return args.reduce((a, b) => a + b); }
// 转译后 ES5
var arr1 = [1, 2];
var arr2 = [].concat(arr1, [3, 4]); // 数组扩展 → concat
function sum() {
var args = Array.prototype.slice.call(arguments); // 参数扩展 → slice 转数组
return args.reduce(function (a, b) {
return a + b;
});
}
二、API 垫片(需引入 polyfill)
ES6 新增的全局 API(如 Array.prototype.includes、Promise),ES5 无对应实现,需通过「垫片(polyfill)」补充(常用 core-js):
// ES6
const arr = [1, 2, 3];
console.log(arr.includes(2)); // true
new Promise((resolve) => resolve(1));
// 转译后 ES5(需引入 core-js 垫片)
var arr = [1, 2, 3];
// 垫片补充 includes 方法
console.log(Array.prototype.includes.call(arr, 2));
// 垫片实现 Promise 构造函数
new Promise(function (resolve) {
resolve(1);
});
三、核心总结
ES6 转 ES5 的核心逻辑:
- 「语法糖」(箭头函数、class、解构等)→ 转译为 ES5 原生语法(普通函数、原型链、手动取值);
- 「新增 API」(includes、Promise 等)→ 需补充 polyfill 才能兼容;
- 工具依赖:实际开发中通过 Babel + core-js 自动完成转译和垫片补充,无需手动编写 ES5 代码。
Maps 和 Objects 的区别
1、Map 中的键值是有序的(FIFO 原则),而添加到对象中的键则不是。
2、Map 的键值对个数可以从 size 属性获取,而 Object 的键值对个数只能手动计算。\
Map 与 Array的转换
Map 构造函数可以将一个 二维 键值对数组转换成一个 Map 对象
使用 Array.from 函数可以将一个 Map 对象转换成一个二维键值对数组
var kvArray = [["key1", "value1"], ["key2", "value2"]];
// Map 构造函数可以将一个 二维 键值对数组转换成一个 Map 对象
var myMap = new Map(kvArray);
// 使用 Array.from 函数可以将一个 Map 对象转换成一个二维键值对数组
var outArray = Array.from(myMap);
Set 对象
Set 中的特殊值
Set 对象存储的值总是唯一的,所以需要判断两个值是否恒等。有几个特殊值需要特殊对待:
+0 与 -0 在存储判断唯一性的时候是恒等的,所以重复;
undefined 与 undefined 是恒等的,所以重复;
NaN 与 NaN 是不恒等的,但是在 Set 中只能存一个,不重复。
mySet.add(1); // Set(1) {1}
mySet.add(5); // Set(2) {1, 5}
mySet.add(5); // Set(2) {1, 5} 这里体现了值的唯一性
mySet.add("some text");
// Set(3) {1, 5, "some text"} 这里体现了类型的多样性
var o = {a: 1, b: 2};
mySet.add(o);
mySet.add({a: 1, b: 2});
// Set(5) {1, 5, "some text", {…}, {…}}
// 这里体现了对象之间引用不同不恒等,即使值相同,Set 也能存储
类型转换
// Array 转 Set
var mySet = new Set([“value1”, “value2”, “value3”]);
// 用…操作符,将 Set 转 Array
var myArray = […mySet];
String
// String 转 Set
var mySet = new Set('hello'); // Set(4) {"h", "e", "l", "o"}
// 注:Set 中 toString 方法是不能将 Set 转换成 String
数组去重
var mySet = new Set([1, 2, 3, 4, 4]);
[...mySet]; // [1, 2, 3, 4]
用es5如何实现let和const?
实现 let
let 大家应该用的非常熟悉了,定义一个仅作用于该代码块的变量。如果去 babel 上面在线转换一下,大家可以看到结果是 var。在 es6 出现以前我们一般使用无限接近闭包的形式或者立即执行函数的形式来定义不会被污染的变量。
我们这也可以做类似的操作。
(function(){
var a = 1;
console.log(a)
})();
console.log(a)
效果不错,这大概也是使用 es6 的方便之处吧。
实现 const
那么 const 该怎么实现呢?const 声明一个只读的常量。一旦声明,常量的值就不能改变。有什么方法是可以限制一个值不能发生改变的呢?。是的,需要用到 Object.defineProperty。es5的Object.freeze();其中有一个属性是这样的:
writable:当前对象元素的值是否可修改。
全局作用域中,用const和Iet声明的变量不在window上,那到底在哪里?如何去获取?
const和let声明的变量在全局作用域中是块级作用域的,不会直接成为window对象的属性。- 可以在声明它们的作用域内直接访问这些变量,但不能通过
window对象来访问。 - 使用
globalThis可以访问全局对象,但const和let变量不会显示在其中。
为什么 const 和 let 不在 window 上?
-
设计考虑:
const和let的设计目标是提供更严格的作用域控制和避免全局变量污染。为了减少意外的全局变量污染,它们不自动添加到window对象上。
-
全局对象的隔离:
- 通过不将
const和let声明的变量添加到window对象,JavaScript 语言保证了全局作用域的变量不会被无意中修改。这有助于避免由于全局变量污染造成的潜在冲突和错误。
- 通过不将
const obj 的属性如何不可变
const定义的基本数据类型的变量确实不能修改。
因为对象是引用类型的,保存的仅是对象的指针,这就意味着,const仅保证指针不发生改变,修改对象的属性不会改变对象的指针,所以是被允许的。也就是说const定义的引用类型只要指针不发生改变,其他的不论如何改变都是允许的。我们试着修改一下指针,让P指向一个新对象,结果如下图:
Set 和 Map ⭐️⭐️⭐️
ES6 引入的 Set 和 Map 是两种新的集合类型,旨在解决传统对象(Object)和数组(Array)在数据存储、查找、去重等场景中的局限性,下面从特性、API、使用场景、与传统数据结构的对比等方面详细讲解:
一、Set:无重复值的有序集合
Set 是一种存储唯一值的有序集合(插入顺序即遍历顺序),支持任意类型的值(原始类型 + 引用类型)作为元素。
1. 核心特性
- 唯一性:元素不可重复,添加重复值会自动忽略;
- 有序性:按插入顺序存储,遍历顺序与插入顺序一致;
- 无索引:无法通过下标访问元素,需通过遍历或
has方法查找; - 支持多种类型:原始类型(字符串、数字、布尔、
null、undefined、Symbol)和引用类型(对象、数组)均可作为元素(引用类型的唯一性基于引用地址)。
2. 常用 API
| 方法 / 属性 | 作用 |
|---|---|
new Set([iterable]) | 构造函数,接收可迭代对象(如数组)初始化 Set |
size | 返回元素数量(类似数组的 length) |
add(value) | 添加元素,返回 Set 本身(支持链式调用) |
delete(value) | 删除指定元素,返回布尔值(是否删除成功) |
has(value) | 判断元素是否存在,返回布尔值 |
clear() | 清空所有元素 |
keys()/values() | 返回迭代器(Set 中键和值相同,keys() 等同于 values()) |
entries() | 返回迭代器,每个元素为 [value, value] 数组 |
forEach(callback) | 遍历元素,回调参数为 (value, key, set)(key 与 value 相同) |
示例:
const s = new Set([1, 2, 2, 'a']);
s.size; // 3(自动去重)
s.add(3).add('b'); // 链式添加
s.has(2); // true
s.delete('a'); // true
s.forEach(v => console.log(v)); // 1, 2, 3, b
[...s]; // [1, 2, 3, 'b'](转为数组)
3. 典型使用场景
-
数组去重:
[...new Set(arr)](最简单高效的去重方式); -
存储唯一值集合:如标签列表、用户 ID 集合、不重复的筛选条件;
-
快速判断元素存在:比数组
includes效率更高(Set.has是 O (1),Array.includes是 O (n)); -
交集 / 并集 / 差集:
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
// 并集
new Set([...a, ...b]); // {1,2,3,4}
// 交集
new Set([...a].filter(x => b.has(x))); // {2,3}
// 差集
new Set([...a].filter(x => !b.has(x))); // {1}
二、Map:键值对的有序集合
Map 是一种键值对集合(类似对象),但键的类型不受限,且保持插入顺序,解决了传统对象的诸多缺陷。
1. 核心特性
- 键的多样性:键可以是任意类型(原始类型 + 引用类型),如对象、数组、Symbol 均可作为键;
- 有序性:按插入顺序存储,遍历顺序与插入顺序一致;
- 键的唯一性:重复键会覆盖原有值(基于严格相等
===判断); - 性能优化:针对频繁增删键值对的场景优化,比对象更高效;
- 可迭代性:直接支持
for...of遍历,无需手动获取键数组。
2. 常用 API
| 方法 / 属性 | 作用 |
|---|---|
new Map([iterable]) | 构造函数,接收二维数组(如 [[key1, value1], [key2, value2]])初始化 |
size | 返回键值对数量 |
set(key, value) | 添加键值对,返回 Map 本身(支持链式调用) |
get(key) | 获取指定键的值,不存在则返回 undefined |
delete(key) | 删除指定键值对,返回布尔值(是否删除成功) |
has(key) | 判断键是否存在,返回布尔值 |
clear() | 清空所有键值对 |
keys() | 返回键的迭代器 |
values() | 返回值的迭代器 |
entries() | 返回迭代器,每个元素为 [key, value] 数组 |
forEach(callback) | 遍历键值对,回调参数为 (value, key, map) |
示例:
const m = new Map([['name', 'Alice'], [1, 'number']]);
m.set({}, 'objKey').set(Symbol('id'), 123); // 键为对象、Symbol
m.size; // 4
m.get(1); // 'number'
m.has({}); // false(对象是新引用,与之前的键不相等)
m.forEach((v, k) => console.log(k, v)); // 遍历所有键值对
3. 典型使用场景
- 需要非字符串键的场景:如用对象 / 数组作为键存储关联数据(普通对象无法实现);
- 频繁增删键值对:如动态缓存、临时状态存储(比对象更高效);
- 有序键值对遍历:如按插入顺序展示配置项、日志记录;
- 替代对象存储复杂键:避免对象键的自动字符串化问题(如数字
1和字符串'1'会被视为同一键)。
三、Set/Map 与传统数据结构的对比
1. Set vs 数组
| 特性 | Set | 数组(Array) |
|---|---|---|
| 元素唯一性 | 自动去重 | 允许重复 |
| 查找效率 | O(1)(has 方法) | O(n)(includes) |
| 插入 / 删除 | O(1) | 尾部 O (1),头部 / 中间 O (n) |
| 遍历方式 | 迭代器、forEach | 索引、forEach、for...of |
2. Map vs 对象(Object)
| 特性 | Map | 对象(Object) |
|---|---|---|
| 键类型 | 任意类型 | 仅字符串 / Symbol |
| 有序性 | 插入顺序 | ES6 后部分浏览器保留插入顺序,但规范未强制 |
| 大小获取 | size 属性 | 需手动计算(Object.keys(obj).length) |
| 迭代性 | 直接可迭代 | 需手动获取键数组后迭代 |
| 性能 | 频繁增删更高效 | 适合静态数据存储 |
| 原型干扰 | 无(无默认属性) | 可能受原型链干扰(如 obj.hasOwnProperty) |
四、WeakSet 和 WeakMap(弱引用版本)
ES6 还提供了 WeakSet 和 WeakMap,与普通版本的核心区别是弱引用:
- WeakSet:仅存储对象,且对对象是弱引用(外部无引用时,对象会被 GC 回收,WeakSet 自动移除该元素),不支持遍历和
size属性; - WeakMap:键仅能是对象,且对键是弱引用(外部无引用时,键对象会被 GC 回收,WeakMap 自动移除该键值对),不支持遍历和
size属性。
使用场景:临时缓存对象数据、关联对象元数据(避免内存泄漏)。
五、总结
- Set:专注于唯一值存储,适合去重、快速查找、集合运算;
- Map:专注于灵活键值对存储,适合非字符串键、有序遍历、频繁增删的场景;
- 若需弱引用避免内存泄漏,选择
WeakSet/WeakMap; - 传统数组 / 对象仍适用于简单场景,但 Set/Map 提供了更高效、更灵活的替代方案。
掌握 Set 和 Map 能显著提升前端数据处理的效率和代码健壮性,尤其在复杂应用中(如状态管理、缓存系统、数据筛选)优势明显。
WeakMap 和 Map
1. WeakMap 和 Map 的区别?为什么 WeakMap 没有 size 属性?
-
区别:
维度 Map WeakMap 键类型 任意类型(包括基本类型) 仅对象(非基本类型) 引用类型 强引用(键不会被垃圾回收) 弱引用(键无其他引用时会被回收) 遍历 / 属性 支持遍历(如 keys())、有size属性不支持遍历、无 size属性 -
无 size 属性的原因:WeakMap 的键是弱引用,键的存在性会随垃圾回收动态变化,无法稳定统计数量,因此不提供
size属性。
2. 为什么 WeakMap 的键不能是基本类型?
WeakMap 依赖 “对象的垃圾回收机制” 实现弱引用,而基本类型(如字符串、数字)是按值存储,不存在 “引用” 的概念,无法被垃圾回收机制追踪,因此键必须是对象类型。
3. 如何使用 WeakMap 封装私有属性?
利用 WeakMap 的弱引用特性,将对象作为键,私有属性作为值,外部无法访问 WeakMap 实例,从而实现私有性:
const privateData = new WeakMap();
class MyClass {
constructor(name) {
// 将私有属性存入 WeakMap,键为当前实例
privateData.set(this, { name });
}
getName() {
// 仅类内部能访问 WeakMap 获取私有属性
return privateData.get(this).name;
}
}
4. WeakMap 如何避免内存泄漏?
WeakMap 对键的引用是 “弱引用”—— 当键对象没有其他强引用时,会被垃圾回收机制自动回收,对应的键值对也会从 WeakMap 中移除,不会因长期持有键的引用而导致内存泄漏。
5. 什么是弱引用?JavaScript 中有哪些弱引用结构?
-
弱引用:对对象的引用不影响其垃圾回收(若对象仅被弱引用持有,会被回收),与 “强引用”(持有引用时对象不会被回收)相对。
-
弱引用结构:
WeakMap:键是弱引用的映射;WeakSet:值是弱引用的集合;FinalizationRegistry(ES2021):监听对象被垃圾回收的时机。
如何理解ES6模块化方案的缓存机制?⭐️⭐️
ES6 引入了模块化方案,即通过 import 和 export 语法来实现模块的导入和导出。ES6 模块化具有一个 缓存机制,它会在模块第一次被加载时缓存该模块,并且在后续的导入中返回该缓存结果,而不是重新执行模块代码。这个特性对于性能优化、避免重复执行和确保模块的一致性具有重要意义。
1. ES6 模块的加载机制
当一个 ES6 模块首次被导入时,浏览器或运行环境会加载该模块并执行其代码。模块的加载和执行遵循以下步骤:
- 第一次加载:当模块第一次被导入时,模块的代码会被执行一次,导出的内容会被返回。
- 缓存模块:一旦模块执行完毕,模块的结果(即导出的内容)会被缓存。这个缓存是以模块的文件路径为键存储的,因此每个模块文件只会被执行一次。
- 后续导入:之后再次导入相同的模块时,模块不会被重新执行,而是直接返回缓存的结果。
2. 缓存机制的实现方式
ES6 模块的缓存是基于模块的 标识符(文件路径) 进行管理的。具体来说:
- 模块对象:每个模块的导出内容都会被保存在一个
模块对象中。这个对象包含了所有导出的成员,并且会在模块第一次加载时进行计算。 - 缓存存储:当模块第一次加载时,它的导出结果会被存储在一个全局的缓存对象中(通常由 JavaScript 引擎负责管理)。在后续的导入中,模块的执行会被跳过,直接从缓存中获取结果。
3. 缓存的优势和特性
- 性能优化:由于 ES6 模块的缓存机制,模块的代码只会执行一次,这大大减少了模块重复加载和执行的性能开销。尤其是在大型应用中,多个模块间可能相互依赖,如果每次都重新执行模块的代码,效率会极低。缓存机制保证了模块的加载是高效的。
- 一致性:缓存机制确保了在整个应用中导入相同模块的多次引用是相同的,模块内部的状态也会保持一致。例如,模块内部的变量会在整个生命周期中保持不变。
4. 模块缓存的影响
-
共享状态:由于缓存是基于模块的路径和标识符的,模块的导出值会在整个应用的生命周期中保持一致。这意味着模块中的状态(例如导出的对象或函数)会被共享,其他导入该模块的地方将能够访问到同一份状态。
例如,假设一个模块导出了一个对象,并且该对象的属性在模块中被修改:
// counter.js export const counter = { value: 0 }; export function increment() { counter.value += 1; }当模块被导入并修改
counter.value时,其他地方导入counter.js时,都会看到同样修改后的值:import { counter, increment } from './counter.js'; increment(); // counter.value === 1 import { counter as counter2 } from './counter.js'; console.log(counter2.value); // 输出 1 -
模块的静态性:模块一旦被导入并缓存,它的导出内容不会再变化。因此,任何导入该模块的地方都会看到相同的导出结果。如果想要动态更新模块的导出内容,就需要依赖其他方式(如重新加载模块或使用不同的状态管理策略)。
5. 与 CommonJS 和 AMD 的对比
- CommonJS:CommonJS 的模块是同步加载的,模块是按需执行的,每次
require都会重新执行模块代码,并返回模块的导出内容。CommonJS 模块的缓存机制也有,但它与 ES6 的静态分析和执行机制不同,CommonJS 更依赖于运行时的加载和执行。 - AMD:AMD 的模块系统通常用于浏览器端,它支持异步加载,适用于动态加载模块。AMD 也会进行缓存,但其工作原理和 ES6 的静态模块导入有所不同,特别是在模块的依赖和异步加载方面。
6. 如何避免缓存的问题?
在某些情况下,可能需要避免 ES6 模块缓存机制带来的问题,尤其是当你需要模块具有不同状态时。可以通过以下几种方式来处理:
- 重新加载模块:如果需要重新加载一个模块,可以通过一些手段来强制浏览器或环境重新加载模块(如在 Webpack 中使用 HMR,或者在 Node.js 中使用
delete require.cache)。 - 动态模块加载:对于需要动态加载且不希望缓存的场景,可以使用
import()动态导入模块,这样每次导入都会重新执行模块的代码。
动态导入与静态导入的主要区别是什么?
- 静态导入:编译时确定、全量加载,适合必用的核心依赖,语法简洁但不灵活;
- 动态导入:运行时触发、按需加载,适合非核心功能 / 条件加载,灵活但需要处理 Promise(配合 async/await 更简洁)。
什么是循环依赖,如何避免在ES6模块中出现循环依赖?
一、什么是循环依赖?
两个或多个模块互相引用,形成 “闭环”—— 比如 A 模块导入 B 模块,B 模块又导入 A 模块,或 A→B→C→A 的间接闭环,导致模块加载时无法正常初始化(可能出现变量未定义、功能异常等问题)。
极简示例:
// a.js
import { b } from './b.js';
export const a = 'a';
console.log(b); // 可能输出 undefined(加载顺序问题)
// b.js
import { a } from './a.js';
export const b = 'b';
console.log(a); // 可能输出 undefined
二、ES6 模块中避免循环依赖的核心方法(简单实用)
核心原则:减少模块间的直接耦合,打破 “互相引用” 的闭环,重点从 “设计” 和 “用法” 两方面优化:
1. 核心:提取公共依赖到独立模块(最有效)
把循环依赖的模块中 “共有的变量 / 函数” 抽离到新的独立模块,让原模块都去导入这个公共模块,而非互相导入。
-
示例优化:
// common.js(公共模块,无依赖) export const commonData = '公共数据'; export const commonFn = () => {}; // a.js(只导入公共模块,不导入 b.js) import { commonData } from './common.js'; export const a = `a + ${commonData}`; // b.js(只导入公共模块,不导入 a.js) import { commonData } from './common.js'; export const b = `b + ${commonData}`;
2. 延迟导入:用动态导入替代静态导入
若必须互相引用,把其中一个模块的导入改为 “运行时动态导入”(而非编译时静态导入),避免加载初期形成闭环。
-
示例优化:
// a.js(静态导入改为动态导入) export const a = 'a'; // 不在顶部静态导入 b.js,而是在需要时动态导入 export const useB = async () => { const { b } = await import('./b.js'); console.log(b); // 此时 b 已初始化完成 }; // b.js(静态导入 a.js,无循环) import { a } from './a.js'; export const b = 'b'; console.log(a); // 正常输出 'a'
3. 优化模块职责:遵循 “单一职责”
避免模块功能过于复杂 —— 若 A 模块既负责 “数据定义” 又负责 “调用 B 功能”,B 模块反之,可拆分模块:让一个模块只存数据(无依赖),另一个模块只提供功能(导入数据模块)。
- 错误示例(职责混乱):A 模块存数据 + 调用 B 功能,B 模块存数据 + 调用 A 功能;
- 正确示例:拆分出 “数据模块”(无依赖),A、B 都只导入数据模块,不互相导入。
4. 避免 “顶部导入 + 立即执行依赖代码”
ES6 静态导入是 “编译时加载”,若模块顶部导入后立即执行依赖对方的代码(如 console.log(b)),此时对方模块可能还未初始化完成,导致异常。
- 解决:把依赖对方的代码延迟到 “使用时执行”(如函数内部),而非模块顶部。
总结
- 循环依赖本质是 “模块耦合过高 + 职责混乱”;
- 最优解:提取公共模块 + 遵循单一职责(从设计上避免闭环);
- 应急解:动态导入(延迟加载,打破加载时闭环)。
什么是尾调用
1. 什么是尾调用?尾调用优化有什么好处?
-
尾调用:函数的最后一步操作是调用另一个函数(即调用语句是函数的 “尾部”,无后续操作)。例如:
'use strict'; function factorial(n, acc = 1) { if (n <= 1) return acc; return factorial(n - 1, acc * n); } -
优化好处:避免创建新的调用栈帧,复用当前栈帧,减少内存占用,防止栈溢出(尤其适用于递归场景)。
2. 尾调用优化需要满足哪些条件?
- 函数的最后一步是调用另一个函数(不能有额外操作,如
return g() + 1不算); - 被调用的函数不能是当前函数的内部函数(即避免闭包,否则无法释放当前栈帧);
- 调用函数后没有额外的引用(如不能将调用结果赋值给变量后再返回)。
3. 为什么尾递归比普通递归更安全?
- 普通递归:每次调用都会创建新的栈帧,递归深度过大时会触发栈溢出(如
RangeError: Maximum call stack size exceeded); - 尾递归:符合尾调用优化条件,复用当前栈帧,栈深度始终保持为 1,不会因递归深度过大导致栈溢出,因此更安全。
4. JS 引擎是否支持尾调用优化?有哪些局限?
-
支持情况:仅部分引擎支持(如 Safari),Chrome、Node.js(V8 引擎)已移除对尾调用优化的支持(因实际场景中符合条件的尾调用较少,优化收益有限)。
-
局限:
- 依赖引擎实现,兼容性差;
- 严格要求 “尾调用” 的语法条件,实际代码中难以满足;
- V8 等主流引擎因性能 / 复杂度权衡,未提供支持。
5. 如何手动优化递归,避免栈溢出?
-
尾递归改写:将递归逻辑调整为尾调用形式(需配合引擎支持);
-
循环替代:将递归逻辑改写为迭代(for/while 循环),完全避免栈帧累积;
-
trampoline 函数:用一个包装函数 “拆分” 递归调用,每次调用后返回下一个函数,手动控制执行流程,避免栈溢出。示例:
function trampoline(fn) { let result = fn; while (typeof result === 'function') { result = result(); } return result; }