一、如何避免 JS 阻塞浏览器渲染
1. 使用 async 和 defer 属性*
-
async属性:为<script>标签添加async属性,脚本会异步加载,下载完成后立即执行。这样脚本不会阻塞 HTML 解析和 CSS 渲染。但要注意,async脚本的执行顺序不确定,适用于那些与页面其他部分没有依赖关系的脚本,如第三方广告脚本、分析脚本等。 -
defer属性:给<script>标签设置defer属性,脚本会异步加载,在 HTML 解析完成后、DOMContentLoaded事件触发之前按照它们在 HTML 中出现的顺序依次执行。这能保证脚本不会阻塞 CSS 渲染,且适合那些需要在页面加载完成后执行,并且脚本之间有依赖关系的情况。
2. 将脚本放在页面底部
把 <script> 标签放在 <body> 标签的底部,这样浏览器会先解析和渲染 HTML 与 CSS,在页面基本结构和样式展示出来后再加载和执行 JavaScript 代码,从而避免脚本阻塞 CSS 渲染。
3. 优化脚本代码
- 减少内联脚本:内联脚本会在解析到它们时立即执行,可能会阻塞 CSS 渲染。尽量将 JavaScript 代码放在外部文件中,并使用
async或defer属性加载。 - 拆分大脚本:将大型的 JavaScript 文件拆分成多个小文件,分别使用
async或defer属性加载,这样可以减少单个脚本的执行时间,降低阻塞的可能性。 - 避免在关键渲染路径上执行复杂脚本:避免在页面加载初期执行复杂的计算、DOM 操作或网络请求等操作,尽量将这些操作推迟到页面渲染完成后进行。
二、如何跳出 foreach 循环
1. 抛出异常
可以通过抛出异常的方式强制中断 forEach 循环。在捕获到异常后,若异常是用于跳出循环的,就不做进一步处理。
try {
const numbers = [1, 2, 3, 4, 5];
numbers.forEach((number) => {
if (number === 3) {
throw new Error('跳出 forEach 循环');
}
console.log(number);
});
} catch (error) {
if (error.message === '跳出 forEach 循环') {
console.log('已成功跳出 forEach 循环');
} else {
// 若不是用于跳出循环的异常,则重新抛出
throw error;
}
}
2. 使用标志变量
可以定义一个标志变量,在满足跳出条件时改变标志变量的值,后续的迭代中通过检查标志变量来决定是否继续执行操作。
const numbers = [1, 2, 3, 4, 5];
let shouldBreak = false;
numbers.forEach((number) => {
if (shouldBreak) {
return;
}
if (number === 3) {
shouldBreak = true;
return;
}
console.log(number);
});
console.log('已成功跳出 forEach 循环');
三、try...catch 能否捕获异步代码
try...catch 语句本身不能直接捕获异步代码中的异常,但 async/await 是基于 Promise 的语法糖,它让异步代码看起来更像同步代码,因此可以使用 try...catch 来捕获 await 语句抛出的异常。
四、Object 和 Map 的区别
数据结构
Map是一种键值对的数据结构,键可以是任意类型,包括对象。Object的键只能是字符串或Symbol类型。
迭代方式
Map可直接迭代,通过for...of循环可以方便地获取键值对。Object需要使用Object.keys()、Object.values()或Object.entries()等方法先获取键、值或键值对数组,再进行迭代。
初始化和操作
Map通过构造函数new Map()初始化,使用set方法添加键值对,get方法获取值,has方法判断键是否存在,delete方法删除键值对。Object可以通过字面量{}或new Object()初始化,通过点语法或方括号语法添加、获取和删除属性。
内存占用
Map在存储大量键值对时,内存使用可能更高效,尤其是当键为对象时。Object由于其内部实现机制,可能会有一些额外的内存开销。
原生属性和方法
Map有size属性表示键值对数量,还有clear方法清空Map等。Object有prototype等属性,以及defineProperty等方法用于操作对象的属性特性。 一般来说,当需要使用非字符串类型的键,或者需要方便地进行迭代和动态操作键值对时,Map更合适;而当处理简单的键值对,键为字符串,且对对象的属性操作和原型链有需求时,Object更为常用。
性能对比
- 需要频繁进行插入、删除、查找和遍历操作的场景中,
Map通常具有更好的性能表现。 - 在一些简单的场景中,使用
Object可能更加方便和直观。
五、如何判断数组
Array.isArray():通过返回结果是否为true判断,ES5 引入,兼容性好,推荐使用。instanceof运算符:通过返回结果是否为Array判断,需注意代码在不同的窗口或 iframe 中运行时,由于每个窗口或 iframe 都有自己的全局对象和构造函数,可能会导致判断不准确。Object.prototype.toString.call():通过返回结果是否为'[object Array]'判断。通用性强,能准确判断各种类型的对象,包括不同窗口或 iframe 中的对象。constructor属性:通过返回结果是否为Array判断,需注意该属性可以被修改,所以使用时需要谨慎,否则可能导致判断结果不准确。
六、闭包
定义
闭包是指有权访问另一个函数作用域中变量的函数。简单来说,即使该函数已经执行完毕,其作用域内的变量也不会被销毁,而是会被闭包所引用,从而延长了这些变量的生命周期。
形成条件
-
函数嵌套:存在一个外部函数和一个内部函数,内部函数定义在外部函数的内部。
-
内部函数引用外部函数的变量:内部函数使用了外部函数作用域中的变量,这样内部函数就形成了一个闭包。
作用
-
读取函数内部的变量:由于闭包可以访问外部函数的变量,因此可以在函数外部读取函数内部的变量。
-
让变量的值始终保持在内存中:闭包会持有对外部函数变量的引用,使得这些变量不会随着外部函数的执行结束而被销毁,从而可以在不同的调用之间保持变量的值。
优缺点
优点
-
数据封装和隐藏:可以将变量封装在函数内部,只通过闭包提供的接口来访问和修改这些变量,从而实现数据的封装和隐藏,提高代码的安全性和可维护性。
-
实现函数私有变量和方法:通过闭包可以模拟类的私有变量和方法,避免全局变量的污染。
缺点
-
内存占用:由于闭包会持有对外部函数变量的引用,这些变量不会被垃圾回收机制回收,因此可能会导致内存占用过高,甚至引发内存泄漏。
-
性能问题:闭包的使用可能会增加代码的复杂度,影响代码的性能,特别是在频繁调用闭包的情况下。
示例
function outerFunction() {
let count = 0;
function innerFunction() {
count++;
console.log(count);
}
return innerFunction;
}
// 创建闭包
const closure = outerFunction();
// 调用闭包
closure(); // 输出: 1
closure(); // 输出: 2
在这个示例中,outerFunction 是外部函数,innerFunction 是内部函数。innerFunction 引用了 outerFunction 中的 count 变量,因此 innerFunction 形成了一个闭包。当调用 outerFunction 时,它返回了 innerFunction,并将其赋值给 closure。每次调用 closure 时,都会访问并修改 count 变量的值,而且 count 变量不会随着 outerFunction 的执行结束而被销毁。
实际应用场景
- 事件处理程序:在事件处理程序中,闭包可以用来保存事件处理所需的上下文信息。
function createButton() {
let clickCount = 0;
const button = document.createElement('button');
button.textContent = '点击我';
button.addEventListener('click', function () {
clickCount++;
console.log(`按钮被点击了 ${clickCount} 次`);
});
document.body.appendChild(button);
}
createButton();
- 函数柯里化:闭包可以用于实现函数柯里化,将一个多参数函数转换为一系列单参数函数。
function add(a, b) {
return a + b;
}
function curryAdd(a) {
return function (b) {
return add(a, b);
};
}
const addFive = curryAdd(5);
console.log(addFive(3)); // 输出: 8
七、原型和原型链
原型
- 定义:每个函数都有一个
prototype属性,这个属性是一个对象,被称为该函数的原型对象。当函数被用作构造函数来创建对象时,新创建的对象会从构造函数的原型对象中继承属性和方法。 - 作用:原型对象提供了一种共享属性和方法的机制,使得多个对象可以共享相同的属性和方法,而不必在每个对象中重复创建。例如,所有数组对象都共享
Array.prototype上的方法,如push、pop等。
原型链
- 定义:每个对象都有一个
__proto__属性(在现代浏览器中可以访问,在某些情况下也被称为原型链的链接),它指向该对象的原型对象。如果原型对象本身还有原型,那么就会形成一条链式结构,这就是原型链。 - 属性查找机制:当访问一个对象的属性时,JavaScript 引擎会首先在该对象本身查找,如果找不到,就会沿着原型链向上查找,直到找到该属性或者到达原型链的顶端(
null)。例如:
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
const person = new Person('Alice');
person.sayHello();
在这个例子中,person 对象没有 sayHello 方法,但它的原型对象 Person.prototype 有。当调用 person.sayHello() 时,JavaScript 会沿着原型链找到 Person.prototype 上的 sayHello 方法并执行。
八、new 的过程
在 JavaScript 中,new 操作符用于创建一个构造函数的实例。new 操作的整个过程涉及多个步骤,下面为你详细解析:
-
创建一个新对象:
new操作首先会在内存中创建一个空的对象,这个对象将作为构造函数的实例。可以把它想象成一个新的容器,用于存储实例的属性和方法。 -
设置原型链:新创建的对象的
__proto__属性(隐式原型)会被设置为构造函数的prototype属性(原型对象)。这一步建立了实例与构造函数原型之间的联系,使得实例可以继承原型对象上的属性和方法。例如:
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
const person = new Person('Alice');
// person.__proto__ === Person.prototype 为 true
- 执行构造函数:将新创建的对象作为
this的值,传入构造函数内部,并执行构造函数中的代码。在构造函数中,可以通过this关键字为新对象添加属性和方法。例如:
function Person(name, age) {
this.name = name;
this.age = age;
}
const person = new Person('Bob', 30);
console.log(person.name); // 输出: Bob
console.log(person.age); // 输出: 30
- 返回值判断:如果构造函数没有显式返回一个对象(即返回
undefined或原始值),那么new操作会返回步骤 1 中创建的新对象;如果构造函数显式返回了一个对象,那么new操作将返回这个显式返回的对象,而不是步骤 1 中创建的新对象。例如:
function Person(name) {
this.name = name;
// 显式返回一个对象
return { message: 'This is a different object' };
}
const person = new Person('Charlie');
console.log(person.message); // 输出: This is a different object
九、this 指向
全局作用域中
在全局作用域(即在任何函数外部)中,this 指向全局对象。在浏览器环境里,全局对象是 window 对象;在 Node.js 环境中,全局对象是 global 对象。
console.log(this === window); // 在浏览器环境中输出 true
函数内部
1. 普通函数调用
当函数作为普通函数调用时,this 指向全局对象(在非严格模式下);在严格模式下,this 为 undefined。
// 非严格模式
function showThis() {
console.log(this);
}
showThis(); // 在浏览器环境中输出 window 对象
// 严格模式
function showThisStrict() {
'use strict';
console.log(this);
}
showThisStrict(); // 输出 undefined
2. 方法调用
当函数作为对象的方法调用时,this 指向调用该方法的对象。
const person = {
name: 'John',
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
};
person.sayHello(); // 输出: Hello, my name is John
3. 构造函数调用
当函数使用 new 关键字作为构造函数调用时,this 指向新创建的对象。
function Person(name) {
this.name = name;
console.log(this);
}
const newPerson = new Person('Alice'); // 输出新创建的 Person 对象
4. 箭头函数
箭头函数没有自己的 this,它的 this 继承自外层函数(即定义该箭头函数的作用域)。
const person = {
name: 'Bob',
sayHello: function() {
const arrowFunction = () => {
console.log(this.name);
};
arrowFunction();
}
};
person.sayHello(); // 输出: Bob
事件处理函数中
在事件处理函数里,this 通常指向触发事件的 DOM 元素。
<!DOCTYPE html>
<html lang="en">
<body>
<button id="myButton">Click me</button>
<script>
const button = document.getElementById('myButton');
button.addEventListener('click', function() {
console.log(this); // 输出按钮元素
});
</script>
</body>
</html>
使用 call、apply 和 bind 方法
call、apply 和 bind 方法可以显式地指定函数内部 this 的指向。
call方法:第一个参数是要绑定的this值,后面的参数是传递给函数的参数。
function greet(message) {
console.log(`${message}, ${this.name}`);
}
const person = { name: 'Eve' };
greet.call(person, 'Hello'); // 输出: Hello, Eve
apply方法:第一个参数是要绑定的this值,第二个参数是一个数组,数组中的元素会作为参数传递给函数。
function greet(message) {
console.log(`${message}, ${this.name}`);
}
const person = { name: 'Frank' };
greet.apply(person, ['Hi']); // 输出: Hi, Frank
bind方法:返回一个新的函数,新函数的this值被绑定到bind方法的第一个参数上。
function greet(message) {
console.log(`${message}, ${this.name}`);
}
const person = { name: 'Grace' };
const boundGreet = greet.bind(person);
boundGreet('Hello'); // 输出: Hello, Grace
十、作用域、作用域链、变量提升
作用域
-
定义:作用域是指变量和函数的可访问范围,它控制着变量和函数的可见性与生命周期。在 JavaScript 中,主要有全局作用域和函数作用域,ES6 引入了块级作用域。
-
全局作用域:在任何函数外部声明的变量拥有全局作用域,这些变量可以在代码的任何地方被访问。例如:
// 全局作用域变量
const globalVariable = 'I am global';
function showGlobal() {
console.log(globalVariable);
}
showGlobal(); // 输出: I am global
- 函数作用域:在函数内部声明的变量只能在该函数内部访问,具有函数作用域。例如:
function showLocal() {
const localVariable = 'I am local';
console.log(localVariable);
}
showLocal(); // 输出: I am local
// console.log(localVariable); // 报错,localVariable 未定义
- 块级作用域:使用
let和const关键字在if语句、for循环等代码块中声明的变量具有块级作用域,只能在该代码块内部访问。例如:
if (true) {
const blockVariable = 'I am in block';
console.log(blockVariable); // 输出: I am in block
}
// console.log(blockVariable); // 报错,blockVariable 未定义
作用域链
-
定义:当访问一个变量时,JavaScript 引擎会先在当前作用域中查找该变量,如果找不到,就会沿着作用域链向上查找,直到找到该变量或者到达全局作用域。作用域链是由多个嵌套的作用域组成的链表结构。
-
示例:
const globalVar = 'global';
function outer() {
const outerVar = 'outer';
function inner() {
const innerVar = 'inner';
console.log(innerVar); // 输出: inner
console.log(outerVar); // 输出: outer
console.log(globalVar); // 输出: global
}
inner();
}
outer();
在这个例子中,inner 函数内部可以访问自己作用域内的 innerVar 变量,当访问 outerVar 时,由于当前作用域中没有该变量,JavaScript 引擎会沿着作用域链向上查找,在 outer 函数的作用域中找到该变量。同理,访问 globalVar 时,会一直查找到全局作用域。
变量提升
-
定义:变量提升是指在 JavaScript 中,变量和函数的声明会被提升到当前作用域的顶部,因此可以在变量和函数声明之前使用它们。不过,变量提升仅提升声明,不会提升赋值。
-
示例:
console.log(myVariable); // 输出: undefined
var myVariable = 'Hello';
在这个例子中,var 声明的 myVariable 被提升到了作用域的顶部,但赋值操作并没有被提升,所以在变量声明之前访问 myVariable 会得到 undefined。
需要注意的是,使用 let 和 const 声明的变量也会被提升,但在声明之前访问会导致 ReferenceError,这是因为 let 和 const 存在暂时性死区(TDZ)。例如:
// console.log(myLetVariable); // 报错,ReferenceError
let myLetVariable = 'World';
十一、继承
原型链继承
原型链继承是 JavaScript 中最基础的继承方式,其核心原理是让子类的原型指向父类的实例。这样,子类实例就能够访问父类原型上的属性和方法。
// 父类构造函数
function Parent() {
this.parentProperty = 'I am from parent';
}
// 父类原型方法
Parent.prototype.sayHello = function () {
console.log('Hello from parent');
};
// 子类构造函数
function Child() {}
// 子类的原型指向父类的实例
Child.prototype = new Parent();
const child = new Child();
console.log(child.parentProperty);
child.sayHello();
不过,原型链继承存在一些问题,比如所有子类实例会共享父类实例的属性,若修改其中一个子类实例的属性,会影响其他子类实例。
构造函数继承
构造函数继承是在子类构造函数中调用父类构造函数,通过 call 或 apply 方法将父类的属性和方法绑定到子类实例上。
// 父类构造函数
function Parent(name) {
this.name = name;
this.sayName = function () {
console.log(`My name is ${this.name}`);
};
}
// 子类构造函数
function Child(name, age) {
// 调用父类构造函数
Parent.call(this, name);
this.age = age;
}
const child = new Child('John', 20);
console.log(child.name);
child.sayName();
构造函数继承解决了原型链继承中属性共享的问题,但它无法继承父类原型上的方法。
组合继承
组合继承结合了原型链继承和构造函数继承的优点。它通过原型链继承父类原型上的属性和方法,通过构造函数继承父类实例的属性。
// 父类构造函数
function Parent(name) {
this.name = name;
}
// 父类原型方法
Parent.prototype.sayName = function () {
console.log(`My name is ${this.name}`);
};
// 子类构造函数
function Child(name, age) {
// 构造函数继承
Parent.call(this, name);
this.age = age;
}
// 原型链继承
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child = new Child('John', 20);
console.log(child.name);
console.log(child.age);
child.sayName();
组合继承虽然解决了原型链继承和构造函数继承的一些问题,但它会调用两次父类构造函数,可能会产生一些不必要的开销。
寄生组合继承
寄生组合继承是对组合继承的优化,它避免了两次调用父类构造函数。通过创建一个空函数,将其原型指向父类的原型,然后让子类的原型指向这个空函数的实例。
// 父类构造函数
function Parent(name) {
this.name = name;
}
// 父类原型方法
Parent.prototype.sayName = function () {
console.log(`My name is ${this.name}`);
};
// 子类构造函数
function Child(name, age) {
// 构造函数继承
Parent.call(this, name);
this.age = age;
}
// 寄生组合继承核心代码
function inheritPrototype(child, parent) {
const prototype = Object.create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
// 实现继承
inheritPrototype(Child, Parent);
const child = new Child('John', 20);
console.log(child.name);
console.log(child.age);
child.sayName();
寄生组合继承是一种比较理想的继承方式,它结合了原型链继承和构造函数继承的优点,同时避免了一些不必要的开销。
类继承(ES6 及以后)
ES6 引入了 class 关键字和 extends 关键字,提供了更简洁的语法来实现继承。
// 父类
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
console.log(`My name is ${this.name}`);
}
}
// 子类
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
sayAge() {
console.log(`I am ${this.age} years old`);
}
}
const child = new Child('John', 20);
console.log(child.name);
console.log(child.age);
child.sayName();
child.sayAge();
十二、axios 和 fecth 的区别
语法风格
axios:基于 Promise,语法相对简洁直观,通过axios.get、axios.post等方法发起请求,易于理解和使用。fetch:使用原生的 Promise,但语法较为底层,需要更多的代码来处理请求和响应,例如设置请求头、处理状态码等。
浏览器兼容性
axios:兼容性较好,能在大多数现代浏览器以及旧版本浏览器中使用,通过polyfill可以解决一些兼容性问题。fetch:是现代浏览器的原生 API,在一些旧版本浏览器中可能不被支持,需要引入polyfill来实现兼容。
功能特性
axios:具有自动转换请求和响应数据的功能,如将 JSON 数据自动解析为 JavaScript 对象。同时,它还支持请求和响应拦截器,方便在请求发送前和响应接收后进行统一的处理,例如添加请求头、处理错误等。fetch:功能较为基础,需要手动处理更多的细节,如将响应数据解析为 JSON 格式等。不过,它提供了更底层的操作,让开发者对请求和响应有更精细的控制。
默认行为
axios:默认会携带跨域的Cookie,可以通过配置来控制是否携带。fetch:默认不会携带跨域的Cookie,需要设置credentials选项来指定是否携带。
十三、ES2015+ 新特性
ES2015(ES6)核心特性*
-
变量声明
let和const:块级作用域变量声明,替代var。const用于声明常量(不可重新赋值)。
-
箭头函数
- 简化函数语法,自动绑定当前
this。
- 简化函数语法,自动绑定当前
-
模板字符串
const name = 'Alice'; console.log(`Hello, ${name}!`);- 支持多行字符串和变量插值。
-
解构赋值
const [x, y] = [1, 2]; // 数组解构 const { name, age } = user; // 对象解构 -
默认参数
function greet(name = 'Guest') { return `Hello, ${name}!`; } -
扩展运算符(
...)const arr = [...oldArr, newItem]; // 数组展开 const obj = { ...oldObj, key: 'value' }; // 对象展开 -
类(Class)
class Person { constructor(name) { this.name = name; } sayHi() { console.log(`Hi, ${this.name}!`); } }- 语法糖,基于原型继承的封装。
-
模块化(Import/Export)
import { func } from './module.js'; export const data = 42; -
Promise
fetch(url) .then(response => response.json()) .catch(error => console.log(error));- 更优雅的异步编程方式。
-
Symbol
const id = Symbol('unique');- 唯一不可变的值,常用作对象属性的键。
-
Map 和 Set
const map = new Map(); map.set('key', 'value'); const set = new Set([1, 2, 3]);
ES2016(ES7)
-
Array.prototype.includes[1, 2, 3].includes(2); // true -
指数运算符(
**)2 ** 3; // 8
ES2017(ES8)
-
async/awaitasync function fetchData() { const data = await fetch(url); console.log(data); }- 基于 Promise 的异步代码同步化写法。
-
Object.values()/Object.entries()Object.values({ a: 1, b: 2 }); // [1, 2] Object.entries({ a: 1, b: 2 }); // [['a', 1], ['b', 2]] -
字符串填充(
padStart/padEnd)'5'.padStart(2, '0'); // '05' -
尾逗号(Trailing Commas)
const obj = { a: 1, b: 2, };
ES2018(ES9)
-
异步迭代器和 for-await-of
用于异步遍历可迭代对象。
async function* asyncGenerator() { let i = 0; while (i < 3) { yield new Promise(resolve => setTimeout(() => resolve(i++), 1000)); } } (async () => { for await (const num of asyncGenerator()) { console.log(num); } })(); -
Promise.finallyfetch(url) .then(data => console.log(data)) .finally(() => stopLoading());
ES2019(ES10)
-
Array.flat()/Array.flatMap()[1, [2]].flat(); // [1, 2] -
Object.fromEntries()Object.fromEntries([['a', 1], ['b', 2]]); // { a: 1, b: 2 } -
String.trimStart()/String.trimEnd()' abc '.trimStart(); // 'abc ' -
可选的
catch绑定try { ... } catch { ... } // 无需写 catch(error)
ES2020(ES11)
-
可选链(
?.)user?.address?.city; // 避免报错 -
空值合并(
??)const value = input ?? 'default'; // 仅在 null/undefined 时生效 -
BigIntconst bigNum = 9007199254740991n; -
动态导入(
import())const module = await import('./module.js'); -
Promise.allSettledPromise.allSettled([promise1, promise2]).then(results => ...);
ES2021(ES12)及之后
-
String.replaceAll'aabb'.replaceAll('b', 'c'); // 'aacc' -
逻辑赋值运算符
a ||= b; // a = a || b a &&= b; // a = a && b -
数字分隔符(
_)const billion = 1_000_000_000;b -
Promise.anyPromise.any([promise1, promise2]).then(first => ...);
十四、require 和 import 的区别
1. 所属规范
require: 来自 CommonJS 规范,主要用于 Node.js 环境import: 来自 ES6 (ECMAScript 2015) 模块规范,是现代 JavaScript 的标准
2. 加载时机
require: 是运行时加载,可以动态引入(可以在代码的任何地方使用)import: 是编译时静态加载(必须放在文件顶部,不能动态引入)
3. 语法差异
// CommonJS (require)
const fs = require('fs');
const { readFile } = require('fs');
// ES6 (import)
import fs from 'fs';
import { readFile } from 'fs';
import * as fs from 'fs';
4. 特点对比
| 特性 | require (CommonJS) | import (ES6) |
|---|---|---|
| 动态加载 | 支持 | 不支持 |
| 静态分析 | 不支持 | 支持 |
| 异步加载 | 不支持 | 支持 |
| 按需加载 | 整个模块加载 | 支持按需导入 |
| 默认导出 | module.exports | export default |
| 命名导出 | exports.name | export const name |
5. 使用场景
- Node.js 环境:传统上使用
require,但现在也支持import(需要在 package.json 中设置"type": "module") - 浏览器/前端:现代前端开发主要使用
import(需要打包工具如 Webpack、Rollup 或使用原生 ES 模块)
十五、事件循环*
graph LR
A[宏任务队列] --> B{事件循环}
C[微任务队列] --> B
B --> D{是否有宏任务?}
D --是--> E[执行宏任务]
E --> F{执行微任务直到队列为空}
F --> D
F --否--> D
1、事件循环的核心概念
事件循环(Event Loop)是 JavaScript 实现异步编程的基础机制,其核心思想是:
- 单线程执行:JavaScript 主线程同一时间只能执行一个任务
- 异步任务队列:异步任务不立即执行,而是放入队列等待
- 循环检查:主线程不断检查队列,取出任务执行
2、宏任务(MacroTask)与微任务(MicroTask)
1. 宏任务队列
-
特点:每次事件循环处理一个宏任务
-
常见任务类型:
- 脚本执行(Script execution) :即浏览器加载并执行 JavaScript 代码的过程
- 解析 HTML 文本:将 HTML 字符串转换为 DOM 节点
setTimeout、setInterval- 浏览器事件回调(
click、resize) setImmediate(Node.js 特有)I/O操作回调MessageChannel消息接收
2. 微任务队列
-
特点:在每个宏任务执行完毕后,会立即执行所有微任务
-
常见任务类型:
- CSS 解析:将 CSS 文本转换为 CSSOM 树
Promise.then/catch/finallyMutationObserver(DOM 变化监听)process.nextTick(Node.js 特有)Object.observe(已废弃)
3、事件循环的执行流程
-
初始化:创建宏任务队列和微任务队列
-
循环检查:
- 检查宏任务队列是否有任务
- 如有,取出第一个宏任务执行
-
执行宏任务:
- 宏任务执行过程中可能会产生新的宏任务或微任务
- 例如:
setTimeout会向宏任务队列添加新任务 - 例如:
Promise.then会向微任务队列添加新任务
-
处理微任务:
- 宏任务执行完毕后,立即执行微任务队列中的所有任务
- 执行微任务时也可能添加新的微任务(形成队列连锁)
-
重复循环:
- 微任务队列清空后,回到步骤 2,继续检查宏任务队列
延伸:浏览器中宏任务的特殊场景
-
多个脚本的宏任务处理
若 HTML 中存在多个<script>标签:- 每个
<script>的执行都是一个独立的宏任务 - 按标签顺序依次进入宏任务队列,顺序执行
- 每个
-
async与defer脚本的宏任务特性-
async脚本:加载完成后立即作为宏任务执行不阻塞 HTML 解析 -
defer脚本:HTML 解析完成后,作为宏任务按顺序执行
-