JavaScript ES6知识

170 阅读14分钟

一、变量声明(let、const)

1.1 let/const的基本使用

  • let关键字:

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

    • 表示保存的数据一旦被赋值,就不能被修改
    • 如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容
  • 注意:let、const不允许重复声明变量

// 1.let
let message = "你好, 世界"
message = 123
console.log(message) // 123

// 不允许重复声明
// let message // Uncaught SyntaxError: Identifier 'message' has already been declared

// 2.const
// const message2 = "nihao, shijie"
// message2 = "nihao, China" // Uncaught TypeError: Assignment to constant variable.

// 赋值引用类型
const info = {
  name: "kobe",
  age: 24
}
// info = {} // Uncaught TypeError: Assignment to constant variable.
info.name = "james"
console.log(info)

1.2 let/const有没有作用域提升

  • 标识符在词法环境被创建时, 会被提前创建出来, 但是不能访问

    // 1.var声明的变量会进行作用域的提升
    // console.log(message)
    // var message = "Hello World"
    
    // 2.let/const声明的变量: 没有作用域提升
    // console.log(address)
    console.log(info) // Uncaught ReferenceError: Cannot access 'info' before initialization
    let address = "北京市"
    const info = {}
    
  • 有没有作用域提升呢?
    • 作用于提升的概念:在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么我们可以称之为作用域提升
    • 所以let、const没有进行作用域提升,但是会在解析阶段被创建出来

1.3 暂时性死区TDZ

  • 从块作用域的顶部一直到变量声明完成之前,这个变量处在暂时性死区(TDZ,temporal dead zone)

    // 1.暂时性死区
    // function foo() {
    //   console.log(bar, baz) // Uncaught ReferenceError: Cannot access 'bar' before initialization
    
    //   let bar = "bar"
    //   let baz = "baz"
    // }
    // foo()
    
    // 2.暂时性死区和定义的位置没有关系, 和代码执行的顺序有关系
    // function foo() {
    //   console.log(message)
    // }
    
    // let message = "Hello World"
    // foo() // Hello World
    // console.log(message)
    
    // 3.暂时性死区形成之后, 在该区域内这个标识符不能访问
    let message = "Hello World"
    function foo() {
      console.log(message) // Uncaught ReferenceError: Cannot access 'message' before initialization
    
      const message = "哈哈哈哈"
    }
    
    foo()
    

1.4 不会添加window

  • 全局通过var来声明一个变量,事实上会在window上添加一个属性,但是let、const是不会给window上添加任何属性

    // 1.var定义的变量是会默认添加到window上的
    // var message = "Hello World"
    // var address = "北京市"
    
    // console.log(window.message)
    // console.log(window.address)
    
    // 2.let/const定义的变量不会添加到window上的
    // let message = "Hello World"
    // let address = "北京市"
    
    // console.log(window.message) // undefined
    // console.log(window.address) // undefined
    

1.5 块级作用域和应用

  • JavaScript中的作用域:全局作用域、函数作用域、块级作用域
  • let/const/class/function会形成块级作用域
  • 函数拥有块级作用域,但是外面依然是可以访问的:这是因为引擎会对函数的声明进行特殊的处理,允许像var那样在外界直接访问
    // foo() // foo is not a function
    {
      var message = "Hello World"
      let age = 18
      const height = 1.88
    
      class Person {}
    
      function foo() {
        console.log("foo function")
      }
    }
    
    // console.log(age) // Uncaught ReferenceError: age is not defined
    // console.log(height) // Uncaught ReferenceError: height is not defined
    // const p = new Person() // Uncaught ReferenceError: Person is not defined
    foo() // foo function
    
  • let的块级作用域的应用
    • btnEl的for循环

      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
      </head>
      <body>
      
        <button>按钮0</button>
        <button>按钮1</button>
        <button>按钮2</button>
        <button>按钮3</button>
      
        <script>
      
          // 监听按钮的点击
          const btnEls = document.querySelectorAll("button")
          // [btn1, btn2, btn3, btn4]
          // for (var i = 0; i < btnEls.length; i++) {
          //   var btnEl = btnEls[i];
          //   // btnEl.index = i
          //   (function(m) {
          //     btnEl.onclick = function() {
          //       debugger
          //       console.log(`点击了${m}按钮`)
          //     }
          //   })(i)
          // }
      
      
          for (let i = 0; i < btnEls.length; i++) {
            const btnEl = btnEls[i];
            btnEl.onclick = function() {
              console.log(`点击了${i}按钮`)
            }
          }
      
        </script>
      </body>
      </html>
      

