数据结构 Set 和 Map 整体概述: Set是一种类似数组的数据结构,区别在于其存储的成员都是不重复的,由此带来了它的一个应用就是:去重。Set通过new关键字实例化,入参可以是数组 or 类数组的对象。
值得注意的是:在Set中,只能存储一个NaN,这说明在 Set 数据结构中,NaN等于NaN。
Set实例的方法: 操作方法 add()、delete()、has()和 clear(); 遍历方法:keys()、values()、entries()和 forEach();扩展运算符…、 数组方法 map()、filter()方法也可以用于 Set 结构。 由此它可以很方便的实现数组的交、并、差集。
WeakSet类似于 Set,主要区别在于 1.成员只能是对象类型; 2.对象都是弱引用(如果其他对象都不再引用该对象,垃圾回收机制会自动回收该对象所占的内存,不可预测何时会发生,故 WeakSet 不可被遍历)
JavaScript 对象 Object 都是键值 K-V 对的集合,但 K 取值只能是字符串和 Symbol,Map 也是 K-V 的集合,然而其 K 可以取任意类型。 如果需要键值对的集合,Map 比 Object 更适合。Map 通过 new 关键字实例化。
Map实例的方法:set()、get()、has()、delete()和 clear(); 遍历方法同 Set。
Map与其它数据结构的互相转换:Map <—> 数组| Map <—> 对象| Map <—> JSON。
WeakMap类似于Map,主要区别在于: 1.只接受对象作为键名; 2.键名所指向的对象不计入垃圾回收机制。
Set Set 是一个类似数组,但是与数组不同,它具有唯一性.里面的元素都是不重复的,本身是一个构造函数.
1 2 3 4 5 6 7 8 9 10 11 const SetArr = new Set ();[1 ,1 ,2 ,2 ,3 ,3 ].forEach (item =>SetArr .add (item)); console .log (SetArr );=> 0 : 1 1 : 2 2 : 3 size : 3 __proto__ : Set
实例属性
constructor: 构造函数,就是 Set 函数
size: 返回的是 Set 实例的长度
实例方法
add(): 往 Set 里添加值,返回 Set 本身
delete(): 删除某个值,返回布尔值判断是否成功
has(value): 判断是否有 value,返回布尔值
clear(): 清除 Set 中所有值,无返回值
遍历方法
forEach(): 使用回调函数遍历元素
entries(): 返回键值对的遍历器,用于遍历[键名,键值]组成的数组
values(): 返回键值遍历器,用于遍历所有键值
keys(): 返回键名遍历器,用于遍历所有键名
由于 Set 结构是只有键值的结构,所有 keys 方法与 values 方法返回一致。
forEach 1 2 3 let s = new Set ([1 , 2 , 3 , 4 , 5 ]);s.forEach ((item ) => console .log (item + 1 ));
entries 1 2 3 4 5 6 7 let s = new Set (["a" , "b" , "c" ]);for (let item of s.entries ()) { console .log (item); }
values 1 2 3 4 5 let s = new Set (["a" , "b" , "c" ]);for (let item of s.values ()) { console .log (item); }
keys 1 2 3 4 5 let s = new Set (["a" , "b" , "c" ]);for (let item of s.keys ()) { console .log (item); }
使用 set 实现交并差 1 2 3 4 5 6 7 8 9 let a = new Set ([1 , 2 , 3 , 4 ]);let b = new Set ([3 , 4 , 5 , 6 ]);let value = new Set ([...a].filter ((item ) => b.has (item)));let value2 = new Set ([...a], [...b]);let value3 = new Set ([...a].filter ((item ) => !b.has (item)));
WeakSet 一个弱的 Set 结构,弱的体现在WeakSet的成员只能是对象.WeakSet的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用.WeakSet没有 size 属性,无法遍历.只有实例方法:
add(): 向WeakSet添加新成员
delete(): 删除WeakSet的成员
has(): 判断WeakSet是否包含某个元素
Map Set 结构是没有键只有值,而 Map 结构是键值的组合.也就可以使用各种数据类型作为键(包括对象).
1 2 3 4 5 let map = new Map ();let obj = { name : "alan" , age : 20 };map.set (obj, "这是一些描述" ); map.get (obj); console .log (map);
实例属性
constructor: Map 构造函数
size: 返回的是 Map 元素的长度
实例方法
set(key, value): 设置 key 所对应的 value 值
get(key): 获取 key 所对应的 value 值,若无则返回 undefined
delete(): 删除某个值,返回布尔值判断是否成功
has(value): 判断是否有 value,返回布尔值
clear(): 清除 Set 中所有值,无返回值
遍历方法
forEach(): 使用回调函数遍历元素
entries(): 返回键值对的遍历器,用于遍历[键名,键值]组成的数组
values(): 返回键值遍历器,用于遍历所有键值
keys(): 返回键名遍历器,用于遍历所有键名
keys 1 2 3 4 5 6 7 8 9 let person = new Map ([ ["name" , "alan" ], ["age" , "20" ], ]); for (let item of person.keys ()) { console .log (item); }
values 1 2 3 4 5 6 7 8 9 let person = new Map ([ ["name" , "alan" ], ["age" , "20" ], ]); for (let item of person.values ()) { console .log (item); }
与其他数据结构转换
1 2 3 4 5 let person = new Map ([ ["name" , "alan" ], ["age" , "20" ], ]); console .log (...person);
1 2 3 4 5 let arr = [ ["name" , "alan" ], ["age" , "20" ], ]; let m = new Map (arr);
1 2 3 4 5 6 7 8 9 10 11 12 13 let person = new Map ([ ["name" , "alan" ], ["age" , "20" ], ]); function swap (map ) { let obj = Object .create (null ); for (let [key, value] of map) { obj[key] = value; } return obj; } swap (person);
1 2 3 4 5 6 7 8 let person = { name : "allen" , age : "20" };function swap (obj ) { for (let key of Object .keys (obj)) { map.set (key, obj[key]); } return map; } swap (person);
WeakMap WeakMap 与 Map 一样可以生成键值对的集合,但是也有不同的地方,主要是有以下的两点:
WeakMap 只接受对象作为键名(不包括 null)
ct
与 WeakSet 相似,WeakMap 也是没有遍历的操作,也没有 size 属性,没有办法列出所有键名(由于垃圾回收机制的运行),也不能清空。
实例方法
get():
set():
delete(): 删除 WeakMap 的成员
has(): 判断 WeakMap 是否包含某个元素
Array.from 将类数组对象转化为数组. 第二个参数可以对传入值进行处理.
模块化 方式有立即执行函数,AMD,CMD,CommonJS,ES6 模块化. 其中最常用的是 CommonJS 和 ES6 模块化
区别
CommonJS 即使用require(),module.exports,通常在 Node 中使用.支持动态导入.属于同步导入.
ES6 模块化即使用import,export default.通常用于浏览器,属于异步导入.
CommonJS 在导出时是值拷贝,就算导出的值变了,导入的也不会变.如果想更新,必须重新导入.ES6 模块化采用实时绑定,导出导入都指向同一个地址,所以导入变化导出也会变化.
ES6 模块会编译成require/exports来执行.
1 2 3 4 5 6 7 8 9 10 11 12 13 define (["./a" , "./b" ], function (a, b ) { a.do (); b.do (); }); define (function (require , exports , module ) { var a = require ("./a" ); a.doSomething (); });
Commonjs 1 2 3 4 5 6 7 8 9 10 module .exports = { a : 1 , }; exports .a = 1 ;var module = require ("./a.js" );module .a ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var module = require ("./a.js" );module .a ;module .exports = { a : 1 , }; var module = { id : "xxxx" , exports : {}, }; var exports = module .exports ;var load = function (module ) { var a = 1 ; module .exports = a; return module .exports ; };
另外虽然 exports 和 module.exports 用法相似,但是不能对 exports 直接赋值。因为 var exports = module.exports 这句代码表明了 exports 和 module.exports 享有相同地址,通过改变对象的属性值会对两者都起效,但是如果直接对 exports 赋值就会导致两者不再指向同一个内存地址,修改并不会对 module.exports 起效。
箭头函数
箭头函数继承而来的 this 指向永远不变.
使用call,apply,bind都无法改变箭头函数 this 指向.
箭头函数不能作为构造函数使用.因为它没有自己的 this
箭头函数没有自己的arguments对象.
在箭头函数中访问 arguments 实际上获得的是外层局部(函数)执行环境中的值. 如果想访问箭头函数中的参数,可以使用rest参数.
箭头函数没有原型prototype
箭头函数不能用作Generator函数,不能使用 yeild 关键字
类和继承 ES6 的类,完全可以看作构造函数的另一种写法。 类的数据类型就是函数,类本身就指向构造函数。
1 2 3 4 5 6 class Point { } typeof Point ; Point === Point .prototype .constructor ;
类的所有方法都定义在类的 prototype 属性上面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Point { constructor ( ) { } toString ( ) { } toValue ( ) { } } Point .prototype = { constructor ( ) {}, toString ( ) {}, toValue ( ) {}, };
constructor 关键字 constructor()方法就是构造方法,而this关键字则代表实例对象。constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有 constructor()方法,如果没有显式定义,一个空的constructor()`方法会被默认添加。
extends 关键字 作用:类继承 底层原理: 替换原型继承(不会覆盖子构造函数的原型) extends 关键字替换原型的原型:s1.__proto__.__proto__ = Person.prototype
super 关键字 用于调用父类的方法. 子类中如果想使用constructor,必须使用super关键字调用父类的方法.super这个关键字,既可以当作函数使用,也可以当作对象使用
在子类的构造函数中,只有调用 super()之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,必须先完成父类的继承,只有 super()方法才能让子类实例继承父类。 如果子类没有定义 constructor()方法,这个方法会默认添加,并且里面会调用 super()。也就是说,不管有没有显式定义,任何一个子类都有 constructor()方法。
当做函数 1 2 3 4 5 6 7 class A {}class B extends A { constructor ( ) { super (); } }
注意,super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指的是 B 的实例,因此 super()在这里相当于 A.prototype.constructor.call(this)。
当做对象 super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
1 2 3 4 5 6 7 8 9 10 11 12 13 class A { p ( ) { return 2 ; } } class B extends A { constructor ( ) { super (); console .log (super .p ()); } } let b = new B ();
上面代码中,子类 B 当中的 super.p(),就是将 super 当作一个对象使用。这时,super 在普通方法之中,指向 A.prototype,所以 super.p()就相当于 A.prototype.p()。
注意,由于 super 指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过 super 调用的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class Person { private name :string = 'Tom' ; public age : number = 10 ; protected sex : string; getName ( ) { return 'Name' } constructor (name: string ) { this .name = name } } const person = new Person ('dell' );console .log (person.name );class Teacher extends Person { getTeacherName ( ) { return 'teacher' } getName ( ){ return super .getName () } constructor (age: number ){ super ('dell' ); } } const person = new Teacher (123 );console .log (person.name ); console .log (person.age );
类中的 getter 和 setter 私有属性无法在外部调用,可以利用getter间接调用私有属性.使用setter间接设置私有属性.
1 2 3 4 5 6 7 8 9 10 11 class Person { constructor (private _name: string ) {} get name () { return this ._name ; } set name (name: string ) { this ._name = name; } } const person = new Person ("dell" );person.name = "dell" ;
单例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Demo { private static instance : Demo ; private constructor ( ) {} static getInstance ( ) { if (!instance) { this .instance = new Demo (); } return this .instance ; } } const demo1 = Demo .getInstance ();const demo2 = Demo .getInstance ();
抽象类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 abstruct class Geom { width : number ; getType ( ) { return 'Geom' } abstract getArea (): number ; } class Circle extends Geom { getArea ( ) { return '123' }
Proxy Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。 Proxy 内置一系列陷阱,用于创建一个对象的代理,从而实现基本操作的拦截和自定义.
简单来说, 通过Proxy创建对于原始对象的代理对象,从而在代理对象中使用Reflect达到对JS操作的拦截.
1 let p = new Proxy (target, handler)
target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。
1 2 3 4 5 6 7 8 9 10 11 const obj = { name : 'Tom' } const proxy = new Proxy (obj, { get (target, key, receiver ) { console .log ('劫持的对象名' , target) console .log ('劫持的属性名' , key) return target[key] } }) proxy.name
上面就是通过Proxy创建了一个代理对象,同时在Proxy内部声明了一个get陷阱.
当我们访问`proxy.name`时实际触发了对应的get陷阱.执行陷阱中的逻辑.
`target`就是劫持的源对象`obj`,
`key`就是劫持的获取的name值,注意getter函数取值时才会触发.
`receiver`表示代理的对象proxy或者继承Proxy的对象.也就是谁调用了函数,就是指的谁.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 const parent = { name : 'parent' , get value () { return this .name ; }, }; const handler = {get (target, key, receiver ) {return Reflect .get (target, key);}, }; const proxy = new Proxy (parent, handler);const obj = {name : 'obj' ,}; Object .setPrototypeOf (obj, proxy);console .log (obj.value ); const handler = {get (target, key, receiver ) {return Reflect .get (target, key, receiver);}, }; `` `` - Proxy 中接受的Receiver 形参表示代理对象本身或者继承自代理对象的对象 - Reflect 中传递的Receiver 实参表示修改执行原始操作时的this 指向.(实际上就是指向调用者). 接下来我们通过 Proxy 来实现一个数据响应式 `` `javascript let onWatch = (obj, setBind, getLogger) => { let handler = { get(target, property, receiver) { getLogger(target, property) return Reflect.get(target, property, receiver) }, set(target, property, value, receiver) { setBind(value, property) return Reflect.set(target, property, value, receiver) } } return new Proxy(obj, handler) } let obj = { a: 1 } let p = onWatch( obj, (v, property) => { console.log(` 监听到属性${property}改变为${v}`) }, (target, property) => { console.log(` '${property}' = ${target[property]}`) } ) p.a = 2 // 监听到属性a改变 p.a // 'a' = 2 ` `` ` 在上述代码中,我们通过自定义 set 和 get 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。 当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要我们在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷可能就是浏览器的兼容性不好了。 # Reflect Reflect 提供拦截 JS 操作的方法.并非一个构造函数,不能用 new 进行调用. 它的所有方法和属性都是静态的. ` `` javascriptReflect .get (target, key, receiver);
虽然也可以直接使用target[key],但是 Reflect 可以用 receiver,更强大.
案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let array = [1 , 2 , 3 ];array[-1 ]; array[-2 ]; array[-3 ]; array = new Proxy (array, { get (target, key ) { key = Number (key); if (Number (key) < 0 ) { key = key + target.length ; } return Reflect .get (target, key); }, });
深层遍历 1 2 3 4 5 6 7 8 9 const handler = { get (target, key ){ let res = Reflect .get (target.key ) retrun typeof res === 'object' ? new Proxy (res,handler) : res }, set (target, key, val ){ return Reflect .set (target, key, val) } }
Generator Generator 即生成器,它会返回一个迭代器.Generator 最大的特点就是可以控制函数的执行。可以作为异步解决方案.
1 2 3 4 5 6 7 8 9 function * foo (x ) { let y = 2 * (yield x + 1 ); let z = yield y / 3 ; return x + y + z; } let it = foo (5 );console .log (it.next ()); console .log (it.next (12 )); console .log (it.next (13 ));
Generator 函数需要手动调用next执行,在遇到yield会暂停,返回一个对象,包括执行结果和是否执行完毕.继续调用next会从之前暂停的地方开始,如果传入参数,会覆盖之前的结果.到下一个yield结束. 分析上面的代码:
第一次执行next,参数是 5,遇到yield暂停,返回值为 5+1 => 6.
第二次执行next,next有参数 12, 覆盖上一次yield处的值,就是y = 2*12,要在这一次的yield处暂停就要 24/3,得到 8
最后一次执行next,z 就被赋值为 13,结合第二次 y 是 24,第一次 x 是 5,最后返回 5+13+24=42.
Promise Promise 是异步问题同步化解决方案. Promise 本身不是异步,它是个构造函数. Promise 中的函数是同步的. Promise 有一个参数,即excutor执行器,它有两个参数,resolve和reject.excutor是同步执行,而then是异步调用的. 如果想连续then需要上一个promise中 return 一个新的promise. Promise 对象的错误具有冒泡性质,会一直向后传递,直到被捕获为止.
状态 promise 内部在resolve之前的代码处于pending状态,之后的是fulfilled. promise 在catch中如果没有抛出错误,则会返回fufilled
链式调用 状态固化后,就不再捕获错误了.比如 resolve()返回后面有报错的内容,但是 catch 就不再捕获了. 案例: 当 promise 作为参数传递到另一个 promise 中,这个第二个 promise 中的状态就会失效.依赖于第一个 promise.
1 2 3 4 5 6 7 8 const p1 = new Promise ((reslove,reject )=> { setTimeout (()=> {reject (new Error ('fail' )},3000 ) }) const p2 = new Promise ((resolve,reject )=> { setTimeout (()=> {resolve (p1)},1000 ) }) p2.then (result =>console .log (result)) .catch (err =>console .log (err))
Promise.all()如果三个内容都失败,只返回第一个失败的信息.如果只有一个出错,也只会返回一个.而且不返回正常的. Promise.race()是谁先完成返回谁,无论是成功还是失败.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 promise .then ((value ) => { return new Promise ((resolve, reject ) => { setTimeout (() => { reject ("Error" ); }, 300 ); }); }) .then ( (value ) => { console .log (value); }, (reason ) => { console .log (reason); } ) .then ( (value ) => { console .log (value); }, (reason ) => { console .log (reason); } );
1 2 3 4 5 6 7 8 9 10 .then ( (value )=> {throw new Error ('Error' ) ) .then ( (value )=> {console .log (value)}, (reason )=> {console .log (reason)} )
缺点 其实它也是存在一些缺点的,比如无法取消 Promise,错误需要通过回调函数捕获。
如何中断 promise 插入一个 pending 状态的 promise.
catch catch 在 promise 的源码层面就是一个 then,如果 catch 中 return 的有值,可以在 catch 后跟 then. 既有 then 中的失败回调,又有 catch 的情况,会走最近的那个.
1 2 3 4 5 6 7 8 9 10 11 12 13 const p1 = Promise .resolve ();p1.then (() => { console .log (1 ); throw new Error (); }) .catch (() => { console .log (2 ); }) .then (() => { console .log (3 ); });
案例 使用 promise 加载图片 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function loadIMg (src ) { return new Promise ((resolve, reject ) => { const img = document .createElement ("img" ); img.onload = function ( ) { resolve (img); }; img.onerror = function ( ) { reject (new Error ("加载失败" )); }; img.src = src; }); } const url1 = "https://i1.hdslb.com/bfs/face/f4d60f852eb1a85696447838c90a94acad31b7ae.jpg@160w_160h_1c_1s.webp" ; const url2 = "https://i0.hdslb.com/bfs/face/a8ef30d6688d0b532bd20baa160417deae8f386d.jpg@240w_240h_1c_1s.webp" ; loadImg (url1) .then ((img ) => { console .log (img); return loadImg (url2); }) .then ((img2 ) => { console .log (img2); });
语法糖 Promise.resolve() Promise.reject()
Promise.all 如果所有都正确执行,会按顺序返回.有一个失败,就失败.
Async/await 一个函数前加了async,它就会返回 Promise.它的返回值会被Promise.resolve()包裹.
await只能配套async使用
缺点 因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。 如果代码没有依赖性的话,完全可以使用 Promise.all 的方式
async/await 和 promise 的关系 await 是等待 promise 的返回,await 做不了 promise 返回状态的工作. await 对应Promise.then成功的情况. await 后如果是函数,则会把函数返回的结果用Promise.then()返回. async 中的Promise.reject()需要使用try/catch进行捕获错误. async 中的函数内部属于同步代码,在遇到 await 之前都可以先执行.
1 2 3 4 5 6 7 8 9 let a = 0 ;let b = async ( ) => { a = a + (await 10 ); console .log ("2" , a); }; b ();a++; console .log ("1" , a);
首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generator ,generator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10
上述解释中提到了 await 内部实现了 generator,其实 await 就是 generator 加上 Promise 的语法糖,且内部实现了自动执行 generator。