今天javascript栏目带大家认识ECMAScript的新特性。
ES6成为JavaScript的下一代标准后,标准委员会(TC39)在每年都会发布一个ES的新版本,每个版本里都引入了很多实用的新特性,在日常的项目开发中,如果我们掌握这些实用的新特性,将大大的提升开发效率,下面让我们全面的掌握这些ES的新特性吧~
之前使用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 'a' has already been declared const B=1;const B=2;//报错 SyntaxError: Identifier 'B' has already been declared 复制代码
let声明的变量,全局对象(window,global,self)不能访问到
let a = 10;console.log(window.a); //undefined复制代码
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世界复制代码
判断字符串是否包含一个指定字符串,返回boolean类型。
const str = "ECMAScript"console.log(str.includes("EC")); //true 找不到返回false 复制代码
startsWith()用来判断字符串是否以指定字符串作为开头,返回boolean类型。
endsWith()用来判断字符串是否以指定字符串作为结尾,返回boolean类型。
const str = "ECMAScript"console.log(str.startsWith("ECM")); //true console.log(str.endsWith("Script")); //true 复制代码
将原有字符串重复n遍,得到一个新的字符串
const str = "ECMAScript";console.log(str.repeat(3)); //ECMAScriptECMAScriptECMAScript复制代码
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复制代码
新引入原始数据类型,用来表示独一无二的值。
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]); }复制代码
为了更方便地实现数据操作,ES6新增了Set和Map两种数据结构。
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与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是一种键值对集合,与对象类似,但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通常可以用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与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()方法从一个类似数组或可迭代对象创建一个新的浅拷贝的数组实例,通常有以下四种实用场景。
//一、克隆一个数组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复制代码
ES6引入reset参数,形式为...变量名,可以用来获取传递给函数的多余参数。
function sum(a, ...values) { console.log(a, values); //1 [2, 3, 4, 5]} sum(1, 2, 3, 4, 5);复制代码
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”。
在后续的开发过程中,我们将会经常使用到箭头函数,在使用的过程中,我们需要有以下几点注意
解构赋值是一种表达式,可以将属性和值从对象和数组中取出,赋值给其他变量。
假如我们拿到一个对象,需要获取指定的属性值。则解构赋值让我们无需通过调用属性的方式赋值,而是通过指定一个与对象结构相同模板的方式,获取想要的属性值。
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()之前,有“==”和“===”两种方式判断值是否相等,但这两个方式都有一定缺陷,如下
//== 在判断相等前会对不是同一类型的变量进行强制转换,最终导致“”与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(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})复制代码
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: ƒ}复制代码
异步编程其实就是处理异步任务,在进行异步编程之前,我们需要了解JavaScript是单线程的,在同一时间只能做一件事。
JavaScript之所以设计成单线程,是与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动以及操作DOM。这决定了它只能是单线程,否则会带来很多复杂的同步问题。例如,如果JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器就不晓得以哪个线程为准。所以,为了避免复杂,从一诞生,JavaScript就是单线程的。
单线程就意味着,所有任务都需要排队,前一个任务结束,后一个任务才会执行。那么如果前一个任务很长的话,那么后面一个任务不是就一直需要等待了吗?于是乎,JS将所有任务分成了两类,同步和异步。
同步:只有前一个任务执行完毕,才能执行后一个任务
异步:当同步任务执行到某个需要耗时执行的运行环境API时,就会触发异步任务,此时运行环境(浏览器或Node)就会单独开线程去处理这些异步任务。
下面是JavaScript运行原理图,同步任务在JS主线程完成,异步任务则新开一个线程
疑问:不是说JavaScript是单线程的吗,那为什么又新开了一条线程处理任务呢,这不是多线程方式吗?
有这个疑问不奇怪,我们需要清楚JavaScript单线程其实说的是JavaScript引擎是单线程的,开发者只能通过单线程的方式进行JavaScript开发,而新开了一条线程处理任务是底层执行环境决定的,JavaScript执行环境是多线程。
在实际项目中,异步编程使用场景极其之多,请求个接口数据、创建个定时器、缓存个数据都离不开异步编程的身影,为了更好的处理异步任务,ES6给我们提供两种新的方式,分别是Promise和Generator。
Promise 是一个代理对象,代表了一个异步任务的最终成功或者失败状态。Promise允许你为异步任务的成功或失败分别设置对应的处理方法,以类似同步的方式便捷的进行异步编程。
一个Promise有三种状态:
const promise = new Promise(function (resolve, reject) { let result=执行异步任务; if(result){ //如果异步任务成功完成 resolve() }else{ //如果异步任务执行失败 reject(); } });复制代码
创建Promise对象需要传递一个executor参数,executor是带有resolve和reject两个参数的函数,这两个参数是JavaScript引擎提供的两个函数,Promise构造函数执行会立即调用executor函数。
实例好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提供了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.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.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函数实现异步编程呢,在学习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可以通过自定义行为来改变对象的基本操作,例如属性赋值、查找、枚举、函数调用等。
const p=new Proxy(target,handler); 参数说明: target:需要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至是另外一个代理) handler:代理目标对象基本操作的对象 返回值: p:target被代理后不可以直接访问,而只能访问
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属性不可读复制代码
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);复制代码
**
ES6在语言标准上,通过Module实现了模块功能,现阶段几乎取代之前用来实现JavaScript模块化的CommonJS和AMD规范,成为了浏览器环境和node环境通用的模块化解决方案。
Module实现的模块化属于“编译时加载”,即在编译时就完成了模块之间的加载,通过这种“编译时加载”的方式,使得在不运行代码的情况下就可以通过词法分析、语法分析等对程序代码进行扫描,以验证代码的规范性、安全性和可维护性,让静态分析成为了可能。
Module实现的模块化功能主要有两个命令构成:
一个模块就是一个独立的文件,该文件内的所有变量,外部无法获取,如果想要外部获取模块内的某些变量,就必须使用export关键字导出变量,在需要引入该导出变量的的模块中必须使用import关键字引入变量。
下面举例说明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 };复制代码
使用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()复制代码
//导出const People = { say: function () { console.log("Hello Module"); }, };export default People;//导入import People from "./ExportModule"; People.say(); //Hello Module复制代码
//假如有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中文网其他相关文章!