Skip to content

一. 展开运算符的使用


  • 展开语法(Spread syntax):

    • 可以在函数调用或数组构造时,将数组表达式 或 string在语法层面展开
    • 还可以在构造字面量对象时,将对象表达式按key-value的方式展开
  • 展开语法的场景:

    • 在函数调用时使用
    • 在数组构造时使用
    • 在构建对象字面量时,也可以使用展开运算符,进行克隆或属性拷贝,这个是在ES2018(ES9)中添加的新特性
  • 基本使用:

    js
    // 1.基本演练
    // ES6
    const names = [1, 2, 3]
    const newNames = [...names, 4, 5]
    console.log(newNames) // [1, 2, 3, 4, 5]
    
    const str = "Hello"
    console.log(...str) // H e l l o
    
    function foo(name1, name2, ...args) {
      console.log(name1, name2, args)
    }
    
    foo(...names) // 1 2 [3]
    foo(...str) // H e ['l', 'l', 'o']
    
    // ES9(ES2018)
    const obj = {
      name: "why",
      age: 18
    }
    // 不可以这样来使用
    // foo(...obj) 
    // 在函数的调用时, 用展开运算符, 将对应的展开数据, 进行迭代
    // 可迭代对象: 数组/string/arguments
    
    const info = {
      ...obj,
      height: 1.88,
      address: "广州市"
    }
    console.log(info) // {name: 'why', age: 18, height: 1.88, address: '广州市'}
  • 注意:展开运算符其实是一种浅拷贝

    js
    const obj = {
      name: "why",
      age: 18,
      height: 1.88,
      friend: {
        name: "curry"
      }
    }
    
    // 1.引用赋值 内存中的表现:info1:0x100  obj:0x100
    const info1 = obj
    
    // 2.浅拷贝 内存中的表现,只会对对象第一层的属性进行拷贝
    const info2 = {
      ...obj
    }
    info2.name = "kobe"
    console.log(obj.name) // why
    console.log(info2.name) // kobe
    info2.friend.name = "james"
    console.log(obj.friend.name) // james
    
    // 3.深拷贝
    // 方式一: 第三方库
    // 方式二: 自己实现 function deepCopy(obj) {}
    // 方式三: 利用现有的js机制, 实现深拷贝JSON,利用JSON.stringify转成一份JSON字符串,再用JSON.parse解析为js对应的数据类型
    const info3 = JSON.parse(JSON.stringify(obj))
    console.log(info3.friend.name) // curry
    info3.friend.name = "james"
    console.log(info3.friend.name) // james
    console.log(obj.friend.name) // curry

二. 进制和长数字的表示


  • ES6中规范了二进制和八进制的写法:

    js
    const num1 = 100   
    // binary
    const num2 = 0b100 // 二进制   以0b开头 
    // octonary 
    const num3 = 0o100 // 八进制   以0o开头
    // hexadecimal
    const num4 = 0x100 // 十六进制 以0x开头
    • 严格模式下,不允许早期以0开头的八进制写法
    • 打印的时候,浏览器为了方便观看,会自动转化为十进制
  • 另外在ES2021新增特性:数字过长时,可以在数字任意位置使用_作为连接符

    js
    const money = 1000000000000
    const money = 1_0000_0000_0000

三. Symbol类型用法


1. Symbol的基本使用

  • Symbol是什么呢?SymbolES6中新增的一个基本数据类型,翻译为符号

  • 那么为什么需要Symbol呢?

    • ES6之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突
    • 比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性
    • 比如我们前面在讲applycallbind实现时,我们有给其中添加一个fn属性,那么如果它内部原来已经有了fn属性了呢?
    • 比如开发中我们使用混入,那么混入中出现了同名的属性,必然有一个会被覆盖掉
  • Symbol就是为了解决上面的问题,用来生成一个独一无二的值

    • Symbol值是通过Symbol函数来生成的,生成后可以作为属性名
    • 也就是在ES6中,对象的属性名可以使用字符串,也可以使用Symbol
  • Symbol即使多次创建值,它们也是不同的:Symbol函数执行后每次创建出来的值都是独一无二的

  • 我们也可以在创建Symbol值的时候传入一个描述description:这个是ES2019(ES10)新增的特性

    js
    // ES6之后可以使用Symbol生成一个独一无二的值
    const s1 = Symbol()
    // const info = { name: "why" }
    const obj = {
      [s1]: "aaa" 
    }
    console.log(obj) // {Symbol(): 'aaa'}
    
    const s2 = Symbol()
    obj[s2] = "bbb"
    console.log(obj) // {Symbol(): 'aaa', Symbol(): 'bbb'}
    console.log(s1 == s2) // false
    console.log(s1 === s2) // false
    
    delete obj[s2]
    console.log(obj) // {Symbol(): 'aaa'}

