Js 中 ES6 基础用法增强

前几篇都是围绕 js 面向对象的实现, 重点是要在基于深度理解函数, 对象原型, 原型链的基础上, 去探索各种方法来实现类的继承, 最终也引入了 es6 中的 class 语法糖, 从而对其三大特性: 封装, 继承, 多态都进行了实现.

那接下来的这几篇, 则开始要探讨 es6+ 的补充知识了, 直到现在都发展到 ES16 了. 因此我们说的 es6 其实是指 es6+ 的版本都算, 那每年都有不一样的东西, 我们则重点关注几个核心的模块:

模块 核心关键词
1. 变量与作用域 let, const
2. 箭头函数 =>, this 继承
3. 模板字符串 Hello ${name}
4. 解构赋值 const {a, b} = obj
5. 参数增强 默认参数, ...rest
6. 模块化 import, export
7. 类 class, extends, super
8. Promise 异步, .then(), .catch()
9. 对象/数组增强 简写, 计算属性, find, includes
10. Symbol & Iterator Symbol(), for...of, 迭代器

但并不会按照上面从上到下的顺序来讲, 这个也是灵活安排的, 整体上都会覆盖到.

对象字面量增强

前几篇一直在讲面向对象, 那这里就先来讲 es6 中对字面的增强内容:

  • 属性简写
  • 方法简写
  • 计算属性
// 对象字面量增强

var name = 'youge'
var age = 18 


var obj = {
  // 1. 属性简写: key-value 同名时, 写 key 即可
  // name: name 
  name,
  age,

  // 2. 方法简写
  foo: function () {
    console.log('key: value 方式', this) // foo
  },

  bar() {
    console.log('方法() { } 方式', this) // bar
  }, 

  baz: () => {
    console.log(this) // window / {}
  },

  // 3. 计算属性
  [age + 10]: "age"


}

obj.baz()  // window / {}
obj.bar()  // obj
obj.foo()  // obj 

// 计算属性
console.log(obj) // '28': 'age',

注意箭头函数是没有 this 的哦, 说的方法简写是针对普通函数的 "key-value" 写法的改进.

数组对象解构

在 es6 中新增了一个从数组或对象中更方便获取数据 方法, 称为 解构 Destructuring.

  • 数组的解构包括: 基本解构, 顺序解构, 解构出数组, 默认值等.

  • 对象的结构包括: 基本结构, 任意顺序, 重命名, 默认等.

数组结构用的多的基本就顺序结构出来接收为主, 偶尔给它分拨类似剩余参数.

// 数组解构
var arr = ['a', 'b', 'c']

// var item1 = arr[0]
// var item2 = arr[1]
// var item3 = arr[2]

// 基础解构 
var [a, b, c, d] = arr
console.log(a, b, c, d) // d 为 undefiend

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

// 解构一个, 后面的元素放到新数组
var [a, ...restArr] = arr 
console.log(a, restArr) // a [b, c]

// 结构默认值, 默认 undefiend
var [a, b, c, d = 'nb'] = arr
console.log(a, b, c, d) // a b c nb 


对象的结构则用的非常高频, 相较于数组是根据索引, 而对象则根据 key 来结构 1 或者 多个均可.

// 对象结构

var obj = {
  name: 'youge',
  age: 18,
  height: 1.8
}

// 对象基本解构, key 要一致, 顺序随机
var {name, age, height} = obj 
console.log(name,age, height) // youge 18 1.8

// 可以结构一或多个, 没匹配则 undefiend
var {a, name} = obj 
console.log(a, name) // undefiend youge

// 可以改名字
var { name: newName } = obj 
console.log(newName) // youge

// 也可以给默认值
var { address: addr = "bj" } = obj
console.log(addr) // bj 


更多实际应用在函数传参结构的这种框架代码中.

// 对象解构应用

var obj = {
  name: 'youge',
  age: 18,
  height: 1.8
}

