knzn.net

12 ES6 知识点讲解

ES6 中对 对象字面量 进行了增强,称之为 Enhanced object literals(增强对象字面量)。

字面量的增强主要包括下面几部分:

  • 属性的简写:Property Shorthand
  • 方法的简写:Method Shorthand
  • 计算属性名:Computed Property Names

字面量的增强

var name = 'why'
var age = 18

var obj = {
  // 1.property shorthand(属性的简写)
  name,
  age,

  // 2.method shorthand(方法的简写)
  foo: function () {
    console.log(this)
  },
  bar() {
    console.log(this)
  },
  baz: () => {
    console.log(this)
  },

  // 3.computed property name(计算属性名)
  [name + 123]: 'hehehehe',
}

解构 Destructuring

ES6 中新增了一个从数组或对象中方便获取数据的方法,称之为解构 Destructuring。

我们可以划分为:数组的解构和对象的解构

数组的解构:

基本解构过程

顺序解构

解构出数组

默认值

var names = ['abc', 'cba', 'nba']
// var item1 = names[0]
// var item2 = names[1]
// var item3 = names[2]

// 对数组的解构: []
var [item1, item2, item3] = names
console.log(item1, item2, item3)

// 解构后面的元素
var [, , itemz] = names
console.log(itemz)

// 解构出一个元素,后面的元素放到一个新数组中
var [itemx, ...newNames] = names
console.log(itemx, newNames)

// 解构的默认值
var [itema, itemb, itemc, itemd = 'aaa'] = names
console.log(itemd)

对象的解构:

基本解构过程

任意顺序

重命名

默认值

var obj = {
  name: 'why',
  age: 18,
  height: 1.88,
}

// 对象的解构: {}
var { name, age, height } = obj
console.log(name, age, height)

var { age } = obj
console.log(age)

var { name: newName } = obj
console.log(newName)

var { address: newAddress = '广州市' } = obj
console.log(newAddress)

解构的应用场景

解构目前在开发中使用是非常多的:

比如在开发中拿到一个变量时,自动对其进行解构使用;

比如对函数的参数进行解构;

function foo(info) {
  console.log(info.name, info.age)
}

foo(obj)

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

bar(obj)

let/const 基本使用

在 ES5 中我们声明变量都是使用的 var 关键字,从 ES6 开始新增了两个关键字可以声明变量:let、const

let、const 在其他编程语言中都是有的,所以也并不是新鲜的关键字;

但是 let、const 确确实实给 JavaScript 带来一些不一样的东西;

var foo = 'foo'
let bar = 'bar'

// const constant(常量/衡量)
const name = 'abc'
name = 'cba' // 报错

let 关键字:

从直观的角度来说,let 和 var 是没有太大的区别的,都是用于声明一个变量

const 关键字:

const 关键字是 constant 的单词的缩写,表示常量、衡量的意思;它表示保存的数据一旦被赋值,就不能被修改;

但是如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容;

// 注意事项一: const本质上是传递的值不可以修改
// 但是如果传递的是一个引用类型(内存地址), 可以通过引用找到对应的对象, 去修改对象内部的属性, 这个是可以的
const obj = {
  foo: 'foo',
}

// obj = {}
obj.foo = 'aaa'
console.log(obj.foo)

注意:

另外 let、const 不允许重复声明变量;

var bar = 'abc'
var bar = 'cba'

let foo = 'abc'
let foo = 'cba' // SyntaxError: Identifier 'foo' has already been declared
console.log(foo)

let/const 作用域提升

let、const 和 var 的另一个重要区别是作用域提升:

我们知道 var 声明的变量是会进行作用域提升的;但是如果我们使用 let 声明的变量,在声明之前访问会报错;

那么是不是意味着 foo 变量只有在代码执行阶段才会创建的呢?

事实上并不是这样的,我们可以看一下 ECMA262 对 let 和 const 的描述;

这些变量会被创建在包含他们的词法环境被实例化时,但是是不可以访问它们的,直到词法绑定被求值;