1.6 var、let、const的选择

  • 对于var的使用:
    • var所表现出来的特殊性:比如作用域提升、window全局对象、没有块级作用域等都是一些历史遗留问题,其实是JavaScript在设计之初的一种语言缺陷
    • 在实际工作中,可以使用最新的规范来编写,也就是不再使用var来定义变量了
  • 对于let、const:
    • 优先推荐使用const,这样可以保证数据的安全性不会被随意的篡改
    • 只有当我们明确知道一个变量后续会需要被重新赋值时,这个时候再使用let
    • 这种在很多其他语言里面也都是一种约定俗成的规范,尽量也遵守这种规范

二、函数扩展

2.1 默认参数

  • 写法:在参数后面写默认值
function foo(arg1 = "我是默认值", arg2 = "我也是默认值") {
  // 1.两种写法不严谨
  // 默认值写法一:
  // arg1 = arg1 ? arg1: "我是默认值"

  // 默认值写法二:
  // arg1 = arg1 || "我是默认值"

  // 2.严谨的写法
  // 三元运算符
  // arg1 = (arg1 === undefined || arg1 === null) ? "我是默认值": arg1

  // ES6之后新增语法: ??
  // arg1 = arg1 ?? "我是默认值"

  // 3.简便的写法: 默认参数
  console.log(arg1)
}
  • 注意事项:
    • 默认参数是不会对null进行处理的
    • 有默认参数的形参尽量写到后面
    • 有默认参数的形参, 是不会计算在length之内(并且后面所有的参数都不会计算在length之内)
    • 剩余参数也是放到后面(默认参数放到剩余参数的前面)
function foo(age, name = "zhangsan", ...args) {
  console.log(name, age, args)
}

foo(18, "abc", "cba", "nba")

console.log(foo.length) // 1
  • 默认参数的解构
function foo({ name = "lilei", age = 18 } = {}) {
  console.log(name, age)
}

foo()

2.2 剩余参数

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

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

    • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参
    • arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作
    • arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6中提供并且希望以此来替代arguments的
function foo(num1, num2, ...otherNums) { 
  // otherNums数组 
  console.log(otherNums) // [111, 222, 333] 
} 

foo(20, 30, 111, 222, 333)
  • 注意:剩余参数必须放到最后一个位置,否则会报错。

2.3 扩展运算符

  • 展开语法(Spread syntax):
    • 可以在函数调用/数组构造时,将数组表达式或者string在语法层面展开
    • 可以在构造字面量对象时, 将对象表达式按key-value的方式展开
// 1.基本演练
// ES6
const names = ["abc", "cba", "nba", "mba"]
const str = "Hello"

// const newNames = [...names, "aaa", "bbb"]
// console.log(newNames)

function foo(name1, name2, ...args) {
  console.log(name1, name2, args)
}

foo(...names) // abc cba ['nba', 'mba']
foo(...str) // H e ['l', 'l', 'o']

// ES9(ES2018)
const obj = {
  name: "lilei",
  age: 18
}
// 不可以这样来使用
// foo(...obj) // 在函数的调用时, 用展开运算符, 将对应的展开数据, 进行迭代
// 可迭代对象: 数组/string/arguments

const info = {
  ...obj,
  height: 1.68,
  address: "北京市"
}
console.log(info) // {name: 'lilei', age: 18, height: 1.68, address: '广州市'}
  • 注意:展开运算符其实是一种浅拷贝
const obj = {
  name: "lilei",
  age: 18,
  height: 1.68,
  friend: {
    name: "hanmeimei"
  }
}

