第 7 章 ES6
前言
本章是第七章ES6相关的内容,也是最后一章。本篇为第二篇,后面还有一篇。
现在ES6使用非常广泛,新增的箭头函数、类、Promise等新特性,可以方便地处理很多复杂的操作,极大地提高了开发效率。本章会记录ES6中最常用的新特性
在学完后,希望掌握下面知识点:
- let和const关键字
- 解构赋值
- 模板字符串
- 箭头函数
- Symbol类型
- Set和Map数据结构
- Proxy
- Reflect
- Promise
- Iterator
- Generator函数
- Class及其用法
- Module及其用法
7.8 Set数据结构和Map数据结构
7.8.1 Set数据结构
ES6中新增了一种数据结构Set,表示的是一组数据的集合,类似于数组,但是Set的成员值都是唯一的,没有重复。
要注意这个是不存在类型转换的,也就是必须严格相等才算重复。但是NaN
是个例外,NaN
与NaN
在进行严格相等的比较时是不相等的,但是在Set内部,NaN
与NaN
是严格相等的,因此一个Set实例中只可以添加一个NaN
。
Set本身是一个构造函数,可以接收一个数组或者类数组对象作为参数。
(1)Set实例的属性
Set.prototype.constructor
:构造函数,默认就是Set函数Set.prototype.size
:返回实例的成员总数
(2)Set实例的函数
Set.prototype.add(value)
:添加一个值,返回Set结构本身Set.prototype.delete(value)
:删除某个值,返回布尔值。删除成功返回true;如果删除失败,返回falseSet.prototype.has(value)
:判断是否是成员,返回布尔值Set.prototype.clear()
:清除所有成员,无返回值
(3)Set的常见用法
- 单一数组的去重
由于Set成员值具有唯一性,因此可以使用Set来进行数组的去重
let arr = [1, 3, 4, 2, 3, 2, 5];
console.log(new Set(arr)); // Set {1,3,4,2,5}
- 多个数组的合并去重
Set可以用于单个数组的去重,也可以用于多个数组的合并去重。实现方法是先使用扩展运算符将多个数组处理成一个数组,然后将合并后得到的数组传递给Set构造函数
let arr1 = [1, 2, 3, 4];
let arr2 = [2, 3, 4, 5, 6];
let set1 = new Set([...arr1, ...arr2]);
console.log(set1); //Set {1, 2, 3, 4, 5, 6}
- Set与数组的转换
将数组转换为Set时,只需要通过Set的构造函数即可;将Set转换为数组时,通过Array.from()
函数或者扩展运算符即可
let arr1 = [1, 3, 5, 7];
// 将数组转换为Set
let set1 = new Set(arr1);
console.log(set1); // Set { 1, 3, 5, 7 }
let set2 = new Set();
set2.add('a');
set2.add('b');
// 将Set转换为数组,通过Array.from()函数
let arr2 = Array.from(set2);
console.log(arr2); // [ 'a', 'b' ]
// 将Set转换为数组,通过扩展运算符
let arr3 = [...set2];
console.log(arr3); // [ 'a', 'b' ]
(4)Set的遍历
forEach()
函数
针对Set数据结构,我们可以使用传统的forEach()
函数进行遍历。forEach()
函数的第一个参数表示的是Set中的每个元素,第二个参数表示的是元素的索引,从0开始。而在Set中没有索引的概念,它实际是键和值相同的集合,第二个参数表示的是键,实际与第一个参数相同,也返回数据值本身。
let set = new Set([4, 5, 'hello']);
set.forEach((item, index) => {
console.log(item, index);
});
// 4 4
// 5 5
// hello hello
keys()
函数:返回键名的遍历器values()
函数:返回键值的遍历器entries()
函数:返回键值对的遍历器
通过后 3 个函数获得的对象都是遍历器对象Iterator,然后通过for...of
循环可以获取每一项的值。
因为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"]
7.8.2 Map数据结构
ES6中另一种新增的数据结构Map,与传统的对象字面量类似,它的本质是一种键值对的组合。
与对象字面量不同的是,对象字面量的键只能是字符串,对于非字符串类型的值会采用强制类型转换成字符串,而Map的键可以由各种类型的值组成。
// 传统的对象类型
const data = {};
const element = document.getElementById('home');
data[element] = 'first';
console.log(data); // {[object HTMLDivElement]: "first"}
// Map
const map = new Map();
const element = document.getElementById('home');
map.set(element, 'first');
console.log(map); // {div#home => "first"}
Map本身是一个构造函数,可以接收一个数组作为参数,数组的每个元素同样是一个子数组,子数组元素表示的是键和值
const map = new Map([
['name', 'kingx'],
['age', 123]
]);
console.log(map); // Map { 'name' => 'kingx', 'age' => 123 }
类似于Set数据结构的元素值唯一性,在Map数据结构中,所有的键都必须具有唯一性。如果对同一个键进行多次赋值,那么后面的值会覆盖前面的值。
对于Map键的唯一性也是用严格相等进行判断。此外同样虽然NaN
与NaN
不严格相等,但是Map会将其视为一个相同的键。
如果Map实例的键是引用数据类型,则需要判断对象是否为同一个引用、是否占据同一个内存地址。
const map = new Map();
map.set([0], '0');
map.set([0], '1');
console.log(map); // Map { [ 0 ] => '0', [ 0 ] => '1' }
在上面的实例中,我们将数组[0]作为map的键,但是[0]作为引用类型数据,每次生成一个新的值都会占据新的内存地址,实际为不同的键,因此map在输出时会有两个元素值。
如果希望元素[0]只占据同一个键,则可以将其赋给一个变量值,通过变量值添加到map中
let arr = [0];
const map = new Map();
map.set(arr, '0');
map.set(arr, '1');
console.log(map); // Map { [ 0 ] => '1' }
(1)Map实例的属性
size
属性:返回Map结构的成员总数
(2)Map实例的函数
set(key, value)
:设置键名key对应的键值为value,set()函数返回的是当前Map对象,因此set()函数可以采用链式调用的写法get(key)
:读取key对应的键值,如果找不到key,返回undefinedhas(key)
:返回一个布尔值,表示某个键是否在当前Map对象中delete(key)
:删除某个键,返回一个布尔值。删除成功返回true;如果删除失败,返回falseclear()
:清除所有成员,没有返回值。
(3)Map的遍历
与Set一样,Map的遍历同样可以采用4种函数,分别是forEach()
函数、keys()
函 数、values()
函数、entries()
函数
(4)Map与其他数据结构的转换
- Map与数组的互相转换
// Map转换为数组,可以通过扩展运算符实现
const map = new Map();
map.set('name', 'kingx');
map.set('age', 12);
const arr = [...map];
console.log(arr); // [ [ 'name', 'kingx' ], [ 'age', 12 ] ]
// 数组转换为Map,可以通过Map构造函数实现,使用new操作符生成Map的实例
const arr = [[ 'name', 'kingx' ], [ 'age', 12 ]];
const map = new Map(arr);
console.log(map); // Map { 'name' => 'kingx', 'age' => 12 }
- Map与对象的互相转换
// Map转换为对象,如果Map的实例的键是字符串,则可以直接转换;如果键不是字符串,则会先转换成字符串然后再进行转换
function mapToObj(map) {
let obj = {};
for(let [key, value] of map) {
obj[key] = value;
}
return obj;
}
console.log(mapToObj(map)); // { name: 'kingx', age: 12 }
// 对象转换为Map,只需要遍历对象的属性并通过set()函数添加到Map的实例中即可。
function objToMap(obj) {
let map = new Map();
for (let k of Object.keys(obj)) {
map.set(k, obj[k]);
}
return map;
}
console.log(objToMap({yes: true, no: false}));
// Map {"yes" => true, "no" => false}
- Map与JSON的互相转换
// Map转换为JSON字符串时,有两种情况,第一种是当Map的键名都是字符串时,可以先将Map转换为对象,然后调JSON.stringify()函数
function mapToJson(strMap) {
// 先将map转换为对象,然后转换为JSON
return JSON.stringify(mapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
console.log(mapToJson(myMap)); // {"yes":true,"no":false}
// Map转换为JSON字符串时的第二种情况是当Map的键名有非字符串时,我们可以先将Map转换为数组,然后调用JSON.stringify()函数
function mapToArrayJson(map) {
// 先通过扩展运算符转换为数组,再转换为JSON
return JSON.stringify([...map]);
}
let myMap2 = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap2); // [[true,7],[{"foo":3},["abc"]]]
// JSON转换为Map。JSON字符串是由一系列键值对构成,键一般都为字符串。我们可以直接通过调用JSON.parse()函数先将JSON字符串转换为对象,然后再转换为Map
function jsonToMap(jsonStr) {
// 先转换为JSON对象,再转换为Map return
objToMap(JSON.parse(jsonStr));
}
jsonToMap('{"yes": true, "no": false}'); // Map { 'yes' => true, 'no' => false }
- Map与Set的互相转换
// Set转换为Map,Set中以数组形式存在的数据可以直接通过Map的构造函数转换为Map
function setToMap(set) {
return new Map(set);
}
const set = new Set([ ['foo', 1], ['bar', 2]
]);
console.log(setToMap(set)); // Map { 'foo' => 1, 'bar' => 2 }
// Map转换为Set,可以将遍历Map本身获取到的键和值构成一个数组,然后通过add()函数添加至set实例中
function mapToSet(map) {
let set = new Set();
for (let [k,v] of map) {
set.add([k, v])
}
return set;
}
const map14 = new Map() .set('yes', true) .set('no', false);
mapToSet(map14); // Set { [ 'yes', true ], [ 'no', false ] }
7.9 Proxy
7.9.1 Proxy概述
ES6中新增了Proxy对象,从字面上看可以理解为代理器,主要用于改变对象的默认访问行为。实际表现是在访问对象之前增加一层拦截,任何对对象的访问行为都会通过这层拦截。在拦截中,我们可以增加自定义的行为。
const proxy = new Proxy(target, handler);
Proxy实际上是一个构造函数,接收两个参数:
target
:目标对象handler
:定义拦截的行为
通过Proxy构造函数可以生成实例proxy,任何对proxy实例的属性的访问都会自动转发至target对象上,我们可以针对访问的行为配置自定义的handler对象,因此外界通过proxy访问target对象的属性时,都会执行handler对象自定义的拦截操作。
// 定义目标对象
const person = {
name: 'kingx',
age: 23
};
// 定义配置对象
let handler = {
get: function (target, prop, receiver) {
console.log("你访问了person的属性");
return target[prop];
}
};
// 生成Proxy的实例
const p = new Proxy(person, handler);
// 执行结果
console.log(p.name);
// 你访问了person的属性
// kingx
使用Proxy需要注意的点 :
- 必须通过代理实例访问
- 配置对象(也就是handler)不能为空对象
7.9.2 Proxy实例函数
Proxy实例函数共13种:
get(target, propKey, receiver)
:拦截对象属性的读取操作。target表示的是目标对象,propKey表示的是读取的属性值,receiver表示的是配置对象set(target, propKey, value, receiver)
:拦截对象属性的写操作,即设置属性值。target表示目标对象,propKey表示的是将要设置的属性,value表示将要设置的属性的值,receiver表示的是配置对象has(target, propKey)
:拦截hasProperty的操作,返回一个布尔值。target表示目标对象,propKey表示判断的属性deleteProperty(target, propKey)
:拦截delete proxy[propKey]的操作,返回一个布尔值,表示是否执行成功ownKeys(target)
:拦截Object.getOwnPropertyNames(proxy)、 Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环等操作。其中target表示的是获取对象自身所有的属性名getOwnPropertyDescriptor(target, propKey)
:拦截Object.getOwnPropertyDescriptor(proxy, propKey)操作,返回属性的属性描述符构成的对象defineProperty(target, propKey, propDesc)
:拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy,propDescs)操作,返回一个布尔值。target表示目标对象,propKey表示新增的属性,propDesc表示的是属性描述符对象preventExtensions(target)
:拦截Object.preventExtensions(proxy)操作,返回一个布尔值。表示的是让一个对象变得不可扩展,不能再增加新的属性getPrototypeOf(target)
:拦截Object.getPrototypeOf(proxy)操作,返回一个对象,表示的是拦截获取对象原型属性isExtensible(target)
:拦截Object.isExtensible(proxy),返回一个布尔值,表示对象是否是可扩展的setPrototypeOf(target, proto)
:拦截Object.setPrototypeOf(proxy, proto)操作,返回一个布尔值,表示的是拦截设置对象的原型属性的行为。proto表示新的原型对象apply(target, object, args)
:拦截Proxy实例作为函数调用的操作,例如proxy(…args)、proxy.call(object, …args)、proxy.apply(…),其中target表示目标对象,object表示函数的调用方,args表示函数调用传递的参数construct(target, args)
:拦截Proxy实例作为构造函数调用的操作。args表示函数调用传递的参数
这些函数都有一个通用的特性,即如果在target中使用了this关键字,再通过Proxy处理后,this关键字指向的是Proxy的实例,而不是目标对象target
7.9.3 Proxy多种函数的基本使用方法
- 读取不存在属性
- 读取负索引的值
- 禁止访问私有属性
- Proxy访问属性的限制
- 拦截属性赋值操作
- 隐藏内部私有属性
- 禁止删除某些属性
- 函数的拦截
7.9.4 Proxy的使用场景
(1)实现真正的私有
JavaScript中虽然没有私有属性的语法,但存在一种约定俗成的下画线写法,我们可以通过Proxy处理下画线写法来实现真正的私有。
需要实现下面几条:
- 不能访问到私有属性,如果访问到私有属性则返回 undefined
- 不能直接修改私有属性的值,即使设置了也无效
- 不能遍历出私有属性,遍历出来的属性中不会包含私有属性
const apis = {
_apiKey: '12ab34cd56ef',
getAllUsers: function () {
console.log('这是查询全部用户的函数');
},
getUserById: function (userId) {
console.log('这是根据用户id查询用户的函数');
},
saveUser: function (user) {
console.log('这是保存用户的函数');
}
};
const proxy = new Proxy(apis, {
get: function (target, prop) {
if (prop[0] === '_') {
return undefined;
}
return target[prop];
},
set: function (target, prop, value) {
if (prop[0] !== '_') {
target[prop] = value;
}
},
has: function (target, prop) {
if (prop[0] === '_') {
return false;
}
return prop in target;
}
});
console.log(proxy._apiKey); // undefined
console.log(proxy.getAllUsers()); // 这是查询全部用户的函数
proxy._apiKey = '123456789'; // 设置无效
console.log('getUserById' in proxy); // true
console.log('_apiKey' in proxy); // false
(2)增加日志记录
在日常的开发中,针对那些调用频繁、运行缓慢或者占用资源密集型的接口,我们期望能记录它们的使用情况,这个时候我们可以通过Proxy作为中间件增加日志记录。
为了达到上面的目的,我们需要使用Proxy进行拦截,首先通过get()函数拦截到调用的函数名,然后通过apply()函数进行函数的调用。
因此在实现上,get()函数会返回一个函数,在这个函数内通过apply()函数调用原始函数,然后调用记录操作日志的函数
const apis = {
_apiKey: '12ab34cd56ef',
getAllUsers: function () {
console.log('这是查询全部用户的函数');
},
getUserById: function (userId) {
console.log('这是根据用户id查询用户的函数');
},
saveUser: function (user) {
console.log('这是保存用户的函数');
}
};
// 记录日志的方法
function recordLog() {
console.log('这是记录日志的函数');
}
const proxy = new Proxy(apis, {
get: function (target, prop) {
const value = target[prop];
return function (...args) {
// 此处调用记录日志的函数
recordLog();
// 调用真实的函数
return value.apply(null, args);
}
}
});
proxy.getAllUsers();
//这是记录日志的函数
//这是查询全部用户的函数
这样就可以在不影响原应用正常运行的情况下增加日志记录。
如果我们只想要对特定的某些函数增加日志,那么可以在get()函数中进行特殊的处理,对函数名进行判断.
(3)提供友好提示或者阻止特定操作
通过Proxy,我们可以增加某些操作的友好提示或者阻止特定的操作,主要包括以下几类:
- 某些被弃用的函数被调用时,给用户提供友好提示
- 阻止删除属性的操作
- 阻止修改某些特定的属性的操作
7.10 Reflect
7.10.1 Reflect概述
总的来说,Reflect 是一个内置的对象,提供了一组有用的方法,用于操作对象和函数。它提供了一种与 Proxy 对象交互的方法,使开发人员可以使用相同的方法来处理对象和函数,同时提供更多的操作和控制选项。在使用 Reflect时候可以使代码更加简洁和易于理解,同时还提供了更多的操作和控制选项。
可以这样理解:有一个名为Reflect的全局对象,上面挂载了对象的某些特殊函数,这些函数可以通过类似于Reflect.apply()这种形式来调用,所有在Reflect对象上的函数要么可以在Object原型链中找到,要么可以通过命令式操作符实现,例如delete和in操作符。
Reflect对象的函数与Proxy对象的函数一一对应,只要是Proxy对象的函数,就能在Reflect对象上找到对应的函数。这就让Proxy对象可以方便地调用对应的Reflect对象上的函数,完成默认行为,并以此作为修改行为的基础。
也就是说,不管Proxy对象怎么修改默认行为,总可以在Reflect对象上获取默认行为。
而事实上Proxy对象也会经常随着Reflect对象一起进行调用。
7.10.2 Reflect静态函数
与Proxy对象不同的是,Reflect对象本身并不是一个构造函数,而是直接提供静态函数以供调用,Reflect对象的静态函数一共有13个:
Reflect.apply(target, thisArg, args)
:通过指定的参数列表执行target函数,等同于执行Function.prototype.apply.call(target, thisArg, args)
。target表示的是目标函数,thisArg表示的是执行target函数时的this对象,args 表示的是参数列表Reflect.construct(target, args [, newTarget])
:执行构造函数,等同于执行new target(...args)
。target表示的是构造函数,args表示的是参数列表。newTarget是选填的参数,如果增加了该参数,则表示将newTarget作为新的构造函数;如果没有增加该参数,则仍然使用第一个参数target作为构造函数Reflect.defineProperty(target, propKey, attributes)
:为对象定义属性。等同于执行Object.defineProperty()
。propKey表示的是新增的属性名, attributes表示的是属性描述符对象集Reflect.deleteProperty(target, propKey)
:删除对象的属性,等同于执行delete obj[propKey]
Reflect.get(target, propKey, receiver)
:获取对象的属性值,等同于执行target[propKey]
。target表示的是获取属性的对象,propKey表示的是获取的属性,receiver表示函数中this绑定的对象Reflect.getOwnPropertyDescriptor(target, propKey)
:得到指定属性的描述对象,等同于执行Object.getOwnPropertyDescriptor()
Reflect.getPrototypeOf(target)
:读取对象的__proto__
属性,等同于执行Object.getPrototypeOf(obj)
Reflect.has(target, propKey)
:判断属性是否在对象中Reflect.isExtensible(target)
:判断对象是否可扩展,等同于执行Object.isExtensible()
函数Reflect.ownKeys(target)
:获取对象的所有属性,包括Symbol属性,等同于Object.getOwnPropertyNames
与Object.getOwnPropertySymbols
之和Reflect.preventExtensions(target)
:让一个对象变得不可扩展,等同于执行Object.preventExtensions()
Reflect.set(target, propKey, value, receiver)
:设置某个属性值,等同于执行target[propKey] = value
。receiver为可选项,表示函数中this绑定的对象Reflect.setPrototypeOf(target, newProto)
:设置对象的原型prototype,等同于执行Object.setPrototypeOf(target, newProto)
。target表示的是目标对象,newProto表示的是新的原型对象
7.10.3 Reflect与Proxy
ES6在设计的时候就将Reflect对象和Proxy对象绑定在一起了,Reflect对象的函数与Proxy对象的函数一一对应,因此很显然会经常在Proxy对象中调用Reflect对象对应的函数。
下面的例子中使用Proxy对象拦截属性的读取、设置和删除操作,并配合Reflect对象实现:
let target = {
name: 'kingx'
};
const proxy = new Proxy(target, {
get(target, prop) {
console.log(`读取属性${prop}的值为${target[prop]}`);
return Reflect.get(target, prop);
},
set(target, prop, value) {
console.log(`设置属性${prop}的值为${value}`);
return Reflect.set(target, prop, value);
},
deleteProperty(target, prop) {
console.log('删除属性: ' + prop);
return Reflect.deleteProperty(target, prop);
}
});
proxy.name; // 读取属性name的值为'kingx'
proxy.name = 'kingx2'; // 设置属性name的值为'kingx2'
delete proxy.name; // 删除属性: name
Proxy对象和Reflect对象配合使用的一个最经典案例就是能够实现观察者模式:
// 目标对象
const target = {
name: 'kingx'
};
// 观察者队列,包含所有的观察者对象
const queueObservers = new Set();
// 第一个观察者对象
function observer1(prop, value) {
console.log(`目标对象的${prop}属性值变为${value},观察者1开心地笑了`);
}
// 第二个观察者对象
function observer2(prop, value) {
console.log(`目标对象的${prop}属性值变为${value},观察者2伤心地哭了`);
}
// Proxy的set()函数,用于拦截目标对象属性修改的操作
function set(target, prop, value) {
// 使用Reflect.set()函数修改属性
const result = Reflect.set(target, prop, value);
// 执行通知函数,通知所有的观察者
result ? queueObservers.forEach(fn => fn(prop, value)) : '';
return result;
}
// 为目标对象添加观察者
const observer = (fn) => queueObservers.add(fn);
observer(observer1);
observer(observer2);
// 通过Proxy生成目标对象的代理的函数
const observable = (target) => new Proxy(target, {set});
// 获取代理
const proxy = observable(target);
proxy.name = 'kingx2';
最后我们执行proxy.name = 'kingx2’后,会进入Proxy的set()函数中,成功地修改了name属性值,并且通知观察者执行各自的操作