JavaScript高级深入浅出:ES6-ES7 扩展语法(下)

716 阅读7分钟

介绍

本文是 JS 高级语法的第12篇,本篇将会承接上一篇,继续介绍 ES6-7 之间的新语法

正文

1. Set

1.1 Set 的基本使用

在 ES6 之前,我们储存数据的方式只有两种:数组和对象

  • 在 ES6 中新增了两种数据结构 SetMap,以及它们的另外形式 WeakSetWeakMap

Set 是一个新增的数据结构,可以用于保存数据,类似于数组,但是区别在于元素不能重复

  • 创建 Set 需要用到 Set 的构造函数(暂时没有字面量的创建方法)Set 的构造函数可以传入一个可迭代的对象
const nums = [1, 2, 3, 4, 1, 2, 3, 6, 4, 8]

const s1 = new Set(nums)

// 可以利用 Set 的特性来给数组去重
console.log(s1)

使用 add 方法来向 Set 的实例中添加数据

const s1 = new Set()

s1.add(1)
s1.add(1)
s1.add(2)

// Set(2) { 1, 2 }
console.log(s1)
  • Set 是通过内存地址来判断是否相同的
const s1 = new Set()
s1.add({})
s1.add({})

const arr = []
s1.add(arr)
s1.add(arr)

// Set(3) { {}, {}, [] }
console.log(s1)
  • 数组去重小案例(Set 也支持展开运算符)
const arr = [1, 2, 1, 2, 3, 4, 4, 5]

// 没有 set
const newArr = []
for (const item of arr) {
  if (newArr.indexOf(item) === -1) {
    newArr.push(item)
  }
}
console.log(newArr)

// 有了 set
const arrSet = new Set(arr)
// const newArr2 = Array.from(arrSet)
// 除了 Array.from,Set 也是支持展开运算符的
const newArr2 = [...arrSet]
console.log(newArr2)

1.2 Set 的常见方法

Set 的常见属性:

  • size:返回 Set 实例中元素的个数

Set 的常见方法:

  • add:向 Set 实例中添加元素
  • delete:向 Set 实例中删除元素,接收元素
  • has:判断 Set 实例中是否有传入的元素
  • clear:清除 Set 实例中的所有元素

对 Set 进行遍历:

  • forEach() 实例方法
  • for...of

2. WeakSet

和 Set 类似的另一个数据结构叫做 WeakSet,也是内部元素不可重复的数据结构

和 Set 的区别:

  • WeakSet 只能存放对象类型,不能存放基本数据类型
  • WeakSet 对元素的引用是弱引用,如果没有其他引用对这个对象进行引用,GC 可以对该对象进行回收

2.1 强引用与弱引用

Set 对于元素是强引用的,GC 会根据标记清除来清除掉无用的数据,而判断数据是否是无用数据,就是根据引用来进行判断的。

let obj = { name: 'alex' }

以上的代码,{ name: 'alex' }储存在堆内存中,而 VE(可以看作是词法环境) 中保存的 obj 其实是对于 { name: 'alex' } 的内存地址。这样 obj 对于 { name: 'alex' } 就有强引用,除非 obj = null,否则 GC 就永远不会清除 { name: 'alex' }

let obj = { name: 'alex' }

const s1 = new Set()
s1.add(obj)

以上的代码,Set 中对于{ name: 'alex' }有强引用,obj也有。

let obj = { name: 'alex' }

const s1 = new Set()
s1.add(obj)

obj = null
// 我们期望的是:删除了 obj 的引用之后
// 此时 Set 中该元素的引用也应该时没有了

// 显然,还是存在的,这和我们期望的情况是不太一样的
// 因为 Set 对于元素是强引用的
console.log(s1)

WeakSet 是弱引用,弱引用会存在一些问题,可能 WeakSet 中储存的数据已经被清除

2.2 WeakSet 常见的方法

  • add:添加某个元素,返回 WeakSet 实例本身
  • delete:WeakSet 实例中删除和传入参数的值相等的元素,返回 boolean
  • has:判断 WeakSet 实例中是否存在某个元素,返回 boolean

2.3 WeakSet 的应用

