JS Advance --- ES6 ~ ES12语法 (三)

368 阅读8分钟

强引用和弱引用

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

一般我们定义一个变量等于一个对象的时候,可以认为该变量有一个引用指向了该变量对象

cosnt obj = { name: 'Klaus' }

此时就可以认为obj有一个引用指向了字面量({ name: 'Klaus' })

而这个引用默认情况下是一个强引用(strong reference)

也就是GC检测的时候,会识别强引用,从而认为该字面量并不是一个垃圾对象

而使用weakset/weakmap创建出来的值的引用,本质上其实是一个弱引用

也就是说,虽然我们依旧可以使用这个引用去存储,获取,访问对应的字面量

但是GC在进行垃圾回收的时候,并不‘认可’弱引用。

如果一个对象字面量没有任何的强引用指向的时候,即使该对象字面量有弱引用指向,GC依旧会将该对象字面量当做垃圾进行处理

set

在ES6之前,我们存储数据的结构主要有两种:数组、对象

在ES6中新增了另外两种数据结构:Set、Map,以及它们的另外形式WeakSet、WeakMap

set,map,weakset,weakmap这些新增的数据类型在本质上也就是一种特殊的对象类型数据

Set是一个新增的数据结构,可以用来保存数据,类似于数组

但是Set和数组最大的区别是: 数组中的元素是可以重复的,但是set中的元素是不可以重复的

所以set最大的功能是可以对数组元素进行去重操作

// 使用set构造函数创建set(set是没有字面量的创建形式的)
let set = new Set()

// set添加的时候,支持传入一个可迭代对象
set = new Set([1, 2, 3, 4, 5])
let set = new Set()

// 数据添加 --- 返回Set对象本身
set.add(1)
set.add(2)
set.add(1)

console.log(set) // => Set(2) {1, 2}

// 字面量创建对象,地址引用是不同的
set.add({})
set.add({})

console.log(set) // => Set(2) {1, 2, {}, {}}
let set = new Set([1, 2, 3, 4, 5])

// set的长度
console.log(set.size) // => 5

// 数据删除 --- 返回boolean类型
set.delete(1)

// 判断元素是否存在 --- 返回boolean类型
console.log(set.has(2)) // => true

// 清除set中的所有元素 --- 没有返回值
set.clear()
let set = new Set([1, 2, 3, 4, 5])

// set的遍历
// 1. forEach
set.forEach(item => console.log(item))

// 2. for ... of
// 在set中是没有key这个概念的,所以不可以使用for...in方法对set进行遍历
for (const item of set) {
  console.log(item)
}
// 数组去重
const arr = [1, 2, 3, 4, 5, 1, 3, 5]

// ES5中去重的方式
const newArr = []

arr.map(item => {
  if (newArr.indexOf(item) === -1) {
    newArr.push(item)
  }
})

console.log(newArr)

const arr = [1, 2, 3, 4, 5, 1, 3, 5]

// ES6 使用set
console.log(Array.from(new Set(arr)))
console.log([...new Set(arr)])

weakset

weakset是和Set类似的另外一个数据结构

weakset和set的区别:

  1. WeakSet中的值只能是对象类型,不能存放基本数据类型
  2. WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC会对该对象进行回收
// 创建
const ws = new WeakSet()

const obj = {}

// 添加元素 --- 返回WeakSet对象本身
ws.add(obj)

// 判断对象是否存在 --- 返回boolean类型
console.log(ws.has(obj)) // => true

// 移除元素 --- 返回boolean类型
ws.delete(obj)

注意: WeakSet不能遍历

WeakSet只是对对象的弱引用,我们并不能确定其中的某一个元素是否会在访问期间被销毁了

也就意味着说多次访问同一个weakset的返回结果可能是不一致的,此时对其中元素值的时候可能会存在问题

所以weakset并不允许使用forEachfor...of等方法进行遍历操作

// WeakSet不可以进行任何形式的遍历操作
// 所以WeakSet上不存在size属性或forEach方法
// 任何时候直接打印WeakSet输出的结果是WeakSet { <items unknown> },因为它不可以遍历取值
console.log(ws) // => WeakSet { <items unknown> }

应用场景

限制对象上的某一个方法只能通过对象的实例来进行调用

const ws = new WeakSet()

class Person {
  constructor() {
    ws.add(this)
  }

  running() {
    if (!ws.has(this)) {
      throw new Error('running方法只能被Person的实例调用')
    }

    console.log('running')
  }
}

const per = new Person()
per.running() // => running
per.running.call({ name: 'Klaus' }) // error

map

map和对象一样,可以用来存储映射关系

但是和对象不同的是,对象来存储键值对的时候,其键只能是字符串类型或Symbol类型的值

如果是其它类型,会将其转换为字符串类型后在作为对象的key

而map对象key没有任何的限制要求,可以使用任何的数据类型来作为map的key

const key = { usernmame: 'Klaus' }

const obj = {
  [key] : 'Kluas'
}

// { usernmame: 'Klaus' } 会自动调用toString方法 转换为 [object Object]
console.log(obj) // => { '[object Object]': 'Kluas' }
// 基本使用
let map = new Map()

// 在使用map构造方法创建的时候
// Map构造方法支持传入entries作为参数 格式类似于[[key, value], [key, value], [key, value], ....]
// 其中key可以是任意类型的数据
map = new Map([[{name: 'Klaus'}, 'aaa'], ['key', 'value'], [1, 3]])

console.log(map) // => Map(3) { { name: 'Klaus' } => 'aaa', 'key' => 'value', 1 => 3 }
const map = new Map([[{name: 'Klaus'}, 'aaa'], [1, 3]])