2. Symbol作为属性名

  • 我们通常会使用Symbol在对象中表示唯一的属性名

    js
    const s1 = Symbol()
    const s2 = Symbol()
    const obj = {}
    // 写法一:属性名赋值
    obj[s1] = 'abc'
    obj[s2] = 'nba'
    
    // 写法二:Object.defineProperty
    Object.defineProperty(obj, s1, {
      enumerable: true,
      configurable: true,
      writable: true,
      value: "abc"
    })
    
    // 写法三:定义字面量时直接使用
    const info = {
      [s1]: 'abc',
      [s2]: 'nba'
    }
    
    console.log(Object.getOwnPropertySymbols(info)) // [Symbol(), Symbol()]
    const symbolKeys = Object.getOwnPropertySymbols(info) // 返回一个给定对象自身的所有 Symbol 属性的数组
    for (const key of symbolKeys) {
      console.log(info[key]) // abc nba
    }

3. 相同值的Symbol

  • 前面我们讲Symbol的目的是为了创建一个独一无二的值,那么如果我们现在就是想创建相同的Symbol应该怎么来做呢?

    • 我们可以使用Symbol.for()方法来做到这一点
      • Symbol.for()方法:返回由给定的key 找到的symbol,否则就是返回新创建的symbol
    • 并且我们可以通过Symbol.keyFor()方法来获取对应的key
      • Symbol.keyFor():用来获取全局symbol 注册表中与某个symbol 关联的键
    js
    // 3.description
    // 3.1.Symbol函数直接生成的值, 都是独一无二
    const s3 = Symbol("ccc")
    console.log(s3.description) // ccc
    const s4 = Symbol(s3.description)
    console.log(s3 === s4) // false
    
    // 3.2. 如果相同的key, 通过Symbol.for可以生成相同的Symbol值
    const s5 = Symbol.for("ddd")
    const s6 = Symbol.for("ddd")
    console.log(s5 === s6) // true
    
    // 获取传入的key
    console.log(Symbol.keyFor(s5)) // ddd
    console.log(Symbol.keyFor(s3)) // undefined

四. 数据结构 - Set 集合

引言

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

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

基本使用

Set 对象允许你存储任何类型(无论是原始值还是对象引用)的唯一值。

Set 对象是值的集合,集合中的每一个值都是唯一的,如果存在多个相等的值,则只会保留最后一个添加进去的值。

  • 创建 Set 对象:需要通过 Set 构造函数(暂时没有字面量创建的方式)

    js
    new Set([iterable])
  • iterable(可选):如果传递一个可迭代对象,它的所有元素将不重复地被添加到新的 Set 中。如果不指定此参数或其值为 null,则新的 Set 为空。

  • 类数组的结构但 Set 中存放的元素是不会重复的,经常用来给数组去重

    js
    // 1.创建Set
    const set = new Set()
    console.log(set)
    
    // 2.添加元素
    set.add(10)
    set.add(22)
    set.add(35)
    set.add(22)
    console.log(set) // Set(3) {10, 22, 35}
    
    const set2 = new Set()
    const info = {}
    const obj = {name: "obj"}
    set2.add(info)
    set2.add({})
    set2.add(obj)
    set2.add(obj)
    console.log(set2) // {{}, {}, {name: 'obj'}}
    
    // 3.应用场景: 数组的去重~
    const names = ["abc", "cba", "nba", "cba", "nba"]
    // const newNames = []
    // for (const item of names) {
    //   if (!newNames.includes(item)) {
    //     newNames.push(item)
    //   }
    // }
    // console.log(newNames)
    const newNamesSet = new Set(names)
    // const newNames = Array.from(newNamesSet) // 对类数组或可迭代对象创建一个新的浅拷贝的数组实例
    const newNames = [...newNamesSet] // 展开语法,在构造数组字面量时,对类数组或可迭代对象创建一个新的浅拷贝的数组
    console.log(newNames) // ['abc', 'cba', 'nba']

属性和方法

常见属性:

  • size:返回 Set 中元素的个数
    js
    const set = new Set([1, 2, 3])
    set.size // 3

