JS与ES6高级编程学习笔记(五)——ECMAScript6 代码组织

2022-08-30 15:06:45 浏览数 (1)

一、概述

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:

代码语言:javascript复制
        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节点,而不用担心这些节点从文档移除时会引发内存泄露,即可以用来避免内存泄露的情况。

代码语言:javascript复制
  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

代码语言:javascript复制
var a = {}; // {} 可访问,a 是其引用
a = null; // 引用设置为 null
// {} 将会被从内存里清理出去

但如果一个对象被多次引用时,例如作为另一对象的键、值或子元素时,将该对象引用设置为 null 时,该对象是不会被回收的,依然存在

代码语言:javascript复制
var a = {};
var arr = [a];
a = null;
console.log(arr)  // [{}]

如果作为 Map 的键:

代码语言:javascript复制
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 为例,看看它是怎么上面问题的:

代码语言:javascript复制
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)

  1. Java中的包,C#中的命名空间可以很好的组织代码,但早期的JavaScript版本没有块级作用域、没有类、没有包、也没有模块,这样会带来一些问题,如复用、依赖、冲突、代码组织混乱等,随着前端的膨胀,模块化显得非常迫切。

通过许多开发者的努力创建了很多JavaScript模块化规范与框架如图5-11所示,但本质上这些都只是替代方案,并非原生的模块化,而ES6新增加的模块化功能改变了这些。

图5-10 前端模块规范

3.1、第一个模块

  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. (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导出

  1. 从"第一个模块"的示例中我们看到了两个指令"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导入

  1. 使用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、默认导出与导入

  1. 每个模块允许默认导出一个成员,导入时可以自定义对象名称,而不需要使用者过多关注导入模块的细节,解决了命名对象导出时使用该模块必须清楚的知道每个导出成员的名称的问题,简单说默认导出使模块的使用更加方便。 //定义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方式声明,即为识别符加一个前缀"#"。"#"是名称的一部分,也用于访问和声明。私有成员仅能在类的内部访问。

4.6、继承

(1)、extends与super。继承是面向对象最重要的特性之一,ES5中的继承相对麻烦,在ES6中使用关键字extends可以很方便的实现类之间的继承,但本质上还是基于原型链实现的。通过super可以访问父类成员。

//形状,父类

class Shape {

constructor(){

this.width=100;

}

draw(){

console.log("宽:" this.width);

}

}

//圆,继承形状

class Circle extends Shape{

height=200;

draw() { //类似重写父类方法

//调用父类方法

super.draw();

console.log("高:" this.height);

}

}

//实例化子类对象

let circle=new Circle();

circle.draw();

console.log(circle instanceof Shape,circle instanceof Object);

输出结果如图5-15所示:

图5-15 ES6 class示例输出结果

从输出结果可以看出circle对象是Shape、Object类型的实例。

(2)、构造方法与this。子类必须调用父类的构造方法,如果不显式调用将自动调用,只有调用super后,才允许用this关键字,否则将出错,因为子类实例是基于父类实例的,子类实例在获得父类实例后再新增自己的方法与属性。super调用父类构造方法时this指向的是子类实例。

//形状,父类

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;

这里演示一下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将指向该对象。

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。

六、异步编程 △

6.1、Generator 生成器

Generator生成器是一种带"*"号的特殊函数,是ES6中提供的一种异步编程解决方案。一个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方法进行统一处理。

//复杂计算

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.3、async-await函数

async-await是promise和generator的语法糖,使用async-await,搭配promise,可以通过编写形似同步的代码实现异步编程,提高代码的可读性,且使代码变得更加简洁。

async用于声明一个函数是异步的,当函数声明为async执行时将不再同步执行,函数执行完成后将返回一个promise对象,对promise对象的处理可以参考上一节。

//定义异步函数

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后的代码"); //先输出

输出结果如图5-21所示:

图5-21 asnyc示例输出结果

从输出结果可以看出来"异步函数getAge后的代码"这一句虽然在最后但是是先输出的,而两次调用getAge虽然在前面但是后输出结果的,可以看出getAge是异步的。

而await用于等待一个异步方法执行完成,await必须定义在异步方法中。

//定义函数

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());

1-1000毫秒时输出结果如图5-22所示:

图5-22 1-1000毫秒时await示例输出结果

因为client是异步方法,所以先输出了一个promise对象,而此时没有值,所有结果为undefined,当1000毫秒后输出的结果如图5-23所示:

图5-23 1000毫秒后await示例输出结果

七、课后作业

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绘画技术。

上机要求

  1. 定义好一个模块shapeModule.js,该模块向外暴露3个类。
  2. 如图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表示读取结束,请使用生成器函数实现读取,可以先试用回调的方法。

八、源代码

https://gitee.com/zhangguo5/JS_ES6Demos.git

九、教学视频

https://www.bilibili.com/video/BV1bY411u7ky?share_source=copy_web

0 人点赞