// 常见方法
// 设置属性 --- 返回整个Map对象
map.set('key', 'value')

// 获取map的长度
console.log(map.size) // => 3

// 判断某一个属性是否存在 --- 返回Boolean类型
console.log(map.has('key')) // => true

// 移除某一个属性 --- 返回Boolean类型
map.delete('key')

// 清空map
map.clear()
const map = new Map([[{name: 'Klaus'}, 'aaa'], ['key', 'value']])

// 遍历方法1
// forEach的时候,第一个参数的值是每一项的value值,第二个参数的值是每一项的key值
map.forEach((value, key) => console.log(key, value))

// 遍历方法2
// for ... of 遍历的时候,遍历出来的每一项的值是map中的entry对象,也就是[key, value]
// 所以需要通过数组解构的方式去获取对应的key和value的值
for (const [key, value] of map) {
  console.log(key, value)
}

weakMap

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

weakMap 和 map的区别

  • WeakMap的key只能使用对象,不接受其他的类型作为key
  • WeakMap的对key的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象
// 基本使用
// 定义
const wm = new WeakMap()

// WeakMap也支持使用entries对象作为参数进行创建
// const wm = new WeakMap([{name: 'key'}, 'value'])

const obj = {name: 'Klaus'}
// 设置值 --- 返回整个Map对象
wm.set(obj, 'Klaus')

// 获取值
// 获取值的时候,因为是对象,存储的是引用地址
// 所以获取值的时候,需要通过同一个对象的引用地址才可以获取到对应的值
console.log(wm.get(obj)) // => Klaus
console.log({ name: 'Klaus' }) // => undefined

// 判断某一个值是否存在 --- 返回Boolean类型
console.log(wm.has(obj)) // => true

// 删除某一个值 --- 返回Boolean类型
wm.delete(obj)

// 和WeakSet一样,WeakMap也是无法进行任何形式的遍历操作,且不存在size属性和forEach方法
// 直接打印会输出 WeakMap { <items unknown> },因为里面的值不可以被遍历获取
console.log(wm) // => WeakMap { <items unknown> }

使用场景

实现响应式功能(Vue3的响应式就是通过WeakMap来进行实现)

所谓响应式,是指当某个对象上的对应属性发生改变的时候,可以被检测到, 同时去执行对应的响应函数,完成对应的功能

// 这里只是对响应式进行简单的模拟 --- 伪代码和基本思路
const obj1 = {
  name: 'Klaus',
  age: 23
}

const obj2 = {
  name: 'Alex',
  age: 20
}

// obj1的name发生改变的时候,需要被触发的函数
// 可以认为这些函数是obj1.name的依赖,因为他们的执行需要依赖于obj1.name的值的改变
const obj1NameChangedFn1 = () => console.log('obj1NameChangedFn1')
const obj1NameChangedFn2 = () => console.log('obj1NameChangedFn2')

const obj1AgeChangedFn = () => console.log('obj1AgeChangedFn')

const obj2NameChangedFn1 = () => console.log('obj2NameChangedFn1')
const obj2NameChangedFn2 = () => console.log('obj2NameChangedFn2')

// 1. 使用WeakMap将对象的值和对应的函数进行关联

// 之所以最外层定义为WeakMap
// 是为了当obj1不在使用的时候,deps中key为obj1的key和value也可以在合适的时机被GC回收
const deps = new WeakMap()

// 内层的map之所以定义为Map而不是WeakMap
// 是因为对象的属性值是字符串类型的,而WeakMap的key必须是对象类型
const obj1Map = new Map()

deps.set(obj1, obj1Map)
obj1Map.set('name', [obj1NameChangedFn1, obj1NameChangedFn2])
obj1Map.set('age', [obj1AgeChangedFn])

const obj2Map = new Map()
deps.set(obj2, obj2Map)
obj2Map.set('name', [obj2NameChangedFn1, obj2NameChangedFn2])

// 2. 修改值的时候,去更新对应的依赖(函数)
obj1.name = 'Steven'
// obj1.name 发生更新的时候,只会更新obj1.name所对应的依赖函数
// 其它相关的依赖函数并不会被执行
deps.get(obj1).get('name').forEach(fn => fn())

ES7

includes

在ES7之前,如果我们想判断一个数组中是否包含某个元素,需要通过 indexOf 获取结果,并且判断是否为 -1

在ES7中,我们可以通过includes来判断一个数组中是否包含一个指定的元素,根据情况,如果包含则返回 true, 否则返回false

// ES7之前
const arr = [1, 2, 3, 4, 5, NaN]

// arr.indexOf(searchElement[, fromIndex])
console.log(arr.indexOf(2) !== -1) // => true
console.log(arr.indexOf(6) !== -1) // => false

// arr.indexOf(NaN) --> 返回值是-1 因为NaN !== NaN 横成立
console.log(arr.indexOf(NaN) !== -1) // => false
const arr = [1, 2, 3, 4, 5, NaN]

// arr.includes(valueToFind[, fromIndex])
console.log(arr.includes(1)) // => true
console.log(arr.includes(6)) // => false

// 特别的: 数组.includes方法 可以判定传入的参数NaN 全等于 数组中元素NaN
// 这是includes分发和indexOf方法之间最大的区别
console.log(arr.includes(NaN)) // => true

// 第二个参数是从第几个索引值开始查找
console.log(arr.includes(1, 2)) // => false

指数(乘方) exponentiation运算符

// ES7之前
console.log(Math.pow(3, 3)) // => 27

// ES7之后 ** 可以认为是Math.pow方法的语法糖
console.log(3 ** 3) // => 27