ES6小结

91 阅读12分钟

我正在参加「掘金·启航计划」

let 和 const

let

用来声明变量,只在let命令所在的代码块内有效,即块级作用域。不存在变量提升,不允许重复声明

function varTest() {
  var a = 1
  if (true) {
    var a = 2
    console.log(a) // 2
  }
  console.log(a) // 2
}

function letTest() {
  console.log(b) // ReferenceError: b is not defined
  let b = 1
  // let b = 2; // Uncaught SyntaxError: Identifier 'b' has already been declared
  if (true) {
    let b = 2
    console.log(b) // 2
  }
  console.log(b) // 1
}

letTest()的 if 语句中,可以再次声明变量 b,是因为变量 b 只在这个 if 语句中有效。 如果在 if 语句中使用var声明变量 b,会报错

let 很适合在 for 循环时声明索引变量

const

const声明一个只读的常量,必须初始化赋值。一旦声明,常量的值就不能改变,只在声明所在的块级作用域内有效。 不存在变量提升,不允许重复声明。复杂类型(数组、对象等)指针指向的地址不能更改,内部数据可以更改

const a = '123'
a = '234' // TypeError: Assignment to constant letiable

const arr = [1, 2, 3]
arr.push(4)
console.log(arr) // [1,2,3,4]
arr = []
console.log(arr) // 改变数组的指向会出错 Uncaught TypeError: Assignment to constant letiable

let 和 const 声明的全局变量不属于顶层对象的属性,只存在于块级作用域中

let a = 1
const b = 2
console.log(window.a) // undefined
console.log(window.b) // undefined

github.com/Advanced-Fr…

模板字符串