常用方法:

  • add(value):添加某个元素,返回 Set 对象本身

    js
    set.add(100) // Set(4) {1, 2, 3, 100}
  • delete(value):从 Set 中删除和这个值相等的元素,返回 true / false。

    js
    set.delete(100) // Set(3) {1, 2, 3}
  • has(value):判断 set 中是否存在某个元素,返回 true / false。

    js
    set.has(3) // true
  • clear():清空 Set 中所有的元素,无返回值。

    js
    set.clear()
    console.log(set) // Set(0) {size: 0}
  • forEach(callback, [, thisArg]):通过 forEach 遍历 set

    js
    set.forEach(console.log)  // 1 2 3

其他注意事项:

  • 支持 for...of 遍历的,因为 Set 是可迭代对象

    js
    for (const i of set) console.log(i) 
    // 1 2 3
  • 不支持 for...in 遍历,因为遍历不出 key

    js
    for(const i in set) console.log('----')
    // 什么都不会输出

WeakSet

和 Set 类似的另外一个数据结构称之为 WeakSet,也是内部元素不能重复的数据结构。

那么和 Set 有什么区别呢?

  • 区别一:WeakSet 中只能存对象类型null 也不支持),不能存基本数据类型
  • 区别二:WeakSet 对元素是弱引用,如果没有其他引用对某个对象进行引用,那么该对象可以被 GC 回收

常见方法:

  • add(value):添加某个元素,返回 WeakSet 对象本身。
  • delete(value):从 WeakSet 中删除和这个值相等的元素,返回 true / false。
  • has(value):判断 WeakSet 中是否存在某个元素,返回 true / false。
js
// 1.Weak Reference(弱引用)和 Strong Reference(强引用)
let obj1 = { name: "why" }
let obj2 = { name: "kobe" }
let obj3 = { name: "jame" }

let arr = [obj1, obj2, obj3] // 默认的引用都是强引用,从可达性的角度来说,arr中保存的是Obj1等数据的内存地址,即使后面对Obj1等数据修改了内存地址指向,也不影响到arr中的值
obj1 = null
obj2 = null
obj3 = null
console.log(arr) // [{name: 'why'}, {name: 'kobe'}, {name: 'jame'}]

const set = new Set(arr) // 即使后面修改了arr的值,指向新的内存地址,这一步已经保存了之前arr中的值的内存地址
arr = null
console.log(set) // Set(3) {{name: 'why'}, {name: 'kobe'}, {name: 'jame'}}

// 2.WeakSet的用法
// 2.1.和Set的区别一: 只能存放对象类型
const weakSet = new WeakSet()
// weakSet.add(1) // Uncaught TypeError: Invalid value used in weak set
weakSet.add({name: 'later'})
console.log(weakSet) // {{name: 'later'}}

// 2.2.和Set的区别二: 对对象的引用都是弱引用

WeakSet 的应用

⚠️ 注意

WeakSet 不能遍历

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

那么这个东西有什么用呢?

事实上这个问题并不好回答,我们来使用一个 Stack Overflow 上的答案。

js
// WeakSet的应用

// 这里之所以不用 Set,是因为 Set 是强引用,如果在后续我们想对 new 构造的某个实例对象进行销毁时,
// 强引用会导致该实例对象内存中的地址仍然被 Set 引用着,而 WeekSet 却不会,
// 因为是弱引用,如果没有其他引用指向该实例对象,后续 GC 会回收掉。
const pWeakSet = new WeakSet()
class Person {
  constructor() {
    pWeakSet.add(this)
  }

  running() {
    if (!pWeakSet.has(this)) {
      console.log("不能通过其他对象: ", this, "来调用")
      return
    }
    console.log("running~")
  }
}

let p = new Person()
// p = null
p.running() // running~
const runFn = p.running
runFn() // 不能通过其他对象: undefined来调用
const obj = { run: runFn }
obj.run() // 不能通过其他对象:  {run: ƒ}来调用

五. 数据结构 - Map 映射

基本使用

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

但是我们可能会想,在之前我们可以使用对象来存储映射关系,为什么还需要 Map 来存储映射关系呢?

  • 在对象中存储映射关系,我们只能用字符串作为属性名(key),后来 ES6 又新增了 Symbol。
  • 某些情况下,我们可能希望通过其他类型作为 key ,比如对象类型,但在普通的对象中会自动将对象类型的 key 转成字符串类型的 key。

Map 允许使用对象类型作为 key。那么我们就可以使用 Map 来解决这个问题:

js
const info = { name: 'later' }
const info2 = { age: 18 }