console.log(foo)
var foo = 'foo'

// let/const他们是没有作用域提升
// foo被创建出来了, 但是不能被访问
// 作用域提升: 能提前被访问
console.log(foo) // Reference(引用)Error: Cannot access 'foo' before initialization(初始化)
let foo = 'foo'

let/const 有没有作用域提升呢?

从上面我们可以看出,在执行上下文的词法环境创建出来的时候,变量事实上已经被创建了,只是这个变量是不能被访问的。

那么变量已经有了,但是不能被访问,是不是一种作用域的提升呢?

作用域提升:在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么我们可以称之为作用域提升;在这里,它虽然被创建出来了,但是不能被访问,我认为不能称之为作用域提升;

所以我的观点是 let、const 没有进行作用域提升,但是会在解析阶段被创建出来。

Window 对象添加属性

我们知道,在全局通过 var 来声明一个变量,事实上会在 window 上添加一个属性:

但是 let、const 是不会给 window 上添加任何属性的。

那么我们可能会想这个变量是保存在哪里呢?

我们先回顾一下最新的 ECMA 标准中对执行上下文的描述

执行上下文(旧版):每一个执行上下文会被关联到一个变量环境(variable object, VO),在源代码中的变量和函数声明会被作为属性添加到 VO 中。对于函数来说,参数也会被添加到 VO 中

执行上下文(新版): 每一个执行上下文会关联到一个变量环境(VariableEnvironment)中,在执行代码中变量和函数的声明会作为环境记录(Environment Record) 添加到变量环境中。对于函数来说,参数也会被作为环境记录添加到变量环境中

var foo = 'foo'
var message = 'Hello World'

console.log(window.foo)
console.log(window.message)

window.message = '哈哈哈'
console.log(message)

let bar = 'foo'
console.log(window.bar) // undefined

变量被保存到 VariableMap 中

也就是说我们声明的变量和环境记录是被添加到变量环境中的:

但是标准有没有规定这个对象是 window 对象或者其他对象呢?其实并没有,那么 JS 引擎在解析的时候,其实会有自己的实现;比如 v8 中其实是通过 VariableMap 的一个 hashmap 来实现它们的存储的。

那么 window 对象呢?而 window 对象是早期的 GO 对象,在最新的实现中其实是浏览器添加的全局对象,并且一直保持了 window 和 var 之间值的相等性;

var 的块级作用域

在我们前面的学习中,JavaScript 只会形成两个作用域:全局作用域和函数作用域。

// 在ES5中只有两个东西会形成作用域
// 1.全局作用域
// 2.函数作用域
function foo() {
  var bar = 'bar'
}

console.log(bar) // error

function foo() {
  function demo() {}
}

ES5 中放到一个代码中定义的变量,外面是可以访问的:

// 声明对象的字面量
var obj = {
  name: 'why',
}

// ES5中没有块级作用域
// 块代码(block code)
{
  // 声明一个变量
  var foo = 'foo'
}

console.log(foo)

let/const 的块级作用域

在 ES6 中新增了块级作用域,并且通过 let、const、function、class 声明的标识符是具备块级作用域的限制的:

// ES6的代码块级作用域
// 对let/const/function/class声明的类型是有效
{
  let foo = 'why'
  class Person {}
  function demo() {
    console.log('demo function')
  }
}
// 不同的浏览器有不同实现的(大部分浏览器为了兼容以前的代码, 让function是没有块级作用域)
demo()
var p = new Person() // Person is not defined

但是我们会发现函数拥有块级作用域,但是外面依然是可以访问的:

这是因为引擎会对函数的声明进行特殊的处理,允许像 var 那样进行提升;

// if语句的代码就是块级作用域
if (true) {
  var foo = 'foo'
  let bar = 'bar'
}

console.log(foo) // foo
console.log(bar) // Uncaught ReferenceError: bar is not defined
// switch语句的代码也是块级作用域
var color = 'red'