模板字符串(templatestring)是增强版的字符串,用反引号`标识,嵌入的变量名写在${}之中。

第一个用途,基本的字符串格式化。

const name = 'world'
// ES5
console.log('hello' + name)
// ES6
console.log(`hello${name}`)

第二个用途,做多行字符串或者字符串一行行拼接。

// ES5
let a =
  'Hi \
    Girl!'
// ES6
let say = `<div>
        <p>hello, world</p>
    </div>`

ES6 还提供了一些字符串方法,如下:

// 1.includes:判断是否包含参数字符串,返回布尔值
const str = 'welcome'
console.log(str.includes('e')) // true

// 2.repeat: 获取字符串重复n次
const str = 'he'
console.log(str.repeat(3)) // 'hehehe'
// 如果带入小数, Math.floor(num) 来处理
// s.repeat(3.1) 或者 s.repeat(3.9) 都当做 s.repeat(3) 来处理

// 3. startsWith 和 endsWith 判断是否以给定文本开始或者结束
const str = 'hello world!'
console.log(str.startsWith('hello')) // true
console.log(str.endsWith('!')) // true

解构赋值

数组的解构赋值

可以从数组中提取值,按照对应位置,对变量赋值。这种写法属模式匹配,只要等号两边的模式相同,左边的变量就会被赋予对应的值

let [a, b, c] = [1, 2, 3]
console.log(a, b, c) // 1 2 3

注意细节:

1、左右结构不同

let [a, b, c, d] = [1, 2, 3]
console.log(a, b, c, d) // 1 2 3 undefined

2、跳过部分

let [a, , c] = [1, 2, 3]
console.log(a, c) // 1 3

3、默认值

let [a, b, c, d = 666] = [1, 2, 3]
console.log(a, b, c, d) // 1 2 3 666

let [a = 11, b = 22, c, d = 666] = []
console.log(a, b, c, d) // 11 22 undefined 666

4、嵌套

let [a, b, c] = [1, 2, [3]]
console.log(a, b, c) // 1 2 [3]

let [a, b, [c]] = [1, 2, [3]]
console.log(a, b, c) // 1 2 3

对象的解构赋值

let { name, age } = { name: 'zgh', age: 22 }
console.log(name, age) // zgh 22

对象与数组解构的不同点

  • 数组的元素是按次序排列的,变量的取值由它的位置决定
  • 对象的属性没有次序,变量必须与属性同名,才能取到正确的值

函数参数的解构赋值

let f = ([a, b]) => a + b
f([1, 2]) // 3

上述代码可将数组[1, 2]看作一个参数param,即param = [1, 2]

函数

为函数的参数设置默认值

可以给函数的参数设置默认值,如果不指定该函数的参数值,就会使用默认参数值

function Person(name = 'zgh', num = 22) {
  const name = name || 'zgh'
  const num = num || 22
}
Person()
Person('Jack', 20)

如果没有设置默认值,调用时 num 传入 0,0 为 false,那么例子中的 num 结果就为 22 而不是 0

箭头函数

ES6 允许使用“箭头”(=>)定义函数

//1.不带参数
let sum = () => 1 + 2
//等同于
let sum = function () {
  return 1 + 2
}

//2.带一个参数
let sum = (a) => a
//等同于
let sum = function (a) {
  return a
}

//3.带多个参数,需要使用小括号将参数括起来
let sum = (a, b) => a + b
//等同于
let sum = function (a, b) {
  return a + b
}

//4.代码块部分多于一条语句需要用大括号将其括起来,并且使用return语句返回。
let sum = (a, b) => {
  let c = a + b
  return c
}

//5.返回对象,就必须用小括号把该对象括起来
let person = (name) => ({ name: 'zgh', age: 22 })
//等同于
let person = function (name) {
  return { name: 'zgh', age: 22 }
}

箭头函数的 this 指向

箭头函数本身是没有thisarguments的,在箭头函数中引用 this 实际上是调用的是定义时的父执行上下文的 this。简单对象(非函数)是没有执行上下文的。

  • 使用call,apply,bind都不能改变 this 指向
  • 箭头函数没有原型属性prototype
  • 不能用作构造函数,即 new 指令
let obj = {
  say() {
    let f1 = () => console.log(this)
    f1()
  }
}
let rs = obj.say
rs() // f1执行时,say函数指向window,所以f1中的this指向window
obj.say() // f1执行时,say函数指向obj,所以f1中的this指向obj

// 下面写法错误!
let Person = (name) => {
  this.name = name
}

对象

对象简写

  • 属性的简写

条件:属性的值是一个变量,且变量名称和键名是一致的

let name = 'zgh'
let age = 22

// ES5写法
let obj = { name: name, age: age }

// ES6写法
let obj = { name, age }
  • 方法的简写
// ES5写法
let obj = {
  hello: function () {
    console.log('hello')
  }
}

// ES6写法
let obj = {
  hello() {
    console.log('hello')
  }
}

Map

类似于对象,是键值对的结构,区别在于:对象中的键名只能是字符串或者Symbol,而 Map 里面的键可以是任意值。

//创建一个Map对象
let myMap = new Map()

//添加键值对
myMap.set('a', 'hello')
myMap.set([1, 2, 3], { name: 'zgh' })

// 也可以在声明时就添加键值对,二维数组
const user = new Map([
  ['foo', 'zgh'],
  ['baz', 23]
])

//查看集合中元素的数量
myMap.size

//获取相应的键值
myMap.get('a')

//删除一个键值对,然后再判断该键值对是否存在
myMap.delete('a')
myMap.has('a')

//删除Map集合中所有的键值对
myMap.clear()

Set

Set对象是一组不重复的、无序的值的集合,可以往里面添加、删除、查询数据。

Set本身是一个构造函数,用来生成Set数据结构

Set()接受具有iterable可迭代接口的数据结构作为参数,如数组、类数组、字符串等。 不能接受对象结构,否则报错Uncaught TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))

// 声明一个Set对象
let mySet = new Set()
let mySet2 = new Set([1, 2, 3])

// 添加元素
mySet.add(1)
mySet.add('hi')
mySet.add([2, 'hello'])

// 判断集合中是否存在一个元素1
mySet.has(1) // true

// 删除集合中的字符串
mySet.delete('hi')

// 获取集合中元素的数量
mySet.size // 3

// 遍历
mySet.forEach((item) => console.log(item))

// 删除集合中所有的元素
mySet.clear()

// 两个对象是不相等的
const set2 = new Set()
set2.add({})
set2.size // 1
set2.add({})
set2.size // 2

遍历操作

let mySet = new Set(['a', 'b', 'c'])

// entries()返回的遍历器同时包括键名和键值,二者一样
for (let i of mySet.entries()) {
  console.log(i)
}
// ["a", "a"]
// ["b", "b"]
// ["c", "c"]

// keys()返回键名
for (let i of mySet.keys()) {
  console.log(i)
}
// 'a'
// 'b'
// 'c'

// values()返回键值,结果同keys()
for (let i of mySet.values()) {
  console.log(i)
}

Set只存储唯一值,可用来数组去重

let arr = [1, 1, 2, 2, 3, 3]
let res1 = [...new Set(arr)] // [1, 2, 3]

// 或者使用 Array.from()
let res2 = Array.from(new Set(arr)) // [1, 2, 3]

也可以用来字符串去重

const str = [...new Set('ababbc')].join('')
console.log(str) // 'abc'

...操作符

...可以叫做 spread(扩展)或者 rest(剩余)操作符

剩余运算符一般会用在函数的参数里面。比如想让一个函数支持更多的参数,参数的数量不受限制,这个时候就可以使用剩余操作符

function Name(x, y, ...z) {
  console.log(x) // a
  console.log(y) // b
  console.log(z) //["c" "d" "e"]
}
Name('a', 'b', 'c', 'd', 'e')

剩余操作符后面的变量会变成一个数组,多余的参数会被放入这个数组中

扩展运算符用在数组的前面,作用就是将这个数组展开

const arr1 = ['a', 'b', 'c', 'd', 'e']
const arr2 = ['f', 'g']
const arr3 = [...arr1, ...arr2] // ["a", "b", "c", "d", "e", "f", "g"]

// 等同于concat
const arr4 = arr1.concat(arr2)
const obj1 = { a: 1, b: 2 }
const obj2 = { ...obj1, c: 3 } // {a: 1, b: 2, c: 3}

使用扩展运算符展开一个新的对象,第二个对象的属性值会覆盖第一个对象的同名属性值

const obj1 = { a: 1, b: 2, c: 3 }
const obj2 = { b: 30, c: 40, d: 50 }
const merged = { ...obj1, ...obj2 }
console.log(merged) // {a: 1, b: 30, c: 40, d: 50}

Class

通过 class 关键字,可以定义。class 可以看作只是一个语法糖, 它的绝大部分功能,ES5 都可以做到,新的 class 写法让对象原型的写法更加清晰、更像面向对象编程的语法

//ES5 中使用面向对象
function Person(name, age) {
  this.name = name
  this.age = age
  this.say = function () {
    console.log('hello')
  }
}
let obj = new Person('zgh', 22)
obj.say()

//ES6 中使用面向对象
class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  say() {
    console.log('hello')
  }
}
let obj = new Person('zgh', 22)
obj.say()

上面代码定义了一个,里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。即 ES5 的构造函数 Person, 对应 ES6 的 Person 类的构造方法。

Person 类除了构造方法,还定义了一个say方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。 另外,方法之间不需要逗号分隔,否则会报错。

function Foo() {}
Foo.prototype.constructor === Foo // true

const fo = new Foo()
fo.constructor === Foo // true

Foo.prototype默认有一个公有且不可枚举的construetor属性,这个属性引用的是对象关联的函数(上例中是 Foo)。 构造函数调用new Foo()创建的对象在__proto__上也有construetor属性,指向创建这个对象的函数

segmentfault.com/a/119000002…

Class 继承

class NBAPlayer {
  constructor(name, age, height) {
    this.name = name
    this.age = age
    this.height = height
  }
  say() {
    console.log(`我是${this.name},${this.age}岁,身高${this.height}cm`)
  }
}
class MVP extends NBAPlayer {
  constructor(name, age, height, year) {
    super(name, age, height)
    this.year = year
  }
  showMVP() {
    console.log(`我是${this.name},在${this.year}获得MVP!`)
  }
}
let r1 = new NBAPlayer('Jack', '39', '198')
r1.say()
let r2 = new MVP('Jack', '39', '198', '2010')
r2.showMVP()

extends关键字用于实现类之间的继承。子类继承父类的所有属性和方法,使用super可以调用父类的方法。

静态方法、静态属性

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前加上static关键字,则这个方法不会被实例继承, 而是直接通过类来调用,这就是静态方法。

class Foo {
  static f() {
    return '666'
  }
}
Foo.f() // '666'
let person = new Foo()
person.f() // TypeError: person.f is not a function

父类的静态方法可以被子类继承

class Foo {
  static f() {
    return '666'
  }
}

class Bar extends Foo {}
Bar.f() // "666"

静态方法也可以被super对象调用

class Foo {
  static f() {
    return '666'
  }
}
class Bar extends Foo {
  static g() {
    return super.f()
  }
}
Bar.g() // "666"

类的静态属性

// es6写法
class Foo {}
Foo.prop = 1

// es7写法,推荐这一种写法
class Bar {
  static prop = 1
  constructor() {
    console.log(Bar.prop)
  }
}

类的实例属性

类的实例属性可以用等式,写入类的定义之中

class Foo {
  state = { value: 1 }
  constructor() {
    console.log(this.state.value) // 1
  }
}

再看看 react 类组件写法,以前定义类的实例属性只能在constructor里面,现在可以写在外面

class Foo extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    }
  }
  modalRef = null
}

Promise

promise用同步编程的方式来编写异步代码,解决回调嵌套问题

new Promise((resolve, reject) => {})

Promise 的三种状态

  • resolved 成功
  • rejected 失败
  • pending 创建 promise 对象实例进行中

then 方法

分别指定resolved状态和rejected状态的回调函数,第二个参数可选(不推荐使用)。 返回的是一个新的Promise,支持链式调用

function pro(params) {
  return new Promise((resolve, reject) => {
    if (params) {
      resolve('hahaha')
    } else {
      reject('error')
    }
  })
}
pro(true).then(
  (res) => {
    console.log(res)
  },
  (err) => console.log(err)
)

Promise 本身是同步的,then 方法是异步的

const p = new Promise((resolve, reject) => {
  console.log(1)
  resolve(3)
})
p.then((res) => console.log(res))
console.log(2)

结果是 1、2、3

catch 方法

function Cat(ready) {
  return new Promise((resolve, reject) => {
    if (ready) {
      resolve('Tom')
    } else {
      reject('Kitty')
    }
  })
}
Cat(false)
  .then((res) => {
    console.log(res)
  })
  .catch((err) => console.log(err))

catch方法可以捕获错误,作用和 then(onFulfilled, onRejected) 当中的 onRejected 函数类似。

Cat(false)
  .then((res) => {
    console.log(tom)
  })
  .catch((err) => console.log(err))

示例未定义变量 tom,如果不使用 catch 会直接报错,终止程序。使用后不会报错,但会将错误信息传递到 catch 方法中,方便处理

catch语句和try/catch语句进行比较?

示例:以下代码输出什么?

try {
  ;(async function () {
    a().b().c()
  })()
} catch (e) {
  console.log(`执行出错:${e.message}`)
}

答案:Uncaught (in promise) ReferenceError: a is not defined

async定义了异步任务,而try catch无法捕获异步任务,所以无法执行catch语句, 改为同步即可await (async function() { a().b().c() })()

all 和 race 方法

Promise.all()提供并行执行异步操作的能力,将多个实例包装成一个新实例,返回全部实例状态变更后的结果数组(全部变更再返回)

Promise.race()将多个实例包装成一个新实例,返回全部实例状态优先变更后的结果(先变更先返回)

let p1 = new Promise(function (resolve) {
  setTimeout(function () {
    resolve('Hello')
  }, 3000)
})

let p2 = new Promise(function (resolve) {
  setTimeout(function () {
    resolve('world')
  }, 1000)
})

Promise.all([p1, p2]).then((res) => {
  console.log(res)
})

Promise.race([p1, p2]).then((res) => {
  console.log(res)
})

结果是 1 秒后打印出world,3 秒后打印出["Hello", "world"],表明Promise.all 方法会按照参数数组里面的顺序将结果返回。 Promise.race方法则是只要该数组中的Promise对象的状态发生变化(无论是resolve还是reject)该方法都会返回。

async、await

asyncawait用来处理异步问题

async放置在函数的前面,返回一个promise

await 只能在 async 函数里面使用,可以让 js 进行等待,直到一个 promise 执行并返回它的结果,js 才会继续往下执行

async function f() {
  let res = await axios.get(url)
  return res.data //  等待返回请求结果后才执行
}
f()

参考

generator

Generator 是一种异步编程解决方案,执行 Generator 函数会返回一个遍历器对象。两个特征:星号*、yield表达式

调用函数返回一个指向内部状态的指针,即遍历器对象。必须调用遍历器的next方法,使得指针移向下一个状态。 yield表达式就是暂停标志

function* g() {
  yield 'hello'
  yield 'world'
  return 'haha'
}
const ee = g() // 函数并不会立即执行
console.log(ee) // g {<suspended>}

console.log(ee.next()) // {value: "hello", done: false}
console.log(ee.next()) // {value: "world", done: false}
console.log(ee.next()) // {value: "haha", done: true}
console.log(ee.next()) // {value: undefined, done: true}

遍历器对象{value: "hello", done: false}表示 value 是yield表达式的值,done: false表示遍历还没有结束

Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。

function* gg() {
  console.log('666')
}
const g1 = gg()

setTimeout(() => {
  g1.next() // 1s后输出666
}, 1000)

yield表达式只能用在 Generator 函数里面

Proxy

外界对目标对象的访问可以被 Proxy 拦截,进行过滤和改写,意为“代理器”

let proxy = new Proxy(target, handler)
  • target 目标对象
  • handler 配置对象

在 ES6 之前,我们可以使用Object.defineProperty去保护对象的私有属性。例如:

let sign = { _appid: '12345678', _appkey: '666', desc: 'zgh的密钥' }

Object.defineProperties(sign, {
  _appid: {
    writable: false
  },
  _appkey: {
    writable: false
  }
})

但是如果想对多个属性进行保护,就得对多个属性进行声明writable: false,显然很麻烦,这时就可以用 Proxy 来解决这个问题

Proxy 意味着我们代理了这个对象,该对象所有的属性操作都会经过 Proxy

let sign = { _appid: '123456', _appkey: '666', desc: 'zgh的密钥' }
let signProxy = new Proxy(sign, {
  get(target, property, receiver) {
    return target[property]
  },
  set(target, propName, value, receiver) {
    if (propName !== 'desc') {
      console.log('该属性是私有属性,不允许修改!')
    } else {
      target[propName] = value
    }
  }
})
console.log(signProxy._appid) // "123456"
signProxy._appkey = 'dd' // 该属性是私有属性,不允许修改!
console.log(signProxy._appkey) // "666"

这时依然可以直接修改 sign 对象,如果希望对象完全不可修改,可以直接将 sign 写到 Proxy 的 target

应用场景:

  • 数据校检
  • 属性保护

示例

数据类型验证

有一个记账的对象,记录着用户的存款金额,为了方便以后计算,要保证存入的数据类型必须为Number

let account = { num: 8888 }

let proxyAccount = new Proxy(account, {
  get(target, property) {
    return target[property]
  },
  set(target, propName, propValue) {
    if (propName === 'num' && typeof propValue != 'number') {
      throw new TypeError('The num is not an number')
    }
    target[propName] = propValue
  }
})

proxyAccount.num = '666'
console.log(proxyAccount.num) // Uncaught TypeError: The num is not an number

空值合并运算符

写法:a ?? b,如果第一个参数是null/undefined,则??返回第一个参数,否则返回第二个参数。 效果等同于(a !== null && a !== undefined) ? a : b

let a
let b = a ?? 1 // 1
  • ??运算符的优先级非常低,仅略高于 ?=,使用时要考虑是否添加括号
  • 如果没有明确添加括号,不能将其与||&&一起使用

||的区别

  • || 返回第一个真值
  • ?? 返回第一个已定义的值

|| 无法区分 false0、空字符串""NaNnull/undefined

let a = 0
a || 1 // 1
a ?? 1 // 0

可选链

当位于 ?. 前面的值为 undefinednull 时,会立即阻止代码的执行,并返回 undefined

const obj = { name: 'zgh' }
obj?.a

三种形式:

  • obj?.pron
  • obj?.[pron]
  • obj.method?.()