// 1.引用赋值
// const info1 = obj

// 2.浅拷贝
const info2 = {
  ...obj
}
info2.name = "kobe"
console.log(obj.name) // lilei
console.log(info2.name) // kobe
// info2.friend.name = "james"
// console.log(obj.friend.name) // james


// 3.深拷贝
// 方式一: 第三方库
// 方式二: 自己实现
// function deepCopy(obj) {}
// 方式三: 利用现有的js机制, 实现深拷贝JSON
const info3 = JSON.parse(JSON.stringify(obj))
console.log(info3.friend.name) // hanmeimei
info3.friend.name = "james"
console.log("info3.friend.name:", info3.friend.name) // james
console.log(obj.friend.name) // hanmeimei

2.4 箭头函数

  • 传送门:JavaScript-箭头函数
  • 补充:
    • 箭头函数是没有显式原型prototype的,所以不能作为构造函数,使用new来创建对象
    • 箭头函数也不绑定this、arguments、super参数
// 在ES6之后, 定义一个类要使用class定义
var bar = () => {}
console.log(bar.__proto__ === Function.prototype) // true
// 没有显式原型
console.log(bar.prototype) // undefined
// var b = new bar() // 报错

三、对象扩展

字面量的增强

const name = "kobe"
const age = 24

const key = "address" + " city"

const obj = {
  // 1.属性的增强
  // name: name,
  name,
  age,

  // 2.方法的增强
  running: function() {
    console.log(this)
  },
  swimming() {
    console.log(this)
  },
  eating: () => {
    console.log(this)
  },

  // 3.计算属性名
  [key]: "北京"
}

obj.running() // obj
obj.swimming() // obj
obj.eating() // window

四、字符串扩展

4.1 模板字符串

  • ES6允许使用字符串模板来嵌入JS的变量或者表达式来进行拼接:
    • 使用 `` 符号来编写字符串,称之为模板字符串,在模板字符串中,我们可以通过 ${expression} 来嵌入动态的内容
const name = "lilei"
const age = 18

// ES6之前
// const info = "my name is" + name + ", age is " + age

// ES6之后
const info = `my name is ${name}, age is ${age}`
console.log(info)

4.2 标签模板字符串

  • JavaScript的函数如果使用标签模板字符串,并且在调用的时候插入其他的变量:
    • 模板字符串被拆分了
    • 第一个元素是数组,是被模块字符串拆分的字符串组合
    • 后面的元素是一个个模块字符串传入的内容
// 标签模板字符串的用法
const name = "lilei"
const age = 18

function foo(...args) {
  console.log("参数:", args)
}

// foo("kobe", 24, 1.88)
foo`my name is ${name}, age is ${age}, height is ${1.88}` // 参数: [Array(4), 'lilei', 18, 1.88]

五、解构赋值

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

  • 具体使用见代码:
const names = ["abc", "cba", undefined, "nba", "mba"]

// 1.数组的解构
// 1.1. 基本使用
// const [name1, name2, name3] = names
// console.log(name1, name2, name3)

// 1.2. 顺序问题: 严格的顺序
// const [name1, , name3] = names
// console.log(name1, name3)

// 1.3. 解构出数组
// const [name1, name2, ...newNames] = names
// console.log(name1, name2, newNames)

// 1.4. 解构的默认值
const [name1, name2, name3 = "default"] = names
console.log(name1, name2, name3)


// 2.对象的解构
const obj = { name: "kobe", age: 24, height: 1.88 }
// 2.1. 基本使用
// const { name, age, height } = obj
// console.log(name, age, height)

// 2.2. 顺序问题: 对象的解构是没有顺序, 根据key解构
// const { height, name, age } = obj
// console.log(name, age, height)


// 2.3. 对变量进行重命名
// const { height: wHeight, name: wName, age: wAge } = obj
// console.log(wName, wAge, wHeight)

// 2.4. 默认值
const { 
  height: wHeight, 
  name: wName, 
  age: wAge, 
  address: wAddress = "中国"
} = obj
console.log(wName, wAge, wHeight, wAddress)