switch (color) {
  case 'red':
    var foo = 'foo'
    let bar = 'bar'
}

console.log(foo)
console.log(bar) // Uncaught ReferenceError: bar is not defined
// for语句的代码也是块级作用域
// for (var i = 0; i < 10; i++) {
// }

// console.log(i); // 10

for (let i = 0; i < 10; i++) {}

console.log(i) // Uncaught ReferenceError: i is not defined

块级作用域的应用

我来看一个实际的案例:获取多个按钮监听点击

const btns = document.getElementsByTagName('button')

for (var i = 0; i < btns.length; i++) {
  ;(function (n) {
    btns[i].onclick = function () {
      console.log('第' + n + '个按钮被点击')
    }
  })(i)
}

console.log(i)

使用 let 或者 const 来实现:

for (let i = 0; i < btns.length; i++) {
  btns[i].onclick = function () {
    console.log('第' + i + '个按钮被点击')
  }
}

块级作用域的补充

const names = ['abc', 'cba', 'nba']

// 不可以使用const
for (let i = 0; i < names.length; i++) {
  console.log(names[i])
}

{
  let i = 0
  console.log(names[i])
}

{
  let i = 1
  console.log(names[i])
}

{
  let i = 2
  console.log(names[i])
}
// for...of: ES6新增的遍历数组(对象)
for (const item of names) {
  console.log(item)
}

{
  const item = 'abc'
  console.log(item)
}

{
  const item = 'cba'
  console.log(item)
}

console.log(item) // Uncaught ReferenceError: item is not defined

let-const 的暂时性死区

var foo = 'foo'

if (true) {
  console.log(foo) // Uncaught ReferenceError: Cannot access 'foo' before initialization

  let foo = 'abc'
}

function bar() {
  console.log(foo) // Uncaught ReferenceError: Cannot access 'foo' before initialization

  let foo = 'abc'
}

bar()

var、let、const 的选择

那么在开发中,我们到底应该选择使用哪一种方式来定义我们的变量呢?

对于 var 的使用:我们需要明白一个事实,var 所表现出来的特殊性:比如作用域提升、window 全局对象、没有块级作用域等都是一些历史遗留问题;其实是 JavaScript 在设计之初的一种语言缺陷;当然目前市场上也在利用这种缺陷出一系列的面试题,来考察大家对 JavaScript 语言本身以及底层的理解;但是在实际工作中,我们可以使用最新的规范来编写,也就是不再使用 var 来定义变量了;

对于 let、const:

对于 let 和 const 来说,是目前开发中推荐使用的;我们会优先推荐使用 const,这样可以保证数据的安全性不会被随意的篡改;只有当我们明确知道一个变量后续会需要被重新赋值时,这个时候再使用 let;这种在很多其他语言里面也都是一种约定俗成的规范,尽量我们也遵守这种规范;

字符串模板基本使用

在 ES6 之前,如果我们想要将字符串和一些动态的变量(标识符)拼接到一起,是非常麻烦和丑陋的(ugly)。

// ES6之前拼接字符串和其他标识符
const name = 'why'
const age = 18
const height = 1.88

console.log('my name is ' + name + ', age is ' + age + ', height is ' + height)

ES6 允许我们使用字符串模板来嵌入 JS 的变量或者表达式来进行拼接:

// ES6提供模板字符串 ``
const message = `my name is ${name}, age is ${age}, height is ${height}`
console.log(message)

首先,我们会使用 `` 符号来编写字符串,称之为模板字符串;其次,在模板字符串中,我们可以通过 ${expression} 来嵌入动态的内容;

const info = `age double is ${age * 2}`
console.log(info)

function doubleAge() {
  return age * 2
}

const info2 = `double age is ${doubleAge()}`
console.log(info2)

标签模板字符串使用

模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals)

我们一起来看一个普通的 JavaScript 的函数

如果我们使用标签模板字符串,并且在调用的时候插入其他的变量:

模板字符串被拆分了;第一个元素是数组,是被模块字符串拆分的字符串组合;后面的元素是一个个模块字符串传入的内容;

// 第一个参数依然是模块字符串中整个字符串, 只是被切成多块,放到了一个数组中
// 第二个参数是模块字符串中, 第一个 ${}
function foo(m, n, x) {
  console.log(m, n, x) // ['Hello', 'Wo', 'rld', raw: Array(3)] 'why' 18
}

const name = 'why'
const age = 18
foo`Hello${name}Wo${age}rld`

函数的默认参数

在 ES6 之前,我们编写的函数参数是没有默认值的,所以我们在编写函数时,如果有下面的需求:

传入了参数,那么使用传入的参数;没有传入参数,那么使用一个默认值;

// ES5以及之前给参数默认值
/**
 * 缺点:
 *  1.写起来很麻烦, 并且代码的阅读性是比较差
 *  2.这种写法是有bug
 */
function foo(m, n) {
  m = m || 'aaa'
  n = n || 'bbb'

  console.log(m, n)
}

而在 ES6 中,我们允许给函数一个默认值

// 1.ES6可以给函数参数提供默认值
function foo(m = 'aaa', n = 'bbb') {
  console.log(m, n)
}

// foo()
foo(0, '')
// 2.对象参数和默认值以及解构
function printInfo({ name, age } = { name: 'why', age: 18 }) {
  console.log(name, age)
}

printInfo({ name: 'kobe', age: 40 })

// 另外一种写法
function printInfo1({ name = 'why', age = 18 } = {}) {
  console.log(name, age)
}

printInfo1()

另外参数的默认值我们通常会将其放到最后(在很多语言中,如果不放到最后其实会报错的):但是 JavaScript 允许不将其放到最后,但是意味着还是会按照顺序来匹配;

// 3.有默认值的形参最好放到最后
function bar(x, y, z = 30) {
  console.log(x, y, z)
}

// bar(10, 20)
bar(undefined, 10, 20)

另外默认值会改变函数的 length 的个数,默认值以及后面的参数都不计算在 length 之内了。

function baz(x, y, z, m, n = 30) {
  console.log(x, y, z, m, n)
}

console.log(baz.length) // 4

函数的剩余参数

ES6 中引用了 rest parameter,可以将不定数量的参数放入到一个数组中:

如果最后一个参数是 ... 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组;

// rest paramaters必须放到最后
function foo(m, n, ...args) {
  console.log(m, n)
  console.log(args)

  console.log(arguments)
}

foo(20, 30, 40, 50, 60)

那么剩余参数和 arguments 有什么区别呢?

  • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参;
  • arguments 对象不是一个真正的数组,而 rest 参数是一个真正的数组,可以进行数组的所有操作;
  • arguments 是早期的 ECMAScript 中为了方便去获取所有的参数提供的一个数据结构,而 rest 参数是 ES6 中提供 并且希望以此来替代 arguments 的;
  • 剩余参数必须放到最后一个位置,否则会报错。

函数箭头函数的补充

在前面我们已经学习了箭头函数的用法,这里进行一些补充:

箭头函数是没有显式原型的,所以不能作为构造函数,使用 new 来创建对象;

var bar = () => {
  console.log(this, arguments)
}

console.log(bar.prototype) // undefined

// bar is not a constructor
const b = new bar()

展开语法

  • 可以在函数调用/数组构造时,将数组表达式或者 string 在语法层面展开;
  • 还可以在构造字面量对象时, 将对象表达式按 key-value 的方式展开;
const names = ['abc', 'cba', 'nba']
const name = 'why'
const info = { name: 'why', age: 18 }

// 1.函数调用时
function foo(x, y, z) {
  console.log(x, y, z)
}

// foo.apply(null, names)
foo(...names)
foo(...name) // w h y

// 2.构造数组时
const newNames = [...names, ...name]
console.log(newNames) // ['abc', 'cba', 'nba', 'w', 'h', 'y']

