ECMAScript6+

【注】此处ES6+泛指ES6(ES5)之后所有版本的新特性

ECMAScript语法改进

参考:ECMAScript2015~2020语法全解析

变量及解构赋值

ES6(ECMAScript 2015)对JavaScript的变量声明和管理方式进行了重大改进,主要体现在引入了新的关键字let和const,以及更严格的变量作用域规则

关键字 类型 定义 可变性 作用域
var 变量(旧) 可以重复定义 不能限制修改 函数级作用域
let 变量 不能重复定义 可以修改 块级作用域
const 常量 不能重复定义 不可修改 块级作用域

解构赋值 :

解构赋值是JavaScript中的一种特殊语法,它允许你将数组或对象的属性直接赋值给不同的变量。这种机制简化了从复合数据结构中提取数据的过程,使得代码更加简洁且易于阅读。

数组解构赋值: 对于数组,你可以按照索引位置将元素值赋给对应位置的变量:

javascript
let [a, b, c] = [1, 2, 3];
// 这里 a=1, b=2, c=3

如果变量的数量少于数组的长度,可以省略部分元素:

javascript
let [a, , c] = [1, 2, 3];
// 这里 a=1, c=3,中间的元素被忽略

还可以使用…rest语法来收集剩余的元素:

javascript
let [a, ...rest] = [1, 2, 3, 4, 5];
// 这里 a=1, rest=[2, 3, 4, 5]

对象解构赋值: 对象解构赋值则是根据属性名来匹配并赋值:

javascript
let {name, age} = {name: 'Alice', age: 30};
// 这里 name='Alice', age=30

你可以指定变量名与属性名不一致的情况,通过冒号(:)实现:

javascript
let {name: userName, age: userAge} = {name: 'Bob', age: 25};
// 这里 userName='Bob', userAge=25

默认值也可以在解构时设定,如果属性不存在或值为undefined,则使用默认值:

javascript
let {height = 170, weight} = {weight: 60};
// 这里 height=170(默认值),weight=60

【小结】

  • 解构赋值就是把数据结构分解,然后给变量进行赋值
  • 如果解构不成功,变量跟数值个数不匹配的时候,变量的值为undefined
  • 数组解构用中括号包裹,多个变量用逗号隔开,对象解构用花括号包裹,多个变量用逗号隔开

参数展开和剩余参数

ES6(ECMAScript 2015)引入了参数展开(Spread Syntax)特性,它允许你将数组或者可迭代对象的内容展开为独立的元素。这一特性在函数调用、数组构造、对象字面量等方面非常有用。

函数调用中的参数展开

当你需要将数组的元素作为单独的参数传递给函数时,可以使用展开语法(…)。

javascript
// 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

数组合并

参数展开也可以用来简便地合并数组。

javascript
let array1 = [1, 2, 3];
let array2 = [4, 5, 6];
let combined = [...array1, ...array2];
console.log(combined); // 输出 [1, 2, 3, 4, 5, 6]

数组拷贝

参数展开可以用来快速创建现有数组的浅拷贝。

javascript
let originalArray = [1, 2, 3];
let copiedArray = [...originalArray];
console.log(copiedArray); // 输出 [1, 2, 3]

在对象字面量中展开

除了在数组中使用,参数展开还可以用于对象字面量,以合并对象的属性。

javascript
let obj1 = { a: 1, b: 2 };
let obj2 = { ...obj1, c: 3 };

console.log(obj2); // 输出 { a: 1, b: 2, c: 3 }

剩余参数(Rest Parameters)

与参数展开相对应的是剩余参数(Rest Parameters),它使用同样的三点(…)语法,但用在函数参数列表中,用于收集不确定数量的参数为一个数组。

javascript
function sumAll(...numbers) {
  return numbers.reduce((acc, curr) => acc + curr, 0);
}

console.log(sumAll(1, 2, 3, 4, 5)); // 输出 15

箭头函数及this

在ES6中引入的箭头函数(Arrow Function)提供了一种更简洁的函数表达方式,并且改进了函数内this值的行为。

