2025 前端面试 —— JS 篇

464 阅读17分钟

一、如何避免 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由于其内部实现机制,可能会有一些额外的内存开销。

原生属性和方法

  • Mapsize属性表示键值对数量,还有clear方法清空Map等。
  • Objectprototype等属性,以及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 上的方法,如 pushpop 等。

原型链

  • 定义:每个对象都有一个 __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 操作的整个过程涉及多个步骤,下面为你详细解析:

  1. 创建一个新对象new 操作首先会在内存中创建一个空的对象,这个对象将作为构造函数的实例。可以把它想象成一个新的容器,用于存储实例的属性和方法。

  2. 设置原型链:新创建的对象的 __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
  1. 执行构造函数:将新创建的对象作为 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
  1. 返回值判断:如果构造函数没有显式返回一个对象(即返回 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>

使用 callapply 和 bind 方法

callapply 和 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.getaxios.post等方法发起请求,易于理解和使用。
  • fetch:使用原生的 Promise,但语法较为底层,需要更多的代码来处理请求和响应,例如设置请求头、处理状态码等。

浏览器兼容性

  • axios:兼容性较好,能在大多数现代浏览器以及旧版本浏览器中使用,通过polyfill可以解决一些兼容性问题。
  • fetch:是现代浏览器的原生 API,在一些旧版本浏览器中可能不被支持,需要引入polyfill来实现兼容。

功能特性

  • axios:具有自动转换请求和响应数据的功能,如将 JSON 数据自动解析为 JavaScript 对象。同时,它还支持请求和响应拦截器,方便在请求发送前和响应接收后进行统一的处理,例如添加请求头、处理错误等。
  • fetch:功能较为基础,需要手动处理更多的细节,如将响应数据解析为 JSON 格式等。不过,它提供了更底层的操作,让开发者对请求和响应有更精细的控制。

默认行为

  • axios:默认会携带跨域的Cookie,可以通过配置来控制是否携带。
  • fetch:默认不会携带跨域的Cookie,需要设置credentials选项来指定是否携带。

十三、ES2015+ 新特性

ES2015(ES6)核心特性*

  1. 变量声明

    • let 和 const:块级作用域变量声明,替代 var
    • const 用于声明常量(不可重新赋值)。
  2. 箭头函数

    • 简化函数语法,自动绑定当前 this
  3. 模板字符串

    const name = 'Alice';
    console.log(`Hello, ${name}!`);
    
    • 支持多行字符串和变量插值。
  4. 解构赋值

    const [x, y] = [1, 2]; // 数组解构
    const { name, age } = user; // 对象解构
    
  5. 默认参数

    function greet(name = 'Guest') {
      return `Hello, ${name}!`;
    }
    
  6. 扩展运算符(...

    const arr = [...oldArr, newItem]; // 数组展开
    const obj = { ...oldObj, key: 'value' }; // 对象展开
    
  7. 类(Class)

    class Person {
      constructor(name) { this.name = name; }
      sayHi() { console.log(`Hi, ${this.name}!`); }
    }
    
    • 语法糖,基于原型继承的封装。
  8. 模块化(Import/Export)

    import { func } from './module.js';
    export const data = 42;
    
  9. Promise

    fetch(url)
      .then(response => response.json())
      .catch(error => console.log(error));
    
    • 更优雅的异步编程方式。
  10. Symbol

    const id = Symbol('unique');
    
    • 唯一不可变的值,常用作对象属性的键。
  11. Map 和 Set

    const map = new Map();
    map.set('key', 'value');
    
    const set = new Set([1, 2, 3]);
    

ES2016(ES7)

  1. Array.prototype.includes

    [1, 2, 3].includes(2); // true
    
  2. 指数运算符(**

    2 ** 3; // 8
    

ES2017(ES8)

  1. async/await

    async function fetchData() {
      const data = await fetch(url);
      console.log(data);
    }
    
    • 基于 Promise 的异步代码同步化写法。
  2. Object.values() / Object.entries()

    Object.values({ a: 1, b: 2 }); // [1, 2]
    Object.entries({ a: 1, b: 2 }); // [['a', 1], ['b', 2]]
    
  3. 字符串填充(padStart/padEnd

    '5'.padStart(2, '0'); // '05'
    
  4. 尾逗号(Trailing Commas)

    const obj = { a: 1, b: 2, };
    

ES2018(ES9)

  1. 异步迭代器和 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);
        }
    })();
    
  2. Promise.finally

    fetch(url)
      .then(data => console.log(data))
      .finally(() => stopLoading());
    

ES2019(ES10)

  1. Array.flat() / Array.flatMap()

    [1, [2]].flat(); // [1, 2]
    
  2. Object.fromEntries()

    Object.fromEntries([['a', 1], ['b', 2]]); // { a: 1, b: 2 }
    
  3. String.trimStart() / String.trimEnd()

    '  abc  '.trimStart(); // 'abc  '
    
  4. 可选的 catch 绑定

    try { ... } catch { ... } // 无需写 catch(error)
    

ES2020(ES11)

  1. 可选链(?.

    user?.address?.city; // 避免报错
    
  2. 空值合并(??

    const value = input ?? 'default'; // 仅在 null/undefined 时生效
    
  3. BigInt

    const bigNum = 9007199254740991n;
    
  4. 动态导入(import()

    const module = await import('./module.js');
    
  5. Promise.allSettled

    Promise.allSettled([promise1, promise2]).then(results => ...);
    

ES2021(ES12)及之后

  1. String.replaceAll

    'aabb'.replaceAll('b', 'c'); // 'aacc'
    
  2. 逻辑赋值运算符

    a ||= b; // a = a || b
    a &&= b; // a = a && b
    
  3. 数字分隔符(_

    const billion = 1_000_000_000;b
    
  4. Promise.any

    Promise.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.exportsexport default
命名导出exports.nameexport 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 节点
    • setTimeoutsetInterval
    • 浏览器事件回调(clickresize
    • setImmediate(Node.js 特有)
    • I/O 操作回调
    • MessageChannel 消息接收
2. 微任务队列
  • 特点:在每个宏任务执行完毕后,会立即执行所有微任务

  • 常见任务类型

    • CSS 解析:将 CSS 文本转换为 CSSOM 树
    • Promise.then/catch/finally
    • MutationObserver(DOM 变化监听)
    • process.nextTick(Node.js 特有)
    • Object.observe(已废弃)

3、事件循环的执行流程

  1. 初始化:创建宏任务队列和微任务队列

  2. 循环检查

    • 检查宏任务队列是否有任务
    • 如有,取出第一个宏任务执行
  3. 执行宏任务

    • 宏任务执行过程中可能会产生新的宏任务或微任务
    • 例如:setTimeout 会向宏任务队列添加新任务
    • 例如:Promise.then 会向微任务队列添加新任务
  4. 处理微任务

    • 宏任务执行完毕后,立即执行微任务队列中的所有任务
    • 执行微任务时也可能添加新的微任务(形成队列连锁)
  5. 重复循环

    • 微任务队列清空后,回到步骤 2,继续检查宏任务队列

延伸:浏览器中宏任务的特殊场景

  1. 多个脚本的宏任务处理
    若 HTML 中存在多个<script>标签:

    • 每个<script>的执行都是一个独立的宏任务
    • 按标签顺序依次进入宏任务队列,顺序执行
  2. asyncdefer脚本的宏任务特性

    • async脚本:加载完成后立即作为宏任务执行不阻塞 HTML 解析

    • defer脚本:HTML 解析完成后,作为宏任务按顺序执行