首页  >  文章  >  web前端  >  全面掌握,ECMAScript的新特性

全面掌握,ECMAScript的新特性

coldplay.xixi
coldplay.xixi转载
2020-10-22 18:34:052005浏览

今天javascript栏目带大家认识ECMAScript的新特性。

全面掌握,ECMAScript的新特性

ES6成为JavaScript的下一代标准后,标准委员会(TC39)在每年都会发布一个ES的新版本,每个版本里都引入了很多实用的新特性,在日常的项目开发中,如果我们掌握这些实用的新特性,将大大的提升开发效率,下面让我们全面的掌握这些ES的新特性吧~

Let 和Const

之前使用var来定义变量,为我们提供了新的方式
let用来声明变量,const用来声明常量。

如何使用

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

四个特点

一、只在块级作用域内有效

let和const为JavaScript新增了块级作用域,通常情况下,{}包裹的代码拥有的作用域就是块级作用域,声明的变量或常量只在块级作用域内有效,外部不能访问。

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复制代码

上面有两个块级作用域,都声明了变量a,但外层块级作用域与内层块级作用域无关,所以(1)处输出的是外层的变量值1,(2)处访问了不在一个块级作用域定义的变量,所以会报错。

另外一个理解块级作用域的示例。

//for循环体内的定时器//在ES6之前,是没有块级作用域的,变量用var声明,直接挂载在全局作用域上for (var i = 0; i < 3; i++) {  setTimeout(function () {    console.log(i); //3、3、3
  }, 100);
}//使用var声明,for同步操作优先于setTimeout异步操作,在开始执行setTimeout的时候,//for循环已经执行完,i为3,则后续每次setTimeout输出的i都是3//使用let声明的话,则会在循环体内部形成闭包,每次for循环都会给闭包提供每次循环i的值,//并且不被释放,最终setTimeout里会分别输出0、1、2for (let i = 0; i < 3; i++) {  setTimeout(function () {    console.log(i); //0 1 2
  }, 100);
}复制代码

二、暂时性死区

不能在变量和常量声明之前使用。
let和const命令会使区块形成封闭作用域,若在声明之前使用,就会报错,这个在语法上,称为“暂时性死区”(简称TDZ)。

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

三、不能重复声明

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

四、不属于顶层对象

let声明的变量,全局对象(window,global,self)不能访问到

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

String

ES6对字符串进行了一些扩展,如下:

模板字符串

ES6新增了模板字符串(字符串)的方式定义字符串,用来解决之前字符串很长要换行、字符串中有变量或者表达式遇到的问题,下面是具体的使用场景

//一、打印长字符串且换行,直接在模板字符串中保留格式即可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世界复制代码

方法

String.prototype.includes()

判断字符串是否包含一个指定字符串,返回boolean类型。

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

startsWith()和endsWith()

startsWith()用来判断字符串是否以指定字符串作为开头,返回boolean类型。
endsWith()用来判断字符串是否以指定字符串作为结尾,返回boolean类型。

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

String.prototype.repeat()

将原有字符串重复n遍,得到一个新的字符串

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

Number

ES6开始逐步减少全局性方法,使得语言逐步模块化,所以把一些处理数值的方法转移到了Number对象上,功能行为保持不变。

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

另外,为了便于开发,Number还增加了一些方法和属性

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

Symbol

新引入原始数据类型,用来表示独一无二的值。

声明方式

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)   打印描述需要转换成字符串复制代码

项目中应用


一、消除魔术字符串
假如我们需要做一个点击菜单,做不同处理的功能,我们通常会这样实现。

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

clickMenu("home")复制代码

"home"这种可能会多次出现,与代码形成强耦合的字符串就是魔术字符串,在项目中我们应该尽量消除魔术字符串,下面使用Symbol消除魔术字符串

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);复制代码


二、作为对象独一无二的属性值
假如我们想生成一个公司人名对象,并以每个人名为key值,这时候如果有人名重名便会有问题,而Symbol能解决这个问题

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

注意,通过Symbol定义的属性,只能通过下面两种方式进行遍历,否则无法获取属性。

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

Set和Map


为了更方便地实现数据操作,ES6新增了Set和Map两种数据结构。

Set

Set是类似于数组,但成员的值都是唯一的数据结构。

新建

新建一个存储月份的Set数据结构,可以定义一个空的Set实例,也可以是带有数组形式的默认数据。

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

基本使用

//添加数据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) {}复制代码

常见应用

Set数据结构在实际项目中还有很多应用场景。

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 #<People> 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主线程完成,异步任务则新开一个线程

全面掌握,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(未完成到失败)


image.png

如何使用

实例好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后面需要加上星号,例如function generatorFuncName()
  • 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(视频)

以上是全面掌握,ECMAScript的新特性的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文转载于:juejin.im。如有侵权,请联系admin@php.cn删除