记学习ES新语法

148 阅读28分钟

前言:学习ES新语法的记录。

1. let与const

let与var作用类似,声明变量用的,而const则是声明常量用的。

1.1 没有变量提升

使用var声明变量的时候,会有变量提升。

console.log(name) // 小明

var name = '小明'

即使在声明变量之前使用了变量,也不会有问题。

而let和const不会有提升,在声明前使用则会报错。

console.log(name) // 报错 name is not defined
let name = '小明'

console.log(age) // 报错 age is not defined
const age = 10

1.2 块级作用域内可用

let和const声明的变量只在其声明的块或者字块中可用。

{
  var name = '小明'
  let age = 10
  const gender = '男'
  
  console.log(age) // 10
}

console.log(name) // 小明
console.log(gender) // 报错 age is not defined

1.3 不允许重复声明

let name = '小明'
let name = '小智' // 报错 Identifier 'name' has already been declared

const name = '小明'
const name = '小智' // 报错 同上

1.4 初始值的不同

var和let初始值可不给值,const必须给定一个值。

var name
let age
const gender // 报错 Missing initializer in const declaration

1.5 修改值的不同

使用let和const定义变量的时候,若定义的值为简单类型,则let与var类似,可以随意地修改值。

let name = '小明'
name = []

console.log(name) // []

const age = 10
age = 11 // 报错 Assignment to constant variable
console.log(age)

可以看到const定义的变量修改之后报错了,是不允许修改的。
但如果定义的是一个复杂类型,是可以修改其中的属性值的。

// 定义数组
const arr = [1, 2, 3]

arr.push(4)
console.log(arr) // [1, 2, 3, 4]

arr[4] = 88 
console.log(arr) // [1, 2, 3, 4, 88]

// 定义对象
const obj = {
  name: '小明'
}

obj.age = 10

console.log(obj) // { name: '小明', age: 10 }

2. 模板字符串

2.1 基本用法

包裹在两个 ` 中间即可使用模板字符串。英文输入法下,按键盘上面的一排数字键的1的左边的按键就可以打出。

let name = `我是,小明`

// 在模板字符串中,换行,空格等都会被保留
let list = `
    <ul>
          <li>1   </li>
      <li>12</li>
    </ul>
`

2.2 拼接变量

以前拼接字符串都是要用上引号和加号,稍不留神就拼错了,找错误还得半天,现在可以直接在模板字符串中用${}包裹变量的方式添加变量来拼接。

let name = '小明',
  age = 10

// 普通拼接
let str = '你好,我的名字是' + name + ',今年' + age + '岁了。'

// 使用模板字符串
let str2 = `你好,我的名字是${name},今年${age}岁了。`

2.3 使用函数

在模板字符串中添加函数也是可以的。

let dom = `
    <ul>
      ${
        lists.map(item => {
          return `<li>${item.name}</li>`
        }).join('')
      }
    </ul>