// 原来, 单独取出来, 再使用
function foo(info) {
  console.log(info.name, info.age)
}

foo(obj)

// 现在, 结构和使用一起
function bar({name, age}) {
  console.log(name, age)
}

// vuex 中的一段
async function getPageListDataAction({ commit } : payload) {
  const pageName = payload.pageName
  const pageUrl = `/${pageName}/list`
  if (pageUrl.length === 0) return 
  const { totalCount, list } = await getPageList(pageUrl, payload.queryInfo)
}

变量声明增强 let / const

在 es6 以前声明变量只能用 var 关键字, 自 es6 之后则新增了 letconst

从直观视角看, let 和 var 没啥大区别, 都用于声明一个变量. 这个 const 是 constant 的缩写, 表示常量,恒量等意思, 它表示保存的数据一旦被赋值则不能被修改. 但如果赋值的是引用类型, 则可以通过引用找到对象, 修改对象内容

注意: let 和 const 不允许 重复声明变量!

// let 和 const 

// let 和 var 基本差不多, 声明变量
let bar = 'bar'
bar = 'cj'
console.log(bar) // cj

// const 定义的值不能修改
// 引用类型存的是地址, 也没改, 对象属性可以改, 因为没被 const 修饰

const pi = 3.14
// pi = 3.15 // 报错

const obj = {
  name: 'youge'
}

// obj = {}, 这就属于改地址了

obj.name = 'cjj'
console.log(obj) // { name: 'cjj' }

// let 和 const 不能重复定义变量
let a = 10
let a = 20 // 报错

let / const 无作用域提升

用 var 声明的变量, 会在词法解析阶段就会被声明(提升), 只不过值为 undefiend

但我们使用 let 或 const 声明的变量, 在声明之前访问会报错的.

// let / const 无作用域提升

console.log(a)
let a = 10 // ReferenceError: Cannot access 'a' before initialization

但这并不意味着 这个 a 变量只有在执行阶段才会创建. 从 es 的描述来看, 这些变量会被创建在, 包含他们的词法环境被实例化时, 但却不可以被访问, 知道词法绑定被求值的时候.

因此对于 a 这个变量来说, 它在词法解析阶段是已经创建了的, 只是不能被访问而已. 作用域提升 是指在声明变量的作用域中, 变量可以在声明之前被访问 (词法解析).

而在这里虽然它会在 执行上下文阶段创建出来了, 但是不能被访问, 因此我们认为 let / const 没有进行作用域提升.

window 对象添加属性

之前通过 var 来声明一个变量, 会在 window 对象上添加一个属性.

但是 let 和 const 是不会在 window 对象中添加的. 它会连同环境一起添加到 变量环境 VE 中, 这个 VE 不一定是 window 对象. 比如 v8 引擎中, 是通过一个叫 VariableMap 的 hashmap 来实现存储的.

而 window 早期是作为 GO 对象, 在最新的实现中其实是浏览器添加的全局对象, 且一直保持和 var 之间值的相等性.

// window / VariableMap

var a = 10
var msg = 'js'

// 浏览器环境下
console.log(window.a) // 10
console.log(window.msg) // js 

window.msg = 'ts'
console.log(msg)  // ts 

let b = 20  // 放在 VariableMap 中, 而而非 window
 

块级作用域

在 es6 之前 js 只会形成两个作用域: 全局作用域, 函数作用域. 而现在新增了一种叫 块级作用域.

// 块级作用域

// 全局 + 函数作用域
var obj = { name: 'youge' }  // 全局
var a = 10 // 全局

function foo() {
  // foo 函数作用域
  function bar() {
    // bar 函数作用域
  }
}

// 新增块级作用域, 用 { } 包起来一个块块
{
  var b = 10
}

console.log(b) // 10, 好像块也没生效, 都能访问到

// 对 let/const/function/class 声明的类型有效
{
  let name = 'youge'

  function test() {
    console.log('block func test')
  }

  class Person {}
}

