前端学习记录~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本身是一个构造函数,可以接收一个数组或者类数组对象作为参数。

(1)Set实例的属性

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

(2)Set实例的函数

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

(3)Set的常见用法

  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' ]

(4)Set的遍历

  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' }

(1)Map实例的属性

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

(2)Map实例的函数

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

(3)Map的遍历

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

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

  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转换为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需要注意的点 :

  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属性值,并且通知观察者执行各自的操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山药泥拌饭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值