JS高级 - ES6 ~ES13新增语法 (二)

200 阅读9分钟

函数增强

默认参数

// ES6之前编写默认参数
function sum(num1, num2, num3, num4) {
  // 编写默认参数方式一
  num1 = num1 ? num1 : 0

  // 编写默认参数方式二
  num2 = num2 || 0

  // 编写默认参数方式三
  // 前两种编写方式是具有缺陷的,因为类似于空字符串或0之类的转换为boolean后为false的值
  // 其也会触发对应的默认值,所以使用编写方式三更为的严谨
  num3 = [null, undefined].includes(num3) ? 0 : num3

  // 空值合并运算符是ES6后提供的一种新的默认值编写方式
  // 其本质是编写方式三的语法糖
  num4 = num4 ?? 0 // => 只有当num4的值为null或undefined的时候,才会使用对应的默认值

  console.log(num1 + num2 + num3 + num4)
}

sum()
// 当函数参数的值为undefined的时候,就会触发对应函数参数的默认值
function sum(num1 = 0, num2 = 0) {
  console.log(num1, num2)
  console.log(num1 + num2)
}

sum()

注意事项

函数参数默认值的注意事项:

  1. 默认参数的放置位置应该在剩余参数之前,没有默认值的参数之后
// 默认值参数推荐放置到没有默认值的参数后边 - 约定俗成 - 否则对应的参数位需要使用undefined进行占位
// 有默认值的参数需要方式到剩余参数之前 - 语法强制规范
function sum(num1, num2 = 0, ...nums) {
  console.log(num1, num2, nums)
}
  1. 有默认值的参数和剩余参数是不计算在函数的length属性值中的

    且自第一个有默认值的参数开始后边所有的参数全部都记入函数的length属性中

    即使存在没有默认值的属性

function sum(num1 = 0, num2, ...args) {
}

console.log(sum.length) // => 0

默认参数和解构结合

function getInfo({name = 'Klaus', age = 23} = {}) {
  console.log(name, age)
}

getInfo()

剩余参数

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

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

剩余参数必须放到最后一个位置,否则会报错

剩余参数和arguments的区别

剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参

arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作

剩余参数的产生目的是为了取代arguments,所以在ES6中推荐使用剩余参数取代arguments

箭头函数补充

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

    如果真的需要使用new关键字,推荐使用class进行类的封装

  • 箭头函数也不绑定this、arguments、super参数

    • 如果需要使用this,arguments,super参数,会去其上层作用域进行查找
    • 箭头函数不绑定super,所以构造函数不推荐写成箭头函数
    • 箭头函数作为Funtion的实例,依旧是存在自己的隐式原型的,其值为Function.prototype

展开运算符

展开语法(Spread syntax)可以在函数调用/数组构造时,将数组表达式或者string在语法层面展开,也可以在构造字面量对象时, 将对象表达式按key-value的方式展开

基本使用

function foo(...args) {
  console.log(args)
}

const name = 'Klaus'
const users = ['Klaus', 'Alex', 'Jhon']

const info = {
  name: 'Klaus',
  age: 23
}

// 如果在函数调用中使用展开运算符
// 其内部会迭代对应的内容,并依次取出对应的值作为参数传入
// 所以其对应的值必须是可迭代对象,例如:字符串,数组,arguments
// 对象默认不是可迭代的,所以默认情况下,函数参数中的展开运算符不可以用在对象上
foo(...name) // => [ 'K', 'l', 'a', 'u', 's' ]
foo(...users) // => [ 'Klaus', 'Alex', 'Jhon' ]
foo(...info) // error
const name = 'Klaus'
const users = ['Klaus', 'Alex', 'Jhon']

const info = {
  name: 'Klaus',
  age: 23
}

// 构造数组字面量的时候,可以使用展开运算符
console.log([...users, 'Steven']) // => [ 'Klaus', 'Alex', 'Jhon', 'Steven' ]

// 在构建对象字面量时,也可以使用展开运算符,这个是在ES2018(ES9)中添加的新特性
// 该行为中,会自动对Object.keys(info)的结果进行遍历,并依次进行浅拷贝赋值
// 所以 展开运算符其实是一种浅拷贝
const userInfo = {
  ...info,
  address: 'shanghai'
}

console.log(userInfo) // => { name: 'Klaus', age: 23, address: 'shanghai' }

数值表示

ES6开始,JS支持十进制表示,二进制表示,八进制表示和十六进制表示

// 十进制
console.log(100) // => 100

// 八进制
// tips: 浏览器控制台在输出的时候为了方便我们进行调试
// 会自动将非十进制数值转换为十进制后在进行输出
console.log(0o100) // => 64

// 十六进制
console.log(0x100) // => 256

// 二进制
console.log(0b100) // => 4

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

// 为了方便我们进行阅读,可以使用_对长数字进行分割
let num = 10_000_000

// 分割的位数是自定义的
// 因为其功能只是为了方便阅读,在进行实际处理的时候,并不会起到任何的实际作用
num = 1_0_00_000

Symbol

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

在ES6之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突

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

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

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

  • Symbol值是通过Symbol函数来生成的,生成后可以作为属性名
  • 也就是在ES6中,对象的属性名可以使用字符串,也可以使用Symbol值
// Symbol是函数 不是构造函数,不需要通过new关键字来进行调用
const s1 = Symbol() // => Symbol()
const s2 = Symbol()