注意:WeakSet 不能遍历

  • 因为 WeakSet 只是对对象的弱引用,如果我们遍历获取到其中的元素,那么有可能造成该元素不能正常的销毁
  • 所以储存在 WeakSet 中的对象时没办法获取的

所以这个东西到底有没有应用场景?

Stack Overflow 的回答:

const pwset = new WeakSet()
class Person {
  constructor() {
    pwset.add(this)
  }
  running() {
    if (!pwset.has(this)) throw new Error('不能通过其他对象调用 running 方法')
    console.log('running', this)
  }
}

const p = new Person()

const obj = {}

p.running()
p.running.call(obj) // 报错

用于边界判断,如果非该实例调用running方法,就会报错

但是用Set能不能做?其实也是可以的,只不过这种情况下只是为了一个计数作用,因此使用强引用是不利于 GC 回收的,而 WeakSet 就很合适了。

3. Map

3.1 Map 的基本使用

另一个新增的数据结构是 Map,用于储存映射关系。

我们可能会问,Map 相对于 Object,有什么区别?

  • 事实上我们对象储存映射关系的键只能是字符串(ES6 新增了 Symbol)
  • 某些情况下,我们并不仅仅想让字符串作为键,此时就可以来使用 Map 了

使用 Object 能不能作为 Object 的 key,从语法来看,是不会报错的

let obj = { name: 'alex' }

let obj2 = {
  [obj]: 'alex2',
}

// 但实际上,这里的 obj,调用了 toString 才作为 key 的
// 因此实质上来说,Object 的 key 只能是字符串 / Symbol

那么我们就可以使用 Map 了

const map = new Map()
let obj1 = { name: 'alex' }
let obj2 = { name: 'tom' }

map.set(obj1, 'alex')
map.set(obj2, 'tom')

console.log(map)

Map 构造函数也可以传入参数,但是参数是一个entries,大概是这样的结构

const map = new Map([[key, value], [key, value], [key, value]])

3.2 常见的属性和方法

Map 常见属性:

  • size:返回 Map 实例中的元素个数

Map 常见方法:

  • set(key, value):向 Map 实例中储存映射数据
  • get(key):根据 key 返回 value
  • has(key):根据 key 判断是否存在
  • delete(key):根据 key 删除映射数据
  • clear():清空 Map 实例

对 Map 进行遍历:

  • forEach((value, key) => {}):forEach 接收一个函数,该函数接收两个参数,第一个是值,第二个是键

    map.forEach((value, key) => {
      console.log(value, key)
    })
    
  • for...of

    for (const item of map) {
      console.log(item) // 每一个 item 是一个数组,该数组结构 [key, value]
    }
    
    // 也可以直接使用解构
    for (const [key, value] of map) {
      console.log(key, value)
    }
    

4. WeakMap

4.1 WeakMap 的使用

和 Map 类型的另一个数据结构称为 WeakMap,也是以键值对的形式储存的

WeakMap 和 Map 的区别:

  • WeakMap 的 key 只能存放对象,不接受其他类型
  • WeakMap 的 key 对对象的引用是弱引用,如果没有其他引用这个对象,GC 就会回收

弱引用请参考 WeakSet 解释,主要是为了提高性能

4.2 WeakMap 常见方法

  • set(key, value):存放元素
  • get(key):获取数据
  • has(key):判断是否包含
  • delete(key):删除

4.3 WeakMap 应用场景

@vue/reactivity源码中就有用到 WeakMap

const foo = {
  name: 'alex',
}

const runner1 = () => {
  console.log('监听 name 执行1')
}
const runner2 = () => {
  console.log('监听 name 执行2')
}

const bar = {
  name: 'tom',
}

const runner3 = () => {
  console.log('监听 name 执行3')
}
const runner4 = () => {
  console.log('监听 name 执行4')
}

// 目的:foo.name 改变, runner1 runner2 执行
//      bar.name 改变, runner3 runner4 执行

// 这一步,就相当于储存依赖(具体可看 @vue/reactivity 源码实现)
const wmap = new WeakMap()
const map = new Map()
const set = new Set()
set.add(runner1)
set.add(runner2)
map.set('name', set)
wmap.set(foo, map)

