ECMAScript6+
【注】此处ES6+泛指ES6(ES5)之后所有版本的新特性
一 ES6+语法改进
1 变量及解构赋值
ES6(ECMAScript 2015)对JavaScript的变量声明和管理方式进行了重大改进,主要体现在引入了新的关键字let和const,以及更严格的变量作用域规则
关键字 | 类型 | 定义 | 可变性 | 作用域 |
---|---|---|---|---|
var | 变量(旧) | 可以重复定义 | 不能限制修改 | 函数级作用域 |
let | 变量 | 不能重复定义 | 可以修改 | 块级作用域 |
const | 常量 | 不能重复定义 | 不可修改 | 块级作用域 |
解构赋值 :
解构赋值是JavaScript中的一种特殊语法,它允许你将数组或对象的属性直接赋值给不同的变量。这种机制简化了从复合数据结构中提取数据的过程,使得代码更加简洁且易于阅读。
数组解构赋值:
对于数组,你可以按照索引位置将元素值赋给对应位置的变量:
let [a, b, c] = [1, 2, 3];
// 这里 a=1, b=2, c=3
如果变量的数量少于数组的长度,可以省略部分元素:
let [a, , c] = [1, 2, 3];
// 这里 a=1, c=3,中间的元素被忽略
还可以使用...rest语法来收集剩余的元素:
let [a, ...rest] = [1, 2, 3, 4, 5];
// 这里 a=1, rest=[2, 3, 4, 5]
对象解构赋值:
对象解构赋值则是根据属性名来匹配并赋值:
let {name, age} = {name: 'Alice', age: 30};
// 这里 name='Alice', age=30
你可以指定变量名与属性名不一致的情况,通过冒号(:)实现:
let {name: userName, age: userAge} = {name: 'Bob', age: 25};
// 这里 userName='Bob', userAge=25
默认值也可以在解构时设定,如果属性不存在或值为undefined,则使用默认值:
let {height = 170, weight} = {weight: 60};
// 这里 height=170(默认值),weight=60
【小结】
- 解构赋值就是把数据结构分解,然后给变量进行赋值
- 如果解构不成功,变量跟数值个数不匹配的时候,变量的值为undefined
- 数组解构用中括号包裹,多个变量用逗号隔开,对象解构用花括号包裹,多个变量用逗号隔开
2 参数展开和剩余参数
ES6(ECMAScript 2015)引入了参数展开(Spread Syntax)特性,它允许你将数组或者可迭代对象的内容展开为独立的元素。这一特性在函数调用、数组构造、对象字面量等方面非常有用。
函数调用中的参数展开
当你需要将数组的元素作为单独的参数传递给函数时,可以使用展开语法(...)。
// ES5 中的传统做法
function sum(a, b, c) {
return a + b + c;
}
var numbers = [1, 2, 3];
console.log(sum.apply(null, numbers)); // 输出 6
// ES6 参数展开
console.log(sum(...numbers)); // 直接输出 6
数组合并
参数展开也可以用来简便地合并数组。
let array1 = [1, 2, 3];
let array2 = [4, 5, 6];
let combined = [...array1, ...array2];
console.log(combined); // 输出 [1, 2, 3, 4, 5, 6]
数组拷贝
参数展开可以用来快速创建现有数组的浅拷贝。
let originalArray = [1, 2, 3];
let copiedArray = [...originalArray];
console.log(copiedArray); // 输出 [1, 2, 3]
在对象字面量中展开
除了在数组中使用,参数展开还可以用于对象字面量,以合并对象的属性。
let obj1 = { a: 1, b: 2 };
let obj2 = { ...obj1, c: 3 };
console.log(obj2); // 输出 { a: 1, b: 2, c: 3 }
剩余参数(Rest Parameters)
与参数展开相对应的是剩余参数(Rest Parameters),它使用同样的三点(...)语法,但用在函数参数列表中,用于收集不确定数量的参数为一个数组。
function sumAll(...numbers) {
return numbers.reduce((acc, curr) => acc + curr, 0);
}
console.log(sumAll(1, 2, 3, 4, 5)); // 输出 15
剩余参数和解构配合使用
let students = ['wangwu', 'zhangsan', 'lisi'];
let [s1, ...s2] = students;
console.log(s1); // 'wangwu'
console.log(s2); // ['zhangsan', 'lisi']
3 箭头函数及this
在ES6中引入的箭头函数(Arrow Function)提供了一种更简洁的函数表达方式,并且改进了函数内this
值的行为。
箭头函数使用=>
的新语法代替了传统的function
关键字。例如:
// 传统函数表达式
const traditionalFunc = function(a, b) {
return a + b;
};
// 箭头函数表达式
const arrowFunc = (a, b) => a + b;
如果箭头函数体只包含一个表达式,则省略return
关键字和花括号;如果函数体有多个语句,需要使用花括号,并且需要写return
(如果需要返回值):
const arrowFuncWithMoreBody = (a, b) => {
const result = a + b;
return result;
};
无参数或多个参数需要用括号括起来,单个参数可以省略括号:
const noParams = () => 'No params';
const singleParam = a => a * 2;
const multipleParams = (a, b) => a + b;
this
指向的改进
与传统的函数不同,箭头函数不绑定自己的this
值。箭头函数内的this
值由外围(即定义时的)最近一个非箭头函数决定。这被称为“词法作用域”或“静态作用域”:
function Counter() {
this.count = 0;
setInterval(() => {
this.count++; // 'this' 指向Counter实例
console.log(this.count);
}, 1000);
}
const counterInstance = new Counter();
在这个例子中,setInterval
中的箭头函数没有自己的this
,所以它会从外部作用域(即Counter
函数)中继承this
。如果我们使用传统的函数,this
将指向全局对象(在浏览器中是window
),或者如果在严格模式下则是undefined
。
注意
箭头函数没有
arguments
对象:尽管箭头函数没有自己的arguments
对象,但可以通过剩余参数语法来访问函数的参数。箭头函数不能用作构造器:尝试使用
new
关键字调用箭头函数会抛出错误。不绑定
this
:这意味着箭头函数内部的this
值不会因方法调用形式(obj.method()
)而改变。
代码示例:
// 用作普通函数
const arrowFunctionExample = (a, b) => {
return a + b;
};
console.log(arrowFunctionExample(5, 4)); // 输出: 9
// 用作对象方法
const objectWithArrowFunction = {
value: 22,
getValue: () => this.value // 这里的this不会绑定到objectWithArrowFunction
};
console.log(objectWithArrowFunction.getValue()); // 输出: undefined
// 箭头函数捕获其所在上下文的this值
function Person() {
this.age = 0;
setInterval(() => {
this.age++; // 'this'正确地指向了person实例
}, 1000);
}
const p = new Person();
在objectWithArrowFunction
的例子中,this
并没有指向该对象,因为箭头函数不会创建自己的执行上下文,而是捕获其所在上下文的this
值。在Person
函数中,使用箭头函数意味着内部的setInterval
回调函数能够正确地引用this
(即Person
实例)。
ES6中引入箭头函数的目的是简化函数的写法,并解决传统函数中this
指向经常发生的错误或混淆。
4 模板字符串
ES6引入了模板字面量(也称模板字符串),是增强版的字符串字面量,可以使用反引号 (`
) 来创建。模板字面量提供了很多新的特性来创建动态字符串。
1. 基本字符串
模板字面量可以像普通字符串一样使用,只是使用反引号包裹:
const basicString = `hello, world!`;
console.log(basicString); // 输出: hello, world!
2. 多行字符串
模板字面量轻松创建多行字符串,无需使用字符串连接符或者特殊的换行符:
const multiLineString = `This is a string
that spans across
multiple lines`;
console.log(multiLineString);
输出结果会保留换行格式。
3. 字符串插值
可以在字符串中嵌入变量或表达式,使用${}
语法:
const name = 'Alice';
const greeting = `Hello, ${name}!`;
console.log(greeting); // 输出: Hello, Alice!
任何有效的JavaScript表达式都可以被嵌入到${}
中,表达式的结果将被拼接到字符串中:
const price = 10;
const taxRate = 0.25;
const total = `Total: $${(price * (1 + taxRate)).toFixed(2)}`;
console.log(total); // 输出: Total: $12.50
4. 嵌套模板
模板字面量可以嵌套。可以在模板字面量中嵌入其他模板字面量:
const user = { name: 'John', age: 28 };
const userInfo = `User Info:
Name: ${user.name}
Age: ${user.age}
Birthday: ${`in ${365 - user.age} days`}`;
console.log(userInfo);
在这个例子中,内部模板字面量计算了John
距离下一个生日还有多少天。
5. 标签模板
模板字面量可以被函数标签化。标签是一个函数,模板字面量是其参数。这个函数可以对模板字符串进行处理:
function highlight(strings, ...values) {
return strings.reduce((result, string, i) => {
return `${result}${string}<em>${values[i] || ''}</em>`;
}, '');
}
const name = 'Alice';
const age = 25;
const sentence = highlight`My name is ${name} and I am ${age} years old.`;
console.log(sentence);
标签模板函数highlight
接收两个参数:第一个参数strings
是一个字符串值的数组,第二个参数是插值表达式的值的数组。在这个例子中,标签函数用于将插值的部分包裹在<em>
标签内,以实现高亮效果。
模板字面量通过简洁且灵活的语法使得创建和处理字符串变得更加容易,尤其是在需要动态插入变量和表达式,或者构建包含多行和嵌套内容的字符串时。这些特性能极大地提高代码的可读性和效率。
5 增强的对象字面量
ES6(ECMAScript 2015)对对象字面量进行了增强,提供了几个新的语法糖,使得对象的创建和操作更加简洁和直观
1. 属性值缩写(Property Shorthand)
当对象的属性名与局部变量名相同,可以只写一个名称,省略:
和值部分。
传统写法:
const name = 'John Doe';
const age = 30;
const person = {
name: name,
age: age
};
ES6写法:
const name = 'John Doe';
const age = 30;
const person = { name, age };
2. 方法简写(Method Shorthand)
可以省略方法的function
关键字和冒号。
传统写法:
const person = {
sayHello: function() {
console.log('Hello!');
}
};
ES6写法:
const person = {
sayHello() {
console.log('Hello!');
}
};
3. 计算属性名(Computed Property Names)
可以在对象字面量中使用方括号[]
来设置动态的属性名。
const propertyKey = 'status';
const project = {
['project_' + propertyKey]: 'active'
};
console.log(project.project_status); // 输出: active
4. 设置原型(Setting the Prototype)
使用__proto__
可以在对象字面量中直接设置该对象的原型。
const animal = {
isAnimal: true
};
const dog = {
__proto__: animal,
bark() {
console.log('Woof!');
}
};
console.log(dog.isAnimal); // 输出: true
dog.bark(); // 输出: Woof!
6 新增数据类型
ES6(ECMAScript 2015)为JavaScript语言引入了几种新的数据类型,旨在丰富语言的功能,提高开发效率和代码的可维护性。
1. Symbol(符号)
Symbol是一种原始数据类型,用于生成唯一的、不可变的值。Symbols非常适合用作对象的键,以避免属性名的冲突。由于每个Symbol值都是唯一的,所以它们可以作为对象属性的唯一标识符。
const sym1 = Symbol();
const sym2 = Symbol();
console.log(sym1 === sym2); // false,表明每个Symbol都是唯一的
const obj = {};
obj[sym1] = 'value';
console.log(obj[sym1]); // 输出: value
2. Map(映射)
Map是一种存储键值对的数据结构,与传统的对象不同,Map的键可以是任何值(包括对象)。Map保持了键值对的插入顺序,并提供了更丰富的方法来操作这些键值对。
const map = new Map();
map.set('key1', 'value1');
map.set('key2', 'value2');
console.log(map.get('key1')); // 输出: value1
3. Set(集合)
Set是一个不包含重复值的有序列表。它可以用于去重、集合运算等场景。
const set = new Set([1, 2, 3, 4, 4]);
console.log(set.size); // 输出: 4,因为重复的4被自动去重
4. WeakMap(弱映射)
WeakMap类似于Map,但是其键必须是对象,而且对这些键的引用是弱引用。这意味着如果对象没有其他引用并且会被垃圾回收,WeakMap中的对应条目也会自动消失。
const wm = new WeakMap();
const obj = {};
wm.set(obj, 'data');
// 如果obj没有其他引用,它将被垃圾回收,同时WeakMap中的条目也会被清除
5. WeakSet(弱集合)
WeakSet与Set类似,但它只接受对象作为成员,并且对这些成员的引用是弱的。同样,如果成员对象没有其他引用,它会被垃圾回收,同时WeakSet中对应的成员也会被移除。
const ws = new WeakSet();
const obj = {};
ws.add(obj);
// 当obj没有其他引用时,它将被垃圾回收,WeakSet中的obj也会随之移除
7 模块系统的改进
ES6(ECMAScript 2015)对JavaScript的模块系统进行了根本性的改进,引入了原生的模块支持,这在之前是通过非标准的解决方案如AMD(异步模块定义)和CommonJS(主要用于Node.js环境)来实现的。ES6模块的关键特性包括静态加载、导入和导出机制,以及模块作用域的明确界定。以下是ES6模块系统的主要改进点及导入导出方式与之前的区别:
静态加载(编译时加载):
- ES6模块在编译阶段就确定了模块间的依赖关系,这使得工具可以进行静态分析,优化代码,比如tree-shaking(移除未使用的代码)。
- 与之相对,CommonJS模块在运行时动态加载,这意味着只有在运行时才能解析模块依赖,无法进行有效的静态分析和优化。
导入导出语法:
- ES6使用
import
和export
关键字来进行模块的导入和导出,语法清晰且语义明确。 - CommonJS使用
require
来导入模块,使用module.exports
或exports
来导出模块,这种方式在处理复杂模块结构时可能会显得混乱。
- ES6使用
默认导出与命名导出:
- ES6允许一个模块有默认导出(
export default
),也可以有多个命名导出(export
)。 - CommonJS主要依赖于导出一个对象,其中包含所有的模块成员,虽然也可以导出单一值,但不区分默认导出和命名导出。
- ES6允许一个模块有默认导出(
模块作用域:
- ES6模块中,顶级变量具有块级作用域,只在模块内部可见,避免了全局污染。
- CommonJS模块中的顶级变量实质上是模块作用域的,但在Node.js环境中,它们可以通过
global
对象间接访问到,可能导致全局污染。
ES6模块导入导出示例:
导出模块 (myModule.js
):
// 命名导出
export const PI = 3.14;
export function add(a, b) {
return a + b;
}
// 默认导出
export default function greet(name) {
console.log(`Hello, ${name}!`);
}
导入模块 (main.js
):
// 导入默认导出
import greet from './myModule.js';
// 导入命名导出
import { PI, add } from './myModule.js';
greet('Alice'); // 输出: Hello, Alice!
console.log(PI); // 输出: 3.14
console.log(add(2, 3)); // 输出: 5
默认导出和命名导出
在ES6模块系统中,默认导入导出
和命名导入导出
是两种不同的模块间交互方式,它们在用途和语法上有明显的区别:
默认导出(default export)
- 目的:默认导出允许模块向外暴露一个单一的、默认的导出项。这通常用于当模块仅需要共享一个主要功能或对象时。
- 语法:
- 导出:在一个模块中,使用
export default
后面跟要导出的值。// myModule.js export default function() { console.log("This is the default export."); }
- 导入:在另一个模块中,使用
import
语句,并可以自由指定一个接收该导出的变量名。// main.js import myFunction from './myModule.js'; myFunction(); // 输出: This is the default export.
- 注意:每个模块只能有一个
export default
,但一个模块可以同时有默认导出和其他命名导出。
- 导出:在一个模块中,使用
在一个模块中,通常推荐只使用一个export default语句。 export default用于导出模块的默认输出,它使得其他模块在导入时可以不必知道导出的确切名字,提供了一种方便和灵活的导入方式。如果一个模块中有多个export default,实际上只有最后一个会生效,前面的会被覆盖,但这通常被视为不良实践,因为它可能导致代码的可读性和预期行为的混乱。
命名导出(named exports)
- 目的:命名导出允许模块导出多个独立的、命名的值。这对于导出多个功能或变量非常有用,接收方可以根据需要选择性地导入。
- 语法:
- 导出:使用
export
关键字,后面跟着要导出的变量名或函数名。// myModule.js export const myVar = "Some value"; export function myFunc() { console.log("This is a named export."); }
- 导入:使用
import
语句,并在花括号中列出想要导入的导出名,可以重命名。// main.js import { myVar, myFunc as renamedFunction } from './myModule.js'; console.log(myVar); // 输出: Some value renamedFunction(); // 输出: This is a named export.
- 也可以使用
import * as
语法一次性导入所有命名导出为一个对象。import * as myModule from './myModule.js'; console.log(myModule.myVar); // 输出: Some value myModule.myFunc(); // 输出: This is a named export.
- 导出:使用
总结
- 默认导出适用于模块只需要分享一个核心功能或对象的情况,且导入时可以自由命名。
- 命名导出适用于模块需要分享多个独立功能或变量,提供了更灵活的选择性和重命名的能力。
- 根据模块的具体需求,可以选择使用默认导出、命名导出,或者两者结合使用。
CommonJS模块导入导出示例(对比):
导出模块 (myModule.js
):
// 命名导出
exports.PI = 3.14;
exports.add = function(a, b) {
return a + b;
};
// 默认导出(通过module.exports)
module.exports = function greet(name) {
console.log(`Hello, ${name}!`);
};
导入模块 (main.js
):
// 导入默认导出
const greet = require('./myModule.js');
// 导入命名导出需要访问exports对象
const { PI, add } = require('./myModule.js');
greet('Alice'); // 输出: Hello, Alice!
console.log(PI); // 输出: 3.14
console.log(add(2, 3)); // 输出: 5
8 类(Class)和对象
在ES6之前,JavaScript使用基于原型链的对象创建和继承机制,这种方式比较灵活但也相对复杂,容易造成理解和维护上的困难。ES6引入了class
语法,使得面向对象编程的语法更加接近于其他面向对象语言,如Java或C++,尽管底层仍然是基于原型的实现。
在ES5及更早版本中,类的实现通常依靠构造函数和原型链:
// ES5实现类和继承
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
};
function Student(name, age, grade) {
Person.call(this, name, age); // 使用call继承属性
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype); // 设置原型链
Student.prototype.constructor = Student; // 修复构造函数指向
Student.prototype.sayGrade = function() {
console.log('I am in grade ' + this.grade);
};
类的声明
在ES6中,使用class
关键字声明一个类,类中可以包含构造函数(constructor)、方法、静态方法和属性。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
}
const person = new Person('Alice', 30);
person.sayHello(); // 输出: Hello, my name is Alice
- 通过class 关键字创建类, 类名我们还是习惯性定义首字母大写
- 类里面有个constructor 函数,可以接受传递过来的参数,同时返回实例对象
- constructor 函数 只要 new 生成实例时,就会自动调用这个函数, 如果我们不写这个函数,类也会自动生成这个函数
- 生成实例 new 不能省略
- 语法规范, 创建类 类名后面不要加小括号,生成实例 类名后面加小括号, 构造函数不需要加function
继承
ES6中的类支持继承,使用extends
关键字来实现。子类可以继承父类的属性和方法,并且可以使用super
关键字来调用父类的构造函数或方法。
示例:
class Student extends Person {
constructor(name, age, grade) {
super(name, age); // 调用父类构造函数
this.grade = grade;
}
sayGrade() {
console.log(`I am in grade ${this.grade}`);
}
}
const student = new Student('Bob', 20, 'Sophomore');
student.sayHello(); // 继承自Person
student.sayGrade(); // 输出: I am in grade Sophomore
静态方法与属性
静态方法和属性属于类本身,而不属于类的实例,通过static
关键字定义。
示例:
class MathUtils {
static PI = 3.14; // 静态属性PI
static add(a, b) { // 静态方法
return a + b;
}
}
console.log(MathUtils.PI); // 输出: 3.14
console.log(MathUtils.add(5, 3)); // 输出: 8
9 迭代器Iterators
在ES6之前,JavaScript中没有内建的迭代器和遍历机制,通常需要使用循环(如for
,while
)来遍历数组或对象的属性。ES6引入了迭代器(Iterator)和可迭代协议(Iterable protocol),使得数据集合的遍历变得更加灵活和高效。
ES6迭代器的改进
迭代器是遵循迭代器协议的对象,该协议定义了一个next
方法,每次调用返回集合中的下一个元素。一个对象若要成为可迭代对象,需要实现可迭代协议,即该对象要有一个Symbol.iterator
属性,它是一个函数,返回一个迭代器。
这项改进的好处包括:
- 统一的遍历接口:对所有可迭代对象,如数组、新引入的
Map
和Set
,甚至是字符串,都可以使用相同的迭代协议进行遍历。 - 更好的控制遍历过程:通过迭代器的
next
方法,可以更精细地控制遍历的过程,比如可以在满足特定条件时提前终止遍历。 - 使得数据结构的操作更为抽象和高级:例如,可以使用新引入的
for...of
循环、扩展运算符(...
)、解构赋值等语法,让数据操作更加简洁和表达性更强。
在ES6之前的遍历方法:
var myArray = [1, 2, 3];
for (var i = 0; i < myArray.length; i++) {
console.log(myArray[i]);
}
使用ES6的迭代器:
对于数组来说,它是天然支持迭代协议的:
let myArray = [1, 2, 3];
let it = myArray[Symbol.iterator](); // 🌟🌟✨代码解释在代码块后
console.log(it.next().value); // 1
console.log(it.next().value); // 2
console.log(it.next().value); // 3
Symbol.iterator方法
让我们逐步解释代码 let it = myArray[Symbol.iterator]();
myArray
:这是一个数组实例,数组在ES6中默认就是一个可迭代对象(iterable)。
即数组默认实现了迭代器接口,自然默认就拥有一个Symbol.iterator
方法,当这个方法被调用时,它返回一个迭代器对象。Symbol.iterator
:这是ES6引入的一个特殊的内置符号,它本身是一个表达式,返回一个symbol值,该symbol值被用作属性键,代表了默认迭代器方法。每种可以被迭代的内建类型(包括数组、字符串、Map、Set等)都有一个默认的Symbol.iterator
方法。myArray[Symbol.iterator]
:这个表达式获取myArray
对象的默认迭代器方法。它访问数组的Symbol.iterator
属性,这个属性存储了一个函数,负责创建一个迭代器。myArray[Symbol.iterator]()
:通过加上圆括号()
,这个表达式现在是一个函数调用表达式。调用数组的Symbol.iterator
方法,返回了一个迭代器对象。let it = ...;
:最后,我们把这个迭代器对象赋值给变量it
。这个迭代器对象具有一个next
方法,可以用来遍历数组。每次调用it.next()
都会返回一个对象,这个对象有两个属性:value
表示当前遍历的数组元素,done
表示迭代是否结束(布尔值,true
表示迭代已结束)。
这个过程可以使得迭代的消费代码(如for...of
循环)与数组的内部表示分离,使得数组可以通过自定义迭代器来改变迭代行为,同时维持和提供一致的迭代接口。
使用for...of遍历可迭代对象:
for...of
循环是基于迭代器协议的高级语法,让遍历操作变得更为简洁:
let myArray = [1, 2, 3];
for (let value of myArray) {
console.log(value); // 依次输出1, 2, 3
}
10 Optional Chaining
可选链(Optional Chaining)操作符最初是在ES2020(ECMAScript 2020)规范中正式引入的。
在可选链出现之前,如果要安全地访问一个对象的深层属性,开发者通常需要逐级检查每一级是否为null
或undefined
,以避免运行时错误。例如,访问obj?.property1?.property2
这样的路径时,如果不使用可选链,可能需要写成:
if (obj && obj.property1 && obj.property1.property2) {
// 安全地使用 obj.property1.property2
}
这种写法不仅冗长,还降低了代码的可读性和维护性,特别是在有多层嵌套的情况下。
代码使用示例:
假设我们有一个用户对象,但不确定其中的某些属性是否存在:
let user = {
profile: {
// 可能存在也可能不存在的address属性
}
};
// 传统做法
let address;
if (user && user.profile) {
address = user.profile.address;
}
// 使用可选链
let addressWithOptionalChaining = user?.profile?.address;
// 如果user.profile不存在,addressWithOptionalChaining会自动被赋予undefined,而不是抛出错误
通过可选链,我们无需显式检查user
或user.profile
是否存在,就能安全地尝试访问address
属性,大大简化了代码。如果访问路径上的任何部分为null
或undefined
,整个表达式的结果就是undefined
,而不是抛出错误。
可选链操作符与解构赋值、扩展运算符、Rest参数等一起构成了ES6及以后版本中关于对象处理的重要改进。它也属于ES的新语法特性,反映了现代JavaScript在类型安全和代码简洁性方面的发展趋势。
二 Async异步编程
JavaScript中的异步编程是处理延迟操作(如网络请求、文件读写等)的关键技术,旨在不阻塞主线程的同时执行这些操作。
setTimeout
和setInterval
是JavaScript中用于处理异步操作的两个重要函数,它们允许你在某段时间后执行代码,或者定期执行代码,分别用于实现单次延时执行和重复执行。这两个函数都是Window接口的方法,因此在浏览器环境下全局可用。在Node.js环境下,这些方法属于全局对象global,但用法相同。
setTimeout: setTimeout
函数用于设置一个计时器,该计时器在指定的毫秒数之后执行一个函数或指定的一段代码。
语法:
setTimeout(function, delay, ...args);
参数:
function
: 要执行的函数。delay
: 延迟的时间,以毫秒为单位。为0时尽快执行,但仍会异步执行。...args
: 可选,函数执行时传递给函数的参数。
返回值:
- 返回一个定时器的标识,可以用来取消该定时器。
示例:
function greet(name) { console.log('Hello, ' + name + '!'); } // 设置一个3秒后执行的定时器 setTimeout(greet, 3000, 'Alice'); // 可以使用 `clearTimeout(timeoutID);` 取消由 `setTimeout`设置的定时器
在这个示例中,greet
函数将在3秒后执行,并输出“Hello, Alice!”。
setInterval: setInterval
函数用于重复调用函数或执行代码片段,以固定的时间间隔执行。
语法:
setInterval(function, interval, ...args);
参数:
function
: 要定期执行的函数。interval
: 运行之间的时间间隔,以毫秒为单位。...args
: 可选,函数执行时传递给函数的额外参数。
返回值:
- 返回一个定时器的标识,可以用来取消该计时器。
示例:
let count = 0; function incrementCounter() { count++; console.log('Count: ' + count); } // 每秒钟增加count并输出 const intervalId = setInterval(incrementCounter, 1000); // 5秒后停止计时器 setTimeout(() => clearInterval(intervalId), 5000); // clearInterval函数用于取消由setInterval设置的定时器
在这个示例中,incrementCounter
函数每秒执行一次,并输出当前的count
值。5秒后,使用clearInterval
停止这个定期执行。
注意:
- 使用
setTimeout
和setInterval
时应考虑函数的执行时间。如果函数执行时间较长,可能会影响间隔调用的准确性。 setTimeout
的延迟参数并不能保证准确的执行时间,而是在至少延迟指定的毫秒数之后执行。系统的执行队列和任务负载可能会导致实际延迟。- 可以使用
clearTimeout
和clearInterval
函数来取消由setTimeout
和setInterval
设置的定时器。需要传递定时器的标识作为参数。
1 Callback
在过去异步编程的主要方式是回调函数
回调函数(Callbacks)和回调地狱(Callback Hell)
- 回调是 一个被作为参数 传递给另一个函数 并在适当时机被调用 的函数
- 回调允许异步操作完成后执行相关的后续代码。
- 大量嵌套回调(俗称“回调地狱”或“回调金字塔”)会使代码难以阅读和维护。
基本的回调函数示例:
function fetchData(callback) {
setTimeout(() => {
callback(null, 'Data retrieved');
}, 1000);
}
fetchData((error, data) => {
if (error) {
console.error(error);
return;
}
console.log(data);
});
上面的例子中,fetchData
函数模拟从服务器获取数据的异步操作。延迟1秒后,它会调用callback
函数,并传入数据。
回调地狱(Callback Hell)的示例:
function login(user, password, callback) {
setTimeout(() => {
console.log('User logged in');
callback(null, user);
}, 1000);
}
function getUserData(userId, callback) {
setTimeout(() => {
console.log('Got user data');
callback(null, { id: userId, name: 'John Doe' });
}, 1000);
}
function displayUserData(userData, callback) {
setTimeout(() => {
console.log(`User Name: ${userData.name}`);
callback(null, 'Displayed user data');
}, 1000);
}
// 连续的异步操作
login('john', '12345', (error, user) => {
if (error) {
console.error(error);
return;
}
getUserData(user, (error, userData) => {
if (error) {
console.error(error);
return;
}
displayUserData(userData, (error, message) => {
if (error) {
console.error(error);
return;
}
console.log(message);
});
});
});
在这个所谓的“回调地狱”例子中,每一个异步动作完成后,下一个操作都嵌套在上一个的回调函数中,导致代码向右边不断地增加缩进,形成“金字塔”状。这样的代码结构很快就变得难以阅读和维护。
2 Promise
Promise是JavaScript中用于异步编程的一种重要机制。它代表了一个异步操作的最终完成(或失败)及其结果值。Promise在ES6(ECMAScript 2015)中被引入,提供了一种比传统回调函数更好的异步处理方案。
Promise实现原理:
Promise是一种基于状态机的编程模型,它的核心是一个具有三种状态的对象:
- Pending(等待中):初始状态,既不是成功也不是失败。
- Fulfilled(已成功):异步操作成功完成。
- Rejected(已失败):异步操作失败。
Promise对象通过其构造函数创建,构造函数接受一个执行器函数(executor),该函数立即执行,并接受两个参数:resolve
和reject
,分别用于改变Promise的状态为fulfilled或rejected。
与回调函数对比的优势:
- 链式调用:Promise支持链式调用,通过
.then()
和.catch()
方法,使得异步操作的顺序控制更加清晰,避免了回调函数的多层嵌套。 - 错误处理:统一的错误处理机制,允许你通过
.catch()
在链的末尾统一捕捉错误,而不是为每一个异步操作指定一个错误处理回调。 - 状态管理:明确的状态管理机制(pending、fulfilled、rejected),使得异步操作的生命周期更容易理解和控制。
- 组合操作:Promises可以用Promise.all()这类API来组合,以处理多个异步操作。
Promise API详解:
- Promise构造函数:
new Promise(executor)
,其中executor
是一个带有resolve
和reject
参数的函数。 - .then(onFulfilled[, onRejected]):注册成功或失败的回调,当Promise状态变为fulfilled时调用
onFulfilled
,rejected时调用onRejected
。 - .catch(onRejected):仅捕获错误的回调,相当于
.then(null, onRejected)
。 - .finally(onFinally):无论Promise状态如何,都会调用的回调。
- Promise.all(iterable):接收一个Promise对象的数组或具有迭代器接口的对象,只有当所有Promise都变为fulfilled时才变为fulfilled,如果任何一个变为rejected则直接变为rejected。
- Promise.race(iterable):同样是接收一个Promise对象的数组或可迭代对象,但只要其中任何一个Promise变为fulfilled或rejected,就立即以此状态结束。
使用代码示例:
// 创建一个Promise
const fetchData = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true; // 模拟异步操作结果
if (success) {
resolve('数据获取成功!');
} else {
reject('数据获取失败!');
}
}, 2000);
});
// 使用.then处理成功情况,.catch处理错误
fetchData
.then(data => console.log(data))
.catch(error => console.error(error));
.finally(() => {
console.log('Cleanup can be performed here if necessary');
});
// 使用async/await进一步简化
async function fetchDataAsync() {
try {
const result = await fetchData;
console.log(result);
} catch (error) {
console.error(error);
}
}
fetchDataAsync();
3 Generator
Generator在ES6(ECMAScript 2015)中被引入。Generator函数通过function*
语法声明。在函数体内部,使用yield
关键字来暂停函数的执行。每次调用Generator的next()
方法时,Generator函数会执行到下一个yield
表达式处并暂停,同时返回一个对象,该对象包含value
(yield表达式的值)和done
(表示Generator是否已运行完毕)两个属性。
与Promise的比较,好处和优势:
- 同步式的异步编程:使用Generator,异步代码可以以类似同步代码的方式书写和理解,这降低了异步编程的复杂性。
- 更好的控制流:与Promise相比,Generator提供了更细粒度的控制异步操作的能力,因为我们可以精确控制何时执行下一个异步操作。
- 更易于错误处理:通过
try-catch
语句,错误处理可以更加直观地嵌入到异步流程中,而不是像在Promise中通常需要链式调用.catch()
方法。
尽管Generator提供了强大的异步编程能力,但它们通常需要配合Promise
或第三方库(如co
)来更有效地处理异步操作。自async/await
语法(也是在ES2017中引入)的出现后,async/await
作为Generator的语法糖,为异步编程提供了更直观且简化的方式。
以下是一个使用Generator和Promise结合处理异步操作的简单示例:
function fetchSomething() {
// 模拟异步操作
return new Promise(resolve => {
setTimeout(() => resolve('Future value'), 1000);
});
}
function* generatorExample() {
const result = yield fetchSomething(); // 等待Promise解决
console.log(result); // Future value
}
function run(generator) {
const gen = generator();
function go(result) {
if (result.done) return;
result.value.then((value) => {
go(gen.next(value));
});
}
go(gen.next());
}
run(generatorExample);
在这个示例中,fetchSomething
模拟了一个返回Promise的异步操作。generatorExample
Generator函数等待这个异步操作。使用一个名为run
的辅助函数自动处理Generator的执行和异步操作的结果。
yield关键字和next()方法
yield
关键字用于Generator函数内部,它有两个基本用途:
暂停执行:当Generator函数执行到
yield
表达式时,函数会暂停执行,并将yield
后面的表达式的值作为结果返回给调用者。这使得函数能够在不同阶段产出多个值,而不是一次性执行完毕。传递值:Generator函数可以通过调用者提供的
next()
方法带入的参数,来接收外部传入的值。这意味着Generator函数可以在暂停后根据外部输入改变其行为或计算结果。
与yield
关键字相对应,next()
方法用于控制Generator函数的执行流程。每次调用next()
方法,Generator函数会从上次暂停的地方继续执行,直到遇到下一个yield
表达式,然后再次暂停,并返回一个新的迭代结果对象。这个迭代结果对象通常包含两个属性:
value
:表示当前yield
表达式的值,或者是上一次调用next()
时通过参数传递给Generator的值。done
:一个布尔值,表示Generator函数是否已经执行完毕。如果为true
,则表示没有更多的值可以产出,函数已经结束。
function* myGenerator() {
let firstValue = yield 'Hello';
console.log('Received:', firstValue);
let secondValue = yield firstValue + ' World';
console.log('Received again:', secondValue);
}
let gen = myGenerator(); // 创建生成器对象
console.log(gen.next()); // {value: 'Hello', done: false}
console.log(gen.next('From User')); // 输出:Received: From User, {value: 'Hello From User', done: false}
console.log(gen.next('Another message')); // 输出:Received again: Another message, {value: undefined, done: true}
在这个示例中,myGenerator
是一个Generator函数,它包含两个yield
表达式。
- 首次调用
gen.next()
时,函数开始执行直到遇到第一个yield
,输出Hello
并暂停。 - 第二次调用
gen.next()
时,用户传入的'From User'
被赋值给firstValue
,然后继续执行到第二个yield
,并打印接收到的值。 - 最后,第三次调用
gen.next()
完成后续代码的执行,函数结束。
通过这种方式,yield
和next()
机制使得编写复杂的异步控制流变得更加灵活和直观。
Generator为异步编程提供了强大而灵活的工具,但在实际应用中,你会发现async/await
更加常用,因为它提供了更简洁和直观的语法。
4 async/await
async/await
是JavaScript中处理异步操作的一个语法特性,它于ECMAScript 2017(ES8)引入
async/await
实际上是基于Promises和Generators的语法糖。一个使用async
关键字声明的函数会返回一个Promise。当函数执行到await
表达式时,它会暂停函数的执行,等待Promise解决。
- async:将一个函数声明为异步函数,它会自动将返回值包装成Promise对象。
- await:用于等待一个Promise对象的解决或拒绝,只能在
async
函数内部使用。
与Promise和Generator的比较
相对于Promise:
- 更简洁的语法:
async/await
让异步代码的书写和读取更加简洁明了,无需通过.then
或.catch
链式调用。 - 更直观的错误处理:通过
try...catch
能够以同步代码的方式捕获异步操作中的错误,这比Promise中的.catch
方法更加直观和灵活。
相对于Generator:
- 更简单的控制流:
async/await
不需要像Generator一样配合外部的迭代器控制函数的执行,提供了更自然的异步操作流控制。 - 无需特殊库辅助:相对于Generator可能需要如
co
之类的库来控制异步流,async/await
自身就能够良好地处理异步操作。
下面是一个使用async/await
实现的异步文件读取操作的例子:
const fs = require('fs').promises;
async function readFileAsync(filePath) {
try {
const data = await fs.readFile(filePath, 'utf8');
console.log(data);
} catch (error) {
console.error('读取文件出错:', error);
}
}
readFileAsync('./example.txt');
在这个例子中,readFileAsync
是一个异步函数,它等待fs.readFile
的Promise解决。如果文件成功读取,输出文件内容;如果发生错误,通过catch
捕捉到并打印错误信息。这显示了async/await
如何用更直观的方式处理异步操作及其错误。
三 MetaProgramming
在JS(JavaScript)中,元编程(metaprogramming)通常指的是操作和扩展语言的默认行为的能力。这些操作包括改变对象属性的读写行为、改变函数调用的行为、动态修改原型链等。
通过元编程,开发者可以实现如下功能:
- 动态的创建或修改对象的属性和方法;
- 拦截、修改或包装函数调用;
- 管理对象的成员访问;
- 动态地修改对象的原型链;
- 使用反射查询对象的信息,如检查对象是否含有特定的属性或方法。
JavaScript中元编程的一些常见工具和方法包括:
Object.defineProperty()
和Object.defineProperties()
:允许开发者精确地控制对象属性的添加或配置。Function
构造函数:可以在运行时创建新的函数对象。eval()
:可以执行字符串形式的JavaScript代码。Reflect
对象:提供了一些与Proxy
对象方法相对应的静态方法,专门用于可拦截的JavaScript操作,它们与Proxy
handlers的方法是一一对应的。Proxy
对象:可以创建一个对象的代理,通过代理可以自定义对象属性访问、函数调用等基本操作的行为。
元编程的应用非常广泛,它提供了极大的灵活性和强大的功能,但是如果使用不当,也可能导致代码难以维护和理解,甚至引入安全问题。因此,合理和谨慎的使用元编程技术是非常重要的。
1 精确操控对象属性
Object.defineProperty()
和 Object.defineProperties()
是JavaScript中使用 描述符(Descriptor)
来精确添加或修改对象属性的方法,提供了比传统赋值更多的控制能力,包括属性是否可枚举、可写、可配置等。
Object.defineProperty()
Object.defineProperty(obj, prop, descriptor)
方法直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。它接受三个参数:
obj
:要在其上定义属性的对象。prop
:要定义或修改的属性的名称或Symbol
。descriptor
:属性描述符对象,这个对象描述了属性的行为。参照后面对 描述符(Descriptor)的介绍
Object.defineProperty() 使用示例
const person = {};
Object.defineProperty(person, 'name', {
value: 'John',
writable: true, // 允许修改属性值
enumerable: true, // 允许属性被枚举
configurable: true // 允许修改属性描述或删除属性
});
console.log(person.name); // "John"
person.name = 'Jane';
console.log(person.name); // "Jane"
Object.defineProperties()
Object.defineProperties(obj, props)
方法直接在一个对象上定义一个或多个新属性或修改现有属性,并返回该对象。它接受两个参数:
obj
:要定义其属性的对象。props
:要定义其可枚举属性或修改的属性描述符的对象。此对象中的每个属性对应一个属性描述符。
Object.defineProperties() 使用示例
const person = {};
Object.defineProperties(person, {
'name': {
value: 'John',
writable: true
},
'age': {
value: 30,
writable: false // 不允许修改属性值
}
});
console.log(person.name); // "John"
console.log(person.age); // 30
person.name = 'Jane';
console.log(person.name); // "Jane"
// 尝试修改age属性值将无效,因为writable:false
person.age = 25;
console.log(person.age); // 30
在使用这些方法时,需要注意默认情况下,除非显式指定,否则所有配置选项(writable
、configurable
、enumerable
)都默认为false
。这使得属性成为不可枚举、不可写和不可配置的,这是使用这些方法的一个重要考虑点。
描述符(Descriptor)
在JavaScript中,描述符(Descriptor)是与对象属性相关联的一组元数据,控制着属性的行为,如是否可写、可配置或可枚举等。描述符主要分为两种类型:数据描述符(用于普通值属性)和存取描述符(用于getter/setter属性)。描述符属于JavaScript的原型和继承机制的知识领域,特别是与Object.defineProperty()
方法紧密相关,这是ECMAScript 5引入的一个重要特性,用于精确控制对象属性的各个方面。
描述符的组成部分
数据描述符 包含以下可选键值:
value
:属性的值。writable
:布尔值,表示属性的值是否可写。configurable
:布尔值,表示属性是否可被删除或其描述符是否可被修改。
存取描述符 包含以下可选键值:
get
:一个函数,用于获取属性值。set
:一个函数,用于设置属性值。configurable
:同上,表示描述符是否可被修改。
数据描述符示例
const obj = {};
// 使用Object.defineProperty定义一个不可写的属性
Object.defineProperty(obj, 'readOnlyProp', {
value: 'This is a read-only property',
writable: false, // 设置为false,使其不可写
enumerable: true, // 可枚举
configurable: true // 可配置
});
console.log(obj.readOnlyProp); // 输出: This is a read-only property
obj.readOnlyProp = 'Attempt to change'; // 尝试修改,但不会生效
console.log(obj.readOnlyProp); // 输出仍为: This is a read-only property
存取描述符示例
const counterObj = {};
let count = 0;
// 使用getter和setter定义一个计数器属性
Object.defineProperty(counterObj, 'count', {
get: function() {
return count;
},
set: function(value) {
if (value >= 0) {
count = value;
} else {
console.log("Count cannot be negative.");
}
},
enumerable: true,
configurable: true
});
console.log(counterObj.count); // 输出: 0
counterObj.count = 5;
console.log(counterObj.count); // 输出: 5
counterObj.count = -1; // 输出: Count cannot be negative.
console.log(counterObj.count); // 仍然是: 5
2 eval和function
eval
和 Function
构造函数都允许在JavaScript中动态执行字符串形式的代码。尽管它们在某些场景下非常有用,但由于安全和性能的考虑,它们的使用应该非常谨慎。
eval():
eval()
函数接受一个字符串参数,并将这个字符串作为JavaScript代码来执行。如果执行的代码有返回值,则eval()
会返回该值;否则,返回undefined
。
eval
直接在调用它的当前词法作用域中执行字符串代码。这意味着被执行的代码可以访问当前作用域中的变量。
const x = 10;
const y = 20;
const result = eval('x + y'); // 动态执行代码
console.log(result); // 输出:30
- 注意:
eval()
可以执行修改当前作用域变量的代码,这可能会引发安全问题。因此,除非特别必要,通常建议避免使用eval()
。
Function 构造函数
Function
构造函数创建一个新的Function
对象。在JavaScript中,几乎所有函数都是Function
对象。它接受字符串形式的参数列表,最后一个参数是包含函数体代码的字符串。
- 与
eval
不同,通过Function
构造函数创建的函数不会在其声明时的词法作用域中执行,而是在全局作用域中执行。这意味着这样的函数不能访问除全局变量和其自身参数以外的任何变量。
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(10, 20)); // 输出:30
Function的定义
语法定义及各个参数解释如下:
new Function ([arg1[, arg2[, ...argN]],] functionBody)
arg1, arg2, ... argN
:表示新建函数的参数名称,可以有任意多个。每个参数必须为字符串类型。如果函数无参数,此处可以忽略。functionBody
:一个字符串,包含了函数体的 JavaScript 语句,这些语句组成了新 Function 对象的函数体。
- 优点:与
eval
相比,Function
构造函数提供了一定程度的作用域隔离,因此比eval
更安全。
两者的区别
- 安全性:
eval
可以访问和修改当前作用域的任何变量,这可能导致安全问题。而Function
构造函数创建的函数只能访问全局变量,提供了更好的安全性。 - 性能:频繁使用
eval
和Function
构造函数可能对性能产生负面影响,因为它们需要JavaScript引擎解析和编译运行时传入的代码字符串,这个过程通常比直接执行静态代码更耗时。
尽量避免使用eval
和Function
构造函数,特别是在处理外部不可控的输入情况下。如果需要,尽量使用Function
构造函数以提供更好的安全性。同时,寻找这些方法的替代方案,比如使用对象映射代替动态执行代码字符串,通常可以提供更安全、更高效的解决方案。
2 Reflect反射
ES6(ECMAScript 2015)添加了一个全新的全局对象Reflect
,该对象提供了一系列静态方法,用于执行JavaScript对象和函数的反射操作。反射特性主要用于拦截和修改底层JavaScript操作。
反射是一种使程序能够对其自身结构进行自我检查和修改的能力。在JavaScript中,这意味着您可以在运行时动态地对对象的属性和方法进行操作,并拦截这些对象在执行环境中的默认行为。
以下是一些常用的Reflect
方法及其简要解释:
Reflect.apply(target, thisArgument, argumentsList)
:与Function.prototype.apply()
类似,调用一个给定的函数。Reflect.construct(target, argumentsList[, newTarget])
:与new
操作符类似,基于目标构造函数创建一个新实例。Reflect.get(target, propertyKey[, receiver])
:获取对象的属性,类似于target[propertyKey]
。Reflect.set(target, propertyKey, value[, receiver])
:将值分配给对象的属性,类似于target[propertyKey] = value
。Reflect.defineProperty(target, propertyKey, attributes)
:定义或修改对象的属性,与Object.defineProperty()
等效。Reflect.deleteProperty(target, propertyKey)
:像delete
操作符一样删除对象的属性。Reflect.has(target, propertyKey)
:判断对象是否有该属性,等同于propertyKey in target
。Reflect.ownKeys(target)
:返回一个由目标对象的所有自有属性键组成的数组。Reflect.isExtensible(target)
:判断一个对象是否可扩展。Reflect.preventExtensions(target)
:防止新属性被添加到对象。Reflect.getOwnPropertyDescriptor(target, propertyKey)
:得到指定属性的属性描述符,类似于Object.getOwnPropertyDescriptor()
。
// 使用Reflect.apply调用函数
function greet(name) {
return `Hello, ${name}!`;
}
console.log(Reflect.apply(greet, undefined, ['John'])); // 输出 "Hello, John!"
// 使用Reflect.construct创建对象实例
class Person {
constructor(name) {
this.name = name;
}
}
const john = Reflect.construct(Person, ['John']);
console.log(john.name); // 输出 "John"
// 使用Reflect.get获取对象属性
const obj = { x: 1, y: 2 };
console.log(Reflect.get(obj, 'x')); // 输出 1
// 使用Reflect.set设置对象属性
Reflect.set(obj, 'z', 3);
console.log(obj.z); // 输出 3
// 使用Reflect.defineProperty定义属性
Reflect.defineProperty(obj, 'writable', {
value: 4,
writable: false
});
console.log(obj.writable); // 输出 4
// 注意:设置writable为false后,obj.writable属性不可再变更
// 使用Reflect.deleteProperty删除对象属性
Reflect.deleteProperty(obj, 'z');
console.log(obj.z); // 输出 undefined
// 使用Reflect.has检查属性存在
console.log(Reflect.has(obj, 'x')); // 输出 true
// 使用Reflect.ownKeys列出对象的键
console.log(Reflect.ownKeys(obj)); // 输出 ["x", "y", "writable"]
// 使用Reflect.isExtensible检查对象是否可扩展
console.log(Reflect.isExtensible(obj)); // 输出 true
// 使用Reflect.preventExtensions阻止对象扩展
Reflect.preventExtensions(obj);
console.log(Reflect.isExtensible(obj)); // 输出 false
// 使用Reflect.getOwnPropertyDescriptor获取属性描述符
const descriptor = Reflect.getOwnPropertyDescriptor(obj, 'x');
console.log(descriptor); // 输出 {value: 1, writable: true, enumerable: true, configurable: true}
当配合Proxy
对象使用时,Reflect
方法在语义上与对应的Proxy
处理程序方法对应,让Proxy
的默认行为更容易实现,同时也确保了Proxy
处理程序的返回值符合期望。
3 Proxy对象代理
ES6引入了一项强大的新特性:代理(Proxy)。代理可以用来创建一个对象的代理,允许你拦截并重新定义基本操作,例如属性查找、赋值、枚举、函数调用等。
代理模式是一种设计模式,它通过引入一个代理对象来控制对另一个对象的访问。在JavaScript中,Proxy
对象用作另一个对象的代理,可以拦截并重定义底层操作。
使用场景:
- 访问控制:可以控制对对象属性的读写权限。
- 数据绑定:对象属性的变化可以自动更新UI。
- 验证:在属性被赋值前校验数据。
- 日志和跟踪:跟踪对象属性的读写或方法调用。
- 延迟初始化:仅在实际需要时才创建对象。
创建一个Proxy
对象的基本语法是:
const proxy = new Proxy(target, handler);
target
:一个对象,其他代码会对其进行访问。handler
:一个对象,其声明了若干"陷阱"方法,用以定义在执行各种操作时代理target
的行为。
创建一个简单的读取和写入拦截的代理:
const target = {
message: "Hello, world!"
};
const handler = {
get(target, prop, receiver) {
console.log(`读取 ${prop}`);
return Reflect.get(...arguments);
},
set(target, prop, value, receiver) {
console.log(`设置 ${prop} 为 ${value}`);
return Reflect.set(...arguments);
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.message); // 读取 message Hello, world!
proxy.message = "Hello, Proxy!"; // 设置 message 为 Hello, Proxy!
使用Proxy实现简单的验证:
const validator = {
set(target, prop, value) {
if (prop === 'age') {
if (typeof value !== 'number' || Number.isNaN(value) || value <= 0) {
throw new Error('年龄必须是一个大于0的数字');
}
}
target[prop] = value;
return true;
}
};
const person = new Proxy({}, validator);
person.age = 25; // 有效
// person.age = 'invalid'; // 抛出异常
handler对象及其方法
代理(Proxy
)的handler
对象是一个包含“陷阱”(trap)方法的对象,这些方法定义了操作代理时的自定义行为。当执行一个操作时,如果handler
对象有相应的“陷阱”方法,就会拦截原始操作,执行“陷阱”方法。下面是一些常用的“陷阱”方法及其说明:
get(target, propKey, receiver)
拦截对象属性的读取操作。
target
:目标对象。propKey
:属性名。receiver
:代理或继承代理的对象。
set(target, propKey, value, receiver)
拦截对象属性的设置操作。
target
:目标对象。propKey
:属性名。value
:属性将被赋予的值。receiver
:最初被调用的对象。
has(target, propKey)
拦截propKey in proxy
的操作,返回一个布尔值。
target
:目标对象。propKey
:属性名。
deleteProperty(target, propKey)
拦截delete proxy[propKey]
的操作,返回一个布尔值。
target
:目标对象。propKey
:属性名。
ownKeys(target)
拦截对象自身属性的读取操作,返回一个数组。该方法返回目标对象所有自有属性的属性名。
target
:目标对象。
getOwnPropertyDescriptor(target, propKey)
拦截Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。
target
:目标对象。propKey
:属性名。
defineProperty(target, propKey, propDesc)
拦截Object.defineProperty(proxy, propKey, propDesc)
,返回一个布尔值。
target
:目标对象。propKey
:属性名。propDesc
:属性描述符。
getPrototypeOf(target)
拦截获取对象原型的操作,返回对象的原型([[Prototype]]
)。
target
:目标对象。
setPrototypeOf(target, prototype)
拦截设置对象原型的操作,返回一个布尔值。
target
:目标对象。prototype
:一个对象或null
。
isExtensible(target)
拦截Object.isExtensible(proxy)
,返回一个布尔值。
target
:目标对象。
preventExtensions(target)
拦截Object.preventExtensions(proxy)
,返回一个布尔值。
target
:目标对象。
apply(target, thisArg, argumentsList)
拦截函数的调用、call
和apply
操作。
target
:目标函数。thisArg
:this
绑定的值。argumentsList
:调用函数时传入的参数列表数组。
construct(target, argumentsList, newTarget)
拦截new
操作,用来初始化对象。
target
:目标函数。argumentsList
:构造函数的参数列表。newTarget
:被new
调用的构造函数。
handler
对象可以根据需要包含上面提到的任意多个陷阱方法,以实现对目标对象的各种操作的拦截和自定义处理。这使得Proxy
成为强大的元编程工具,允许开发人员对JavaScript的基本操作进行控制和扩展。
常用的包含“陷阱”(trap)方法::
虽然handler对象中含“陷阱”(trap)方法的很多,但常用的只有下面几个:
get
:拦截对象属性的读取。set
:拦截对象属性的设置。has
:拦截in
操作符。deleteProperty
:拦截delete
操作符。apply
:拦截函数调用。construct
:拦截new
命令。
通过Proxy
和Reflect
的合作,您可以轻松地控制和修改对象的底层操作行为,从而实现高级抽象和功能,如数据绑定、访问控制、以及其他自定义行为。