// console.log(name)  // 报错, 访问不到
// 为了兼容老代码, function 也设置为没有块作用域了

test() 

// var p = new Person()  // 报错, 访问不到

// for 语句也有块级作用域, 但以前能外部能访问
for (var i = 0; i < 5; i++) {
  console.log("hello, ", i)
}

console.log(i) // 5, 因为用 var 定义的

// 改为 用 let 就隔离了 
for (let j = 0; j < 5; j++) {
  console.log(i)
}

console.log(j) // 报错, 访问不了

通过let、const、function、class声明的标识符是具备块级作用域的限制, 但 function 也可以没有, 因为它要兼容一起的老代码. 还有这个 for 循环也会产生块级作用域, 用 var 声明能访问到, 但后面用 let 则就实现隔离啦.

继续来看一个小案例: 获取多个按钮监听点击

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <button>按钮1</button>
  <button>按钮2</button>
  <button>按钮3</button>
  <button>按钮4</button>

  <script>
    var btns = document.getElementsByTagName('button')

    // 给每个按钮绑定点击事件
    for (var i = 0; i < btns.length; i++) {
      btns[i].onclick = function() {
        console.log("第 " + i + ' 个按钮被点击啦...')
      }
    }

  </script>
</body>
</html>

可以发现,不管点击哪个按钮, 都是会打印 "第4个...". 因为这个 for 循环里面的函数, 没有形成块级作用域, 要访问 i 的会就会从上层, 即这里是全局去找, 此时循环完的话, i 为 4 了.

以前解决这种问题是通过将这个函数改为 IEE 立即执行函数, 即包一层函数,让它形成作用域, 而不去全局找.

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

}

而现在直接用 let 替代 var 就天然形成作用域了.

// 给每个按钮绑定点击事件, let 

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

在补充一点, 使用 for - of 遍历数组也会产生块作用域.

const arr = [1, 2, 3]

// 这里可以使用 const, 每次迭代会创建一个小白的块级变量 item
for (const item of arr) {
  console.log(item)
}

类似这样的过程:

// 第 1 次迭代
{
  const item = 1; // 创建一个新的 const 变量 item
  console.log(item); // 1
}

// 第 2 次迭代
{
  const item = 2; // 创建一个全新的 const 变量 item (和上一个同名但不同变量)
  console.log(item); // 2
}

// 第 3 次迭代
{
  const item = 3; // 再次创建一个全新的 const 变量 item
  console.log(item); // 3
}

var / let / const 的选择

这里再补充一个 暂时性死区 的概念. 即在一个代码中, 使用 let , const 声明的变量, 在声明之前变量是不允许访问的. 这种现象就是 temporal dead zone, 简称 TDZ. 这也符合常规思维认知, 变量就是要先声明, 再访问嘛.

那我们在定义变量的时候, 应该如何选择呢?

尽量抛弃 var 的使用

  • var 的特性, 如作用域提升, window 全局对象, 无块级作用域等, 都是 js 设计缺陷问题, 是不好的
  • var 更多是出现在一些笔面试中, 故意用 "缺陷" 来考察对 js 语言本身及底层理解

优先使用 const

  • 尽量优先选择 const, 这样可以保证数据的安全性, 不会被随意篡改值
  • 明确变量后续会被重新赋值时, 则再使用 let

现代的这种构建工具来创建项目, 如 webpack, vite, rollup 等, 在打包的时候, 都会将我们写的 es6, es5 等代码全部转为 es5 的代码. 但这并不影响我们在开发阶段的规范性.

模板字符串

在 es6 之前我们拼接变量和字符串是通过 "+" 来实现的, 就非常不灵活和麻烦, 而 es6 则引入模板字符串.

// 模板字符串-基本使用

const name = 'youge'
const age = 18
const height = 1.8

// es6 之前就比较麻烦
console.log("my name is " + name + ", age is " + age + ", height is " + height)