// Symbol即使多次创建值,它们也是不同的:Symbol函数执行后每次创建出来的值都是独一无二的
console.log(s1 == s2) // => false
console.log(s1 === s2) // => false
// 我们也可以在创建Symbol值的时候传入一个描述description
// 默认情况下,打印symbol值 输出结果皆为 Symbol()
// 所以为了方便我们进行区分,我们可以为Symbol添加对应的description

// 描述符只能是字符串或数字 
// 如果是其它类型的值会被转换为字符串再作为其描述符
// 其实数字实际上也会被转换为字符串,但是TS中允许描述符的类型是数字或字符串
const s1 = Symbol('name')
console.log(s1) // => Symbol(name)

const user = {
  [s1]: 'Klaus'
}

console.log(user[s1]) // => Klaus
// Symbol值不能进行运算
const symbol = Symbol()

// console.log(symbol + 2) => error

// Symbol值可以转换为字符串和布尔值
console.log(symbol.toString()) // => 'Symbol()'
console.log(!!symbol) // => true
// Symbol的目的是为了创建一个独一无二的值
// 但有的时候,我们就希望创建相同的Symbol,这个时候可以使用Symbol.for方法

// 当Symbol.for所传入的key一致时,即认为这2个symbol值是相等的
// 如果不传参数,也会被认为是相同的key

// ps: Symbol.for方法的key其实就是其对应的参数
// 在Symbol方法中 其对应的参数名称为 description
// 他们虽然名称不一致,但是本质是一样的

// Symbol.for方法会先根据key去进行查找
// 如果对应的key已经被创建就直接返回对应的symbol值
// 如果对应的key没有被创建,那么会新建一个新的symbol值,并将其返回
const s1 = Symbol.for()
const s2 = Symbol.for()
console.log(s1 === s2) // => true
// 凡是使用Symbol方法创建出来的symbol值一定是独一无二的
// 只有使用Symbol.for方法创建出来的key才可能相同
const s = Symbol('foo')
const s1 = Symbol.for(s.description)
const s2 = Symbol.for(s.description)

console.log(s === s1) // => false
console.log(s1 === s2) // => true
// 对于description, 可以通过symbol的description属性去进行获取
let s = Symbol('bar')
console.log(s.description) // => bar

// 对于key, 可以通过symbol的keyFor方法进行获取
s = Symbol.for('baz')
console.log(Symbol.keyFor(s)) // => baz
const user = {
  name: 'Klaus',
  age: 23,

  [Symbol('address')]: 'shanghai',
  [Symbol('height')]: 1.88
}

// for-of 和 Object.keys,Object.getOwnPropertyNames 遍历出来的key只能是可迭代的非symbol属性
// JSON.stringify 方法将对象转换为字符串的时候,也会自动过滤symbol属性
console.log(Object.keys(user)) // => [ 'name', 'age' ]

// 如果我们需要获取到Symbol属性值,需要使用Object.getOwnPropertySymbols方法
console.log(Object.getOwnPropertySymbols(user)) // => [ Symbol(address), Symbol(height) ]

// Object.getOwnPropertySymbols方法的返回结果也是一个可迭代对象
for (const key of Object.getOwnPropertySymbols(user)) {
  console.log(user[key])
  /*
    =>
      shanghai
      1.88
  */
}

// 使用Reflect.ownKeys 可以获取对象自身的所有的属性名,包括symbol属性名,可迭代属性名和不可迭代属性名
console.log(Reflect.ownKeys(user)) // => [ 'name', 'age', Symbol(address), Symbol(height) ]

几个内置Symbol值

Symbol.toStringTag

const user = {
  name: 'Klaus',
  age: 23,

  // Symbol.toStringTag的值如果是一个函数,则该函数需要是一个get访问器函数
  get [Symbol.toStringTag]() {
    // Symbol.toStringTag方法需要返回一个字符串类型的值
    // 如果返回的值的类型不是字符串类型,js会直接使用默认的type类型
    // 例如 如果这里的Symbol.toStringTag方法返回值为2, 是一个number类型值
    // 那么user.toString()的结果为 [object Object]
    // 也就是直接使用默认的type值 Object, 而不会尝试将2转换为'2'
    return this.name
  }
}

console.log(user.toString()) // => [object Klaus]
const user = {
  name: 'Klaus',
  age: 23,
  // 如果Symbol.toStringTag的值是字符串,会自动作为get方法的返回值使用
  [Symbol.toStringTag]: 'Klaus'
}

console.log(user.toString()) // => [object Klaus]

Symbol.toPrimative

const obj = {
  name: '',

  // 将对象转换为基本数据类型时,会调用Symbol.toPrimitive指向的方法
  // 1. 当hint值为number, 表示 对象 -> number
  // 2. 当hint值为string, 表示 对象 -> string
  // 3. 当hint值为default, 表示 对象 -> string | number
  //    + 加号可能是数字相加 也可能是字符串拼接 --- hint的值就是default
  //    + 判等操作(即 == 和 != --> 全等操作不会进行任何隐式类型转换) --- hint的值也是default

  // 对象 -> boolean --- 不会调用Symbol.toPrimitive指向的方法
  [Symbol.toPrimitive]: hint => {
    console.log(hint)

    if (!hint) {
      console.log('boolean')
    }

    if (hint === 'number') {
      return 0
    } else if (hint === 'string') {
      return ''
    } else {
      return false
    }
  }
}

console.log(!!obj)