Home>Article>Web Front-end> Comprehensive mastery of the new features of ECMAScript

Comprehensive mastery of the new features of ECMAScript

coldplay.xixi
coldplay.xixi forward
2020-10-22 18:34:05 1936browse

Todayjavascriptcolumn will introduce you to the new features of ECMAScript.

Comprehensive mastery of the new features of ECMAScript

After ES6 became the next generation standard for JavaScript, the standards committee (TC39) will release a new version of ES every year, and many new versions are introduced in each version. Practical new features. In daily project development, if we master these practical new features, development efficiency will be greatly improved. Let us fully grasp these new features of ES~

Let and Const

Previously used var to define variables, which provides us with a new way
let is used to declare variables, and const is used to declare constants.

How to use

const TAG = "我是常量";let a; a = 2;console.log(TAG, "a=" + a); //我是常量 a=2复制代码

Four features

1. Only valid within block-level scope

let and const add new block-level functions to JavaScript Domain, usually, the scope owned by the code wrapped by {} is the block-level scope. The declared variables or constants are only valid within the block-level scope and cannot be accessed from outside.

if (true) { //外层块级作用域 let a = 1; const A = 1; if (true) { //内层块级作用域 let a = 2; } console.log(a,A); //(1)输出:1 , 1}console.log(a); //(2)Uncaught ReferenceError: a is not defined复制代码

There are two block-level scopes above, both of which declare variable a, but the outer block-level scope has nothing to do with the inner block-level scope, so the output at (1) is the outer variable. Value 1, (2) accesses a variable that is not defined in a block-level scope, so an error will be reported.

Another example of understanding block-level scope.

//for循环体内的定时器//在ES6之前,是没有块级作用域的,变量用var声明,直接挂载在全局作用域上for (var i = 0; i 

2. Temporary dead zone

cannot be used before variables and constants are declared.
The let and const commands will cause the block to form a closed scope. If used before declaration, an error will be reported. This is syntactically called "temporary dead zone" (TDZ for short).

if (true) { tmp = "abc"; // ReferenceError let tmp; }复制代码

3. Cannot be declared repeatedly

let a = 1;let a = 2;//报错 SyntaxError: Identifier 'a' has already been declared const B=1;const B=2;//报错 SyntaxError: Identifier 'B' has already been declared 复制代码

4. Variables that do not belong to the top-level object

let cannot be accessed by global objects (window, global, self)

let a = 10;console.log(window.a); //undefined复制代码

String

ES6 has made some extensions to strings, as follows:

Template string

ES6 adds a new template string (character String) is used to define strings to solve the problems encountered before when the string is too long and needs to be wrapped, or when there are variables or expressions in the string. The following is the specific usage scenario

//一、打印长字符串且换行,直接在模板字符串中保留格式即可let welcome=` 你好 欢迎来到ES6 ——谢谢 `console.log(welcome);/* 输出结果为: 你好 欢迎来到ES6 ——谢谢 *///二、字符串中有变量或者表达式,直接在模板字符串中使用${变量/表达式}即可let type = "ES6";let name1 = "mango";let name2 = "和goman";let welcome = `欢迎${name1 + name2}来到${type}世界`; console.log(welcome); //learn1.js?c1a0:7 欢迎mango和goman来到ES6世界复制代码

Method

String.prototype.includes()

Determine whether the string contains a specified string and return the boolean type.

const str = "ECMAScript"console.log(str.includes("EC")); //true 找不到返回false 复制代码

startsWith() and endsWith()

startsWith() is used to determine whether a string starts with the specified string and returns a boolean type.
endsWith() is used to determine whether a string ends with the specified string and returns a boolean type.

const str = "ECMAScript"console.log(str.startsWith("ECM")); //true console.log(str.endsWith("Script")); //true 复制代码

String.prototype.repeat()

Repeat the original string n times to get a new string

const str = "ECMAScript";console.log(str.repeat(3)); //ECMAScriptECMAScriptECMAScript复制代码

Number

ES6 The global methods began to be gradually reduced, making the language gradually modular, so some methods for processing numerical values were transferred to the Number object, and the functional behavior remained unchanged.

//将目标转换为整数//ES5parseInt("5.6") //5//ES6Number.parseInt("5.6") //5//将目标转换为浮点数//ES5parseFloat("12.45str") //12.45//ES6Number.parseFloat("12.45str") //12.45复制代码

In addition, in order to facilitate development, Number has also added some methods and attributes

一、判断一个数值是否是整数Number.isInteger(25) // trueNumber.isInteger(25.1) // false二、获取JavaScript最大安全值和最小安全值Number.MAX_SAFE_INTEGER=9007199254740991Number.MIN_SAFE_INTEGER=-9007199254740991三、判断一个数值是否是在安全范围Number.isSafeInteger(9007199254740992) //false复制代码

Symbol

Newly introduced primitive data types are used to represent unique values.

Declaration method

let sym = Symbol();let sym2 = Symbol();console.log(sym == sym2); //false 生成的值是独一无二的,所以不相等console.log(typeof sym); //symbol typeof查看值的类型为symbollet symWithDesc = Symbol("name"); //Symbol()括号内可以添加描述console.log(symWithDesc.toString()); //输出:Symbol(name) 打印描述需要转换成字符串复制代码

Application in the project


1. Eliminate magic string
If we need to make a click menu to perform different processing functions, This is how we usually do it.

const clickMenu = function (menu) { switch (menu) { case "home": break; case "me": break; } }; clickMenu("home")复制代码

"home" may appear many times. The string that forms a strong coupling with the code is the magic string. In the project, we should try our best to eliminate the magic string. Let's use Symbol to eliminate the magic string

const MENU_TYPE = { home: Symbol(), me: Symbol(), };const clickMenu = function () { switch (menu) { case MENU_TYPE.home: break; case MENU_TYPE.me: break; } }; clickMenu(MENU_TYPE.home);复制代码


2. Unique attribute value as an object
If we want to generate a company name object and use each person's name as the key value, then there will be a problem if someone has the same name. Symbol can solve this problem

const scores = { [Symbol("张三")]: { age: 22, }, [Symbol("李四")]: { age: 21, }, [Symbol("张三")]: { age: 20, }, };复制代码

Note that the properties defined by Symbol can only be traversed in the following two ways, otherwise the properties cannot be obtained.

for (let key of Object.getOwnPropertySymbols(scores)) { console.log(key, key); }for (let key of Reflect.ownKeys(scores)) { console.log(key, scores[key]); }复制代码

Set and Map


In order to implement data operations more conveniently, ES6 adds two new data structures: Set and Map.

Set

Set is a data structure similar to an array, but the values of its members are unique.

New

Create a new Set data structure to store the month. You can define an empty Set instance or default data in the form of an array.

let monthSets = new Set();let monthSets2 = new Set(["一月","二月","三月"]);复制代码

Basic usage

//添加数据monthSets.add("一月"); monthSets.add("二月").add("三月");console.log(monthSets); //Set(3) {"一月", "二月", "三月"}//遍历集合Set//forEach():使用回调函数遍历每个成员monthSets.forEach((item) => console.log(item)); //一月 二月 三月//for...of:直接遍历每个成员for (const item of monthSets) { console.log(item); //一月 二月 三月}//删除数据monthSets.delete("二月");console.log(monthSets); // Set(2) {"一月", "三月"}monthSets.clear(); //console.log(monthSets); // Set(0) {}复制代码

Common applications

Set data structure has many application scenarios in actual projects.

let monthSets = new Set(["一月", "二月", "三月"]);//一、快速判断数据元素是否存在monthSets.has("一月"); //true//二、统计数据元素个数monthSets.size; //3console.log(monthSets.size); //3//三、数组去重let arr = [1, 2, 3, 2, 3, 4, 5];let set = new Set(arr);console.log(set); // {1, 2, 3, 4, 5}//四、合并去重let arr = [1, 2, 3];let arr2 = [2, 3, 4];let set = new Set([...arr, ...arr2]);console.log(set); // {1, 2, 3, 4}//五、取数组交集let arr1 = [1, 2, 3];let arr2 = [2, 3, 4];let set1 = new Set(arr1);let set2 = new Set(arr2);let resultSet = new Set(arr1.filter((item) => set2.has(item)));console.log(Array.from(resultSet)); // [2, 3]//六、取数组差级let arr1 = [1, 2, 3];let arr2 = [2, 3, 4];let set1 = new Set(arr1);let set2 = new Set(arr2);let arr3 = arr1.filter((item) => !set2.has(item));let arr4 = arr2.filter((item) => !set1.has(item));console.log([...arr3, ...arr4]); //[1, 4]复制代码

WeakSet

WeakSet与Set类似,也是不重复的值的集合,但WeakSet的成员只能是对象。WeakSet引用的对象都是弱引用,如果其他对象不再引用该对象,那么垃圾回收机制就会自动回收这些对象所占用的内存,不考虑该对象还存在于WeakSet之中。
React源码中有很多地方使用到了WeakSet,例如在react-reconciler/src/ReactFiberHotReloading.new.js中。

export function markFailedErrorBoundaryForHotReloading(fiber: Fiber) { if (__DEV__) { if (resolveFamily === null) { // Hot reloading is disabled. return; } if (typeof WeakSet !== 'function') { return; } if (failedBoundaries === null) { failedBoundaries = new WeakSet(); } failedBoundaries.add(fiber); } }复制代码

Map

Map是一种键值对集合,与对象类似,但Object只支持“字符串:值”,而Map支持“各种类型的值:值”,map给我们提供了更合适的“键值对”数据结构。

基本使用

//定义let map = new Map();//添加数据let address = { address: "江苏" }; map.set("name", "ES6"); map.set(27, "年龄信息"); map.set(address, "地址信息");console.log(map); //{"name" => "ES6", 27 => "年龄信息", {…} => "地址信息"}//获取数据let name = map.get("name");let age = map.get(27);let addressObj = map.get(address);console.log(name, age, addressObj);//获取成员数量console.log(map.size); //3//判断是否指定key成员console.log(map.has("name")); //true复制代码

Map的遍历

map通常可以用forEach和for...of的方式进行遍历。

//定义let map = new Map(); map.set("id", 1); map.set("name", "mango"); map.set("address", { province: "江苏", city: "南京", }); map.forEach((key, value) => console.log(key, value));for (const [key, value] of map) { console.log(key, value); }//输出 id 1 name mango address {province: "江苏", city: "南京"}复制代码

WeakMap

WeakMap与Map类似,也是用来生成键值对的集合。但WeakMap只接受对象作为键名,并且键名所指向的对象,属于弱引用对象。

数组的扩展

ES6对数组进行了很多的扩展,具体如下

扩展运算符

扩展运算符是三个点(...),将一个数组转为用逗号分隔的参数序列,通常用在函数参数中。
假如我们需要一个求和函数,并且支持传入任意数量的值。

function sum(...params) { let sum = arr.reduce(function (prev, cur) { return prev + cur; }); return sum; }let arr = [1, 2, 3, 4, 5];console.log(sum(arr)); //输出 15复制代码

Array.from()

Array.from()方法从一个类似数组或可迭代对象创建一个新的浅拷贝的数组实例,通常有以下四种实用场景。

//一、克隆一个数组let num = [1, 2, 3];let newNum = Array.from(num);console.log(newNum, num === newNum); //[1, 2, 3] false//二、使用指定值,初始化一个数组//给定长度为10,默认值是数组2和对象{key:1}let length = 4;let defaultValue = 2;let defaultObj = { key: 1 };let arrValue = Array.from({ length }, (item,index) => defaultValue);let arrObj = Array.from({ length }, (item,index) => defaultObj);console.log(arrValue); // [2, 2, 2, 2]console.log(JSON.stringify(arrObj)); //[{"key":1},{"key":1},{"key":1},{"key":1}]//三、生成值范围数组function range(end) { return Array.from({ length: end }, (item, index) => index); }let arr = range(4);console.log(arr); // [0, 1, 2, 3]//四、数组去重,结合set使用let arr = [1, 1, 2, 3, 3];let set = new Set(arr);console.log(Array.from(set));复制代码

创建数组

如何创建一个数组,有下面几种常用方式

//一、数组字面量const arr1 = [];//二、构造函数const arr2 = Array(3); //[null,null,null]const arr3 = Array("3"); //["3"]//这时想要用构造函数创建一个数字为7的数组,发现上面方式是无法满足的,而ES6提供了Array.of()能满足我们的需求const arr3 = Array.of(7); //[7]复制代码

数组查找


find()方法返回数组中满足提供的测试函数的第一个元素的值,若没有找到对应元素返回undefined
findIndex()方法返回数组中满足提供的测试函数的第一个元素的索引,若没有找到对应元素则返回-1。

假如我们想要在一个成绩数组中,找到达到及格分数的最低分。

const score = [34, 23, 66, 12, 90, 88, 77, 40];const passMin = score.find((value) => value > 60);console.log(passMin); //66const pass = score.findIndex((value) => value > 60);console.log(pass); //2复制代码

数组遍历

ES6新增 for...of 数组遍历方式

const score = [34, 23, 66, 12,];for (let value of score) { console.log(value); // 34, 23, 66, 12}复制代码

函数的扩展


ES6对函数进行了很多的扩展,具体如下

函数参数设置默认值

ES6允许为函数的参数设置默认值,即可以直接写在参数定义的后面。

//参数b设置了默认值为2,在方法调用的时候并没有传值,所以b直接使用默认值function sum(a, b = 2) { return a + b; }console.log(sum(1)); //3复制代码

Rest参数

ES6引入reset参数,形式为...变量名,可以用来获取传递给函数的多余参数。

function sum(a, ...values) { console.log(a, values); //1 [2, 3, 4, 5]} sum(1, 2, 3, 4, 5);复制代码

name和length属性

name属性返回函数名,length属性返回没有指定默认值的参数个数。

function sum(a, b, c, d = 1) { console.log(a, values); //1 [2, 3, 4, 5]}console.log(sum.name); //sumconsole.log(sum.length); //3复制代码

箭头函数

ES6允许使用箭头(=>)的方式定义函数,有下面几种箭头函数实现形式。
想要实现一个加法函数,ES5的形式如下

function sum(a, b) { return a + b; }复制代码

而如果使用箭头函数实现的话,则如下

sumArrow = (a, b) => { return a + b; };复制代码

上面是箭头函数的基本变现形式,不同的场景还有不同的实现形式。

//对于上面的sumArrow函//一、如果只有一个参数,可以省略括号sumArrow = a => { return a; }; 二、如果返回值是表达式,可以省略return和{} sumArrow = a => a; 三、如果返回值是字面量对象,一定要用小括号包起来 sumArrow = () => ({ a: 1, b: 2 });复制代码

箭头函数与普通函数除了实现方式不同外,还有个不同的点就是对this的处理方式。

//普通函数let math = { name: "mathName", sum: function (a, b) { console.log(this.name); //math return a + b; }, }; math.sum();//箭头函数globalThis.name = "globalName";let math = { name: "mathName", sum: (a, b) => { console.log(this.name); //globalName return a + b; }, }; math.sum();复制代码

从上面示例可以看到,箭头函数和普通函数最终打印的this.name不一致。对于普通函数,this指向的是调用sum方法的math对象,所以this.name打印的是“mathName”。而对于箭头函数,this指向的是定义sum方法的全局对象,所以this.name打印的是“globalName”。

在后续的开发过程中,我们将会经常使用到箭头函数,在使用的过程中,我们需要有以下几点注意

  1. 箭头函数中this指向定义时所在的对象,而不是调用时所在的对象
  2. 不可以当作构造函数
  3. 不可以使用yield命令,不能作用generator函数

解构赋值


解构赋值是一种表达式,可以将属性和值从对象和数组中取出,赋值给其他变量。

如何使用

对象解构赋值

假如我们拿到一个对象,需要获取指定的属性值。则解构赋值让我们无需通过调用属性的方式赋值,而是通过指定一个与对象结构相同模板的方式,获取想要的属性值。

const people = { name: "ES6", age: 27, sex: "male", };//如果通过调用属性赋值,则需要这么做let name = people.name;let age = people.age;let sex = people.sex;console.log(name, age, sex); //ES6 27 male//而使用解构赋值的方式,代码会更加的清晰简单const { name, age } = People;console.log(name, age); //ES6 27 male复制代码

除了上面这种基本用法,还有其他使用方式

const people = { name: "ES6", age: 27, sex: "male", };// 一、属性顺序不需保持一致,名称相同即可const { age, name, sex } = people;console.log(name, age, sex); //ES6 27 male//二、取值时,重新定义变量名const { age: newAge, name: newName, sex: newSex } = people;console.log(name, age, sex); //Uncaught ReferenceError: age is not definedconsole.log(newName, newAge, newSex); //ES6 27 male//三、赋值过程中设置默认值const { nickName = "昵称", age } = people;console.log(nickName, age); //昵称 27//四、reset运算符。只获取想要的属性,其他属性都放在新的变量里。const { name, ...peopleParams } = people;console.log(name, peopleParams); //ES6 {age: 27, sex: "male"}//五、嵌套对象取值const people = { name: "ES6", address: { province: "江苏", }, };const { address: { province }} = people;console.log(province); //江苏复制代码

数组解构赋值

假如我们拿到一个数组,需要获取指定的元素值。

const [a, b, c] = [1, 2, 3];console.log(a, b, c); //1 2 3复制代码

除了上面这种基本用法,还有其他使用方式

//一、待解构的除了是数组,还可以是任意可遍历的对象const [a, b, c] = new Set([1, 2, 3]);console.log(a, b, c); //1 2 3//二、被赋值的变量还可以是对象的属性,不局限于单纯的变量const num = {}; [num.a, num.b, num.c] = [1, 2, 3];console.log(num); //{a: 1, b: 2, c: 3}//三、解构赋值在循环体中的应用const num = { a: 10, b: 20, c: 30, };for (const [key, value] of Object.entries(num)) { console.log(key, value); //a 10 b 20 c 30}//四、跳过赋值元素const [a, , c] = [1, 2, 3]; //存在空位的数组叫稀疏数组console.log(a, c); //1 3//五、rest 参数const [a,...other] = [1, 2, 3];console.log(a, other); //1 [2, 3]//六、赋值过程中设置默认值const [a, , , d = 10] = [1, 2, 3];console.log(d); //10复制代码

字符串解构赋值

字符串解构赋值可以当成数组解构赋值

const [a, b, c, d] = "ECMAScript2015";console.log(a, b, c, d); //E C M A复制代码

对象的扩展


ES6对对象进行了很多的扩展,具体如下

属性的简洁表示法

从ES6开始,如果对象的属性名和属性值相同,则有简写的方式。

let province = "江苏";const address = { province, //等同于 province: province city: "南京", };复制代码

属性名表达式

从ES6开始,可以使用变量或表达式定义对象的属性。

let key = "province";const address = { [key]: "省份", city: "南京", };console.log(address); //{province: "省份", city: "南京"}复制代码

Object.is()

判断两个值是否是同一个值。在Object.is()之前,有“==”和“===”两种方式判断值是否相等,但这两个方式都有一定缺陷,如下

//== 在判断相等前会对不是同一类型的变量进行强制转换,最终导致“”与false相等console.log("" == false); //true//=== 会将-0与+0视为相等,而将Number.NaN与NaN视为不相等console.log(-0 === +0); //trueconsole.log(Number.NaN === NaN); //false复制代码

所以,需要一种运算,在所有场景下,只要两个值是一样的,那么就应该相等,在实际项目开发过程中,推荐使用Object.is()来判断值相等。

console.log(Object.is(-0, +0)); //falseconsole.log(Object.is(Number.NaN, NaN)); //truelet a = { value: 1 };let b = { value: 1 };console.log(Object.is(a, b)); //false 对象都是同一个引用才相等复制代码

Object.assign()


用于将所有可枚举属性的值从一个或多个源对象复制到目标对象,它将返回目标对象。

语法:

Object.assign(target, ...sources) 参数说明: target:目标对象 sources:源对象 返回值:合并后的目标对象

const target = { a: 1,};const source = { b: "B", c: "C" };const assignObj = Object.assign(target, source);console.log(assignObj); //{a: 1, b: "B", c: "C"} //其他应用//一、如果目标对象与源对象属性具有相同值,则源对象属性值会覆盖目标对象属性值const target = { a: 1,b: 2};const source = { b: "B", c: "C" };const assignObj = Object.assign(target, source);console.log(assignObj); //{a: 1, b: "B", c: "C"} //目标对象的b属性值被覆盖//二、源对象可以有多个值const target = { a: 1 };const source1 = { b: "B", c: "C" };const source2 = { d: "D", e: "E" };const assignObj = Object.assign(target, source1, source2);console.log(assignObj); //{a: 1, b: "B", c: "C", d: "D", e: "E"}复制代码

对象遍历

假如我们想要循环遍历一个对象的键与值,则可以使用下面几种方式进行遍历

const score = { name: "mango", age: "25", score: 80, };//for...infor (let key in score) { console.log(key, score[key]); // 分别输出:name mango 、 age 25 、score 80}//Object.keys()用来获取所有key组成的数组Object.keys(scoreObj).forEach(key => { console.log(key, scoreObj[key]) //分别输出:name mango 、 age 25 、score 80})//Object.getOwnPropertyNames()用来获取所有key组成的数组Object.getOwnPropertyNames(scoreObj).forEach(key => { console.log(key, scoreObj[key]) //分别输出:name mango 、 age 25 、score 80})//Reflect.ownKeys()用来获取所有key组成的数组Reflect.ownKeys(scoreObj).forEach(key => { console.log(key, scoreObj[key]) //分别输出:name mango 、 age 25 、score 80})复制代码

Class

JavaScript是一种基于对象的语言,我们遇到的所有东西几乎都是对象,但ES6之前是没有class的,而在ES6版本中正式引入了class,让JavaScript成为了一种真正的面向对象语言,我们可以像下面这样在JavaScript中进行面向对象编程。

//通过class关键字定义类class People{ //类的构造函数 constructor(name, age) { this.name = name; this.age = age; } //实例方法 getName() { return this.name; } //静态方法 static say() { console.log("Hello ES6"); } }//继承class Student extends People { constructor(name, age) { super(name, age); } }//对象创建与调用let student = new Student("mango", "27"); student.getName(); Student.say();复制代码

通过上面的代码,我们具体说明下JavaScript中进行面向对象编程。

类的声明

通过class关键字声明类,支持构造函数construct做对象初始化。

class People{ constructor() { //初始化 } }复制代码

属性

Class对象中有两种对象属性,分别是实例属性和静态属性。实例属性必须定义在类的方法里,而静态属性必须定义在类的外面。

class People{ constructor() { //定义实例属性 this.name = ""; this.age = 0; } } People.desc="类描述"; //定义的静态属性//访问People people=new People();console.log(people.name);console.log(People.name);复制代码

类中定义的属性,默认都是可读可写的,但是如果这时候我们想指定属性不可被修改该如何实现呢?那么便要用到set和get了,set和get可以定义一个属性,但是如果只有get而没有set,则属性不可以进行修改。

class People { get sex() { return "男"; } }let people = new People();console.log(people.sex); people.sex="女" //Uncaught TypeError: Cannot set property sex of # which has only a getter复制代码

方法

Class对象中有三种方法,分别是构造方法、实例方法还有静态方法。

class People { //构造方法 constructor(name, age) { this.nameA = name; this.age = age; } //实例方法 getName() { return this.nameA; } //静态方法 static say() { console.log("Hello " + People.desc); } } People.desc = "类描述";let people = new People("mango", "27");let name = people.getName();console.log(name); //mangoPeople.say(); //Hello 类描述复制代码

继承

继承是面向对象语言很重要的一大特征,ES6新加入了extends和super关键字来实现继承。

class People { constructor(name) { this.name = name; } getName() { return this.name; } }//继承class Student extends People { constructor(name, age) { super(name, age); } }//Student类继承了People类,student对象中super调用了父类的构造函数,并传递了name参数,因为继承的特性,student也拥有了父类的getName()方法let student = new Student("ES6");console.log(student.getName());复制代码


通过以上对class的学习,我们得知道其实class并不是新引入的数据类型,其实class只是一种语法糖,它的实质完全可以看作构造函数的另一种写法。

class People { constructor(name) { this.name = name; } getName() { return this.name; } }console.log(typeof People); //functionconsole.log(People.prototype); //{constructor: ƒ, getName: ƒ}复制代码

JS是单线程的

异步编程其实就是处理异步任务,在进行异步编程之前,我们需要了解JavaScript是单线程的,在同一时间只能做一件事。

JavaScript之所以设计成单线程,是与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动以及操作DOM。这决定了它只能是单线程,否则会带来很多复杂的同步问题。例如,如果JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器就不晓得以哪个线程为准。所以,为了避免复杂,从一诞生,JavaScript就是单线程的。

单线程就意味着,所有任务都需要排队,前一个任务结束,后一个任务才会执行。那么如果前一个任务很长的话,那么后面一个任务不是就一直需要等待了吗?于是乎,JS将所有任务分成了两类,同步和异步。
同步:只有前一个任务执行完毕,才能执行后一个任务
异步:当同步任务执行到某个需要耗时执行的运行环境API时,就会触发异步任务,此时运行环境(浏览器或Node)就会单独开线程去处理这些异步任务。

JavaScript运行原理

下面是JavaScript运行原理图,同步任务在JS主线程完成,异步任务则新开一个线程

Comprehensive mastery of the new features of ECMAScript

疑问:不是说JavaScript是单线程的吗,那为什么又新开了一条线程处理任务呢,这不是多线程方式吗?
有这个疑问不奇怪,我们需要清楚JavaScript单线程其实说的是JavaScript引擎是单线程的,开发者只能通过单线程的方式进行JavaScript开发,而新开了一条线程处理任务是底层执行环境决定的,JavaScript执行环境是多线程。


在实际项目中,异步编程使用场景极其之多,请求个接口数据、创建个定时器、缓存个数据都离不开异步编程的身影,为了更好的处理异步任务,ES6给我们提供两种新的方式,分别是Promise和Generator。

Promise

Promise 是一个代理对象,代表了一个异步任务的最终成功或者失败状态。Promise允许你为异步任务的成功或失败分别设置对应的处理方法,以类似同步的方式便捷的进行异步编程。

一个Promise有三种状态:

  • pending:初始状态,既不是成功,也不是失败状态。
  • fulfilled:成功状态,代表着任务执行完成
  • rejected:失败状态,代表着任务执行失败

基本使用

创建Promise对象

const promise = new Promise(function (resolve, reject) { let result=执行异步任务; if(result){ //如果异步任务成功完成 resolve() }else{ //如果异步任务执行失败 reject(); } });复制代码

创建Promise对象需要传递一个executor参数,executor是带有resolve和reject两个参数的函数,这两个参数是JavaScript引擎提供的两个函数,Promise构造函数执行会立即调用executor函数。

  • 当Promise中的异步任务执行成功时,调用resolve(),将Promise对象的状态从pending改为fulfilled(未完成到成功)
  • 当Promise中的异步任务执行失败时,调用reject(),将Promise对象的状态从pending改为rejected(未完成到失败)


Comprehensive mastery of the new features of ECMAScript

如何使用

实例好promise对象后,我们可以使用下面的语法来对异步任务完成后的状态进行处理。

promise.then(onFulfilled,onRejected)

promise.then( (result) => { console.log("异步任务处理成功,执行相应方法"); }, (error) => { console.log("异步任务处理失败,执行相应方法"); } );复制代码

但在具体的项目开发中,我们通常都是使用已经存在的Promise对象,下面我们就通过使用常用的promise对象fetch获取接口数据。

案例实现

我们需要调用接口,获取一个用户列表数据

fetch("http://jsonplaceholder.typicode.com/users") .then(function (response) { return response.json(); }) .then(function (res) { console.log(res); // [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}] });复制代码

response是一个包含响应结果的Response对象,它只是一个HTTP响应,而不是真正的JSON。为了获取JSON的内容,需要使用json()方法获取一个Promise对象,然后再使用then获取JSON数据。

其他使用

Promise.prototype.catch()

Promise提供了catch()方法,用来捕获异步操作过程中遇到的错误异常,使用场景如下

const CatchPromise = new Promise(function (resolve, reject) { reject(new Error("error msg")); }); CatchPromise.then().catch((e) => { console.error(e); //Error: error msg});复制代码

在Promise对象中,除了可以使用reject(new Error())的方式触发异常,还可以使用throw new Error()的方式触发异常,但不建议使用throw new Error()的方式,因为这种方式不会改变Promise的状态。

Promise.prototype.all()

Promise.all()用于处理多个异步任务,例如处理多张图片上传。Promise.all()接受一个promise对象数组作为参数,执行完毕返回一个Promise对象。

Promise.all()的状态变化:
传入的promise对象数组全部变为fulfill状态则返回成功,调用resolve()
传入的promise对象数组有一个变为reject状态则返回失败,调用reject()

const promise1 = new Promise(function (resolve, reject) { setTimeout(function () { resolve("promise1"); }, 2000); });const promise2 = new Promise(function (resolve, reject) { setTimeout(function () { resolve("promise2"); }, 1000); });const promise3 = new Promise(function (resolve, reject) { setTimeout(function () { resolve("promise3"); }, 3000); });const promiseAll = Promise.all([promise1, promise2, promise3]); promiseAll.then(function (results) { console.log(results); // ["promise1", "promise2", "promise3"]});复制代码

Promise.prototype.race()

Promise.race()也是用于处理多个异步任务,与Promise.all()一样,Promise.race()接受一个promise对象数组作为参数,执行完毕返回一个Promise对象。

Promise.race()的状态变化:
传入的promise对象数组有一个变为resolve状态则返回成功,调用resolve()
传入的promise对象数组有一个变为reject状态则返回失败,调用reject()

const promise1 = new Promise(function (resolve, reject) { setTimeout(function () { reject("promise1"); }, 2000); });const promise2 = new Promise(function (resolve, reject) { setTimeout(function () { resolve("promise2"); }, 1000); });const promise3 = new Promise(function (resolve, reject) { setTimeout(function () { resolve("promise3"); }, 3000); });const promiseAll = Promise.race([promise1, promise2, promise3]); promiseAll.then(function (results) { console.log(results); // promise2});复制代码

Generator函数

Generator函数是用来处理异步任务的函数,函数内部包裹的就是异步任务的处理。Generator不同于普通函数,当执行到异步任务,可以暂停,直到异步任务执行完毕再继续往下执行,类似同步的方法进行异步编程。

如何使用

Generator函数在使用上具体有以下特点

  • :定义generator函数时,function后面需要加上星号,例如functiongeneratorFuncName()
  • yield:需要暂停去执行异步任务时,需要在代码前面加上yield标识,例如yield fetch()
  • next():调用generator函数后获得的是一个指针对象,调用指针的next方法,可以用来分阶段逐步执行generator函数。


那么具体如何使用generator函数实现异步编程呢,在学习Promise的时候,我们实现了一个获取一个用户列表数据的案例,下面我们看看如何使用generator函数实现吧。

function* loadUsers() { const API = "http://jsonplaceholder.typicode.com/users"; console.log("等待数据请求"); yield fetch(API); //暂停,开始执行异步任务 console.log("数据请求完成"); console.log("继续其他逻辑操作"); }const generator = loadUsers();const promise = generator.next().value;console.log(promise); promise .then(function (response) { return response.json(); }) .then(function (result) { console.log(result); //[{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}] generator.next(); //打印:数据请求完成 继续其他逻辑操作。异步任务执行完毕后调用next(),继续执行generator函数中后续代码 });复制代码

Proxy

Proxy翻译过来叫代理,Proxy可以通过自定义行为来改变对象的基本操作,例如属性赋值、查找、枚举、函数调用等。

基本语法

const p=new Proxy(target,handler); 参数说明: target:需要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至是另外一个代理) handler:代理目标对象基本操作的对象 返回值: p:target被代理后不可以直接访问,而只能访问

Proxy使用场景

设置对象属性的读写权限

let people = { name: "mango", //设置属性不可读 age: 27, //设置属性不可被修改};let peopleHandler = { //设置属性值读取的捕捉器 get: function (target, prop, receiver) { if (Object.is(prop, "name")) { return prop + "属性不可读"; } }, //设置属性值操作的捕捉器 set: function (target, prop, value) { if (Object.is(prop, "age")) { throw new Error(prop + "属性不可写"); } }, };let peopleProxy = new Proxy(people, peopleHandler); peopleProxy.age = 10; // Uncaught Error: age属性不可写console.log(peopleProxy.name); //输出:name属性不可读复制代码

给接口返回的对象中字段为null的属性设置默认值

let people = { address: null, };let peopleHandler = { //设置属性值读取的捕捉器 get: function (target, prop, receiver) { return target[prop] ?? "默认值"; //空值合并操作符 }, };let peopleProxy = new Proxy(people, peopleHandler);console.log(peopleProxy.address); //输出:默认值复制代码

拦截函数调用

//handler.apply()用于拦截函数的调用function sum(a, b) { console.log(a + b); return a + b; }////对于sum方法,关注的是处理数据相加的逻辑//通过代理则可以处理在调用方法时候,对参数的校验,数据打点等const sumProxy = new Proxy(sum, { apply: function (target, thisArg, argumentsList) { console.log("调用了方法", "打点"); }, }); sumProxy(1, 2);复制代码

**

Module


ES6在语言标准上,通过Module实现了模块功能,现阶段几乎取代之前用来实现JavaScript模块化的CommonJS和AMD规范,成为了浏览器环境和node环境通用的模块化解决方案。
Module实现的模块化属于“编译时加载”,即在编译时就完成了模块之间的加载,通过这种“编译时加载”的方式,使得在不运行代码的情况下就可以通过词法分析、语法分析等对程序代码进行扫描,以验证代码的规范性、安全性和可维护性,让静态分析成为了可能。

如何使用


Module实现的模块化功能主要有两个命令构成:

  • export:导出命令,用于提供模块的对外接口
  • import:导入命令,用于引入其他模块提供的接口


一个模块就是一个独立的文件,该文件内的所有变量,外部无法获取,如果想要外部获取模块内的某些变量,就必须使用export关键字导出变量,在需要引入该导出变量的的模块中必须使用import关键字引入变量。

export

下面举例说明export命令导出对外接口的几种方式,在ExportDemo.js文件中,

  • 导出变量
//方法一export let a = 1;//方法二let b = 2;export { b };复制代码
  • 导出函数
//方法一export function test(){ console.log("name"); }//方法二let test2=function test(){ console.log("name"); }export {test2 as newName}复制代码

注意在方法二中,使用了as关键字,as关键字可以在导出时重命名对外的接口名。

  • 导出类
//方法一export class People { say() { console.log("Hello Module"); } }//方法二export { People };复制代码

import

使用export导出了模块中的对外接口后,其他JS文件就可以通过import关键字加载这个模块,使用如下。

//大括号中的变量名必须与被导出对外接口名一致import { a, b, test as newTest, People } from "./ExportDemo";//导入a和b的变量console.log(a, b);//导入test方法,同样可以使用as关键字在导入的时候重命名newTest();//导入People类let people = new People(); people.say();复制代码


其他使用方式**
在日常开发过程中,Module模块化还有下面几种常见的使用方式。

  • 使用*指定一个对象,加载模块中的所有导出
//import { a, b, test, People } from "./ExportDemo";//上面的导入方式可以改写成下面方式import * as ExportModule from "./ExportModule";//使用的使用,加上前缀即可ExportModule.test()复制代码
  • 通过export default,为模块指定默认导出名
//导出const People = { say: function () { console.log("Hello Module"); }, };export default People;//导入import People from "./ExportModule"; People.say(); //Hello Module复制代码
  • export和import一起使用,处理先输入后输出的情况
//假如有a、b、c三个文件模块,//c文件模块如下 c.jslet people={name:"mango",age:27};let address="南京";export { people, address };//有下面几种使用场景//一、在b中导入c中的people和address,并导出给a使用export {people,address} from 'c'//二、在b中整体导入c,并导出给a使用export * from 'c'//三、在b中导入people,并作为b的导出名称【具名接口改为默认接口】export {people as default} from 'c'//当c的导出方式为export default的时候,并可以使用【默认接口改为具名接口】export {default as NewPeople} from 'c'复制代码

相关免费学习推荐:javascript(视频)

The above is the detailed content of Comprehensive mastery of the new features of ECMAScript. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:juejin.im. If there is any infringement, please contact admin@php.cn delete