// 3.构建对象字面量时ES2018(ES9)
const obj = { ...info, address: '广州市', ...names }
console.log(obj) // {0: 'abc', 1: 'cba', 2: 'nba', name: 'why', age: 18, address: '广州市'}

注意:展开运算符其实是一种浅拷贝;

const info = {
  name: 'why',
  friend: { name: 'kobe' },
}

const obj = { ...info, name: 'coderwhy' }
// console.log(obj)
obj.friend.name = 'james'

console.log(info.friend.name) // james

数值的表示

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

const num1 = 100 // 十进制

// b -> binary
const num2 = 0b100 // 二进制
// o -> octonary
const num3 = 0o100 // 八进制
// x -> hexadecimal
const num4 = 0x100 // 十六进制

console.log(num1, num2, num3, num4) // 100 4 64 256

另外在 ES2021 新增特性:数字过长时,可以使用_作为连接符

// 大的数值的连接符(ES2021 ES12)
const num = 10_000_000_000_000_000
console.log(num)

Symbol 的基本使用

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

那么为什么需要 Symbol 呢?

在 ES6 之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突;比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性;

比如我们前面在讲 apply、call、bind 实现时,我们有给其中添加一个 fn 属性,那么如果它内部原来已经有了 fn 属性了 呢?

比如开发中我们使用混入,那么混入中出现了同名的属性,必然有一个会被覆盖掉;

Symbol 就是为了解决上面的问题,用来生成一个独一无二的值。

Symbol 值是通过 Symbol 函数来生成的,生成后可以作为属性名;也就是在 ES6 中,对象的属性名可以使用字符串,也可以使用 Symbol 值;

Symbol 即使多次创建值,它们也是不同的:Symbol 函数执行后每次创建出来的值都是独一无二的;我们也可以在创建 Symbol 值的时候传入一个描述 description:这个是 ES2019(ES10)新增的特性;

// ES6中Symbol的基本使用
const s1 = Symbol()
const s2 = Symbol()

console.log(s1 === s2) // false

// ES2019(ES10)中, Symbol还有一个描述(description)
const s3 = Symbol('aaa')
console.log(s3.description) // aaa

Symbol 作为属性名

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

// Symbol值作为key
// 1.在定义对象字面量时使用
const obj = {
  [s1]: 'abc',
  [s2]: 'cba',
}

// 2.新增属性
obj[s3] = 'nba'

// 3.Object.defineProperty方式
const s4 = Symbol()
Object.defineProperty(obj, s4, {
  enumerable: true,
  configurable: true,
  writable: true,
  value: 'mba',
})

console.log(obj[s1], obj[s2], obj[s3], obj[s4])
// 注意: 不能通过.语法获取
// console.log(obj.s1)

// 4.使用Symbol作为key的属性名,在遍历/Object.keys等中是获取不到这些Symbol值
// 需要Object.getOwnPropertySymbols来获取所有Symbol的key
console.log(Object.keys(obj))
console.log(Object.getOwnPropertyNames(obj))
console.log(Object.getOwnPropertySymbols(obj))
const sKeys = Object.getOwnPropertySymbols(obj)
for (const sKey of sKeys) {
  console.log(obj[sKey])
}

相同值的 Symbol

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

我们可以使用 Symbol.for 方法来做到这一点;并且我们可以通过 Symbol.keyFor 方法来获取对应的 key;

const sa = Symbol.for('aaa')
const sb = Symbol.for('aaa')
console.log(sa === sb) // true

const key = Symbol.keyFor(sa)
console.log(key) // aaa
const sc = Symbol.for(key)
console.log(sa === sc) // true

Set 的基本使用

在 ES6 之前,我们存储数据的结构主要有两种:数组、对象。在 ES6 中新增了另外两种数据结构:Set、Map,以及它们的另外形式 WeakSet、WeakMap。

Set 是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是元素不能重复。创建 Set 我们需要通过 Set 构造函数(暂时没有字面量创建的方式):