// es6 模板字符串能将变量灵活拼接进去
const msg = `my name is ${name}, age is ${age}, height is ${height}`
console.log(msg)

// 还能搞表达式
const msg2 = `double age is ${age * 2}`
console.log(msg2) // double age is 36

// 还能调用函数
function doubleAge() {
  return age * 2
}

const msg3 = `double age is ${doubleAge()}`
console.log(msg3) // double age is 36

这个很好用的, 要是没有它得自己解析, 还是有点难搞哦.

除了字符串拼接外, 它还有一种用法叫做: 标签模板字符串. 就是个骚操作吧

// 标签模板字符串

// 第一个参数仍是整个字符串, 只是被切成了多块, 放到了一个数组中
// 第二个参数是第一个的 ${} 变量

function foo(a, b) {
  console.log(a, b, '-----')
}

// 可通过标签模板字符串方式调用 foo``
foo``  // [ '' ] undefined -----

foo`youge` // [ 'youge' ] undefined -----

const name = 'cj'
const age = 18

// [ 'hello ', ' wo ', ' rld' ] cj -----
foo`hello ${name} wo ${age} rld` 

这种特性平时几乎不会用, 但是在一些框架中则会用, 比如在 react 中, 它编写 css 的方式中 css in js 是比较流行的, 对于模板来说我们定义的一个 div 其实会被设计为一个函数, 那给它添加样式的时候, 就可以用到这种方法啦.

// 通过标签模板方式调用函数
style.div`
  .banner {
		color: ${color};
  }
`

函数增强

es6 的函数也是加了很多特性, 包括默认参数, 剩余参数, 箭头函数等, 尤其是箭头函数, 在很多场景都非常有用.

默认参数

// 默认参数

// 以前写起来比较麻烦
// 传 0, null, "" 会产生 bug
function foo(a, b) {
  a = a || 'aaa'
  b = b || 'bbb'

  console.log(a, b)
}

foo()      // aaa, bbb 
foo(1, 2)  // 1, 2
foo(1)     // 1, bbb
foo(0, "") // aaa, bbb 

// es6 会提供默认参数
function foo2(a = "aaa", b = "bbb") {
  console.log(a, b)
}

foo2()      // aaa, bbb
foo2(1)     // 1, bbb
foo2(0, "") // 0, ""

这个剩余参数的实现原理, 就是通过对 arguments 的长度, 值判断. 具体过程可以用 babel 工具看看咋转的.

function foo2(a = "aaa", b = "bbb") {}

// babel 转换
function foo() {
  var a = arguments.length > 0 && arguments[0] !== undefiend? arguments[0]: "aaa"
  var b = arguments.length > 1 && arguments[1] !== undefiend? arguments[1]: "bbb"
}

对象参数也是可以用默认值和配合解构的.

// 对象参数和默认值, 以及解构

function printInfo({name, age} = {name: 'test', age: 18}) {
  console.log(name, age)
}

printInfo({name: 'cj', age: 30}) // cj, 30


// 另一种写法, 给默认值的同时 + 解构
function printInfo2({name = 'test', age = 18} = {}) {
  console.log(name, age)
}

printInfo2({name: 'cj', age: 30}) // cj, 30

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

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

剩余参数

这个 rest parameter 可以将不定数量的参数都放到一个数组中.

如果最后一个参数是 ... 为前缀 的, 则会将剩余的参数放到一个参数数组中. 注意不是展开运算符哈.

// 函数的剩余参数

function foo(a, b, ...args) {
  console.log(a, b)
  console.log(args)  // [3, 4, 5]
  
  console.log(arguments) // [1, 2, 3, 4,5]
}

foo(1, 2, 3, 4, 5) // 1, 2, [3, 4, 5]

这个剩余参数 ...args 和 arguments 的区别:

  • 剩余参数无没有对应形参的实参, 而 arguments 对象包含了传给函数的所有实参
  • rest 参数是个真正的数组, 有所有数组的方法, 而 arguments 只是一个类数组
  • rest 是为了替换调 arguments 而设计的