`

3. 箭头函数

3.1 基本语法

let func = (x, y)=> { 
    return x + y
}

翻译成普通函数就是

let func = function (x, y) {
  return x + y
}

在箭头函数里,如果参数为一个,可以省略括号,如果只有一条执行语句,可以省略花括号和return。
如:

let func = x => x

3.2 与普通函数的区别

3.2.1 this指向

箭头函数是没有自己this的。

可以回顾下普通函数的this是如何确定的:

  • 如何函数时一个构造函数,那么this指向实例对象
  • 全局函数的this指向Window,严格模式下this指向undefined
  • 如果是一个对象中的方法,则this指向该对象 箭头函数的this:

来自MDN:箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。详解

作用域值的是函数内部,箭头函数的this指向定义该函数时所在作用域指向的对象。

// 构造函数中有一个定时器,定时器的this指向Window
function Person() {
  setTimeout(function () {
    console.log(this) // Window
  }, 1000)
}

// 定时器的this指向Window,箭头函数的this指向外层作用域
function Person() {
  setTimeout(() => {
    console.log(this) // Person {}
  }, 1000)
}

换个例子 aFunc的this指向obj,obj不是函数,所以bFunc的this其实是指向Window的。

let obj = {
  count: 1,
  aFunc: function () {
    console.log(this, this.count)
  },
  bFunc: () => {
    console.log(this, this.count)
  }
}

obj.aFunc() // obj {}, 1
obj.bFunc() // Window, undefined

上面的例子改造一下

var count = 0

let obj = {
  count: 1,
  aFunc: function () {
    console.log(this, this.count)
  },
  bFunc: () => {
    console.log(this, this.count)
  }
}

let obj2 = {
  count: 2
}

obj.aFunc.call(obj2) // obj2 {}, 2
obj.bFunc.call(obj2) // Window, 0

可以看到的是箭头函数的this无法被call改变,同理,apply,bind也是无效的。

3.2.2 没有arguments参数

在普通函数中,可以通过arguments对象获取参数列表,但是在箭头函数中,没有自己的此参数。

function aFunc() {
  console.log(arguments)
}

aFunc(1, 2, 3)

let bFunc = () => {
  console.log(arguments) // arguments is not defined
}

bFunc(1, 2, 3)

为什么说是没有自己的arguments呢,改造一下上面的bFunc。

let bFunc = function () {
  return () => {
    console.log(arguments)
  }
}

bFunc(1, 2, 3)() // [1, 2, 3]

可以看到箭头函数中可以拿到父作用域的arguments。

那这样很不方便啊,有时参数不定,该怎么办呢。

对于这样的情况可以使用reset参数,在传参的时候使用(...参数)的形式,一样可以拿到参数列表。

let cFunc = (...args) => {
  console.log(args)
}

cFunc(1, 2, 3) // [1, 2, 3]
3.2.3 不能作为构造函数来使用
function Person(name) {
  this.name = name
}

let people = new Person('小明')
console.log(people.name) // 小明

let Animal = (name) => {
  this.name = name
}
let cat = new Animal('猫') // 报错 Animal is not a constructor

为什么不能使用new关键字呢,可以回顾下new出实例的步骤:

  • 创建一个新对象
  • 将构造函数的作用域赋给新对象(因此this指向新对象)
  • 执行构造函数中的代码
  • 返回此对象 原因:在过程中

(1)会将构造函数的prototype赋给新对象的__proto__属性,但是构造函数没有prototype。

let Animal = (name) => {
  this.name = name
}
console.log(Animal.prototype) // undefined

(2)通过call将this指向新对象,而call对箭头函数无效。

4. Symbol

Symbol是在ES6引入的一种新的基本数据类型,表示独一无二的值。

4.1 定义Symbol

let s = Symbol()

// 也可以传入一个字符串作为描述
let s1 = Symbol('s1')

console.log(s) // Symbol()
console.log(s1) // Symbol(s1)

console.log(typeof s) symbol

因为是一个独一无二的值,即使传入的描述字符串相同,也是不同的。

let sym1 = Symbol('s')
let sym2 = Symbol('s')

console.log(sym1 === sym2) // false

4.2 描述参数

描述参数也可以传一个对象。

let obj = {
 name: '小明'
}

let arr = [1, 2, 3]

let s = Symbol(obj)
let s1 = Symbol(arr)

console.log(s) // Symbol([object Object])
console.log(s1) // Symbol(1,2,3)

当描述参数是一个对象的时候,会先调用对象的toString方法,然后将返回的值当做参数,生成Symbol。

let obj = {
  toString: function () {
    return '小明'
  }
}
let s = Symbol(obj)

console.log(s) // Symbol(小明)

4.3 不能进行运算

let s = Symbol('s')

console.log(s + 10) // 报错 Cannot convert a Symbol value to a number

console.log('Symbol是:' + s) // 报错 Cannot convert a Symbol value to a string

console.log(`Symbol值是:${s}`) // 报错 Cannot convert a Symbol value to a string

4.4 可以转化为字符串

let s = Symbol('s')

console.log(String(s)) // 'Symbol(s)'
console.log(s.toString()) // 'Symbol(s)'

转化为字符串后,相同描述参数的Symbol就相同了。

4.5 JSON.stringify会被忽略

当作为对象的属性值的时候,使用stringify的时候会被忽略。

let s = Symbol('s')

let obj = {
  name: '小明',
  sym: s
}

console.log(JSON.stringify(obj)) // {"name":"小明"}

4.6 可作为对象属性名出现

因为Symbol是一个独一无二的值,所以当它作为一个对象的属性名的时候,可以确保不会出现相同名字的属性。

let s = Symbol('s')

let obj = {
    [s]: '你好'
}

console.log(obj) // {Symbol(s): "你好"}

但是它不会被 Object.getOwnPropertyNamesfor...infor...ofObject.keys捕获。

这时可以使用另一个方法:Object.getOwnPropertySymbols 获取对象中用Symbol作为key的属性名。

let s = Symbol('s')
let s1 = Symbol('s1')

let obj = {
  name: '小明',
  age: 10,
  [s]: 's',
  [s1]: 's1',
}

let propNames = Object.getOwnPropertyNames(obj)
let propSymbolNames = Object.getOwnPropertySymbols(obj)

console.log(propNames) // ["name", "age"]
console.log(propSymbolNames) // [Symbol(s), Symbol(s1)]

4.7 Symbol.for 和 Symbol.keyFor

来自MDN:要创建跨文件可用的symbol,甚至跨域(每个都有它自己的全局作用域) , 使用 Symbol.for() 方法和  Symbol.keyFor() 方法从全局的symbol注册表设置和取得symbol。

使用Symbol.for创建一个Symbol,此方法创建的Symbol会被放在一个全局的Symbol注册表中,如何再次创建一个新的Symbol,参数与之前的相同,变回返回这个Symbol,而不会新建。值得一提的是,这个Symbol跨域都可以共享。

let s = Symbol.for('s')

// 会返回已经存在的Symbol
console.log(s === Symbol.for('s')) // true

可以通过symbol.keyFor去取通过Symbol.for创建的Symbol的描述参数。不是通过这种方式创建的Symbol无法获取。

let s = Symbol('s')
console.log(Symbol.keyFor(s)) // undefined

let s1 = Symbol.for('s1')
console.log(Symbol.keyFor(s1)) // s1

5. BigInt

BigInt是新增的一种基本类型数据。原先的Number类型,它可以表示的最大安全整数就是2^53 - 1,即9007199254740991。而BigInit可以用于表示大于2^53 - 1的整数。

5.1 定义BIgInt

let b1 = 10n
let b2 = BigInt(20)

console.log(b1, b2) // 10n 20n
console.log(typeof b1, typeof b2) // bigint bigint

来自MDN:它在某些方面类似于 Number ,但是也有几个关键的不同点:不能用于 Math 对象中的方法;不能和任何 Number 实例混合运算,两者必须转换成同一种类型。在两种类型来回转换时要小心,因为 BigInt 变量在转换成 Number 变量时可能会丢失精度。

5.2 与运算符使用

let b = 100n

b + 20n // 120n
b - 20n // 80n
b * 20n // 2000n
b ** 2n // 10000n
b / 20n // 5n

值得注意的是/运算符,该操作符的结果会向下取整,不会返回小数部分。

let b = 5n

console.log(b / 2n) // 按道理是2.5n,但其实是2n

5.3 比较

2n === 2n // true

5n === 2n // false

BigInt和Number是可以进行比较的。

// 当宽松比较的时候,结果为true
2n == 2 // true

// 当严格比较的时候,结果为false
2n === 2 // false

大于和小于的比较也是可以的。

(2n > 2 // false

2n >= 2 // true

2n > 5 // false

5n > 2 // true

5.4 转化Boolean值

转化为Boolean时与Number类似

Boolean(0n) // false

Boolean(2n) // true

!2n // false

0n && 2n // 0n

0n || 2n // 2n

6. 函数的默认参数

函数现在可以传入默认参数了,允许在没有值或者值为undefined的时候使用传入的默认参数了。

在以前,对参数判空,如果是空就设置一个默认值可能需要这样。

function func(name) {
  if (!name) {
    name = '小明'
  }
  return name
}

func() // 小明

现在有了默认参数,可以这么写。

function func(name = '小明') {
  return name
}

func()

很方便,在形参位置设置即可。

7. 解构赋值

解构赋值是一种js表达式。可以通过解构赋值,将属性或者值从对象或者数组中取出,赋值给其他的变量。

在以前,交换变量的值,需要一个中间变量。比如,现在我想交换a和b的值。

let a = 1,
  b = 2;

let change;

change = a
a = b
b = change

console.log(a, b) // 2 1

有了解构赋值之后,我们只需要这么做。

[a, b] = [b, a]

console.log(a, b) // 2 1

7.1 在数组中使用

在数组中使用,可以方便快捷地把数组中的值赋给变量。

let arr = [1, 2, 3]

let [a, b, c] = arr

console.log(a, b, c) // 1 2 3

也可以设置一个默认值,应对想要的值不存在的情况。

let arr = [1, 2, 3]

let [a, b, c = 8, d = 4] = arr

console.log(a, b, c, d) // 1 2 3 4

在取值的过程中,也可以忽略某些值。

let arr = [1, 2, 3]

let [a, , b] = arr

console.log(a, b) // 1 3

7.2 在对象中使用

在对象中使用

let obj = {
  name: '小明',
  age: 10
}

let {name} = obj

console.log(name) // 小明

也可以对取出的变量重新设置变量名。

let obj = {
  name: '小明',
  age: 10
}

let {name: x} = obj

console.log(x) // 小明

也是可以设置默认值的,这个与数组类似。

7.3 针对于嵌套的对象与数组

let obj = {
  name: '小明',
  message: {
    age: 10,
    gender: '男',
    books: ['西游记', '红楼梦', ['三国演义', '水浒传']]
  }
}

let {
  name,
  message: {
    age: y,
    books: [book1, book2, [, book3]]
  }
} = obj

8. 展开语法

来自MDN:展开语法(Spread syntax), 可以在函数调用/数组构造时, 将数组表达式或者string在语法层面展开;还可以在构造字面量对象时, 将对象表达式按key-value的方式展开。 可以将数组的值展开传入函数中作为参数。

let arr = ['小明', 10]

function func(name, age) {
  console.log(name, age)
}

func(...arr)

可以合并两个对象作为一个新的对象。

let obj = {name: '小明'}

let obj2 = {age: 10}

let obj3 = {...obj, ...obj2}

console.log(obj3) // {name: "小明", age: 10}

获得一个新的数组。

let arr = [1, 2, 3]

let arr2 = [4, 5, 6]

let arr3 = [...arr, ...arr2, 7]

console.log(arr3) // [1, 2, 3, 4, 5, 6, 7]

拷贝对象或者数组。

let arr = [1, 2, 3]

let arr2 = [...arr]

arr[0] = 11

console.log(arr, arr2)// [11, 2, 3] [1, 2, 3]

let obj = {name: '小明'}

let obj2 = {...obj}

obj.name = '小智'

console.log(obj, obj2) // {name: "小智"} {name: "小明"}

虽然改变原数组或者原对象的值,对拷贝过的数组或者对象没有影响,但其实还是浅拷贝,只会遍历一层,多维数组或者多层对象,就会出现问题。

9. for...of与迭代器

来自MDN:for...of语句可迭代对象(包括 ArrayMapSetStringTypedArrayarguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。

let arr = [1, 2, 3]

for (let num of arr){
  console.log(num) 
}

这一个最基本的结构的for...of循环结构,循环体内会依次输出1 2 3。

上面说了要在一个可迭代对象上面使用,比如Object就不是可迭代对象。

let obj = {name: '小明'}

for (let value of obj) {
  console.log(value) // 报错 obj is not iterable
}

那如何成为一个可迭代对象呢。那就是必须实现@@iterator方法。意思就是在此对象的原型链上,必须有一个@@iterator属性。这个属性是可以通过Symbol.iterator访问的。 迭代器,是一个对象,在其中有一个叫next的方法,该方法返回两个属性的值,value和done。value表示每次迭代返回的值;done为布尔值,表示是否迭代完毕。

可以自定义一个可迭代对象:

// 使用 Generator
let obj = {name: '小明'}

obj[Symbol.iterator] = function* () {
  yield 1
  yield 2
  yield 3
}

for (let value of obj) {
  console.log(value) // 分别输出 1 2 3
}
// 自定义
let obj = {name: '小明', age: 10}

// 添加Symbol.iterator属性,一个无参数的函数
// next方法返回一个对象,包含value和done
obj[Symbol.iterator] = function () {
  let index = 0
  let propList = Object.keys(obj)
  return {
    next: () => {
      let value = this[propList[index]]
      let done = (index >= propList.length)
      index++
      return {
        value,
        done
      }
    }
  }
}

for (let value of obj) {
  console.log(value) // 分别输出 小明 10
}

现在可以改造一下上面报错的例子。

let obj = {name: '小明'}

function createIterator(obj) {
  let index = 0
  let propList = Object.keys(obj)
  return {
    next: () => {
      let done = index >= propList.length
      let value = obj[propList[index++]]
      return {value, done}
    }
  }
}

obj[Symbol.iterator] = function () {
  return createIterator(this)
}

for (let value of obj) {
  console.log(value) // 小明
}

10. Set和WeakSet

10.1 Set

Set对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
Set对象中的元素是唯一的。

10.1.1 创建Set

直接调用Set构造函数可以创建一个新的Set对象,其中可以传入任意类型(带有iterable接口)的唯一值。

如果传递的是一个可迭代对象,那么其中的元素会不重复地添加进Set对象中。

// 调用Set构造函数
let set = new Set()
let set1 = new Set([1, 2, 3])
10.1.2 Set的长度

使用当前Set对象的size属性会返回其中元素的个数。

let set = new Set()
let set1 = new Set([1, 2, 3, 3])

console.log(set.size) // 0
console.log(set1.size) // 3
10.1.3 添加元素

使用add方法可以往Set对象的尾部添加新的元素。可以链式调用,重复的值无法调用。

let set = new Set()

set.add(1)
set.add(2).add(3).add(3)

console.log(set) // Set {1, 2, 3}
10.1.4 移除所有元素

使用clear方法可以清空Set对象中所有的元素。

let set = new Set([1, 2, 3])

set.clear()

console.log(set) // Set {size: 0}
10.1.5 移除指定元素

使用delete方法,传入将要删除的元素,可以移除对应元素。返回值为布尔值,代表成功或者失败。

let set = new Set([1, 2, 3])

// 传入将要删除的元素
set.delete(2)

console.log(set) // Set {1, 3}
10.1.6 entries迭代

使用entries方法会返回一个新的迭代器,循环返回 [value, value] 形式的数组。

let set = new Set([1, 2])

set.add(55)

let setIterable = set.entries()

for (let value of setIterable) {
  console.log(value) // 依次输出[1, 1] [2, 2] [55, 55]
}
10.1.7 values和keys迭代

values和keys一样,都是返回一个新的迭代器,循环返回Set对象中的元素值。

let set = new Set([1, 2, 3])

set.add(55)

for (let value of set.values()) {
  console.log(value) // 1 2 3 55 
}

for (let value of set.keys()) {
  console.log(value) // 1 2 3 55 
}
10.1.8 forEach循环

与数组的forEach了,依次遍历每个元素,并提供回调函数。

let set = new Set([1, 2, 3])

set.forEach(item => {
  console.log(item)
})
10.1.9 has

使用has方法,传入一个值,可以返回一个布尔值来表示这个值是否在当前Set对象中。

let set = new Set([1, 2, 3])

console.log(set.has(1)) // true
console.log(set.has(4)) // false
10.1.10 与数组的相互转换

数组转换成Set对象,将数组传入Set构造函数即可。
Set对象转换成数组,可以使用...扩展运算符。

let set = new Set([1, 2, 3])

set.add(55)

console.log([...set]) // [1, 2, 3, 55]

前面有提起过,Set对象中的元素是不重复的,传入一个可迭代对象,会不重复地把元素添加进去。利用这个特性,可以实现数组的去重。

// 有重复值的数组
let repeatArr = [1, 1, 2, 2, 3, 4, 4, 5, 6]

// 塞入Set
let set = new Set(repeatArr)

console.log(set) // Set {1, 2, 3, 4, 5, 6}

// 转换为数组
let arr = [...set]

console.log(arr) // [1, 2, 3, 4, 5, 6]

10.2 WeakSet

WeakSet对象允许将弱保持对象存储在一个集合中。

WeakSet与Set类似,但是只有add,delete,has三个方法。

区别有两点:

  • WeakSet只能是对象的集合,而不能是任意类型的值。 只能添加对象类型的值,添加基本数据类型的值会报错。
let weakSet = new WeakSet()

let obj = {name: '小明'}
let arr = [8, 88]
let str = '1'

weakSet.add(obj)
weakSet.add(arr)
weakSet.add(str) // Invalid value used in weakset
  • 集合中对象的引用为弱引用。垃圾回收机制会这样做,如果一个对象,没有被除了WeakSet以外的其他地方引用,那么这个对象会被回收。

因为没有size属性,所以无法得知长度,无法遍历。

11. Map和WeakMap

11.1 Map

Map对象保存键值对,并且能够记住键的原始插入顺序。任何值都可以作为一个键或一个值。

11.1.1 创建Map

调用Map构造函数可以创建一个Map,其中的参数应为一个数组或者可迭代对象,其中元素为两个元素的数组,代表键与值。

let map = new Map([
  ['name', '小明'],
  [Symbol(), {a: 1}],
  ['age', 10]
])
11.1.2 Map的长度

使用size属性可以知道一个Map对象的长度。

let map = new Map([
  ['name', '小明'],
  [Symbol(), {a: 1}],
  ['age', 10]
])

console.log(map.size) // 3
11.1.3 添加键值对

使用set方法可以在Map对象中添加键值对。

let map = new Map([
  ['name', '小明']
])

map.set('age', 10)

console.log(map) // Map {"name" => "小明", "age" => 10}
11.1.4 获取值

使用get方法可以方便地获取Map对象中的值。

let map = new Map([
  ['name', '小明'],
  ['age', 10]
])

console.log(map.get('age')); // 10
11.1.5 迭代Map对象

与Set类似,Map也有entriesvalueskeys三个方法,返回一个新的迭代器,之后可以用for...of迭代Map对象。直接迭代Map对象的话,效果与entries相同。还可以使用forEach迭代Map对象。

  • entries 与直接迭代Map对象效果相同
let map = new Map([
  ['name', '小明']
])

map.set('age', 20)

for (let value of map.entries()){
  console.log(value) // 依次输出 ['name', '小明'] ['age', 20]
}

for (let value of map){
  console.log(value) // 效果同上
}
  • 11.5.2 values 使用values进行迭代,会拿到Map对象的值。
let map = new Map([
  ['name', '小明']
])

map.set('age', 20)

for (let value of map.values()){
  console.log(value) // 依次输出 小明  20
}
  • 10.5.3 keys 使用keys进行迭代,会拿到Map对象的键。
let map = new Map([
  ['name', '小明']
])

map.set('age', 20)

for (let value of map.keys()){
  console.log(value) // 依次输出 name age
}
  • 10.5.4 forEach
 let map = new Map([
   ['name', '小明']
 ])

 map.set('age', 20)

map.forEach((value,key)=>{
  console.log(value,key) // 依次输出键和值
})
10.1.6 与数组的相互转化

数组转化为Map对象。因为创建Map对象的时候需要去传参,那么按照要求的格式传入一个数组,就可以了。

let arr = [['name', '小明'], ['age', 10]]

let map = new Map(arr)

Map对象转化为数组。使用Array.from与扩展运算符都是可以的。

let arr = [['name', '小明'], ['age', 10]]

let map = new Map(arr)

// 均可转化为原数组
console.log(Array.from(map));
console.log([...map]);

在使用keys,values方法的时候也可以转化为数组,转化为相对应的只包含键或者值的数组。

11.1.7 合并Map对象

在合并时,使用扩展运算符可以合并两个Map对象。在键相同的时候,则后面的会覆盖前面的。

let map1 = new Map([['name', '小明']])

let map2 = new Map([['age', 10]])

let map3 = new Map([...map1, ...map2])
11.1.8 清空与删除

使用clear方法可以清空Map,使用delete可以删除指定的元素。

11.2 WeakMap

与Map类似,WeakMap对象是一组键值对的集合,其中的键是弱引用的。其键必须是对象(基本数据类型不可以作为键),而值可以是任意的。

// 这是错误的
new WeakMap([
  ['name', '小明'] // Invalid value used as weak map key
])

// 这是正确的
let func = function () {}

let weakMap = new WeakMap([
  [[1, 2], 'arr'],
  [{a: 1}, 'obj'],
  [func, 'func']
])

其他的与Map类似,但没有迭代方法,不可迭代。

12. Promise

来自MDN:Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。

在以前,针对js中的异步处理,大多是利用回调函数。
当一个函数,作为另一个函数的参数,在满足一定条件下执行,这个函数就被称为回调函数。

定时器中的第一个参数是函数,这个函数就是回调函数。

setTimeout(function () {
  console.log(1)
  setTimeout(function () {
    console.log(2)
    setTimeout(function () {
      console.log(3)
    }, 1000)
  }, 1000)
}, 1000)

此段代码会在1秒后输出3次,是一段典型的使用回调函数的代码。那么如果想在后面按顺序继续输出,就会往里面加入更多的代码,代码的缩进会越来越多,就形成了回调地狱。

用Promise就可以解决这个问题。

let promise = new Promise(resolve => {
  setTimeout(() => {
    console.log(1)
    resolve()
  }, 1000)
})

promise.then(() => {
  setTimeout(() => {
    console.log(2)
    return 2
  }, 2000)
})
  .then(() => {
    setTimeout(() => {
      console.log(3)
      return 3
    }, 3000)
  })

12.1 Promise定义

通过Promise构造函数就可以创建一个promise对象,它可以接收一个函数参数,函数参数中有两个方法,resolvereject,在此函数中,进行异步操作,成功了调用resolve,失败了调用reject。

成功之后可以使用promise.then()方法去接收结果,进行下一步操作。
失败了可以使用promise.catch()方法去处理错误。

let promise = new Promise((resolve, reject) => {
  if ( /* 成功 */) {
    resolve(true)
  } else {
    reject(false)
  }
})

promise
  .then((res) => {

  })
  .catch(err => {
  
  })

promise对象有三种状态。

  • Fulfilled:resolve的时候。
  • Rejected:reject的时候。
  • Pending:promise对象刚被创建时的初始状态。 而promise对象的状态,只会从 Pending 转变为 Fulfilled 或者 Rejected,此后,这个对象的状态就不会变化了。

12.2 then与catch

它需要两个参数,promise的成功或者失败的回调函数。在执行完其中的回调函数时候,会返回一个全新的promise对象,所以它还可以链式调用,在上一个then中return就可以将其作为下一个then中的回调函数的参数。
像这样。

let promise = new Promise((resolve, reject) => {
  resolve(1)
})

promise
  .then(res => {
    console.log(res) // 1
    return res + 1
  }, err => {

  })
.then(res => {
  console.log(res) // 2
})

而catch其实是then的第二个参数的语法糖,它可以处理promise中reject或者出错的情况。

比如上一段代码,可以这么写。

let promise = new Promise((resolve, reject) => {
  resolve(1)
})

promise
  .then(res => {
    console.log(res) // 1
    return res + 1
  })
  .catch(err => {

  })
  .then(res => {
    console.log(res) // 2
  })

那么这两种写法的区别就是:第一种写法无法捕捉then中出现的错误,从而无法进行统一的错误处理。所以一般的做法是在链式调用的最后写上一个catch,进行错误处理。

12.3 resolve和reject

Promise.resolve(value)会返回一个Fulfilled的promise对象,可以直接跟上then使用。 Promise.reject(value)会返回一个Rejected的promise对象,可以直接跟上catch使用。

12.4 Promise.all

Promise.all方法可接受一个带iterable接口的参数,如一个包含n多操作的数组,返回一个Promise实例。使用then之后,参数为数组,其中包含了操作的结果。操作中的如果出现了错误,不会调用then,会被catch捕捉。

let a = Promise.resolve(1)

let b = new Promise(resolve => {
  setTimeout(() => {
    resolve(2)
  },1000)
})

let c = 3

Promise.all([a, b, c])
  .then(values => {
    console.log(values) // [1, 2, 3]
  })

上面的代码会在1秒之后输出[1, 2, 3],由此可见,不管传入的操作是异步还是同步,都会等到所有的操作全部变成resolve或者reject之后返回结果,调用then方法。

function delayLog(delay) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(delay)
    }, delay)
  })
}

Promise.all([
  delayLog(1),
  delayLog(100),
  delayLog(1000),
  delayLog(3000),
  delayLog(2000)
])
  .then(values => {
    console.log(values) // [1, 100, 1000, 3000, 2000]
  })

可以看到,即使参数数组中的程序是同时进行的,最短时间是1毫秒,还是会在3秒之后一起输出结果。

12.5 Promise.race

与Promise.all类似,Promise.race也是用于处理多个promise对象的方法。 他们的不同点是,all会在所有的操作resolve或者reject之后才会调用then,而race,顾名思义,比赛,赛跑,就是看谁快,在一个操作resolve或者reject之后,就可以调用then。

比如上面的例子,改为使用Promise.race之后,只会输出最快完成的那个,所以会输出1。当然了,这个输出的是谁和传入的的顺序无关。

function delayLog(delay) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(delay)
    }, delay)
  })
}

Promise.race([
  delayLog(1),
  delayLog(100),
  delayLog(1000),
  delayLog(3000),
  delayLog(2000)
])
  .then(values => {
    console.log(values) // 1
  })

12.6 Promise.allSettled

前面说了,all方法如果在promise数组中某个操作reject的时候,会被catch捕捉,不会执行then。

allSettled方法返回一个在所有给定的promise都已经Fulfilled或者rejected后的promise,并带有一个对象数组,每个对象表示对应的promise结果。

let a = Promise.resolve(1)

let b = new Promise(resolve => {
  setTimeout(() => {
    resolve(2)
  }, 1000)
})

let c = Promise.reject(3)

Promise.allSettled([a, b, c])
  .then(values => {
    values.forEach(item => {
      console.log(item)
      // 会依次输出
      // {status: "fulfilled", value: 1}
      // {status: "fulfilled", value: 2}
      // {status: "rejected", reason: 3}
    })
  })

需要知道每个promise的结果的时候,适合使用。

12.7 Promise.finally

finally方法在promise结束时,无论结果如何,都会执行指定的回调函数。在需要执行不论怎样都要执行的代码时,是一个好方式。

let a = Promise.resolve(1)

let b = Promise.reject(2)

a.then(res => {
  console.log(res) // 1
})
  .finally(() => {
    console.log('a finally') // a finally
  })

b.then(null, err => {
  console.log(err) // 2
})
  .finally(() => {
    console.log('b finally') // b finally
  })

12.8 Promise.any

这个方法与all方法相反。也是接收一个Promise对象数组,只要其中一个resolve,就返回成功的promise;如若都失败了,就返回一个失败的promise。

目前该方法未被任何浏览器支持。

13. Generator

Generator,生成器函数,异步解决方案,执行之后可生成一个可遍历对象,通过yield可多次返回值。每次执行在yield时停止,通过调用next方法可以继续执行。

13.1 创建Generator

function* createGen () {
  yield 1
  yield 2
  yield 3
}

let gen = createGen()

console.log(gen.next()) // {value: 1, done: false}
console.log(gen.next()) // {value: 2, done: false}
console.log(gen.next()) // {value: 3, done: false}
console.log(gen.next()) // {value: undefined, done: true}

function后加一个*,就可以创建一个Generator函数,这是它与普通函数的区别。
用一个变量接收Generator函数,便可以通过调用next方法去依次获取yield返回的值。

13.2 next方法

next的方法的返回值是一个对象。

  • done:为布尔类型,表示是否迭代完成。如果还有迭代器超过了迭代序列的末尾,则为true;如果迭代器可以生成下一个值,则为false
  • value:代表的是在函数中给定的返回值。当done的值为true时,该值为undefined

next方法是可以传参的,每个传入next的参数,会替代上一个yeild表达式返回的值。

function* createGen(x) {
  let y = yield x + 1
  console.log(y) // 7

  let z = yield y * 2
  console.log(z) // 8
}

let gen = createGen(90)

console.log(gen.next())
console.log(gen.next(7))
console.log(gen.next(8))

在上述代码中,给Generator函数传入一个90,执行next方法,y变为91,但其实是7,因为下调一个next方法传入了一个7,代替了上一个yeild表达式返回的值,同理,z就是8了。

这就可以看出,在第一个next方法传入参数是不起作用的,因为在此之前并没有执行yeild表达式,所以会被忽略。

13.3 return方法

return方法可以给一个值,并结束Generator函数,等同于在Generator函数中使用return。

像正常的使用next方法会这样。

function* createGen() {
  yield 1
  yield 2
  yield 3
}

let gen = createGen()

console.log(gen.next()) // {value: 1, done: false}
console.log(gen.next()) // {value: 2, done: false}
console.log(gen.next()) // {value: 3, done: false}

依次会是1,2,3。但使用了return方法就可以提前结束Generator方法,并返回给定的值。

function* createGen() {
  yield 1
  yield 2
  yield 3
}

let gen = createGen()

console.log(gen.next()) // {value: 1, done: false}
console.log(gen.return('over')) // {value: 'over', done: true}
console.log(gen.next()) // {value: undefined, done: true}

可以看到使用了return的地方,返回了{value: 'over', done: true},而且下一个next也并没有返回值,可知Generator函数被提前结束了。

return方法是可以调用多次的。但Generator函数会一直在结束状态。

13.4 throw方法

用于抛出异常。
如果在Generator函数内部没有try...catch,那么函数的运行会终止。

function* createGen() {
  yield 1
  yield 2
  yield 3
}

let gen = createGen()

console.log(gen.next()) // {value: 1, done: false}
console.log(gen.throw('error')) // error
console.log(gen.next()) // 不会运行

在Generator函数内部添加try...catch可以继续运行。

function* createGen() {
  try{
    yield 1
    yield  2
    yield 3
  }catch (e) {
    console.log(e) // error
  }
}

let gen = createGen()

console.log(gen.next()) // {value: 1, done: false}
console.log(gen.throw('error')) // {value: undefined, done: true}
console.log(gen.next()) // {value: undefined, done: true}

如果在Generator函数内部没有添加try...catch,而是在执行的时候添加,也会被捕捉到,但是接下来的next方法也不会执行。

function* createGen() {
  yield 1
  yield  2
  yield 3
}

let gen = createGen()

try {
  console.log(gen.next()) // {value: 1, done: false}
  console.log(gen.throw('error'))
  console.log(gen.next()) // 不会执行
} catch (e) {
  console.log(e) // error
}

13.5 异步处理

Generator函数是异步处理方案,那么怎么处理呢。

function getNumber(x, time) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x)
    }, time)
  })
}

function* createGen() {
  yield getNumber(1, 1500)
}

let gen = createGen()

gen.next().value.then(res => {
  console.log(res)
  return res
})

这段代码用setTimeout模拟异步,返回一个Promise对象,在next方法执行后,可以用then去接收并处理接下来的事务。

可以多添加几个异步任务。

// 模拟异步任务
function getNumber(x, time) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x)
    }, time)
  })
}

function* createGen() {
  yield getNumber(1, 1500)
  yield getNumber(2, 100)
  yield getNumber(3, 1000)
}

let gen = createGen()

gen.next().value.then(res => {
  console.log(res) // 1
  return res
})
  .then(res => {
    // 返回promise
    return gen.next().value
  })
  .then(res => {
    console.log(res) // 2
    // do sth
  })
  .then(res => {
    // 返回promise
    return gen.next().value
  })
  .then(res => {
    console.log(res) // 3
    // do sth
  })

越写越多,这种写法和回调函数感觉也差不了多少。

所以可以添加一个nextGen函数。

function nextGen(generator) {
  let gen = generator()

  function getNext(res) {
    let result = gen.next(res)
    if (result.done) return
    result.value
      .then(res => {
        console.log(res)
        getNext(res)
      })
  }
  getNext()
}

nextGen(createGen)

nextGen函数的作用就是每次执行完一个next,返回一个Promise对象,利用递归再次执行,一直到donetrue时停止。

14. async/await

14.1 描述

async在函数前使用,await在一个操作前使用,可以用同步的方式写出异步代码。

在使用了async的函数里面,可以使用0或无数个await
await在一个Promise对象或者需要去等待的值前面使用,返回Promise对象的执行结果,若非Promise对象,则返回该值本身。await无法在没有使用async的函数中使用。

await会暂停整个函数的执行进程并让出其控制权,只有当其等待的操作执行完毕之后才会恢复,Promise的解决值或者其他值会被当做该await的返回值。抛出的错误可以用try...catch捕捉。

function getNumber(x, time) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(x)
      resolve(x)
    }, time)
  })
}

async function run() {
  await getNumber(1, 1000) // 1
  await console.log(2) // 2
  await getNumber(3, 500) // 3
  await getNumber(4, 0) // 4
}

run()

async函数一定会返回一个Promise对象,一个非Promise对象也会被隐式地包装在Promise中。

async function func() {
  return 1
}

等同于:

function func() {
  return Promise.resolve(1)
}

14.2 与Promise

相比Promise来说,使用async/await会使代码更加简洁。

// 以前这么写
function getData() {
  ajax.get('http://url.com/test')
    .then(res => {
      return res.data
    })
}

// 现在这么写
async function getData() {
  const res = await ajax.get('http://www.url.com/test')
  return res.data
}
// 以前这么写
function getData() {
  ajax.get('http://www.url.com/test')
    .then(res => {
      if (res.status) {
        ajax.get('http://www.url.com/test2')
          .then(res => {
            return res
          })
      }
    })
}

// 现在这么写
async function getData() {
  const res = await ajax.get('http://www.url.com/test')
  if (res.status) {
    const res2 = ajax.get('http://www.url.com/test2')
    return res2.data
  }
}

15. Proxy

来自MDN:Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

15.1 创建Proxy代理

使用Proxy构造函数就可以创建一个代理对象,

  • 第一个参数为:需要代理的对象。
  • 第二个参数为:以函数为属性的对象,其中属性为函数,定义了各种在操作代理对象时的行为。
let proxy = new Proxy({}, {})

15.2 各种操作的方法

15.2.1 get

get用于拦截对象的读取属性的操作。

可以接收三个参数:

  • target:表示目标对象。
  • prop:想要获取的属性名称。
  • receiver:Proxy代理对象。 里面的this,是整个操作对象。
let obj = {
  name: '小明',
  age: 10
}

let proxy = new Proxy(obj, {
  get: function (target, prop, receiver) {
    console.log('get')
    return target[prop]
  }
})

// 操作代理对象才可以,操作原对象是不会触发给定的操作的
console.log(proxy.name); // get 小明
15.2.2 set

set用于拦截设置属性值操作。

可以接收四个参数:

  • target:表示目标对象。
  • prop:将要被设置的属性名。
  • value:想要设置的属性值。
  • receiver:最初被调用的对象。 里面的this,是整个操作对象。
let obj = {
  name: '小明',
  age: 10
}

let proxy = new Proxy(obj, {
  set(target, prop, value, receiver) {
    console.log('set')
    obj[prop] = value
  }
})

proxy.age = 11 // set
15.2.3 getPrototypeOf

当读取代理对象的原型的时候,该方法就会被调用。

可以接收一个参数:

  • target:表示目标对象。 有五种触发的方法:
  • Object.getPrototypeOf()
  • Reflect.getPrototyprOf()
  • __proto__
  • Object.prototype.isPrototypeOf()
  • instanceof 该方法的返回的值必须是一个对象或者null。

let obj = {
  name: '小明',
  age: 10
}

let proxy = new Proxy(obj, {
  getPrototypeOf(target) {
    console.log('getPrototypeOf')
    return Object.prototype
  }
})

// 以下操作可以触发
// 1. Object.getPrototypeOf
console.log(Object.getPrototypeOf(proxy))
// 2. Reflect.getPrototypeOf
console.log(Reflect.getPrototypeOf(proxy))
// 3. __ proto__
console.log(proxy.__proto__)
// 4. Object.prototype.isPrototypeOf
console.log(Object.prototype.isPrototypeOf(proxy))
// 5. instanceof
console.log(proxy instanceof Object)
15.2.4 setPrototypeOf

主要用于拦截Object.setPrototypeOf()。

可以接收两个参数:

  • target:代表目标对象。
  • prototype:对象的新原型或为null。 有返回值:如果成功修改了[[prototype]],会返回true,否则返回false
let obj = {
  name: '小明',
  age: 10
}

let proxy = new Proxy(obj, {
  setPrototypeOf(target, prototype) {
    console.log('setPrototypeOf')
    return true
  }
})

// 可拦截以下操作
// 1. Object.setPrototypeOf
Object.setPrototypeOf(proxy, Array.prototype)
// 2. Reflect.setPrototypeOf
Reflect.setPrototypeOf(proxy, null)
15.2.5 isExtensible

用于拦截对象的Object.isExtensible()。

可以接收一个参数:

  • target:代表目标对象。 有返回值:必须返回一个布尔值或者可转化为布尔值的值。

Object.isExtensible() 方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。 Object.preventExtensions()可创建一个无法扩展的对象。

let obj = {
  name: '小明',
  age: 10
}

let proxy = new Proxy(obj, {
  isExtensible(target) {
    console.log('isExtensible')
    return true
  }
})

console.log(Object.isExtensible(proxy)) // isExtensible true
15.2.6 preventExtensions

用于设置Object.preventExtensions()的拦截。

可以接收一个参数:

  • target:表示目标对象。 有返回值:布尔值,如果目标是可扩展的,那么只能返回false
let obj = {
  name: '小明',
  age: 10
}

let proxy = new Proxy(obj, {
  preventExtensions(target) {
    console.log('preventExtensions')
    // 将当前目标对象设置为不可扩展,否则将会报错
    Object.preventExtensions(target)
    return true
  }
})

console.log(Object.preventExtensions(proxy)) // preventExtensions
15.2.7 getOwnPropertyDescriptor

用于设置Object.getOwnPropertyDescriptor()的拦截。

可以接收两个参数:

  • target:表示目标对象。
  • prop:属性名。 有返回值:必须返回一个对象或者undefined。
let obj = {
  name: '小明',
  age: 10
}

let proxy = new Proxy(obj, {
  getOwnPropertyDescriptor(target, prop) {
    console.log('getOwnPropertyDescriptor')
    return {configurable: true, value: '小明'}
  }
})

console.log(Object.getOwnPropertyDescriptor(proxy, 'name'))
// getOwnPropertyDescriptor 
// {value: '小明', writable: false, enumerable: false, configurable: true}
15.2.8 defineProperty

用于拦截Object.defineProperty()操作。

可以接收两个参数:

  • target:表示目标对象。
  • prop:属性名。
  • desc:待定义或者修改的属性的描述。
    • enumerable
    • configurable
    • writable
    • value
    • get
    • set 有返回值:布尔值,表示操作是否成功。
let obj = {
  name: '小明',
  age: 10
}

let proxy = new Proxy(obj, {
  defineProperty(target, prop, desc) {
    console.log('defineProperty')
    return true
  }
})

Object.defineProperty(proxy, 'gender', {
  configurable: true,
  enumerable: true,
  value: '男'
}) // defineProperty
15.2.9 has

是针对in操作符的代理方法。

可以接收两个参数:

  • target:目标对象。
  • prop:需要检测是否存在的属性名。 有返回值:布尔值。
let obj = {
  name: '小明',
  age: 10
}

let proxy = new Proxy(obj, {
  has(target, prop) {
    console.log('has')
    return true
  }
})

console.log('name' in obj)
15.2.10 deleteProperty

用于拦截对象属性的delete操作。

可以接收两个参数:

  • target:目标对象。
  • prop:需要删除的属性名。 有返回值:布尔值,表示是否删除成功。
let obj = {
  name: '小明',
  age: 10
}

let proxy = new Proxy(obj, {
  deleteProperty(target, prop) {
    console.log('deleteProperty')
    delete target[prop]
    return true
  }
})

delete proxy.name // deleteProperty
15.2.11 ownKeys

用于拦截Reflect.ownKeys()。

可以接收以下参数:

  • target:目标对象。 有返回值:必须返回一个可枚举对象(包含String或Symbol的数组)。
let obj = {
  name: '小明',
  age: 10
}

let proxy = new Proxy(obj, {
  ownKeys(target) {
    console.log('ownKeys')
    return ['a', 'b', Symbol('s')]
  }
})

// 以下操作可以触发
// 1. Object.getOwnPropertyNames
console.log(Object.getOwnPropertyNames(proxy));
// 2. Object.getOwnPropertySymbols
console.log(Object.getOwnPropertySymbols(proxy))
// 3.Object.keys
console.log(Object.keys(proxy))
// 4. Reflect.keys
console.log(Reflect.ownKeys(proxy))
15.2.12 apply

用于拦截函数的调用。

可以接收以下参数:

  • target:目标对象(必须是一个函数)。
  • thisArg:被调用时的上下文。
  • argsList:被调用时的参数数组。 有返回值:可返回任意值。
function say(name) {
  console.log(`你好,我是${name}`)
}

let proxy = new Proxy(say, {
  apply(target, thisArg, argsList) {
    console.log('apply')
  }
})

proxy('小明')
15.2.13 construct

用于拦截new操作符。

可以接收以下参数:

  • target:目标对象,必须是可以new的。
  • argsList:参数列表。
  • newTarget:最初被调用的构造函数。 有返回值:必须返回一个对象。
function Person(name, age) {
  this.name = name
  this.age = age
}

let proxy = new Proxy(Person, {
  construct(target, argsList, newTarget) {
    console.log('construct')
    return new target(...argsList)
  }
})

console.log(new proxy('小明', 10));

15.3 应用

15.3.1 为对象设置默认值
let obj = {
  name: '小明',
  age: 10
}

console.log(obj.gender)

获取一个对象中不存在的属性值,会获得undefined,如果想在获取不存在的属性时设置一个默认值,可以使用proxy。

let obj = {
  name: '小明',
  age: 10
}

let proxy = new Proxy(obj, {
  get(target, prop, receiver) {
    return prop in target ? target[prop] : '默认值'
  }
})

console.log(proxy.name, proxy.gender) // 小明 默认值
15.3.2 对表单数据进行验证

假设formData是一个表单的值,每次表单中输入框的值改变,会重新设置formData中的值,那么可以通过proxy对表单的值进行验证,做出一些限制。

let formData = {
  name: '小明',
  age: 10
}

let validator = {
  name(value) {
    return {
      result: value.length < 5,
      err: value.length < 5 ? '' : 'error: name'
    }
  },
  age(value) {
    return {
      result: typeof value === 'number',
      err: typeof value === 'number' ? '' : 'error: age'
    }
  }
}

let proxy = new Proxy(formData, {
  set(target, prop, value, receiver) {
    let {result, err} = validator[prop](value)
    if (!result) alert(err)
    target[prop] = value
  }
})

proxy.name = '小智'
proxy.age = '11' // 报错 error: age

16. Reflect

Reflect是一个内置对象,提供了拦截js操作的方法,与proxy内的操作方法相同。
因为Reflect并非一个构造函数,所以不能通过new调用。

16.1 Reflect.apply

Reflect.apply()调用目标函数,并传入指定的参数列表。

可以接收三个参数:

  • target:目标函数对象。
  • thisArg:target函数调用时绑定的对象。
  • argsList:参数列表,为一个数组或者类数组。 这个方法类似于Function.prototype.apply(),绑定this之后去执行函数。
function func(a, b) {
  return a + b
}

console.log(Reflect.apply(func, null, [1, 2])); // 3

let name = '小明'

let obj = {
  name: '小智',
  sayName() {
    return '我是' + this.name
  }
}

console.log(Reflect.apply(obj.sayName, window, [...name])) // 我是小明

16.2 Reflect.construct

Reflect.construct()相当于new操作符,可以调用构造函数。

可以接收三个参数:

  • target:构造函数,必须是。
  • argsList:参数列表。
  • newTarget:作为新创建对象的原型对象的constructor属性,默认值为target,可不传。
function Person(name, age) {
  this.name = name
  this.age = age
}

// new
let xm = new Person('小明', 10)
// Reflect.construct
let xz = Reflect.construct(Person, ['小智', 11])

16.3 Reflect.defineProperty

Reflect.defineProperty()等同于Object.defineProperty(),会返回一个布尔值,表示操作是否成功。 可以接收三个参数:

  • target:目标对象。
  • prop:需要定义或修改的属性名。
  • attr:需要定义或修改的属性描述。

相比于Object.defineProperty()Reflect.defineProperty()在指定属性未被成功定义的时候,不会抛出错误,而是返回false,如果未使用try...catch,保证了程序不被中断。

let obj = {
  name: '小明'
}

// Object.defineProperty
Object.defineProperty(obj, 'age', {
  configurable: true,
  enumerable: true,
  value: 10
})

// Reflect.defineProperty
Reflect.defineProperty(obj, 'name', {
  value: '小智'
})

console.log(obj) // {name: '小智', age: 10}

16.4 Reflect.deleteProperty

Reflect.deleteProperty用于删除对象的指定属性,会返回一个布尔值,表示是否删除成功,功能类似于delete操作符。

可以接收两个参数:

  • target:目标对象。
  • propKey:需要删除的属性名称。
let obj = {
  name: '小明',
  age: 10
}

Reflect.defineProperty(obj, 'gender', {
  configurable: false,
  value: '男'
})

console.log(Reflect.deleteProperty(obj, 'age')) // true
console.log(Reflect.deleteProperty(obj, 'gender')) // false

16.5 Reflect.get

Reflect.get() 方法与从对象 (target[propKey]) 中读取属性类似,但它是通过一个函数执行来操作的。

可以接收三个参数:

  • target:目标对象。
  • propKey:需要获取的属性名称。
  • receiver:如果target对象中指定了getter(读取函数),receiver则为getter调用时的this值。
let obj = {
  name: '小明',
  age: 10,
  get sayName() {
    return '我是' + this.name
  }
}

let obj2 = {
  name: '小智'
}

console.log(Reflect.get(obj, 'age')) // 10
console.log(Reflect.get(obj, 'sayName', obj2)) // 我是小智

16.6 Reflect.set

Reflect.set() 方法就是在对象上设置一个属性。

可以接收四个参数:

  • target:目标对象。
  • propKey:需要获取的属性名称。
  • value:设置的值。
  • receiver:如果target对象中指定了setter(赋值函数),receiver则为setter调用时的this值。
let obj = {
  name: '小明',
  set setName(value) {
    console.log(value)
    return this.name = value
  }
}


let obj2 = {}

Reflect.set(obj, 'age', 10)
console.log(obj) // {name: '小明', age: 10}

Reflect.set(obj, 'setName', '小智', obj2) // 小智
console.log(obj2) // {name: '小智'}

16.7 Reflect.getOwnPropertyDescriptor

Reflect.getOwnPropertyDescriptort() 方法返回给定的属性的属性描述符,如属性不存在,则返回undefined

可以接收两个参数:

  • target:目标对象,必须是一个对象,否则会报错。
  • propKey:需要获取的属性描述符的属性名称。
let obj = {}

Reflect.defineProperty(obj, 'name', {
  configurable: true,
  enumerable: true,
  writable: false,
  value: '小明'
})

Reflect.defineProperty(obj, 'age', {
  configurable: false,
  enumerable: true,
  writable: false,
  value: 10
})

console.log(Reflect.getOwnPropertyDescriptor(obj, 'name')) 
// {value: '小明', writable: false, enumerable: true, configurable: true}

console.log(Reflect.getOwnPropertyDescriptor(obj, 'age')) 
// {value: 10, writable: false, enumerable: true, configurable: false}

16.8 Reflect.getPrototypeOf

Reflect.getPrototypeOf() 方法返回对象的原型,即[[Prototype]]属性的值。

可以接收一个参数:

  • target:目标对象,必须是一个对象,否则会报错。
let obj = {
  name: '小明'
}

console.log(Reflect.getPrototypeOf(obj) === Object.prototype) // true

16.9 Reflect.has

Reflect.has() 方法与in操作符相同,返回一个布尔值表示属性是否存在。

可以接收两个参数:

  • target:目标对象,必须是一个对象,否则会报错。
  • propKey:需要检查是否存在的属性名。
let obj = {
  name: '小明'
}

console.log(Reflect.has(obj, 'name')) // true
console.log(Reflect.has(obj, 'age')) // false

16.10 Reflect.isExtensible

Reflect.has() 方法可判断一个对象是否可以扩展(即是否能添加新的属性),会返回一个布尔值表示是否可以扩展。

可以接收一个参数:

  • target:目标对象,如果不是对象,会报错。
let obj = {
  name: '小明'
}

console.log(Reflect.isExtensible(obj)) // true

// 使用Object.freeze冻结对象
Object.freeze(obj)
console.log(Reflect.isExtensible(obj)) // false

16.11 Reflect.preventExtensions

Reflect.preventExtensions() 方法会使对象变得不可扩展,会返回一个布尔值表示目标对象是否成功设置为不可扩展。与Object.preventExtensions()类似。

可以接收一个参数:

  • target:目标对象,如果不是对象,会报错。
let obj = {
  name: '小明',
}

// 属性可以添加
obj.age = 10

// 检测到可以扩展
Reflect.isExtensible(obj) // true

// 使用Reflect.preventExtensions禁止扩展
Reflect.preventExtensions(obj)

// 检测到无法扩展
Reflect.isExtensible(obj) // false

console.log(obj) // {name: '小明', age: 10}

16.12 Reflect.ownKeys

Reflect.ownKeys() 方法返回一个由目标对象自身的属性名组成的数组。

可以接收一个参数:

  • target:目标对象,如果不是对象,会报错。
let obj = {
  name: '小明',
  age: 10,
  [Symbol('gender')]: '男'
}

let arr = [1, 2, 3]

console.log(Reflect.ownKeys(obj)) // ['name', 'age', Symbol(gender)]

console.log(Reflect.ownKeys(arr)) // ['0', '1', '2', 'length']

16.13 Reflect.setPrototypeOf

Reflect.setPrototypeOf() 方法可将对象的原型,即[[Prototype]]属性设置为另一个对象或null,返回一个布尔值表示操作是否成功。

可以接收两个参数:

  • target:目标对象,如果不是对象,会报错。
  • prototype:新原型。
let obj = {
  name: '小明'
}

console.log(Reflect.setPrototypeOf({}, Array.prototype)) // true

console.log(Reflect.setPrototypeOf([], Object.prototype)) // true

console.log(Reflect.setPrototypeOf([], null)) // true

console.log(Reflect.setPrototypeOf(Object.freeze({}), null)) // false

console.log(Reflect.setPrototypeOf(obj, Object.create(obj))) // false

17. 数组新方法

17.1 Array.from

Array.from用将一个类数组或可迭代对象创建一个新的,浅拷贝的数组实例。

可以接收三个参数:

  • arrLike:类数组或可迭代对象。
  • callback:可选,回调函数,每个新数组中的元素都会执行这个函数。
  • thisArg:可选,执行回调函数callback时的this对象。

可将字符串转化为数组。

let str = '123456'

console.log(Array.from(str)) // ['1', '2', '3', '4', '5', '6']

或是将参数列表这样的类数组转化为数组,并指定回调函数。

function add() {
  return Array.from(arguments, (item) => {
    return item * 2
  })
}

console.log(add(1, 2, 3, 6, 5)) // [2, 4, 6, 12, 10]

17.2 Array.of

Array.of方法创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型。
可以接收一个参数: eleNum:任意个数的参数,他们将会按顺序成为新数组中的元素。

Array.of(1);         // [1]
Array.of(1, 2, 3);   // [1, 2, 3]
Array.of(undefined); // [undefined]

17.3 Array.prototype.includes

includes()方法用来判断一个数组是否包含一个指定的值,返回一个布尔值,表示是否包含。

let arr = [1, 2, 3]

console.log(arr.includes(1)) // true
console.log(arr.includes(4)) // false

17.4 Array.prototype.copyWithin

coptWith方法会浅复制数组的一部分到同一数组的另一个位置,并返回他,不会改变原数组的长度,只会改变数组的内容。

可以接收三个参数:

  • index:索引需要复制到的位置。
  • startIndex:开始复制元素的起始位置。为负数则从末尾开始计算,若被忽略则从0开始。
  • endIndex:开始复制元素的结束位置,不会包含这个位置元素。为负数则从末尾开始计算,若被忽略则会一直复制到数组结尾(arr.length)。

endIndex参数需要注意,因为是不包含它的。

[1, 2, 3, 4, 5].copyWithin(0, 1, 3) // [2, 3, 3, 4, 5]
  
[1, 2, 3, 4, 5].copyWithin(0, 1, 4) // [2, 3, 4, 4, 5]

17.5 Array.prototype.find / Array.prototype.findIndex

find() 方法返回满足条件的第一个值,否则返回 undefined
findIndex() 方法返回满足条件的第一个值的索引,否则返回-1

可以接收两个参数:

  • callback:回调函数,可以接收三个参数:
    • item:当前遍历到的元素
    • index:当前遍历到的索引
    • array:数组本身
  • thisArg:执行回调函数时用作this的对象。
let arr = [
  {name: '小明', age: 10},
  {name: '小智', age: 11}
]

console.log(arr.find((item, index, array) => {
  return item.name === '小明'
})) // {name: '小明', age: 10}

console.log(arr.findIndex((item, index, array) => {
  return item.name === '小智'
})) // 1

17.6 Array.prototype.fill

find() 方法用于将一个固定值填充一个数组给定的范围内的所有元素,不包括结束索引。

可以接收三个参数:

  • value:用于填充的值。
  • startIndex:起始索引,默认为0。
  • endndex:结束索引,默认为数组的length。
[1, 2, 3].fill(4) // [4, 4, 4]

[1, 2, 3].fill(4, 1) // [1, 4, 4]

[1, 2, 3].fill(4, 1, 2) // [1, 4, 3]

[1, 2, 3].fill() // [undefined, undefined, undefined]

17.7 Array.prototype.keys

keys() 方法将返回一个以索引为元素的可遍历对象。

let arr = [11, 22, 33]

for (let k of arr.keys()){
  console.log(k) // 依次输出 1 2 3
}

17.8 Array.prototype.values

values() 方法将返回一个以值为元素的可遍历对象。

let arr = [11, 22, 33]

for (let k of arr.values()){
  console.log(k) // 依次输出 11 22 33
}

17.9 Array.prototype.entries

entries() 方法将返回一个以值与索引为元素的可遍历对象。

let arr = [11, 22, 33]

for (let key of arr.entries()) {
  console.log(key) // [0, 11]
                   // [1, 22]
                   // [2, 33]
}

17.10 Array.prototype.flat

flat()方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

可接受一个参数:

  • depth:可选,指定要提取嵌套数组的深度,默认为1,传入Infinity,可展开任意深度的嵌套数组。
let arr = [1, [2, 3, [4, 5]]]

console.log(arr.flat()) // [1, 2, 3, [4, 5]]

console.log(arr.flat(Infinity)) // [1, 2, 3, 4, 5]

flat()方法还会移除数组中的空项。

let arr = [1, [2, , 3, [4, null, 5, [6, undefined, 7, , 8]]]]

console.log(arr.flat(Infinity)) // [1, 2, 3, 4, null, 5, 6, undefined, 7, 8]

17.11 Array.prototype.flatMap

flaMapt()方法首先会使用map()遍历每一个元素,执行回调函数,将结果压缩成一个新数组,接着使用depth为1的flat()返回一个新数组,不会改变原数组。

可接受两个参数:

  • callback:回调函数,可生成一个新数组中的元素的函数,可接受三个参数:
    • currentValue:当前遍历到的元素。
    • index:可选,当前遍历到的元素的索引。
    • array:可选,当前数组。
  • thisArg:可选,执行回调函数时的this。
let arr = [1, 2, 3, 4, 5, 6]

arr.flatMap(item => {
  return item + 1
}) // [2, 3, 4, 5, 6, 7]

arr.flatMap(item => {
  return [item + 1]
}) // [2, 3, 4, 5, 6, 7]

arr.flatMap(item => {
  return [[item + 1]]
}) // [[2], [3], [4], [5], [6], [7]]

18. 对象新方法

18.1 Object.values

Object.values()方法会返回一个指定对象的可枚举属性值的数组。

会过滤Symbol为键的属性,不可枚举属性的值也不会出现。

let obj = {
  name: '小明',
  age: 10,
  [Symbol('s')]: '哈哈'
}

Reflect.defineProperty(obj, 'gender', {
  value: '男',
  enumerable: false
})

// 
console.log(Object.values(obj)) // ['小明', 10]

当使用数字作为对象的键时,遍历的顺序会根据数组的大小来,这与使用 for...in 的顺序相同。

let obj2 = {
  10: 'a',
  5: 'b',
  100: 'c',
  1: 'd'
} 

console.log(Object.values(obj2)) // ['d', 'b', 'a', 'c']

当对字符串使用时,会将它转化为对象。

// 
Object.values('一段文字') // ['一', '段', '文', '字']

对数字和布尔值使用,会返回空数组。

Object.values(123) // []
Object.values(true) // []

对null和undefined使用会报错。

Object.values(null) // 报错
Object.values(undefined) // 报错

18.2 Object.entries

Object.entries()方法返回一个指定对象的可枚举属性的键值对数组。性质与Object.values()类似。

let obj = {
  name: '小明',
  age: 10,
  [Symbol('s')]: '哈哈'
}

Reflect.defineProperty(obj, 'gender', {
  value: '男',
  enumerable: false
})

// 会过滤Symbol为键的属性,不可枚举属性的值也不会出现
console.log(Object.entries(obj)) // [['name', '小明'], ['age', 10]]

18.3 Object.fromEntries

Object.fromEntries()方法把键值对列表(可迭代对象)转换为一个对象。

Object.fromEntries([
  ['name', '小明'],
  ['age', 10]
]) // {name: '小明', age: 10}

Object.fromEntries(new Map([
  ['a',1],
  ['b',2]
])) // {a: 1, b: 2}

18.4 Object.getOwnPropertyDescriptor / Object.getOwnPropertyDescriptors

Object.getOwnPropertyDescriptor()方法返回指定对象上一个自有属性对应的属性描述符。

可以接收两个参数

  • obj:需要查找的对象
  • prop:可选,需要查找的对象的属性名,不传时返回所有属性的属性描述符。
let obj = {
  name: '小明',
  age: 10,
  [Symbol('s')]: '哈哈'
}

Reflect.defineProperty(obj, 'gender', {
  value: '男',
  writable: true,
  enumerable: false
})

console.log(Object.getOwnPropertyDescriptor(obj, 'gender')) // {value: '男', writable: true, enumerable: false, configurable: false}

Object.getOwnPropertyDescriptors()方法返回指定对象上所有的属性描述符。

let obj = {
  name: '小明',
  age: 10,
  [Symbol('s')]: '哈哈'
}

Reflect.defineProperty(obj, 'gender', {
  value: '男',
  writable: true,
  enumerable: false
})

console.log(Object.getOwnPropertyDescriptors(obj).gender.enumerable) // false

18.5 Object.assign

Object.assign() 方法会拷贝源对象自身的并且可枚举的属性到目标对象,最后返回目标对象。

可以接收两个参数:

  • target:目标对象。
  • sources:源对象,可以传多个。
let obj1 = {
  name: '小明',
}

let obj2 = {
  gender: '男'
}

let obj3 = {
  age: 10
}

console.log(Object.assign(obj1, obj2, obj3)) // {name: '小明', age: 10, gender: '男'}

console.log(obj1) // {name: '小明', gender: '男', age: 10}

18.6 Object.defineProperties

Object.defineProperties()方法与Object.defineProperty()类似,可以直接在一个对象上定义新的属性或修改现有属性,并返回该对象。它可以批量修改。

let obj = {}

Object.defineProperties(obj, {
  name: {
    value: '小明'
  },
  age: {
    value: 10,
    configurable: true
  }
})

console.log(obj) // {name: '小明', age: 10}

19. 字符串新方法

19.1 String.prototype.padStart

padStart()方法用另一个字符串填充当前字符串(如果需要的话,会重复多次),以便产生的字符串达到给定的长度。从当前字符串的左侧开始填充。

可以接收两个参数:

  • targetLength:需要填充到的目标长度。如果小于当前字符串的长度,则会返回当前字符串。
  • padString:用于填充的字符串。会被重复或截断。
let str = '123'

console.log(str.padStart(6, 'ab')) // aba123

19.2 String.prototype.padEnd

padEnd()方法与padStart()类似,从右侧开始填充。

let str = '123'

console.log(str.padEnd(6, 'ab')) // 123aba

19.3 String.prototype.trimStart / trimEnd

这两个方法可以去除字符串开头或者结尾的空格。
trimStart可写作trimLeft
trimEnd可写作trimRight

let str = '  123  '

console.log(str.trimStart()) // '123  '
console.log(str.trimEnd()) // '123'

20. Class类

class声明创建一个基于原型继承的具有给定的名字的新类。

在以前,想要一个实例对象,需要先定义一个构造函数,需要这么写。

function Person(name, age) {
  this.name = name
  this.age = age
}

Person.prototype.sayHi = function () {
  console.log('hi,我是' + this.name);
}

let xm = new Person('小明',10)
xm.sayHi()

有了class之后,可以这么写。

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }

  sayHi() {
    console.log('hi,我的名字是:' + this.name)
  }
}

let xm = new Person('小明', 10)
xm.sayHi()

class可看做一个语法糖,使对象原型的写法看上去更清晰,更像是面向对象的语法。

20.1 constructor

constructor就是构造方法。

当通过new命令生成实例对象的时候,会调用该方法。如未定义,会默认添加一个空的constructor方法。

// 这两个是一样的
class Person{}

class Person{
  constructor() {
  }
}

其中的this关键字代表实例对象。constructor方法默认返回实例对象。

类的方法都会定义在prototype属性上。

20.2 静态方法

当在类中的一个方法前面加上了static之后,该方法就不会被实例继承,只能通过类本身来调用,这就称为静态方法。

class Person {
  funcA(){
    console.log('a')
  }

  static funcB(){
    console.log('b')
  }
}

let xm = new Person()

xm.funcA() // a
Person.funcB() // b
xm.funcB() // xm.funcB is not a function

在静态方法中有this,表示的是这个类,而不是实例。

class Person {
  funcA() {
    console.log(this)
  }

  static funcB() {
    console.log(this)
  }
}

Person.funcB() // class Person {}

20.3 静态属性

静态属性与静态方法类似,在属性前加上static,只能通过类来访问。

class Person {
  // 定义静态属性num
  static num = 1

  constructor(num) {
    this.num = num
    console.log(Person.num) // 1
    console.log(this.num) // 2
  }
}

let per = new Person(2)

console.log(Person.num) // 1

20.4 继承

类可以通过extends关键字实现继承。

class Animal {
  constructor(name) {
    this.name = name
  }
}

class Cat extends Animal {
  constructor(name, sound) {
    super(name)
    this.sound = sound
  }

  say() {
    console.log(this.sound)
  }
}

let cat = new Cat('猫', 'miao')

cat.say() // miao

其中的super代表父类的构造函数,子类必须在constructor中调用此方法,否则会报错,因为子类的对象是通过super得到父类的属性和方法,然后加上子类的属性和方法,如不调用,子类获取不了this
但是子类不写constructor方法的话,会默认调用这个方法。

class Animal {
  constructor(name) {
    this.name = name
  }
}

class Cat extends Animal {}

let cat = new Cat('猫')

console.log(cat.name) // 猫

20.5 super

super关键字用于访问和调用一个对象的父对象上的函数。

20.5.1 在类中使用super
class Animal {
  constructor(name) {
    this.name = name
  }
}

class Cat extends Animal {
  constructor(name, age) {
    super(name);
    this.age = age
  }
}

let cat = new Cat('猫', 1)

console.log(cat) // '猫'
20.5.2 可以用于访问父类的静态方法

此时的super是指向父类的。

class Animal {
  constructor(name) {
    this.name = name
  }

  static say() {
    console.log(`hihi`)
  }
}

class Cat extends Animal {
  constructor(name, age) {
    super(name);
    this.age = age
  }

  static catSay() {
    super.say()
  }
}

Cat.catSay() // hihi
20.5.3 可以访问父类的普通方法

此时的super是指向父类的原型对象的。

class Animal {
  constructor(name) {
    this.name = name
  }

  say() {
    console.log(`hihi`)
  }
}

class Cat extends Animal {
  constructor(name, age) {
    super(name);
    this.age = age
  }

  catSay() {
    super.say()
  }
}

let cat = new Cat('猫', 2)
cat.catSay() // hihi

20.6 set / get

在类的内部,可以使用setget关键字,对某个属性设置存取行为的拦截。

class Person {
  constructor(name) {
    this.name = name
  }

  get getName() {
    console.log(`getter: ${this.name}`);
    return this.name
  }

  set setName(value) {
    console.log(`setter: ${value}`)
    this.name = '小智'
  }
}

let xm = new Person('小明')

xm.getName // getter: 小明

xm.setName = 1 // setter: 1

console.log(xm.name) // 小智

20.7 其他

根据class语法,可以简单地实现一个webSocket连接方法。

class WebSocketConnect {
  constructor(url) {
    this.url = url
    this.socket = null
    this.timer = null
  }

  // 创建连接
  create = () => {
    this.socket = new WebSocket(this.url)
    console.warn('连接')
    this.open()
  }

  // 连接之后
  open = () => {
    this.socket.onopen = () => {
      this.sendPing()
    }
  }

  // 接收信息
  msg = (cb) => {
    this.socket.onmessage = function (data) {
      cb(data)
    }
  }

  // 发送信息
  send = (data) => {
    this.socket.send(JSON.stringify(data))
  };

  // 连接错误,重连
  error = () => {
    this.socket.error = () => {
      console.warn('连接错误')
      clearInterval(this.timer)
      this.close()
      this.create()
    }
  }

  // 关闭连接
  close = () => {
    this.send({msg: 'close'})
    this.socket.close()
    clearInterval(this.timer)
    console.warn('关闭')
  }

  // 心跳检测
  sendPing = () => {
    this.send('ping')
    this.timer = setInterval(() => {
      this.send('ping')
    }, 3000)
  }
}

over。