箭头函数使用=>的新语法代替了传统的function关键字。例如:

javascript
// 传统函数表达式
const traditionalFunc = function(a, b) {
  return a + b;
};

// 箭头函数表达式
const arrowFunc = (a, b) => a + b;

如果箭头函数体只包含一个表达式,则省略return关键字和花括号;如果函数体有多个语句,需要使用花括号,并且需要写return(如果需要返回值):

javascript
const arrowFuncWithMoreBody = (a, b) => {
  const result = a + b;
  return result;
};

无参数或多个参数需要用括号括起来,单个参数可以省略括号:

javascript
const noParams = () => 'No params';
const singleParam = a => a * 2;
const multipleParams = (a, b) => a + b;

this 指向的改进

与传统的函数不同,箭头函数不绑定自己的this值。箭头函数内的this值由外围(即定义时的)最近一个非箭头函数决定。这被称为“词法作用域”或“静态作用域”:

javascript
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

注意

  1. 箭头函数没有arguments对象:尽管箭头函数没有自己的arguments对象,但可以通过剩余参数语法来访问函数的参数。

  2. 箭头函数不能用作构造器:尝试使用new关键字调用箭头函数会抛出错误。

  3. 不绑定this:这意味着箭头函数内部的this值不会因方法调用形式(obj.method())而改变。

代码示例:

javascript
// 用作普通函数
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指向经常发生的错误或混淆。

模板字符串