箭头函数

它没有自己的 this绑定 (父级作用域) , 也没有显式原型, 因此不能作为构造函数, 使用 new 去创建对象.

// 箭头函数, 无显示原型


// 普通函数
function bar() {}

console.log(bar.prototype) // {}

const obj = new bar()
console.log(obj.__proto__ === bar.prototype)  // true


// 箭头函数不能 new 
const foo = () => {
  console.log('foo func')
}

var obj2 = new foo() // 报错: foo is not a constructor
特性 普通函数 (function) 箭头函数 (=>)
this 绑定 动态绑定(调用时确定) 词法绑定(定义时确定,继承外层 this
arguments 对象 无(需用 ...rest 参数)
可用作构造函数 可以(new 调用) 不可以(会报错)
prototype 属性

展开语法

它可以在函数调用, 数组构造时, 将数组表达式或者 string 在语法层面展开.

还以而在构造字面量对象时, 将对象表达式按照 key-value 方式展开.

// 展开运算符

const names = ['yogue', 'cj', 'yaya']
const name = 'jack'

// 1. 函数调用时
function foo(a, b, c) {
  console.log(a, b, c)
}

// 不使用 ... 的话, 要用 apply 和指定 this
foo.apply(null, names) // yogue cj yaya

// 展开运算符就直接按顺序展开
foo(...names) // yogue cj yaya

// 也可以直接对字符串展开
foo(...name) // j a c

// 2. 构造数组时, 拼接起来
const newNames = [...names, 'lucy', 'jane']
console.log(newNames) // [ 'yogue', 'cj', 'yaya', 'lucy', 'jane' ]

// 3. 解构对象, 按照 k-v 展开
const info = { name: 'youge', age: 18 }

const obj = {...info, city: "长安镇"}
console.log(obj) // { name: 'youge', age: 18, city: '长安镇' }

// 数组也是特殊对象, 只不过 key 变成了索引
const arr = ['aa', 'bb']
console.log({...arr}) // { '0': 'aa', '1': 'bb' }

注意: 展开运算符, 做的是一个浅拷贝哦

// 展开运算符是浅拷贝

const info = {
  name: 'youge',
  friend: { name: 'cj'}
}

// 浅拷贝, 再修改第一层的 name 
const obj = { ...info, name: "yaya" }
console.log(obj) // { name: 'yaya', friend: { name: 'cj' } }

// 现在改 obj 里面的 friend.name, info 也会被改掉!

// 1. info 存的实际是引用地址, 地址里面, 有个对象又是一个地址
// 2. obj 是浅拷贝, 存的就是 info 的引用地址, 它也是指向 上面 info 指向的对象
// 3. 则 info.friends -> 对象 === obj.friends -> 对象
// 4. 因此只要对象的对象被改了, 另外一个也是跟着改, 因为都是引用的同一个对象

obj.friend.name = 'ccc'

console.log(info) // { name: 'youge', friend: { name: 'ccc' } }


特性 剩余参数 (Rest) 展开运算符 (Spread)
语法 ...parameterName ...iterableOrObject
位置 函数定义时, 最后一个参数 函数调用中数组字面量 [...]对象字面量 {...}
作用 收集多个参数 → 组合成一个数组 展开一个数组/对象 → 打散成多个元素/属性
结果 创建一个新数组 将元素/属性插入到新上下文中

Symbol 数据类型

它是在 es6 中新增的一个 基本数据类型, 翻译为符号.

在 es6 之前, 对象的属性名都是字符串, 就写的变量名, 底层也会转为字符串, 这很容易造成 属性名冲突.

尤其给一个大对象添加某个属性时, 当我不知道它原来有啥, 直接添加则容易 覆盖内部已有属性.

之前的 apply, call, bind 的实现时, 我们添加了一个 fn 属性, 这其实有 bug, 万一对象内部也有 fn 就完蛋了

因此, Symbol 这个基本数据类型, 就是来 生成一个独一无二的值, 有点像平时用的 uuid

  • Symbol 的值可以通过 Symbol函数 来创建, 生成后可作为 对象的属性名
  • Symbol 即使多次创建, 它的值也是不同的, 每次创建的都是独一无二的
// symbol 基本使用

// es6 之前, 对象的属性都会转为字符串
var obj = {
  name: "youge", 
  "age": 18,  // age 不加引用内置也会加
}

// 如果添加 重名的 属性, 则会进行覆盖
// 在不知道是否存在, 就麻烦了, 比如第三方库, 某个框架等
obj['name'] = 'yaya'
console.log(obj) // { name: 'yaya', age: 18 }
  
// es6 新增了 Symbol 它是唯一的, 类似 uuid, 但看不到的
const s1 = Symbol()
const s2 = Symbol()

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

// 虽然 s1 的值不知道是啥, 但是可以给它添加描述符
const s3 = Symbol('cj')
console.log(s3) // Symbol(cj)
console.log(s3.description) // cj 

// Symbol 值可作为对象的 key 

// 1. 在定义对象字面量时使用
// [s1] 是计算属性, 表示 s1 的值, 如 [1+2]: 'bbb' -> "3": "bbb"
const obj2 = {
  [s1]: "aaa",
  [s2]: "bbb"
}

console.log(obj2) // { [Symbol()]: 'aaa', [Symbol()]: 'bbb' }

// 2. 在新增属性时使用
obj2[s3] = "ccc"  
// { [Symbol()]: 'aaa', [Symbol()]: 'bbb', [Symbol(cj)]: 'ccc' }
console.log(obj2) 

// 3. 在 Object.defineProperty() 添加
const s4 = Symbol()
Object.defineProperty(obj, s4, {
  configurable: true,
  enumerable: true,
  writable: true,
  value: "nb"
})

console.log(obj) // { name: 'yaya', age: 18, [Symbol()]: 'nb' }

// 获取则通 [] 即可, 但不能通过 点语法来获取的哈
console.log(obj2[s1], obj2[s2]) // aaa, bbb 

console.log(obj2.s1) // undefined

// 4. 用 Symbol 作为 key 的属性名, Object.keys 获取不到 Symbol 
// 需要用 Object.getOwnPropertySymbols
console.log(Object.keys(obj)) // [ 'name', 'age' ]
console.log(Object.getOwnPropertyDescriptors(obj))  // 可以 

// [ Symbol(), Symbol(), Symbol(cj) ]
const keys = Object.getOwnPropertySymbols(obj2)
for (const key of keys) {
  console.log(key, obj2[key])
}

// Symbol() aaa
// Symbol() bbb
// Symbol(cj) ccc


虽说 Sybmol() 的是值是独一无二的, 但有的是有, 却希望能产生一样的值, 这通过 Symbol.for() 来实现

// 生成相同的 Symbol 值

// Symbol.for(key)
const s1 = Symbol.for('aaa')
const s2 = Symbol.for('aaa')

// for 这种方式, 是会根据描述符来判断, 存在则复用
console.log(s1 === s2)
console.log(s1, s2) // Symbol(aaa) Symbol(aaa)

// key 是可以获取的 
const key = Symbol.keyFor(s1)
const key2 = Symbol.keyFor(s2)
console.log(key, key2) // aaa aaa 

至此, 关于 es6 的知识拓展上篇就先到这了, 主要是一些作用域增强, 函数增强, 字符串增强, 数据类型增强等, 难度不大, 了解这些基本特性和会使用即可. 而下篇则会重点将 es6 新增的数据结构篇, 如 set , map 等, 先到这吧.

posted @ 2025-08-18 23:08  致于数据科学家的小陈  阅读(11)  评论(0)    收藏  举报