JavaScript高级语法(coderwhy版本)(五)

简介: JavaScript高级语法(coderwhy版本)

展开语法—— 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的用法

  1. 在定义对象字面量时使用
const obj = {
  [s1]: "abc",
  [s2]: "cba"
}
  1. 新增属性
// 新增属性
obj[s3] = "nba"
//Object.defineProperty方式来新增/定义
const s4 = Symbol()
Object.defineProperty(obj, s4, {
  enumerable: true,
  configurable: true,
  writable: true,
  value: "mba"
})
  1. 获取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结构类似,也是用于生成键值对的集合

WeakMapMap的区别有两点:

  • 首先,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)

缺点:

  1. Object.defineProperty  就不是用来监听对象的属性的,但利用它的特性可以实现
  2. Object.defineProperty  不能监听新增属性、删除属性的操作等其它操作
  3. 无法监听数组变化,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捕获器,接下来介绍一些其他常见的捕获器

  1. 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)
  1. 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捕获顺序

  1. 当上面调用的是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
})
  1. 当上面调用的是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

  1. 传入普通的值
const promise = Promise.resolve({ name: "why" })
// 相当于
const promise2 = new Promise((resolve, reject) => {
  resolve({ name: "why" })
})
  1. 传入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 }
        }
      }
    }
  }
}

用于可迭代对象的语法:

  1. for ...of 可以遍历的东西必须是一个可迭代对象
  2. 展开运算符 (...)   [ 在ES9之后新增的特性:普通对象也可以使用展开运算符 ]
  3. 解构赋值   [ 在ES9之后新增的特性:普通对象也可以使用解构赋值]
  4. yield*
  5. 使用Set 、Array.from 创建对象时需要传入可迭代对象

内置可迭代对象

StringArrayTypedArrayMapSet都是内置可迭代对象,因为它们的原型对象都拥有一个 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)


目录
相关文章
|
1月前
|
JavaScript 前端开发 索引
JavaScript字符串检查:从基础到高级
【2月更文挑战第26天】
26 0
JavaScript字符串检查:从基础到高级
|
30天前
|
JavaScript 前端开发 Web App开发
JavaScript基础语法(codewhy版本)(一)
JavaScript基础语法(codewhy版本)
88 1
JavaScript基础语法(codewhy版本)(一)
|
30天前
|
JavaScript 前端开发 Java
JavaScript高级笔记-coderwhy版本(六)
JavaScript高级笔记-coderwhy版本
74 0
|
30天前
|
JavaScript 前端开发 Java
JavaScript高级笔记-coderwhy版本(一)
JavaScript高级笔记-coderwhy版本
34 0
JavaScript高级笔记-coderwhy版本(一)
|
1月前
|
JavaScript 前端开发 网络架构
JavaScript的数组教程(最详细,更新至es6新语法)
JavaScript的数组教程(最详细,更新至es6新语法)
|
1月前
|
JSON JavaScript 前端开发
javascript语言ES5版本详解(一)
javascript语言ES5版本详解(一)
|
1月前
|
移动开发 前端开发 JavaScript
Java 和 JavaScript 的奇妙协同:语法结构的对比与探索(下)
Java 和 JavaScript 的奇妙协同:语法结构的对比与探索(下)
|
2月前
|
JavaScript
Node.js【GET/POST请求、http模块、路由、创建客户端、作为中间层、文件系统模块】(二)-全面详解(学习总结---从入门到深化)
Node.js【GET/POST请求、http模块、路由、创建客户端、作为中间层、文件系统模块】(二)-全面详解(学习总结---从入门到深化)
27 0
|
2月前
|
消息中间件 Web App开发 JavaScript
Node.js【简介、安装、运行 Node.js 脚本、事件循环、ES6 作业队列、Buffer(缓冲区)、Stream(流)】(一)-全面详解(学习总结---从入门到深化)
Node.js【简介、安装、运行 Node.js 脚本、事件循环、ES6 作业队列、Buffer(缓冲区)、Stream(流)】(一)-全面详解(学习总结---从入门到深化)
77 0
|
4天前
|
JavaScript 前端开发 测试技术
学习JavaScript
【4月更文挑战第23天】学习JavaScript
11 1