ES6引入了模板字面量(也称模板字符串),是增强版的字符串字面量,可以使用反引号 (`) 来创建。模板字面量提供了很多新的特性来创建动态字符串。

1. 基本字符串

模板字面量可以像普通字符串一样使用,只是使用反引号包裹:

javascript
const basicString = `hello, world!`;
console.log(basicString); // 输出: hello, world!

2. 多行字符串

模板字面量轻松创建多行字符串,无需使用字符串连接符或者特殊的换行符:

javascript
const multiLineString = `This is a string
that spans across
multiple lines`;
console.log(multiLineString);

输出结果会保留换行格式。

3. 字符串插值

可以在字符串中嵌入变量或表达式,使用${}语法:

javascript
const name = 'Alice';
const greeting = `Hello, ${name}!`;
console.log(greeting); // 输出: Hello, Alice!

任何有效的JavaScript表达式都可以被嵌入到${}中,表达式的结果将被拼接到字符串中:

javascript
const price = 10;
const taxRate = 0.25;
const total = `Total: $${(price * (1 + taxRate)).toFixed(2)}`;
console.log(total); // 输出: Total: $12.50

4. 嵌套模板

模板字面量可以嵌套。可以在模板字面量中嵌入其他模板字面量:

javascript
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. 标签模板

模板字面量可以被函数标签化。标签是一个函数,模板字面量是其参数。这个函数可以对模板字符串进行处理:

javascript
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>标签内,以实现高亮效果。

模板字面量通过简洁且灵活的语法使得创建和处理字符串变得更加容易,尤其是在需要动态插入变量和表达式,或者构建包含多行和嵌套内容的字符串时。这些特性能极大地提高代码的可读性和效率。

增强的对象字面量

ES6(ECMAScript 2015)对对象字面量进行了增强,提供了几个新的语法糖,使得对象的创建和操作更加简洁和直观

1. 属性值缩写(Property Shorthand)

当对象的属性名与局部变量名相同,可以只写一个名称,省略:和值部分。

传统写法

javascript
const name = 'John Doe';
const age = 30;

const person = {
  name: name,
  age: age
};

ES6写法

javascript
const name = 'John Doe';
const age = 30;

const person = { name, age };

2. 方法简写(Method Shorthand)

可以省略方法的function关键字和冒号。

传统写法

javascript
const person = {
  sayHello: function() {
    console.log('Hello!');
  }
};

ES6写法

javascript
const person = {
  sayHello() {
    console.log('Hello!');
  }
};

3. 计算属性名(Computed Property Names)

可以在对象字面量中使用方括号[]来设置动态的属性名。

javascript
const propertyKey = 'status';

const project = {
  ['project_' + propertyKey]: 'active'
};

console.log(project.project_status); // 输出: active

4. 设置原型(Setting the Prototype)

使用__proto__可以在对象字面量中直接设置该对象的原型。

javascript
const animal = {
  isAnimal: true
};

const dog = {
  __proto__: animal,
  bark() {
    console.log('Woof!');
  }
};

console.log(dog.isAnimal); // 输出: true
dog.bark(); // 输出: Woof!

新增数据类型

ES6(ECMAScript 2015)为JavaScript语言引入了几种新的数据类型,旨在丰富语言的功能,提高开发效率和代码的可维护性。

1. Symbol(符号)

Symbol是一种原始数据类型,用于生成唯一的、不可变的值。Symbols非常适合用作对象的键,以避免属性名的冲突。由于每个Symbol值都是唯一的,所以它们可以作为对象属性的唯一标识符。

javascript
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保持了键值对的插入顺序,并提供了更丰富的方法来操作这些键值对。

javascript
const map = new Map();
map.set('key1', 'value1');
map.set('key2', 'value2');
console.log(map.get('key1')); // 输出: value1

3. Set(集合)

Set是一个不包含重复值的有序列表。它可以用于去重、集合运算等场景。

javascript
const set = new Set([1, 2, 3, 4, 4]);
console.log(set.size); // 输出: 4,因为重复的4被自动去重

4. WeakMap(弱映射)

WeakMap类似于Map,但是其键必须是对象,而且对这些键的引用是弱引用。这意味着如果对象没有其他引用并且会被垃圾回收,WeakMap中的对应条目也会自动消失。

javascript
const wm = new WeakMap();
const obj = {};
wm.set(obj, 'data');
// 如果obj没有其他引用,它将被垃圾回收,同时WeakMap中的条目也会被清除

5. WeakSet(弱集合)

WeakSet与Set类似,但它只接受对象作为成员,并且对这些成员的引用是弱的。同样,如果成员对象没有其他引用,它会被垃圾回收,同时WeakSet中对应的成员也会被移除。

javascript
const ws = new WeakSet();
const obj = {};
ws.add(obj);
// 当obj没有其他引用时,它将被垃圾回收,WeakSet中的obj也会随之移除

模块系统的改进

ES6(ECMAScript 2015)对JavaScript的模块系统进行了根本性的改进,引入了原生的模块支持,这在之前是通过非标准的解决方案如AMD(异步模块定义)和CommonJS(主要用于Node.js环境)来实现的。ES6模块的关键特性包括静态加载、导入和导出机制,以及模块作用域的明确界定。以下是ES6模块系统的主要改进点及导入导出方式与之前的区别:

  1. 静态加载(编译时加载)

    • ES6模块在编译阶段就确定了模块间的依赖关系,这使得工具可以进行静态分析,优化代码,比如tree-shaking(移除未使用的代码)。
    • 与之相对,CommonJS模块在运行时动态加载,这意味着只有在运行时才能解析模块依赖,无法进行有效的静态分析和优化。
  2. 导入导出语法

    • ES6使用importexport关键字来进行模块的导入和导出,语法清晰且语义明确。
    • CommonJS使用require来导入模块,使用module.exportsexports来导出模块,这种方式在处理复杂模块结构时可能会显得混乱。
  3. 默认导出与命名导出

    • ES6允许一个模块有默认导出(export default),也可以有多个命名导出(export)。
    • CommonJS主要依赖于导出一个对象,其中包含所有的模块成员,虽然也可以导出单一值,但不区分默认导出和命名导出。
  4. 模块作用域

    • ES6模块中,顶级变量具有块级作用域,只在模块内部可见,避免了全局污染。
    • CommonJS模块中的顶级变量实质上是模块作用域的,但在Node.js环境中,它们可以通过global对象间接访问到,可能导致全局污染。

ES6模块导入导出示例

导出模块 (myModule.js):

javascript
// 命名导出
export const PI = 3.14;
export function add(a, b) {
    return a + b;
}

// 默认导出
export default function greet(name) {
    console.log(`Hello, ${name}!`);
}

导入模块 (main.js):

javascript
// 导入默认导出
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

CommonJS模块导入导出示例(对比)

导出模块 (myModule.js):

javascript
// 命名导出
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):

javascript
// 导入默认导出
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

类(Class)和对象

在ES6之前,JavaScript使用基于原型链的对象创建和继承机制,这种方式比较灵活但也相对复杂,容易造成理解和维护上的困难。ES6引入了class语法,使得面向对象编程的语法更加接近于其他面向对象语言,如Java或C++,尽管底层仍然是基于原型的实现。

在ES5及更早版本中,类的实现通常依靠构造函数和原型链:

javascript
// 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)、方法、静态方法和属性。

javascript
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关键字来调用父类的构造函数或方法。

示例:

javascript
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关键字定义。

示例:

javascript
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

迭代器Iterators

在ES6之前,JavaScript中没有内建的迭代器和遍历机制,通常需要使用循环(如forwhile)来遍历数组或对象的属性。ES6引入了迭代器(Iterator)和可迭代协议(Iterable protocol),使得数据集合的遍历变得更加灵活和高效。

ES6迭代器的改进

迭代器是遵循迭代器协议的对象,该协议定义了一个next方法,每次调用返回集合中的下一个元素。一个对象若要成为可迭代对象,需要实现可迭代协议,即该对象要有一个Symbol.iterator属性,它是一个函数,返回一个迭代器。

这项改进的好处包括:

  • 统一的遍历接口:对所有可迭代对象,如数组、新引入的MapSet,甚至是字符串,都可以使用相同的迭代协议进行遍历。
  • 更好的控制遍历过程:通过迭代器的next方法,可以更精细地控制遍历的过程,比如可以在满足特定条件时提前终止遍历。
  • 使得数据结构的操作更为抽象和高级:例如,可以使用新引入的for...of循环、扩展运算符(...)、解构赋值等语法,让数据操作更加简洁和表达性更强。

在ES6之前的遍历方法

javascript
var myArray = [1, 2, 3];
for (var i = 0; i < myArray.length; i++) {
  console.log(myArray[i]);
}

使用ES6的迭代器

对于数组来说,它是天然支持迭代协议的:

javascript
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

使用for…of遍历可迭代对象

for...of循环是基于迭代器协议的高级语法,让遍历操作变得更为简洁:

javascript
let myArray = [1, 2, 3];
for (let value of myArray) {
  console.log(value); // 依次输出1, 2, 3
}

Optional Chaining

可选链(Optional Chaining)操作符最初是在ES2020(ECMAScript 2020)规范中正式引入的。

在可选链出现之前,如果要安全地访问一个对象的深层属性,开发者通常需要逐级检查每一级是否为nullundefined,以避免运行时错误。例如,访问obj?.property1?.property2这样的路径时,如果不使用可选链,可能需要写成:

javascript
if (obj && obj.property1 && obj.property1.property2) {
    // 安全地使用 obj.property1.property2
}

这种写法不仅冗长,还降低了代码的可读性和维护性,特别是在有多层嵌套的情况下。

代码使用示例

假设我们有一个用户对象,但不确定其中的某些属性是否存在:

javascript
let user = {
    profile: {
        // 可能存在也可能不存在的address属性
    }
};

// 传统做法
let address;
if (user && user.profile) {
    address = user.profile.address;
}

// 使用可选链
let addressWithOptionalChaining = user?.profile?.address;

// 如果user.profile不存在,addressWithOptionalChaining会自动被赋予undefined,而不是抛出错误

通过可选链,我们无需显式检查useruser.profile是否存在,就能安全地尝试访问address属性,大大简化了代码。如果访问路径上的任何部分为nullundefined,整个表达式的结果就是undefined,而不是抛出错误。

可选链操作符与解构赋值、扩展运算符、Rest参数等一起构成了ES6及以后版本中关于对象处理的重要改进。它也属于ES的新语法特性,反映了现代JavaScript在类型安全和代码简洁性方面的发展趋势。


Async异步编程详解

JavaScript中的异步编程是处理延迟操作(如网络请求、文件读写等)的关键技术,旨在不阻塞主线程的同时执行这些操作。

setTimeoutsetInterval是JavaScript中用于处理异步操作的两个重要函数,它们允许你在某段时间后执行代码,或者定期执行代码,分别用于实现单次延时执行和重复执行。

这两个函数都是Window接口的方法,因此在浏览器环境下全局可用。在Node.js环境下,这些方法属于全局对象global,但用法相同。

setTimeout

setTimeout函数用于设置一个计时器 —> 在指定的毫秒数之后执行一个函数或指定的一段代码

javascript
setTimeout(function, delay, ...args);
  • 参数

    • function: 要执行的函数。
    • delay: 延迟的时间,以毫秒为单位。为0时尽快执行,但仍会异步执行。
    • ...args: 可选,函数执行时传递给函数的参数。
  • 返回值

    • 返回一个定时器的标识,可以用来取消该定时器。

    示例:

    javascript
    function greet(name) {
      console.log('Hello, ' + name + '!');
    }
    
    // 设置一个3秒后执行的定时器
    setTimeout(greet, 3000, 'Alice');
    
    // 可以使用 `clearTimeout(timeoutID);` 取消由 `setTimeout`设置的定时器
    

在这个示例中,greet函数将在3秒后执行,并输出“Hello, Alice!”。


setInterval

setInterval: setInterval函数用于重复调用函数或执行代码片段,以固定的时间间隔执行。

javascript
setInterval(function, interval, ...args);
  • 参数

    • function: 要定期执行的函数。
    • interval: 运行之间的时间间隔,以毫秒为单位。
    • ...args: 可选,函数执行时传递给函数的额外参数。
  • 返回值

    • 返回一个定时器的标识,可以用来取消该计时器。

    示例

    javascript
    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停止这个定期执行。

注意

  • 使用setTimeoutsetInterval时应考虑函数的执行时间。如果函数执行时间较长,可能会影响间隔调用的准确性。
  • setTimeout的延迟参数并不能保证准确的执行时间,而是在至少延迟指定的毫秒数之后执行。系统的执行队列和任务负载可能会导致实际延迟。
  • 可以使用clearTimeoutclearInterval函数来取消由setTimeoutsetInterval设置的定时器。需要传递定时器的标识作为参数。

Callback

在过去异步编程的主要方式是回调函数

回调函数(Callbacks)和回调地狱(Callback Hell)

  • 回调是 一个被作为参数 传递给另一个函数 并在适当时机被调用 的函数
  • 回调允许异步操作完成后执行相关的后续代码。
  • 大量嵌套回调(俗称“回调地狱”或“回调金字塔”)会使代码难以阅读和维护。

基本的回调函数示例:

javascript
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)的示例:

javascript
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);
    });
  });
});

在这个所谓的“回调地狱”例子中,每一个异步动作完成后,下一个操作都嵌套在上一个的回调函数中,导致代码向右边不断地增加缩进,形成“金字塔”状。这样的代码结构很快就变得难以阅读和维护。


Promise

Promise是JavaScript中用于异步编程的一种重要机制。它代表了一个异步操作的最终完成(或失败)及其结果值。Promise在ES6(ECMAScript 2015)中被引入,提供了一种比传统回调函数更好的异步处理方案。

Promise实现原理: Promise是一种基于状态机的编程模型,它的核心是一个具有三种状态的对象:

  1. Pending(等待中):初始状态,既不是成功也不是失败。
  2. Fulfilled(已成功):异步操作成功完成。
  3. Rejected(已失败):异步操作失败。

Promise对象通过其构造函数创建,构造函数接受一个执行器函数(executor),该函数立即执行,并接受两个参数:resolvereject,分别用于改变Promise的状态为fulfilled或rejected。

与回调函数对比的优势

  1. 链式调用:Promise支持链式调用,通过.then().catch()方法,使得异步操作的顺序控制更加清晰,避免了回调函数的多层嵌套。
  2. 错误处理:统一的错误处理机制,允许你通过.catch()在链的末尾统一捕捉错误,而不是为每一个异步操作指定一个错误处理回调。
  3. 状态管理:明确的状态管理机制(pending、fulfilled、rejected),使得异步操作的生命周期更容易理解和控制。
  4. 组合操作:Promises可以用Promise.all()这类API来组合,以处理多个异步操作。

Promise API详解

  • Promise构造函数new Promise(executor),其中executor是一个带有resolvereject参数的函数。
  • .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,就立即以此状态结束。

使用代码示例

javascript
// 创建一个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();

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结合处理异步操作的简单示例:

javascript
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的异步操作。generatorExampleGenerator函数等待这个异步操作。使用一个名为run的辅助函数自动处理Generator的执行和异步操作的结果。

Generator为异步编程提供了强大而灵活的工具,但在实际应用中,你会发现async/await更加常用,因为它提供了更简洁和直观的语法。


async/await

async/await是JavaScript中处理异步操作的一个语法特性,它于ECMAScript 2017(ES8)引入

async/await实际上是基于Promises和Generators的语法糖。一个使用async关键字声明的函数会返回一个Promise。当函数执行到await表达式时,它会暂停函数的执行,等待Promise解决。

  • async:将一个函数声明为异步函数,它会自动将返回值包装成Promise对象。
  • await:用于等待一个Promise对象的解决或拒绝,只能在async函数内部使用。

下面是一个使用async/await实现的异步文件读取操作的例子:

javascript
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对象:可以创建一个对象的代理,通过代理可以自定义对象属性访问、函数调用等基本操作的行为。

元编程的应用非常广泛,它提供了极大的灵活性和强大的功能,但是如果使用不当,也可能导致代码难以维护和理解,甚至引入安全问题。因此,合理和谨慎的使用元编程技术是非常重要的。

精确操控对象属性

Object.defineProperty()Object.defineProperties() 是JavaScript中使用 描述符(Descriptor) 来精确添加或修改对象属性的方法,提供了比传统赋值更多的控制能力,包括属性是否可枚举、可写、可配置等。

Object.defineProperty()

Object.defineProperty(obj, prop, descriptor) 方法直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。它接受三个参数:

  1. obj:要在其上定义属性的对象。
  2. prop:要定义或修改的属性的名称或Symbol
  3. descriptor:属性描述符对象,这个对象描述了属性的行为。参照后面对 描述符(Descriptor)的介绍

Object.defineProperty() 使用示例

javascript
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) 方法直接在一个对象上定义一个或多个新属性或修改现有属性,并返回该对象。它接受两个参数:

  1. obj:要定义其属性的对象。
  2. props:要定义其可枚举属性或修改的属性描述符的对象。此对象中的每个属性对应一个属性描述符。

Object.defineProperties() 使用示例

javascript
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

在使用这些方法时,需要注意默认情况下,除非显式指定,否则所有配置选项(writableconfigurableenumerable)都默认为false。这使得属性成为不可枚举、不可写和不可配置的,这是使用这些方法的一个重要考虑点。


描述符(Descriptor)

在JavaScript中,描述符(Descriptor)是与对象属性相关联的一组元数据,控制着属性的行为,如是否可写、可配置或可枚举等。描述符主要分为两种类型:数据描述符(用于普通值属性)和存取描述符(用于getter/setter属性)。描述符属于JavaScript的原型和继承机制的知识领域,特别是与Object.defineProperty()方法紧密相关,这是ECMAScript 5引入的一个重要特性,用于精确控制对象属性的各个方面。

描述符的组成部分

数据描述符 包含以下可选键值:

  • value:属性的值。
  • writable:布尔值,表示属性的值是否可写。
  • configurable:布尔值,表示属性是否可被删除或其描述符是否可被修改。

存取描述符 包含以下可选键值:

  • get:一个函数,用于获取属性值。
  • set:一个函数,用于设置属性值。
  • configurable:同上,表示描述符是否可被修改。

数据描述符示例

javascript
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

存取描述符示例

javascript
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

eval和function

evalFunction 构造函数都允许在JavaScript中动态执行字符串形式的代码。尽管它们在某些场景下非常有用,但由于安全和性能的考虑,它们的使用应该非常谨慎。

eval()

eval() 函数接受一个字符串参数,并将这个字符串作为JavaScript代码来执行。如果执行的代码有返回值,则eval()会返回该值;否则,返回undefined

  • eval直接在调用它的当前词法作用域中执行字符串代码。这意味着被执行的代码可以访问当前作用域中的变量。
javascript
const x = 10;
const y = 20;
const result = eval('x + y'); // 动态执行代码
console.log(result); // 输出:30
  • 注意eval() 可以执行修改当前作用域变量的代码,这可能会引发安全问题。因此,除非特别必要,通常建议避免使用eval()

Function 构造函数

Function 构造函数创建一个新的Function对象。在JavaScript中,几乎所有函数都是Function对象。它接受字符串形式的参数列表,最后一个参数是包含函数体代码的字符串。

  • eval不同,通过Function构造函数创建的函数不会在其声明时的词法作用域中执行,而是在全局作用域中执行。这意味着这样的函数不能访问除全局变量和其自身参数以外的任何变量。
javascript
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(10, 20)); // 输出:30
  • 优点:与eval相比,Function构造函数提供了一定程度的作用域隔离,因此比eval更安全。

两者的区别

  • 安全性eval可以访问和修改当前作用域的任何变量,这可能导致安全问题。而Function构造函数创建的函数只能访问全局变量,提供了更好的安全性。
  • 性能:频繁使用evalFunction构造函数可能对性能产生负面影响,因为它们需要JavaScript引擎解析和编译运行时传入的代码字符串,这个过程通常比直接执行静态代码更耗时。

尽量避免使用evalFunction构造函数,特别是在处理外部不可控的输入情况下。如果需要,尽量使用Function构造函数以提供更好的安全性。同时,寻找这些方法的替代方案,比如使用对象映射代替动态执行代码字符串,通常可以提供更安全、更高效的解决方案。


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()
javascript
// 使用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处理程序的返回值符合期望。


Proxy对象代理

ES6引入了一项强大的新特性:代理(Proxy)。代理可以用来创建一个对象的代理,允许你拦截并重新定义基本操作,例如属性查找、赋值、枚举、函数调用等。

代理模式是一种设计模式,它通过引入一个代理对象来控制对另一个对象的访问。在JavaScript中,Proxy对象用作另一个对象的代理,可以拦截并重定义底层操作。

使用场景

  • 访问控制:可以控制对对象属性的读写权限。
  • 数据绑定:对象属性的变化可以自动更新UI。
  • 验证:在属性被赋值前校验数据。
  • 日志和跟踪:跟踪对象属性的读写或方法调用。
  • 延迟初始化:仅在实际需要时才创建对象。

创建一个Proxy对象的基本语法是:

javascript
const proxy = new Proxy(target, handler);
  • target:一个对象,其他代码会对其进行访问。
  • handler:一个对象,其声明了若干"陷阱"方法,用以定义在执行各种操作时代理target的行为。

创建一个简单的读取和写入拦截的代理:

javascript
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实现简单的验证:

javascript
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'; // 抛出异常

常用的包含“陷阱”(trap)方法:: 虽然handler对象中含“陷阱”(trap)方法的很多,但常用的只有下面几个:

  • get:拦截对象属性的读取。
  • set:拦截对象属性的设置。
  • has:拦截in操作符。
  • deleteProperty:拦截delete操作符。
  • apply:拦截函数调用。
  • construct:拦截new命令。

通过ProxyReflect的合作,您可以轻松地控制和修改对象的底层操作行为,从而实现高级抽象和功能,如数据绑定、访问控制、以及其他自定义行为。