// 2.5. 对象的剩余内容
const {
  name,
  age,
  ...newObj
} = obj
console.log(newObj) // {height: 1.88}


// 应用: 在函数中(其他类似的地方)
function getPosition({ x, y }) {
  console.log(x, y)
}

getPosition({ x: 10, y: 20 })
getPosition({ x: 25, y: 35 })

function foo(num) {}

foo(123)

六、数值扩展

6.1 规范二进制和八进制的写法

console.log(100)
// 二进制
console.log(0b100) // 4
// 八进制
console.log(0o100) // 64
// 十六进制
console.log(0x100) // 256

6.2 ES12新增数字连接符

  • 在ES12(2021)新增特性:数字过长时,可以使用_作为连接符
const money = 100_00_00_0000_00_00

七、Symbol

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

7.1 基本用法

  • 在ES6之前,对象的属性名都是字符串形式很容易造成属性名的冲突
    • 比如原来有一个对象,要在其中添加一个新的属性和值,但是不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性
  • Symbol用来生成一个独一无二的值
    • Symbol值是通过Symbol函数来生成的,生成后可以作为属性名,即在ES6中,对象的属性名可以使用字符串,也可以使用Symbol值
const s1 = Symbol()
const obj = {
  [s1]: "aaa" 
}
console.log(obj)

const s2 = Symbol()
obj[s2] = "bbb"
console.log(obj)

function foo(obj) {
  const sKey = Symbol()
  obj[sKey] = function() {}
  delete obj[sKey]
}

foo(obj)

7.2 Symbol作为属性名

const s1 = Symbol() // aaa
const s2 = Symbol() // bbb

// 1.加入对象中
const obj = {
  name: "kobe",
  age: 24,
  [s1]: "aaa",
  [s2]: "bbb"
}

// const obj1 = {}
// obj1[s1] = "aaa"
// obj2[s2] = "bbb"

// const obj2 = {}
// Object.defineProperty(obj2, s1, {
//   value: "aaa"
// })

// 2.获取symbol对应的key
console.log(Object.keys(obj))
console.log(Object.getOwnPropertySymbols(obj))
const symbolKeys = Object.getOwnPropertySymbols(obj)
for (const key of symbolKeys) {
  console.log(obj[key])
}

7.3 相同值的Symbol

  • 如何创建一个相同的Symbol
    • 使用Symbol.for方法来做到这一点,并且可以通过Symbol.keyFor方法来获取对应的key
// 1.Symbol函数直接生成的值, 都是独一无二
const s1 = Symbol("ccc")
console.log(s1.description) // ccc
const s2 = Symbol(s1.description)
console.log(s1 === s2) // false

// 2. 如果相同的key, 通过Symbol.for可以生成相同的Symbol值
const s3 = Symbol.for("ddd")
const s4 = Symbol.for("ddd")
console.log(s3 === s4) // true

// 获取传入的key
console.log(Symbol.keyFor(s4)) // ddd

八、Set集合

8.1 Set基本使用

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

  • 创建Set需要通过Set构造函数(暂时没有字面量创建的方式)
// 1.创建Set
const set = new Set()
console.log(set) // Set(0) {size: 0}

// 2.添加元素
set.add(10)
set.add(22)
set.add(35)
set.add(22)
console.log(set) // Set(3) {10, 22, 35}

const info = {}
const obj = {name: "obj"}
set.add(info)
set.add(obj)
set.add(obj)
console.log(set) // Set(5) {10, 22, 35, {…}, {…}}
  • 应用
    • 数组去重
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)
console.log(newNames)

8.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的遍历的

8.3 Set和数组的比较

  • Set是无序的,数组是有序的
    • Set使用forEach只有value值,没有index
  • Set只能使用add方法添加元素,数组可以通过unshiftpush添加元素
    • 数组的unshift添加元素很慢
  • Set查找元素用has方法,相比较数组的includes速度会快

8.4 WeakSet

  • 类似于Set,区别如下:
    • WeakSet中只能存放对象类型,不能存放基本数据类型
    • WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收