const set = new Set()
set.add(10)
set.add(20)
set.add(40)
set.add(333)

set.add(10)
console.log(set) // Set(4) {10, 20, 40, 333}

添加对象时特别注意:

const set = new Set()
set.add({})
set.add({})

const obj = {}
set.add(obj)
set.add(obj)

console.log(set) // Set(3) {{…}, {…}, {…}}

我们可以发现 Set 中存放的元素是不会重复的,那么 Set 有一个非常常用的功能就是给数组去重。

// 对数组去重(去除重复的元素)
const arr = [33, 10, 26, 30, 33, 26]

// 方法一
const newArr = []
for (const item of arr) {
  if (newArr.indexOf(item) !== -1) {
    newArr.push(item)
  }
}

// 方法二
const arrSet = new Set(arr)
const newArr = Array.from(arrSet)
// const newArr = [...arrSet];
console.log(newArr)

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 的遍历的。
// size属性
console.log(arrSet.size)

// Set的方法
// add
arrSet.add(100)
console.log(arrSet)

// delete
arrSet.delete(33)
console.log(arrSet)

// has
console.log(arrSet.has(100))

// clear
// arrSet.clear()
console.log(arrSet)

// 对Set进行遍历
arrSet.forEach((item) => {
  console.log(item)
})

for (const item of arrSet) {
  console.log(item)
}

WeakSet 使用

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

那么和 Set 有什么区别呢?

  • 区别一:WeakSet 中只能存放对象类型,不能存放基本数据类型;
  • 区别二:WeakSet 对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么 GC 可以对该对象进行回收;
const weakSet = new WeakSet()

// 1.区别一: 只能存放对象类型
// TypeError: Invalid value used in weak set
weakSet.add(10)

// 2.区别二: 对对象是一个弱引用
let obj = {
  name: 'why',
}

const set = new Set()
// 建立的是强引用
set.add(obj)

// 建立的是弱引用, 没有其它指向该对象的引用,会被垃圾回收
weakSet.add(obj)

WeakSet 的应用

注意:WeakSet 不能遍历

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

那么这个东西有什么用呢?事实上这个问题并不好回答,我们来使用一个 Stack Overflow 上的答案;

const personSet = new WeakSet()
class Person {
  constructor() {
    personSet.add(this)
  }

  running() {
    if (!personSet.has(this)) {
      throw new Error('不能通过非构造方法创建出来的对象调用running方法')
    }
    console.log('running~', this)
  }
}

let p = new Person()
p.running()
// p = null;

p.running.call({ name: 'why' }) // VM280:9 Uncaught Error: 不能通过非构造方法创建出来的对象调用running方法

Map 的基本使用

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

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

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

JavaScript 中对象中是不能使用对象来作为 key 的

const obj1 = { name: 'why' }
const obj2 = { name: 'kobe' }

const info = {
  [obj1]: 'aaa',
  [obj2]: 'bbb',
}

console.log(info) // {[object Object]: 'bbb'}

那么我们就可以使用 Map:Map 就是允许我们对象类型来作为 key 的

// 构造方法的使用
const map = new Map()
map.set(obj1, 'aaa')
map.set(obj2, 'bbb')
map.set(1, 'ccc')
console.log(map) // Map(3) {{…} => 'aaa', {…} => 'bbb', 1 => 'ccc'}

Map 的常用方法

Map 常见的属性:

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

Map 常见的方法:

  • set(key, value):在 Map 中添加 key、value,并且返回整个 Map 对象;
  • get(key):根据 key 获取 Map 中的 value;
  • has(key):判断是否包括某一个 key,返回 Boolean 类型;
  • delete(key):根据 key 删除一个键值对,返回 Boolean 类型;
  • clear():清空所有的元素;
  • forEach(callback, [, thisArg]):通过 forEach 遍历 Map;
  • Map 也可以通过 for of 进行遍历。