// 大概是这样的结构
/**
 *  所有的对象都放在一个 WeakMap 中,wm 的 key 是每一个对象
 *  每一个 wm 的值对应的是一个 map,该 map 储存的是 { '属性名称': Set }
 *  Set 里面放所有跟这个属性有关的 runner
 */
// 模拟:例子:
// effect(() => {
//   // 假设 foo.name 发生了变化
// })

effect()

function effect() {
  // 获取对象对应的 map
  const map = wmap.get(foo)
  // 通过键来获取指定的 runnerSet
  const runnerSet = map.get('name')
  // 再依次执行所有的 runner
  runnerSet.forEach(item => {
    item()  // 成功执行 runner1 和 runner2
  })
}

为什么用 WeakMap,因为它是弱引用,如果使用了 Map,那么即使原始引用对象已被销毁,但是 Map 有引用,所以导致堆中的数据未销毁。在 Vue 中,例如我们销毁了组件(实际上就是销毁了数据对象),那么即使 WeakMap 中有引用,堆中的真实数据也会被销毁,提高了大量性能。

5. Array Includes

在 ES7 之前,如果想要判断数组中是否包含某个元素,只能使用array.indexOf(value) !== -1,实际上,这在语义上来说是有歧义的,而array.includes(value)则会更准确的描述做的事情(就是看数组是否包含某个值),返回true/false

const names = ['Alex', 'Tom', 'Jason', 'Alice', 'Andrew']

function _includes(arr, value) {
  if (arr.indexOf(value) !== -1) {
    return true
  }
  return false
}

console.log(_includes(names, 'Jason')) // true
console.log(_includes(names, 'Luna')) // false

// 使用 ES7 的 Array.prototype.includes()

console.log(names.includes('Jason')) // true
console.log(names.includes('Luna')) // false

includes还有第二个参数(From Index),该参数表示从指定的索引后开始查找:

const names = ['Alex', 'Tom']

console.log(names.includes('Alex')) // true
console.log(names.includes('Alex', 1)) // 从索引为 1 开始寻找,就找不到了,false

// 如果给定的 fromIndex >= 数组.length,则直接返回 false,并不搜索数组
console.log(names.includes('Alex', 3)) // false
// 如果给定的 fromIndex 是负值,那么就会计算 (数组.length + fromIndex) < 0 ? 
// 如果 < 0,则会查询整个数组,反之则是从计算出来的结果开始查找
// 1. names.length === 2, 2 + (-100) = -98 < 0 ,整个数组都会搜索
console.log(names.includes('Alex', -100)) // true
// 2. 2 + (-1) = 1 ,所以这里的 -1 就成了 1,所以是 false
console.log(names.includes('Alex', -1)) // false

includesindexOf区别如下:

  • 对于NaN的判断,indexOf无法判断 NaN,但是includes可以判断 NaN

    const arr = [NaN]
    console.log(arr.indexOf(NaN) !== -1) // false
    console.log(arr.includes(NaN)) // true
    
  • includes被设计为一个通用方法,因此类数组也可以使用该方法

    ;(function () {
      console.log([].includes.call(arguments, 'a')) // true
      console.log([].includes.call(arguments, 'd')) // false
      console.log(arguments.indexOf('a') !== -1) // 没有 indexOf 方法
    })('a', 'b', 'c')
    

6. 指数运算符

如果我们之前想要获取一个数的指数,需要用到 Math.pow()

console.log(Math.pow(3, 3))
// ES7
// 获取 3 的 3 次方
let result1 = 3 ** 3
console.log(result1)
// 获取 9 的 4 次方
let result2 = 9 ** 4
console.log(result2)

总结:

本文,承接上一篇,继续对于 ES6-ES7 之间的扩展语法进行讲解,包含了 4 种新增的数据结构:

  • Set
  • WeakSet
  • Map
  • WeakMap

同时,我们用WeakMap的例子来讲解了 Vue 3 响应式的核心

接着,我们介绍了 ES7 新增的语法:

  • Array.prototype.includes
  • 指数运算符 **

特别鸣谢

  • 催学社(崔大,cuixiaorui)