展开语法—— spread 运算符
展开运算符 (spread)是三个点 ( ... ),可以将一个数组转为用逗号分隔的参数序列。
说的通俗易懂点,有点像化骨绵掌,把一个大元素给打散成一个个单独的小元素。
剩余运算符也是用三个点 ( ... )表示,它的样子看起来和展开操作符一样,但是它是用于解构数组和对象。
使用 spread 运算符展开数组项
ES6 引入了展开操作符,可以展开数组以及需要多个参数或元素的表达式。
下面的 ES5 代码使用了 apply() 来计算数组的最大值:
var arr = [6, 89, 3, 45]; var maximus = Math.max.apply(null, arr); //maximus 的值为 89
我们必须使用 Math.max.apply(null, arr)
,因为 Math.max(arr)
返回 NaN。
Math.max()
函数中需要传入的是一系列由逗号分隔的参数,而不是一个数组。
且这里使用apply有些不合适,因为我们一般使用apply来指定this。
展开操作符可以提升代码的可读性,使代码易于维护。
const arr = [6, 89, 3, 45]; const maximus = Math.max(...arr); //maximus 的值应该是 89。
...arr 返回一个解压的数组。 也就是说,它展开数组。 然而,展开操作符只能够在函数的参数中或者数组中使用。 下面的代码将会报错:
const spreaded = ...arr;
构建对象字面量时ES2018(ES9)
const names = ["abc", "cba", "nba"] const info = {name: "why", age: 18} //构建对象字面量时ES2018(ES9) const obj = { ...info, address: "广州市", ...names } console.log(obj)
补充:展开运算符其实进行的是一个浅拷贝
const info = { name: "why", friend: { name: "kobe" } } const obj = { ...info, name: "coderwhy" } // console.log(obj) obj.friend.name = "james" console.log(info.friend.name)
上面代码在内存中的展示:
Symbol
数据类型
ECMAScript有6种简单数据类型(内存直接存的值),分别是Undefined,Null,Boolean,Number,String,Symbol,还有一种复杂的数据类型(内存存的是引用地址)=>Object(对象)。
而我们接下来要讲的是ES6中新增的一个基本数据类型——Symbol
Symbol
在我们与别人合作的时候,我们不知道别人会在某个对象中定义了什么属性,而我们往里面添加同名的属性,很容易会造成冲突,将内部的属性覆盖掉,这是我们不想要的结果,而 Symbol 能够解决这样的问题
Symbol 数据类型是一种原始数据类型,表示独一无二的值。该类型的性质在于这个类型的值可以用来创建匿名的对象属性。
- Symbol值是通过Symbol函数来生成的,生成后可以作为属性名;
- 也就是在ES6中,对象的属性名可以使用字符串,也可以使用Symbol值
// ES6之前, 对象的属性名(key) 都是 字符串 var obj = { name: "a", // 属性名name相当于'name' } // 虽然首字母大写,但它仍是一个函数 const s1 = Symbol() const s2 = Symbol() console.log(s1 === s2) // false
Symbol值作为key的用法
- 在定义对象字面量时使用
const obj = { [s1]: "abc", [s2]: "cba" }
- 新增属性
// 新增属性 obj[s3] = "nba" //Object.defineProperty方式来新增/定义 const s4 = Symbol() Object.defineProperty(obj, s4, { enumerable: true, configurable: true, writable: true, value: "mba" })
- 获取obj 中用 Symbol 值作为 key 的属性
console.log(obj[s1], obj[s2], obj[s3], obj[s4]) // 注意: 不能通过.语法获取 // console.log(obj.s1) 注意:使用Symbol作为key的属性名,在遍历Object.keys等中是获取不到这些Symbol值,需要Object.getOwnPropertySymbols来获取所有Symbol的key var obj = {}; var a = Symbol('a'); var b = Symbol('b'); obj[a] = 'Hello'; obj[b] = 'World'; var objectSymbols = Object.getOwnPropertySymbols(obj); console.log(objectSymbols); // [Symbol(a), Symbol(b)]
扩:Symbol.for(key)/Symbol.keyFor(symbol)
如果我们希望使用同一个 Symbol 值,可以使用 Symbol.for const sa = Symbol.for("aaa") const sb = Symbol.for("aaa") console.log(sa === sb) // true Symbol.keyFor 方法返回一个已登记的 Symbol 类型值的 key const sa = Symbol.for("aaa") const key = Symbol.keyFor(sa) console.log(key) // aaa
参考资料和扩展:
冴羽的博客——ES6 系列之模拟实现 Symbol 类型:https://github.com/mqyqingfeng/Blog/issues/87
Set
基本用法:
Set本身是一个构造函数,用来生成 Set 数据结构。它类似于数组,但是成员的值都是唯一的,没有重复的值。
//例一 const s = new Set(); [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x)); for (let i of s) { console.log(i); }// 2 3 5 4 //例二 const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]); items.size // 5
通过add()方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。
添加对象时特别注意: const set = new Set() //下面代码添加的是不同的对象 set.add({}) set.add({}) console.log(set) // Set(2) { {}, {} } //下面代码添加的是同一个对象 const set1 = new Set() const obj = {} set1.add(obj) set1.add(obj) console.log(set1) // Set(1) { {} }
操作方法:
- Set.prototype.add(value):添加某个值,返回 Set 结构本身。
- Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
- Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
- Set.prototype.clear():清除所有成员,没有返回值
实例:
const s = new Set(); s.add(1).add(2).add(2); // 注意2被加入了两次 s.size // 获取s的长度:2 s.has(1) // true s.has(2) // true s.has(3) // false s.delete(2); s.has(2) // false Array.from方法可以将 Set 结构转为数组 function dedupe(array) { return Array.from(new Set(array)); } dedupe([1, 1, 2, 3]) // [1, 2, 3]
遍历方法:
- Set.prototype.keys():返回键名的遍历器
- Set.prototype.values():返回键值的遍历器
- Set.prototype.entries():返回键值对的遍历器
- Set.prototype.forEach():使用回调函数遍历每个成员
(1)keys(),values(),entries()
由于 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"]
上面代码中,entries方法返回的遍历器,同时包括键名和键值,所以每次输出一个数组,它的两个成员完全相等。
因为value是默认的,所以使用的时候可以将其省略:for (let item of set) {}
(2)forEach()
Set 结构的实例与数组一样,也拥有forEach方法,用于对每个成员执行某种操作,没有返回值。
let set = new Set([1, 4, 9]); set.forEach((value, key) => console.log(key + ' : ' + value)) // 1 : 1 // 4 : 4 // 9 : 9 //value : 键值 key :键名
(3)遍历的应用
扩展运算符(...)内部使用for...of循环,所以也可以用于 Set 结构
let set = new Set(['red', 'green', 'blue']); let arr = [...set]; // ['red', 'green', 'blue']
用途:
1.去除数组中重复的成员
2.去除字符串里面的重复字符
WeakSet
含义:
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
- WeakSet 的成员只能是对象类型,而不能是其他类型的值。
- WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
因此 ES6 规定 WeakSet 不可遍历。
语法:
WeakSet 是一个构造函数,可以使用new命令,创建 WeakSet 数据结构。
const ws = new WeakSet();
WeakSet 可以接受一个数组或类似数组的对象作为参数。
注意,是a数组的成员成为 WeakSet 的成员,而不是a数组本身。这意味着,数组的成员只能是对象
const a = [[1, 2], [3, 4]]; const ws = new WeakSet(a); // WeakSet {[1, 2], [3, 4]} const b = [3, 4]; const ws = new WeakSet(b); // Uncaught TypeError: Invalid value used in weak set(…)
上面代码中,数组b的成员不是对象,加入 WeakSet 就会报错
操作方法:
- WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
- WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
- WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
var ws = new WeakSet(); var obj = {}; var foo = {}; ws.add(window); ws.add(obj); ws.has(window); // true ws.has(foo); // false, foo 没有添加成功 ws.delete(window); // 从结合中删除 window 对象 ws.has(window); // false, window 对象已经被删除
应用场景:
判断是否为构造函数从而进行不同的操作
const personSet = new WeakSet() class Person { constructor() { personSet.add(this) } running() { if (!personSet.has(this)) { throw new Error("不能通过非构造方法创建出来的对象调用running方法") } console.log("running~", this) } } let p = new Person() p.running() // running~ Person {} p.running.call({name: "why"}) // Error: 不能通过非构造方法创建出来的对象调用running方法
Map
用来存储映射关系
ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,在之前,Object 结构只提供了“字符串—值”的对应,而ES6之后,Map 结构提供了“值—值”的对应
作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
const map = new Map([ ['name', '张三'], ['title', 'Author'] ]); map.size // 2 map.has('name') // true map.get('name') // "张三" map.has('title') // true map.get('title') // "Author" //上面代码在新建 Map 实例时,就指定了两个键name和title。 如果对同一个键多次赋值,后面的值将覆盖前面的值。 const map = new Map(); map .set(1, 'aaa') .set(1, 'bbb'); map.get(1) // "bbb"
实例的属性和操作方法
(1)size 属性
size属性返回 Map 结构的成员总数。
const map = new Map(); map.set('foo', true); map.set('bar', false); map.size // 2
(2)Map.prototype.set(key, value)
set方法设置键名key对应的键值为value,然后返回整个 Map 结构
const m = new Map(); m.set('edition', 6) // 键是字符串 m.set(262, 'standard') // 键是数值 m.set(undefined, 'nah') // 键是 undefined set方法返回的是当前的Map对象,因此可以采用链式写法 let map = new Map() .set(1, 'a') .set(2, 'b') .set(3, 'c');
(3)Map.prototype.get(key)
get方法读取key对应的键值,如果找不到key,返回undefined
(4)Map.prototype.has(key)
has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
(5)Map.prototype.delete(key)
delete方法删除某个键,返回true。如果删除失败,返回false
(6)Map.prototype.clear()
clear方法清除所有成员,没有返回值。
遍历方法
Map 结构原生提供三个遍历器生成函数和一个遍历方法。
- Map.prototype.keys():返回键名的遍历器。
- Map.prototype.values():返回键值的遍历器。
- Map.prototype.entries():返回所有成员的遍历器。
- Map.prototype.forEach():遍历 Map 的所有成员
const map = new Map([ ['F', 'no'], ['T', 'yes'], ]); for (let key of map.keys()) { console.log(key); } // "F" // "T" for (let value of map.values()) { console.log(value); } // "no" // "yes" for (let item of map.entries()) { console.log(item[0], item[1]); } // "F" "no" // "T" "yes" // 或者 for (let [key, value] of map.entries()) { console.log(key, value); } // "F" "no" // "T" "yes"
WeakMap
含义
WeakMap结构与Map结构类似,也是用于生成键值对的集合
WeakMap与Map的区别有两点:
- 首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
- 其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。
WeakMap ,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
const wm = new WeakMap(); const element = document.getElementById('example'); wm.set(element, 'some information'); wm.get(element) // "some information"
总之,WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。
注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。
const wm = new WeakMap(); let key = {}; let obj = {foo: 1}; wm.set(key, obj); obj = null; wm.get(key) // Object {foo: 1}
上面代码中,键值obj是正常引用。所以,即使在 WeakMap 外部消除了obj的引用,WeakMap 内部的引用依然存在
WeakMap 的语法
WeakMap只有四个方法可用:get()、set()、has()、delete()。
应用场景:(vue3响应式原理)
原始代码:
const obj1 = { name: "why", age: 18 } function obj1NameFn1() { console.log("obj1NameFn1被执行") } function obj1NameFn2() { console.log("obj1NameFn2被执行") } function obj1AgeFn1() { console.log("obj1AgeFn1") } function obj1AgeFn2() { console.log("obj1AgeFn2") } const obj2 = { name: "kobe", height: 1.88, address: "广州市" } function obj2NameFn1() { console.log("obj1NameFn1被执行") } function obj2NameFn2() { console.log("obj1NameFn2被执行") }
要求:
在value改变的时候相关的函数会被调用
完整代码:
// 应用场景(vue3响应式原理) const obj1 = { name: "why", age: 18 } function obj1NameFn1() { console.log("obj1NameFn1被执行") } function obj1NameFn2() { console.log("obj1NameFn2被执行") } function obj1AgeFn1() { console.log("obj1AgeFn1") } function obj1AgeFn2() { console.log("obj1AgeFn2") } const obj2 = { name: "kobe", height: 1.88, address: "广州市" } function obj2NameFn1() { console.log("obj1NameFn1被执行") } function obj2NameFn2() { console.log("obj1NameFn2被执行") } // 1.创建WeakMap const weakMap = new WeakMap() // 2.收集依赖结构 // 2.1.对obj1收集的数据结构 const obj1Map = new Map() obj1Map.set("name", [obj1NameFn1, obj1NameFn2]) // 将name与obj1NameFn1, obj1NameFn2相映射 obj1Map.set("age", [obj1AgeFn1, obj1AgeFn2]) // 将age与obj1AgeFn1, obj1AgeFn2相映射 weakMap.set(obj1, obj1Map) // 2.2.对obj2收集的数据结构 const obj2Map = new Map() obj2Map.set("name", [obj2NameFn1, obj2NameFn2]) // 将name与obj2NameFn1, obj2NameFn2相映射 weakMap.set(obj2, obj2Map) // 3.如果obj1.name发生了改变 // Proxy/Object.defineProperty obj1.name = "james" const targetMap = weakMap.get(obj1) const fns = targetMap.get("name") fns.forEach(item => item()) // 将改变之后生成的键值对遍历出来
上面代码补充说明:
为什么使用WeakMap?
因为当有一天我们不再需要obj1对象的时候,obj1里面对应的value也会自动销毁掉,而Map不能将其销毁
为什么使用Map?
因为要映射的键名是字符串,而WeakMap只接受对象作为键名(null除外)
ES7新增知识点解析
array-includes方法
判断数组是否包含相关元素
// 在ES7之前 const names = ["abc", "cba", "nba", "mba", NaN] if (names.indexOf("cba") !== -1) { console.log("包含cba元素") } // ES7 ES2016 if (names.includes("cba", 2)) { // 从第二个判断是否包含 console.log("包含cba元素") }
includes与indexOf的区别
区别:对于NaN的判断
if (names.indexOf(NaN) !== -1) { console.log("包含NaN") // 没有打印 } if (names.includes(NaN)) { console.log("包含NaN") // 包含NaN }
指数的运算方法
const result1 = Math.pow(3, 3) // ES7: ** const result2 = 3 ** 3 // 3的三次方 console.log(result1, result2) // 27 27
ES8新增知识点解析
Object.value
在ES8之后提供了Object.value来获取所有的value
const obj = { name: "why", age: 18 } // 在ES8之前只提供了Object.keys来获取一个对象所有的key console.log(Object.keys(obj)) // [ 'name', 'age' ] // 在ES8之后提供了Object.value来获取所有的value console.log(Object.values(obj)) // [ 'why', 18 ] 扩展: // 用的非常少 console.log(Object.values(["abc", "cba", "nba"])) // [ 'abc', 'cba', 'nba' ] console.log(Object.values("abc")) // 获取到字符串的所有字符 [ 'a', 'b', 'c' ]
Object.entries
获取到对应的键值对(key:value) const obj = { name: "why", age: 18 } //获取到对应的键值对(key:value) console.log(Object.entries(obj)) // [ [ 'name', 'why' ], [ 'age', 18 ] ] // 获取对象的键值对并对其进行遍历 const objEntries = Object.entries(obj) objEntries.forEach(item => { console.log(item[0], item[1]) // name why // age 18 }) // 传入数组 console.log(Object.entries(["abc", "cba", "nba"])) //[ [ '0', 'abc' ], [ '1', 'cba' ], [ '2', 'nba' ] ] console.log(Object.entries("abc")) // [ [ '0', 'a' ], [ '1', 'b' ], [ '2', 'c' ] ]
padStart和padEnd
字符串填充
padStart 在字符串前面进行填充并生成新的字符串
padEnd 在字符串后面进行填充并生成新的字符串
const message = "Hello World" const newMessage = message.padStart(15, "*").padEnd(20, "-") console.log(newMessage) 小案例:银行卡号隐藏数字并用*号填充 const cardNumber = "321324234242342342341312" const lastFourCard = cardNumber.slice(-4) // 截取后四位 const finalCard = lastFourCard.padStart(cardNumber.length, "*") console.log(finalCard) // ********************1312
Trailing-Commas使用
在ES8之前n后面不可以再多写一个逗号的,否则会报错,同样在调用的时候后面不可以再多写一个逗号的
在ES8之后就可以添加逗号了
// 为什么会有人在后面加逗号: 方便扩展 function foo(m, n,) { } foo(20, 30,)
ES10新增知识点解析
flat
flat的使用 :降维
const nums = [10, 20, [2, 9], [[30, 40], [10, 45]], 78, [55, 88]] // nums.flat() 括号里面填降维深度,不填默认为1 const newNums = nums.flat() console.log(newNums) // [ 10, 20, 2, 9, [ 30, 40 ], [ 10, 45 ], 78, 55, 88 ] const newNums2 = nums.flat(2) console.log(newNums2) // [10, 20, 2, 9, 30,40, 10, 45, 78, 55,88]
flatMap
flatMap的使用: 先映射,再降维生成一个新数组
const messages = ["Hello World", "hello lyh", "my name is coderwhy"] const words = messages.flatMap(item => { return item.split(" ") }) console.log(words)
如果是单纯的map是没有降维的效果的:
const messages = ["Hello World", "hello lyh", "my name is coderwhy"] const words = messages.map(item => { return item.split(" ") }) console.log(words)
Object.fromEntries
将[ [ 'name', 'why' ], [ 'age', 18 ], [ 'height', 1.88 ] ]
转成 { name: 'why', age: 18, height: 1.88 }
就可以用 Object.fromEntries
Object.fromEntries的应用场景: 发送网络请求的时候URL后面会有一串字符串,对其进行解析
就像下面的东西:
const queryString = 'name=why&age=18&height=1.88' // URLSearchParams是一个api const queryParams = new URLSearchParams(queryString) const paramObj = Object.fromEntries(queryParams) console.log(paramObj) // { name: 'why', age: '18', height: '1.88' }
trim
trim:去除首尾空格 trimStart: 去除首部空格 trimEnd: 去除尾部空格 const message = " Hello World " console.log(message.trim()) console.log(message.trimStart()) console.log(message.trimEnd())
ES11新增知识点解析
BigInt
在早期的JavaScript不能准确表达过大的数字
// ES11之前 max_safe_integer const maxInt = Number.MAX_SAFE_INTEGER console.log(maxInt) // 9007199254740991 console.log(maxInt + 1) // 9007199254740992 console.log(maxInt + 2) //9007199254740992 ES11之后: 用 BigInt 来表示大整数 注意:bigInt 和 number 是不同类型,所以大整数和整数相加的时候要转换 // 转换的方式一:在整数后面加 n const bigInt = 900719925474099100n console.log(bigInt + 10n) // 转换的方式二: BigInt( ) const num = 100 console.log(bigInt + BigInt(num))
Nullish-Coalescing-operator
空值合并运算 ?? const foo = undefined //以前:const bar = foo || "default value" //以前的有弊端:如果是空的字符串或0打印的还是default value // 现在:只有undefined或null才打印default value const bar = foo ?? "defualt value" console.log(bar)
OptionalChaining
可选链:让我们的代码在进行null和undefined判断时更加清晰和简洁
应用场景:解决报错后面代码不能运行的问题
const info = { name: "why" } console.log(info.friend.girlFriend.name) // 报错后面代码不能运行 console.log('其他的代码逻辑') 可选链之前(太麻烦): if (info && info.friend && info.friend.girlFriend) { console.log(info.friend.girlFriend.name) } 有了可选链之后(更简洁): console.log(info.friend?.girlFriend?.name)
Global This
在浏览器和Node中获取全局对象的代码是不一样的,而ES11之后可以通过Global This来获取这两个的全局对象(在不同环境下运行指向的全局对象是不一样的)
// 获取某一个环境下的全局对象(Global Object) // 在浏览器下 console.log(window) console.log(this) // 在node下 console.log(global) // ES11 console.log(globalThis) for...in for...in:遍历,ES11使其标准统一 // for...in 标准化: ECMA const obj = { name: "why", age: 18 } for (const item in obj) { console.log(item) }
ES12新增知识点解析
finalizationRegistry类
可以监听对象的销毁
const finalRegistry = new FinalizationRegistry((value) => { console.log("注册在finalRegistry的对象, 某一个被销毁", value) }) let obj = { name: "why" } let info = { age: 18 } finalRegistry.register(obj, "obj") finalRegistry.register(info, "value") obj = null info = null
注意:GC垃圾回收不是实时的,所以浏览器打印的销毁信息要过一会才出现
WeakRef
如果原对象没有销毁, 那么可以获取到原对象
如果原对象已经销毁, 那么获取到的是undefined
// ES12: WeakRef类 // WeakRef.prototype.deref: const finalRegistry = new FinalizationRegistry((value) => { console.log("注册在finalRegistry的对象, 某一个被销毁", value) }) let obj = { name: "why" } let info = new WeakRef(obj) // 虽然info指向obj,但因为WeakRef还是一个弱引用,所以obj被销毁时,obj的内存还是会被回收 finalRegistry.register(obj, "obj") obj = null setTimeout(() => { console.log(info.deref()?.name) // 可选链 console.log(info.deref() && info.deref().name) }, 10000)
logical-assign-operator
逻辑赋值运算( ||= &&= ??= )
||= 逻辑或赋值运算
let message = "hello world" message = message || "default value" message ||= "default value" console.log(message)
&&= 逻辑与赋值运算(少用)
// &&= 的使用 let info = { name: "why" } // 1.判断info // 2.有值的情况下, 取出info.name // info = info && info.name 这种用法更常见 info &&= info.name console.log(info) ??= 逻辑空赋值运算 let message = 0 message ??= "default value" console.log(message)
Proxy-Reflect
监听对象的操作
场景:监听一个对象中的属性被设置或获取的过程
方式一:可以利用Object.defineProperty实现
const obj = { name: "唔西迪西", age: 18 } Object.keys(obj).forEach(key => { let value = obj[key] Object.defineProperty(obj, key, { get: function() { console.log(`监听到obj对象的${key}属性被访问了`) return value }, set: function(newValue) { console.log(`监听到obj对象的${key}属性被设置值`) value = newValue } }) }) obj.name = "玛卡巴卡" obj.age = 30 console.log(obj.name) console.log(obj.age)
缺点:
- Object.defineProperty 就不是用来监听对象的属性的,但利用它的特性可以实现
- Object.defineProperty 不能监听新增属性、删除属性的操作等其它操作
- 无法监听数组变化,Vue 通过 Hack 改写八种数组方法实现
Proxy
ES6新增的一个类 ,Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
所以实现监听时监听的不是原来的对象,而是监听原来对象的代理对象
使用:
// target:要代理的对象; handler:里面包含捕获器方法
const proxy = new Proxy(target, handler);
MDN对Proxy的传入参数解释:
handler
:包含捕捉器(Trap)的占位符对象,可译为处理器对象traps
:提供属性访问的方法,这类似于操作系统中捕获器的概念target
:被 Proxy 处理虚拟化的对象,它常被作为代理的存储后端,根据目标验证关于对象不可扩展性或不可配置属性的不变量(保持不变的语义)
现在我们通过Proxy中捕获器的重写来实现监听一个对象中的属性被设置或获取的过程:
const obj = { name: "唔西迪西", age: 18 } const proxy = new Proxy(obj, { // 获取值时的捕获器 get: function(target, key) { console.log(`监听到对象的${key}属性被访问了`, target) return target[key] }, // 设置值时的捕获器 set: function(target, key, newValue,receiver) { console.log(`监听到对象的${key}属性被设置值`, target) target[key] = newValue } }) }); proxy.name = "玛卡巴卡" proxy.age = 30 console.log(proxy.name) console.log(proxy.age)
Proxy捕获器
上面已经使用了get和set捕获器,接下来介绍一些其他常见的捕获器
- in 操作符的捕捉器
handler.has()
方法是针对in操作符的代理方法
const obj = { name: "唔西迪西", age: 18 } const objProxy = new Proxy(obj, { // 监听in的捕获器 // has 有target, key,没有 receiver has: function(target, key) { console.log(`监听到对象的${key}属性in操作`, target) return key in target } }) // in操作符 console.log("name" in objProxy)
- delete 操作符的捕捉器。
handler.deleteProperty()
方法用于拦截对对象属性的 delete 操作。
const obj = { name: "唔西迪西", age: 18 } const objProxy = new Proxy(obj, { // 监听delete的捕获器 deleteProperty: function(target, key) { console.log(`监听到对象的${key}属性in操作`, target) delete target[key] } }) // delete操作 delete objProxy.name
Reflect
ES6新增的一个API,它是一个内置的对象,它提供拦截 JavaScript 操作的方法。但它不是一个函数对象,因此它是不可构造的。
一般我们见到Reflect是跟Proxy一起使用的
Reflect对象是一个内置对象,提供了与 JavaScript 对象交互的方法,与原来我们学过的Object方法类似,但还是有一些差异,可以看看MDN对它们差异的总结:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect/Comparing_Reflect_and_Object_methods
现在我们通过Proxy和Reflect来实现开头的场景:
const obj = { name: "唔西迪西", age: 18 } const proxy = new Proxy(obj, { get: function(target, key) { console.log(`监听到对象的${key}属性被访问了`, target) return Reflect.get(target, key) }, set: function(target, key, newValue) { console.log(`监听到对象的${key}属性被设置值`, target) Reflect.set(target, key, newValue) // 返回的是Boolean } }) }); proxy.name = "玛卡巴卡" console.log(proxy.name) 补充:只有get和set的参数有receiver,receiver是指创建出来的代理对象 get: function(target, key,receiver) {} set: function(target, key, newValue,receiver) {}
Promise使用详解
异步请求的处理方式
案例要求:模拟网络请求,通过请求结果来调用成功的回调函数/调用失败的回调函数
在没有使用promise之前,需要callback来调用(自己封装好并定义好名称,要使用的时候才能调用)
function requestData(url, successCallback, failureCallback) { // 模拟网络请求 setTimeout(() => { // 拿到请求的结果 // url传入的是aaa, 请求成功 if (url === "aaa") { // 成功 let names = ["abc", "cba", "nba"] successCallback(names) } else { // 否则请求失败 // 失败 let error = "请求失败, url错误" failureCallback(error) } }, 3000); } // 成功的回调函数 function successCallback(result) { console.log("请求成功" + result); } // 失败的回调函数 function failureCallback(error) { console.log(error); } requestData("aac", successCallback, failureCallback)
弊端:因为是自定义的回调函数的名称和方法,所以在开发使用的时候要查看源码知道名称和方法后才能调用函数,增加了工作量
更好的方案:统一名称 promise,并规范好所有的代码编写逻辑
Promise
Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。
简单使用:
function foo() { // 传入的这个函数, 被称之为 executor // > resolve: 回调函数, 在成功时, 回调resolve函数 // >reject: 回调函数, 在失败时, 回调reject函数 return new Promise((resolve, reject) => { resolve("success message") // reject("failture message") }) } const fooPromise = foo() // 调用resolve(请求成功)会来到then方法 fooPromise.then((res) => { console.log(res) } // 调用reject(请求失败)会来到catch方法 fooPromise.catch((err) => { console.log(err) }) 用promise重构原来的代码去解决案例(异步请求处理): function requestData(url,) { return new Promise((resolve, reject) => { setTimeout(() => { if (url === "aaa") { // 成功 let names = ["abc", "cba", "nba"] resolve(names) } else { // 失败 let errMessage = "请求失败, url错误" reject(errMessage) } }, 3000); }) } // main.js const promise = requestData("aaa") promise.then((res) => { console.log("请求成功:", res) }) promise.catch((err) => { console.log("请求失败:", err) }) 在node中then和catch分开会报错 promise中的then和catch可以合并,给then传入两个回调函数: 第一个回调函数, 会在Promise执行resolve函数时, 被回调 第二个回调函数, 会在Promise执行reject函数时, 被回调 const promise = requestData("aaa") promise.then((res) => { console.log("请求成功:", res) }, (err) => { console.log("请求失败:", err) })
promise的三种状态
一个Promise必然处于以下几种状态之一:
- 待定(pending):初始状态,既没有被兑现,也没有被拒绝。(resolve函数和reject函数都没有被调用)
- 已兑现(fulfilled):意味着操作成功完成。(已调用resolve函数)
- 已拒绝(rejected):意味着操作失败。(已调用reject函数)
当resolve函数已经被调用时,promise的状态就被确定了,这时再去调用reject函数是没有效果的,反之亦然。
Promise 的链式调用
连续执行两个或者多个异步操作(一个promise包含着多个promise)
在上一个操作执行成功之后,开始下一个的操作,并带着上一步操作所返回的结果。我们可以通过创造一个 Promise 链来实现连续执行两个或者多个异步操作的需求。
一个promise:
new Promise((resolve, reject) => { resolve('res message') }).then(res => { console.log("res:", res) }, err => { console.log("err:", err) }) 两个promise(当前promise的状态由传入promise决定): const promise = new Promise((resolve, reject) => { // resolve("aaaaaa") reject("err message") }) const newPromise = new Promise((resolve, reject) => { resolve(Promise) }).then(res => { console.log("res:", res) }, err => { console.log("err:", err) })
Promise对象方法
// 可以通过下面代码查看Promise有哪些对象方法 console.log(Object.getOwnPropertyDescriptors(Promise.prototype))
Promise.prototype.then()
简单使用:
const promise = new Promise((resolve, reject) => { resolve() }) promise.then(res => { console.log('res:',res) }) // 当上面调用resolve时会调用下面的回调
同一个Promise可以被多次调用then方法,当我们的resolve方法被回调时, 所有的then方法传入的回调函数都会被调用
promise.then(res => { console.log("res1:", res) }) promise.then(res => { console.log("res2:", res) }) promise.then(res => { console.log("res3:", res) })
then方法传入的 "回调函数: 可以有返回值,返回值是新的Promise
- 如果我们返回的是一个普通值(数值/字符串/普通对象/undefined), 那么这个普通的值被作为一个新的Promise的resolve值
// 下面是一个链式调用 promise.then(res => { return "aaaaaa" // 没有返回值默认返回undefined }).then(res => { console.log("res:", res) return "bbbbbb" })
上面代码中第二个then方法接收的值是第一个then方法返回的,且上面调用resolve是与第二个then方法无关的(注意:这里两个then方法所捕获的Promise不是同一个,第二个then方法捕获的是第一个then方法返回产生的一个的新Promise)
- 如果我们返回的是一个Promise,则后一个的then方法所捕获的Promise取决于前一个返回的Promise
promise.then(res => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(111111) }, 3000) }) }).then(res => { console.log("res:", res) })
Promise.prototype.catch()
通过catch方法来传入错误(拒绝)捕获的回调函数
简单使用:
const promise = new Promise((resolve, reject) => { reject() }) promise.catch(err => { console.log('err:',err) })
注意:catch捕获顺序
- 当上面调用的是resolve时:
- 如果我们的catch方法是写在then方法之后的,当then方法返回的是一个普通值,那then方法和catch方法捕获的是同一个Promise。
- 但如果then方法返回的是一个新的Promise且调用了reject/throw Error,则这个then方法之后的catch方法优先捕获的是新的Promise
const promise = new Promise((resolve, reject) => { resolve('111') }) promise.then(res => { return new Promise((resolve, reject) => { reject("then rejected status") }) }).catch(err => { console.log("err:", err) // err: then rejected status })
- 当上面调用的是reject时:
- catch方法捕获的是与then方法同一个Promise,无论then方法返回的是什么值
const promise = new Promise((resolve, reject) => { reject('111') }) promise.then(res => { return new Promise((resolve, reject) => { reject("then rejected status") }) }).catch(err => { console.log("err:", err) // err: 111 })
catch返回值:返回值也是新的Promise
- 如果我们返回的是一个普通值(数值/字符串/普通对象/undefined), 那么这个普通的值被作为一个新的Promise的resolve值(与then方法一样)
- 如果我们希望后续继续执行catch,那么需要抛出一个异常
Catch 的后续链式操作
在回调的时候抛出错误之后想要再次进行新的操作则可以使用catch来实现
new Promise((resolve, reject) => { console.log('开始回调-------'); resolve(); }) .then(() => { throw new Error('error message'); console.log('aaa'); }) .catch(() => { console.log('bbb'); }) .then(() => { console.log('ccc'); });
在还没有开始执行之前,VSCode就已经检测出第一个then里面抛出错误之后的代码不会执行:
执行输出:
总结:当出现失败的情况时,使用catch()中断(在catch回调和抛出错误的then回调之间的then回调也是不会执行的,catch回调之后的then回调可以执行)
Promise.prototype.finally()
finally() 方法返回一个 Promise。在 promise 结束时,无论结果是 fulfilled 或者是 rejected,都会执行指定的回调函数。
这为在 Promise 是否成功完成后都需要执行的代码提供了一种方式。这避免了同样的语句需要在 then() 和 catch() 中各写一次的情况
const promise = new Promise((resolve, reject) => { // resolve("resolve message") reject("reject message") }) promise.then(res => { console.log("res:", res) }).catch(err => { console.log("err:", err) }).finally(() => { console.log("finally code execute") })
Promise类方法
直接通过类名调用的方法
Promise.resolve
将普通对象转成Promise对象并调用resolve
- 传入普通的值
const promise = Promise.resolve({ name: "why" }) // 相当于 const promise2 = new Promise((resolve, reject) => { resolve({ name: "why" }) })
- 传入Promise
const promise = Promise.resolve(new Promise((resolve, reject) => { resolve("11111") }))
Promise.reject
将普通对象转成Promise对象并调用reject
const promise = Promise.reject("rejected message") // 相当于 const promise2 = new Promsie((resolve, reject) => { reject("rejected message") })
Promise.all
所有的Promise都变成fulfilled状态时(所有Promise都调用resolve之后), 再拿到结果
// 创建多个Promise const p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve(11111) }, 1000); }) const p2 = new Promise((resolve, reject) => { setTimeout(() => { reject(22222) }, 2000); }) const p3 = new Promise((resolve, reject) => { setTimeout(() => { resolve(33333) }, 3000); }) Promise.all([p2, p1, p3, "aaaa"]).then(res => { console.log(res) // [22222,11111,33333,'aaaa'] })
但是在拿到所有结果之前, 有一个promise变成了rejected状态, 那么整个promise是rejected状态(Promise.all方法会被中断)
Promise.allSettled
所有的Promise都有结果(无论是fulfilled状态还是rejected状态)之后, 再拿到结果
// 创建多个Promise const p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve(11111) }, 1000); }) const p2 = new Promise((resolve, reject) => { setTimeout(() => { reject(22222) }, 2000); }) const p3 = new Promise((resolve, reject) => { setTimeout(() => { resolve(33333) }, 3000); }) // allSettled Promise.allSettled([p1, p2, p3]).then(res => { console.log(res) }).catch(err => { console.log(err) })
Promise.race
只要有一个Promise变成fulfilled状态, 那么就结束,但有一个Promise状态先变为rejected,则结束
Promise.any
等到至少有一个Promise变成fulfilled状态, 才就结束,不管有没有Promise状态先变为rejected。如果全为rejected状态则等到全部执行完才结束并执行catch方法(全部rejected状态的错误信息)
迭代器、生成器
迭代器
迭代器是一个对象,让我们能够遍历某个数据结构(如:链表或数组)
在JS中,迭代器是一个对象且还需要有next函数(符合迭代器协议)
const iterator = {next: function() {return {}}}
next函数
一个无参数函数,返回一个应当拥有done和value属性的对象:
done (boolean):
- 如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)
- 如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
value:迭代器返回的任何 JavaScript 值。done 为 true 时可省略。
// 数组 const names = ["abc", "cba", "nba"] // 创建一个迭代器对象来访问数组 let index = 0 const namesIterator = { next: function() { if (index < names.length) { return { done: false, value: names[index++] } } else { return { done: true, value: undefined } } } } console.log(namesIterator.next()) console.log(namesIterator.next())
可迭代对象
可迭代对象是一个对象且需要实现 @@iterator方法,我们可以使用Symbol.iterator访问该属性(符合可迭代协议)
const iterableObj = {[Symbol.iterator]: function(){return 迭代器}}
const iterableObj = { names: ["abc", "cba", "nba"], [Symbol.iterator]: function() { let index = 0 return { next: () => { // 使用箭头函数使this指向iterableObj if (index < this.names.length) { return { done: false, value: this.names[index++] } } else { return { done: true, value: undefined } } } } } }
用于可迭代对象的语法:
- for ...of 可以遍历的东西必须是一个可迭代对象
- 展开运算符 (...) [ 在ES9之后新增的特性:普通对象也可以使用展开运算符 ]
- 解构赋值 [ 在ES9之后新增的特性:普通对象也可以使用解构赋值]
- yield*
- 使用Set 、Array.from 创建对象时需要传入可迭代对象
内置可迭代对象
String、Array、TypedArray、Map 和 Set都是内置可迭代对象,因为它们的原型对象都拥有一个 Symbol.iterator方法
// 以数组为例: const names = ["abc", "cba", "nba"] console.log(names[Symbol.iterator])
生成器
与函数相关,是ES6新增的一种函数控制、使用的方案(控制函数什么时候继续执行、暂停执行等操作)
生成器函数返回值是一个生成器
生成器函数
生成器函数特点:
- 生成器函数需要在function的后面加一个符号:*
- 生成器函数可以通过yield关键字来控制函数的执行流程
- 生成器函数的返回值是一个Generator(生成器)
function* foo() { console.log("函数开始执行~") const value1 = 100 console.log("第一段代码:", value1) yield const value2 = 200 console.log("第二段代码:", value2) yield console.log("函数执行结束~") } // 调用生成器函数时, 会给我们返回一个生成器对象 const generator = foo() // 开始执行第一段代码 generator.next()
我们使用第一个next()调用的时候,执行的是第一个yield上面的代码
- 当遇到yield时候值暂停函数的执行
- 当遇到return时候生成器就停止执行
- 如果想要第一个next返回的结果不是undefined,则在yield之后加上想要返回的值:
yield value1
生成器本质上是一个特殊的迭代器
//打印的结果与迭代器的形式是一样的 const generator = foo() console.log("返回值:", generator.next()) // 返回值: { value: undefined, done: false }
生成器的方法使用
next传递参数
我们在调用next函数的时候,可以给它传递参数,那么这个参数会作为上一个yield语句的返回值;而这个传递进来的参数则为下一个代码块执行提供了一个值
function* foo(num) { console.log("函数开始执行~") const value1 = 100 * num console.log("第一段代码:", value1) const n = yield value1 const value2 = 200 * n console.log("第二段代码:", value2) console.log("函数执行结束~") return "123" } // 生成器上的next方法可以传递参数 //给第一个代码块传参 const generator = foo(5) // 传入的5被上面的num接收 generator.next() // 给第二个代码块传参,传入的10对应上面的n generator.next(10)
return终止执行
// 相当于在代码块的后面加上return, 就会提前终端生成器函数代码继续执行 generator.return(15)