1. ES6 和 ES5的联系、区别、转换等关系
1. 简述 ES6 代码转成 ES5 代码的实现思路是什么?
es6转es5主要通过babel工具实现,Babel是JavaScript编译器,能够将es6转换成向后兼容的es5,从而在现有的浏览器和环境中运行。
主要步骤如下:
- 词法分析:
Babel首先会将输入的代码进行词法分析,将代码分割成一个个 词法单元。 - 语法分析:
Babel会对分割后的词法单元进行 语法分析,生成抽象语法树(AST)。 - 转换:通过对
AST进行遍历和修改,Babel将ES6代码 转换 成ES5代码。 - 代码生成:最后,
Babel会将转换后的AST生成 可运行的ES5代码。
在转换过程中,
Babel会根据预定义的插件和预设对代码进行转换。插件和预设可以分别处理一些特定的语法和功能,如箭头函数、类和模块等。同时,Babel还支持开发者自定义插件和预设来处理更加特殊的个性化的需求。
-
语法转换:本质是将
ES6语法转成AST,再将AST转为ES5语法代码。- 例如:将
let、const转换成var,箭头函数转换成Function函数声明等。
- 例如:将
-
API转换:采用Babel-polyfill等工具对ES5中不存在的API(包括Set等ES6中新的数据结构)做修复。- 例如:
Array.prototype.includes、Set、Map等在ES5中不存在,需要用响应的ES5代码实现这些API。
- 例如:
综上所述,
ES6代码转成ES5代码的实现思路涉及词法分析、语法分析、AST的生成和转换、以及使用插件和预设进行特定语法的处理,同事还需要考虑API地兼容性问题。
1.1 解释 babel 是什么?有什么作用?
Babel是一个JavaScript编译器,它可以将ECMAScript 2015+版本的代码转换成向后兼容的JavaScript代码,以便在现有的浏览器中运行。Babel可以帮助开发者使用最新的JavaScript语言特性,而不用担心浏览器兼容性问题。
Babel转换代码。这些特性主要包括箭头函数、解构赋值、模版字符串、let和const等等。还支持转换JSX语法,使得React的代码可以在浏览器中运行。
Babel可以使用插件来扩展其功能。例如,可以将TypeScript转换成JavaScript代码,或者使用插件来判断新的ECMAScript特性。Babel还可以与许多构建工具(如webpack、Rollup等)集成,以便在构建过程中自动转换代码。
2. ES5、ES6(ES2015)有什么区别?
ES5:指的是ECMAScriot的第五个版本,发布于2009年,是目前最广泛使用的JavaScript版本。ES6:指的是ECMAScriot的第六个版本,也称为ES2015,发布于2015年,引入了许多新的语言特性和语法糖。
ES6相较于ES5的主要区别包括:
- 新的语法特性,如箭头函数、类、模版字符串、解构赋值等;
- 新的数据类型,如
Set、Map、Symbol等;- 新的迭代器和生成器,使得处理数据集合更加方便;
- 新的模块化系统,使得代码的组织和管理更加容易;
- 新的
Promise函数,使得异步编程更加简单可读;- 新的默认参数和剩余参数语法,使得函数的定义和调用更多灵活。
ES6新增的特性:
let、const定义块级作用域- 箭头函数
- 解构赋值
- 扩展运算符
- 常见的数组的方法、伪数组
- 模版字符串
class类- 参数设置默认值
promisefor...in,for...of
3. 详细描述 ES6 和 ECMAScript 2015 的关系?
ES2015 是 ES6 的官方名称,但是由于 ES6 引入了太多的新特性,因此人们通常使用 ES2015 代替 ES6.
4. 简述 ES5/ES6 的继承除了写法以外还有什么区别?
ES5/ES6 除了继承除了写法外,主要还有继承的机制和类的方法属性。
4.1. 继承机制:
-
ES5的继承主要通过原型链和构造函数来实现。- 可以通过
Parent.apply(this, arguments)将父类的属性和方法添加到子类的实例上,这种方法相对复杂且不够直观。 - 这种方式下,子类的示例创建后,通过调用父类的构造函数来初始化父类的部分属性。
- 可以通过
-
ES6引入了class关键字,使得继承的写法更加简洁和直观。- 子类的构造函数必须首先调用
super()来获取父类的属性和方法,然后再添加或修改子类特有的属性和方法。 - 在这种方式下,子类的实例创建基于父类实例,只能通过
super()才能访问父类的属性和方法。
- 子类的构造函数必须首先调用
js
体验AI代码助手
代码解读
复制代码
// ES5
// 定义父类
function Parent(value) {
this.language = ["js", "less", "react"];
this.value = value;
}
// 定义子类
function child() {
Parent.apply(this, arguments);
}
const test = new Child(888);
test.language; // ["js", "less", "react"]
test.value; // 888
// ES6
class Child2 extends Parent2 {
constructor(props) {
super(props);
}
}
var test2 = new Child2("super data");
test2.value; // super data
4.2. 类的方法和属性
ES5中,通过原型链添加的方法和属性是可枚举的。例如,通过Object.keys(Bar)可以获取到Bar上的所有可枚举数据类型,包括实例方法和静态方法。ES6中,类的所有方法(包括静态方法和实例方法)都是不可枚举的。这意味着使用Object.keys(Foo)无法获取到Foo类上的任何方法或属性,包括其原型上的方法。
5. 简述 ES6 之前使用 prototype 实现继承?
在引入 class 之前,JavaScript 使用 prototype 来实现继承。每个函数都有一个 prototype 属性,这个属性指向一个对象,而这个对象被用作通过该构造函数创建的所有对象的原型。通过修改这个原型对象,可以添加属性和方法,这些属性和方法会被所有实例共享。
js
体验AI代码助手
代码解读
复制代码
// 父类构造函数
function Parent(name) {
this.name = name;
}
// 父类方法
Parent.prototype.greet = function () {
console.log("Hello, my name is" + this.name);
};
// 子类构造函数
function Child(name, age) {
Parent.call(this, name); // 调用父类构造函数给子类实例设置name属性
this.age = age;
}
// 创建一个Parent实例,将其方法赋予Children.prototype
Child.prototype = Object.create(Parent.prototype);
// 修正Child.prototype的构造器指向
Child.prototype.constructor = Child;
// 子类持有方法
Child.prototype.introduce = function () {
console.log("My name is " + this.name + ", I am " + this.age);
};
// 测试继承
var childInstance = new Child("Alice", 22);
childInstance.greet(); // Hello, my name isAlice
childInstance.introduce(); // My name is Alice, I am 22
6. 简述怎样通过 ES5 及 ES6 声明一个类?
ES5中,类的创建主要通过构造函数来实现,构造函数本身可以看作是一种特殊的函数,用于创建和初始化对象。
js
体验AI代码助手
代码解读
复制代码
// 创建构造函数
let Animal = function (type) {
this.type = type;
};
// 在构造函数的原型上添加共享的方法,这样所有通过该构造函数创建的对象都会继承这些方法。
Animal.prototype.walk = function () {
console.log("I am walking");
};
new Animal("dog");
new Animal("monkey");
// 这样,通过 `new Animal("dog")` 创建的对象 `dog`和通过 `new Animal("monkey")`创建的对象 `monkey`都会继承`walk`方法。
ES6中,类的声明变得更加直观和简洁。ES6引入了class关键字,允许开发者以更接近传统面向对象编程的方式来声明类
js
体验AI代码助手
代码解读
复制代码
// 声明类
class Animal {
// consttuctor:一个特殊的方法,用于创建对象时初始化对象的属性。
constructor(type) {
this.type = type;
}
// 其他的方法,则是类的成员方法,所有该类的实例都会继承这些方法。
walk() {
console.log("I am walking");
}
}
// 语法简洁,更符合传统的面向对象编程思维,代码更易于理解和维护。
2. ES6 的升级
1. 简述 ES6 let 有什么用?有了 var 为什么还要用 let?
在 ES6 之前,声明变量只能用 var,var 方式声明变量其实是很不合理的,准确的说,是因为 ES5 里面没有块级作用域是很不合理的,甚至可以说是一个语言层名的 bug。没有块级作用域会带来很多难以理解的问题,比如 for循环 var 变量泄露,变量覆盖等问题,let 声明的变量拥有自己的块级作用域,且修复了 var 声明变量带来的变量提升问题。
js
体验AI代码助手
代码解读
复制代码
{
var i = 10;
}
console.log(i); // 10
{
let j = 10;
}
console.log(j); // Uncaught ReferenceError: j is not defined
let在for循环中使用:每次循环都会创建一个新的且独立的块级作用域,使用let声明变量传入到for循环体的作用域中,不会改变,不会受到外界的影响。
js
体验AI代码助手
代码解读
复制代码
// var:
var a = [];
for (var i = 0; i < 3; i++) {
a[i] = function () {
console.log(i);
};
}
a[0](); // 3
a[1](); // 3
a[2](); // 3
// 结果:不是预期的0,1,2
// 解释:因为使用 var 声明变量,i 是全局变量,每次循环,都会修改 i 的值,相当于给 i 重新赋值,因此最后输出的 i 是 for循环 完成后的值,因此输入结果全是 3
// var换成let后:
var b = [];
for (let j = 0; j < 3; j++) {
b[j] = function () {
console.log(j);
};
}
b[0](); // 0
b[1](); // 1
b[2](); // 2
let没有变量提升- 变量提升:用
let声明不存在的变量提升,必须等let执行完毕之后,变量才可以使用,否则会报错Uncaught ReferenceError
js
体验AI代码助手
代码解读
复制代码
console.log(c); // Uncaught ReferenceError: Cannot access 'c' before initialization
let c = "c";
// var 存在变量提升,无论是开头还是结尾使用var声明都可以使用。
console.log(d); // undefined
var d = "d";
let同一个作用域不可重复声明,var则不会报错
js
体验AI代码助手
代码解读
复制代码
let a = 1;
let e = 2; // Uncaught SyntaxError: Identifier 'e' has already been declared
var a = 1;
var a = 2;
2. 简述 ES6 对 String 字符串类型做的常用升级优化?
- 模版字符串(
Template Literals):使用反引号( ` )来定义字符串,可以内嵌变量和表达式,还能包含换行符。
js
体验AI代码助手
代码解读
复制代码
let name = "Name";
let greeting = `Hello, ${name}!`;
console.log(greeting); // Hello, Name!
- 字符串扩展:包含了多个新的方法,如
includes(),startsWith(),endsWith(), 用于判断字符串是否包含、是否以特定子串开始或结束。
js
体验AI代码助手
代码解读
复制代码
let test = "hello world!";
console.log(test.startsWith("hello")); // true;
console.log(test.endsWith("world!")); // true;
console.log(test.includes("world")); // true;
- 重复字符串:新增了字符串的重复功能,可以用
repeat(n)来生成一个新的字符串,其中包含原始字符串n次。
js
体验AI代码助手
代码解读
复制代码
let str = "abc";
let repeated = str.repeat(3);
console.log(repeated); // abcabcabc
- 字符串迭代器:
String类型现在可以用for...of循环来进行迭代,每次迭代的是字符串中的没一个字符。
js
体验AI代码助手
代码解读
复制代码
for (const char of "hello") {
console.log(char);
}
// h
// e
// l
// l
// o
3. 简述 ES6 对 Array 数组类型做的常用升级优化?
- 解构赋值:可以直接从数组中提取值并赋值给变量。
js
体验AI代码助手
代码解读
复制代码
let [a, b, c] = [1, 2, 3];
// a=1,b=2,c=3
- 扩展运算符(
...):用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。
js
体验AI代码助手
代码解读
复制代码
let arr1 = [1, 2, 3];
let arr2 = [...arr1]; // [1, 2, 3]
Array.from():可以将类似数组的对象和可遍历对象转换为数组。
js
体验AI代码助手
代码解读
复制代码
let arr3 = { 0: "a", 1: "b", 2: "c", length: 3 };
let arr4 = Array.from(arr3); // ['a', 'b', 'c']
- 数组的
map()和filter()方法:这两个方法可以用于对数组进行更复杂的操作,map()用于创建新数组,filter()用于过滤新数组。
js
体验AI代码助手
代码解读
复制代码
let numbers = [1, 2, 3, 4, 5];
let doubled = numbers.map((number) => number * 2); // [2, 4, 6, 8, 10]
let even = numbers.filter((num) => num % 2 === 0); // [2, 4]
- 简写属性名:在对象字面量中,可以在属性名与变量名相同时使用简写。
js
体验AI代码助手
代码解读
复制代码
let a = 1,
b = 2,
c = 3;
let arr = [a, b, c];
console.log(`output->obj`, arr); // [1, 2, 3]
rest参数(...):用户获取函数的多余参数,这些参数以数组形式表示。
js
体验AI代码助手
代码解读
复制代码
function add(...values) {
let sum = 0;
for (let val of values) {
sum += val;
}
return sum;
}
console.log(add(1, 2, 3)); // 6
- 迭代器与生成器:
ES6中引入了新的for...of循环,用于迭代数据集合,比如Arrays,Maps,Sets等
js
体验AI代码助手
代码解读
复制代码
for (let value of ["a", "b", "c"]) {
console.log(value);
}
// a
// b
// c
Promise与异步编程:ES6引入了Promise对象,用于更优雅地处理异步编程。
js
体验AI代码助手
代码解读
复制代码
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000);
});
promise.then(alert); // 1秒后弹窗显示 "done!"
4. 简述 ES6 对 Number 类型做的常用升级优化?
- 指数运算符: 使用
**代替Math.pow()来进行指数运算。
js
体验AI代码助手
代码解读
复制代码
// 指数运算符
let x = 2;
let y = 3;
let result = x ** y; // 结果为8
Number.isFinite(): 用于检查一个数值是否为有限数。
js
体验AI代码助手
代码解读
复制代码
// Number.isFinite()
console.log(Number.isFinite(0.1)); // 输出:true
console.log(Number.isFinite(Infinity)); // 输出:false
Number.isNaN(): 用于检查一个值是否为NaN。
js
体验AI代码助手
代码解读
复制代码
// Number.isNaN()
console.log(Number.isNaN(NaN)); // 输出:true
Number.parseInt()和Number.parseFloat(): 作为静态方法,可以直接调用,而不用将它们绑定到Number.prototype。
js
体验AI代码助手
代码解读
复制代码
// Number.parseInt()
let num = Number.parseInt("123", 10); // 输出:123
Number.isInteger(): 判断一个值是否为整数。
js
体验AI代码助手
代码解读
复制代码
// Number.isInteger()
console.log(Number.isInteger(123)); // 输出:true
- 安全整数:
Number.MIN_SAFE_INTEGER和Number.MAX_SAFE_INTEGER标识JavaScript中最小和最大安全整数。
js
体验AI代码助手
代码解读
复制代码
// 安全整数
console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
5. 简述 ES6 对 Object 对象类型做的常用升级优化【重要】?
- 属性的简写
js
体验AI代码助手
代码解读
复制代码
let name = "Alice";
let age = 29;
const person = { name, age };
- 属性名可以使用表达式
js
体验AI代码助手
代码解读
复制代码
let key = "name";
const person = { [key]: "Alick", age: "23" };
- 方法的简写
js
体验AI代码助手
代码解读
复制代码
const person = {
name: "Alice",
greet() {
return `Hello, my name is ${this.name}!`;
},
};
- 新的方法
js
体验AI代码助手
代码解读
复制代码
// Object.is()
console.log(Object.is("foo", "foo")); // true
console.log(Object.is({}, {})); // false,因为对象比较是引用比较
// Object.assign()
const object1 = { a: 1 };
const object2 = { b: 2 };
const object3 = { c: 3 };
const mergedObject = Object.assign(object1, object2, object3);
// mergedObject 现在有属性 a, b, 和 c
6. 简述 ES6 对 Function 函数类型做的常用升级优化?
- 箭头函数:简化了函数的定义的方式,用关键字
=>代替function
js
体验AI代码助手
代码解读
复制代码
// ES5
var sum = function (a, b) {
return a + b;
};
// ES6
const sum = (a, b) => a + b;
- 函数参数默认值:允许给函数参数指定默认值,避免了在函数体内部进行判断
js
体验AI代码助手
代码解读
复制代码
// ES5
function multiply(a, b) {
b = b || 1;
return a * b;
}
// ES6
function multiply(a, b = 1) {
return a * b;
}
- rest 参数:允许函数接收数组或是可迭代对象中的多个参数
js
体验AI代码助手
代码解读
复制代码
// ES5
function sum(arr) {
return arr.reduce((a, b) => a + b, 0);
}
// ES6
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
- 扩展运算符:允许一个可迭代的对象展开为函数参数
js
体验AI代码助手
代码解读
复制代码
// ES5
var numbers = [1, 2, 3, 4, 5];
Math.max.apply(null, numbers);
// ES6
const numbers = [1, 2, 3, 4, 5];
Math.max(...numbers);
- 对象方法:允许在对象字面量中使用简写方法定义
js
体验AI代码助手
代码解读
复制代码
// ES5
var obj = {
value: 1,
double: function () {
return this.value * 2;
},
};
// ES6
const obj = {
value: 1,
double() {
return this.value * 2;
},
};
- 函数
name属性: 函数现在有了一个标准化的名字
js
体验AI代码助手
代码解读
复制代码
function foo() {}
console.log(foo.name); // "foo"
- 尾调用优化(Tail Call Optimization): 允许函数的最后一个操作是返回一个函数的递归调用或者一个构造函数的调用
js
体验AI代码助手
代码解读
复制代码
// ES5
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
// ES6
function factorial(n) {
if (n === 1) return 1;
// 尾调用
return n * factorial(n - 1);
}
7. 简述 ES6 Symbol 的作用?
Symbol 是 ES6 引入的一种新的原始数据类型。它是唯一且不可变的数据类型。主要用于创建对象的唯一属性名,以解决命名冲突的问题,以及为对象添加独一无二的属性,这些属性不会与其他属性键冲突。
Symbol 的主要作用和特性包括:
-
唯一性:每个通过
Symbol()函数创建的symbol值都是唯一的,即使是用相同的参数创建的symbol也不相等。这保证了使用symbol作为对象属性名时,不会与其他属性名发生冲突。 -
不可变性:
symbol一旦被创建,就不能被修改,他们是不可变的确保了属性名的稳定性。 -
使用场景:
- 私有属性:
Symbol常被用来作为对象的私有成员,因为Symbol类型的属性不会出现在常规的对象属性枚举中,例如for...in循环或Object.keys()方法中,这使得symbol属性可以被视为对象的私有属性。 - 防止命名冲突:在大型项目或者是多人协作的项目中,使用
symbol可以防止属性名的冲突,特别是扩展第三方库的对象时尤其重要。 - 使用
Well-known Symbols来实现对象接口:ES6定义了一些内置的well-known symbols,他们通过Symbol构造函数的静态属性访问,如Symbol.iterator,Symbol.asyncIterator,Symbol.toStringTag等。这些Symbols用于实现对象的标准行为,例如定义迭代器、异步迭代器或改变对象的字符串描述等。
- 私有属性:
创建 Symbol
js
体验AI代码助手
代码解读
复制代码
let sym1 = Symbol();
let sym2 = Symbol("desc");
let sym3 = Symbol("desc");
console.log(sym2 === sym3); // false
使用 Symbol 作为对象的属性名
js
体验AI代码助手
代码解读
复制代码
let mySymbol = Symbol();
let obj = {
[mySymbol]: "value",
};
console.log(obj[mySymbol]); // "value"
Symbol 保证属性不会被意外覆盖或枚举
js
体验AI代码助手
代码解读
复制代码
let id = Symbol("id");
let person = {
name: "John",
age: 20,
[id]: 111,
};
for (let key in person) {
console.log(key); // name,age
}
console.log(Object.keys(person)); // ["name","age"]
console.log(person[id]); // 111
8. 简述 ES6 Set 的作用?
ES6 引入 Set 新的数据结构,其主要作用是提供一种存储唯一值的集合,无论这个值是原始值还是对象引用,Set 对象允许存储任何类型的唯一值,无论是原始值还是对象引用,他们在 Set 种不会重复出现。
Set 的主要特性和作用包括:
- 唯一性:
Set内部的值都是唯一的,这意味着Set集合中没有重复的值,这对于需要元素唯一性的数据结构非常有用。例如:去重 - 值的类型:
Set可以存储任何类型的值,包括原始类型和对象引用。 - 数据操作:
Set提供了简单的操作方法,包括add(value)添加新元素,delete(value)删除元素,has(value)检查元素是否存在,以及clear()清空所有元素。这些方法提高了数据操作的便利性。 - 迭代方法:
Set是可迭代的,它提供了forEach方法及keys,values,entries迭代器方法,使得遍历集合变得非常简单。由于Set的值是唯一的,所以keys()和values()方法的行为是相同的。 - 集合大小:通过
size属性,可以很方便的获取集合中元素的数量。
创建 Set 并添加元素
js
体验AI代码助手
代码解读
复制代码
let mySet = new Set();
mySet.add(1);
mySet.add("some text");
mySet.add({ a: 1, b: 2 });
console.log(mySet.size); // 3
检查值是否在 Set 中
js
体验AI代码助手
代码解读
复制代码
console.log(mySet.has(1)); // true
console.log(mySet.has(3)); // false
遍历 Set
js
体验AI代码助手
代码解读
复制代码
mySet.forEach((value) => {
console.log(value);
});
// 1
// some text
// {a: 1, b: 2}
使用 Set 去重
js
体验AI代码助手
代码解读
复制代码
const numbers = [2, 3, 4, 5, 2, 3, 8];
const uniqueNumbers = new Set(numbers);
console.log(uniqueNumbers); // Set(5) {2, 3, 4, 5, 8}
9. 简述 ES6 Map 的作用?
ES6 引入 Map 对象作为一种新的键值对集合结构,它提供了比传统对象字面量更灵活和强大的方式来存储数据。Map 对象可以使用任何类型的值(包括对象)作为键,这里它与传统对象最大的不同之处,后者仅支持字符串和 Symbol 作为键名。
Map 的主要特性和作用:
- 键的多样性:在
Map中,键可以是任意类型的值,包括函数、对象或任何原始类型。 - 元素顺序:
Map对象维护键值对的插入顺序,当进行迭代时,会按照元素的插入顺序返回键值对。 - 大小可测:通过
Map的size属性可以直接获取集合的大小,这比传统对象需要手动计数更为方便。 - 性能优化:对于频繁增删键值对的场景,
Map的性能通常优于传统的对象,因为Map是专门为了大量数据的存储而设计的。 - 更好的迭代器支持:
Map对象是可迭代的,它提供了forEach方法以及keys(),values(),entries()这些迭代器方法,使得遍历数据变得非常简单。 - 直接数据操作方法:
Map提供了set(key, value),get(key),has(key),delete(key)和clear等方法,用于更加直接和便捷的操作数据。
创建 Map 并添加元素:
js
体验AI代码助手
代码解读
复制代码
let myMap = new Map();
myMap.set("key1", "value1");
myMap.set("1", "value2");
myMap.set({}, "value3");
console.log(myMap); // Map(3) {'key1' => 'value1', '1' => 'value2', {…} => 'value3'}
console.log(myMap.size); // 3
获取和设置值:
js
体验AI代码助手
代码解读
复制代码
console.log(myMap.get("key1")); // value1
console.log(myMap.get(1)); // value2
console.log(myMap.get({})); // undefined,因为{}是一个新的对象引用
遍历 Map:
js
体验AI代码助手
代码解读
复制代码
myMap.forEach((value, key) => {
console.log(key, value);
});
// key1 value1
// 1 'value2'
// Object {} 'value3'
使用 Map 进行数据结构的优化:
Map 的引入使得 JavaScript 在处理复杂的数据结构时更加灵活和强大,尤其是在需要键值对存储且键为非字符串时。此外,Map 的性能优化和迭代器支持使得数据操作和遍历更为高效和方便。
简述 ES6 Proxy 的作用?
ES6 Proxy(代理)是 ES6 新增的核心特性,用于创建一个对象的 “代理” ,从而拦截并自定义该对象的基本操作(如属性读取、赋值、删除、函数调用等)。它本质上是在目标对象与外部访问之间建立了一层 “拦截层”,让开发者可以精细化控制对象的行为。
核心作用
1. 拦截并自定义对象操作
Proxy 支持对对象的几乎所有底层操作进行拦截(称为 “陷阱”,Trap),常见拦截场景包括:
- 属性读取(
get):拦截obj.xxx或obj['xxx']; - 属性赋值(
set):拦截obj.xxx = value; - 属性删除(
deleteProperty):拦截delete obj.xxx; - 函数调用(
apply):拦截函数执行(如fn()); - 构造函数调用(
construct):拦截new 构造函数(); - 其他:如
has(拦截in运算符)、ownKeys(拦截Object.keys()等)。
通过这些拦截器,可自定义操作逻辑,比如校验赋值、禁止删除属性、日志记录等。
2. 数据劫持与响应式
Proxy 是实现响应式数据的核心(如 Vue 3 响应式系统)。通过拦截 get 和 set,可以在属性被读取时收集依赖,在属性被修改时触发更新,从而实现数据变化自动更新视图。
示例(简易响应式):
javascript
运行
const reactive = (target) => {
return new Proxy(target, {
get(target, key) {
// 收集依赖(如记录当前依赖的视图函数)
track(target, key);
return Reflect.get(target, key);
},
set(target, key, value) {
Reflect.set(target, key, value);
// 触发更新(执行依赖的视图函数)
trigger(target, key);
return true;
}
});
};
3. 数据校验与保护
可对对象属性的读写、删除等操作做校验,限制非法操作:
- 赋值时校验类型 / 范围(如年龄必须是正数);
- 禁止修改只读属性;
- 禁止删除核心属性;
- 拦截不存在的属性访问,返回默认值。
示例(数据校验):
javascript
运行
const user = { age: 18 };
const proxyUser = new Proxy(user, {
set(target, key, value) {
if (key === 'age' && (typeof value !== 'number' || value < 0)) {
throw new Error('年龄必须是正数');
}
return Reflect.set(target, key, value);
}
});
proxyUser.age = -5; // 抛出错误:年龄必须是正数
4. 虚拟化对象 / 属性
Proxy 可模拟不存在的属性或对象结构,无需实际定义这些属性:
- 访问
obj.xxx时,动态计算返回值; - 模拟 “虚拟属性”(如
obj.fullName由firstName + lastName拼接)。
示例(虚拟属性):
javascript
运行
const person = { firstName: '张', lastName: '三' };
const proxyPerson = new Proxy(person, {
get(target, key) {
if (key === 'fullName') {
return `${target.firstName}${target.lastName}`;
}
return Reflect.get(target, key);
}
});
console.log(proxyPerson.fullName); // 张三(无需定义 fullName 属性)
5. 函数 / 类的增强
通过 apply 拦截函数调用,可实现函数防抖、节流、参数校验;通过 construct 拦截类的实例化,可限制实例创建次数等。
示例(函数参数校验):
javascript
运行
const sum = (a, b) => a + b;
const proxySum = new Proxy(sum, {
apply(target, thisArg, args) {
if (args.some(item => typeof item !== 'number')) {
throw new Error('参数必须是数字');
}
return Reflect.apply(target, thisArg, args);
}
});
proxySum(1, '2'); // 抛出错误:参数必须是数字
关键特点
- 非侵入式:无需修改目标对象本身,通过代理层实现逻辑;
- 全面拦截:覆盖对象的绝大多数底层操作(相比
Object.defineProperty更全面,支持数组、动态属性等); - 反射配合:通常与
Reflect配合使用,Reflect提供了与 Proxy 拦截器对应的方法,保证操作的原生行为。
简述 ES6 Reflect 的作用?
ES6 Reflect 是一个内置的静态对象(非构造函数,不能 new),核心作用是将对象的底层原生操作(如属性读写、函数调用、原型操作等)封装为统一的函数方法,并规范这些操作的返回值和行为,常与 Proxy 配合使用,是 ES6 为了优化对象操作逻辑、适配代理场景而设计的核心特性。
核心作用
1. 统一对象原生操作的调用方式
在 ES6 之前,对象的底层操作既有 “运算符形式”(如 obj.xxx、delete obj.xxx、in),也有 “方法形式”(如 Object.defineProperty),语法不统一且语义不直观。Reflect 将这些操作全部封装为函数方法,让对象操作更统一、更具函数式编程风格。
| 传统操作方式 | Reflect 统一方法 | 说明 |
|---|---|---|
obj.xxx | Reflect.get(obj, 'xxx') | 读取属性 |
obj.xxx = value | Reflect.set(obj, 'xxx', value) | 赋值属性 |
delete obj.xxx | Reflect.deleteProperty(obj, 'xxx') | 删除属性 |
'xxx' in obj | Reflect.has(obj, 'xxx') | 判断属性是否存在 |
new Fn(...) | Reflect.construct(Fn, [...args]) | 构造函数实例化 |
fn.call(thisArg, ...args) | Reflect.apply(fn, thisArg, [...args]) | 调用函数 |
示例:
javascript
运行
const obj = { name: '张三' };
// 传统方式
console.log(obj.name); // 张三
delete obj.name;
// Reflect 方式
console.log(Reflect.get(obj, 'name')); // undefined
Reflect.set(obj, 'name', '李四');
console.log(Reflect.has(obj, 'name')); // true
2. 规范操作的返回值,避免异常
传统对象操作中,部分操作失败会抛出异常(如 Object.defineProperty 定义属性失败),部分返回无意义的值(如 delete 运算符返回布尔值,但 obj.xxx = value 无返回值)。Reflect 统一了返回值规则:所有方法均返回布尔值(表示操作是否成功)或对应操作结果,避免手动捕获异常,简化错误处理。
示例:
javascript
运行
const obj = Object.freeze({ name: '张三' }); // 冻结对象,禁止修改属性
// 传统方式:赋值无报错,但操作失败(静默失败)
obj.age = 18;
console.log(obj.age); // undefined
// Reflect 方式:返回布尔值,明确操作结果
const isSuccess = Reflect.set(obj, 'age', 18);
console.log(isSuccess); // false(操作失败)
// 传统 Object.defineProperty 失败会抛异常
try {
Object.defineProperty(obj, 'name', { writable: true });
} catch (e) {
console.log('定义失败');
}
// Reflect.defineProperty 返回布尔值,无需 try/catch
const defineSuccess = Reflect.defineProperty(obj, 'name', { writable: true });
console.log(defineSuccess); // false
3. 适配 Proxy 拦截器,保证原生行为
Reflect 的方法与 Proxy 的拦截器(陷阱)一一对应(如 Proxy 的 get 拦截器对应 Reflect.get,apply 对应 Reflect.apply)。在 Proxy 中使用 Reflect 调用方法,可精准复现目标对象的原生操作逻辑,避免手动模拟原生行为导致的漏洞(比如忽略 this 绑定、属性描述符、继承属性等细节)。
示例(Proxy + Reflect 配合):
javascript
运行
const proxy = new Proxy({ name: '张三' }, {
get(target, key) {
console.log('拦截读取:', key);
// 调用 Reflect.get 保证原生读取行为(如继承属性、访问器属性)
return Reflect.get(target, key);
},
set(target, key, value) {
console.log('拦截赋值:', key, value);
// 返回 Reflect.set 的布尔值,让 Proxy 知道赋值是否成功
return Reflect.set(target, key, value);
}
});
proxy.name = '李四'; // 拦截赋值:name 李四
console.log(proxy.name); // 拦截读取:name → 李四
4. 补充对象操作的缺失能力
Reflect 提供了一些传统方式难以实现的原生操作能力:
Reflect.construct:支持不通过new调用构造函数(等效于new Fn(...args),但更灵活,可指定原型);Reflect.getPrototypeOf/Reflect.setPrototypeOf:比Object.getPrototypeOf更规范(返回布尔值表示是否成功);Reflect.ownKeys:返回对象自身所有键(包括不可枚举、Symbol 键),等效于Object.getOwnPropertyNames + Object.getOwnPropertySymbols,且与Proxy的ownKeys拦截器匹配。
示例:
javascript
运行
// 不使用 new 调用构造函数
function Person(name) {
this.name = name;
}
const p = Reflect.construct(Person, ['张三']);
console.log(p instanceof Person); // true
关键特点
- 静态性:所有方法都是静态方法(如
Reflect.get),无实例方法; - 无副作用:方法仅执行对应原生操作,不额外修改对象(除非操作本身是修改行为);
- 与 Proxy 强绑定:是 Proxy 拦截器的 “原生行为兜底工具”,两者配合是 ES6 代理模式的最佳实践;
- 行为规范化:修复了传统操作的不一致性(如
Reflect.set支持接收receiver参数,正确处理继承属性的访问器)。
总结
Reflect 的核心价值是统一、规范、安全地封装对象底层操作:
- 对开发者:简化对象操作的语法,统一错误处理逻辑;
- 对 Proxy:提供原生行为的 “兜底实现”,让代理逻辑更可靠;
- 整体上:让 JavaScript 对对象的底层操作从 “零散的运算符 / 方法” 走向 “统一的函数式接口”,是 ES6 完善对象模型的重要补充。
简述 ES6 Promise 的作用?
ES6 Promise 是 JavaScript 中用于处理异步操作的核心对象,它解决了传统回调函数嵌套(“回调地狱”)导致的代码可读性差、错误处理混乱等问题,为异步编程提供了更优雅、可维护的范式,是现代前端异步编程的基础(如 async/await 基于 Promise 实现)。
核心作用
1. 解决 “回调地狱” 问题
传统异步操作(如 AJAX、文件读取)依赖嵌套回调,多层嵌套会导致代码横向膨胀、逻辑混乱(即 “回调地狱”)。Promise 通过链式调用将嵌套的回调转为线性的调用链,让异步逻辑更清晰。
示例:
javascript
运行
// 传统回调地狱(多层嵌套)
ajax('url1', (res1) => {
ajax('url2?id=' + res1.id, (res2) => {
ajax('url3?name=' + res2.name, (res3) => {
console.log(res3);
}, (err) => { console.error(err); });
}, (err) => { console.error(err); });
}, (err) => { console.error(err); });
// Promise 链式调用(线性逻辑)
ajaxPromise('url1')
.then(res1 => ajaxPromise('url2?id=' + res1.id))
.then(res2 => ajaxPromise('url3?name=' + res2.name))
.then(res3 => console.log(res3))
.catch(err => console.error(err));
2. 统一异步操作的状态管理
Promise 定义了异步操作的三种明确状态,且状态一旦改变(定型)就不可逆,避免了异步操作状态混乱的问题:
pending(进行中):初始状态,异步操作未完成;fulfilled(已成功):异步操作完成,调用resolve;rejected(已失败):异步操作出错,调用reject。
状态只能从 pending → fulfilled 或 pending → rejected,确保异步操作的结果唯一且可预测。
3. 标准化的错误处理
传统回调需要为每个异步操作单独处理错误,Promise 提供了统一的错误捕获机制:
- 链式调用中任意环节抛出错误,都会终止后续
then执行,直接进入最近的catch; - 支持通过
catch捕获整个异步链的所有错误,无需层层嵌套错误处理逻辑; - 可通过
finally执行无论成功 / 失败都需要的收尾操作(如关闭加载动画)。
示例:
javascript
运行
fetch('/api/data')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error('请求失败:', err))
.finally(() => console.log('请求结束(无论成败)'));
4. 支持异步操作的并行 / 串行 / 竞态控制
Promise 提供了多个静态方法,方便管理多个异步操作的执行逻辑:
Promise.all(iterable):并行执行多个 Promise,全部成功则返回结果数组;任意一个失败则立即触发reject(适用于 “所有操作都完成才继续” 的场景,如多接口并行请求);Promise.race(iterable):多个 Promise 竞速,第一个完成(无论成功 / 失败)的结果即为最终结果(适用于 “超时控制” 等场景);Promise.allSettled(iterable):等待所有 Promise 完成(无论成功 / 失败),返回每个 Promise 的状态和结果(适用于 “需要知道所有操作最终状态” 的场景);Promise.resolve()/Promise.reject():快速创建已定型的 Promise,简化异步逻辑封装。
示例(并行请求):
javascript
运行
// 并行请求两个接口,全部成功后处理数据
Promise.all([fetch('/api/user'), fetch('/api/order')])
.then(([userRes, orderRes]) => Promise.all([userRes.json(), orderRes.json()]))
.then(([user, order]) => console.log(user, order))
.catch(err => console.error('至少一个请求失败:', err));
5. 作为异步编程的基础适配层
Promise 是 ES7 async/await 的底层依赖(async 函数的返回值本质是 Promise),也是现代 API(如 Fetch API、Node.js 异步方法)的标准返回类型,成为 JavaScript 异步编程的 “通用接口”,让不同异步库 / API 之间的交互更统一。
示例(async/await 基于 Promise):
javascript
运行
async function getData() {
try {
const res = await fetch('/api/data'); // await 等待 Promise 完成
const data = await res.json();
return data;
} catch (err) {
console.error(err);
}
}
// 调用 async 函数返回 Promise
getData().then(data => console.log(data));
关键特点
- 一次性:状态定型后无法修改,避免异步操作重复触发;
- 微任务执行:Promise 的
then/catch/finally回调会进入微任务队列,保证执行顺序的确定性; - 可链式调用:
then方法返回新的 Promise,支持无限链式扩展,且可透传数据。
总结
Promise 的核心价值是规范化、结构化地处理异步操作:
- 解决回调地狱,让异步逻辑线性可读;
- 统一状态管理和错误处理,降低异步编程的复杂度;
- 提供异步操作的组合能力(并行、竞速等);
- 作为
async/await的基础,成为现代 JavaScript 异步编程的核心范式。
简述 ES6 Iterator 的作用【重要】?
ES6 Iterator(迭代器)是一种标准化的遍历接口,核心作用是为不同数据结构(如数组、对象、Set、Map 等)提供统一的遍历机制,让原本不具备遍历能力的对象(如普通对象)可被迭代,同时支撑 for...of 循环、解构赋值、扩展运算符等 ES6 新特性的底层实现,是 ES6 统一数据遍历逻辑的核心设计。
核心作用
1. 统一各类数据结构的遍历接口
ES6 之前,不同数据结构的遍历方式杂乱:
- 数组用
for循环、forEach; - 对象用
for...in; - 类数组(如
arguments)需手动转换为数组后遍历。
Iterator 定义了一套通用遍历规则:
- 迭代器对象必须有
next()方法,调用后返回{ value: 遍历值, done: 是否遍历结束 }; - 任何数据结构只要部署了
[Symbol.iterator]方法(返回迭代器对象),就被称为 “可迭代对象”,可通过统一逻辑遍历。
ES6 原生部署 Iterator 的数据结构包括:Array、Set、Map、String、TypedArray、arguments、NodeList,普通对象默认未部署。
示例(手动调用迭代器):
javascript
运行
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator](); // 获取迭代器
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
2. 支撑 for...of 循环的底层实现
for...of 是 ES6 新增的遍历语法,其本质是自动调用可迭代对象的迭代器,循环过程就是不断调用 next() 直到 done: true,让遍历代码更简洁、语义更清晰。
对比传统遍历与 for...of:
javascript
运行
const set = new Set(['a', 'b', 'c']);
// 传统(Set 无索引,无法用 for 循环)
let arr = Array.from(set);
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
// for...of(基于 Iterator,直接遍历)
for (const item of set) {
console.log(item); // a → b → c
}
3. 赋能解构赋值、扩展运算符等新特性
ES6 中依赖 “可迭代” 能力的语法,底层均通过 Iterator 实现:
- 解构赋值:对数组 / Set 等解构时,通过迭代器读取值;
- 扩展运算符:
[...obj]或{...obj}(对象扩展是独立逻辑,但数组 / Set 扩展依赖 Iterator); - Promise.all/race:接收的可迭代参数(如数组)需通过 Iterator 遍历;
- yield*:生成器中
yield* 可迭代对象会自动遍历迭代器。
示例:
javascript
运行
const [a, b] = new Map([['name', '张三'], ['age', 18]]);
console.log(a, b); // name 张三(Map 迭代器返回 [key, value])
const str = 'abc';
console.log([...str]); // ['a', 'b', 'c'](字符串迭代器遍历字符)
4. 让普通对象具备可迭代能力
普通对象默认无 Iterator,但可手动部署 [Symbol.iterator] 方法,自定义遍历逻辑,解决对象无法被 for...of 遍历的问题。
示例(自定义对象迭代器):
javascript
运行
const user = {
name: '张三',
age: 18,
hobbies: ['篮球', '音乐'],
// 部署迭代器,自定义遍历 hobbies
[Symbol.iterator]() {
let index = 0;
const hobbies = this.hobbies;
return {
next() {
if (index < hobbies.length) {
return { value: hobbies[index++], done: false };
}
return { value: undefined, done: true };
}
};
}
};
// 普通对象 now 可被 for...of 遍历
for (const hobby of user) {
console.log(hobby); // 篮球 → 音乐
}
5. 为生成器(Generator)提供底层支撑
Generator 函数(function*)返回的是 “生成器对象”,该对象同时实现了 Iterator 接口,因此生成器可直接作为可迭代对象使用,也是异步迭代(Async Iterator)的基础。
示例:
javascript
运行
function* gen() {
yield 1;
yield 2;
yield 3;
}
// 生成器对象可被 for...of 遍历(底层是 Iterator)
for (const num of gen()) {
console.log(num); // 1 → 2 → 3
}
关键特点
- 单向遍历:迭代器只能向前遍历(
next()无法回退),遍历过程不可逆; - 惰性遍历:只有调用
next()才会返回下一个值,适合处理大数据集(如无限序列); - 接口标准化:无论数据结构如何,迭代器的
next()方法返回格式统一,遍历逻辑无需适配具体结构。
总结
Iterator 的核心价值是定义了 “可迭代” 的统一标准:
- 抹平不同数据结构的遍历差异,让
for...of、解构赋值等语法能跨结构复用; - 允许自定义遍历逻辑,扩展了普通对象的遍历能力;
- 是 ES6 遍历体系、生成器、异步编程(Async Iterator)的底层基石,也是现代 JavaScript 数据遍历的核心接口。
简述 ES6 Generator 的作用?
ES6 Generator(生成器)是一种特殊的函数(声明为 function*),核心作用是打破函数 “一次性执行到底” 的传统流程,实现可暂停、可恢复的分步执行,并通过 yield 关键字实现数据的 “双向传递”(函数内向外输出、外部向函数内输入)。它是 ES6 为解决异步编程复杂度、实现惰性迭代、自定义遍历逻辑而设计的重要特性,也是 async/await 的底层雏形。
核心作用
1. 实现函数的暂停 / 恢复执行(可控的执行流程)
传统函数调用后会从头执行到尾,无法中断;Generator 函数调用后不会立即执行,而是返回一个 “生成器对象”,只有调用其 next() 方法,才会执行到下一个 yield 关键字处暂停,并返回 { value: yield 后的值, done: 是否执行完毕 };再次调用 next() 则从暂停处继续执行,直到下一个 yield 或函数结束。
这种 “暂停 - 恢复” 能力,让函数执行流程完全由外部控制,解决了传统函数执行不可控的问题。
示例:
javascript
运行
function* gen() {
console.log('执行第一步');
yield '第一步结果'; // 暂停,向外输出值
console.log('执行第二步');
yield '第二步结果'; // 再次暂停
console.log('执行结束');
return '最终结果';
}
const g = gen(); // 调用生成器函数,返回生成器对象(未执行函数体)
g.next(); // 执行到第一个 yield,输出:执行第一步 → 返回 { value: '第一步结果', done: false }
g.next(); // 从暂停处继续,输出:执行第二步 → 返回 { value: '第二步结果', done: false }
g.next(); // 继续执行到结束,输出:执行结束 → 返回 { value: '最终结果', done: true }
2. 双向数据传递(函数内外的灵活交互)
Generator 支持外部向暂停的函数内部传递数据:调用 next(参数) 时,参数会作为上一个 yield 表达式的返回值,实现 “外部输入 → 函数内部处理 → 外部输出” 的双向交互,比普通函数的参数传递更灵活。
示例:
javascript
运行
function* gen() {
const a = yield '请输入第一个数'; // yield 向外输出,同时接收 next 传入的参数
const b = yield '请输入第二个数';
return a + b;
}
const g = gen();
console.log(g.next()); // { value: '请输入第一个数', done: false }(无参数,yield 返回 undefined)
console.log(g.next(10)); // { value: '请输入第二个数', done: false }(10 作为第一个 yield 的返回值,赋值给 a)
console.log(g.next(20)); // { value: 30, done: true }(20 作为第二个 yield 的返回值,赋值给 b,最终返回 30)
3. 简化迭代器实现(天然的可迭代对象)
Generator 函数返回的生成器对象内置实现了 Iterator 接口(即拥有 [Symbol.iterator] 方法,且返回自身),因此可直接作为可迭代对象使用,无需手动部署迭代器逻辑,极大简化了自定义遍历器的开发。
对比 “手动写迭代器” 和 “用 Generator 实现迭代器”:
javascript
运行
// 手动实现迭代器(繁琐)
const iterable = {
[Symbol.iterator]() {
let index = 0;
return {
next() {
return index < 3 ? { value: index++, done: false } : { value: undefined, done: true };
}
};
}
};
// Generator 实现迭代器(简洁)
function* genIterator() {
yield 0;
yield 1;
yield 2;
}
const iterable2 = genIterator();
// 两者均可被 for...of 遍历(Generator 更简洁)
for (const num of iterable2) {
console.log(num); // 0 → 1 → 2
}
4. 解决异步编程回调地狱(async/await 的雏形)
ES6 时期,Generator 常与 Promise 结合,将异步操作的回调逻辑转为 “同步风格” 的线性代码,避免回调嵌套(即 “回调地狱”)。这也是 ES7 async/await 的核心设计思路(async/await 本质是 Generator + 自动执行器的语法糖)。
示例(用 Generator 简化异步流程):
javascript
运行
// 模拟异步请求
const fetchData = (url) => {
return new Promise(resolve => {
setTimeout(() => resolve(`数据:${url}`), 1000);
});
};
// Generator 封装异步逻辑(线性写法)
function* genAsync() {
const res1 = yield fetchData('/api/1'); // 暂停,等待异步完成
const res2 = yield fetchData(`/api/2?data=${res1}`);
console.log(res2); // 数据:/api/2?data=数据:/api/1
}
// 自动执行器(简化手动调用 next)
function run(gen) {
const g = gen();
function next(data) {
const result = g.next(data);
if (result.done) return;
result.value.then(res => next(res)); // 异步完成后继续执行
}
next();
}
run(genAsync); // 执行异步逻辑
5. 实现惰性生成 / 无限序列
Generator 的 “惰性执行” 特性(只有调用 next() 才生成下一个值),适合处理大数据集或无限序列(如自然数序列),避免一次性生成所有数据导致的内存占用过高。
示例(生成无限自然数序列):
javascript
运行
function* infiniteNum() {
let num = 0;
while (true) {
yield num++; // 每次调用 next() 才生成下一个数,无内存溢出
}
}
const g = infiniteNum();
console.log(g.next().value); // 0
console.log(g.next().value); // 1
console.log(g.next().value); // 2
// 可无限调用,按需生成值
关键特点
- 特殊声明:函数名前加
*(function* gen() {}),是 Generator 函数的标识; - yield 关键字:暂停执行的 “标记”,也是数据传递的桥梁;
- 生成器对象:调用 Generator 函数返回的对象,兼具迭代器(Iterator)和可暂停执行的特性;
- 不可逆:一旦执行到
done: true,再次调用next()也不会重新执行。
总结
Generator 的核心价值是重塑函数的执行流程:
- 从 “一次性执行” 变为 “可控的分步执行”,实现暂停、恢复和双向数据传递;
- 简化迭代器开发,支持惰性生成数据;
- 作为
async/await的底层基础,是 ES6 解决异步编程复杂度的关键方案,也为自定义遍历、流程控制提供了灵活的范式。
简述 ES6 Class、extends 是什么?有什么作用?
ES6 Class、extends 是什么?
ES6 Class 是 JavaScript 中面向对象编程(OOP)的语法糖,并非新增的 “类” 机制(JS 仍基于原型链),而是对传统 “构造函数 + 原型” 模式的封装,提供了更接近传统面向对象语言(如 Java、Python)的类定义语法;extends 则是 Class 体系中用于实现类继承的关键字,对应传统原型链继承的封装,让继承逻辑更清晰、语义更直观。
简单来说:
Class:把 “构造函数(实例属性)+ 原型方法” 的组合,封装为统一的 “类” 语法;extends:替代手动修改原型链(如Child.prototype.__proto__ = Parent.prototype),实现类之间的继承。
核心作用
一、Class 的核心作用
1. 简化构造函数与原型的定义,语义更清晰
ES5 中实现 “类” 需手动区分构造函数(实例属性)和原型方法,语法零散且不直观;Class 用统一的语法包裹,明确区分 “实例成员” 和 “原型成员”,代码结构更易读。
| ES5 原型模式 | ES6 Class 语法 |
|---|---|
| ```javascript | |
| // 构造函数(实例属性) | |
| function Person(name) { | |
| this.name = name; | |
| } | |
| // 原型方法(共享方法) | |
| Person.prototype.sayHi = function() { | |
console.log(Hi, ${this.name}); | |
| }; | |
| ``` | ```javascript |
| // Class 封装 | |
| class Person { | |
| // 构造方法(对应 ES5 构造函数) | |
| constructor(name) { | |
| this.name = name; // 实例属性 | |
| } | |
| // 原型方法(自动挂载到原型) | |
| sayHi() { | |
console.log(Hi, ${this.name}); | |
| } | |
| } |
|
##### 2. 支持静态成员(static),区分“类方法”和“实例方法”
ES5 中需手动给构造函数添加静态方法(如 `Person.staticMethod = function() {}`),Class 通过 `static` 关键字直接定义静态成员,语义更明确,且静态成员只能通过类调用,不能通过实例调用。
示例:
```javascript
class Person {
constructor(name) {
this.name = name;
}
// 实例方法(原型上)
sayHi() { console.log(this.name); }
// 静态方法(类上)
static create(name) {
return new Person(name); // 静态方法常用于创建实例
}
}
const p = Person.create('张三'); // 静态方法调用
p.sayHi(); // 张三
p.create(); // 报错:实例无 create 方法
3. 支持私有成员(ES2022+),强化封装性
Class 支持通过 # 定义私有属性 / 方法,只能在类内部访问,外部无法修改,解决了 ES5 中 “伪私有属性(如 _name)可被外部篡改” 的问题,强化了面向对象的封装特性。
示例:
javascript
运行
class Person {
#age = 18; // 私有属性
constructor(name) {
this.name = name;
}
getAge() {
return this.#age; // 类内部可访问
}
}
const p = new Person('张三');
console.log(p.getAge()); // 18
console.log(p.#age); // 报错:私有属性不可外部访问
4. 统一 getter/setter 语法
Class 内置对访问器属性(get/set)的支持,无需手动调用 Object.defineProperty,语法更简洁。
示例:
javascript
运行
class Person {
constructor(name) {
this._name = name;
}
// 读取 name 时触发
get name() {
return `【${this._name}】`;
}
// 赋值 name 时触发
set name(val) {
this._name = val.trim();
}
}
const p = new Person('张三');
console.log(p.name); // 【张三】
p.name = ' 李四 ';
console.log(p.name); // 【李四】
二、extends 的核心作用
1. 简化原型链继承,避免手动修改原型的繁琐
ES5 实现继承需手动处理 “原型链指向” 和 “构造函数绑定”,容易出错;extends 一键实现 “子类继承父类的原型方法 + 实例属性”,底层自动处理原型链(Child.prototype.__proto__ = Parent.prototype)和构造函数继承。
对比 ES5 继承与 ES6 extends:
javascript
运行
// ES5 实现继承(繁琐且易出错)
function Student(name, score) {
Person.call(this, name); // 继承实例属性
this.score = score;
}
// 继承原型方法
Student.prototype = Object.create(Person.prototype);
// 修复构造函数指向
Student.prototype.constructor = Student;
// ES6 extends(简洁直观)
class Student extends Person {
constructor(name, score) {
super(name); // 调用父类构造函数,继承实例属性
this.score = score; // 子类自有属性
}
// 子类自有方法
showScore() {
console.log(`${this.name} 分数:${this.score}`);
}
}
2. 支持 super 关键字,灵活访问父类成员
extends 配合 super 可实现:
- 构造方法中
super(...):调用父类构造函数,必须在子类this之前调用; - 原型方法中
super.方法名():调用父类的原型方法; - 静态方法中
super.方法名():调用父类的静态方法。
示例:
javascript
运行
class Parent {
static say() { console.log('父类静态方法'); }
sayHi() { console.log('父类原型方法'); }
}
class Child extends Parent {
static say() {
super.say(); // 调用父类静态方法
console.log('子类静态方法');
}
sayHi() {
super.sayHi(); // 调用父类原型方法
console.log('子类原型方法');
}
}
Child.say(); // 父类静态方法 → 子类静态方法
new Child().sayHi(); // 父类原型方法 → 子类原型方法
3. 支持继承内置对象,扩展原生类型
ES5 难以优雅继承数组、对象等内置对象,extends 可直接继承原生类,实现自定义扩展。
示例(自定义数组):
javascript
运行
class MyArray extends Array {
// 自定义方法:求和
sum() {
return this.reduce((a, b) => a + b, 0);
}
}
const arr = new MyArray(1, 2, 3);
console.log(arr.sum()); // 6
console.log(arr instanceof Array); // true(仍属于数组)
关键特点
- 语法糖本质:Class/extends 未改变 JS “原型链继承” 的底层逻辑,只是简化了语法;
- 严格模式:Class 内部默认启用严格模式(
use strict),避免非严格模式的怪异行为; - 不可提升:Class 与
let/const类似,存在暂时性死区,不能在定义前调用; - 继承单向性:子类只能继承一个父类(JS 无多继承,但可通过 “混入(Mixin)” 模拟)。
总结
Class:封装了 ES5 “构造函数 + 原型” 的模式,提供更直观、语义化的类定义语法,支持静态成员、私有成员、访问器等特性,强化了 JS 面向对象的封装性;extends:替代手动修改原型链的繁琐操作,实现类的继承,配合super关键字可灵活访问父类成员,还能扩展原生内置对象,让继承逻辑更清晰、不易出错。
两者共同让 JS 的面向对象编程更接近传统 OOP 语言的风格,降低了理解和开发成本,是现代 JS 中实现类、继承的主流方式(如 React 组件、Vue 类式组件均基于 Class 实现)。
简述 ES6 module、export、import 的作用?
ES6 Module(ES6 模块)是 JavaScript 官方标准化的模块化方案,用于将大型程序拆分为独立、可复用的文件(模块),解决了 ES5 时代依赖全局变量、IIFE(立即执行函数)等 “伪模块化” 方案的弊端;export 和 import 是 ES6 Module 的核心关键字:
export:用于向外暴露模块内的变量、函数、类等成员,让其他模块可访问;import:用于引入其他模块暴露的成员,实现模块间的依赖引用。
简单来说:ES6 Module 是模块化的 “规范”,export 是 “对外输出”,import 是 “对内引入”,三者共同实现 JS 代码的模块化拆分与复用。
核心作用
一、ES6 Module 的核心作用(整体价值)
1. 实现代码的模块化拆分与复用
将复杂程序按功能拆分为多个独立模块(文件),每个模块专注于单一功能,降低代码耦合度,提升可维护性和复用性。例如:
- 把工具函数封装为
utils.js模块; - 把 API 请求封装为
api.js模块; - 业务代码按需引入这些模块,避免代码冗余。
2. 隔离作用域,解决全局变量污染
ES6 模块拥有独立的顶级作用域(而非全局作用域),模块内定义的变量、函数默认仅在模块内可见,不会污染全局命名空间;只有通过 export 暴露的成员,才能被其他模块访问,彻底解决了 ES5 时代 “全局变量冲突” 的问题。
示例:
javascript
运行
// moduleA.js(模块内作用域隔离)
const num = 10; // 仅模块内可见,全局无法访问
export const fn = () => console.log(num);
3. 支持静态分析,赋能工程化优化
ES6 Module 是静态的(import/export 只能出现在模块顶层,不能嵌套在条件语句等动态逻辑中),这使得编译器 / 打包工具(如 Webpack、Rollup)可在编译阶段做:
- 树摇(Tree Shaking):剔除未使用的导出成员,减小打包体积;
- 按需加载:静态分析依赖关系,实现模块的懒加载;
- 类型检查:如 TypeScript 基于静态导入做类型校验。
4. 统一模块化标准,替代非官方方案
ES5 时代的模块化方案(CommonJS 用于 Node.js、AMD 用于浏览器)存在语法和运行时差异,ES6 Module 是官方统一标准,可跨环境(浏览器 / Node.js)使用(Node.js 需开启 type: module,浏览器需通过 <script type="module"> 加载)。
二、export 的核心作用:向外暴露模块成员
export 负责定义模块的 “对外接口”,有两种核心用法:
1. 命名导出(Named Export)
暴露多个具名成员,可按需导入,适合模块内有多个独立成员的场景。
javascript
运行
// utils.js
export const PI = 3.14; // 导出单个变量
export function sum(a, b) { // 导出单个函数
return a + b;
}
export class Person {} // 导出类
// 也可集中导出(效果同上)
const PI = 3.14;
function sum(a, b) { return a + b; }
export { PI, sum };
2. 默认导出(Default Export)
每个模块只能有一个默认导出,导入时可自定义名称,适合模块只暴露一个核心成员的场景。
javascript
运行
// api.js
export default function fetchData() { // 默认导出函数
return Promise.resolve({ data: 'xxx' });
}
// 也可先定义再默认导出
const fetchData = () => Promise.resolve({ data: 'xxx' });
export default fetchData;
3. 重命名导出
解决成员名称冲突,或简化导出名称。
javascript
运行
// utils.js
const calculateSum = (a, b) => a + b;
export { calculateSum as sum }; // 重命名为 sum 导出
三、import 的核心作用:引入其他模块的成员
import 负责消费其他模块暴露的成员,用法与 export 一一对应:
1. 导入命名导出的成员
按需导入具名成员,名称需与导出时一致(可重命名)。
javascript
运行
// 引入单个/多个命名成员
import { PI, sum } from './utils.js';
// 重命名导入
import { sum as add } from './utils.js';
console.log(PI); // 3.14
console.log(add(1, 2)); // 3
2. 导入默认导出的成员
导入时可自定义名称,无需与导出名称一致。
javascript
运行
// 导入默认成员(自定义名称 fetch)
import fetch from './api.js';
fetch().then(res => console.log(res));
// 同时导入默认成员和命名成员
import fetch, { PI } from './utils.js';
3. 整体导入(命名空间导入)
将模块所有导出成员封装为一个对象,适合批量使用模块成员。
javascript
运行
import * as utils from './utils.js';
console.log(utils.PI); // 3.14
console.log(utils.sum(1, 2)); // 3
4. 仅执行模块(不导入成员)
适合模块执行初始化逻辑(如全局注册组件、绑定事件),无需暴露成员的场景。
javascript
运行
import './init.js'; // 仅执行 init.js 模块内的代码
5. 动态导入(ES2020+)
突破静态导入的限制,支持在条件语句、函数内动态导入模块,返回 Promise,适合懒加载场景(如路由懒加载)。
javascript
运行
// 动态导入,按需加载
if (needFetch) {
import('./api.js').then(({ default: fetchData }) => {
fetchData();
});
}
关键特点
- 静态性:
import/export必须在模块顶层,不能嵌套在if、函数等动态逻辑中(动态导入import()除外); - 单例模式:同一模块被多次导入时,仅执行一次,后续导入复用已执行的模块实例;
- 严格模式:模块内默认启用
use strict; - 路径规范:浏览器中导入需指定完整路径(如
./utils.js),Node.js 中支持省略后缀(需配置)。
总结
ES6 Module:作为官方模块化标准,解决了代码拆分、作用域隔离、全局污染等问题,是现代 JS 工程化的基础;export:模块的 “输出口”,定义对外暴露的成员,支持命名导出、默认导出等方式;import:模块的 “输入口”,引入其他模块的成员,支持按需导入、重命名、动态导入等灵活用法。
简述 ES6 规定 for...in 和 for...of 有什么区别?
for...in 和 for...of 是 ES6 中两种核心的遍历语法,核心区别在于遍历目标、遍历机制、适用场景的本质不同:for...in 是为遍历对象的可枚举属性名设计,基于 “属性遍历”;for...of 是为遍历可迭代对象的元素值设计,基于 Iterator 接口,是 ES6 统一的迭代器遍历语法。
核心区别对比
| 维度 | for...in | for...of |
|---|---|---|
| 遍历目标 | 遍历对象的可枚举属性名(含原型链上的) | 遍历可迭代对象的元素值(数组、Set、Map、字符串等) |
| 底层机制 | 基于对象属性枚举,无迭代器依赖 | 基于 Iterator 接口,调用 next() 取 value |
| 遍历结果 | 返回属性名(字符串类型,数组则是索引) | 返回元素的实际值 |
| 原型链属性 | 会遍历原型链上的可枚举属性 | 仅遍历对象自身的元素,不涉及原型链 |
| 适用数据类型 | 主要用于普通对象,也可遍历数组(不推荐) | 可迭代对象(数组、Set、Map、字符串、Generator、NodeList 等);普通对象默认不支持(需手动部署 Iterator) |
| 中断遍历 | 支持 break/continue/return | 支持 break/continue/return |
| 数组遍历问题 | 索引是字符串(如 "0"),可能遍历非数字索引、原型属性 | 直接遍历数值,无索引类型问题,更适合数组 |
具体说明
1. 遍历目标与结果的本质差异
-
for...in:核心是 “遍历属性”,即使遍历数组,也只是把数组当作 “键为索引的对象”,返回的是索引字符串(而非数值),且会包含数组的非数字属性(如手动添加的
arr.name = 'test')。javascript
运行
const arr = [10, 20, 30]; arr.foo = 'bar'; // 新增非数字属性 for (const key in arr) { console.log(key); // 0, 1, 2, foo(返回属性名/索引字符串) console.log(typeof key); // string(索引是字符串类型) } -
for...of:核心是 “遍历元素值”,基于
Iterator接口,直接返回可迭代对象的元素值,不会遍历非元素属性。javascript
运行
const arr = [10, 20, 30]; arr.foo = 'bar'; for (const value of arr) { console.log(value); // 10, 20, 30(仅返回元素值) }
2. 原型链属性的处理差异
for...in 会遍历原型链上的可枚举属性,容易引入冗余;for...of 仅遍历对象自身的迭代元素,与原型链无关。
javascript
运行
// 给 Array 原型添加可枚举属性
Array.prototype.test = '原型属性';
const arr = [1, 2];
// for...in 遍历到原型属性
for (const key in arr) {
console.log(key); // 0, 1, test
}
// for...of 不受原型影响
for (const value of arr) {
console.log(value); // 1, 2
}
3. 适用场景差异
-
for...in:仅适合遍历普通对象的自有可枚举属性(需配合
Object.hasOwn()过滤原型属性),不推荐用于数组 / 类数组。javascript
运行
const user = { name: '张三', age: 18 }; // 遍历对象自有属性(过滤原型) for (const key in user) { if (Object.hasOwn(user, key)) { console.log(`${key}: ${user[key]}`); // name: 张三, age: 18 } } -
for...of:适合遍历有序的可迭代数据(数组、字符串、Set/Map、Generator、NodeList 等),是 ES6 推荐的数组 / 集合遍历方式。
javascript
运行
// 遍历字符串(按字符) for (const char of 'abc') { console.log(char); // a, b, c } // 遍历 Map(按 [key, value]) const map = new Map([['name', '张三'], ['age', 18]]); for (const [key, value] of map) { console.log(`${key}: ${value}`); }
4. 普通对象的兼容性
普通对象默认未部署 Iterator 接口,无法直接用 for...of 遍历;若需遍历普通对象的属性值,需手动部署 Iterator 或先转为可迭代对象(如 Object.entries())。
javascript
运行
const user = { name: '张三', age: 18 };
// 普通对象直接用 for...of 报错
// for (const value of user) {} // TypeError: user is not iterable
// 转为可迭代对象后遍历
for (const [key, value] of Object.entries(user)) {
console.log(`${key}: ${value}`); // name: 张三, age: 18
}
总结
for...in:面向对象属性的遍历工具,返回属性名,易受原型链影响,仅适合普通对象的自有属性遍历;for...of:面向可迭代对象元素的遍历工具,返回元素值,基于 Iterator 接口,是 ES6 统一的、更合理的数组 / 集合遍历方式,无原型链干扰,语义更清晰。
开发中需遵循:遍历普通对象用 for...in(配合 Object.hasOwn),遍历数组 / Set/Map/ 字符串等用 for...of
简述 ES6 async 函数?
async 函数(异步函数)是 ES2017(ES8)纳入标准的特性(常被归为 ES6 生态扩展),是Generator 函数 + 自动执行器的语法糖,核心基于 Promise 实现,让异步代码以 “同步语法” 的形式编写,彻底解决了回调地狱问题,是目前 JavaScript 异步编程的终极方案。
简单来说:async 函数通过 async 关键字声明,内部用 await 关键字等待 Promise 完成,将异步操作的 “等待 - 执行” 逻辑线性化,无需手动调用 Generator 的 next() 方法,也无需封装自动执行器
核心作用
1. 简化异步代码书写,告别回调地狱
async/await 让异步代码的结构与同步代码几乎一致,无需嵌套回调或 Promise 链式调用(.then()),极大提升代码可读性和可维护性。
对比不同异步写法:
javascript
运行
// 1. 回调地狱(ES5)
fetchData1((res1) => {
fetchData2(res1, (res2) => {
fetchData3(res2, (res3) => console.log(res3));
});
});
// 2. Promise 链式调用(ES6)
fetchData1()
.then(res1 => fetchData2(res1))
.then(res2 => fetchData3(res2))
.then(res3 => console.log(res3));
// 3. async/await(ES8)
async function getData() {
const res1 = await fetchData1(); // 等待异步完成,同步写法
const res2 = await fetchData2(res1);
const res3 = await fetchData3(res2);
console.log(res3);
}
2. 统一异步错误处理逻辑
async 函数支持用 try/catch 捕获所有异步错误(包括 await 后的 Promise 失败、代码执行错误),替代 Promise 的 .catch() 链式捕获,错误处理更集中、更符合同步代码的直觉。
示例:
javascript
运行
async function getData() {
try {
const res = await fetch('/api/data'); // 若 Promise reject 会触发 catch
const data = await res.json();
console.log(data);
} catch (err) {
console.error('请求/解析失败:', err); // 捕获所有异步/同步错误
}
}
3. 明确的返回值规则
async 函数的返回值始终是一个 Promise 对象:
- 若函数内部返回非 Promise 值(如
return 123),会被自动包装为Promise.resolve(123); - 若函数内部抛出错误(如
throw new Error()),返回的 Promise 会变为rejected状态; - 若
await的 Promise 被reject且未被try/catch捕获,整个async函数返回的 Promise 也会reject。
示例:
javascript
运行
async function fn1() {
return 123; // 等效于 return Promise.resolve(123)
}
fn1().then(res => console.log(res)); // 123
async function fn2() {
throw new Error('出错了'); // 等效于 return Promise.reject(new Error('出错了'))
}
fn2().catch(err => console.log(err)); // Error: 出错了
4. 支持并行异步操作
await 会阻塞后续代码执行(串行),但可通过 Promise.all 结合 await 实现异步操作并行执行,兼顾简洁性和性能。
示例(并行请求):
javascript
运行
async function getMultiData() {
// 先发起所有异步请求(并行),再等待全部完成
const [userRes, orderRes] = await Promise.all([
fetch('/api/user'),
fetch('/api/order')
]);
const user = await userRes.json();
const order = await orderRes.json();
return { user, order };
}
5. 与现有 Promise 生态完全兼容
async 函数基于 Promise 实现,可无缝对接所有返回 Promise 的 API(如 Fetch、Axios、Node.js 异步方法),也可作为 Promise 链式调用的一环,兼容性极强。
示例:
javascript
运行
// async 函数返回 Promise,可链式调用
getData()
.then(res => console.log('最终结果:', res))
.catch(err => console.error('全局错误:', err));
关键特点
- 非阻塞:
await仅阻塞async函数内部的代码执行,不会阻塞主线程(底层仍是异步微任务); - 顺序执行:多个
await按书写顺序串行执行(除非用Promise.all并行); - await 限制:
await只能出现在async函数内部(或 ES2022 后的模块顶层),普通函数中使用会报错; - 微任务执行:
async函数中await后的代码会进入微任务队列,执行顺序符合 EventLoop 规则。
总结
async 函数的核心价值是以同步语法实现异步逻辑:
- 彻底简化异步代码的书写和阅读成本,替代回调和复杂的 Promise 链式调用;
- 用
try/catch统一错误处理,符合开发者对同步代码的错误处理直觉; - 基于 Promise 实现,兼容现有异步生态,是目前 JavaScript 异步编程的主流方案(如前端接口请求、Node.js 异步 IO 处理等场景均广泛使用)。
简述开发过程中有哪些值得用 ES6 去改进的编程优化或者规范?
在开发过程中,ES6 提供的新特性不仅能简化代码、提升可读性,还能规范编程范式、降低维护成本。以下是值得用 ES6 改进的核心优化方向和编程规范,覆盖语法、性能、工程化等维度:
一、变量声明:用 let/const 替代 var,规范作用域
优化点:
ES5 的 var 存在函数级作用域、变量提升、可重复声明等问题,易导致变量污染和逻辑错误;let/const 提供块级作用域,更符合直觉。
规范 & 优化:
- 优先用
const声明只读、不重新赋值的变量(如常量、函数、对象 / 数组引用),强制代码不可变,减少意外修改; - 用
let声明需重新赋值的变量,替代var; - 禁止同一作用域内重复声明变量,避免
var的 “重复声明无报错” 陷阱 -
二、函数优化:箭头函数、默认参数、解构入参
1. 箭头函数简化回调,绑定 this
- 替代匿名回调函数,简化语法;
- 箭头函数无自身
this,继承外层作用域的this,解决 ES5 中this指向混乱的问题(如回调中this丢失)。 -
// 不良(ES5) const arr = [1,2,3]; const newArr = arr.map(function(item) { return item * 2; }); const obj = { name: 'test', fn: function() { setTimeout(function() { console.log(this.name); }, 100); // this 指向 window } };
// 优化(ES6) const newArr = arr.map(item => item * 2); const obj = { name: 'test', fn: function() { setTimeout(() => { console.log(this.name); }, 100); // this 继承 obj } };
2. 函数默认参数替代手动赋值
ES6 支持直接在参数列表定义默认值,替代 ES5 中 var a = a || 1 的繁琐写法,且避免 “假值(如 0、'')被误判” 的问题。
javascript
运行
// 不良(ES5)
function fn(a, b) {
a = a || 10;
b = b || 20;
return a + b;
}
fn(0, ''); // 结果 30(0 被误判为 false)
// 优化(ES6)
function fn(a = 10, b = 20) {
return a + b;
}
fn(0, ''); // 结果 0(正确保留假值)
3. 解构入参简化对象 / 数组参数
对多参数场景,用解构提取入参,避免 “参数列表过长” 或 “对象属性多次读取” 的问题,提升可读性。
javascript
运行
// 不良
function getUserInfo(user) {
const name = user.name;
const age = user.age;
console.log(name, age);
}
// 优化
function getUserInfo({ name, age }) {
console.log(name, age);
}
getUserInfo({ name: '张三', age: 18 });
三、数据结构:解构赋值、扩展运算符、Set/Map
1. 解构赋值简化变量提取
替代多次赋值语句,快速提取数组 / 对象的成员,代码更简洁。
javascript
运行
// 不良(ES5)
const arr = [1,2,3];
const a = arr[0];
const b = arr[1];
const user = { name: '张三', age: 18 };
const name = user.name;
const age = user.age;
// 优化(ES6)
const [a, b] = arr;
const { name, age } = user;
// 重命名 + 默认值
const { name: userName, gender = '男' } = user;
2. 扩展运算符简化数组 / 对象操作
替代 concat、slice、Object.assign 等方法,实现数组 / 对象的浅拷贝、合并,语法更直观。
javascript
运行
// 数组操作(ES5 vs ES6)
const arr1 = [1,2];
const arr2 = [3,4];
// 合并
const mergeES5 = arr1.concat(arr2);
const mergeES6 = [...arr1, ...arr2];
// 拷贝
const copyES5 = arr1.slice();
const copyES6 = [...arr1];
// 对象操作
const obj1 = { a: 1 };
const obj2 = { b: 2 };
// 合并
const mergeObj = { ...obj1, ...obj2 };
3. 用 Set/Map 替代数组 / 对象的特殊场景
Set:快速去重、判断元素是否存在(替代indexOf),性能更高;Map:键支持任意类型(替代对象 “字符串键” 限制),可遍历、可获取长度,适合键值对场景。
javascript
运行
// 数组去重(ES5 vs ES6)
const arr = [1,2,2,3];
const uniqueES5 = arr.filter((item, index) => arr.indexOf(item) === index);
const uniqueES6 = [...new Set(arr)];
// 键值对存储(对象 vs Map)
// 不良:对象键只能是字符串,无法直接存对象
const obj = {};
obj[{ id: 1 }] = 'test'; // 键被转为 "[object Object]"
// 优化:Map 键支持任意类型
const map = new Map();
map.set({ id: 1 }, 'test');
map.get({ id: 1 }); // 注意:对象引用不同,需存变量
四、面向对象:Class/extends 替代原型链
用 ES6 Class 封装类,extends 实现继承,替代 ES5 繁琐的原型链操作,语义更清晰,符合传统 OOP 规范。
javascript
运行
// 不良(ES5 原型链)
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() { console.log(this.name); };
function Student(name, score) {
Person.call(this, name);
this.score = score;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
// 优化(ES6 Class)
class Person {
constructor(name) {
this.name = name;
}
sayHi() { console.log(this.name); }
}
class Student extends Person {
constructor(name, score) {
super(name);
this.score = score;
}
}
五、异步编程:async/await 替代回调 / Promise 链式调用
用 async/await 实现 “同步风格” 的异步代码,替代回调地狱和冗长的 .then() 链式调用,错误处理更统一(try/catch)。
javascript
运行
// 不良(Promise 链式调用)
fetch('/api/user')
.then(res => res.json())
.then(user => fetch(`/api/order?uid=${user.id}`))
.then(res => res.json())
.then(order => console.log(order))
.catch(err => console.error(err));
// 优化(async/await)
async function getOrder() {
try {
const userRes = await fetch('/api/user');
const user = await userRes.json();
const orderRes = await fetch(`/api/order?uid=${user.id}`);
const order = await orderRes.json();
console.log(order);
} catch (err) {
console.error(err);
}
}
六、模块化:export/import 替代全局变量 / CommonJS
用 ES6 模块化(export/import)替代全局变量、IIFE 或 CommonJS(require),实现代码隔离、按需加载,适配工程化(Tree Shaking、静态分析)。
javascript
运行
// 不良(全局变量)
// utils.js
window.utils = { sum: (a,b) => a+b };
// index.js
const sum = window.utils.sum;
// 优化(ES6 模块)
// utils.js
export const sum = (a,b) => a+b;
// index.js
import { sum } from './utils.js';
七、字符串与正则:模板字符串、正则扩展
1. 模板字符串替代字符串拼接
支持多行字符串、变量插值,避免 + 拼接的繁琐和引号嵌套错误。
javascript
运行
// 不良(ES5)
const name = '张三';
const str = '姓名:' + name + '\n年龄:' + 18;
// 优化(ES6)
const str = `姓名:${name}
年龄:${18}`;
2. 正则 u 修饰符支持 Unicode,y 修饰符实现粘连匹配
解决 ES5 正则无法正确处理 Unicode 字符(如 emoji、中文)的问题。
javascript
运行
// ES5 无法匹配 4 字节 Unicode
/😀/.test('😀'); // true(但无法正确识别码点)
// ES6 u 修饰符
/^\u{1F600}$/u.test('😀'); // true(正确匹配 Unicode 码点)
八、性能与可读性:迭代器 / 生成器(按需遍历)
用 for...of 替代 for...in 遍历数组 / Set/Map,用 Generator 实现惰性遍历(如大数据集、无限序列),避免一次性加载所有数据。
javascript
运行
// 不良(for...in 遍历数组)
const arr = [1,2,3];
for (const key in arr) {
console.log(arr[key]); // 需通过索引取值,且可能遍历原型属性
}
// 优化(for...of 遍历元素)
for (const item of arr) {
console.log(item);
}
// Generator 惰性生成数据(避免内存溢出)
function* infiniteNum() {
let num = 0;
while(true) yield num++;
}
const gen = infiniteNum();
gen.next().value; // 0(按需生成,无内存压力)
总结:核心优化原则
- 可读性优先:用箭头函数、模板字符串、解构等简化语法,减少冗余;
- 作用域安全:
let/const替代var,避免作用域污染; - 异步规范化:
async/await统一异步逻辑,替代回调 / Promise 链; - 工程化适配:ES6 模块化、静态语法(如
import)适配打包工具优化; - 数据结构合理:Set/Map 替代数组 / 对象的特殊场景,提升性能。
详细简述 ES6 箭头函数?
一、箭头函数的核心定义
箭头函数(Arrow Function)是 ES6 引入的一种简洁的函数声明语法,它通过 => 语法糖简化了普通函数的书写形式,同时具备与普通函数不同的内部特性,主要用于解决普通函数中this指向不明确、回调函数书写繁琐等问题。
二、基本语法(从简洁到完整)
箭头函数的语法灵活多变,可根据参数数量和函数体复杂度简化:
1. 无参数
若函数无参数,需用圆括号 () 占位,后接箭头和函数体:
javascript
运行
// 普通函数
function sayHello() {
return "Hello ES6!";
}
// 箭头函数(完整写法)
const sayHello = () => {
return "Hello ES6!";
};
// 箭头函数(极简写法,单返回值可省略大括号和return)
const sayHello = () => "Hello ES6!";
2. 单个参数
若只有一个参数,可以省略圆括号 () (推荐简洁写法,也可保留圆括号):
javascript
运行
// 普通函数
function double(num) {
return num * 2;
}
// 箭头函数(省略圆括号)
const double = num => num * 2;
// 箭头函数(保留圆括号,语法合法)
const double = (num) => num * 2;
3. 多个参数
若有多个参数,必须使用圆括号 () 包裹参数列表:
javascript
运行
// 普通函数
function sum(a, b, c) {
return a + b + c;
}
// 箭头函数
const sum = (a, b, c) => a + b + c;
// 带函数体大括号的写法(需显式return)
const sum = (a, b, c) => {
const total = a + b + c;
return total; // 大括号包裹函数体时,return不能省略
};
4. 返回对象字面量
若箭头函数直接返回一个对象字面量,必须将对象用圆括号 () 包裹(避免大括号被解析为函数体分隔符):
javascript
运行
// 错误写法(会被解析为函数体大括号,返回undefined)
const createUser = (name, age) => { name: name, age: age };
// 正确写法(用圆括号包裹对象)
const createUser = (name, age) => ({ name: name, age: age });
// 简化写法(对象属性名与变量名一致时)
const createUser = (name, age) => ({ name, age });
三、核心特性(与普通函数的关键差异)
1. 不绑定自身的 this(最核心特性)
箭头函数没有自己的 this 绑定,它的 this 是词法作用域(静态作用域)的,即继承自外层最近的非箭头函数 / 全局作用域的 this,且 this 指向一旦确定,在函数生命周期内不会改变(普通函数的 this 指向随调用方式变化)。
javascript
运行
// 示例1:普通函数 vs 箭头函数的this指向
const person = {
name: "张三",
age: 20,
// 普通方法(this指向person实例)
sayName1: function() {
console.log("普通函数:", this.name); // 张三
},
// 箭头函数方法(this继承外层全局作用域,浏览器中指向window)
sayName2: () => {
console.log("箭头函数:", this.name); // undefined(window无name属性)
},
// 嵌套函数场景(箭头函数的优势)
sayNameAfter1s: function() {
// 普通嵌套函数:this指向window
setTimeout(function() {
console.log("普通嵌套函数:", this.name); // undefined
}, 1000);
// 箭头嵌套函数:this继承外层sayNameAfter1s的this(指向person)
setTimeout(() => {
console.log("箭头嵌套函数:", this.name); // 张三
}, 1000);
}
};
person.sayName1();
person.sayName2();
person.sayNameAfter1s();
2. 不能作为构造函数(无法使用 new 关键字调用)
箭头函数没有 prototype 原型属性,也没有 new.target,无法通过 new 关键字创建实例,否则会抛出错误。
javascript
运行
const Person = (name) => {
this.name = name;
};
// 错误:Person is not a constructor
const p = new Person("李四");
3. 没有 arguments 对象
箭头函数内部不存在 arguments 伪数组(用于获取函数实参列表),若需要获取实参,可使用 ES6 剩余参数(...rest) 替代。
javascript
运行
// 普通函数:有arguments对象
function fn1() {
console.log(arguments); // 伪数组:[1,2,3]
}
fn1(1, 2, 3);
// 箭头函数:无arguments对象,使用剩余参数替代
const fn2 = (...args) => {
// console.log(arguments); // 错误:arguments is not defined
console.log(args); // 真数组:[1,2,3]
};
fn2(1, 2, 3);
4. 不能使用 yield 关键字,无法作为生成器函数
箭头函数无法作为 Generator 函数(生成器函数),不能在其内部使用 yield 关键字(yield 只能用于 Generator 函数,用于暂停和恢复函数执行)。
javascript
运行
// 正确:普通生成器函数
function* gen1() {
yield 1;
yield 2;
}
const g1 = gen1();
console.log(g1.next().value); // 1
// 错误:箭头函数不能作为生成器函数
const gen2 = *() => { // 语法错误
yield 1;
};
5. 没有 super 关键字(无自身 this 导致无 super 绑定)
箭头函数没有自己的 this,因此也没有 super 关键字(super 用于访问父类的属性 / 方法,依赖于当前函数的 this 绑定),其 super 同样继承自外层作用域。
四、适用场景
-
回调函数:尤其适合定时器(
setTimeout/setInterval)、数组方法(map/filter/reduce等)的回调,简化书写并解决this指向问题。javascript
运行
// 数组map方法使用箭头函数 const arr = [1, 2, 3, 4]; const newArr = arr.map(item => item * 3); console.log(newArr); // [3,6,9,12] -
简单逻辑函数:函数体逻辑简单(单返回值、无复杂流程),用箭头函数简化代码。
-
需要固定
this指向的场景:避免普通函数this指向随调用方式变化的问题,无需手动绑定(bind/call/apply)。
五、不适用场景
- 对象的方法:若方法需要访问对象自身的属性(依赖
this指向对象实例),不推荐使用箭头函数。 - 构造函数:需要创建实例时,必须使用普通函数。
- 需要使用
arguments/yield/new的场景:箭头函数不支持这些特性,需使用普通函数。 - 复杂逻辑函数:函数体包含多步流程、条件判断、循环等,使用大括号包裹后,箭头函数的简洁性优势消失,可使用普通函数提高可读性。
总结
- 箭头函数是 ES6 简洁的函数语法,语法可根据参数 / 函数体复杂度灵活简化;
- 核心特性:无自身
this(继承外层作用域)、不可作为构造函数、无arguments、不支持yield; - 优势:简化回调函数书写、固定
this指向,避免手动绑定的繁琐; - 关键区分:与普通函数的核心差异在于
this绑定机制和构造函数能力
解释ES6 includes()、startsWith()、endsWidth()?
| 方法名 | 核心功能 | 第二个参数语义 |
|---|---|---|
includes() | 判断是否包含指定子串 | 查找的起始索引(默认 0) |
startsWith() | 判断是否以指定子串开头 | 判断的起始索引(默认 0) |
endsWith() | 判断是否以指定子串结尾 | 截取的字符串长度(默认原字符串长度) |
简述ES中什么是padStart()、padEnd()?
ES6 为字符串类型新增 padStart() 和 padEnd() 两个字符串填充方法,核心作用是:当目标字符串长度不足指定长度时,在其头部(左侧)或尾部(右侧)填充指定的字符(串),直到达到指定长度;若目标字符串本身长度已大于或等于指定长度,则直接返回原字符串。
这两个方法解决了 ES5 中手动实现字符串补位
padStart() | 补位至目标长度,返回新字符串 | 左侧(头部) |
|---|---|---|
padEnd() | 补位至目标长度,返回新字符串 | 右侧(尾部) |
实际应用场景(高频使用)
这两个方法在日常开发中常用于字符串格式化,是高频实用技巧:
1. 数字 / 编号补零(如订单号、序号、页码)
javascript
运行
// 序号补零:确保序号为3位,不足左侧补0
const orderNo1 = "1";
const orderNo2 = "12";
const orderNo3 = "123";
const orderNo4 = "1234";
console.log(orderNo1.padStart(3, "0")); // "001"
console.log(orderNo2.padStart(3, "0")); // "012"
console.log(orderNo3.padStart(3, "0")); // "123"
console.log(orderNo4.padStart(3, "0")); // "1234"(长度超3,返回原字符串)
2. 时间格式化(时分秒补零,如 09:05:03 而非 9:5:3)
javascript
运行
// 补零工具函数
const formatTimeUnit = (unit) => {
// 确保时间单位为2位,不足左侧补0
return unit.toString().padStart(2, "0");
};
// 格式化时间
const hour = 9;
const minute = 5;
const second = 3;
const formattedTime = `${formatTimeUnit(hour)}:${formatTimeUnit(minute)}:${formatTimeUnit(second)}`;
console.log(formattedTime); // "09:05:03"
3. 字符串对齐(控制台输出、表格展示时的格式美化)
javascript
运行
// 模拟表格数据,左侧/右侧补空格对齐
const data = [
{ name: "张三", score: "98" },
{ name: "李四", score: "87" },
{ name: "王五", score: "100" }
];
console.log("姓名 成绩");
console.log("--------");
data.forEach(item => {
// 姓名左侧补空格(总长度6),成绩右侧补空格(总长度4)
const name = item.name.padStart(6);
const score = item.score.padEnd(4);
console.log(`${name} ${score}`);
});
// 输出效果(格式整齐)
// 姓名 成绩
// --------
// 张三 98
// 李四 87
// 王五 100
总结
padStart()(左侧填充)和padEnd()(右侧填充)是 ES6 字符串补位方法,用于将字符串补至指定目标长度;- 两者语法一致:第一个参数为目标总长度(必填),第二个参数为填充字符(串,可选,默认空格);
- 核心特性:返回新字符串(不修改原字符串),原字符串长度≥目标长度时直接返回原字符串,填充字符超长时自动截取;
- 高频场景:数字补零、时间格式化、字符串对齐,大幅简化格式化逻辑。
简述 ES var、let、const、之间的区别?
| 特性维度 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 完整提升(初始化undefined) | 不完全提升(存在 TDZ) | 不完全提升(存在 TDZ) |
| 重复声明 | 允许 | 禁止 | 禁止 |
| 赋值规则 | 声明可不赋值,后续可重新赋值 | 声明可不赋值,后续可重新赋值 | 声明必须赋值,禁止重新赋值 |
| 变量可变性 | 可变 | 可变 | 引用地址不可变(引用类型内部可变) |
| 全局挂载 | 挂载到window/global | 不挂载到全局对象 | 不挂载到全局对象 |
四、使用场景推荐
- 优先使用
const:当变量的值(或引用地址)不需要修改时(如配置项、固定常量、引用类型初始后仅修改内部数据),优先用const,提高代码可读性和安全性,明确变量不可变的语义。 - 其次使用
let:当变量需要后续重新赋值时(如循环变量、状态切换变量、临时变量),使用let,利用块级作用域避免变量泄露。 - 避免使用
var:var的函数作用域、变量提升、允许重复声明等特性容易引发 bug,在 ES6 及后续项目中,应完全摒弃var的使用。
总结
- 三者核心差异:作用域(函数 / 块级)、变量提升(完整 / 不完全)、重复声明(允许 / 禁止)、赋值规则(灵活 / 严格);
var是 ES5 老旧声明方式,存在缺陷,应避免使用;let适用于需要重新赋值的块级作用域变量;const适用于声明时初始化、后续不重新赋值的常量(引用类型内部可修改);- 最佳实践:
const优先,let次之,禁用var
简述汇总 ES6 中数组新增了那些扩展?
一、 数组创建相关扩展(新增创建方式)
ES6 新增了更灵活、更强大的数组创建方法,弥补了 ES5 Array() 构造函数的缺陷。
1. Array.from():将类数组 / 可迭代对象转为真数组
-
核心功能:把 ** 类数组对象(拥有
length属性的对象、DOM 集合等)和可迭代对象(String、Set、Map等)** 转换为真正的数组(Array实例)。 -
额外能力:支持第二个参数(类似
map方法),用于对转换后的每个元素进行处理;支持第三个参数绑定this。 -
示例:
javascript
运行
// 类数组对象转数组 const likeArr = { 0: "a", 1: "b", length: 2 }; const arr1 = Array.from(likeArr); // ["a", "b"] // 可迭代对象(字符串)转数组 const arr2 = Array.from("hello"); // ["h", "e", "l", "l", "o"] // 带第二个参数(元素处理) const arr3 = Array.from([1, 2, 3], item => item * 2); // [2, 4, 6]
2. Array.of():创建指定元素的数组
-
核心功能:接收任意数量的参数,返回一个包含这些参数的数组,解决了
Array()构造函数传入单个数字时创建空数组的缺陷。 -
对比
Array():javascript
运行
// Array() 缺陷:单个数字参数表示数组长度 console.log(new Array(3)); // [empty × 3](长度为3的空数组) // Array.of():直接创建包含指定元素的数组 console.log(Array.of(3)); // [3] console.log(Array.of(1, 2, 3)); // [1, 2, 3] console.log(Array.of()); // [](无参数返回空数组)
二、 数组实例方法扩展(数组对象的新增方法)
ES6 为数组实例新增了多个实用方法,涵盖查找、填充、遍历等场景,提升开发效率。
1. 查找相关:find() + findIndex()
两者均为按条件查找元素,遍历数组直到找到符合条件的元素,停止遍历(性能更优),支持第二个参数绑定 this。
-
find():返回第一个符合条件的元素本身,若未找到返回undefined。 -
findIndex():返回第一个符合条件的元素索引,若未找到返回-1(优于indexOf(),支持复杂条件判断)。 -
示例:
javascript
运行
const arr = [10, 20, 30, 40, 50]; // find() 查找元素 const targetItem = arr.find(item => item > 25); // 30 // findIndex() 查找索引 const targetIndex = arr.findIndex(item => item > 25); // 2
2. 填充相关:fill()
-
核心功能:用指定的值填充数组的指定范围,会修改原数组(注意:若填充引用类型,所有位置共享同一个引用)。
-
语法:
arr.fill(value, start?, end?)(start起始索引,默认 0;end结束索引,默认数组长度,不包含 end 位置)。 -
示例:
javascript
运行
const arr = [1, 2, 3, 4, 5]; // 全部填充 arr.fill(0); // [0, 0, 0, 0, 0](修改原数组) // 指定范围填充 const arr2 = [1, 2, 3, 4, 5]; arr2.fill(9, 1, 3); // [1, 9, 9, 4, 5](填充索引1到2)
3. 遍历相关:entries()、keys()、values()
三者均返回迭代器对象,用于遍历数组,适配 for...of 循环,分别遍历「键值对」、「索引」、「元素值」。
-
keys():遍历数组的索引(键名) 。 -
values():遍历数组的元素值(键值) 。 -
entries():遍历数组的 **[索引,元素值] 键值对 **。 -
示例:
javascript
运行
const arr = ["a", "b", "c"]; // 遍历索引 for (const key of arr.keys()) { console.log(key); // 0, 1, 2 } // 遍历元素值 for (const value of arr.values()) { console.log(value); // "a", "b", "c" } // 遍历索引+元素值 for (const [key, value] of arr.entries()) { console.log(key, value); // 0 "a"、1 "b"、2 "c" }
4. 包含判断:includes()
-
核心功能:判断数组是否包含指定元素,返回布尔值(
true/false),区分大小写,解决了indexOf()需判断-1的繁琐问题。 -
优势:支持判断
NaN(indexOf()无法识别NaN)。 -
示例:
javascript
运行
const arr = [1, 2, NaN, 3]; console.log(arr.includes(2)); // true console.log(arr.includes(NaN)); // true(支持NaN判断) console.log(arr.indexOf(NaN)); // -1(无法识别NaN)
三、 解构赋值与扩展运算符(数组操作扩展)
ES6 新增的解构赋值和扩展运算符,大幅简化了数组的赋值、拷贝、合并等操作。
1. 数组解构赋值
-
核心功能:按照对应位置,将数组的元素快速赋值给多个变量,支持默认值、剩余参数。
-
示例:
javascript
运行
const [a, b, c] = [1, 2, 3]; // a=1, b=2, c=3 const [x, , y] = [4, 5, 6]; // 跳过第二个元素,x=4, y=6 const [m, n = 0] = [7]; // 默认值:m=7, n=0 const [first, ...rest] = [1, 2, 3, 4]; // 剩余参数:first=1, rest=[2,3,4]
2. 数组扩展运算符(...)
-
核心功能:将数组 “展开” 为单个元素序列,支持数组拷贝(浅拷贝)、合并、作为函数参数传递等场景。
-
示例:
javascript
运行
// 数组浅拷贝 const arr1 = [1, 2, 3]; const arr2 = [...arr1]; // [1,2,3](修改arr2不影响arr1,浅拷贝) // 数组合并(优于concat(),更简洁) const arr3 = [4, 5]; const arr4 = [...arr1, ...arr3]; // [1,2,3,4,5] // 作为函数参数(优于apply()) function sum(a, b, c) { return a + b + c; } sum(...arr1); // 6(等价于sum(1,2,3))
四、 核心扩展汇总表
| 扩展类型 | 具体内容 | 核心作用 |
|---|---|---|
| 数组创建 | Array.from() | 类数组 / 可迭代对象转真数组 |
Array.of() | 快速创建包含指定元素的数组 | |
| 实例方法 | find() | 查找第一个符合条件的元素本身 |
findIndex() | 查找第一个符合条件的元素索引 | |
fill() | 用指定值填充数组指定范围(修改原数组) | |
entries()/keys()/values() | 遍历数组的键值对 / 索引 / 元素值(返回迭代器) | |
includes() | 判断数组是否包含指定元素(返回布尔值,支持 NaN) | |
| 操作扩展 | 数组解构赋值 | 快速批量赋值数组元素 |
扩展运算符(...) | 数组展开、浅拷贝、合并、作为函数参数 |
总结
- ES6 数组扩展主要分为「创建方法」「实例方法」「操作语法」三大类,大幅提升数组操作的便捷性;
- 创建类:
Array.from()解决类数组转数组问题,Array.of()修复Array()构造函数缺陷; - 实例方法类:
find()/findIndex()实现条件查找,fill()实现数组填充,entries()等实现灵活遍历,includes()简化包含判断; - 操作语法类:解构赋值简化批量赋值,扩展运算符简化拷贝、合并等操作,是日常开发高频使用技巧
简述汇总 ES7 对象新增了那些扩展?
一、 核心新增特性(ES7 对象扩展核心)
1. Object.values():获取对象的所有属性值组成的数组
核心功能
遍历对象自身的可枚举属性(不包含继承的属性、原型链上的属性),返回一个包含所有属性值的数组,数组中元素的顺序与 for...in 循环遍历的顺序一致(按属性定义顺序排列,数字属性优先按数值升序排列)。
语法
javascript
运行
Object.values(obj)
obj:必填,需要获取属性值的目标对象(若传入非对象类型,会先自动转换为对象)。
使用示例
javascript
运行
// 普通对象
const person = { name: "张三", age: 20, gender: "男" };
const values = Object.values(person);
console.log(values); // ["张三", 20, "男"]
// 带有数字属性的对象(数字属性按数值升序排列)
const obj1 = { 3: "c", 1: "a", 2: "b" };
console.log(Object.values(obj1)); // ["a", "b", "c"]
// 非对象参数自动转换
console.log(Object.values("hello")); // ["h", "e", "l", "l", "o"](字符串转对象后取字符值)
console.log(Object.values(123)); // [](数字转对象后无枚举属性)
2. Object.entries():获取对象的键值对组成的二维数组
核心功能
遍历对象自身的可枚举属性,返回一个二维数组,数组中的每个元素是 [属性名, 属性值] 的键值对数组,顺序与 Object.values() 一致。
核心价值
- 实现对象的「可迭代遍历」,适配
for...of循环; - 方便将普通对象转换为
Map实例(Map支持以键值对数组为参数初始化);
语法
javascript
运行
Object.entries(obj)
obj:必填,目标对象(非对象类型会自动转换为对象)。
使用示例
javascript
运行
// 普通对象
const person = { name: "张三", age: 20, gender: "男" };
const entries = Object.entries(person);
console.log(entries); // [["name", "张三"], ["age", 20], ["gender", "男"]]
// 1. 用 for...of 遍历对象
for (const [key, value] of Object.entries(person)) {
console.log(`${key}: ${value}`); // name: 张三、age: 20、gender: 男
}
// 2. 快速转换为 Map 实例
const personMap = new Map(Object.entries(person));
console.log(personMap.get("name")); // 张三
console.log(personMap.get("age")); // 20
// 非对象参数自动转换
console.log(Object.entries("hello")); // [["0","h"], ["1","e"], ["2","l"], ["3","l"], ["4","o"]]
console.log(Object.entries(123)); // []
二、 补充:ES7 对象属性相关语法优化
除了上述两个静态方法,ES7 还新增了对象属性的幂运算简写(指数运算符) ,虽非对象本身的结构扩展,但常用于对象属性赋值场景,一并汇总:
指数运算符 ** 与赋值运算符 **=
核心功能
替代 ES5 中的 Math.pow() 方法,实现更简洁的幂运算,支持赋值简写,常用于对象属性的数值计算。
示例
javascript
运行
const obj = {
a: 2 ** 3, // 等价于 Math.pow(2,3),结果为8
b: 5
};
obj.b **= 2; // 等价于 obj.b = Math.pow(obj.b, 2),结果为25
console.log(obj); // { a: 8, b: 25 }
三、 关键特性总结
1. 共性特点
- 均针对对象的「可枚举属性」操作,不包含继承属性、原型属性(如
toString、valueOf等内置不可枚举属性); - 支持非对象参数自动转换(字符串、布尔值等),数字、
null、undefined转换后无有效枚举属性时返回空数组; - 不修改原始对象,仅返回新的数组结果;
- 元素顺序:字符串属性按定义顺序排列,数字属性按数值升序排列(优先于字符串属性)。
2. 与 ES6 Object.keys() 的对比
ES6 新增的 Object.keys() 与 ES7 的 Object.values()、Object.entries() 为配套方法,三者互补:
| 方法名 | 返回值类型 | 核心作用 |
|---|---|---|
Object.keys() | 一维字符串数组 | 获取对象所有可枚举属性名 |
Object.values() | 一维数组(属性值类型不限) | 获取对象所有可枚举属性值 |
Object.entries() | 二维数组(键值对) | 获取对象所有可枚举键值对 |
配套使用示例
javascript
运行
const person = { name: "张三", age: 20, gender: "男" };
const keys = Object.keys(person);
const values = Object.values(person);
const entries = Object.entries(person);
console.log(keys); // ["name", "age", "gender"]
console.log(values); // ["张三", 20, "男"]
console.log(entries); // [["name","张三"], ["age",20], ["gender","男"]]
3. 实用场景
- 对象遍历:替代
for...in循环(无需额外判断hasOwnProperty),更简洁安全; - 对象转 Map:通过
Object.entries()快速实现,充分利用 Map 的键类型灵活特性; - 数据格式化:将对象数据转换为数组格式,方便结合数组方法(
map、filter等)进行处理; - 批量操作属性:通过获取属性名 / 值数组,实现对象属性的批量修改、筛选。
四、 核心汇总
- ES7 对象的核心新增扩展为
Object.values()和Object.entries()两个静态方法,补充**幂运算简写; Object.values()返回对象属性值数组,Object.entries()返回键值对二维数组,均操作可枚举自身属性;- 两者与 ES6
Object.keys()配套使用,大幅简化对象遍历和转换操作; - 核心实用场景:对象安全遍历、快速转 Map、数据格式化,是日常开发高频技巧
简述你对 ES6 中新增的 Set、Map 两种数据结构的理解?
一、 核心新增特性(ES7 对象扩展核心)
1. Object.values():获取对象的所有属性值组成的数组
核心功能
遍历对象自身的可枚举属性(不包含继承的属性、原型链上的属性),返回一个包含所有属性值的数组,数组中元素的顺序与 for...in 循环遍历的顺序一致(按属性定义顺序排列,数字属性优先按数值升序排列)。
语法
javascript
运行
Object.values(obj)
obj:必填,需要获取属性值的目标对象(若传入非对象类型,会先自动转换为对象)。
使用示例
javascript
运行
// 普通对象
const person = { name: "张三", age: 20, gender: "男" };
const values = Object.values(person);
console.log(values); // ["张三", 20, "男"]
// 带有数字属性的对象(数字属性按数值升序排列)
const obj1 = { 3: "c", 1: "a", 2: "b" };
console.log(Object.values(obj1)); // ["a", "b", "c"]
// 非对象参数自动转换
console.log(Object.values("hello")); // ["h", "e", "l", "l", "o"](字符串转对象后取字符值)
console.log(Object.values(123)); // [](数字转对象后无枚举属性)
2. Object.entries():获取对象的键值对组成的二维数组
核心功能
遍历对象自身的可枚举属性,返回一个二维数组,数组中的每个元素是 [属性名, 属性值] 的键值对数组,顺序与 Object.values() 一致。
核心价值
- 实现对象的「可迭代遍历」,适配
for...of循环; - 方便将普通对象转换为
Map实例(Map支持以键值对数组为参数初始化);
语法
javascript
运行
Object.entries(obj)
obj:必填,目标对象(非对象类型会自动转换为对象)。
使用示例
javascript
运行
// 普通对象
const person = { name: "张三", age: 20, gender: "男" };
const entries = Object.entries(person);
console.log(entries); // [["name", "张三"], ["age", 20], ["gender", "男"]]
// 1. 用 for...of 遍历对象
for (const [key, value] of Object.entries(person)) {
console.log(`${key}: ${value}`); // name: 张三、age: 20、gender: 男
}
// 2. 快速转换为 Map 实例
const personMap = new Map(Object.entries(person));
console.log(personMap.get("name")); // 张三
console.log(personMap.get("age")); // 20
// 非对象参数自动转换
console.log(Object.entries("hello")); // [["0","h"], ["1","e"], ["2","l"], ["3","l"], ["4","o"]]
console.log(Object.entries(123)); // []
二、 补充:ES7 对象属性相关语法优化
除了上述两个静态方法,ES7 还新增了对象属性的幂运算简写(指数运算符) ,虽非对象本身的结构扩展,但常用于对象属性赋值场景,一并汇总:
指数运算符 ** 与赋值运算符 **=
核心功能
替代 ES5 中的 Math.pow() 方法,实现更简洁的幂运算,支持赋值简写,常用于对象属性的数值计算。
示例
javascript
运行
const obj = {
a: 2 ** 3, // 等价于 Math.pow(2,3),结果为8
b: 5
};
obj.b **= 2; // 等价于 obj.b = Math.pow(obj.b, 2),结果为25
console.log(obj); // { a: 8, b: 25 }
三、 关键特性总结
1. 共性特点
- 均针对对象的「可枚举属性」操作,不包含继承属性、原型属性(如
toString、valueOf等内置不可枚举属性); - 支持非对象参数自动转换(字符串、布尔值等),数字、
null、undefined转换后无有效枚举属性时返回空数组; - 不修改原始对象,仅返回新的数组结果;
- 元素顺序:字符串属性按定义顺序排列,数字属性按数值升序排列(优先于字符串属性)。
2. 与 ES6 Object.keys() 的对比
ES6 新增的 Object.keys() 与 ES7 的 Object.values()、Object.entries() 为配套方法,三者互补:
| 方法名 | 返回值类型 | 核心作用 |
|---|---|---|
Object.keys() | 一维字符串数组 | 获取对象所有可枚举属性名 |
Object.values() | 一维数组(属性值类型不限) | 获取对象所有可枚举属性值 |
Object.entries() | 二维数组(键值对) | 获取对象所有可枚举键值对 |
配套使用示例
javascript
运行
const person = { name: "张三", age: 20, gender: "男" };
const keys = Object.keys(person);
const values = Object.values(person);
const entries = Object.entries(person);
console.log(keys); // ["name", "age", "gender"]
console.log(values); // ["张三", 20, "男"]
console.log(entries); // [["name","张三"], ["age",20], ["gender","男"]]
3. 实用场景
- 对象遍历:替代
for...in循环(无需额外判断hasOwnProperty),更简洁安全; - 对象转 Map:通过
Object.entries()快速实现,充分利用 Map 的键类型灵活特性; - 数据格式化:将对象数据转换为数组格式,方便结合数组方法(
map、filter等)进行处理; - 批量操作属性:通过获取属性名 / 值数组,实现对象属性的批量修改、筛选。
四、 核心汇总
- ES7 对象的核心新增扩展为
Object.values()和Object.entries()两个静态方法,补充**幂运算简写; Object.values()返回对象属性值数组,Object.entries()返回键值对二维数组,均操作可枚举自身属性;- 两者与 ES6
Object.keys()配套使用,大幅简化对象遍历和转换操作; - 核心实用场景:对象安全遍历、快速转 Map、数据格式化,是日常开发高频技巧。
ES6中新增的Set、Map` 两种数据结构的理解
ES6 Set、Map 数据结构全面解析
你希望深入理解 ES6 新增的 Set 和 Map 两种数据结构,我会从核心定义、特性、常用方法、使用示例及核心差异等方面进行系统梳理,帮你厘清其本质和使用场景。
一、 Set 数据结构
1. 核心定义
Set 是 ES6 新增的有序集合(无重复值)数据结构,它类似于数组,但成员的值都是唯一的,不允许重复(内部使用 SameValueZero 算法判断值是否相等,能正确识别 NaN,解决了数组中 NaN 无法被 indexOf 识别的问题,且 +0 和 -0 视为同一个值)。
2. 核心特性
- 唯一性:自动去重,添加重复值时不会报错,仅忽略该重复值;
- 有序性:成员按添加顺序排列,支持按顺序遍历;
- 无索引:无法通过索引(如
set[0])访问成员,需通过遍历或特定方法获取; - 可迭代:支持
for...of循环和迭代器方法(keys()、values()、entries()); - 成员类型灵活:可以存储任意类型的值(原始类型 + 引用类型),引用类型仅判断内存地址是否一致(内存地址不同则视为不同成员)。
3. 常用方法与属性
| 分类 | 名称 | 核心功能 |
|---|---|---|
| 核心属性 | size | 返回 Set 实例的成员总数(类似数组的 length) |
| 添加成员 | add(value) | 向 Set 中添加指定值,返回 Set 实例本身(支持链式调用) |
| 删除成员 | delete(value) | 删除指定值,返回布尔值(true 表示删除成功,false 表示不存在该值) |
| 查找成员 | has(value) | 判断是否包含指定值,返回布尔值(优于数组 indexOf,支持 NaN 判断) |
| 清空成员 | clear() | 清空 Set 中所有成员,无返回值 |
| 遍历方法 | keys() | 返回迭代器对象,遍历所有成员的值(与 values() 功能一致,因 Set 无键名) |
| 遍历方法 | values() | 返回迭代器对象,遍历所有成员的值(默认迭代器) |
| 遍历方法 | entries() | 返回迭代器对象,遍历 [value, value] 键值对(保持与 Map 接口一致) |
| 遍历方法 | forEach() | 按添加顺序遍历所有成员,支持第二个参数绑定 this |
4. 使用示例
javascript
运行
// 1. 创建 Set 实例(可传入数组/可迭代对象初始化,自动去重)
const s1 = new Set(); // 空 Set
const s2 = new Set([1, 2, 2, 3, NaN, NaN]); // 自动去重:Set(4) {1, 2, 3, NaN}
console.log(s2.size); // 4
// 2. 新增成员(链式调用)
s1.add(1).add(2).add(3);
console.log(s1); // Set(3) {1, 2, 3}
// 3. 判断是否包含成员
console.log(s2.has(2)); // true
console.log(s2.has(NaN)); // true(支持 NaN 判断)
console.log(s2.has(4)); // false
// 4. 删除成员
console.log(s2.delete(3)); // true
console.log(s2); // Set(3) {1, 2, NaN}
console.log(s2.delete(4)); // false
// 5. 遍历成员
// for...of 直接遍历(默认遍历 values())
for (const val of s2) {
console.log(val); // 1、2、NaN
}
// forEach 遍历
s2.forEach((val, key) => {
console.log(val, key); // 1 1、2 2、NaN NaN(key 和 val 一致)
});
// 6. 清空成员
s2.clear();
console.log(s2.size); // 0
// 7. 常用场景:数组去重
const arr = [1, 2, 2, 3, 3, 3];
const uniqueArr = [...new Set(arr)]; // [1, 2, 3]
二、 Map 数据结构
1. 核心定义
Map 是 ES6 新增的有序键值对集合数据结构,它类似于普通对象({ key: value }),但解决了普通对象的诸多缺陷,核心优势是键的类型不受限制(可支持任意类型作为键)。
2. 核心特性
- 键类型灵活:键可以是任意类型(原始类型:字符串、数字、布尔值、
NaN等;引用类型:对象、数组、Set等),而普通对象仅支持字符串 /Symbol作为键; - 有序性:成员按键的添加顺序排列,支持按顺序遍历(普通对象在 ES6 前不保证属性顺序);
- 无键名冲突:自定义键不会覆盖内置属性(如
toString、hasOwnProperty等),普通对象存在该问题; - 可迭代:支持
for...of循环和迭代器方法,遍历便捷; - 键的唯一性:使用
SameValueZero算法判断键是否相等,NaN视为同一个键,+0和-0视为同一个键; - 引用类型键:以引用类型作为键时,仅判断内存地址是否一致(内存地址不同则视为不同键)。
3. 常用方法与属性
| 分类 | 名称 | 核心功能 |
|---|---|---|
| 核心属性 | size | 返回 Map 实例的键值对总数 |
| 添加键值对 | set(key, value) | 向 Map 中添加指定键值对,返回 Map 实例本身(支持链式调用,已存在的键会覆盖旧值) |
| 获取值 | get(key) | 根据指定键获取对应值,若键不存在返回 undefined |
| 删除键值对 | delete(key) | 删除指定键对应的键值对,返回布尔值(true 表示删除成功,false 表示不存在该键) |
| 查找键 | has(key) | 判断是否包含指定键,返回布尔值 |
| 清空键值对 | clear() | 清空 Map 中所有键值对,无返回值 |
| 遍历方法 | keys() | 返回迭代器对象,遍历所有键 |
| 遍历方法 | values() | 返回迭代器对象,遍历所有值 |
| 遍历方法 | entries() | 返回迭代器对象,遍历 [key, value] 键值对(默认迭代器) |
| 遍历方法 | forEach() | 按添加顺序遍历所有键值对,支持第二个参数绑定 this |
4. 使用示例
javascript
运行
// 1. 创建 Map 实例(可传入二维数组/可迭代对象初始化)
const m1 = new Map(); // 空 Map
const m2 = new Map([
["name", "张三"],
["age", 20],
[NaN, "测试值"],
[{ id: 1 }, "用户信息"] // 引用类型作为键
]);
console.log(m2.size); // 4
// 2. 添加键值对(链式调用 + 覆盖已有键)
m1.set("gender", "男")
.set(123, "数字键")
.set(NaN, "NaN键")
.set("name", "李四"); // 覆盖已存在的 "name" 键
console.log(m1); // Map(4) {"gender" => "男", 123 => "数字键", NaN => "NaN键", "name" => "李四"}
// 3. 获取值
console.log(m2.get("name")); // 张三
console.log(m2.get(NaN)); // 测试值
console.log(m2.get({ id: 1 })); // undefined(引用类型内存地址不同,视为不同键)
console.log(m1.get("gender")); // 男
// 4. 判断是否包含指定键
console.log(m2.has("age")); // true
console.log(m2.has({ id: 1 })); // false
console.log(m1.has(123)); // true
// 5. 删除键值对
console.log(m2.delete("name")); // true
console.log(m2); // Map(3) {"age" => 20, NaN => "测试值", {id:1} => "用户信息"}
console.log(m2.delete("xxx")); // false
// 6. 遍历键值对
// for...of 遍历(默认遍历 entries())
for (const [key, value] of m2) {
console.log(key, value); // age 20、NaN 测试值、{id:1} 用户信息
}
// forEach 遍历
m2.forEach((value, key) => {
console.log(`键:${key},值:${value}`);
});
// 7. 清空 Map
m2.clear();
console.log(m2.size); // 0
三、 Set 与 Map 的核心差异
| 对比维度 | Set | Map |
|---|---|---|
| 核心本质 | 有序无重复值的集合 | 有序键值对的集合 |
| 存储形式 | 仅存储「值」(无键名) | 存储「键 - 值」对(键值关联) |
| 核心特性 | 自动去重 | 键类型灵活、键值一一对应 |
| 常用属性 | size(成员总数) | size(键值对总数) |
| 新增方法 | add(value) | set(key, value) |
| 获取成员 / 值 | 无直接获取方法(需遍历) | get(key)(按键取值) |
| 核心使用场景 | 数组去重、数据去重、存储唯一值集合 | 存储键值关联数据、非字符串键的映射关系、需要保持插入顺序的键值对 |
四、 与普通数组 / 对象的对比优势
1. Set vs 普通数组
| 特性 | Set | 普通数组 |
|---|---|---|
| 唯一性 | 自动去重,无需手动处理 | 存在重复值,需手动去重(如 filter + indexOf) |
| NaN 判断 | 支持 has(NaN) 准确判断 | indexOf(NaN) 返回 -1,无法识别 |
| 去重效率 | 效率更高(内部哈希实现) | 效率较低(需遍历对比) |
| 增删查操作 | 方法简洁(add/delete/has) | 增删查繁琐(push/splice/indexOf) |
2. Map vs 普通对象
| 特性 | Map | 普通对象 |
|---|---|---|
| 键类型 | 支持任意类型(原始类型 + 引用类型) | 仅支持字符串 /Symbol 类型 |
| 顺序性 | 保持插入顺序,支持有序遍历 | ES6 前不保证属性顺序,遍历不便捷 |
| 成员总数 | size 属性直接获取 | 需手动遍历统计(Object.keys(obj).length) |
| 内置属性冲突 | 无冲突(自定义键不覆盖内置方法) | 可能覆盖内置属性(如 obj.toString = 123) |
| 迭代器支持 | 原生支持 for...of 遍历 | 需先转为数组(Object.entries(obj))再遍历 |
| 键值对增删效率 | 频繁增删时效率更高 | 频繁增删时效率较低 |
五、 核心总结
- Set:有序无重复值集合,核心价值是「自动去重」,适用于存储唯一值(如数组去重、标签集合等),无索引,需通过遍历或
has/delete操作成员; - Map:有序键值对集合,核心优势是「键类型灵活」和「保持插入顺序」,适用于存储键值关联数据(如非字符串键的映射、需要有序遍历的配置项等),通过
set/get操作键值对; - 两者均支持迭代器和便捷遍历,解决了普通数组 / 对象的诸多缺陷,是 ES6 开发中提升数据处理效率的重要工具;
- 核心区分:仅需存储唯一值用
Set,需要键值关联映射用Map
如何理解 ES6 中的 Promise?
一、 Promise 核心定义
Promise 是 ES6 新增的异步编程解决方案,它是一个封装了异步操作结果的对象,用于表示一个异步操作的最终完成(或失败)及其返回的结果。
简单来说:Promise 就像一个 “异步操作的容器”,它把原本嵌套混乱的回调函数(回调地狱),转换成了链式调用的形式,让异步代码的逻辑更清晰、可读性更强、更易于维护。
二、 Promise 解决的核心问题
在 ES6 之前,异步编程主要依赖回调函数实现(如定时器、AJAX 请求、文件读取等),存在明显缺陷,Promise 正是为解决这些问题而生:
-
解决回调地狱(Callback Hell) :多层异步操作嵌套时,回调函数会层层嵌套,代码缩进越来越深,可读性极差、难以调试和维护。
Promise通过链式调用扁平化代码结构,彻底摆脱嵌套陷阱。javascript
运行
// 回调地狱(多层嵌套,可读性差) setTimeout(() => { console.log("第一步异步操作"); setTimeout(() => { console.log("第二步异步操作"); setTimeout(() => { console.log("第三步异步操作"); }, 1000); }, 1000); }, 1000); // Promise 链式调用(扁平化,可读性强) new Promise((resolve) => { setTimeout(() => { console.log("第一步异步操作"); resolve(); }, 1000); }).then(() => { return new Promise((resolve) => { setTimeout(() => { console.log("第二步异步操作"); resolve(); }, 1000); }); }).then(() => { setTimeout(() => { console.log("第三步异步操作"); }, 1000); }); -
统一异步操作的处理规范:不同异步操作(AJAX、定时器、文件操作)的回调格式不统一,
Promise提供了标准化的 API(then/catch/finally),让异步操作的成功处理、失败捕获、最终收尾有了统一的写法。 -
解决回调函数的信任问题:普通回调函数可能被多次调用(成功 / 失败回调同时执行),
Promise的状态一旦改变就无法逆转,确保异步操作的结果只会被处理一次。
三、 Promise 的核心特性
1. 三种不可逆转的状态
Promise 实例存在三种状态,状态的转换是单向的、不可逆的,只有两种合法的转换路径,不存在其他转换可能:
| 状态名称 | 含义 | 能否改变 |
|---|---|---|
pending(待定) | 初始状态,异步操作未完成 | 可以改变 |
fulfilled(已成功) | 异步操作成功完成 | 不可改变 |
rejected(已失败) | 异步操作执行失败 | 不可改变 |
合法状态转换:
pending→fulfilled(异步操作成功,调用resolve函数触发)pending→rejected(异步操作失败,调用reject函数触发)
一旦状态从 pending 转换为 fulfilled 或 rejected,就会永久保持该状态,无法再切换到其他状态。
2. 状态的唯一性(一旦改变,不可逆转)
这是 Promise 的核心特性之一,确保异步操作的结果只会被处理一次。例如:异步操作成功后,resolve 函数调用后,再调用 reject 函数不会产生任何效果,状态已固定为 fulfilled。
javascript
运行
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("异步操作成功"); // 状态转为 fulfilled
reject("异步操作失败"); // 无效,状态已不可逆,不会触发失败处理
}, 1000);
});
p.then((res) => {
console.log(res); // 输出:异步操作成功
}).catch((err) => {
console.log(err); // 不会执行
});
3. 保存异步操作的结果
Promise 实例会保存异步操作的结果:
- 当状态转为
fulfilled时,会保存成功的结果值(通过resolve(value)传入); - 当状态转为
rejected时,会保存失败的原因(通过reject(reason)传入); - 这些结果会被传递给对应的
then或catch方法进行处理。
四、 Promise 的基本使用(实例创建 + 结果处理)
1. 创建 Promise 实例
通过 new Promise() 构造函数创建实例,构造函数接收一个「执行器函数」作为参数,执行器函数立即执行,内部封装异步操作,同时接收两个内置参数 resolve 和 reject(均为函数):
resolve(value):异步操作成功时调用,将状态转为fulfilled,并传递成功结果value;reject(reason):异步操作失败时调用,将状态转为rejected,并传递失败原因reason。
javascript
运行
// 基本创建语法
const p = new Promise((resolve, reject) => {
// 封装异步操作(示例:模拟AJAX请求/定时器)
const isSuccess = true; // 模拟异步操作是否成功
setTimeout(() => {
if (isSuccess) {
// 成功:传递结果值
resolve("异步操作执行成功,返回数据");
} else {
// 失败:传递失败原因
reject("异步操作执行失败,报错信息");
}
}, 1000);
});
2. 处理 Promise 结果(核心方法)
通过 then、catch、finally 方法处理异步操作的结果,三者均支持链式调用(方法返回新的 Promise 实例)。
(1)then():处理成功结果(可选处理失败)
-
核心功能:主要用于处理
fulfilled状态的成功结果,也可可选处理rejected状态的失败原因; -
语法:
p.then(onFulfilled, onRejected),两个参数均为函数:- 第一个参数(必填 / 可选):
onFulfilled,状态为fulfilled时执行,接收成功结果作为参数; - 第二个参数(可选):
onRejected,状态为rejected时执行,接收失败原因作为参数;
- 第一个参数(必填 / 可选):
-
推荐用法:仅用
then处理成功,失败交给catch处理,职责更清晰。
(2)catch():专门处理失败结果
- 核心功能:捕获
Promise的rejected状态(包括异步操作失败、then方法中抛出的异常),专门处理失败原因; - 语法:
p.catch(onRejected),参数onRejected为失败处理函数,接收失败原因作为参数。
(3)finally():最终收尾操作
- 核心功能:无论
Promise状态是fulfilled还是rejected,都会执行该方法,用于做最终的收尾工作(如关闭加载动画、释放资源等); - 语法:
p.finally(onFinally),参数onFinally为回调函数,无参数(不接收异步操作的结果)。
javascript
运行
// 结果处理示例(推荐写法:then 处理成功,catch 处理失败,finally 收尾)
p.then((res) => {
// 处理成功结果
console.log("成功:", res); // 输出:成功:异步操作执行成功,返回数据
return res + ",已处理"; // 可以返回值,供下一个 then 链式调用
}).catch((err) => {
// 处理失败原因
console.log("失败:", err); // 若 isSuccess 为 false,输出失败信息
}).finally(() => {
// 最终收尾操作
console.log("异步操作结束,无论成功失败都会执行");
});
五、 Promise 的常用静态方法
ES6 为 Promise 提供了多个实用静态方法,用于批量处理多个 Promise 实例,高频使用的有:
1. Promise.all(iterable)
- 核心功能:批量执行多个异步操作(并行执行),等待所有 Promise 实例都转为
fulfilled后,才会返回成功结果;若有任意一个 Promise 实例转为rejected,立即返回失败结果,其他异步操作仍会执行,但结果会被忽略。 - 返回值:一个新的 Promise 实例;
- 成功结果:一个数组,数组元素顺序与传入的 Promise 实例顺序一致(与异步操作完成顺序无关);
- 失败结果:第一个失败的 Promise 实例的失败原因。
javascript
运行
const p1 = Promise.resolve("p1 成功");
const p2 = new Promise((resolve) => setTimeout(() => resolve("p2 成功"), 2000));
const p3 = Promise.resolve("p3 成功");
// 所有成功
Promise.all([p1, p2, p3]).then((res) => {
console.log(res); // ["p1 成功", "p2 成功", "p3 成功"](等待2秒后输出)
}).catch((err) => {
console.log(err); // 不执行
});
// 有一个失败
const p4 = Promise.reject("p4 失败");
Promise.all([p1, p4, p2]).then((res) => {
console.log(res); // 不执行
}).catch((err) => {
console.log(err); // 立即输出:p4 失败
});
2. Promise.race(iterable)
- 核心功能:批量执行多个异步操作(并行执行),哪个 Promise 实例最先改变状态,就返回该实例的结果(成功 / 失败均可),其他异步操作的结果会被忽略。
- 字面含义:“竞赛”,谁先完成就取谁的结果。
javascript
运行
const p1 = new Promise((resolve) => setTimeout(() => resolve("p1 成功"), 2000));
const p2 = new Promise((resolve, reject) => setTimeout(() => reject("p2 失败"), 1000));
Promise.race([p1, p2]).then((res) => {
console.log(res); // 不执行
}).catch((err) => {
console.log(err); // 1秒后输出:p2 失败(p2 先改变状态)
});
3. Promise.resolve(value)
- 核心功能:快速创建一个状态为
fulfilled的 Promise 实例,直接传递成功结果value; - 便捷用法:替代
new Promise((resolve) => resolve(value)),简化成功状态的 Promise 创建。
4. Promise.reject(reason)
- 核心功能:快速创建一个状态为
rejected的 Promise 实例,直接传递失败原因reason; - 便捷用法:替代
new Promise((resolve, reject) => reject(reason)),简化失败状态的 Promise 创建。
六、 核心总结
Promise是 ES6 异步编程解决方案,封装异步操作结果,解决回调地狱等问题;- 核心特性:三种状态(
pending/fulfilled/rejected)、状态单向不可逆、结果仅处理一次; - 基本使用:
new Promise()创建实例(执行器函数 +resolve/reject),then(成功)、catch(失败)、finally(收尾)处理结果; - 常用静态方法:
Promise.all(所有成功才成功)、Promise.race(谁先完成取谁结果)、Promise.resolve/Promise.reject(快速创建实例); - 核心价值:扁平化异步代码、统一异步处理规范、确保结果唯一可预测,是后续
async/await的基础。
如何理解 ES6 中的 Generator 的?使用场景?
一、 Generator 函数核心定义
Generator 函数是 ES6 新增的特殊函数,它是一种「可暂停、可恢复执行」的函数,也是实现异步编程和迭代器生成的重要工具。
与普通函数相比,Generator 函数的核心差异在于:它不会一次性执行完毕,而是可以在执行过程中被暂停,暂停后可恢复执行,并且能在暂停 / 恢复时实现数据的双向传递(普通函数只能一次性传入参数、返回结果,执行过程不可中断)。
简单来说:Generator 函数就像一个 “可中断的任务执行器”,允许我们手动控制函数的执行流程。
二、 Generator 函数的核心语法特性
1. 函数定义标识:function*
Generator 函数的定义必须带有 * 标识,位置可灵活放置(function* gen() / function * gen() / function *gen() 均合法,推荐 function* gen() 规范写法),以此区分普通函数。
javascript
运行
// 正确定义 Generator 函数
function* gen1() {}
function * gen2() {}
function *gen3() {}
// 错误定义(普通函数,非 Generator 函数)
function gen4() {}
2. 内部暂停标记:yield 关键字
yield(译为 “产出”)是 Generator 函数内部的暂停指令,也是实现函数暂停 / 恢复的核心:
- 当 Generator 函数执行到
yield关键字时,函数会立即暂停执行,并将yield后面的表达式结果作为 “暂停时的返回值”; yield只能在 Generator 函数内部使用,普通函数中使用会报错;- 一个 Generator 函数内部可以有多个
yield,对应多个暂停点。
javascript
运行
function* gen() {
console.log("执行到第一个暂停点前");
yield 1; // 第一个暂停点,暂停执行并返回 1
console.log("恢复执行后,到第二个暂停点前");
yield 2; // 第二个暂停点,暂停执行并返回 2
console.log("恢复执行后,函数即将结束");
return 3; // 函数执行完毕,返回最终结果
}
3. 执行结果:返回迭代器(Iterator)对象
Generator 函数调用后不会立即执行函数体,而是返回一个「迭代器对象(Iterator Object)」,这个迭代器对象是控制 Generator 函数执行的 “控制器”,通过其核心方法实现函数的暂停 / 恢复控制。
javascript
运行
const genIterator = gen(); // 调用 Generator 函数,返回迭代器对象,函数体未执行
console.log(genIterator); // Object [Generator] {}(迭代器对象)
三、 Generator 函数的执行机制(核心:暂停与恢复)
Generator 函数的执行完全依赖于其返回的迭代器对象的 API,核心是 next() 方法,同时支持 throw() 和 return() 方法辅助控制执行流程。
1. 核心方法:next() - 控制暂停 / 恢复执行
next() 方法是控制 Generator 函数执行的核心,作用是恢复 Generator 函数的执行,直到下一个 yield 或函数结束,并返回一个包含执行状态和结果的对象。
(1)next() 方法的返回值
返回值是一个普通对象,包含两个属性:
value:当前暂停点(yield)的返回值,或函数结束时return的值;done:布尔值,表示 Generator 函数是否执行完毕(false表示未执行完,还有后续暂停点;true表示已执行完毕)。
(2)next() 方法的执行流程
javascript
运行
function* gen() {
yield 1;
yield 2;
return 3;
}
const it = gen();
// 第一次调用 next():恢复执行,到第一个 yield 暂停
console.log(it.next()); // { value: 1, done: false }
// 第二次调用 next():恢复执行,到第二个 yield 暂停
console.log(it.next()); // { value: 2, done: false }
// 第三次调用 next():恢复执行,到函数 return 结束
console.log(it.next()); // { value: 3, done: true }
// 第四次调用 next():函数已执行完毕,返回默认值
console.log(it.next()); // { value: undefined, done: true }
(3) 数据双向传递:next(param) 传入参数
next() 方法支持接收一个参数,该参数会作为上一个 yield 表达式的返回值,实现从外部向 Generator 函数内部传递数据(实现双向通信)。
javascript
运行
function* gen() {
const a = yield 1; // a 接收下一次 next() 传入的参数
console.log("外部传入的参数 a:", a);
const b = yield (a + 2); // b 接收下一次 next() 传入的参数
console.log("外部传入的参数 b:", b);
return a + b;
}
const it = gen();
// 第一次 next() 不可传参(无对应的上一个 yield),传参也无效
it.next(); // { value: 1, done: false }
// 第二次 next() 传入 10,作为第一个 yield 的返回值(赋值给 a)
it.next(10); // 输出:外部传入的参数 a:10;返回 { value: 12, done: false }
// 第三次 next() 传入 20,作为第二个 yield 的返回值(赋值给 b)
it.next(20); // 输出:外部传入的参数 b:20;返回 { value: 30, done: true }
2. 辅助方法:throw() - 主动抛出异常
throw() 方法用于在 Generator 函数内部主动抛出异常,异常会在当前暂停点恢复执行时被触发,可通过 try/catch 捕获。
javascript
运行
function* gen() {
try {
yield 1;
yield 2;
} catch (err) {
console.log("捕获异常:", err);
}
yield 3;
}
const it = gen();
it.next(); // { value: 1, done: false }
it.throw("自定义异常"); // 抛出异常,被内部 catch 捕获;同时恢复执行到下一个 yield
// 输出:捕获异常:自定义异常;返回 { value: 3, done: false }
it.next(); // { value: undefined, done: true }
3. 辅助方法:return() - 强制结束执行
return() 方法用于强制终止 Generator 函数的执行,无论是否还有未执行的 yield,调用后 done 状态会立即变为 true。
javascript
运行
function* gen() {
yield 1;
yield 2;
yield 3;
}
const it = gen();
it.next(); // { value: 1, done: false }
it.return("强制结束"); // { value: "强制结束", done: true }
it.next(); // { value: undefined, done: true }(函数已终止,无法恢复)
四、 Generator 函数的核心使用场景
1. 异步编程(解决回调地狱,async/await 的前身)
这是 Generator 函数最核心的使用场景。在 async/await 出现之前,Generator 函数常与 co 库(自动执行器)配合,实现扁平化的异步代码,彻底解决回调地狱问题。
其核心思路:将异步操作放在 yield 后面,利用 Generator 函数的暂停特性,等待异步操作完成后再恢复执行,让异步代码看起来像同步代码一样直观。
javascript
运行
// 模拟异步操作(AJAX/文件读取)
function fetchData(url) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`成功获取 ${url} 的数据`);
}, 1000);
});
}
// Generator 函数封装异步流程
function* genAsync() {
console.log("开始请求第一个接口");
const res1 = yield fetchData("/api/user"); // 暂停,等待异步操作完成
console.log(res1);
console.log("开始请求第二个接口");
const res2 = yield fetchData("/api/order"); // 暂停,等待异步操作完成
console.log(res2);
return "所有接口请求完成";
}
// 手动执行 Generator 异步函数
function runGen(genFunc) {
const it = genFunc();
function next(data) {
const result = it.next(data);
if (result.done) {
console.log(result.value);
return;
}
// 若 value 是 Promise,等待其完成后再调用 next()
result.value.then((res) => {
next(res);
});
}
next();
}
// 执行异步流程(无嵌套,扁平化代码)
runGen(genAsync);
// 输出顺序(每隔1秒输出):
// 开始请求第一个接口
// 成功获取 /api/user 的数据
// 开始请求第二个接口
// 成功获取 /api/order 的数据
// 所有接口请求完成
注意:
async/await本质是 Generator 函数的语法糖,底层依赖 Generator 函数的暂停 / 恢复特性,简化了手动执行的流程。
2. 生成自定义迭代器(遍历复杂数据结构)
Generator 函数默认返回迭代器对象,且 yield 可以便捷地生成迭代数据,因此常用来创建自定义迭代器,用于遍历复杂数据结构(如树形结构、无限序列等),比手动实现迭代器更简洁。
(1) 生成无限序列(按需生成,不占用多余内存)
javascript
运行
// 生成无限递增的数字序列(Generator 按需生成,不会一次性创建所有数据)
function* infiniteNumGenerator() {
let num = 1;
while (true) {
yield num++;
}
}
const numIt = infiniteNumGenerator();
console.log(numIt.next().value); // 1
console.log(numIt.next().value); // 2
console.log(numIt.next().value); // 3
// 可无限调用,按需生成数据,无内存溢出风险
(2) 遍历树形数据结构(简化递归遍历)
javascript
运行
// 树形数据
const tree = {
value: 1,
children: [
{
value: 2,
children: [{ value: 4 }]
},
{ value: 3 }
]
};
// Generator 函数实现树形结构遍历
function* treeTraversal(node) {
yield node.value; // 先返回当前节点值
if (node.children) {
for (const child of node.children) {
// 递归遍历子节点,使用 yield* 调用另一个 Generator 函数
yield* treeTraversal(child);
}
}
}
// 遍历树形结构
const treeIt = treeTraversal(tree);
for (const value of treeIt) {
console.log(value); // 1、2、4、3(按深度优先顺序遍历)
}
3. 控制任务执行流程(分步执行任务)
Generator 函数的暂停 / 恢复特性,可用于实现「分步执行的任务」,例如:表单分步验证、步骤化的业务流程(如注册流程:填写基本信息 → 验证手机 → 提交信息),手动控制每一步的执行时机。
javascript
运行
// 分步执行的注册流程
function* registerFlow() {
const basicInfo = yield "1. 填写基本信息(姓名/邮箱)";
console.log("已填写基本信息:", basicInfo);
const phoneCode = yield "2. 输入手机验证码";
console.log("已输入验证码:", phoneCode);
const submitResult = yield "3. 提交注册信息";
console.log("注册结果:", submitResult);
return "注册流程全部完成";
}
// 获取迭代器
const registerIt = registerFlow();
// 第一步:执行到第一个 yield,获取步骤提示
console.log(registerIt.next().value); // 1. 填写基本信息(姓名/邮箱)
// 第二步:传入基本信息,恢复执行到第二个 yield
registerIt.next({ name: "张三", email: "zhangsan@xxx.com" });
console.log(registerIt.next().value); // 2. 输入手机验证码
// 第三步:传入验证码,恢复执行到第三个 yield
registerIt.next("123456");
console.log(registerIt.next().value); // 3. 提交注册信息
// 第四步:传入提交结果,完成流程
registerIt.next("注册成功");
4. 数据懒加载(按需生成数据,优化性能)
懒加载的核心是 “不提前生成所有数据,只在需要时生成”,Generator 函数的 yield 特性天然支持懒加载,可用于优化大数据量场景的性能(如分页数据、大数据列表渲染等)。
javascript
运行
// 模拟分页数据懒加载(每页10条,按需生成)
function* lazyLoadData(total, pageSize = 10) {
let page = 1;
while ((page - 1) * pageSize < total) {
// 模拟生成当前页数据
const pageData = Array.from({ length: pageSize }, (_, i) => {
const id = (page - 1) * pageSize + i + 1;
return { id, name: `用户${id}` };
}).filter(item => item.id <= total); // 过滤最后一页的多余数据
yield pageData; // 按需返回当前页数据
page++;
}
}
// 总数据15条,每页10条
const dataIt = lazyLoadData(15, 10);
// 第一次获取:第一页数据(1-10条)
console.log("第一页数据:", dataIt.next().value);
// 第二次获取:第二页数据(11-15条)
console.log("第二页数据:", dataIt.next().value);
// 第三次获取:无数据(流程结束)
console.log(dataIt.next().value); // undefined
五、 核心总结
-
核心本质:Generator 是 ES6 可暂停 / 恢复执行的特殊函数,通过
function*定义,yield实现暂停,迭代器对象控制执行; -
执行机制:调用后返回迭代器,通过
next()恢复执行(支持双向传参),throw()主动抛异常,return()强制结束; -
核心场景:
- 异步编程:解决回调地狱,是
async/await的前身; - 自定义迭代器:简化复杂数据结构(树形、无限序列)的遍历;
- 任务流程控制:实现分步执行的业务流程(如注册、表单验证);
- 数据懒加载:按需生成数据,优化大数据量场景性能;
- 异步编程:解决回调地狱,是
-
关键地位:Generator 函数是
async/await的底层基础,理解它有助于更深入掌握现代 JavaScript 异步编程。
如何理解 ES6 中的 Proxy 的?使用场景?
一、 Proxy 核心定义
Proxy 是 ES6 新增的对象代理机制,它能够创建一个「目标对象的代理对象」,对目标对象的所有访问(属性读取、赋值、方法调用等),都必须先经过这个代理对象。
代理对象可以在「访问目标对象的流程中」,插入自定义的拦截逻辑(如校验、拦截、转换、监控等),实现对目标对象的 “包装” 和 “控制”,而无需修改目标对象本身的结构和逻辑。
简单来说:Proxy 就像目标对象的 “门卫”,所有对目标对象的操作都要经过它的审核和处理,它可以决定允许操作、拒绝操作,或对操作进行加工后再传递给目标对象。
二、 Proxy 核心语法与基本使用
1. 基本语法
创建 Proxy 实例的语法固定,接收两个必填参数,返回一个代理对象:
javascript
运行
const proxy = new Proxy(target, handler);
参数说明
| 参数名 | 含义 |
|---|---|
target | 必填,被代理的目标对象(可以是任意类型的对象:普通对象、数组、函数等) |
handler | 必填,配置拦截规则的对象。其属性是「拦截器方法」(如 get、set),当对代理对象执行对应操作时,会自动触发对应的拦截器方法,执行自定义逻辑 |
2. 基本使用示例
下面是最常用的 get(拦截属性读取)和 set(拦截属性赋值)拦截器的基础示例,实现对对象属性的读取监控和赋值校验:
## 三、 Proxy 的核心特性
1. **非侵入式**:代理对象不会修改目标对象的任何属性和方法,所有自定义逻辑都在 `handler` 中实现,保持目标对象的纯净性,便于维护和复用。
1. **全面拦截**:支持对对象的几乎所有操作进行拦截(共 13 种拦截器方法),涵盖属性读取、赋值、删除、函数调用、原型访问等场景,控制力远超 `Object.defineProperty`。
1. **代理对象与目标对象独立**:操作代理对象的属性(合法操作)会同步到目标对象,操作目标对象的属性也会同步到代理对象,但两者的引用地址不同,是两个独立对象。
1. **无默认拦截**:若 `handler` 中未配置对应操作的拦截器方法,该操作会直接透传给目标对象,执行默认行为(即 “无拦截时,代理对象与目标对象行为一致”)。
1. **支持嵌套代理**:若目标对象的属性值是复杂对象(如嵌套对象、数组),可对其再创建 `Proxy` 实例,实现深层拦截。
## 四、 常用拦截器方法(核心能力)
`Proxy` 提供了 13 种拦截器方法,对应对象的不同操作,以下是最常用、最核心的几种:
| 拦截器方法 | 拦截的操作场景 | 核心用途 |
| ---------------- | ----------------------------------------- | ------------------ |
| `get` | 属性读取(`obj.prop` / `obj[prop]`) | 读取监控、默认值设置、属性转换 |
| `set` | 属性赋值(`obj.prop = value`) | 赋值校验、赋值监控、数据同步 |
| `has` | `in` 运算符判断(`prop in obj`) | 隐藏某些属性,避免被 `in` 检测 |
| `deleteProperty` | `delete` 运算符删除属性(`delete obj.prop`) | 删除权限控制、删除监控 |
| `apply` | 函数调用(`fn()` / `fn.call()` / `fn.apply()`) | 函数调用监控、参数校验、返回值转换 |
| `construct` | `new` 关键字创建实例(`new fn()`) | 实例创建监控、参数校验、限制实例化 |
)
五、 Proxy 的核心使用场景
1. 数据校验与数据保护(最常用场景)
利用 set 拦截器,可以对属性赋值进行严格校验(类型、范围、格式等),确保数据的合法性;利用 deleteProperty、has 等拦截器,可以保护核心属性不被删除、不被检测,实现数据只读或部分权限控制。
### 2. 数据响应式(Vue3 核心底层原理)
`Proxy` 是 Vue3 实现数据响应式的核心技术(替代 Vue2 的 `Object.defineProperty`)。通过 `get` 拦截器收集依赖(记录哪些组件使用了该数据),通过 `set` 拦截器触发依赖更新(当数据变化时,通知对应组件重新渲染)。
### 3. 对象属性默认值与属性转换
利用 `get` 拦截器,可以为对象的不存在属性设置默认值,避免获取到 `undefined`;也可以对属性值进行自动转换(如统一格式化日期、转换数据类型等),简化业务逻辑。
**示例:默认值 + 自动类型转换**
4. 日志监控与调试
通过 Proxy 的各种拦截器,可以无侵入式地监控目标对象的所有操作(读取、赋值、删除、函数调用等),记录操作日志(谁、在什么时候、执行了什么操作、操作结果如何),便于后期调试和问题排查,尤其适合复杂业务系统的监控。
### 5. 函数 / 类的包装与增强
`Proxy` 不仅可以代理普通对象,还可以代理函数和类,通过 `apply`(拦截函数调用)、`construct`(拦截 `new` 实例化)拦截器,实现对函数 / 类的增强(如参数校验、调用次数限制、实例化限制等)。
**示例:增强函数,限制调用次数**
javascript
运行
// 目标函数:需要被限制调用次数的函数
function sendRequest(data) {
console.log(发送请求,数据:${JSON.stringify(data)});
return "请求成功";
}
// 拦截器:限制函数最多调用3次
const limitHandler = {
count: 0, // 调用次数计数器
maxCount: 3, // 最大调用次数
apply(target, thisArg, args) {
if (this.count >= this.maxCount) {
console.log(函数已达到最大调用次数(${this.maxCount}次),禁止继续调用);
return "调用失败";
}
this.count++;
console.log(函数第 ${this.count} 次调用);
return target.apply(thisArg, args);
}
};
// 创建代理函数 const limitedRequest = new Proxy(sendRequest, limitHandler);
// 前3次调用:正常执行
// 第4次调用:被限制 console.log(limitedRequest({ id: 4 }));
## 六、 核心总结
1. **核心本质**:`Proxy` 是 ES6 对象代理机制,创建目标对象的代理对象,实现对目标对象操作的拦截与控制,非侵入式修改对象行为。
1. **核心语法**:`new Proxy(target, handler)`,`target` 为被代理对象,`handler` 为拦截器配置对象,包含 `get`、`set` 等拦截方法。
1. **核心特性**:非侵入式、全面拦截、代理与目标对象独立、无默认拦截(无配置则透传操作)。
1. **核心场景**:
- 数据校验与保护:控制属性赋值、删除权限,确保数据合法性;
- 数据响应式:Vue3 底层核心,实现数据变化驱动视图更新;
- 默认值与属性转换:避免 `undefined`,自动格式化数据;
- 日志监控:无侵入式记录对象操作,便于调试;
- 函数 / 类增强:实现参数校验、调用次数限制等功能。
1. **关键优势**:相比 `Object.defineProperty`,`Proxy` 支持拦截更多操作、支持数组和嵌套对象、无需手动遍历属性,灵活性更强。