let obj1 = { name: "lilei" }
let obj2 = { name: "hanmeimei" }
let obj3 = { name: "jim" }

const weakSet = new WeakSet()
weakSet.add(obj1)
weakSet.add(obj2)
weakSet.add(obj3)
  • WeakSet常见的方法:
    • add(value):添加某个元素,返回WeakSet对象本身
    • delete(value):从WeakSet中删除和这个值相等的元素,返回boolean类型
    • has(value):判断WeakSet中是否存在某个元素,返回boolean类型
  • WeakSet只是对对象的弱引用,如果遍历获取到其中的元素,有可能造成对象不能正常的销毁
    • 所以存储到WeakSet中的对象是没办法获取的,不能够遍历,并且没有size属性
  • 应用
const pWeakSet = new WeakSet()
class Person {
  constructor() {
    pWeakSet.add(this)
  }

  running() {
    if (!pWeakSet.has(this)) {
      throw new Error("不能通过其他对象调用该类方法")
    }
    console.log("running~")
  }
}

let p = new Person()
// 销毁
// p = null
p.running()
const runFn = p.running
runFn()
const obj = { run: runFn }
obj.run()

九、Map映射

9.1 Map基本使用

  • Map用于存储映射关系
    • 对象存储映射关系只能用字符串(ES6新增了Symbol)作为属性名(key);
    • 某些情况下可能希望通过其他类型作为key,比如对象,这个时候会自动将对象转成字符串来作为key
const info = { name: "kobe" }
const info2 = { age: 24 }

// 1.对象类型的局限性: 不可以使用复杂类型作为key
// const obj = {
//   address: "北京市",
//   [info]: "哈哈哈",
//   [info2]: "呵呵呵"
// }
// console.log(obj) // {address: '北京市', [object Object]: '呵呵呵'}

// 2.Map映射类型
const map = new Map()
map.set(info, "aaaa")
map.set(info2, "bbbb")
console.log(map) // Map(2) {{…} => 'aaaa', {…} => 'bbbb'}

9.2 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进行遍历
// map.forEach(item => console.log(item)) // 拿到的是值

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

9.3 Map和对象的比较

  • Map可以以任意类型为key,Object只能以字符串为key
  • Map是有序的,Object是无序的
// Map是有序的
const m = new Map([
  ['key1', 'hello'],
  ['key2', 100],
  ['key3', { x: 100 }]
])
m.forEach((value, key) => console.log(key, value))
// Object是无序的
const obj = {  2: 20, 3: 30, 1: 10 }
console.log(Object.keys(obj)) // ['1', '2', '3']
  • Map虽然有序,但是操作很快
const obj = {}
for (let i = 0; i < 1000 * 10000; i++) {
    obj[i + ''] = i
}
console.time('obj find')
obj['5000000']
console.timeEnd('obj find') // obj find: 0.008056640625 ms
console.time('obj delete')
delete obj['5000000']
console.timeEnd('obj delete') // obj delete: 0.004150390625 ms

// Map 有多块
const m = new Map()
for (let i = 0; i < 1000 * 10000; i++) {
    m.set(i + '', i)
}
console.time('map find')
m.has('5000000')
console.timeEnd('map find') // map find: 0.010986328125 ms
console.time('map delete')
m.delete('5000000')
console.timeEnd('map delete') // map delete: 0.009033203125 ms

9.4 WeakMap

  • 类似于WeakMap,也是以键值对的形式存在的
    • WeakMap的key只能使用对象,不接受其他的类型作为key
    • WeakMap的key对对象的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象
  • WeakMap常见的方法:
    • set(key, value):在Map中添加key、value,并且返回整个Map对象
    • get(key):根据key获取Map中的value
    • has(key):判断是否包括某一个key,返回Boolean类型
    • delete(key):根据key删除一个键值对,返回Boolean类型
  • WeakMap也是不能遍历的,没有forEach方法,也不支持通过for of的方式进行遍历
  • 应用:Vue3响应式原理中用到

十、其他知识(后续补充)

  • Proxy、Reflect
  • Promise:后续会详细介绍
  • ES Module模块化开发