const map2 = new Map([
  [obj1, 'aaa'],
  [obj2, 'bbb'],
  [2, 'ddd'],
])
console.log(map2) // Map(3) {{…} => 'aaa', {…} => 'bbb', 2 => 'ddd'}

// 常见的属性和方法
console.log(map2.size) // 3

// set
map2.set('why', 'eee')
console.log(map2) // Map(4) {{…} => 'aaa', {…} => 'bbb', 2 => 'ddd', 'why' => 'eee'}

// get(key)
console.log(map2.get('why')) // eee

// has(key)
console.log(map2.has('why')) // true

// delete(key)
map2.delete('why')
console.log(map2) // Map(3) {{…} => 'aaa', {…} => 'bbb', 2 => 'ddd'}

// clear
// map2.clear()
// console.log(map2)

// 遍历map
map2.forEach((item, key) => {
  console.log(item, key)
})

for (const item of map2) {
  console.log(item[0], item[1])
}

for (const [key, value] of map2) {
  console.log(key, value)
}

WeakMap 的使用

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

那么和 Map 有什么区别呢?

  • 区别一:WeakMap 的 key 只能使用对象,不接受其他的类型作为 key;
  • 区别二:WeakMap 的 key 对对象想的引用是弱引用,如果没有其他引用引用这个对象,那么 GC 可以回收该对象;
const obj = { name: 'obj1' }
// Map 的 key 是强引用
const map = new Map()
map.set(obj, 'aaa')

// WeakMap 的 key 是弱引用,如果没有其它指向该 key  的引用,会被垃圾回收
const weakMap = new WeakMap()
weakMap.set(obj, 'aaa')

// 不能使用基本数据类型
weakMap.set(1, 'ccc') // VM172:11 Uncaught TypeError: Invalid value used as weak map key

WeakMap 常见的方法有四个:

  • set(key, value):在 Map 中添加 key、value,并且返回整个 Map 对象;
  • get(key):根据 key 获取 Map 中的 value;
  • has(key):判断是否包括某一个 key,返回 Boolean 类型;
  • delete(key):根据 key 删除一个键值对,返回 Boolean 类型;
// get方法
console.log(weakMap.get(obj)) // aaa

// has方法
console.log(weakMap.has(obj)) // true

// delete方法
console.log(weakMap.delete(obj)) // true

WeakMap 的应用

注意:WeakMap 也是不能遍历的;因为没有 forEach 方法,也不支持通过 for of 的方式进行遍历;那么我们的 WeakMap 有什么作用呢?

// 应用场景(vue3响应式原理)
const obj1 = {
  name: 'why',
  age: 18,
}

function obj1NameFn1() {
  console.log('obj1NameFn1被执行')
}

function obj1NameFn2() {
  console.log('obj1NameFn2被执行')
}

function obj1AgeFn1() {
  console.log('obj1AgeFn1')
}

function obj1AgeFn2() {
  console.log('obj1AgeFn2')
}

const obj2 = {
  name: 'kobe',
  height: 1.88,
  address: '广州市',
}

function obj2NameFn1() {
  console.log('obj1NameFn1被执行')
}

function obj2NameFn2() {
  console.log('obj1NameFn2被执行')
}

// 1.创建WeakMap
const weakMap = new WeakMap()

// 2.收集依赖结构
// 2.1.对obj1收集的数据结构
const obj1Map = new Map()
obj1Map.set('name', [obj1NameFn1, obj1NameFn2])
obj1Map.set('age', [obj1AgeFn1, obj1AgeFn2])
weakMap.set(obj1, obj1Map)

// 2.2.对obj2收集的数据结构
const obj2Map = new Map()
obj2Map.set('name', [obj2NameFn1, obj2NameFn2])
weakMap.set(obj2, obj2Map)

// 3.如果obj1.name发生了改变
// Proxy/Object.defineProperty
obj1.name = 'james'
const targetMap = weakMap.get(obj1)
const fns = targetMap.get('name')
fns.forEach((item) => item())

目录