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集合


1. Set的基本使用

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

    • ES6中新增了另外两种数据结构SetMap,以及它们的另外形式WeakSetWeakMap
  • Set是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是元素不能重复

    • 创建Set我们需要通过Set构造函数(暂时没有字面量创建的方式)
    • 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']

2. Set的常见方法

  • Set常见的属性:

    • size:返回Set中元素的个数
  • Set常用的方法:

    • add(value):添加某个元素,返回Set对象本身
    • delete(value):从set中删除和这个值相等的元素,返回boolean类型
    • has(value):判断set中是否存在某个元素,返回boolean类型
    • clear():清空set中所有的元素,没有返回值
    • forEach(callback, [, thisArg]):通过forEach遍历set
  • 另外Set是支持for...of遍历的,因为**Set是可迭代对象**

    • Set使用for...in是遍历不出key
    js
    const arr = [1, 2, 3]
    const set = new Set(arr)
    // 属性
    console.log(set.size) // 3
    // 方法
    // add方法
    set.add(100)
    console.log(set) // Set(4) {1, 2, 3, 100}
    // delete方法
    set.delete(100) 
    console.log(set) // Set(3) {1, 2, 3}
    // has方法
    console.log(set.has(3)) // true
    // clear方法
    // set.clear()
    // console.log(set) // Set(0) {size: 0}
    // forEach方法
    set.forEach(item => console.log(item)) // 1 2 3
    
    // set支持for...of
    for (const item of set) {
      console.log(item) // 1 2 3
    }

3. WeakSet的使用

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

  • 那么和Set有什么区别呢?

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

    • add(value):添加某个元素,返回WeakSet对象本身
    • delete(value):从WeakSet中删除和这个值相等的元素,返回boolean类型
    • has(value):判断WeakSet中是否存在某个元素,返回boolean类型
    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的区别二: 对对象的引用都是弱引用

4. WeakSet的应用

  • 注意:WeakSet不能遍历

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

    • 事实上这个问题并不好回答,我们来使用一个Stack Overflow上的答案
    js
    // WeakSet的应用
    const pWeakSet = new WeakSet() // 这里之所以不用Set,是因为Set是强引用,如果在后续我们想对new构造的某个实例对象进行销毁时,强引用会导致该实例对象内存中的地址仍然被Set引用着,而WeekSet却不会,因为是弱引用,如果没有其他引用指向该实例对象,后续GC会回收掉
    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映射


1. Map的基本使用

  • 另外一个新增的数据结构是Map用于存储映射关系,允许使用对象类型作为key

  • 但是我们可能会想,在之前我们可以使用对象来存储映射关系,他们有什么区别呢?

    • 事实上我们对象存储映射关系只能用字符串(ES6新增了Symbol)作为属性名(key
    • 某些情况下我们可能希望通过其他类型作为key,比如对象,这个时候会自动将对象转成字符串来作为key
  • 那么我们就可以使用Map

    js
    const info = {
      name: 'later'
    }
    const info2 = {
      age: 18
    }
    
    // 1. 对象类型的局限性:不可以使用复杂类型如对象,作为key
    const obj = {
      [info]: 'haha' // [info] 计算属性 对info对象转为字符串 => 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"}

2. Map的常用方法

  • 常见属性:

    • size:返回Map中元素的个数
  • 常见方法:

    • set(key, value):在Map中添加keyvalue,并且返回整个Map对象
    • get(key):根据key获取Map中的value
    • has(key):判断是否包括某一个key,返回Boolean类型
    • delete(key):根据key删除一个键值对,返回Boolean类型
    • clear():清空所有的元素
    • forEach(callback, [, thisArg]):通过forEach遍历Map
  • Map也可以通过for...of进行遍历,可迭代对象

    js
    const obj1 = {name: 'later'}
    const obj2 = {age: 18}
    const obj3 = {sex: 'male'}
    const map = new Map(
      [
        [obj1, 'aaa'],
        [obj2, 'bbb']
      ]
    )
    console.log(map) // Map(2) {{…} => 'aaa', {…} => 'bbb'}
    
    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(item => console.log(item)) // 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'

3. WeakMap的使用

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

  • 那么和Map有什么区别呢?

    • 区别一:WeakMapkey只接受对象类型作为key
    • 区别二:WeakMapkey对对象的引用是弱引用,如果没有其他引用引用这个对象,那么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
  • WeakMap常见方法:

    • set(key, value):在WeakMap中添加keyvalue,并且返回整个WeakMap对象
    • get(key):根据key获取WeakMap中的value
    • has(key):判断是否包括某一个key,返回Boolean类型
    • delete(key):根据key删除一个键值对,返回Boolean类型

4. WeakMap的应用

  • 注意:WeakMap也是不能遍历的

    • 没有forEach方法,也不支持通过for...of的方式进行遍历
  • 那么我们的WeakMap有什么作用呢?(后续专门讲解)

    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
    }

Released under the MIT License.