1.Symbol
Symbol是ES6引入的,继undefind,null,String,Number,Boolean,Object之后的第七种原始数据类型。Symbol变量通过Symbol函数生成,可以传入一个字符串用于描述(主要是方便在控制台打印和遍历识别)symbol.Symbol表示独一无二的值。
//由于 Symbol 表示独一无二的值,则使用Symbol()生成的值不存在和别的值相等
const s1 = Symbol('s1');
const s2 = Symbol('s2');
console.log('s1:',s1);
console.log('s2:',s2);
console.log('s1 === s2', s1 === s2);//false
1.Symbol作为属性存在
Symbol独一无二的特性很适合将其用作标识符,比如作为对象的属性名存在。
const sy = Symbol();
let a = {};
a[sy] = '你好';
a['name'] = '名称';
// let a = {
// // []代表用sy这个变量作为key值,否则key值为字符串
// [sy]: '你好'
// };
// let a = {};
// Object.defineProperty(a, sy, { value: '你好defineProperty'} );
console.log('a:',a);
// 通过Object.getOwnPropertySymbols方法来遍历Symbol,且只可以遍历出其中的Symbol
const arr = Object.getOwnPropertySymbols(a);
console.log('symbol arr:', arr);
tips:
Symbol只可以通过Object.getOwnPropertySymbols()方法遍历出来,且只可以遍历出其中的Symbol.
for...in,for...of,Object.keys(),Object.getOwnPropertyNames(),JSON.stringify()等方法均不可以用于获取到Symbol.其实这点已经表示了Symbol不会在日常开发中被常用
2.Symbol.for
当希望使用同一个Symbol值的时候,可以使用Symbol.for(str) 方法查找到已全局注册的Symbol值进行使用.
当Symbol.for(str)查找不到时则新建str标识的Symbol并全局注册,否则返回str标识的Symbol.
// 没有时则新建
// const s1 = Symbol.for('foo');
// // 查找到之前新建的(s1用for时则表示已在全局注册,s2通过字符串描述查找到上s1)
// const s2 = Symbol.for('foo');
// console.log('s1',s1);
// console.log('s2',s2);
// console.log('s1 === s2', s1 === s2);// true
// 新建symbol,但未在全局注册,下面s2查找不到则新建了symbol
const s1 = Symbol('你好');
const s2 = Symbol.for('你好');
console.log('s1 === s2', s1 === s2);// false
总结起来就是Symbol.for(str)可以生成Symbol,并依赖str标识进行查找.
tips:ES6提供了11个内置的Symbol值,指向语言内部使用的方法。例如Symbol.iterator。
3.Symbol日常开发中的一些用途
当Symbol表示独一无二的值时,就意味着它并不接地气。我们通常会对它的获取,使用场景,及用途感到疑问。
用于消除魔术字符串
const shapeOption = {
// 不需要关心value值
triangle: Symbol(),
rectangle: Symbol()
;
function getArea(shape, options){
let area = 0;
switch(shape){
// 直接使用字符串会和代码形成强耦合,不利于维护
case shapeOption.triangle:
area = .5 * options.width * options.height;
break;
}
return area;
}
getArea(shapeOption.triangle, { width: 100, height: 100 });
用于实现私有属性
但依然可以获取,通过Object.getOwnPropertySymbols()方法。前端的私有属性属实难以界定。
var Person = (function(){
var nameSy = Symbol('name');
function Person(name){
this[nameSy] = name
}
Person.prototype.getName = function(){
return this[nameSy];
}
return Person;
}());
2.Set和WeakSet
1.Set
ES6提供新的数据结构Set,类似于数组,成员唯一,无法重复,利用这一特性常用于数组去重。
通过new构造函数生成Set
let set = new Set();
操作
通过 new Set().size 属性获取set的长度
共有四个操作方法
- add(value) 添加元素,可链式调用
- delete(value) 删除元素,返回boolean值
- has(value) 判断元素是否存在,返回boolean值
- clear() 清空Set
let s = new Set();
console.log('s.size:', s.size);// 0
s.add(1);
s.add('a').add(1);//add可以链式调用
console.log('变更后s.size:',s.size);// 2
s.delete(1);
console.log('变更后s.size:',s.size);// 1
console.log(s.has(1));
s.clear();
console.log('s.size:',s.size);// 0
遍历
共有四个遍历方法
- keys() 遍历键名
- values() 遍历键值
entries() 遍历键值对
forEach((key,value) => {})
tips:Set 键名同时也是键值,则keys也是values,确实有些鬼畜. set去重很常见,这里不赘叙
let s = new Set().add(1).add(2);
// 键名遍历
// for(let key of s.keys()){
// console.log(key);
// }
// 键值遍历
// for(let value of s.values()){
// console.log(value);
// }
// 键值对遍历
// for(let entry of s.entries()){
// console.log(entry);//[1,1] 每次打印结果是数组,第一个元素是key,第二个元素是value
// }
s.forEach((key, value) => console.log(key, value));
2.WeakSet
WeakSet与Set,成员也是唯一不重复,它和Set有以下区别
- WeakSet成员只能是对象
- WeakSet中的对象是弱引用
- WeakSet由于弱引用,不能遍历
tips:当一个对象无任何引用时,会尽快被垃圾回收机制回收. 弱引用是垃圾回收机制不考虑WeakSet对该对象的引用。所以当一个对象是有弱引用且无其他对象引用该对象时,此时垃圾回收机制依然会尽快回收。
操作
- add(value) 添加元素
- delete(value) 删除元素
- has(value) 判断元素是否存在,返回boolean值
<div>我是一个div</div>
<script>
let div = document.querySelector('div');
// let s = new Set();
let s = new WeakSet();//弱引用,无需删除,内存自会回收
s.add(div);
document.body.removeChild(div);
// dom对象依然在内存中,因为Set中依然引用此对象
div = null;
</script>
3.Map和WeakMap
1.Map
ES6提供Map数据结构,类似于对象,是键值对的集合。键和值可以是任何类型。通过new构造函数来创建。
操作
通过 new Map().size 属性获取set的长度
共有五个操作方法
- set(key, value) 设置键值对
- get(key) 根据键获取对应值
- delete(key) 删除键值对
- has(key) 判断键值对是否存在,返回boolean值
- clear() 清空Map
// 初始化
// let m = new Map();
let m = new Map([['name', 'jack'],['age',18]]);
m.set('name','rose');
m.set('birth','1998');
m.set({},1);
m.set(undefined,1);
m.set(1,1);
m.set('1',1);
m.set(['jack','rose'],1);
console.log(m);
console.log(m.size);
console.log('name:',m.get('name'));
console.log('has age:',m.has('age'));
m.delete('name');
console.log(m);
m.clear();
console.log('清空后size:',m.size);
遍历
共有四个遍历方法
- keys() 遍历键名
- values() 遍历键值
entries() 遍历键值对
forEach((key,value) => {})
tips:和Set遍历方法一致
let m = new Map([['name', 'jack'],['age',18]]);
m.set('name','rose');
m.set('birth','1998');
m.set({},1);
m.set(undefined,1);
m.set(1,1);
m.set('1',1);
m.set(['jack','rose'],1);
// keys()
// for(let key of m.keys()){
// console.log(key);
// }
// values()
// for(let value of m.values()){
// console.log(value);
// }
//entries()
// for(let entry of m.entries()){
// console.log(entry); //为数组,其中只包含key,value 二个元素
// }
// forEach
m.forEach((key, value) => {
console.log(key, value);
});
使用Map实现私有属性
在闭包的基础上使用Map来保存私有字段,但有弊端,使用该类new出来的实例必须全部清除,才不会导致Map占用内存
var Person = (function(){
var map = new Map();
function Person(name){
var privateProperty = {
name
};
// 示例对应私有属性
map.set(this, privateProperty);
}
Person.prototype.getName = function(){
return map.get(this).name;
}
return Person;
}());
2.WeakMap
WeakMap和Map有以下区别
- WeakMap的键只能是对象
- WeakMap的键是弱引用
- WeakMap不能遍历
- WeakMap只有四个操作方法:get(),set(),has(),delete()
const map = new WeakMap();
let key = {};
let obj = { foo: 1 };
map.set(key, obj);
// obj置为空时
obj = null;
// 依然能通过key获取到
console.log(map.get(key));
WeakMap实现私有属性
使用WeakMap解决了需要清除所有实例才能去除内存占用的问题
var Person = (function(){
var map = new WeakMap();
function Person(name){
var privateProperty = {
name
};
// 示例对应私有属性
map.set(this, privateProperty);
}
Person.prototype.getName = function(){
return map.get(this).name;
}
return Person;
}());
4.Proxy
Proxy一词意为 `代理` ,它用于代理对代理对象的一些增删改操作。Proxy在目标对象之前架设一层`拦截`,外界对该Proxy对象的访问,都必须先通过这层拦截,其实就是Proxy提供了一种机制来对外界的访问进行过滤和改写。Proxy并不对原对象进行拦截操作,而是对原对象代理后所生成的Proxy示例来进行拦截操作。
而Object.defineProperty()是拦截对象属性的读取和修改操作。两者不同的地方是:Proxy拦截的是原对象,而Object.defineProperty()拦截的是原对象的属性。
//target: 被代理对象
//handler: 拦截函数(操作)的集合
let proxy = new Proxy(target, handler)
tips:handler为空时则代表不拦截任何操作
let obj = {};
// handler为空对象
// let proxy = new Proxy(obj, {});
// // proxy代理需要直接操作proxy对象
// proxy.a = 1;
// proxy.b = 2;
// console.log('obj:', obj);
let proxy = new Proxy(obj, {
// get拦截
get(target, key, receiver){
//get中直接修改返回值
return target.a + 1;
}
});
proxy.a = 1;
console.log('proxy:',proxy.a);//35
Proxy可以拦截的13种操作
- get(target, key, receiver) 拦截对象属性的读取,比如proxy.foo和proxy['foo']
set(target, key, value, receiver) 拦截对象属性的设置,比如proxy.foo = v或者proxy['foo'] = v,返回boolean值
has(target, propKey) 拦截 propKey in proxy的操作,返回boolean值
deleteProperty(target, propKey) 拦截 delete proxy[propKey]的操作,返回boolean值
ownKeys(target) 拦截Object.getOwnPropertyNames(proxy),Object.getOwnPropertySymbols(proxy),Object.keys(proxy),for...in,返回一个数组,该方法返回目标对象所有自身属性的属性名,而Object.keys()的返回结果仅包括墓表对象自身的可遍历属性.
getOwnPropertyDescriptor(target, propKey) 拦截Object.getOwnPropertyDescriptor(proxy,propKey),返回属性的描述对象
defineProperty(target, propKey, propDesc) 拦截Object.defineProperty(proxy,propKey,propDesc),Object.defineProperties(proxy,propDesc),返回boolean值
getPrototypeOf(target) 拦截Object.getPrototypeOf(proxy),返回对象
setPrototypeOf(target, proto) 拦截Object.setPrototypeOf(proxy, proto) ,返回boolean值
apply(target, object, args) 拦截Proxy实例作为函数调用的操作,比如proxy(...args),proxy.call(object,...args),proxy.apply(...).
construct(target, args) 拦截Proxy实例作为构造函数调用的操作,比如new proxy(...args).
isExtensible(target) 拦截Object.isExtensible(proxy),返回boolean值
preventExtensions(target) 拦截Object.preventExtensions(proxy),返回boolean值
Proxy给了开发者拦截语言默认行为的权限,可以不改变原有对象或函数的情况下,轻松运用在很多场景。例如:统计函数调用次数,实现响应式数据观测(Vue3.0),实现不可变数据(Immutable)等等.
let obj = {};
let func = function(){
}
let proxy = new Proxy(obj, {
get(target, key, receiver){
console.log(`get拦截 ${key}`);
},
set(target, key, value, receiver){
console.log(`set拦截 ${key}`);
},
has(target, propKey){
console.log(`has拦截`);
},
deleteProperty(target, propKey){
console.log(`deleteProperty拦截`);
},
ownKeys(target){
console.log('ownKeys拦截');
},
getOwnPropertyDescriptor(target, propKey){
console.log('getOwnPropertyDescriptor拦截');
},
defineProperty(target, propKey, propDesc){
console.log('defineProperty拦截');
},
getPrototypeOf(target){
console.log('target拦截');
},
setPrototypeOf(target, proto){
console.log('setPrototypeOf拦截');
},
apply(target, object, args){
console.log('apply拦截');
},
construct(target, args){
console.log('construct拦截');
},
isExtensible(target){
console.log('isExtensible拦截');
},
preventExtensions(target){
console.log('preventExtensions拦截');
}
});
tips:面试常问,Proxy和Object.defineProperty()有什么区别及优势?
5.Reflect
反射机制指的是程序在运行时能够获取自身的信息
ES6提供了Reflect,把原先版本中很多语言层面的API,比如Object.defineProperty(), delete in
等几种放在Reflect的静态方法上。
将Object对象的一些明显属于语言内部的方法(如Object.defineProperty()),放到Reflect对象上。现阶段,某些对象上同时在Object和Reflect对象上部署,未来的新方法则只在Reflect对象上,也就是说,从Reflect对象上可以拿到语言内部的方法。
Reflect较之前方法使用的一些好处:
- 修改某些Object方法的返回结果,让其更合理
let target = {}; let property = 'a'; Object.defineProperty(target, property, { configurable: false, value: 1 }); // 旧写法 // try { // Object.defineProperty(target, property, { // configurable: true // }); // } catch (error) { // console.log('异常:', error); // } // 新写法 Reflect.defineProperty(target, property,{ configurable: true }) ? '' : console.log('reflect失败');
- 将命令式操作转变为函数调用,避免更多保留字占用
// 老写法 'assign' in Object // 新写法 Reflect.has(Object, 'assign');
- Reflect对象的方法与Proxy对象的方法一一对应,想要调用默认行为,直接在Reflect上调用方法,省去人工写默认行为的代码
tips:关于Reflect的背景,使用场景,用途查了一些资料,但是和我上述这段描述相差无几。找不到更多的资料去描述,以下提供一些用于参考:
JS 反射机制及 Reflect 详解 - Leophen - 博客园
6.Iterator
Iterator(迭代器)是一个对象. Iterator对象需要包含一个next方法,该方法返回一个包含两个属性(value表示当前结果,done表示是否继续迭代)的对象. ES6规定:如果数据结构的`Symbol.iterator`是一个返回iterator的对象的方法,是认为该数据结构是`可遍历的`.
tips:ES6提供的11个内置Symbol值,指向语言内部使用的方法. Symbol.iterator是其中之一,对象的Symbol.iterator属性,指向该对象的默认生成遍历器的方法
let it = makeIterator();
// 返回遍历器对象
function makeIterator(){
var nextIndex = 0;
return {
// 闭包
next: function (){
return nextIndex < 5 ?
{ value: nextIndex++, done: false } : { value: undefined, done: true };
}
};
}
let obj = {
[Symbol.iterator]: makeIterator
};
// for...of循环
// for(let value of obj){
// console.log('value:',value);
// }
ES6中以下场景默认调用Iterator接口(Symbol.iterator方法)
- for...of循环
- 数组解构
- 扩展运算符
- yield*
let arr = []; arr[Symbol.iterator] = makeIterator; // yield function* f(){ // 相当于把arr展开 yield* arr } for(let value of f()){ console.log(value); }
- 其他隐式调用,如new Set([a, b]),Promise.all()等
ES6以下数据结构默认为可遍历对象,即默认部署了Symbol.iterator方法
- Array
- Map
- Set
- String
- 函数的arguments对象
function f1(){ console.log('arguments:',arguments[Symbol.iterator]); } f1()
- NodeList对象
// divs NodeList对象 let divs = document.querySelectorAll('div'); console.log(divs[Symbol.iterator]);
7.Generator
8.async await
async函数是Generator函数的语法糖
1.async
执行async函数相当于执行了一个自动运行的Generator函数,async函数的结果返回如果不是promise,会将其包装为一个promise返回
// 单独 async
// async function f(){
// // 控制台执f()会发现返回一个包裹undefined的Promise
// console.log(1);
// }
// f()返回Promise对象,后面可以接then()
// f().then(() => {
// console.log(2);
// });
async function f(){
console.log(1);
return 'done';
// 相当于返回Promise.resolve('done');
}
2.await
async函数中使用await关键字。await关键字后面一般会跟一个Promise实例,async函数执行过程中,每次遇到await关键字,会将控制权转回外部环境。
- 如果await后面是Promise实例,则会等到该Promise实例被resolve后,才会把本次await到下次await之间的代码推到MircoTask(微任务)中等候执行,且await的返回值是该Promise实例resolve的值
- 如果await后面不是Promise实例,则会立即将本次await到下次await之间的代码推到mircoTask(微任务)中等候执行,且await的返回值就是await后面表达式的值
// await Promise实例
// async function f(){
// let data = await new Promise((resolve, reject) => {
// setTimeout(() => {
// resolve('a');
// }, 2000);
// });
// console.log(data);
// }
//await 非Promise实例
async function f(){
let data = await 'a';
console.log(data);
}
f();
console.log('end');
3.async函数的错误处理
若Promise被reject或抛出异常,await之后的代码不会被执行,那么就通过try...catch对await进行异常捕获
// 未捕获异常
// async function f(){
// let data = await new Promise((resolve, reject) => {
// setTimeout(() => {
// reject('123');
// }, 2000);
// });
// // reject后,后续代码无法执行
// console.log('done');
// }
// 捕获异常
async function f(){
try {
let data = await new Promise((resolve, reject) => {
// setTimeout(() => {
// reject('123');
// }, 2000);
throw new Error('1234');
});
// reject后,后续代码无法执行
console.log('done');
} catch (error) {
console.log('异常:', error);
}
}
tips:实际上,try...catch并不能完美处理async函数的异常抛出,那么如何优雅的处理async函数的异常抛出呢?
4.async函数处理并发异步任务
因为async函数中的每一个await都是等到前面await resolve后才会执行,如果想并发执行,可以使用Promise.all.但此情况仅限于多个异步任务之间无数据关联
// 不并发的情况
// async function f(){
// let time1 = new Date();
// let data1 = await new Promise((resolve, reject) => {
// setTimeout(() => {
// resolve('123')
// }, 2000);
// });
// let data2 = await new Promise((resolve, reject) => {
// setTimeout(() => {
// resolve('1234')
// }, 3000);
// });
// // 123 1234 用时:5002 该用时处理无法接受
// console.log(data1, data2, '用时:' + (new Date() - time1));
// }
// 并发处理异步 Promise.all 但该情况限于两个异步任务无数据关联的情况下使用
async function f(){
let time1 = new Date();
let [data1, data2] = await Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('123')
}, 2000);
}),
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('1234')
}, 3000)
})
]);
// 123 1234 用时:3000
console.log(data1, data2, '用时:' + (new Date() - time1));
}
tips:当多个异步任务之间有数据关联时,如何处理?