ES6: (ECMAScript第六个版本)
1. 解构(destruct)
在旧 js 中,要想使用对象中的成员或数字中的元素,必须带着"对象名."或"数组名[ ]"前缀。但是在实际开发中,对象或数组的嵌套结构可能很深,这样的话前缀就可能写很长: "对象名.子对象名.子对象名....",非常麻烦。而解构方式就是用来来减少数组或对象的嵌套结构,便于使用。解构分为三种:
(1)数组解构
数组解构用于从一个复杂的数组中只提取出需要的元素单独使用,格式如下:
var [变量1, 变量2, ...] = 数组;
右边数组中相同下标位置的元素值会自动赋值给=左边相同下标位置的变量,一一对应;
变量1 = 数组[0];
变量2 = 数组[1];
要注意此处的 [ ] 并不带表创建一个新数组,仅仅是将个变量“装扮”成数组的样式。
举例:从数组中解构出年、月、日;
<script> var arr = [2021, 9, 3, 33]; // 解构 // 提取出数组中年月日三个值使用 // var [a, b, c] = arr; // 如果不要年,只要月日 var [, b, c] = arr; // console.log(`今年是${a}年`); console.log(`本月是${b}月`); console.log(`今天是${c}日`); </script>
(2)对象解构
对象解构用于从一个大的对象中只提取出个别属性值单独使用,格式如下:
var { 属性名1:变量1, 属性名2:变量2,... } = 对象;
当 : 左右两边的名字相同时,ES6为我们提供了简写方式:
如果 : 左边的属性名刚好和 : 右边的变量名相同,则只需要写一个即可;此时一个名字就起到了两两个作用,既当属性名进行配对、又当变量名进行接值。简写后格式如下:
var {属性名1, 属性名2, ...} = 对象;
举例:解构出对象中的姓名和年龄单独使用;
<script> var lilei = {sname: "李雷",sage: 21} // 解构 // var {sname: sname,sage: sage} = lilei; // 简写 //一个名字两用:既当属性名配对;又当变量名接值 var {sname,sage} = lilei console.log(`我叫${sname},今年${sage}岁`); </script>
(3)参数解构
单靠参数默认值,无法解决任意一个形参不确定有没有的情况。只要实参值不确定没有,但是又要求实参值必须传给指定的形参,顺序不能乱,就要用用参数解构。格式如下:
//定义函数时: function 函数名({ 属性名1: 形参1, 属性名2: 形参2, ... : ... }){ 函数体 } //调用函数时: 函数名({ 属性名1: 实参值1, 属性名2: 实参值2, ... : ... })
简写后格式如下:
//定义函数时: function 函数名({ 属性名1 = 默认值1, 属性名2 = 默认值2, ... : ... }){ 函数体 } //调用时: 函数名({ 属性名1: 实参值1, 属性名2: 实参值2, ... : ... })
举例:定义订套餐函数,用户可任意更换套餐中菜品;
<script> // 定义一个点套餐的函数 function order({ zhushi = "香辣鸡腿堡", xiaochi = "烤鸡翅", yinliao = "可乐" }) { console.log(` 您点的套餐为: 主食:${zhushi} 小吃:${xiaochi} 饮料:${yinliao} `); } // a点默认套餐 order({}); // b自定 order({ zhushi: "牛肉汉堡", xiaochi: "鸡米花", yinliao: "雪碧" }) // c只换主食 order({ zhushi: "烤全鸡" }) // d只换小吃 order({ xiaochi: "正新鸡排" }) </script>
打印结果如下:
2. class
在旧 js 中,构造函数和原型对象是分开定义的,这样不符合"封装"概念;class 是程序中专门集中保存一种类型的所有子对象的统一属性结构和方法定义的程序结构。所以今后只要在 es6 中创建一种新的类型,包含构造函数 + 原型对象方法,都要用 class 来创建。
定义 class 的方法:
a. 先用 class{ } 包裹原构造函数+原型对象方法;(虽直接放在 class{} 内的方法定义,其实还是保存在原型对象中的)
b. 原构造函数名升级为整个 class 的名字,所有构造函数统一更名为 "constructor";
c. 原型对象中的方法,不再加 prototype 前缀,也不用=function,直接简写为: 方法名(){ ...}。
使用 class:
var 对象名=new class名(属性值,...);
虽说用了 class,但本质并没有变:构造函数中的属性,依然会成为子对象的自有属性;直接定义在 class 中的方法,依然保存在子对象的原型对象中;子对象依然使用 _ _proto_ _ 指向原型对象。
举例:定义学生类型 class;
<script> // 定义学生类型,描述所有学生的统一结构和功能 class Student { constructor(sname, sage) { this.sname = sname; this.sage = sage; } intr() { console.log(`我是${this.sname},我今年${this.sage}岁。`); } } // 创建一个学生对象 var lilei = new Student("李雷", 21); console.log(lilei); lilei.intr(); </script>
但是上述用法也有一定的不足,若多个子对象共用相同的属性值,属性值应该放在哪里?
虽然直接在 class 中定义的方法,都默认保存在原型对象中。但是直接在 class 中定义的属性,却不会成为共有属性,不会保存在原型对象中,而是成为每个子对象的自有属性。
在旧 js 中,是和共有方法一起放在原型对象中;而为了和其它主流开发语言尽量一致,ES6的 class 放弃了在原型对象中保存共有属性的方式。而是改为用静态属性 static 保存!
静态属性
不需要创建子对象,单靠类型名就可直接访问的属性,就称为静态属性;今后在ES6中,如果希望所有子对象,都可使用一个共同的属性值时,都要用静态属性代替原来的原型对象属性。
静态属性定义与调用格式如下:
//定义静态属性: class 类型名{ static 共有属性名=属性值 ... ... } //访问静态属性: 类型名.静态属性
注意访问静态属性时不可写成 this.静态属性 。
标有 static 的静态属性,都保存在构造函数对象身上。因为构造函数在程序中不会重复,所以静态属性也不会重复;任何时候,任何地点,访问一个类型的静态属性,永远访问的都是同一份!
举例:使用静态属性替所有子对象保存共用的班级名;
<script> //定义学生类型,描述所有学生的统一结构和功能 class Student { // 定义静态属性,可以多个对象共用 static className = "初一2班"; constructor(sname, sage) { this.sname = sname; this.sage = sage; } intr() { console.log(`我是${this.sname},我今年${this.sage}岁`); } } // 创建学生对象 var lilei = new Student("李雷", 21); var hmm = new Student("韩梅梅", 20) console.log(lilei); console.log(hmm); lilei.intr(); hmm.intr(); // 一年后,初一升初二 Student.className = "初二2班"; lilei.intr(); hmm.intr(); console.log(Student); //log 默认输出的是Student构造函数的函数体(内容),不是对象结构 console.dir(Student); //dir 不输出函数的内容,而是输出对象在内存中的存储结构 </script>
两种类型间的继承
两种 class 之间可能包含部分相同的属性结构和方法定义,这时候就应该用到继承。
进行继承的方法:
(1)额外创建一个父级 class;
i. 父级 class 的构造函数中包含子类型 class 中相同部分的属性结构定义;
ii. 父级 class 的原型对象中包含子类型 class 中相同部分的方法定义;
iii. 既然父级 class 中保存了相同的属性结构和方法定义,则子类型 class 中,就可以删除所有重复的属性结构和方法定义;
(2)让子类型 class 继承父类型的 class;
i. 设置子类型的原型对象继承父类型的原型对象;
class 子类型 extends 父类型{ ... }
ii. 使用 super 关键字,调用父级的父级类型的构造函数。
举例:使用类型间继承,实现飞机大战游戏中敌机和降落伞类型的定义,并创建敌机对象和降落伞对象;
<script> //定义爷爷class,保存共有属性结构和方法 class enemy { constructor(x, y) { this.x = x; this.y = y; } fly() { console.log(`目标飞到x=${this.x},y=${this.y}的位置。`); } } class Plane extends enemy { constructor(x, y, score) { super(x, y); this.score = score; } getScore() { console.log(`击落敌机得${this.score}分!`); } } class Jls extends enemy { constructor(x, y, ming) { super(x, y); this.ming = ming; } getMing() { console.log(`击落降落伞,得生命值${this.ming}!`); } } var p1 = new Plane(255, 255, 20); var p2 = new Jls(222, 125, 1) p1.fly(); p1.getScore(); p2.fly(); p2.getMing(); </script>
3. Promise
promise 是专门保证多个异步任务必须顺序执行的一种特殊方式;在实际开发中,经常需要让多个异步任务顺序执行,而单纯先后调用多个异步函数的话,异步函数各自执行各自的,互不干扰,互相之间也不会等待,是错误的。
解决以上问题可以用回调函数,举例:使用回调函数保证多个异步任务顺序执行;
<script> function zhangsan(box) { console.log(`张三起跑!`); setTimeout(function () { console.log(`张三跑到了终点!`); box(); }, 6000) } function lisi(box) { console.log(`李四起跑!`); setTimeout(function () { console.log(`李四跑到了终点!`); box(); }, 4000) } function wangwu() { console.log(`王五起跑!`); setTimeout(function () { console.log(`王五跑到了终点!`); }, 2000) } zhangsan(function () { lisi(function () { wangwu(); }); }); </script>
但是用回调函数的话,如果要先后执行的任务多了,就会形成很深的嵌套结构——回调地狱,不仅极其不优雅,而且极其不便于维护。
这种情况下,就需要用到 promise 来代替回调函数。步骤:
(1)定义前一项任务
<script> function zhangsan(box) { console.log(`张三起跑!`); setTimeout(function () { console.log(`张三跑到了终点!`); box(); }, 6000) } function lisi(box) { console.log(`李四起跑!`); setTimeout(function () { console.log(`李四跑到了终点!`); box(); }, 4000) } function wangwu() { console.log(`王五起跑!`); setTimeout(function () { console.log(`王五跑到了终点!`); }, 2000) } zhangsan(function () { lisi(function () { wangwu(); }); }); </script>
(2)连接前后两个异步任务
前一项任务().then( 后一项任务 ) //注意最后一项任务不要再加()
两个任务之间也可以进行传参:
//前一项任务: function 前一项任务(){ return new Promise( function(开关){ var 变量=值 调用开关( 变量 ) } ) } //后一项任务: function 后一项任务(形参){ //形参=前一项任务中的变量值 }
举例:使用 Promise 模拟接力跑传接力棒;
<script> function zhangsan() { return new Promise( function (open) { var JieLiBang = "张三的接力棒"; console.log(`张三拿着${JieLiBang}起跑!`); setTimeout(function () { console.log(`张三跑到了终点!`); open(JieLiBang); }, 6000) } ) } function lisi(JieLiBang) { return new Promise( function (open) { console.log(`李四拿着${JieLiBang}起跑!`); setTimeout(function () { console.log(`李四跑到了终点!`); open(JieLiBang); }, 4000) } ) } function wangwu(JieLiBang) { console.log(`王五拿着${JieLiBang}起跑!`); setTimeout(function () { console.log(`王五跑到了终点!`); }, 2000) } zhangsan().then(lisi).then(wangwu); </script>
(3)错误处理,格式如下:
//前一项任务: function 前一项任务(){ return new Promise( function(成功的开关, 失败的开关){ var 变量=值 原异步任务 异步任务最后一句话 如果异步任务执行成功 调用成功的开关( 变量 )//此处开关通.then(),自动执行.then中的下一项任务 否则如果一部任务执行失败 调用失败的开关(错误提示信息)//此处开关通最后的.catch(),后续.then()不再执行。 } ) } //调用时: 前一项任务() .then(下一项任务) .then(...) .catch(function(错误提示信息){ 错误处理代码 })
举例:假设有人在跑步过程中摔倒了,要添加错误处理;
<script> function zhangsan() { return new Promise( function (resolve, reject) { var JieLiBang = "张三的接力棒"; console.log(`张三拿着${JieLiBang}起跑!`); setTimeout(function () { if (Math.random() < 0.5) { console.log(`张三拿着${JieLiBang}到达了终点!`); resolve(JieLiBang); } else { reject(`张三摔倒了!!`); } }, 6000) } ) } function lisi(JieLiBang) { return new Promise( function (resolve, reject) { console.log(`李四拿着${JieLiBang}起跑!`); setTimeout(function () { if (Math.random() < 0.5) { console.log(`李四拿着${JieLiBang}跑到了终点!`); resolve(JieLiBang); } else { reject(`李四摔倒了!!`); } }, 4000) } ) } function wangwu(JieLiBang) { console.log(`王五拿着${JieLiBang}起跑!`); setTimeout(function () { console.log(`王五跑到了终点!`); }, 2000) } zhangsan() .then(lisi) .then(wangwu) .catch(function (msg) { console.log(msg); console.log(`出现紧急状况,比赛终止!!!!`); }); </script>
(4)Promise对象三大状态 (记忆)
a:当异步任务执行过程中,整个 new Promise() 对象处于 pending(挂起) 状态;
b:当异步任务成功执行完,调用成功的开关函数时,整个 new Promise() 对象切换为 fulfilled(成功) 状态,new Promise() 会自动调用 .then() 执行下一项任务;
c:当异步任务执行出错,调用失败的开关函数,整个 new Promise() 对象切换为 rejected(出错) 状态,new Promise() 会自动调用 .catch() 执行错误处理代码。
在行业中,这两个开关常用(规范):
正确的开关:resolve(同意继续)
失败的开关:reject(拒绝继续)