Set和Map数据结构
Set
用法:
ES6提供了新的数据结构Set.它类似于数组,但是成员的值都是唯一的,没有重复的值.
Set本身是一个构造函数,用来生成Set数据结构.
const s = new Set();
[2,3,4,5,4,2,2].forEach(x => s.add(x));
for(let i of s){
console.log(i);
}
// 2,3,4,5
//Set结构不会添加重复的值
Set函数可以接受一个数组(或者具有iterable接口的其他数据结构)作为参数,用来初始化.
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
// 例二
function divs () {
return [...document.querySelectorAll('div')];
}
const set = new Set(divs());
set.size // 56
// 类似于
divs().forEach(div => set.add(div));
set.size // 56
所以去除数组重复成员的方法:
[...new Set(array)]
向Set加入值的时候,不会发生类型转换
在Set内部,两个NaN是相等的
两个对象总是不相等的
Set实例的属性和方法
属性:
Set.prototype.constructor:构造函数,默认就是Set函数
Set.prototype.size:返回Set实例的成员总数.
方法:分为两大类
1.操作方法(用于操作数据)
2.遍历方法(用于遍历成员)
操作方法:
add(value):添加某个值,返回Set结构本身.
delete(value):删除某个值,返回一个布尔值,表示删除是否成功.
has(value):返回一个布尔值,表示该值是否为Set的成员.
clear():清除所有成员,没有返回值.
## Array.from方法可以将Set解构转为数组.
作用:去除数组重复成员
function dedupe(array){
return Array.from(new Set(array));
}
dedupe([1,1,2,3])//[1,2,3]
遍历方法:
keys():返回键名的遍历器
values():返回键值的遍历器
entries():返回键值对的遍历器
forEach():使用回调函数遍历每个成员
keys(),values(),entries()
keys方法,values方法,entries方法返回的都是遍历器对象.由于Set结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致.
let set = new Set(['red','green','blue']);
for (let item of set.keys()){
console.log(item);
}
//red
//green
//blue
for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
Set结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法.这意味着,可以省略values方法,直接用for…of循环遍历Set.
forEach()
Set结构的实例与数组一样,也拥有forEach方法,用于对每个成员执行某种操作,没有返回值.
set = new Set([1,4,9]);
set.forEach((value,key) => console.log(key + ':' + value););
//1:1
//4:4
//9:9
遍历的应用
扩展运算符(…)内部使用for…of循环,所以也可以用于Set结构.
let set = new Set(['red','green','blue']);
let arr = [...set];
//['red','green','blue']
扩展运算符和Set结构相结合,就可以去除数组的重复成员.
let arr = [3,5,2,2,5,5];
let unique = [...new Set(arr)];
//[3,5,2]
而且,数组的map和filter方法也可以间接用于Set了.
let set = new Set([1,2,3]);
set = new Set([...set].map(x => x*2));
//返回Set结构{2,4,6}
let set = new Set([1,2,3,4,5]);
set = new Set([...set].filter(x => (x%2) == 0));
//返回Set结构{2,4}
WeakSet
含义:WeakSet结构与Set类似,也是不重复的值的集合.但是,它与Set区别:
WeakSet的成员只能是对象,而不能是其他类型的值.
Map
Map数据结构,类似于对象,也是键值对的集合,但是’键’的范围不限于字符串,各种类型的值(包括对象)都可以当做键.也就是说,Object结构提供了"字符串-值"的对应,Map结构提供了"值-值"的对应,是一种更完善的Hash结构实现.
const m = new Map();
const o = {p:'Hello World'};
m.set(o,'content');
m.get(o);//"content"
m.has(o);//true
m.delete(o);//true
m.has(o);//false
实例的属性和操作方法
size,set(key,value),get(key),has(key),delete(key),clear()
遍历方法
Map结构原生提供三个遍历器生成函数和一个遍历方法
keys():返回键名的遍历器.
values():返回键值的遍历器.
entries():返回所有成员的遍历器.
forEach():遍历Map的所有成员.
注意:Map的遍历顺序就是插入顺序.
Map结构转为数组结构,比较快速的方法是使用扩展运算符(…).
结合数组的map方法,filter方法,可以实现Map的遍历和过滤(Map本省没有map和filter方法).
WeakMap
WeakMap与Map的区别有:
(1)WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名.
(2)WeakMap的键名所指向的对象,不计入垃圾回收机制.
API区别:WeakMap只有四个方法可用:get(),get(),has(),delete()
Proxy
理解为:在目标对象之前架设一层’拦截’,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写.Proxy这个词的原意是代理,用在这里表示由它"代理"某些操作,可以译为"代理器".
Reflect
概述
Reflect对象与Proxy对象一样,也是ES6为了操作对象而提供的新API.
Reflect对象的设计目的:
(1)将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上.现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上.也就是说,从Reflect对象上可以拿到语言内部的方法.
(2)修改某些Object方法的返回结果,让其变得更合理.比如,Object.defineProperty(obj,name,desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj,name,desc)则会返回false.
/ 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
(3)让Object操作变成函数行为.某些Object操作是命令式比如name in obj 和 delete obj[name],而Reflect.has(obj,name)和Reflect.deleteProperty(obj,name)让它们变成了函数行为.
//老写法
'assign' in Object //true
//新写法
Reflect.has(Objcet,'assign') //true
(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法,这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础.
var loggedObj = new Proxy(obj, {
get(target, name) {
console.log('get', target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log('delete' + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log('has' + name);
return Reflect.has(target, name);
}
});
Reflect对象一共有13个静态方法
Reflect.apply(target,thisArg,args)
Reflect.construct(target,args)
Reflect.get(target, name, receiver)
Reflect.set(target, name, value, receiver)
Reflect.defineProperty(target, name, desc)
Reflect.deleteProperty(target, name)
Reflect.has(target, name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
实例:使用Proxy实现观察者模式
观察者模式(Obsever mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行.
Promise对象
含义:Promise是异步编程的一种解决方案,比传统的解决方案–回调函数和事件–更合理更强大.ES6将其写进了语言标准,统一了用法,原生提供了Promise对象.
Promise对象有两个特点:
对象的状态不受外界影响.Promise对象代表异步操作,有三种状态:pending(进行中),fulfilled(已成功),和rejected(已失败).
一旦状态改变,就不会再变,任何时候都可以得到这个结果.
缺点:
无法取消Promise,一旦新建它就会立即执行,无法中途取消.
如果不设置回调函数,Promise内部抛出的错误,不会反应到外部.
基本用法
let promise = new Promise(function(resolve,reject){
console.log('Promise');
resolve();
});
promise.then(function(){
console.log('resolved');
});
console.log('Hi');
//Promise
//Hi
//resolved
Promise.prototype.then()
Promise实例具有then()方法,也就是说then方法是定义在原型对象Promise.prototype上的,它的作用是为Promise实例添加状态改变时的回调函数.then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数.
Promise.prototype.catch()
Promise.prototype.catch方法是.then(null,rejection)的别名,用于指定发生错误时的回调函数.
Promise.all()
Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例.
const p=Promise.all([p1,p2,p3]);
p的状态由p1,p2,p3决定,分成两种情况.
(1)只有p1,p2,p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1,p2,p3的返回值组成一个数组,传递给p的回调函数.
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数.
Promise.race()
Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例.
const p = Promise.race([p1,p2,p3]);
上面代码中,只要p1,p2,p3之中有一个实例率先改变状态,p的状态就跟着改变.那个率先改变的Promise实例的返回值,就传递给p的回调函数.
Promise.resolve()
有时需要将现有对象转为Promise对象,Promise.resolve方法就起到这个作用.
Generator函数的语法
执行Generator函数会返回一个遍历器对象,也就是说.Generator函数除了状态机,还是一个遍历器对象生成函数.返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态.
形式上,Generator函数是一个普通函数,但是有两个特征:
一是,function关键字与函数名之间有一个星号;
二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思是"产出").
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
async函数
async函数是什么?一句话,它就是Generator函数的语法糖.
(语法糖:计算机语言中添加的某种语法,使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。)
Generator函数,依次读取两个文件
const fs = require('fs');
const readFile = function(fileName){
return new Promise(function(resolve,reject){
fs.readFile(fileName,function(error,data){
if(error) return reject(error);
resolve(data);
});
});
}
const gen = function* (){
const f1 = yield readFile('/etc/aaa');
const f2 = yield readFile('/etc/bbb');
console.log(f1.toString());
console.log(f2.toString());
}
写成async函数,如下
const asyncReadFile = async function() {
const f1 = await readFile('/etc/aaa');
const f2 = await readFile('/etc/bbb');
console.log(f1.toString());
console.log(f2.toString());
}
比较发现,async函数就是将Generator函数的星号(*)替换async,将yield替换成await,仅此而已.
async函数对Generator函数的改进有四点:
(1)内置执行器.
async函数的执行,与普通函数一模一样,只要一行.
asyncReadFile();
上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果.这完全不像Generator函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果.
(2)更好的语义.
(3)更广的适应性.
co模块约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面,可以是Promise对象和原始类型的值(数值,字符串和布尔值,但这时等同于同步操作).
(4)返回值是Promise.
async函数的返回值是Promise对象,这比Generator函数的返回值是Iterator对象方便多了.你可以用then方法指定下一步操作.
进一步说,async函数完全可以看作多个异步操作,包装成的一个Promise对象,而await命令就是内部then命令的语法糖.
Class的基本语法
传统方法:构造函数
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
用ES6的class改写
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
ES6的类,完全可以看做构造函数的另一种写法.使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致.
class Bar {
doStuff() {
console.log('stuff');
}
}
var b = new Bar();
b.doStuff() // "stuff"
类的所有方法都定义在类的prototype属性上面.
Object.assign方法可以很方便地一次向类添加多个方法.
class Point {
constructor(){
//...
}
}
Object.assign(Point.prototype,{
toString(){},
toValue(){}
});
prototype对象的constructor属性,直接指向’类’的本身,这与ES5的行为是一致的.
Point.prototype.constructor === Point //true
Class的继承
Class可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多.
class Point{}
class ColorPoint extends Point{}
注意:在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错.这是因为子类实例的构建,是基于对父类实例的加工,只有super方法才能返回父类实例.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正确
}
}
下面是生成子类实例的代码:
let cp = new ColorPoint(25,8,'green');
cp instanceof ColorPoint //true
cp instanceof Point //true
父类的静态方法,也会被子类继承.
Object.getPrototypeOf()
Object.getPrototypeOf方法可以用来从子类上获取父类.
Object.getPrototypeOf(ColorPoint)===Point //true
因此可以使用这个方法判断,一个类是否继承了另一个类.
Module的语法
export命令
模块功能主要有两个命令构成:export和import.export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能.
//profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year =1958;
export {firstName,lastName,year};
通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名.
function v1() {...}
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
import命令
//main.js
import {firstName, lastName, year} from './profile';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名.
import {lastName as surname} from './profile';
注意,import命令具有提升效果,会提升到整个模块的头部,首先执行.因为import在静态解析阶段执行,所以它是一个模块之中最早执行的.
模块的整体加载
import * as circle from './circle';
console.log('圆面积:'+circle.area(4));
console.log('圆周长:'+circle.circumference(14));
export default命令
export default 命令,为模块指定默认输出.
其他模块加载该模块时,import命令可以为该匿名函数指定任意名字.
// import-default.js
import customName from './export-default';
customName(); // 'foo'
上面代码的import命令.可以用任意名称指向export-default.js输出的方法,这时就不需要知道原模块输出的函数名.需要注意的是,这时import命令后面,不使用大括号.
ES6模块与CommonJS模块的差异
CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用.
CommonJS模块是运行时加载,ES6模块是编译时输出接口.
第一个差异:
CommonJS模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值.
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
上面代码输出内部变量counter和改写这个变量的内部方法incCounter.然后,在main.js里面加载这个模块.
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
第二个差异:
是因为CommonJS加载的是一个对象(即module.export属性),该对象只有在脚本运行完才会生成.而ES6模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成.
ES6模块的运行机制与CommonJS不一样.JS引擎对脚本静态分析的时候,遇到模块加载命令import.就会生成一个只读引用,等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值.