Javascript相关面试题
有哪些基本数据类型?
| 数据类型 | 描述 |
|---|---|
| Number | 数字 |
| String | 字符串 |
| Boolean | 布尔值 |
| Object | 对象 |
| Null | 空值 |
| Undefined | 未定义 |
| Symbol | 符号 |
| BigInt | 大整数 |
let、var、const区别
| 区别 | let | var | const |
|---|---|---|---|
| 可否被修改 | ✅ | ✅ | ❌ |
| 作用域 | 块级作用域 | 函数作用域 | 块级作用域 |
| 声明提升 | ❌(会被提升,但会进入暂时性死区) | ✅ | ❌ (会被提升,但会进入暂时性死区) |
| 重复声明 | ❌ | ✅ | ❌ |
为什么使用const修饰的对象还能被修改?
因为const修饰的对象,只是对象引用不能被修改,对象内部属性是可以被修改的。
为什么typeof null返回object?
js对变量的类型检测采用二进制标签法。 每个值在底层存储时,会用一个 3 位的二进制标签 标识类型:
- 000 → 表示「对象(Object)」;
- 1 → 表示「数字(Number)」;
- 100 → 表示「字符串(String)」;
- 110 → 表示「布尔(Boolean)」;
- 001 → 表示「函数(Function)」;
null 被设计为「全零二进制(000000...)」,而类型检测逻辑仅读取前 3 位(000),因此误判为「对象
为什么0.1+0.2!==0.3?
在计算时会将十进制转为二进制来进行计算 对于浮点数,其二进制是无限的,计算时会进行截取,所以相加后会丢失精度。
const num = 5
console.log(num.toString(2)); // 101
console.log(0.1.toString(2)); // 0.010000001000000010000001...
console.log(0.2.toString(2)); // 0.001100110011001100110011...解决
- toFixed()方法
console.log((0.1 + 0.2).toFixed(1));- 先转为整数再转为小数
function floatAddUp(a, b) {
const multiple = Math.pow(10, 100);
return (a * multiple + b * multiple) / multiple;
}
console.log(floatAddUp(0.1, 0.2));数组的方法
会修改原数组的方法(Mutating Methods)
| 方法 | 作用 | 使用示例 | 返回值 |
|---|---|---|---|
push() | 向数组末尾添加元素,返回新长度 | arr.push(4) | 新数组长度 |
pop() | 删除最后一个元素并返回 | arr.pop() | 被删除的元素 |
shift() | 删除第一个元素并返回 | arr.shift() | 被删除的元素 |
unshift() | 向数组开头添加元素,返回新长度 | arr.unshift(0) | 新数组长度 |
splice() | 添加/删除元素,返回被删除元素 | arr.splice(1, 2, 'a') | 被删除元素组成的数组 |
reverse() | 反转数组元素顺序 | arr.reverse() | 反转后的原数组 |
sort() | 对数组元素排序 | arr.sort() | 排序后的原数组 |
fill() | 用固定值填充数组 | arr.fill(0) | 填充后的原数组 |
copyWithin() | 复制数组元素到同一数组指定位置 | arr.copyWithin(0, 3, 4) | 修改后的原数组 |
不修改原数组的方法(Non-mutating Methods)
| 方法 | 作用 | 使用示例 | 返回值 |
|---|---|---|---|
concat() | 连接两个或多个数组 | arr1.concat(arr2) | 新数组 |
slice() | 提取数组的一部分 | arr.slice(1, 3) | 新数组 |
forEach() | 对每个元素执行函数 | arr.forEach(fn) | undefined |
map() | 创建新数组,每个元素执行函数 | arr.map(x => x*2) | 新数组 |
filter() | 创建符合条件元素的新数组 | arr.filter(x => x > 0) | 新数组 |
reduce() | 从左到右执行reducer函数 | arr.reduce((a,b) => a+b) | 累积值 |
reduceRight() | 从右到左执行reducer函数 | arr.reduceRight((a,b) => a+b) | 累积值 |
find() | 返回第一个符合条件的元素 | arr.find(x => x > 0) | 找到的元素或undefined |
findIndex() | 返回第一个符合条件元素的索引 | arr.findIndex(x => x > 0) | 索引或-1 |
some() | 检查是否有元素符合条件 | arr.some(x => x > 0) | 布尔值 |
every() | 检查是否所有元素都符合条件 | arr.every(x => x > 0) | 布尔值 |
includes() | 判断是否包含某个元素 | arr.includes(1) | 布尔值 |
indexOf() | 返回元素索引(从前往后) | arr.indexOf('a') | 索引或-1 |
lastIndexOf() | 返回元素索引(从后往前) | arr.lastIndexOf('a') | 索引或-1 |
toString() | 转换为字符串 | arr.toString() | 字符串 |
join() | 连接数组元素为字符串 | arr.join('-') | 字符串 |
isArray() | 判断是否为数组 | Array.isArray(arr) | 布尔值 |
Array.from() | 从类数组对象创建数组 | Array.from(str) | 新数组 |
Array.of() | 创建数组实例 | Array.of(1, 2, 3) | 新数组 |
使用建议
- 需要修改原数组时:使用
push、pop、shift、unshift、splice等 - 需要保持原数组不变时:使用
map、filter、slice等 - 链式调用:不修改原数组的方法可以链式调用
- 性能考虑:直接修改原数组通常性能更好
浅拷贝和深拷贝
浅拷贝:只复制 “引用类型的地址指针”,不复制深层数据;新对象与原对象共享同一层堆数据,修改深层数据会相互影响
const obj = {a: 1}, obj2 = {}
obj2 = obj
Object.assign(obj, obj2)
obj2 = {
...obj
}深拷贝: 递归复制引用类型的所有层级数据,新对象与原对象完全独立,堆中存储两份互不影响的数据
const obj3 = JSON.parse(JSON.stringify(obj1));for...in和for...of区别
| 对比维度 | for...in | for...of |
|---|---|---|
| 遍历目标 | 遍历对象的可枚举属性名(含原型链属性) | 遍历可迭代对象的值(数组、字符串、Map、Set 等) |
| 适用类型 | 任意对象(Object)、数组(不推荐) | 可迭代对象(Array、String、Map、Set、Generator 等) |
| 遍历数组 | 遍历数组索引(字符串类型,如 "0"、"1") | 遍历数组元素值(如 1、"a") |
| 原型链影响 | 会遍历原型链上的可枚举属性(需手动过滤) | 仅遍历自身可迭代值,不涉及原型链 |
| 中断遍历 | 支持 break/continue/return | 支持 break/continue/return |
| 异步遍历 | 无原生支持(需手动处理) | 可配合 for await...of 遍历异步可迭代对象 |
| 是否可修改原数据 | 可通过属性名修改(如 arr[index] = 新值) | 可直接拿到值,但修改值本身不影响原数据(需通过索引) |
使用xhr进行数据请求
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", url, true);
// 设置状态监听函数,可监听进度、错误、完成
xhr.onreadystatechange = function (event) {
if (event.readyState !== 4) return;
// 当请求成功时
if (event.status === 200) {
handle(event.response);
} else {
console.error(event.statusText);
}
};
// 设置请求失败时的监听函数
xhr.onerror = function () {
console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);箭头函数和普通函数区别
| 对比维度 | 箭头函数 | 普通函数 |
|---|---|---|
| this指向 | 继承外层作用域的this值 | 取决于函数调用方式(谁调用指向谁) |
| arguments对象 | 无arguments对象 | 有arguments对象 |
| 构造函数 | 不能用new调用 | 可以用new调用作为构造函数 |
| prototype属性 | 无prototype属性 | 有prototype属性 |
| call/apply/bind | 无法改变this指向 | 可通过这些方法改变this指向 |
| yield关键字 | 不能用于generator函数 | 可用于generator函数 |
关键差异说明
this绑定:
- 箭头函数:词法绑定,在定义时确定this值
- 普通函数:动态绑定,在调用时确定this值
使用场景:
- 箭头函数:适合回调函数、简化代码
- 普通函数:适合需要动态this或作为构造函数的场景
set和map的区别
| 对比维度 | Set | Map |
|---|---|---|
| 存储内容 | 只存储值(value) | 存储键值对(key-value) |
| 数据结构 | 值唯一,无重复 | 键唯一,值可重复 |
| 数据类型 | 任何类型的值 | 任何类型的键和值 |
使用场景建议
使用 Set 的场景:
- 需要去重的数组处理
- 成员资格检查(是否存在某值)
- 集合运算(并集、交集、差集)
使用 Map 的场景:
- 需要键值对映射关系
- 键的类型不仅限于字符串(可用对象作为键)
- 频繁的增删查改操作
- 需要保持插入顺序的键值对集合
map和weakMap的区别
| 对比维度 | Map | WeakMap |
|---|---|---|
| 键的类型 | 任何类型的键(包括对象、基本类型) | 只能是对象作为键 |
| 内存管理 | 强引用,即使键对象不再使用也不会被垃圾回收 | 弱引用,键对象可被垃圾回收 |
| 迭代能力 | 支持 forEach、entries、keys、values 等迭代方法 | 不可迭代,无法获取所有键值对 |
| 大小获取 | map.size 获取元素数量 | 无 size 属性 |
| 清空操作 | map.clear() 清空所有元素 | 无 clear() 方法 |
| 键枚举 | 可以枚举所有键 | 无法枚举键 |
使用场景建议
使用 Map 的场景:
- 需要键值对存储且键可以是任意类型
- 需要遍历所有键值对
- 需要知道集合大小
- 需要清空整个集合
- 一般的数据缓存和映射场景
使用 WeakMap 的场景:
- 私有数据存储:将对象作为键存储私有数据,对象销毁时数据自动清理
- DOM元素关联数据:避免DOM元素内存泄漏
- 缓存机制:当缓存对象不再需要时自动释放内存
- 避免内存泄漏:不需要手动清理的场景
示例代码
// Map 使用示例
const map = new Map();
map.set('stringKey', 'value1');
map.set({}, 'value2');
map.set(function () {
}, 'value3');
// WeakMap 使用示例
const weakMap = new WeakMap();
const obj = {};
weakMap.set(obj, 'private data');
// 当 obj 被垃圾回收时,weakMap 中的条目也会自动删除什么是原型
- 原型是JavaScript中实现继承的核心机制,每个函数都有一个
prototype属性,它指向一个对象 - 通过该函数构造的实例对象,其内部
[[Prototype]]槽(通常通过__proto__属性访问)会指向该函数的prototype对象 - 所有对象在创建时都会关联到另一个对象,这个关联对象就是原型,它提供了对象可以共享的属性和方法
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
console.log(`Hello, I'm ${this.name}`);
};
const person1 = new Person('Alice');
const person2 = new Person('Bob');
console.log(person1.__proto__ === Person.prototype); // true
console.log(person1.sayHello === person2.sayHello); // true原型链
- 每个对象实例都会拥有
__proto__属性(该属性为浏览器提供的非标准访问方式,标准推荐使用Object.getPrototypeOf()),它指向该实例对应的原型对象;而原型对象本身也是对象,同样拥有自己的原型对象,由此形成层层向上的链式结构,直至原型链的顶层终点 null 为止。
- 当访问对象的某个属性时,JavaScript 引擎会沿该对象的原型链依次查找:先检查对象自身是否存在该属性,若不存在则查找其
__proto__指向的原型对象,再逐层向上查找,直至Object.prototype;若Object.prototype中仍未找到该属性,会继续查找其原型(即null),最终确定属性不存在并返回undefined。
// 原型链示例
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true四种JS继承方式
一、原型链继承
核心原理
将子类的原型(Child.prototype)指向父类的实例,让子类实例能通过原型链访问父类的属性和方法。
修正代码
function Parent() {
this.name = "蔡徐坤";
this.hobby = ["唱", "跳", "rap"]; // 引用类型属性
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child() {
}
// 核心:子类原型指向父类实例
Child.prototype = new Parent();
// 修复constructor指向(否则Child实例的constructor会指向Parent)
Child.prototype.constructor = Child;
// 测试
const child1 = new Child();
const child2 = new Child();
child1.hobby.push("打篮球"); // 引用类型属性被共享
console.log(child1.hobby); // ["唱", "跳", "rap", "打篮球"]
console.log(child2.hobby); // ["唱", "跳", "rap", "打篮球"](问题核心)
child1.sayName(); // 蔡徐坤(能继承父类原型方法)优点
- 简单易实现,子类能继承父类原型上的方法。
- 实现了原型方法的复用(所有子类实例共享父类原型方法)。
缺点
- 引用类型属性共享:父类实例的引用类型属性(如数组、对象)会被所有子类实例共享,修改一个会影响所有。
- 无法向父类构造函数传参:创建子类实例时,不能自定义父类的属性值(比如想给不同子类实例设置不同
name)。 - 子类原型的
constructor指向错误:需手动修正为子类本身。
二、构造函数继承(借用构造函数)
核心原理
在子类构造函数中通过call/apply调用父类构造函数,让父类的属性绑定到子类实例上,实现属性的独立继承。
修正代码
function Parent(name) {
this.name = name;
this.hobby = ["唱", "跳", "rap"];
this.sayHobby = function () {
console.log(this.hobby);
};
}
// 父类原型上的方法(构造函数继承无法继承)
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name) {
// 核心:调用父类构造函数,绑定this为子类实例,支持传参
Parent.call(this, name);
}
// 测试
const child1 = new Child("蔡徐坤1号");
const child2 = new Child("蔡徐坤2号");
child1.hobby.push("打篮球");
console.log(child1.name); // 蔡徐坤1号(支持传参)
console.log(child2.name); // 蔡徐坤2号
console.log(child1.hobby); // ["唱", "跳", "rap", "打篮球"]
console.log(child2.hobby); // ["唱", "跳", "rap"](引用类型属性独立)
child1.sayHobby(); // 正常执行(父类构造函数内的方法)
child1.sayName(); // 报错:child1.sayName is not a function(无法继承原型方法)优点
- 引用类型属性独立:每个子类实例都有自己的父类属性副本,修改互不影响。
- 支持向父类构造函数传参:创建子类实例时可自定义父类属性。
缺点
- 无法继承父类原型上的方法/属性:只能继承父类构造函数内定义的属性和方法。
- 方法复用性差:父类构造函数内的方法会在每个子类实例上重新创建,内存占用高(每个实例都有独立的方法副本)。
三、组合继承(原型链+构造函数)
核心原理
结合原型链继承(继承父类原型方法)和构造函数继承(继承父类实例属性),兼顾属性独立和方法复用。
修正代码
function Parent(name) {
this.name = name;
this.hobby = ["唱", "跳", "rap"];
}
Parent.prototype.sayName = function () {
console.log(`我是${this.name}`);
};
function Child(name) {
// 1. 构造函数继承:继承实例属性(第二次调用Parent)
Parent.call(this, name);
}
// 2. 原型链继承:继承原型方法(第一次调用Parent)
Child.prototype = new Parent();
// 修复constructor指向
Child.prototype.constructor = Child;
// 测试
const child1 = new Child("蔡徐坤1号");
const child2 = new Child("蔡徐坤2号");
child1.hobby.push("打篮球");
console.log(child1.name); // 蔡徐坤1号(支持传参)
console.log(child2.hobby); // ["唱", "跳", "rap"](属性独立)
child1.sayName(); // 我是蔡徐坤1号(继承原型方法)优点
- 兼顾属性独立和方法复用:既解决了原型链继承的引用类型共享问题,又解决了构造函数继承无法继承原型方法的问题。
- 支持向父类传参:保留了构造函数继承的传参优势。
缺点
- 父类构造函数被调用两次:
- 第一次:
Child.prototype = new Parent()(创建父类实例作为子类原型,多余的实例属性会被覆盖)。 - 第二次:
Parent.call(this)(子类实例创建时,重新调用父类构造函数,覆盖原型上的同名属性)。
- 第一次:
- 子类原型上会存在多余的父类实例属性(无实际作用,浪费内存)。
四、寄生组合继承(最佳方案)
核心原理
通过Object.create(Parent.prototype)创建一个空对象(原型指向父类原型),作为子类的原型,替代“子类原型 = 父类实例”,避免父类构造函数被重复调用;同时保留Parent.call(this)继承实例属性。
完整代码
function Parent(name) {
this.name = name;
this.hobby = ["唱", "跳", "rap"];
}
Parent.prototype.sayName = function () {
console.log(`我是${this.name}`);
};
function Child(name) {
// 继承实例属性(仅调用一次父类构造函数)
Parent.call(this, name);
}
// 核心:创建空对象,原型指向父类原型,避免父类构造函数调用
Child.prototype = Object.create(Parent.prototype);
// 修复constructor指向(否则指向Parent)
Child.prototype.constructor = Child;
// 子类可扩展自己的原型方法
Child.prototype.sayHobby = function () {
console.log(`我会${this.hobby.join(',')}`);
};
// 测试
const child = new Child("蔡徐坤");
child.hobby.push("打篮球");
child.sayName(); // 我是蔡徐坤
child.sayHobby(); // 我会唱,跳,rap,打篮球
console.log(child.constructor === Child); // true(constructor修复成功)优点
- 父类构造函数仅调用一次:解决了组合继承的重复调用问题,内存占用最优。
- 属性独立+方法复用:保留组合继承的所有优点。
- 原型链干净:子类原型上无多余的父类实例属性。
缺点
- 语法稍复杂:需要手动修复
constructor指向(属于小问题,可封装工具函数简化)。 - (ES5环境下)需兼容
Object.create(低版本浏览器可通过垫片实现)。
补充:ES6 Class继承(语法糖)
ES6的class+extends本质是寄生组合继承的语法糖,更简洁易读:
class Parent {
constructor(name) {
this.name = name;
this.hobby = ["唱", "跳", "rap"];
}
sayName() {
console.log(`我是${this.name}`);
}
}
class Child extends Parent {
constructor(name) {
super(name); // 等价于Parent.call(this, name)
}
sayHobby() {
console.log(`我会${this.hobby.join(',')}`);
}
}
// 测试
const child = new Child("蔡徐坤");
child.hobby.push("打篮球");
child.sayName(); // 我是蔡徐坤
child.sayHobby(); // 我会唱,跳,rap,打篮球优点
- 语法简洁、语义化强,符合面向对象编程习惯。
- 底层实现寄生组合继承,无需手动处理原型和
constructor。
缺点
- ES6语法需环境支持(可通过Babel转译)。
闭包及其使用场景
什么是闭包
一个函数内定义了另一个函数,这个内部函数能访问外部函数的变量,外函数的返回值是这个内部函数。
function outer() {
let count = 0;
return function inner() {
count++;
}
return inner;
}
let inner = outer();
inner();优点
- 创建私有变量:闭包可以创建私有变量,外部无法访问内部变量。
- 可以实现封装、缓存
缺点
浏览器的垃圾回收机制无法回收闭包中的变量,容易造成内存泄漏,只能手动清理。
使用场景
- 用于创建全局私有变量
- 封装类和模块
- 实现函数柯里化
call、apply、bind区别
call、apply 和 bind 都是 JavaScript 中用于改变函数运行时上下文(this 指向)的方法。
| 特征 | call | apply | bind |
|---|---|---|---|
| 参数传递方式 | 第一个参数为this,后续参数逐个传递 | 第一个参数为this,第二个参数为数组 | 第一个参数为this,后续参数逐个传递 |
| 绑定规则 | 可多次绑定 | 可多次绑定 | 硬绑定,无法再次改变this指向 |
| 执行时机 | 立即执行 | 立即执行 | 返回新函数,不立即执行 |
| 返回值 | 函数执行结果 | 函数执行结果 | 返回绑定了this的新函数 |
call
call 方法调用一个函数,其具有指定的 this 值和参数列表。
function greet(greeting, punctuation) {
console.log(`${greeting}, I'm ${this.name}${punctuation}`);
}
const person = {name: 'Alice'};
// 使用 call 改变 this 指向,参数逐个传递
greet.call(person, 'Hello', '!'); // "Hello, I'm Alice!"
// call 也可用于调用数组方法
const numbers = [1, 2, 3, 4, 5];
const max = Math.max.call(null, ...numbers); // 5apply
apply 方法调用一个函数,其具有指定的 this 值和参数数组。
function greet(greeting, punctuation) {
console.log(`${greeting}, I'm ${this.name}${punctuation}`);
}
const person = {name: 'Bob'};
const args = ['Hi', '?'];
// 使用 apply 改变 this 指向,参数以数组形式传递
greet.apply(person, args); // "Hi, I'm Bob?"
// apply 常用于处理数组
const numbers = [1, 2, 3, 4, 5];
const min = Math.min.apply(null, numbers); // 1bind
bind 方法创建一个新的函数,当被调用时,将其 this 关键字设置为提供的值,并在调用新函数时,可选择地提供一系列参数。
function greet(greeting, punctuation) {
console.log(`${greeting}, I'm ${this.name}${punctuation}`);
}
const person = {name: 'Charlie'};
// 使用 bind 创建新函数,绑定 this 和部分参数
const boundGreet = greet.bind(person, 'Hey');
boundGreet('!'); // "Hey, I'm Charlie!"
// 也可以只绑定 this,参数在调用时传递
const boundGreet2 = greet.bind(person);
boundGreet2('Hello', '.'); // "Hello, I'm Charlie."
// bind 的硬绑定特性:bind 创建的函数无法再次改变 this 指向
const person2 = {name: 'David'};
const boundGreet3 = boundGreet.bind(person2);
boundGreet3('Hi', '!'); // "Hi, I'm Charlie!" (仍然是 Charlie,不是 David)使用场景
call/apply 使用场景:
- 调用函数并指定 this 值
- 借用其他对象的方法
- 处理数组(如 Math.max.apply(null, array))
bind 使用场景:
- 创建绑定特定 this 值的函数
- 事件处理器中保持 this 指向
- 回调函数中保持上下文
关键差异总结
- 参数传递:
call逐个传递参数,apply以数组形式传递参数,bind返回新函数等待执行 - 执行时机:
call和apply立即执行,bind返回新函数 - this 绑定强度:
bind创建的函数具有硬绑定的 this,无法通过call或apply再次改变
ES6新特性
let/const:用于声明变量,
let声明的变量具有块级作用域,const声明的变量具有块级作用域且不可修改。箭头函数:语法糖,函数定义更简洁,
this指向外层作用域的this值。模板字符串:
- 使用反引号(`)包裹字符串
- 支持多行字符串和变量插值
javascriptconst name = 'World'; const greeting = `Hello ${name}!`;解构赋值:
- 从数组或对象中提取值并赋给变量
javascriptconst [a, b] = [1, 2]; const {name, age} = {name: 'John', age: 30};默认参数:
- 函数参数可以设置默认值
javascriptfunction greet(name = 'Guest') { return `Hello, ${name}!`; }扩展运算符:
...用于展开数组或对象
javascriptconst arr1 = [1, 2]; const arr2 = [...arr1, 3, 4];Promise:提供更好的异步编程解决方案
模块化:
import/export实现模块导入导出类(Class):基于原型的面向对象编程语法糖
Symbol:新增基本数据类型,用于创建唯一标识符
CommonJS 模块化规范详解
基本概念
CommonJS 是一种用于 Node.js 的模块化规范,它允许开发者将代码组织成独立的模块,通过 require() 引入模块,并使用 module.exports 或 exports 对外提供接口。
- exports 是 module.exports 的引用,最终导出的是 module.exports(比如 exports.xxx 等价于 module.exports.xxx,但 exports = {} 无效);
- 模块缓存:同一模块多次 require 只会执行一次,后续取缓存(删除缓存可通过 delete require.cache[模块路径]);
- 动态加载:可在 if 里用 require(),比如 if (env === 'dev') require('./dev.js'),ES6 模块不行。
核心特性
- 同步加载:模块加载是同步进行的,适用于服务器端编程
- 缓存机制:模块只会被加载一次,后续引用都会使用缓存
- 作用域隔离:每个模块都有独立的作用域,避免全局污染
使用方式
// math.js - 模块导出
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
// 方式1:exports 对象
exports.add = add;
exports.subtract = subtract;
// 方式2:module.exports 对象
module.exports = {
add,
subtract
};
// app.js - 模块引入
const math = require('./math');
console.log(math.add(2, 3)); // 5使用场景建议
- Node.js 开发:服务端编程首选
- npm 包开发:大多数 npm 包使用 CommonJS 规范
- 工具库开发:适用于需要同步加载的场景
ES6模块如何动态导入?
ES6 提供了 import() 函数(动态导入),它是运行时执行的,返回一个 Promise,可嵌套在函数、条件语句、循环等任意位置
async function loadUtils() {
if (Math.random() > 0.5) {
const {add} = await import('./utils.js');
console.log(add(1, 2));
} else {
const foo = await import('./foo.js');
console.log(foo.default);
}
}
if (process.env.NODE_ENV === 'dev') {
import('./dev-log.js').then(({log}) => {
log('开发环境日志');
});
}ES6模块和CommonJS对比
| 对比维度 | ES6模块 | CommonJS |
|---|---|---|
| 语法 | 模块导入和导出使用 import/export/export default 关键字 | 使用 require() 、 module.exports和exports |
| 模块加载 | 异步加载,可以做到按需加载 | 同步加载 |
异步编程
什么是Promise?
Promise是一种异步编程解决方案,它允许我们使用一种更简洁的方式处理异步操作,从而避免回调地狱。 它有三种状态:
- pending:初始状态,表示异步操作未完成。
- fulfilled:表示异步操作成功完成。
- rejected:表示异步操作失败。
Promise对象接收带两个参数的回调函数,第一个参数是resolve,第二个参数是reject。
- resolve:异步操作成功时调用,将Promise对象的状态改为fulfilled。
- reject:异步操作失败时调用,将Promise对象状态改为rejected。
缺点:
- 无法取消Promise,一旦状态改变,则无法改变。
- 如果不设置回调函数,Promise内部抛出的错误不会传递到外层,需要手动处理。
- 无法得知当前Promise状态,只能通过then方法获取结果。
const request = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
request.then(res => {
console.log(res)// success
})async/await 优化说明
async:用于定义异步函数,返回值会自动包装为Promise对象(无返回值则包装undefined)await:用于等待Promise执行完成,只能在async函数内部使用
优势对比
const request = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
// 使用 async/await
async function fetchDataWithAsync() {
try {
const response = await request();
console.log(response);
} catch (error) {
console.error(error);
}
}关键特性
- 错误处理:使用
try/catch统一处理同步和异步错误 - 代码可读性:使异步代码看起来像同步代码,更易理解
- 调试友好:可以使用常规的断点调试
使用建议
- 错误处理:始终使用
try/catch包装await调用 - 并发处理:对于无关的异步操作,使用
Promise.all()实现并发执行 - 避免滥用:不是所有异步操作都需要
await,根据业务需求选择合适的处理方式