JS与ES6高级编程学习笔记(五)——ECMAScript6 代码组织
一、概述
ES6不仅在语法上有很大的改进,在代码的组织结构上也有重大升级,ES6中新增加了像Set、WeakSet、Map、WeakMap、ArrayBuffer、TypedArray和DataView等数组结构;原生的模块化解决了复用、依赖、冲突、代码组织混乱的问题,让开发复杂的前端项目变得更加容易;类(class)的加入使JavaScript面向对象更加易于理解。
ES6除了弥补了传统语言特性的不足外,在许多方面也增强了JavaScript动态语言的特性,可以说是扬长避短。在元编程中增加了Reflect反射对象与Proxy代理构造器,元编程是对编程语言进行编程,元编程的目标使代码更具描述性、拥有更强的表现力和灵活性。异步流程控制可以更加优雅、方便的编写异步程序,给用户带来更好的体验与性能。
二、集合
ES6中新增加了多种数据结构,Set可以存放任意不重复的值,Map弥补了对象类型存放key-value对的不足,而WeakSet与WeakMap则解决了Set与Map在GC回收垃圾时存在内存泄漏的风险, ArrayBuffer、TypedArray和DataView的引入是为了更加方便操作底层二进制数据的视图。
2.1、Set
在ES6中新增加了Set这种数据结构,通常称为集合,Set对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用,Set中的成员不允许重复。
//创建一个Set对象,使用数组初始化集合,注意2重复了
let numbers=new Set([1,2,2,3,4,5]);
//向Set中添加成员
numbers.add(5);
numbers.add(6);
//输出集合的大小
console.log("size:"+numbers.size);
//遍历集合
for(let n of numbers){
console.log(n);
}
输出结果如图5-1所示:
图5-1 Set示例输出结果
示例中共添加了8个元素,但size的值为6是因为有两个重复的元素。这里使用数组初始了一个新的Set对象,也可以是实现了iterable 接口的其他数据结构,当然如果不指定此参数或其值为null,则新的Set为空。
(1)、Set对象的常用操作
size属性:返回Set对象的值的个数,属性的默认值为0。
add(value)方法:在Set对象尾部添加一个元素。返回该Set对象。
has(value)方法:返回一个布尔值,表示该值在Set中存在与否。
delete(value)方法:移除Set的中与这个值相等的元素,返回has(value)在这个操作前会返回的值(即如果该元素存在,返回true,否则返回false)。has(value)在此后会返回false。
clear()方法:移除Set对象内的所有元素。
<script>
//创建一个空的Set对象
var numbers=new Set();
//添加
numbers.add("hello");
numbers.add("hello");
numbers.add({name:"tom"});
numbers.add({name:"tom"}); //注意对象总是不重复的
//输出Set的元素个数
console.log("size:"+numbers.size);
//测试元素是否存在
console.log("hello在集合中吗?"+numbers.has("hello"));
console.log("对象{name:\"tom\"}在集合中吗?"+numbers.has({name:"tom"}));
//删除元素
numbers.delete("hello");
numbers.delete({name:"tom"});
//输出Set的元素个数
console.log("删除后 size:"+numbers.size);
//清空元素
numbers.clear();
console.log("清空后 size:"+numbers.size);
//创建一个set对象,初始化特殊的重复对象
let set=new Set([NaN,NaN,undefined,undefined,null,null,{},{}]);
//使用...运算展开(spread)集合
var array=[...set];
console.log(array);
输出结果如图5-2所示:
图5-2 Set示例输出结果
示例中需要特别注意的是因为Set中的值总是唯一的,所以需要判断两个值是否相等,可以参考===操作符的使用;NaN与NaN相等,undefined与undefined相等;对象(含空对象)总是不相等的。
(2)、Set对象的遍历
keys()方法:返回键名的遍历器
values()方法:返回一个新的迭代器对象,该对象包含Set对象中的按插入顺序排列的所有元素的值。
entries():返回键值对的遍历器
forEach(callbackFn[,thisArg])方法:按照插入顺序,为Set对象中的每一个值调用一次callBackFn。如果提供了thisArg参数,回调中的this会是这个参数。
var numbers=new Set([1,2,3]);
//遍历所有的键
for(let n of numbers.keys()){
console.log(n); //输出1,2,3
}
//遍历所有的值
for(let n of numbers.values()){
console.log(n); //输出1,2,3
}
//遍历所有的键值对
for(let obj of numbers.entries()){
console.log(obj); //[1, 1] [2, 2] [3, 3]
}
//调用对象的forEach方法
numbers.forEach((value,key)=>console.log(value,key)); //输出1 1 2 2 3 3
//给回调函数指定参数
numbers.forEach(function(n){
console.log(n+this); //输出101 102 103
},100);
输出结果如图5-3所示:
图5-3 Set示例输出结果
从输出结果可以看出因为Set对象并没有区分键与值所以输出的结果是相同的,另外需要注意的是forEach中的回调函数带参数时不能使用箭头函数,因为此时箭头函数的this指向Window对象。
(3)、Set的使用技巧
使用Set可以方便的处理数组中的数据去重复、对多个数组进行集合运算操作:
//1、去除数组中的重复元素
var array=[1,1,2,2,3,"3","3","4","5"];
//定义Set对象,清除重复元素
var set=new Set(array);
//将set展开获得元素唯一的数组
var unique=[...set];
console.log(unique);
var x=new Set([100,200,300]);
var y=new Set([300,400,500]);
//2、并集(合并去重)
var set1=new Set([...x,...y]);
console.log(...set1.values());
//3、交集(共有元素)
var set2=new Set([...x].filter(n=>y.has(n)));
console.log(...set2.values());
//4、补集(x中存在而y中不存在的元素)
var set3=new Set([...x].filter(n=>!y.has(n)));
console.log(...set3.values());
输出结果如图5-4所示:
图5-4 Set示例输出结果
filter是Array对象中的一个过滤方法,语法如下:
var newArray = array.filter(callback(element[,index[,array]])[,thisArg])
callback:筛选数组中每个元素的函数。返回true表示该元素保留,false则不保留。
element:数组中当前正在处理的元素。
index可选参数,正在处理的元素在数组中的索引。
array可选参数,数组本身。
2.2、WeakSet
ES6中新增加的WeakSet对象的作用是可以将弱引用对象保存在集合中,该对象的使用方法与Set基本一样,但有如下几点不同:
(1)、WeakSet只允许添加对象类型,不允许添加原生类型值,因为没有引用,而Set都可以。
(2)、WeakSet对象中存储的对象值都是被弱引用的,如果没有其他的变量或属性引用这个对象值,则这个对象值会被当成垃圾回收掉.正因为这样,WeakSet对象是无法被枚举的,没有办法拿到它包含的所有元素,而Set则不然。
(3)、WeakSet比Set更适合(和执行)跟踪对象引用,尤其是在涉及大量对象时,可以避免一些性能问题,如内存泄漏。
下面是Stack Overflow中的一段脚本,可以用于更好的理解WeakSet:
const requests = new WeakSet(); class ApiRequest { constructor() { requests.add(this); } makeRequest() { if (!request.has(this)) throw new Error("Invalid access"); // do work } }
从上面的代码中可以看出这里集合并不想控制对象的生命周期但又需要判断对象是否存在使用WeakSet比Set要更加合适。
用于存储DOM节点,而不用担心这些节点从文档移除时会引发内存泄露,即可以用来避免内存泄露的情况。
const foos = new WeakSet() class Foo { constructor() { foos.add(this) } method() { if(!foos.has(this)) { throw new TypeError("Foo.prototype..method 只能在Foo的实例上调用") } } } // 这段代码的作用是保证了Foo 的实例方法只能在Foo的实例上调用。 // 这里用WeakSet的好处:数组foos对实例的引用不会被计入内存回收机制,所以删除实例时无需考虑foos, 也不会出现内存泄露
2.3、Map
键值对集合是非常常用的散列数据结构(Hash),ES6之前常常使用Object当作键值对集合使用,但Object只能是String与Symbol作为键,而ES6中新增加的Map的键可以是任意值,包括函数、对象或任意基本类型;Map中的key是有序的。
//定义用户对象
var jack={name:"jack"};
var mark={name:"mark"};
//定义一个Object字面量对象,当着Key-value集合使用
var objectMap={};
objectMap[jack]=jack.name; //向对象中添加元素,使用对象作为key
objectMap[mark]=mark.name;
console.log(objectMap[jack],objectMap[mark]);
console.log(objectMap["[object Object]"]);
输出结果如图5-5所示:
图5-5 Object作Map使用示例输出结果
当使用对象类型作为键向对象中添加成员时会自动转换为字符串,这里的jack与mark都转换成了" [object Object]",所以看到的输出结果都是mark,这并没有达到我们的预期,使用Map可以做到。
//定义用户对象
var jack={name:"jack"};
var mark={name:"mark"};
//创建Map对象
var map=new Map();
//向集合中添加key为jack对象,值为字符类型的key-value对
map.set(jack,jack.name);
map.set(mark,mark.name);
console.log(map.get(jack),map.get(mark));
输出结果如图5-6所示:
图5-6 Map示例输出结果
(1)、Map对象的常用操作
set(key,value)方法:向Map对象中设置键为key的值。
size属性:获得Map对象的键值对总数。
get(key)方法:获取键对应的值,如果不存在,则获取undefined。
has(key)方法:获取一个布尔值,表示Map实例是否包含键对应的值。
delete(key)方法:根据key删除集合中的对象,成功删除返回true,否则返回false。
clear()方法:移除Map对象的所有键/值对。
//定义一个空的Map对象
let users=new Map();
//设置成员
users.set("mark",{name:"mark",height:195});
//添加键为jack,值为{name:"jack",height:173}对象
users.set("jack",{name:"jack",height:173});
users.set("rose",{name:"玫瑰",height:188});
users.set("rose",{name:"rose",height:168}); //重复添加key为rose的对象
//获得成员个数
console.log("size:"+users.size);
//获取成员
console.log(users.get("rose"));
console.log(users.get("tom"));
//删除对象
users.delete("jack"); //返回true
users.delete("jack"); //返回false
//判断成员是否存在
console.log("jack是否存在:"+users.has("jack"));
//删除所有成员
users.clear();
console.log("size:"+users.size);
输出结果如图5-7所示:
图5-7 Map示例输出结果
示例中有几处需要注意的地方:重复添加key为rose的对象会覆盖原有对象,类似修改;删除成功时回返回true,如果key不存在则删除失败,返回false。
(2)、Map对象的遍历
keys()方法:获取迭代(Iterator)对象,含每个元素的key的数组。
values()方法:获取迭代(Iterator)对象,含每个元素的value的数组。
entries()方法:获取迭代(Iterator)对象,含每个元素的 [key, value] 数组。
forEach(callbackFn[, thisArg])方法:遍历集合,如果为forEach提供了thisArg,它将在每次回调中作为this值。
//定义一个Map对象,使用数组初始化
let users=new Map([
["mark",{name:"mark",height:195}],
["jack",{name:"jack",height:173}],
["rose",{name:"rose",height:168}]
]);
//1、使用for同时获取键与值,等同于users.entries()
for(let [key,value] of users){
console.log(key,value);
}
//2、获取所有的键
for(var key of users.keys()){
console.log(key);
}
//3、获取所有的值
for(var value of users.values()){
console.log(value);
}
//4、获取所有的键值对,等同于直接users
for(var entity of users.entries()){
console.log(entity[0],entity[1]);
}
//5、使用forEach遍历,注意value与key的顺序
users.forEach((value,key)=>console.log(value,key));
//带参数
users.forEach(function (v,k) {
console.log(this+k,v);
},'user:');
输出结果如图5-8所示:
图5-8 Map示例输出结果
(3)、Map与其它对象的转换
Map可以与数组、对象、JSON等其它类型进行相互转换,部分转换示例如下:
//1、数组转map
var array1=[[1,'a'],[2,'b']];
var map1=new Map(array1);
console.log(map1);
//2、map转数组
var array21=[...map1.entries()]; //[[1,'a'],[2,'b']];
var array22=[...map1]; //[[1,'a'],[2,'b']];
var array23=[...map1.values()]; //['a','b'];
console.log(array21,array22,array23);
//3、对象转成map
var user={name:"mark",height:195};
console.log(Object.entries(user)); //[["name", "mark"],["height", 195]]
var map3=new Map(Object.entries(user));
输出结果如图5-9所示:
图5-9 Map示例输出结果
注意展开运算符"…"的使用,Object.entries()的作用是获取对象自身可枚举属性的键值对数组。
2.4、WeakMap
ES6中新增加的WeakMap与WeakSet类似也是一个弱引用的数据结构,使用方法也与Map基本相同但两者的区别主要是内存分配与回收。
Map可能会导致内存泄漏因为Map内部数组会一直引用着每个键和值(强引用),如果在使用Map时只想引用对象而不想管理其生命周期则可以考虑使用WeakMap,注意只有key是弱引用。
<body>
<div id="div1"></div>
<div id="div2"></div>
<script>
var div1=document.querySelector("#div1");
var div2=document.querySelector("#div2");
let elements=[
[div1,"文章管理"],
[div2,"商品管理"]
];
var map=new Map(elements);
</script>
</body>
示例中Map使用div1与div2作为key,map对这两个对象是强引用的,如果不再需要使用则需要手动释放,否则可能会引起内存泄漏。
elements[0]=null;
elements[1]=null;
当然如果将上面的代码修改为WeakMap则不需要手动来管理对象的释放了。
WeakMap只接受对象作为键名,不支持clear方法,不支持遍历,也就没有了keys、values、entries、forEach这4个方法,也没有属性size;WeakMap 键名中的引用类型是弱引使用,假如这个引使用类型的值被垃圾机制回收了,WeakMap实例中的对应键值对也会消失;WeakMap中的key不计入垃圾回收,即若只有WeakMap中的key对某个对象有引用,那么此时执行垃圾回收时就会回收该对象。
WeakMap对象只允许使用对象作为key而Map可以是任意类型。而这些作为键的对象是弱引用的,值非弱引用,如果作为key的对象被GC回收则WeakMap中对应的对象也将被删除,因为不能确保key是否存在,所以key不可以枚举。
在我们的开发过程中,如果我们想要让垃圾回收器回收某一对象,就将对象的引用直接设置为 null
var a = {}; // {} 可访问,a 是其引用 a = null; // 引用设置为 null // {} 将会被从内存里清理出去
但如果一个对象被多次引用时,例如作为另一对象的键、值或子元素时,将该对象引用设置为 null 时,该对象是不会被回收的,依然存在
var a = {}; var arr = [a]; a = null; console.log(arr) // [{}]
如果作为 Map 的键:
var a = {}; var map = new Map(); map.set(a, 'hello map') a = null; console.log(map.keys()) // MapIterator {{}} console.log(map.values()) // MapIterator {"hello map"}
如果想让 a 置为 null 时,该对象被回收,该怎么做?
ES6 考虑到了这一点,推出了: WeakMap 。它对于值的引用都是不计入垃圾回收机制的,所以名字里面才会有一个"Weak",表示这是弱引用(对对象的弱引用是指当该对象应该被GC回收时不会阻止GC的回收行为)。
Map 相对于 WeakMap :
Map 的键可以是任意类型,WeakMap 只接受对象作为键(null除外),不接受其他类型的值作为键
Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键; WeakMap 的键是弱引用,键所指向的对象可以被垃圾回收,此时键是无效的
Map 可以被遍历, WeakMap 不能被遍历
下面以 WeakMap 为例,看看它是怎么上面问题的:
var a = {}; var map = new WeakMap(); map.set(a, 'hello map') map.get(a) a = null;
2.5、ArrayBuffer、TypedArray和DataView
ES6中引入了ArrayBuffer、TypedArray和DataView,方便操作底层二进制数据的视图,如在Canvas、Fetch API、File API、WebSockets、XMLHttpRequest等对象的API操作中会使用到。
(1)ArrayBuffer操作内存中的一段原始二进制数据。
(2)TypedArray共有 9 种类型的视图:
Int8Array();
Uint8Array();
Uint8ClampedArray();
Int16Array();
Uint16Array();
Int32Array();
Uint32Array();
Float32Array();
Float64Array();
基本构成是"类型+位数+Array",U表示无符号,如Uint16Array()表示无符16位整数视图。用来读写简单类型的二进制数据。
(3)DataView可以自定义复合格式的视图,用来读写复杂类型的二进制数据。
new DataView(buffer [,byteOffset[,byteLength]])
buffer:一个已经存在的ArrayBuffer或SharedArrayBuffer对象,DataView对象的数据源。
byteOffset:第一个字节在 buffer 中的字节偏移,默认从第1个字节开始。
byteLength:此DataView对象的字节长度。
//1、定义一个长度为16个字节的buffer
var buffer=new ArrayBuffer(16);
console.log(buffer,buffer.byteLength);
//2、定义可以存放2个16位的整型数据视图
var int16 = new Int16Array(2);
int16[0] = 13;
console.log(int16[0]); // 13
//3、定义一个DataView,从第10个字节开始的3个长度
var dataview1=new DataView(buffer,10,3);
console.log(dataview1,dataview1.byteLength);
输出结果如图5-10所示:
图5-10 二进制数组示例输出结果
2.6、Iterator 迭代器 △
Iterator(迭代器)是一个接口,实现该接口的对象拥有可迭代的功能,迭代器对象可以通过重复调用next()方法迭代。常见可迭代的内置对象有Array、String、Map、Set、TypedArray、Generator等,使用for…of循环可以直接迭代一个符合规范的iterator迭代器。
获得内置对象的迭代器对象。
var greeting="Hi World!";
for(let c of greeting){
console.log(c);
}
//获取字符串对象上的迭代器对象
var itor=greeting[Symbol.iterator]();
console.log(itor.next()); //输出:{value: "H", done: false}
console.log(itor.next()); //输出:{value: "i", done: false}
输出结果如图5-18所示:
图5-18 Iterator示例输出结果
当然,除了可以获取内置对象的迭代器之外也可以自定义迭代器,自定义迭代器需要遵循接口约束。
var MyIterator = {
content: "MyIterator",
[Symbol.iterator]() {
const that = this;
let currentIndex = 0;
return {
next() {
if (currentIndex < that.content.length) {
return {done: false, value: that.content[currentIndex++]}
} else {
return {done: true, value: undefined}
}
}
}
}
}
for(let c of MyIterator){
console.log(c); //输出:MyIterator
}
方法[Symbol.iterator]()在被调用时返回带有next()和return()方法的迭代器对象。
三、模块(module)
-
Java中的包,C#中的命名空间可以很好的组织代码,但早期的JavaScript版本没有块级作用域、没有类、没有包、也没有模块,这样会带来一些问题,如复用、依赖、冲突、代码组织混乱等,随着前端的膨胀,模块化显得非常迫切。
通过许多开发者的努力创建了很多JavaScript模块化规范与框架如图5-11所示,但本质上这些都只是替代方案,并非原生的模块化,而ES6新增加的模块化功能改变了这些。
图5-10 前端模块规范
3.1、第一个模块
-
为了让大家快速了解ES6中的模块化,现在我们在项目的js文件夹下定义第一个模块,并引用该模块,使用模块中的成员。
(1)、定义模块,module1.js文件的内容如下:
export let number = 100;
export function add(x,y) {
console.log(x+y);
}
(2)、引用模块,页面内容如下:
<script type="module">
import {add,number} from './js/module1.js';
add(number,200);
</script>
输出结果如图5-11所示:
图5-11 第一个ES6模块输出结果
在引用模块时需要注意声明type="module";当前并非所有的浏览器都支持原生的模块,请注意兼容性,本示例运行的浏览器版本是:Chrome 79.0.3945.13(正式版本) (64 位),至少版本61以后。
3.2、ES6中模块的特点
-
(1)、模块代码强制运行在严格模式下,并且没有任何办法退出严格模式,不管是否声明"use strict",在前面的章节中关于严格模式的要求已详细说明。
(2)、一个模块一个文件,文件名可以是*.mjs或*.js,为了区别模块与其它js脚本V8推荐使用*.mjs,但考虑到兼容性问题暂时我们建议还是使用*.js。示例中module1.js就是一个独立的文件。
(3)、模块间可以相互依赖。A模块可以引用B模块,B模块也可依赖A模块。
(4)、模块的API是静态的,顶层内容导出之后不能被动态修改。
(5)、模块都是单例,每一个模块只加载一次,只执行一次,如果下次再去加载相同文件,直接从内存中读取。
(6)、每个模块内声明的变量都是局部变量,不会污染全局作用域。在模块顶层作用域创建的变量,不会被自动添加到共享的全局作用域,他们只会在模块的顶层作用域内部存在,模块的顶层作用域this值为undefined;
3.3、export导出
-
从"第一个模块"的示例中我们看到了两个指令"export和import",export用于导出,import用于导入。
在模块中使用export可以导出模块想暴露给外部使用的接口信息,这些对象可以是变量、对象、函数、类或其它模块的内容,比如你想外部能够访问add这个函数,在模块中就需要导出这个函数,否则外部不可见。
export完成导出功能时可以放在成员的声明前。
//导出变量i
export let i=100;
//导出常量PI
export const PI=3.14;
//导出函数add
export function add(m,n) {
return m+n;
}
也可以先定义然后再集中导出。
let i=100;
const PI=3.14;
function add(m,n) {
return m+n;
}
//集中导出i,PI与add
export {i,PI,add};
也允许两种方法混合,导出时可以使用as重新命名,也可以将同一个对象重命名后导出多次。
//导出变量i
export let i=100;
const PI=3.14;
function add(m,n) {
return m+n;
}
//导出PI,导出函数add并重命名对外暴露的接口名称为plus
export {PI,add as plus}
导出值以修改后的为准,如下模块中导出的i最终的值为200。
var i=100;
export {i};
i=200;
直接导出值是不正确的,因为没有接口外部不能访问;集中导出时的大括号不能省略。
//直接导出值是错误的
export 3.14;
var i=100;
//这里会被认为是导出声明,但i没正确声明,如果想以集中方式导出则这里需要加大括号
export i;
正确的导出方式应该如下脚本所示。
//导出声明的成员pi
export var pi=3.14;
var i=100;
//集中导出已定义的成员i
export {i};
3.4、import导入
-
使用import指令可以加载模块并将export导出的成员导入到使用模块的上下文。假如已定义好的模块module8.js如下:
export let i=100;
const N=200;
function add(m,n) {
console.log(m+'+'+n+'=',m+n);
}
export {N,add as plus}
在另一个模块或页面中导入该模块的代码如下:
//加载模块module8.js,并指定导入成员i,N,plus
import {i,N,plus} from './js/module8.js';
plus(i,N); //输出100+200= 300
需要注意的是这里路径如果是相对路径则必须以"/"、"./"、或"../"开始;不需要将所有成员导入,但导入的成员必须在导出模块中定义且名称一致,否则将报语法错误。当然可以使用as将导入的成员重命名。
//加载模块module8.js,并指定导入成员N,plus,并将plus重命名为plus
import {N,plus as sum} from './js/module8.js';
sum(100,N); //输出100+200= 300
使用*号可以将所有导入的成员绑定到一个特定的对象,使用时可以通过"对象名.成员"的方式访问,我们常常把这种导入方式称为命名空间导入(namespace import)。
//导入模块module8.js中所有成员到m8这个对象中
import * as m8 from './js/module8.js';
//访问m8对象中的成员
m8.plus(m8.i,m8.N); //输出100+200= 300
上面的代码将module8中所有的对象都导出给了m8这个对象,使用时需要使加对象名访问,可以理解为m8就是命名空间。
模块允许多次导入,但因为是单例所以实际只会执行一次;导出的顶层对象是只读的,不允许修改,但对象中的成员允许修改。
模块文件module9.js的内容如下:
//导出变量i
export let i=100;
//导出对象math
export var math={
j:200,
add(m,n){
console.log(m+'+'+n+'=',m+n);
}
};
console.log("module9.js 被加载!");
导入并使用该模块的内容如下:
//加载模块9
import {i,math} from './js/module9.js';
//再次加载并重命名对象,为了解决冲突
import {i as m,math as calculator} from './js/module9.js';
//将对象的成员重新赋值,允许
math.j=300;
//调用add方法
math.add(i,math.j);
calculator.add(m,calculator.j);
//查看math与calculator是否为同一个对象
console.log(math===calculator);
//直接修改导入成员的值,不允许
i=200; //错误
math={}; //错误
输出结果如图5-12所示:
图5-12 ES6模块示例输出结果
从错误提示可以知道i被视为常量,所以不允许修改;虽然加载了两次模块,但控制台只输出了一次"module9.js被加载",可见module9.js只执行了一次;另外math与calculator相等可以看出导出的对象是单例的。
3.5、默认导出与导入
-
每个模块允许默认导出一个成员,导入时可以自定义对象名称,而不需要使用者过多关注导入模块的细节,解决了命名对象导出时使用该模块必须清楚的知道每个导出成员的名称的问题,简单说默认导出使模块的使用更加方便。
//定义math对象
let math={
add(m,n){ //加法方法
console.log(m+'+'+n+'=',m+n);
},
sub(m,n){ //减法方法
console.log(m+'-'+n+'=',m-n);
}
};
//默认导出math对象
export default math;
导入上面定义的模块:
//导入module10模块,注意这里没有使用{}
import calculator from './js/module10.js';
//调用calculator对象中的方法
calculator.add(200,100); //输出:200+100= 300
calculator.sub(200,100); //输出:200-100= 100
默认导出允许使用匿名对象、匿名函数或匿名变量。
//匿名对象
export default {price:100};
//匿名函数
export default function () {
}
//匿名变量
export default 900;
默认导出可以与命名导出混合使用。
export let math={};
export var i=100;
var j=200;
var k=300;
//j作为默认导出成员,k为命名导出成员
export {j as default,k};
导入时同样可以将命名与默认成员混合导入。
//导出模块名的成员,默认导出成员重命名为j
import {default as j,i,k} from './js/module12.js';
console.log(j,i,k); //输出:200 100 300
导入其它模块时允许将导入的内容再次导出。
//导入模块module12的成员,重命名后导出
export {i as n1,k} from './js/module12.js';
//导入模块module12的所有成员并重新导出
export * from './js/module12.js';
通过上面的方法可以实现模块间的"继承"。
四、类(class)
面向对象编程中class是非常重要的,如果你熟悉像Java、C#、C++这样的面向对象编程语言,你想用其中的面向对象思维来理解JavaScript是非常难的,因为JavaScript并非真正的面向对象语言,所以这给开发者带来了较大的障碍,ES6中增加了类(class),这样可以让JavaScript更加接近传统面向对象语言。
4.1、第一个类
假定我们现在要定义一个"形状(方、圆、五角形…)"类,该类拥有"颜色"属性,与"显示"颜色的方法。
传统定义如下:
//定义形状类(构造器)
function Shape(color) {
this.color=color;
}
//在构造器的原型对象中添加show方法
Shape.prototype.show=function () {
console.log("形状的颜色:"+this.color);
}
//创建对象,并调用show方法
var shape=new Shape("蓝色");
shape.show();
控制台输出结果:形状的颜色:蓝色
ES6定义如下:
//定义Shape类
class Shape{
//带参构造函数
constructor(color){
this.color=color;
}
//show方法
show(){
console.log("形状的颜色:"+this.color);
}
}
//创建对象,并调用show方法
let shape=new Shape("蓝色");
shape.show();
控制台输出结果:形状的颜色:蓝色
可以看出输出结果是完全一样的,但ES6定义类的方法明显更加接近传统OOP的方式。
4.2、ES6中类的特点
(1)、class只是语法糖,class定义的类本质还是一个构造函数,但这种写法更加清晰,更加接近经典面向对象的写法。
(2)、类的所有实例方法定义在类的prototype属性中,类中定义的方法默认为原型中所有对象共享的方法,但ES5中定义在构造器中的方法属于对象或构造器,如图5-13所示:
图5-13 ES6 class示例输出结果
(3)、使用class定义的类不具有提升特性,而构造函数具有提升特性。真正执行声明语句之前,会一直存在于临时死区中。
(4)、类中的代码强制运行在严格模式下,并且没有任何办法退出严格模式,不管是否声明"use strict",在前面的章节中关于严格模式的要求已详细说明。
(5)、在类中定义的方法不可枚举。
(6)、类默认都拥有Constructor内部方法。
4.3、字段
类中可以定义多种成员,包含字段、构造方法、属性、公共实例方法、静态方法。
(1)、实例字段,字段可以分为实例字段与静态字段,实例字段是每个对象独有的,相互间不会影响,定义时不需要使用关键字声明,如果不指定值则默认为undefined。
//定义Shape类
class Shape{
//公有实例字段
size=0;
name={};
width;
}
let rect1=new Shape();
rect1.width=100;
let rect2=new Shape();
console.log(rect1.name,rect1.size,rect1.width); //输出:{} 0 100
console.log(rect2.name,rect2.size,rect2.width); //输出:{} 0 undefined
console.log(rect1.name===rect2.name); //输出:false
(2)、静态字段,实例字段是每个实例独享的,如果需要共享则可以定义成静态字段,在字段声明前加上关键字static,静态成员属于类这点与传统面向对象一致。
class Shape{
//定义静态字段
static width=100;
}
let s1=new Shape();
console.log(s1.width); //输出:undefined
console.log(Shape.width); //输出:100
因为静态字段属于类,访问时只能用类名访问,所以s1中并没有width字段而需要使用Shape访问。静态字段可用于存放缓存数据、固定结构数据或者其他你不想在所有实例都复制一份的数据。
4.4、方法
方法也可以分为实例方法、静态方法与构造方法。实例方法属于实例,通过实例名访问;静态方法通过类名访问;在实例方法中可以通过类名访问静态字段,但是在静态方法中不能直接通过this访问实例成员。
class Shape {
//实例字段
width="100";
//静态字段
static PI=3.14;
//实例方法
getWidth(){
console.log("宽:"+this.width);
}
//静态方法
static getPI(){
console.log("PI:"+Shape.PI);
}
}
var shape=new Shape();
shape.getWidth(); //输出:宽:100
Shape.getPI(); //输出:PI:3.14
构造方法是通过new关键字创建对象时调用的特殊方法,ES6中class的构造方法具有如下特性:
(1)、方法名为constructor,这与经典的面向对象为类名的区别较大;
(2)、每个类都有一个默认的空构造方法;
(3)、构造方法默认会返回this,不建议指定返回对象;
(4)、一个类只能定义一个构造方法,没有重载;
class Shape {
constructor(width){ //构造方法
this.width=width;
}
}
4.5、属性
在class中通过get与set关键字可以声明属性,get方法用于取值,set方法用于设置值。
class Shape {
//获取宽度
get width(){
return this._width;
}
//设置宽度
set width(value){
//约束属性值
if(value>=0) {
this._width = value;
}else{
throw "宽度必须大于等于0";
}
}
}
let shape=new Shape();
shape.width=100; //设置正确的值
console.log(shape.width); //获取值
shape.width=-100; //设置不合理的值
输出结果如图5-14所示:
图5-14 ES6 class示例输出结果
类中的成员还有一些,比如Generator生成器、私有成员等;私有成员暂时没有统一的解决方法,可以通过"_名称"的方式命名约束,通过Symbol隐藏,私有成员一般使用#names方式声明,即为识别符加一个前缀"#"。"#"是名称的一部分,也用于访问和声明。私有成员仅能在类的内部访问。
结合闭包和Symbol实现私有属性:
var Shape=function(){ let name=Symbol("name"); class Shape{ constructor(n){ this[name]=n; } sayName(){ console.log(this[name]); } } return Shape; }(); let shape=new Shape("正方形"); shape.sayName(); console.log(shape[Symbol("name")]);
运行结果:
可见name是只读的,外部通过实例访问不到。
升级版:
//使用IIFE+闭包+class+Symbol实现私有成员 var Shape=function(){ let widthname=Symbol("width"); class Shape{ constructor(w){ this[widthname]=w; //使用Symbol作为属性名,定义宽度 } get width(){ return this[widthname]; } set width(value){ if(value>0){ this[widthname]=value; }else{ throw "宽度必须大于0"; } } } return Shape; }(); let s1=new Shape(); s1.width=100; console.log(s1.width); s1.width=-100;
使用#号实现私有成员:
class Shape{ #a; constructor(color,size){ this.color=color; this.#a=size; } getSize(){ console.log(this.#a); } } let shape=new Shape("red",100); shape.getSize(); //Private field '#a' must be declared in an enclosing class console.log(shape.#a);
运行结果:
完整示例:
<script> //使用#号实现私有成员 class Shape{ #width; //私有成员 get width(){ return this.#width; } set width(value){ if(value>0){ this.#width=value; } else{ throw "宽度只能大于0"; } } } let s1=new Shape(); s1.width=100; //Private field '#width' must be declared in an enclosing class //console.log(s1.#width); console.log(s1.width); s1.width=-100; </script>
4.6、继承
(1)、extends与super。继承是面向对象最重要的特性之一,ES5中的继承相对麻烦,在ES6中使用关键字extends可以很方便的实现类之间的继承,但本质上还是基于原型链实现的。通过super可以访问父类成员。
class Shape{ constructor(width){ this.width=width; } draw(){ console.log("宽:"+this.width); } } class Circle extends Shape{ constructor(width,height){ super(width); this.height=height; } draw(){ super.draw(); console.log("高:"+this.height); } } let circle=new Circle(100,200); circle.draw(); console.log(circle instanceof Shape);
输出结果如图5-15所示:
图5-15 ES6 class示例输出结果
从输出结果可以看出circle对象是Shape、Object类型的实例。
(2)、构造方法与this。子类必须调用父类的构造方法,如果不显式调用将自动调用,只有调用super后,才允许用this关键字,否则将出错,因为子类实例是基于父类实例的,子类实例在获得父类实例后再新增自己的方法与属性。super调用父类构造方法时this指向的是子类实例。
class Shape{ constructor(){ this.width=100; } draw(){ console.log("宽:"+this.width); } } class Circle extends Shape{ constructor(width,height){ super.width=width; this.height=height; } draw(){ super.draw(); console.log("高:"+this.height); } }
这样会报错:Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructorat new Circle
只有调用super后,才允许用this关键字。在super.width=width;前加上super();就解决问题了。
//形状,父类
class Shape {
constructor(type="形状"){ //构造方法
this.type=type;
console.log("调用父类构造方法");
}
draw(){
console.log("这是一个"+this.type);
}
}
//圆,继承形状
class Circle extends Shape{
constructor(radius){ //子类构造方法
super("圆形"); //调用父类构造函数
this.radius=radius;
}
draw() {
super.draw(); //调用父类中的draw()方法,该方法在原型中
console.log(this.type+"的半径是"+this.radius);
}
}
let circle=new Circle(75); //输出:调用父类构造方法
circle.draw();
输出结果如图5-16所示:
图5-16 ES6 class示例输出结果
在构造函数中定义的属性和方法相当于定义在父类实例上,而不是原型对象上。super作为对象时,在实例方法中,指向父类的原型对象;在静态方法中,指向父类。
(3)、静态成员继承。父类的静态成员也将被子类继承,这可能与经典的面向对象有些区别。
//形状,父类
class Shape {
static width = 100; //静态字段
static show() { //静态方法
console.log("宽度:" + Shape.width);
}
}
//圆,继承形状
class Circle extends Shape {
}
Circle.show(); //输出:宽度:100
console.log(Circle.width); //输出:100
(4)、扩展原生类。使用继承不仅可以扩展自定的类,也可以扩展系统中内置的类型,如:Boolean、Number、String、Array、Date、Function、RegExp、Error、Object等。
//定义类ArrayPro,继承自内置类型Array
class ArrayPro extends Array{
getData(index){ //自定义获得数据的方法
return this[index];
}
get size(){ //自定义属性,获得数组长度
return this.length;
}
get last(){ //自定义属性,获得最后一个元素
return this[this.size-1];
}
}
let arraypro=new ArrayPro(1,2,3,4,5,6);
console.log(arraypro.getData(1)); //输出:2
console.log(arraypro.size); //输出:6
console.log(arraypro.last); //输出:6
当然ES5也可以扩展内置类型,但方法相对复杂且并不支持真正array的性质,ES6可以非常自然的完成内置类型的扩展功能。
五、元编程 △
5.1、Reflect 反射
Reflect是ES6中新增加的一个对象,并非构造器,该对象中含有多个可完成"元编程(对编程语言进行编程)"功能的静态函数,能方便的对对象进行操作,也可以结合Proxy实现拦截功能,共计13个函数,TypeScript定义如下:
apply(target: Function, thisArgument: any, argumentsList: ArrayLike<any>): any;
construct(target: Function, argumentsList: ArrayLike<any>, newTarget?: any): any;
defineProperty(target: object, propertyKey: PropertyKey, attributes: PropertyDescriptor): boolean;
deleteProperty(target: object, propertyKey: PropertyKey): boolean;
get(target: object, propertyKey: PropertyKey, receiver?: any): any;
getOwnPropertyDescriptor(target: object, propertyKey: PropertyKey): PropertyDescriptor | undefined;
getPrototypeOf(target: object): object;
has(target: object, propertyKey: PropertyKey): boolean;
isExtensible(target: object): boolean;
ownKeys(target: object): PropertyKey[];
preventExtensions(target: object): boolean;
set(target: object, propertyKey: PropertyKey, value: any, receiver?: any): boolean;
setPrototypeOf(target: object, proto: any): boolean;
- Reflect 是为了操作对象而提供的新 API
- Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。
- 这些方法与proxy handlers (en-US)的方法相同。
- Reflect不是一个函数对象,因此它是不可构造的。
- 所以不能通过new运算符对其进行调用,或者将Reflect对象作为一个函数来调用。
- Reflect的所有属性和方法都是静态的
这里演示一下get与set方法的使用:
//要反射的对象
var shape = {
width: 100, height: 200, get area() {
return this.width * this.height;
}
};
//获取shape对象中的width属性值
console.log(Reflect.get(shape,"width")); //输出:100
//设置shape对象中的height属性值为300
Reflect.set(shape,"height",300); //true
console.log(Reflect.get(shape,"area")); //输出:30000
//获取shape对象中area的属性值,area中的this使用指定的对象替代
console.log(Reflect.get(shape,"area",{width:200,height:300}));//输出:60000
这里需要注意的是get与set方法的最后一个参数receiver是可选参数,默认为当前操作对象,如果指定后则this将指向该对象。
1. 取值:Reflect.get() Reflect.get() 方法:获取对象身上某个属性的值(类似于 target[name]) /* 参数说明 */ 1. target: 需要取值的目标对象 2. propertyKey: 需要获取的值的键值 3. receiver: 如果target对象中指定了getter,receiver则为getter调用时的this值。 /* 语法 */ Reflect.get(target, propertyKey[, receiver]) /* 返回值 */ 1. 如果含有该属性,返回对应属性值 2. 如果没有该属性,则返回 undefined /* 异常 */ 1. 如果目标值类型不是 Object,则抛出一个 TypeError。 示例: var myObject = { foo: 1, bar: 2, get baz() { return this.foo + this.bar; }, } Reflect.get(myObject, 'foofoo') // undefined Reflect.get(myObject, 'bar') // 2 Reflect.get(myObject, 'baz') // 3 // Object var obj = { x: 1, y: 2 }; Reflect.get(obj, "x"); // 1 // Array var arr = ["zero", "one"]; Reflect.get(arr, 1); // "one" // 带有get处理程序的代理 var x = {p: 1}; var obj = new Proxy(x, { get(t, k, r) { return k + "bar"; } }); Reflect.get(obj, "foo"); // "foobar" // 非对象 Reflect.get(1, 'foo') // 报错 Reflect.get(false, 'foo') // 报错 如果 propertyKey 属性部署了读取函数(getter),则读取函数的this绑定receiver。 var myObject = { foo: 1, bar: 2, get baz() { return this.foo + this.bar; }, }; var myReceiverObject = { foo: 4, bar: 4, }; Reflect.get(myObject, 'baz', myReceiverObject) // 8 2. 设值:Reflect.set() Reflect.set():以函数的方式给属性赋值。 /* 参数说明 */ 1. target: 设置属性的目标对象。 2. propertyKey: 设置的属性的名称。 3. value: 设置的值。 4. receiver: 如果遇到 setter,receiver则为setter调用时的this值。 /* 语法 */ Reflect.set(target, propertyKey, value[, receiver]) /* 返回值 */ 1. 如果设置成功,返回 true 2. 如果设置失败,则返回 false /* 异常 */ 1. 如果目标值类型不是 Object,则抛出一个 TypeError。 示例: var myObject = { foo: 1, set bar(value) { return this.foo = value; }, } myObject.foo // 1 Reflect.set(myObject, 'foo', 2); myObject.foo // 2 Reflect.set(myObject, 'bar', 3) myObject.foo // 3 // Object var obj = {}; Reflect.set(obj, "prop", "value"); // true obj.prop; // "value" // Array var arr = ["duck", "duck", "duck"]; Reflect.set(arr, 2, "goose"); // true arr[2]; // "goose" // 它可以截断数组. Reflect.set(arr, "length", 1); // true arr; // ["duck"]; // With just one argument, propertyKey and value are "undefined". var obj = {}; Reflect.set(obj); // true Reflect.getOwnPropertyDescriptor(obj, "undefined"); // { value: undefined, writable: true, enumerable: true, configurable: true } // 非对象类型 Reflect.set(1, 'foo', {}) // 报错 Reflect.set(false, 'foo', {}) // 报错 如果propertyKey属性设置了赋值函数,则赋值函数的this绑定receiver。 var myObject = { foo: 4, set bar(value) { return this.foo = value; }, }; var myReceiverObject = { foo: 0, }; Reflect.set(myObject, 'bar', 1, myReceiverObject); myObject.foo // 4 myReceiverObject.foo // 1 3. 判断:Reflect.has() Reflect.has(): 判断一个对象是否存在某个属性(等同于 in 运算符) /* 参数说明 */ 1. target: 目标对象. 2. propertyKey: 属性名,需要检查目标对象是否存在此属性。 /* 语法 */ Reflect.has(target, propertyKey) /* 返回值 */ 1. 如果该属性存在,返回 true 2. 如果该属性不存在,则返回 false /* 异常 */ 1. 如果目标值类型不是 Object,则抛出一个 TypeError。 示例: var myObject = { foo: 1, }; // 旧写法 'foo' in myObject // true // 新写法 Reflect.has(myObject, 'foo') // true Reflect.has({x: 0}, "x"); // true Reflect.has({x: 0}, "y"); // false // 如果该属性存在于原型链中,返回true Reflect.has({x: 0}, "toString"); // Proxy 对象的 .has() 句柄方法 obj = new Proxy({}, { has(t, k) { return k.startsWith("door"); } }); Reflect.has(obj, "doorbell"); // true Reflect.has(obj, "dormitory"); // false 4. 删除:Reflect.deleteProperty() Reflect.deleteProperty():用于删除对象的属性(等同于delete obj[name]) /* 参数说明 */ 1. target: 删除属性的目标对象。 2. propertyKey: 需要删除的属性的名称。 /* 语法 */ Reflect.get(target, propertyKey) /* 返回值 */ 1. 如果该属性不存在 或者 删除成功,返回 true 2. 如果该属性删除失败,则返回 false /* 异常 */ 1. 如果目标值类型不是 Object,则抛出一个 TypeError。 示例: const myObj = { foo: 'bar' }; // 旧写法 delete myObj.foo; // 新写法 Reflect.deleteProperty(myObj, 'foo'); var obj = { x: 1, y: 2 }; Reflect.deleteProperty(obj, "x"); // true obj; // { y: 2 } var arr = [1, 2, 3, 4, 5]; Reflect.deleteProperty(arr, "3"); // true arr; // [1, 2, 3, , 5] // 如果属性不存在,返回 true Reflect.deleteProperty({}, "foo"); // true // 如果属性不可配置,返回 false Reflect.deleteProperty(Object.freeze({foo: 1}), "foo"); // false 5. 定义:Reflect.defineProperty() Reflect.defineProperty():用来为对象定义属性(等同于 Object.defineProperty) /* 参数说明 */ 1. target: 目标对象。 2. propertyKey: 要定义或修改的属性的名称。 3. attributes: 要定义或修改的属性的描述。 /* 语法 */ Reflect.defineProperty(target, propertyKey, attributes) /* 返回值 */ 1. 如果该定义成功,返回 true 2. 如果该定义失败,则返回 false /* 异常 */ 1. 如果目标值类型不是 Object,则抛出一个 TypeError。 示例: function MyDate() { /*…*/ } // 旧写法 Object.defineProperty(MyDate, 'now', { value: () => Date.now() }); // 新写法 Reflect.defineProperty(MyDate, 'now', { value: () => Date.now() }); let obj = {} Reflect.defineProperty(obj, 'x', {value: 7}) // true obj.x // 7 6. 原型:Reflect.getPrototypeOf() Reflect.getPrototypeOf():用于读取对象的__proto__属性(等同于 Object.getPrototypeOf(obj)) /* 参数说明 */ target: 获取原型的目标对象。 /* 语法 */ Reflect.getPrototypeOf(target) /* 返回值 */ 1. 给定对象的原型。 2. 如果给定对象没有继承的属性,则返回 null。 /* 异常 */ 1. 如果目标值类型不是 Object,则抛出一个 TypeError。 示例: const myObj = new FancyThing(); // 旧写法 Object.getPrototypeOf(myObj) === FancyThing.prototype; // 新写法 Reflect.getPrototypeOf(myObj) === FancyThing.prototype; Reflect.getPrototypeOf({}); // Object.prototype Reflect.getPrototypeOf(Object.prototype); // null Reflect.getPrototypeOf(Object.create(null)); // null 与Object.getPrototypeOf的区别是: // 如果参数为 Object,返回结果相同 Object.getPrototypeOf({}) // Object.prototype Reflect.getPrototypeOf({}) // Object.prototype // 在 ES5 规范下,对于非 Object,抛异常 Object.getPrototypeOf('foo') // Throws TypeError Reflect.getPrototypeOf('foo') // Throws TypeError // 在 ES2015 规范下,Reflect 抛异常, Object 强制转换非 Object Object.getPrototypeOf('foo') // String.prototype Reflect.getPrototypeOf('foo') // Throws TypeError 7. 原型:Reflect.setPrototypeOf() Reflect.setPrototypeOf():方法用于设置目标对象的原型(等同于 Object.setPrototypeOf()) /* 参数说明 */ 1. target: 设置原型的目标对象。 2. propertyKey: 对象的新原型(一个对象或 null)。 /* 语法 */ Reflect.setPrototypeOf(target, prototype) /* 返回值 */ 1. 原型设置成功,返回 true。 2. 原型设置失败,则返回 false。 /* 异常 */ 1. 如果目标值类型不是 Object ,抛出一个 TypeError。 2. prototype 既不是对象也不是 null,抛出一个 TypeError。 示例: const myObj = {}; // 旧写法 Object.setPrototypeOf(myObj, Array.prototype); // 新写法 Reflect.setPrototypeOf(myObj, Array.prototype); myObj.length // 0 Reflect.setPrototypeOf({}, Object.prototype); // true // 它可以改变一个对象的 [[Prototype]] 为 null. Reflect.setPrototypeOf({}, null); // true // 如果目标不是可扩展的,则返回false Reflect.setPrototypeOf(Object.freeze({}), null); // false // 如果它导致原型链循环,则返回false。 var target = {}; var proto = Object.create(target); Reflect.setPrototypeOf(target, proto); // false 第一个参数情况: /* 第一个参数非对象 */ Object.setPrototypeOf(1, {}) // 1 Reflect.setPrototypeOf(1, {}) // TypeError: Reflect.setPrototypeOf called on non-object /* 第一个参数是undefined或null */ Object.setPrototypeOf(null, {}) // TypeError: Object.setPrototypeOf called on null or undefined Reflect.setPrototypeOf(null, {}) // TypeError: Reflect.setPrototypeOf called on non-object 8. 绑定:Reflect.apply() Reflect.apply():用于绑定this对象后执行给定函数(等同于 Function.prototype.apply.call()) /* 参数说明 */ 1. target: 目标函数。 2. thisArgument: target函数调用时绑定的this对象。 3. argumentsList: target函数调用时传入的实参列表,该参数应该是一个类数组的对象。值。 /* 语法 */ Reflect.apply(target, thisArgument, argumentsList) /* 返回值 */ 1. 返回值是调用完带着指定参数和 this 值的给定的函数后返回的结果。 /* 异常 */ 1. 如果 target 对象不可调用,抛出 TypeError。 示例: Reflect.apply(Math.floor, undefined, [1.75]); // 1; Reflect.apply(String.fromCharCode, undefined, [104, 101, 108, 108, 111]); // "hello" Reflect.apply(RegExp.prototype.exec, /ab/, ["confabulation"]).index; // 4 Reflect.apply("".charAt, "ponies", [3]); // "i" 一般来说,如果要绑定一个函数的this对象,可以这样写fn.apply(obj, args), 但是如果函数定义了自己的apply方法,就只能写成Function.prototype.apply.call(fn, obj, args) 采用Reflect对象可以简化这种操作。 const ages = [11, 33, 12, 54, 18, 96]; // 旧写法 const youngest = Math.min.apply(Math, ages); const oldest = Math.max.apply(Math, ages); const type = Object.prototype.toString.call(youngest); // 新写法 const youngest = Reflect.apply(Math.min, Math, ages); const oldest = Reflect.apply(Math.max, Math, ages); const type = Reflect.apply(Object.prototype.toString, youngest, []); 9. new:Reflect.construct() Reflect.construct():调用构造函数的方法(等同于 new target(...args)) /* 参数说明 */ 1. target: 被运行的目标构造函数 2. argumentsList: 类数组,目标构造函数调用时的参数。 3. newTarget: 作为新创建对象的原型对象的constructor属性, 参考 new.target 操作符,默认值为target。 /* 语法 */ Reflect.construct(target, argumentsList[, newTarget]) /* 返回值 */ 1. 以target(如果newTarget存在,则为newTarget)函数为构造函数,argumentList为其初始化参数的对象实例。 /* 异常 */ 1. 如果target或者newTarget不是构造函数,抛出TypeError,异常。 示例: function Greeting(name) { this.name = name; } // new 的写法 const instance = new Greeting('张三'); // Reflect.construct 的写法 const instance = Reflect.construct(Greeting, ['张三']); unction OneClass() { this.name = 'one'; } function OtherClass() { this.name = 'other'; } // 创建一个对象: var obj1 = Reflect.construct(OneClass, args, OtherClass); // 与上述方法等效: var obj2 = Object.create(OtherClass.prototype); OneClass.apply(obj2, args); console.log(obj1.name); // 'one' console.log(obj2.name); // 'one' console.log(obj1 instanceof OneClass); // false console.log(obj2 instanceof OneClass); // false console.log(obj1 instanceof OtherClass); // true console.log(obj2 instanceof OtherClass); // true 10. 扩展:Reflect.isExtensible () Reflect.isExtensible():判断当前对象是否可扩展(等同于 Object.isExtensible) /* 参数说明 */ 1. target: 检查是否可扩展的目标对象。 /* 语法 */ Reflect.isExtensible(target) /* 返回值 */ 1. 如果当前对象可扩展,返回 true 2. 如果当前对象不可扩展,则返回 false /* 异常 */ 1. 如果目标值类型不是 Object,则抛出一个 TypeError。 示例: const myObject = {}; // 旧写法 Object.isExtensible(myObject) // true // 新写法 Reflect.isExtensible(myObject) // true // 新对象是可扩展的。 var empty = {}; Reflect.isExtensible(empty); // === true // 但这是可以改变的. Reflect.preventExtensions(empty); Reflect.isExtensible(empty); // === false // 根据定义,密封对象是不可扩展的. var sealed = Object.seal({}); Reflect.isExtensible(sealed); // === false // 根据定义,冻结对象也是不可扩展的. var frozen = Object.freeze({}); Reflect.isExtensible(frozen); // === false 如果参数不是对象,Object.isExtensible会返回false,因为非对象本来就是不可扩展的, 而Reflect.isExtensible会报错。 Object.isExtensible(1) // false Reflect.isExtensible(1) // 报错 11. 禁扩:Reflect.preventExtensions() Reflect.preventExtensions():阻止新属性添加到对象(等同于 Object.preventExtensions) /* 参数说明 */ 1. target: 需要取值的目标对象阻止扩展的目标对象。 /* 语法 */ Reflect.preventExtensions(target) /* 返回值 */ 1. 如果设置成功,返回 true 2. 如果设置失败,则返回 false /* 异常 */ 1. 如果目标值类型不是 Object,则抛出一个 TypeError。 示例: var myObject = {}; // 旧写法 Object.preventExtensions(myObject) // Object {} // 新写法 Reflect.preventExtensions(myObject) // true // 默认情况下,对象是可扩展的 var empty = {}; Reflect.isExtensible(empty); // === true // 但这是可以改变的 Reflect.preventExtensions(empty); Reflect.isExtensible(empty); // === false 与Object.preventExtensions的区别 // ES5 环境 Object.preventExtensions(1) // 报错 // ES6 环境 Object.preventExtensions(1) // 1 // ES5、ES6 环境 Reflect.preventExtensions(1) // 报错 12. 属性:Reflect.ownKeys() Reflect.ownKeys():返回对象的所有属性(等同于 Object.getOwnPropertyNames+Object.getOwnPropertySymbols) /* 参数说明 */ 1. target: 获取自身属性键的目标对象。 /* 语法 */ Reflect.ownKeys(target) /* 返回值 */ 1. 由目标对象的自身属性键组成的 Array。 /* 异常 */ 1. 如果目标值类型不是 Object,则抛出一个 TypeError。 示例: var myObject = { foo: 1, bar: 2, [Symbol.for('baz')]: 3, [Symbol.for('bing')]: 4, }; // 旧写法 Object.getOwnPropertyNames(myObject) // ['foo', 'bar'] Object.getOwnPropertySymbols(myObject) //[Symbol(baz), Symbol(bing)] // 新写法 Reflect.ownKeys(myObject) // ['foo', 'bar', Symbol(baz), Symbol(bing)] 13. 描述:Reflect.getOwnPropertyDescriptor() Reflect.getOwnPropertyDescriptor():获取指定属性的描述对象(等同于 Object.getOwnPropertyDescriptor) /* 参数说明 */ 1. target: 需要寻找属性的目标对象。 2. propertyKey: 获取自己的属性描述符的属性的名称。 /* 语法 */ Reflect.getOwnPropertyDescriptor(target, propertyKey) /* 返回值 */ 1. 如果属性存在于给定的目标对象中,则返回属性描述符; 2. 否则,返回 undefined。 /* 异常 */ 1. 如果目标值类型不是 Object,则抛出一个 TypeError。 示例: Reflect.getOwnPropertyDescriptor({x: "hello"}, "x"); // {value: "hello", writable: true, enumerable: true, configurable: true} Reflect.getOwnPropertyDescriptor({x: "hello"}, "y"); // undefined 与Object.getOwnPropertyDescriptor的区别 // 第一个参数不是对象,抛出错误,表示参数非法 Reflect.getOwnPropertyDescriptor("foo", 0); // TypeError: "foo" is not non-null object // 第一个参数不是对象,不报错,返回 undefined, Object.getOwnPropertyDescriptor("foo", 0); // { value: "f", writable: false, enumerable: tru
-
Proxy:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
-
Reflect:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
5.2、Proxy 代理
Proxy是ES6中新增加的"元编程(对编程语言进行编程)"内容,使用Proxy可以对被代理的对象进行拦截,当被代理对象被访问时可以实现统一的处理。
//定义被代理的对象
var shape={width:100};
//定义代理代理
let proxy=new Proxy(shape,{
get:function (target, key, receiver) {
//输出被代理的目标对象,属性名称,receiver为getter调用时的this值(当前对象)
console.log(target, key, receiver);
//使用get方法从目标对象中获取值,把取得的值加100
return Reflect.get(target, key, receiver)+100;
},
set:function (target, key, value, receiver) {
//输出被代理的目标对象,属性名称,值,receiver为getter调用时的this值(当前对象)
console.log(target, key, value, receiver);
//在目标对象上设置属性值,设置值时将值加100
return Reflect.set(target, key, value+100, receiver);
}
});
proxy.width=101;
console.log(proxy.width);
输出结果如图5-19所示:
图5-19 Proxy示例输出结果
示例中我们的被代理对象是shape,当对该对象执行读取操作时将自动执行get方法,拦截后将值增加了100,当对该对象执行设置值操作时将自动执行set方法,拦截后将值也增加了100,所以最后输出301。
-
Proxy:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
-
Reflect:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
注意这个示例只做到的浅层的代理,如果要做到更多层的代理则需要递归。
六、异步编程 △
6.1、Generator
6.1.1、Generator 生成器可以解决的问题
Generator生成器是一种带"*"号的特殊函数,是ES6中提供的一种异步编程解决方案。一个Generator可以在运行期间暂停,可以立即或稍后再继续执行。
假定我们有一个这样的需求,1秒后输出aaa,输出aaa后2秒输出bbb,输出bbb后3秒输出ccc,代码如下:
<script> setTimeout(() => { console.log("aaa"); setTimeout(() => { console.log("bbb"); setTimeout(() => { console.log("ccc"); }, 3000); }, 2000); }, 1000); </script>
输出结果:
示例可以满足要求,但是需要在回调函数中一层层嵌套,代码的可读性与可维护性变得非常差,出现了所谓的“回调地狱”,使用生成器函数可以解决问题。
6.1.2、Generator 生成器基本使用
生成器是一个带*号的特殊函数,直接调用不能获取预期结果,yield可以实现方法分隔并返回值:
function * gen(){ console.log(1); yield "aaa"; console.log(2); yield "bbb"; console.log(3); yield "ccc"; } var iterator=gen(); console.log(iterator.next()); //{value: 'aaa', done: false} console.log(iterator.next()); //{value: 'bbb', done: false} console.log(iterator.next()); //{value: 'ccc', done: false} console.log(iterator.next()); //{value: undefined, done: true} for(let v of gen()) { console.log(v); }
输出:
6.1.3、Generator 生成器函数的参数传递
next()方法是可以传入参数的,传入的参数作为第一条(上一条)语句yield的返回结果。
function * gen(args){ console.log(args); let x=yield "aaa"; console.log(x); let y=yield "bbb"; console.log(y); let z=yield "ccc"; console.log(z); } let iterator=gen("函数参数"); console.log(iterator.next()); //执行yield "aaa" console.log(iterator.next("xxx")); //yield aaa的返回值 console.log(iterator.next("yyy")); //yield bbb的返回值 console.log(iterator.next("zzz")); //yield ccc的返回值
结果:
6.1.4、Generator 生成器实例一
假定我们有一个这样的需求,1秒后输出aaa,输出aaa后2秒输出bbb,输出bbb后3秒输出ccc,代码如下:
function one(){ setTimeout(() => { console.log("aaa"); iterator.next(); }, 1000); } function two(){ setTimeout(() => { console.log("bbb"); iterator.next(); }, 2000); } function three(){ setTimeout(() => { console.log("ccc"); }, 3000); } function* gen(){ yield one(); yield two(); yield three(); } let iterator=gen(); iterator.next();
运行结果:
6.1.5、Generator 生成器实例二
假定需要模拟网上商城业务,第一步获取用户数据,第二步根据用户数据获取订单数据,第三步根据订单数据获取产品数据:
function getUser(){ setTimeout(() => { iterator.next("用户数据"); }, 1000); } function getOrder(){ setTimeout(() => { iterator.next("订单数据"); }, 1000); } function getProduct(){ setTimeout(() => { iterator.next("产品数据"); }, 1000); } function* gen(){ let userData=yield getUser(); console.log(userData); let orderData=yield getOrder(); console.log(orderData); let productData=yield getProduct(); console.log(productData); } let iterator=gen(); iterator.next();
输出结果:
6.1.6、Generator 生成器实例三
function *SendDataGenerator() {
yield "建立连接"; //产出一个状态,暂停点
console.log("1");
yield "传输数据";
console.log("2");
return "断开连接"; //完成
}
//调用生成器创建一个生成器实例
var sender=SendDataGenerator();
console.log(sender.next()); //输出:{value: "建立连接",done: false}
console.log(sender.next()); //输出:{value: "传输数据",done: false}
console.log(sender.next()); //输出:{value: "断开连接",done: true}
console.log(sender.next()); //输出:{value: undefined,done: true}
输出结果如图5-17所示:
图5-17 ES6 生成器示例输出结果
从输出结果可以看出函数并没有一次执行完成,每当调用一次next方法后获得一个状态,向下执行一步,直到return后完成的状态值为true。
6.2、Promise 异步控制流
Promise提供一种异步编程解决方案,比传统的回调函数和事件解决方案更合理、强大、简洁。让回调函数变成了链式调用,避免了层层嵌套,使程序流程变得清晰,并为一个或者多个回调函数抛出的错误通过catch方法进行统一处理。
Promise 是 ES6 引入的异步编程的新解决方案。语法上 Promise 是一个构造函数,用来封装异步操作 并可以获取其成功或失败的结果;
1. Promise 构造函数: Promise (excutor) {};
2. Promise.prototype.then 方法;
3. Promise.prototype.catch 方法;
6.2.1、基本使用
示例一:
const promise1 = new Promise((resolve, reject) => { //模拟耗时操作 setTimeout(() => { resolve("成功时的数据"); }, 1000); }); promise1.then(data => { console.log("执行成功,数据是:" + data); }); const promise2 = new Promise((resolve, reject) => { //模拟耗时操作 setTimeout(() => { reject("失败时的原因"); }, 1000); }); promise2.catch(error => { console.log("发生了错误,原因:" + error); });
执行结果:
示例二:
//复杂计算 function complexCompute(millseconds) { //返回一个Promise对象 return new Promise(function (resolve, reject) { if (millseconds < 0) { throw new Error("毫秒数必须大于0"); //异常 } else if (millseconds < 1000) { reject("毫秒数必须大于1000"); //失败时回调 } //在指定的毫秒数millseconds结束后返回一个随机数 setTimeout(() => { resolve(Math.random() * 1000); //成功时回调 }, millseconds); }); } //延迟时间为3000毫秒,执行成功,指定成功时的处理函数 complexCompute(3000).then(v => console.log(v)) //延迟时间为200毫秒,执行失败,指定成功与失败时的处理方法 complexCompute(200).then(v => console.log(v), r => console.log(r)); //延迟时间为-10毫秒,抛出异常,指定成功与异常时的处理函数 complexCompute(-10).then(v => console.log(v)).catch(r => console.log(r));
输出结果如图5-20所示:
图5-20 Promise示例输出结果
6.2.2、封装文件读取
const fs = require("fs"); let filePromise = new Promise((resolve, reject) => { fs.readFile("./data.json1", (err, data) => { if (err) reject(err); if (data) { resolve(data.toString()); } else{ resolve(""); } }); }); filePromise.then(data => { console.log(data); }, reason => { console.warn("错误消息:" + reason); }); // filePromise.then(data=>{ // console.log(data); // }).catch(reason=>{ // console.warn(reason); // });
运行结果:
6.2.3、封装AJAX请求
一般写法:
<script> //普通写法 //1、创建请求对象 const xhr=new XMLHttpRequest(); //2、初始化 xhr.open("get","http://127.0.0.1:5500/data.json1"); //3、发送请求 xhr.send(null); //4、绑定事件,响应回调 xhr.onreadystatechange=function(){ if(xhr.readyState===4){ //发送成功 if(xhr.status>=200&&xhr.status<=300){ //请求成功 console.log(xhr.responseText); }else{ console.log(xhr.status); } } } </script>
运行结果:
失败时:
成功时:
Promise写法:
<script> //Promise写法 const AJAXPromise = new Promise((resolve, reject) => { //1、创建请求对象 const xhr = new XMLHttpRequest(); //2、初始化 xhr.open("get", "http://127.0.0.1:5500/data.json"); //3、发送请求 xhr.send(null); //4、绑定事件,响应回调 xhr.onreadystatechange = function () { if (xhr.readyState === 4) { //发送成功 if (xhr.status >= 200 && xhr.status <= 300) { //请求成功 resolve(xhr.responseText); //成功时回调 } else { reject(xhr.status); //失败时回调 } } } }); AJAXPromise.then(data=>{ console.log(data); }).catch(err=>{ console.warn("错误:"+err); }); </script>
运行结果:
成功时:
失败时:
6.2.4、Promise.prototype.then
默认情况下promise.then方法将返回一个新的promise对象,这样可以实现链式编程:
<script> const p=new Promise((resolve,reject)=>{ setTimeout(() => { resolve("用户数据"); }, 1000); }); let p2=p.then(p=>{ console.log(p); return p; }); console.log(p2); p2.then(data=>{ console.log(data); }); </script>
运行结果:
1、如果不返回值,则成功fulfilled,但数据为undefined
2、如果返回值,则成功fulfilled,数据为返回值的内容,如在then中return "hello"
3、如果返回一个Promise对象,如果对象执行resolve,则结果为fulfilled,反之为rejected
4、如果抛出异常,则rejected
<script> const p=new Promise((resolve,reject)=>{ setTimeout(() => { resolve("用户数据"); }, 1000); }); var out=p.then(data=>{ console.log(data); throw "异常了"; return new Promise((resolve,reject)=>{ //resolve("订单数据"); reject("发生了错误"); }); }); console.log(out); //1、如果不返回值,则成功,但数据为undefined // [[PromiseState]]: "fulfilled" // [[PromiseResult]]: undefined //2、如果返回值,则成功,数据为返回值的内容,如在then中return "hello" // [[PromiseState]]: "fulfilled" // [[PromiseResult]]: "hello" //3、如果返回一个Promise对象,如果对象执行resolve,则结果为fulfilled,反之为rejected //如果返回的值如下: // return new Promise((resolve,reject)=>{ // resolve("订单数据"); // }); //则 //[[PromiseState]]: "fulfilled" //[[PromiseResult]]: "订单数据" //如果返回值如下: // return new Promise((resolve,reject)=>{ // reject("发生了错误"); // }); //则 // [[PromiseState]]: "rejected" // [[PromiseResult]]: "发生了错误" //4、如果抛出异常,则rejected //如 //throw "异常了"; //则 // [[PromiseState]]: "rejected" // [[PromiseResult]]: "异常了" </script>
运行结果:
6.2.5、Promise实践练习
如果不嵌套直接读取3个文件,则不能预知完成先后:
const fs = require("fs"); fs.readFile("./test/a.txt", (err, data) => { if (err) throw err; if (data) console.log(data.toString()); }); fs.readFile("./test/b.txt", (err, data) => { if (err) throw err; if (data) console.log(data.toString()); }); fs.readFile("./test/c.txt", (err, data) => { if (err) throw err; if (data) console.log(data.toString()); });
结果:
从结果可以看出因为3个文件读取的函数是异步的,无法确定哪个文件先读取完成。
嵌套读取3个文件,回调地狱式写法
const fs = require("fs"); fs.readFile("./test/a.txt", (err1, data1) => { fs.readFile("./test/b.txt", (err2, data2) => { fs.readFile("./test/c.txt", (err3, data3) => { if (data1) console.log(data1.toString()); if (data2) console.log(data2.toString()); if (data3) console.log(data3.toString()); }); }); });
这样做确实解决了先后顺序问题但有命名冲突与回调地狱的新问题。
Promise写法:
const fs = require("fs"); let filePromise = new Promise((resolve, reject) => { fs.readFile("./test/a.txt", (err, data) => { if (err) reject(err); if (data) resolve([data]); }); }); filePromise.then(value => { return new Promise((resolve, reject) => { fs.readFile("./test/b.txt", (err, data) => { if (err) reject(err); if (data) { value.push(data); resolve(value); } }); }); }).then(value => { return new Promise((resolve, reject) => { fs.readFile("./test/c.txt", (err, data) => { if (err) reject(err); if (data) { value.push(data); resolve(value); } }); }); }).then(data=>{ console.log(data.join("\r\n")); });
运行结果:
使用promise就解决了先后顺序问题、命名冲突与回调地狱的问题。
6.2.6、Promise对象catch方法
// Promise对象catch方法 const p = new Promise((resolve, reject) => { setTimeout(() => { // 设置p对象的状态为失败,并设置失败的值 reject("失败啦~!"); }, 1000); }) // p.then(value=>{ // console.log(value); // },reason=>{ // console.warn(reason); // }); p.catch(reason => { console.warn(reason); });
reject的回调方法可以直接放在then的第二个参数位置也可以使用catch传入回调函数。
6.3、async-await函数
async-await是promise和generator的语法糖,使用async-await,搭配promise,可以通过编写形似同步的代码实现异步编程,提高代码的可读性,且使代码变得更加简洁。
6.3.1、async函数的返回值
async用于声明一个函数是异步的,当函数声明为async执行时将不再同步执行,函数执行完成后将返回一个promise对象,对promise对象的处理可以参考上一节。
async的返回值与then的返回值非常类似:
1、如果不返回值,则成功fulfilled,但数据为undefined
2、如果返回值,则成功fulfilled,数据为返回值的内容,如在then中return "hello"
3、如果返回一个Promise对象,如果对象执行resolve,则结果为fulfilled,反之为rejected
4、如果抛出异常,则rejected
<script> async function foo() { // 1、如果不返回值,则成功fulfilled,但数据为undefined //[[PromiseState]]: "fulfilled" //[[PromiseResult]]: undefined // 2、如果返回值,则成功fulfilled,数据为返回值的内容, 如在then中return "hello" // 如: // return "hello" // 则: // [[PromiseState]]: "fulfilled" // [[PromiseResult]]: "hello" // 3、如果返回一个Promise对象,如果对象执行resolve,则结果为fulfilled, 反之为rejected //如: // return new Promise((resolve,reject)=>{ // resolve("成功时的数据"); // }); //则: //[[PromiseState]]: "fulfilled" //[[PromiseResult]]: "成功时的数据" //如 // return new Promise((resolve,reject)=>{ // reject("错误信息"); // }); //则 // [[PromiseState]]: "rejected" // [[PromiseResult]]: "错误信息" // 4、如果抛出异常,则rejected //如 throw new Error("异常数据"); //[[PromiseState]]: "rejected" //[[PromiseResult]]: Error: 异常数据 } let result = foo(); console.log(result); </script>
运行结果:
6.3.2、await 表达式
await一般需要配合async函数一起使用,而async函数不一定需要await表达式,使用async-await,搭配promise,可以通过编写形似同步的代码实现异步编程,提高代码的可读性,且使代码变得更加简洁。
1. await 必须写在 async 函数中;
2. await 右侧的表达式一般为 promise 对象;
3. await 返回的是 promise 成功的值;
4. await 的 promise 失败了, 就会抛出异常, 需要通过 try...catch 捕获处理;
<script> // async函数 + await表达式:异步函数 // 创建Prmise对象 let promise=new Promise((resolve,reject)=>{ setTimeout(() => { resolve("成功时的数据"); }, 1000); }); async function foo(){ // await 返回的是 promise 成功的值 let result=await promise; console.log(result); //成功时的数据 } foo(); </script>
结果:
<script> // async函数 + await表达式:异步函数 // 创建Prmise对象 let promise = new Promise((resolve, reject) => { setTimeout(() => { reject("发生了错误"); }, 1000); }); async function foo() { try { // await 返回的是 promise 成功的值 let result = await promise; console.log(result); } catch (err) { console.log(err); } } foo(); </script>
结果:
6.3.3、async 和 await 读取文件案例
示例代码:
//导入模块 const fs = require("fs"); //读取文件a function readfileA() { return new Promise((resolve, reject) => { fs.readFile('./test/a.txt', (error, data) => { if (error) reject(error); //如果失败 resolve(data); //如果成功 }); }); } //读取文件b function readfileB() { return new Promise((resolve, reject) => { fs.readFile('./test/b.txt', (error, data) => { if (error) reject(error); //如果失败 resolve(data); //如果成功 }); }); } //读取文件c function readfileC() { return new Promise((resolve, reject) => { fs.readFile('./test/c.txt', (error, data) => { if (error) reject(error); //如果失败 resolve(data); //如果成功 }); }); } //主函数 async function main(){ //使用await读取文件a.txt的内容 let filea=await readfileA(); //使用await读取文件b.txt的内容 let fileb=await readfileB(); //使用await读取文件c.txt的内容 let filec=await readfileC(); console.log(filea.toString()); console.log(fileb.toString()); console.log(filec.toString()); } main();
运行结果:
6.3.4、async 和 await 结合发送ajax请求
示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script> function sendAJAX(url) { return new Promise((resolve, reject) => { //1、创建请求对象 const xhr = new XMLHttpRequest(); //2、初始化 xhr.open("get",url); //3、发送请求 xhr.send(null); //4、绑定事件,响应回调 xhr.onreadystatechange = function () { if (xhr.readyState === 4) { //发送成功 if (xhr.status >= 200 && xhr.status <= 300) { //请求成功 resolve(xhr.responseText); //成功时回调 } else { reject(xhr.status); //失败时回调 } } } }); } async function main(){ let result=await sendAJAX("http://127.0.0.1:5500/data.json"); console.log(result); } main(); </script> </body> </html>
结果:
6.3.5、async 和 await年龄示例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script> //定义异步函数 async function getAge(age) { if (age > 0) { return age; //成功 resolve } else { throw "年龄必须大于0"; //失败 reject } } //执行getAge获得promise,指定成功时的处理方法 getAge(28).then(v => console.log(v)); //执行getAge获得promise,指定失败时的处理方法 getAge(-10).catch(r => console.log(r)); console.log("异步函数getAge后的代码"); //先输出 </script> </body> </html>
输出结果如图5-21所示:
图5-21 asnyc示例输出结果
从输出结果可以看出来"异步函数getAge后的代码"这一句虽然在最后但是是先输出的,而两次调用getAge虽然在前面但是后输出结果的,可以看出getAge是异步的。
而await用于等待一个异步方法执行完成,await必须定义在异步方法中。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script> //定义函数 function getAge(age) { //1秒后返回结果 return new Promise((resolve, reject) => { setTimeout(() => resolve(age), 1000); }); } //定义异步函数 async function client() { //等待getAge执行成功后返回结果,未返回结果前不向下执行 let age = await getAge(18); console.log(age); } //执行 console.log(client()); </script> </body> </html>
1-1000毫秒时输出结果如图5-22所示:
图5-22 1-1000毫秒时await示例输出结果
因为client是异步方法,所以先输出了一个promise对象,而此时没有值,所有结果为undefined,当1000毫秒后输出的结果如图5-23所示:
图5-23 1000毫秒后await示例输出结果
6.3.6、async 和 await输出字符示例
(1)、假定我们有一个这样的需求,1秒后输出aaa,输出aaa后1秒输出bbb,输出bbb后1秒输出ccc,代码如下:
<script> setTimeout(() => { console.log("aaa"); }, 1000); setTimeout(() => { console.log("bbb"); }, 1000); setTimeout(() => { console.log("ccc"); }, 1000); </script>
如果这样做,结果如下:
这样做的问题:
1、aaa,bbb,ccc是异步执行的,不知道谁先执行完成,不能保证先后
2、没有达到效果1秒后输出aaa,输出aaa后1秒输出bbb,输出bbb后1秒输出ccc
改进:
<script> setTimeout(() => { console.log("aaa"); setTimeout(() => { console.log("bbb"); setTimeout(() => { console.log("ccc"); }, 1000); }, 1000); }, 1000); </script>
运行效果:
这里运行时确实可以实现每隔1秒后输出aaa再输出bbb再输出ccc,但这里是使用嵌套回调的办法,会产生“回调地狱”的问题。
(2)、使用生成器函数改进:
function funa(){ setTimeout(() => { console.log("aaa"); iterator.next(); }, 1000); } function funb(){ setTimeout(() => { console.log("bbb"); iterator.next(); }, 1000); } function func(){ setTimeout(() => { console.log("ccc"); }, 1000); } //定义生成器函数 function * gen(){ yield funa(); yield funb(); yield func(); } //调用生成器函数,返回一个iterator对象 let iterator=gen(); //调用第一个yield块 iterator.next();
运行效果:
没有回调地狱,问题解决。
(3)、使用Promise改进:
先看一个容易犯错的问题
<script> //构造promise对象 let promisea=new Promise((resolve,reject)=>{ //模拟耗时操作 setTimeout(() => { resolve("aaa"); }, 1000); }); let promiseb=new Promise((resolve,reject)=>{ setTimeout(() => { resolve("bbb"); }, 1000); }) let promisec=new Promise((resolve,reject)=>{ setTimeout(() => { resolve("ccc"); }, 1000); }) promisea.then(data=>{ console.log(data); return promiseb; }).then(data=>{ console.log(data); return promisec; }).then(data=>{ console.log(data); }); </script>
输出结果:
aaa,bbb,ccc同时完成的,并不是每隔1秒完成的,我们把输出修改一下
//构造promise对象 let promisea=new Promise((resolve,reject)=>{ //模拟耗时操作 setTimeout(() => { resolve("aaa"); }, 3000); }); let promiseb=new Promise((resolve,reject)=>{ setTimeout(() => { resolve("bbb"); }, 3000); }) let promisec=new Promise((resolve,reject)=>{ setTimeout(() => { resolve("ccc"); }, 3000); }) console.log(promisea); console.log(promiseb); console.log(promisec);
开始状态:
3秒后的状态:
问题是:new Promise对象时中间的代码已经在执行。
<script> function funa() { return new Promise((resolve, reject) => { setTimeout(() => { resolve("aaa"); }, 1000); }) } function funb() { return new Promise((resolve, reject) => { setTimeout(() => { resolve("bbb"); }, 1000); }) } function func() { return new Promise((resolve, reject) => { setTimeout(() => { resolve("ccc"); }, 1000); }) } funa().then(data => { console.log(data); return funb(); }).then(data => { console.log(data); return func(); }).then(data => { console.log(data); }); </script>
结果:
使用promise解决问题。
(4)、使用async-await解决:
<script> function funa() { return new Promise((resolve, reject) => { setTimeout(() => { resolve("aaa"); }, 1000); }) } function funb() { return new Promise((resolve, reject) => { setTimeout(() => { resolve("bbb"); }, 1000); }) } function func() { return new Promise((resolve, reject) => { setTimeout(() => { resolve("ccc"); }, 1000); }) } async function main(){ let a=await funa(); console.log(a); let b=await funb(); console.log(b); let c=await func(); console.log(c); } main(); </script>
结果:
使用async-await完美解决问题,而且比promise更加简洁,像写同步代码一样写异步代码。
七、课后作业
7.1、上机任务一(90分钟内完成)
上机目的
1、掌握ES6中集合Set与Map的应用。
2、巩固DOM操作。
上机要求
1、定义一个app对象,在该对象中封装好产品管理的业务逻辑,完成产品管理功能,如图5-24所示:
图5-24 产品管理原型
2、使用Set集合封装所有的数据。
3、完成产品的展示、添加、编辑、删除功能,删除时需要提示用户是否删除,添加时需要校验字段是否为空,尝试添加重复数据到Set集合中。
4、先用Set完成所有功能,复制页面后将Set替换成Map,实现相同的功能,试比较两者的区别。
推荐实现步骤
步骤1:创建好app对象,根据业务设计出对象的结构,参考结构如下,可以根据自己的思路调整。
var app = {
data: new Set([{...}, {...}, {...}...]),
current:null,
init() {
//初始化
},
query() {
//搜索与展示
},
delete() {
//删除
},
findById(id) {
//根据编号获得产品对象
},
edit() {
//编辑
},
save() {
//保存
}
};
步骤2:根据不同的方法完成相应的功能,先不需要考虑界面,在控制台完成所有的方法测试,通过后再根据需要渲染界面,完成其它功能。
步骤3:反复测试运行效果,优化代码,关键位置书写注释,必要位置进行异常处理。
7.2、上机任务二(90分钟内完成)
上机目的
1、掌握ES6中模块的定义、导入与导出。
2、掌握ES6中模块间的引用与应用。
上机要求
1、使用模块改进本章上机任务一,完成一个升级版本的产品管理功能,效果如图5-24所示:
2、定义5个模块,模块间的依赖关系与基本功能如图5-25所示,模块中的成员仅供参考,可以根据自己的实现思路进行调整。
图5-25 产品管理模块间依赖关系
3、页面最终只允许使用app.js主模块与utils.js工具模块。
4、所有功能要求请参照本章的上机任务一。
5、必须使用到import、export、默认导入与导出技术。
推荐实现步骤
步骤1:根据依赖关系逐个创建好每个模块,先创建没有依赖任何模块的模块,控制台测试各模块功能。
步骤2:保证模块的正确性后按要求完成每个功能。
步骤3:反复测试运行效果,优化代码,关键位置书写注释,必要位置进行异常处理。
7.3、上机任务三(60分钟内完成)
上机目的
1、掌握ES6中模块与类的定义。
2、掌握类的继承。
3、了解Canvas绘画技术。
上机要求
- 定义好一个模块shapeModule.js,该模块向外暴露3个类。
- 如图5-26所示创建3个class(类),定义好属性与方法,父类中draw方法向控制台输出当前形状的基本信息,不需要实现绘图功能,area方法计算形状的面积,PI是静态字段。
图5-26 继承关系图
2、实现形状间的继承关系,构造方法要求可以初始化所有参数,子类构造方法要求调用父类构造方法,如图5-26所示。
3、分别创建不同类型的测试对象,定义对象时传入参数,调用对象中的方法。
4、重写draw方法,通过Canvas实现绘图功能,参考代码如下所示:
<canvas id="canvas1" width="500" height="500"></canvas>
<script>
var c=document.getElementById("canvas1");
var cxt=c.getContext("2d");
cxt.fillStyle="dodgerblue";
//fillRect(x: number, y: number, w: number, h: number): void;
cxt.fillRect(200,200,100,200);
cxt.beginPath();
//arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, anticlockwise?: boolean): void;
cxt.arc(100,100,100,0,Math.PI*2,true);
cxt.closePath();
cxt.fillStyle="orangered";
cxt.fill();
</script>
图5-27 Canvas绘图参考示例
5、定义一个drawHandler方法,接受不同的形状实例,调用绘图方法,在页面上绘出不同的图形,请使用多态的方式。
推荐实现步骤
步骤1:创建模块与页面,按要求定义好三个类并export,并实现其继承关系,测试效果。
步骤2:学会HTML5中使用Canvas绘画的基本技巧后,重写draw方法。
步骤3:在页面中导入模块,创建测试对象,调用方法实现绘图功能。
步骤4:反复测试运行效果,优化代码,关键位置书写注释,必要位置进行异常处理。
7.4、代码题
1、使用XMLHttpRequest第2版XHR2从服务器获取任意一张图片的二进制数据,显示在页面中如图5-28所示。
图5-28 AJAX获得图片数据显示在页面中
2、在第1题的基础上将请求到的图片进行水平翻转,如下图5-29所示。
图5-29 客户端翻转图片效果
7.5、扩展题
1、在nodejs环境下读取文件file1.txt,在文件file1.txt中包含了下一个要读取的内容file2.txt,在file2.txt中包含file3.txt文件内容,在file3.txt中包含end,表示读取结束,试想如果有n个文件,只有在最后一个文件中存放end表示读取结束,请使用生成器函数实现读取,可以先试用回调的方法。