// 1. 对象类型的局限性:不可以使用复杂类型如对象,作为key
const obj = {
  [info]: 'haha' // [info] 计算属性 对象类型的key被转为字符串类型的key => String(info) => [object Object]
}

console.log(obj) // {[object Object]: "haha"}
console.log(obj.info) // undefined

obj[info2] = 'yeye'
console.log(obj) // {[object Object]: 'yeye'}
console.log(obj.info2) // undefined

// 2. Map映射类型
const map = new Map()
map.set(info, 'aaa')
map.set(info2, 'bbb')
console.log(map) // {{name: 'later'} => "aaa", {age: 18} => "bbb"}

属性和方法

常见属性

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

常见方法

  • set(key, value):往 Map 中添加 key 和 value,返回添加后的 Map 对象。
  • get(key):获取 Map 中 指定 key 对应的 value,如果 key 不存在,则返回 undefined。
  • has(key):判断是否包括某一个 key,返回 true / false。
  • delete(key):根据 key 删除一个键值对,返回 true / false。
  • clear():清空所有的元素。
  • forEach(callback, [, thisArg]):通过 forEach 遍历 Map。

Map 也可以通过 for...of 进行遍历,可迭代对象

js
const obj1 = {name: 'later'}
const obj2 = {age: 18}
const obj3 = {sex: 'male'}
// 使用常规的 Map 构造函数可以将一个二维的键值对数组转换成一个 Map 对象
const map = new Map(
  [
    [obj1, 'aaa'],
    [obj2, 'bbb']
  ]
)
console.log(map) // Map(2) {{…} => 'aaa', {…} => 'bbb'}
console.log(map.get(obj1)) // 'aaa'

// 使用 Array.from 函数可以将一个 Map 对象转换成一个二维的键值对数组
console.log(
  Array.from(map)
) // [[obj1, 'aaa'],[obj2, 'bbb']]

// 更简洁的方法来做如上同样的事情,使用展开运算符
console.log(
  [...map]
) // [[obj1, 'aaa'],[obj2, 'bbb']]

// 或者在键或者值的迭代器上使用 Array.from,进而得到只含有键或者值的数组
console.log(
  Array.from(map.keys())
) // 输出 [obj1, obj2]

map.set(obj3, 'ccc')
console.log(map) // Map(3) {{…} => 'aaa', {…} => 'bbb', {…} => 'ccc'}

console.log(map.get(obj3)) // ccc

console.log(map.has(obj3)) // true

map.delete(obj3)
console.log(map) // Map(2) {{…} => 'aaa', {…} => 'bbb'}

//  map.clear()
//  console.log(map) // Map(0) {size: 0}

map.forEach(value => console.log(value)) // aaa bbb

for (const item of map) {
  console.log(item)
  const [key, value] = item
  console.log(key, value)
}

// [{…}, 'aaa']
// {name: 'later'} 'aaa'
// [{…}, 'bbb']
// {age: 18} 'bbb'

WeakMap

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

那么和 Map 有什么区别呢?

  • 区别一:WeakMap key 只能是对象类型
  • 区别二:WeakMap 的 key 对对象的引用是弱引用,如果没有其他引用指向这个对象,GC 可以回收该对象。
js
const weakMap = new WeakMap()
weakMap.set({name: 'later'}, 'ccc')
console.log(weakMap)        // WeakMap {{…} => 'ccc'}
weakMap.set(1, 'aaa')       // Uncaught TypeError: Invalid value used as weak map key
weakMap.set('hello', 'bbb') // Uncaught TypeError: Invalid value used as weak map key

常见方法

  • set(key, value):往 WeakMap 中添加 key 和 value,返回添加后的 WeakMap 对象。
  • get(key):获取 WeakMap 中指定 key 对应的 value。
  • has(key):判断是否包括某一个 key,返回 true / false。
  • delete(key):根据 key 删除一个键值对,返回 true / false。

WeakMap 的应用

⚠️ 注意

WeakMap 也是不能遍历的,没有 forEach 方法,也不支持通过 for...of 的方式进行遍历。

那么我们的 WeakMap有 什么作用呢?vue 响应式系统中有用到。

js
// WeakMap({key(对象):value}): key是一个对象,弱引用
const targetMap = new WeakMap()

function getDep(target, key) {
  // 1. 根据对象(target) 取出对应的收集依赖的 Map 对象
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  // 2. 取出具体的 dep 对象
  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Dep()
    depsMap.set(key, dep)
  }
  return dep
}

如有转载或CV请标注本站原文地址