前端学习记录~2023.8.19~JavaScript重难点实例精讲~第7章 ES6(2)


前言

本章是第七章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是个例外NaNNaN在进行严格相等的比较时是不相等的但是在Set内部NaNNaN是严格相等的因此一个Set实例中只可以添加一个NaN

Set本身是一个构造函数可以接收一个数组或者类数组对象作为参数。

1Set实例的属性

  • Set.prototype.constructor构造函数默认就是Set函数
  • Set.prototype.size返回实例的成员总数

2Set实例的函数

  • Set.prototype.add(value)添加一个值返回Set结构本身
  • Set.prototype.delete(value)删除某个值返回布尔值。删除成功返回true如果删除失败返回false
  • Set.prototype.has(value)判断是否是成员返回布尔值
  • Set.prototype.clear()清除所有成员无返回值

3Set的常见用法

  1. 单一数组的去重

由于Set成员值具有唯一性因此可以使用Set来进行数组的去重

let arr = [1, 3, 4, 2, 3, 2, 5];
console.log(new Set(arr));	// Set {1,3,4,2,5}
  1. 多个数组的合并去重

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}
  1. 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' ]

4Set的遍历

  1. 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
  1. keys()函数返回键名的遍历器
  2. values()函数返回键值的遍历器
  3. 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键的唯一性也是用严格相等进行判断。此外同样虽然NaNNaN不严格相等但是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' }

1Map实例的属性

  • size属性返回Map结构的成员总数

2Map实例的函数

  • set(key, value)设置键名key对应的键值为valueset()函数返回的是当前Map对象因此set()函数可以采用链式调用的写法
  • get(key)读取key对应的键值如果找不到key返回undefined
  • has(key)返回一个布尔值表示某个键是否在当前Map对象中
  • delete(key)删除某个键返回一个布尔值。删除成功返回true如果删除失败返回false
  • clear()清除所有成员没有返回值。

3Map的遍历

与Set一样Map的遍历同样可以采用4种函数分别是forEach()函数、keys()函 数、values()函数、entries()函数

4Map与其他数据结构的转换

  1. 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 }
  1. 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}
  1. 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 }
  1. Map与Set的互相转换
// Set转换为MapSet中以数组形式存在的数据可以直接通过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需要注意的点

  1. 必须通过代理实例访问
  2. 配置对象也就是handler不能为空对象

7.9.2 Proxy实例函数

Proxy实例函数共13种

  1. get(target, propKey, receiver)拦截对象属性的读取操作。target表示的是目标对象propKey表示的是读取的属性值receiver表示的是配置对象
  2. set(target, propKey, value, receiver)拦截对象属性的写操作即设置属性值。target表示目标对象propKey表示的是将要设置的属性value表示将要设置的属性的值receiver表示的是配置对象
  3. has(target, propKey)拦截hasProperty的操作返回一个布尔值。target表示目标对象propKey表示判断的属性
  4. deleteProperty(target, propKey)拦截delete proxy[propKey]的操作返回一个布尔值表示是否执行成功
  5. ownKeys(target)拦截Object.getOwnPropertyNames(proxy)、 Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环等操作。其中target表示的是获取对象自身所有的属性名
  6. getOwnPropertyDescriptor(target, propKey)拦截Object.getOwnPropertyDescriptor(proxy, propKey)操作返回属性的属性描述符构成的对象
  7. defineProperty(target, propKey, propDesc)拦截Object.defineProperty(proxy, propKey, propDesc、Object.defineProperties(proxy,propDescs)操作返回一个布尔值。target表示目标对象propKey表示新增的属性propDesc表示的是属性描述符对象
  8. preventExtensions(target)拦截Object.preventExtensions(proxy)操作返回一个布尔值。表示的是让一个对象变得不可扩展不能再增加新的属性
  9. getPrototypeOf(target)拦截Object.getPrototypeOf(proxy)操作返回一个对象表示的是拦截获取对象原型属性
  10. isExtensible(target)拦截Object.isExtensible(proxy)返回一个布尔值表示对象是否是可扩展的
  11. setPrototypeOf(target, proto) 拦截Object.setPrototypeOf(proxy, proto)操作返回一个布尔值表示的是拦截设置对象的原型属性的行为。proto表示新的原型对象
  12. apply(target, object, args)拦截Proxy实例作为函数调用的操作例如proxy(…args)、proxy.call(object, …args)、proxy.apply(…)其中target表示目标对象object表示函数的调用方args表示函数调用传递的参数
  13. construct(target, args)拦截Proxy实例作为构造函数调用的操作。args表示函数调用传递的参数

这些函数都有一个通用的特性即如果在target中使用了this关键字再通过Proxy处理后this关键字指向的是Proxy的实例不是目标对象target

7.9.3 Proxy多种函数的基本使用方法

  1. 读取不存在属性
  2. 读取负索引的值
  3. 禁止访问私有属性
  4. Proxy访问属性的限制
  5. 拦截属性赋值操作
  6. 隐藏内部私有属性
  7. 禁止删除某些属性
  8. 函数的拦截

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个

  1. Reflect.apply(target, thisArg, args)通过指定的参数列表执行target函数等同于执行Function.prototype.apply.call(target, thisArg, args)。target表示的是目标函数thisArg表示的是执行target函数时的this对象args 表示的是参数列表
  2. Reflect.construct(target, args [, newTarget])执行构造函数等同于执行new target(...args)。target表示的是构造函数args表示的是参数列表。newTarget是选填的参数如果增加了该参数则表示将newTarget作为新的构造函数如果没有增加该参数则仍然使用第一个参数target作为构造函数
  3. Reflect.defineProperty(target, propKey, attributes)为对象定义属性。等同于执行Object.defineProperty()。propKey表示的是新增的属性名 attributes表示的是属性描述符对象集
  4. Reflect.deleteProperty(target, propKey)删除对象的属性等同于执行delete obj[propKey]
  5. Reflect.get(target, propKey, receiver)获取对象的属性值等同于执行target[propKey]。target表示的是获取属性的对象propKey表示的是获取的属性receiver表示函数中this绑定的对象
  6. Reflect.getOwnPropertyDescriptor(target, propKey)得到指定属性的描述对象等同于执行Object.getOwnPropertyDescriptor()
  7. Reflect.getPrototypeOf(target)读取对象的__proto__属性等同于执行Object.getPrototypeOf(obj)
  8. Reflect.has(target, propKey)判断属性是否在对象中
  9. Reflect.isExtensible(target)判断对象是否可扩展等同于执行 Object.isExtensible()函数
  10. Reflect.ownKeys(target)获取对象的所有属性包括Symbol属性等同于Object.getOwnPropertyNamesObject.getOwnPropertySymbols之和
  11. Reflect.preventExtensions(target)让一个对象变得不可扩展等同于执行Object.preventExtensions()
  12. Reflect.set(target, propKey, value, receiver)设置某个属性值等同于执行target[propKey] = value。receiver为可选项表示函数中this绑定的对象
  13. 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属性值并且通知观察者执行各自的操作

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: JavaScriptJava