复习一下常用的ES6语法

·  阅读 1391

ES6 常用语法是一个面试中经常被问到的问题,我们来聊聊在工作用一般比较常用的ES6语法

一、模版字符串

es6 新增的模版字符串中可以使用${}用来执行JS表达式。 在括号中可以放:变量、算术计算、三元表达式、对象属性、创建对象、调用函数、访问数组元素以及表达式。 不可以放分支/判断、循环等程序结构。

let count = 1
let str = `我是一条字符串,后面的\${}里面可以放一个表达式${ count }`
复制代码

二、let与const

这里我会占用少量篇幅来解释一下变量提升以帮助理解var有什么问题,以及letconst解决了什么问题。

变量提升

在聊letconst解决了什么问题之前我们先搞懂什么是变量提升(Hoisting)

say()
console.log(myname)
var myname = '俊酱'

function say() {
    console.log('say hello')
}
复制代码

我们都知道,JS 代码的执行是按照从上往下的顺序执行的,上面这段代码如果按照这个思维逻辑,它的执行应该是这样的:

  • 当执行到第 1 行的时候,由于函数 say 还没有定义,所以执行应该会报错;
  • 假如能执行到第 2 行的时候,由于变量 myname 也未定义,所以同样也会报错。

但当我们真正将其放到浏览器中执行,却会得到这样的结果:

'say hello' // say 正常运行
undefined // myname 为 undefined
复制代码

我们将上面实例中的 var name = '小明'删除掉后执行会发现执行的结果为

'say hello'
Uncaught ReferenceError: name is not defined // 执行到 myname 这一句抛错了
复制代码

我们有以下结论:

  1. JS 在执行过程中,使用了未声明的变量,JS 执行会报错
  2. 在一个变量定义之前使用时不会报错,其值为undefined
  3. 在一个function函数定义之前使用可以正常运行这个函数

所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。 我们用代码来描述一下上面的示例是怎样在 JS 引擎中执行的

// 编译阶段
var myname = undefined
function say() {
    console.log('say hello')
}

// 执行阶段
say()
console.log(myname) // undefined
myname = "俊酱"
复制代码

通过上面的示例,简单的理解了一下 JS 中的变量提升,我们接下来聊varletconst

var 的问题

我们先来聊一下var的问题:

  • 存在变量提升问题,会打乱程序正常的执行顺序
  • 没有块级作用域,代码块内的变量会超出代码块的范围,影响外部的变量以及容易在不易察觉的情况下被覆盖。关于作用域可以看这篇👉点击前往
  • 全局作用域下声明的变量会挂载到 window 上
// 声明提前与没有块级作用域
console.log(a) // undefined   可以被访问到
console.log(b) // ❌ 抛错: b is not defined

if (false) {
    var a = 1
}

// 容易被覆盖或修改
var i = 10
for(var i = 0; i < 100; i++) {
    console.log(i) // 0 ~ 99
}
console.log(i) // 100

// 莫名就被改了
var count = 10
changeCount()
console.log(count) // 100

function changeCount() {
    count = 100
}

// var 在全局作用域中声明的变量会挂载到 window上
console.log(window.count) // 100
复制代码

let 与 const 解决 var 的问题

ES6 中新增的letconst两种定义变量的方式解决了上面var的问题。 let 的特性:

  1. 不能在声明前使用该变量
  2. 在相同的作用域内,不能声明两个同名的变量
  3. 全局作用域下声明的变量不会挂载到 window 上
//因为上面也抛错了就执行不到下面来了,所以请分开执行~
console.log(b) // ❌ 抛错 Cannot access 'b' before initialization
let b = 10

// 不能在相同作用域中声明同名变量
let b = 100 // ❌ 抛错 Identifier 'b' has already been declared

// 拥有了“块级作用域”
console.log(a) // ❌ 抛错 a is not defined
if (false) {
    let a = 1
}

// 全局作用域下声明的变量不会挂载到 window 上
let num = 200
window.num // undefined
复制代码

其中const除了拥有以上提到的let的特点以外,还有以下特点:

  1. 在定义时必须要有赋值
  2. 赋值以后不能改变其值,如果是引用类型可以修改值内部的属性。
const x // ❌ 抛错 Missing initializer in const declaration

const num = 1
num = 2 // ❌ 抛错 Assignment to constant variable.

const xiaoming = {
    name: '小明',
    age: 18
}

xiaoming = null // ❌ 抛错 Assignment to constant variable.

xiaoming.age = 19 // ✅ 对于引用类型的内部属性是可以修改的
复制代码

var、let和const的区别

区别varletconst
是否产生“块级作用域”
是否有变量提升
是否保存到 window 上
相同作用域能否重复声明变量
是否能提前使用
是否必须设置初始值
能否修改实际保存在变量中的原始类型值或引用类型地址

三、箭头函数

  1. 箭头函数内的 this 与函数外作用域的 this 保持一致。(👉点击这篇理解为什么getMyname能指向xiaoming这个对象中的name属性)
const xiaoming = {
    name: '小明',
    getName: () => {
        console.log(this.name)
    },
    getMyname: function() {
        console.log(this.name)
    },
}

xiaoming.getName() // undefined

xiaoming.getMyname() // '小明'
复制代码
  1. 箭头函数不绑定arguments(使用剩余参数是更好的选择)。
const foo = () => {
    console.log(arguments)
}
foo(1, 2) // ❌ 抛错 arguments is not defined

const foo1 = (...args) => {
    console.log(args)
}
foo1(1, 2) // ✅ [1, 2]
复制代码
  1. 没有supernew.target所以不能作为构造函数使用,通过new调用会抛错。
const Foo = (name) => {
    this.name = name
}

const f = new Foo('小明') // ❌ 抛错 Foo is not a constructor
复制代码
  1. 由于箭头函数没有自己的this指针(或者说箭头函数被永久的绑定为外部作用域),所以通过callapply方法调用时第一个参数会被忽略,后面的参数有效。
const foo = (p, q) => {
    console.log(this, p, q)
}

const xiaoming = { name: '小明' }
foo.call(xiaoming, 1, 2) // window, 1, 2

foo.apply(xiaoming, [1, 2]) // window, 1, 2

const temp = foo.bind(xiaoming, 1, 2)
temp() // window, 1, 2
复制代码

四、for of

对于需要获取数组内部的元素值,可以通过for of来获取。并且无法使用forEach的类数组也支持使用for of

const arr = ["冰墩墩", "小明", "雪容融"]

for (let item of arr) {
    console.log(item) // "冰墩墩"		"小明"		"雪容融"
}

function foo() {
    for (let item of arguments) {
        console.log(item) // "冰墩墩"		"小明"		"雪容融"
    }
}

foo("冰墩墩", "小明", "雪容融")
复制代码

for of不关心下标位置,只关心元素值, 这种优势在于处理数字下标的数组(或类数组)。 for of的缺点:

  • 无法获取到所取元素的下标,只能获得元素值
  • 无法控制遍历的顺序,只能从头到尾遍历
  • 无法遍历下标名为自定义下标的对象和关联数组

下面列一个表格针对几种常用的 for 做一个对比:

forforEachfor offor in
数字下标索引数组
类数组对象
自定义下标关联数组
对象
  • 下标为数字选择 for of
  • 下标为自定义字符串则选择for in

五、剩余参数

举个🌰,我们有一个add函数,可以对传进来的若干个参数进行相加,这在普通的function函数中很简单,在函数内可以通过arguments来做,但是如果我们使用的是箭头函数,无法使用arguments,这时候剩余参数就登场了。在函数形参处使用...自定义变量名就是剩余参数的语法。

// args 可以由自己自定义命名
const add = (...args) => {
    // args 就可以拿到传入的所有参数了
    return args.reduce((a, b) => a + b)
}
console.log(add(1,2,3,4,5,6)) // 21

// 或者这样使用
// 可以固定N个自己命名的参数,剩余的参数就由剩余参数去处理
const add1 = (n, m, ...args) => {
    return n + m + args.reduce((a, b) => a + b)
}
console.log(add1([1,2,3,4,5,6])) // 21
复制代码

剩余参数相较arguments有三个优点

  1. 支持箭头函数
  2. 生成的数组是纯正的数组类型
  3. 自定义命名

六、展开运算符

展开运算符的语法与剩余参数一模一样,但其使用场景是在非函数形参处使用时为展开运算符。 展开运算符的使用场景非常的广泛,我们简单举两个🌰

  1. 取最大值
Math.max(...[1, 2, 36, 21, 3]) // 36
复制代码
  1. 浅拷贝
const arr = [1, 2, 3, 4, 5]
const arr1 = [...arr]
console.log(arr === arr1) // false

const obj = {
	name: '小明',
  age: 16
}
const obj1 = { ...obj }
复制代码
  1. 数据合并
const arr = [1, 2, 3, 4]
const arr1 = [7, 10, 0, 9]
const arr2 = [100, ...arr, 200, ...arr1]

const obj = {
    name: '小明',
    age: 16
}

const obj1 = {
    name: '小光',
    ...obj
}

// obj1 = { name: '小明', age: 16 }
复制代码

七、解构

在ES6之前,我们要想使用对象成员,数组中的元素,都必须带着"对象名.""或数组名[下标]"的形式。这种方式如果嵌套的比较深可能会写成这样obj.p.r.a.......

但是在ES6中新增了解构,解构有三种形式:

  • 数组解构

数组结构根据数组下标返回对应的值,下标从0开始,中间不可跳跃

const [name, age] = ["小明", 18]
console.log(name, age) // "小明" 18

// react 的 useState 就是用的数组解构
const [state, dispatch] = useState(0)
复制代码
  • 对象解构

对象解构功能十分强大,可以重命名、键值简写合并、设置默认值、多层结构解构

const obj = {
    name: "小明",
    age: 18
}
// ES6之前,上面的这个 obj 中我想要拿到 name 和 age 需要这样写
console.log(obj.name, obj.age)

// className 本来是没有的,可以设置默认值防止解构失败
const { name, age, className = "三年二班" } = obj

// 针对多层结构的对象也可以一次解构出来
const obj1 = {
    p: {
        k: {
            j: 1
        }
    }
}
const { p: { k: { j } } } = obj1
console.log(p, k , j)
复制代码
  • 参数解构

参数解构基本跟对象解构一致

function foo(params) {
    const { name = "无名", age = 16 } = params
    console.log(name, age)
}

foo({ name: "小明" }) // "小明" 16
复制代码

八、class 与继承

关于面向对象的两篇👉 面向对象的特点如何创建对象与实现继承

基本使用

class 是ES6中新增的创建对象的方式,这种方式的本质与function构造函数是一样的,只是一种语法糖。

class Person {
    constructor(name, age, className) {
        this.name = name
        this.age = age
        if (className) {
            this.className = className
        }
    }
  
    static className = '一年二班' // 静态属性(不能通过this访问,只能通过Person访问)
  
    say(msg) { // 静态属性
        console.log(`${this.name}说了这些话:${msg}`)
    }
}

const xiaoming = new Person('小明', 18, '三年级')
console.log(xiaoming.className) // '三年级'

const xiaoguang = new Person('小光', 16)
console.log(xiaoguang.className)

复制代码

上面的写法相当于使用构造函数如下写:

function Person(name, age) {
    this.name = name
    this.age = age
}

Person.className = '一年二班' // 静态属性

Person.prototype.say = function(msg) { // 原型方法
    console.log(`${this.name}说了这些话:${msg}`)
}
复制代码

继承

我们以一个简易的飞机大战游戏来举例

飞机大战中从屏幕上方向下方移动的有敌机和空降补给(降落伞)两种类型对象。它们有共同属性xy表示当前位置,和fly方法,以及自己特定的属性或方法

class EnemyPlane { // 敌机
  constructor(x, y, score) {
    this.x = x
    this.y = y
    this.score = score
  }
  fly() {
    console.log('根据当前位置进行判断后,随机选择一个可移动的位置')
  }
  payScore() {
    console.log('被击落了,返回分数', this.score)
    return this.score
  }
}

class Parachute { // 降落伞
  constructor(x, y, award) {
    this.x = x
    this.y = y
    this.award = award
  }
  fly() {
    console.log('根据当前位置进行判断后,随机选择一个可移动的位置')
  }
  payAward() {
    console.log('被击落了,返回奖品', this.award)
    return this.award
  }
}
复制代码

我们发现这两者之间有很多的共通点,那么我们针对其进行代码优化——通过继承

class Enemy {
  constructor(x, y) {
    this.x = x
    this.y = y
  }
  fly() {
    console.log('根据当前位置进行判断后,随机选择一个可移动的位置')
  }
}

class EnemyPlane extends Enemy {
  constructor(x, y, score) {
    
    this.score = score
  }
  payScore() {
    console.log('被击落了,返回分数', this.score)
    return this.score
  }
}

class Parachute extends Enemy {
  constructor(x, y, award) {
  
    this.award = award
  }
  payAward() {
    console.log('被击落了,返回奖品', this.award)
    return this.award
  }
}
复制代码

其实以上代码是不能正常运行的,但我们先不关注这个。我们先来看看我们子类少写的fly有没有从父类那边继承过来(能否访问)

image.png

在子类中通过原型链查找是能够找到fly方法,这说明继承是成功的。我们再来尝试new一个敌机试试看

image.png

根据报错提示,我们需要在new EnemyPlane之前需要先在派生类(子类)中通过调用 super才能访问到this,简单来讲,就是我们在EnemyPlane中想要使用this就必须先调用super


class EnemyPlane extends Enemy {
  constructor(x, y, score) {
    super(x, y) // 通过调用 super 并传入 x, y
    this.score = score
  }
  payScore() {
    console.log('被击落了,返回分数', this.score)
    return this.score
  }
}

class Parachute extends Enemy {
  constructor(x, y, award) {
    super(x, y)
    this.award = award
  }
  payAward() {
    console.log('被击落了,返回奖品', this.award)
    return this.award
  }
}
复制代码

我们调用super以后就实现了整个继承的过程了。

九、Promise

在实际开发中,经常需要多个异步任务按照顺序去执行,后面的异步任务依赖前面的异步任务处理后的结果,我们给出一些实例代码,其中log信息与真实执行会存在一些出入,主要方便理解,具体事件执行时机或内容可在这个👉 JavaScript 的事件执行可视化的网站中查看。

Tips:我们使用 setTimeout 来模拟异步请求

function task1() {
  console.log('task1进入队列')
  // 真正进入队列的不是task1 这个函数,是setTimeout中的 function
  // 上面已经说过了,就不做过多的解释,可自行前往上面说的这个执行可视化网站查看
  setTimeout(function(){
    console.log('task1执行完成')
  }, 6000)
}

function task2() {
  console.log('task2进入队列')
  setTimeout(function(){
    console.log('task2执行完成')
  }, 5000)
}

function task3() {
  console.log('task3进入队列')
  setTimeout(function(){
    console.log('task3执行完成')
  }, 4000)
}

task1()
task2()
task3()
复制代码

假若我们按照上面的同时执行,那么这三个任务的执行流程如下图所示,无法达到我们想要的按照次序执行,并且也无法向后面执行的任务传递参数

在 promise 出来之前,我们一般都是使用回调函数来解决,将后续的函数(如task2)作为前面的函数(task1)的参数传入,然后在前面的函数中的异步任务执行完以后再执行这个参数(回调函数)

function task1(callback) {
  console.log('task1进入队列')
  setTimeout(function(){
    console.log('task1执行完成')
    callback(1)
  }, 6000)
}

function task2(res, callback) {
  console.log('task2进入队列')
  setTimeout(function(){
    console.log('task2执行完成', res)
    callback(res + 1)
  }, 5000)
}

function task3(res) {
  console.log('task3进入队列')
  setTimeout(function(){
    console.log('task3执行完成', res)
  }, 4000)
}

task1(
  function(res) { 
    task2(res, task3)
  }
)
// 'task1进入队列'
  // 'task1执行完成'				// 6s 后
  // 'task2进入队列'
    // 'task2执行完成', 1			// 6s + 5s 后
    // 'task3进入队列'
      // 'task3执行完成', 2			// 6s + 5s + 4s 后
复制代码

回调函数如果多了,就会形成很深的嵌套结构,形成所谓的回调地狱,这种方式极其不优雅,并且不利于开发与维护。

我们使用 promise 来看看(注意,setTimeout 仍是模拟异步请求所需耗时)

function task1() {
  console.log('task1进入队列')
  return new Promise((resolve, reject) => {
    setTimeout(function(){
      console.log('task1执行完成')
      resolve(1)
    }, 6000)
  })
}

function task2(res) {
    console.log('task2进入队列')
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('task2执行完成')
            resolve(res + 1)
        }, 5000)
    })
}

task1().then(res => task2(res)) // 简写 task1().then(task2)
复制代码

task1 中的 resolve 函数中传入的参数会传入给在 then 中接受的函数,如上面的例子中,then 中的 res 就是 resolve 传入的 1。

在面试中,经常会有面试官问,promise 解决了什么问题? 有些年轻的面试者可能会回答,解决了回调地狱的问题。 但其实,promise 除了解决了回调地狱的问题以外,我认为还提供了在 JavaScript 中进行异步编程的方式,以及提供了良好的异常处理能力

function task1(val) {
  console.log('task1 进入Task Queue')
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      console.log('task1 执行完成')
      if (val) {
        resolve('芜湖,task1 执行成功')
      } else {
        reject('哎呀,task1 出错了') // 出现异常就使用reject
      }
    }, 6000)
  })
}

function task2(val) {
    console.log('task2 进入Task Queue')
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('task2 执行完成')
            if (val) {
                resolve('芜湖,task2 执行成功')
            } else {
                reject('哎呀,task2 出错了')
            }
        }, 5000)
    })
}

function task3(val) {
    console.log('task3 进入Task Queue')
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('task3 执行完成')
            if (val) {
                resolve('芜湖,task3 执行成功')
            } else {
                reject('哎呀,task3 出错了')
            }
        }, 4000)
    })
}


task1(false)
  .then(task2)
  .then(task3)
  .catch(err => {
    console.log('错误处理', err)
  })
  .finally(() => {
    console.log('无论怎样都会执行,与前面的执行状态无关')
  })

// 'task1开始执行'
  // 'task1执行完成' 6s
  // '错误处理', '哎呀,task1 出错了'
  // '无论怎样都会执行,与前面的执行状态无关'
复制代码

推荐在MDN上了解更多,帖子主要是针对面试来聊的,重点还是这句:promise 除了解决了回调地狱的问题以外,我认为还提供了在 JavaScript 中进行异步编程的方式,以及提供了良好的异常处理能力。

另外还是再推荐一遍这个在线描述 JavaScript 事件循环的可视化网站,这对理解事件循环非常有用

十、async/awiat

Promise 虽然解决了回调地狱的问题,但是嵌套问题依然存在,并且不利于理解,而async/await就是解决这个问题。 async/await 提供将异步转化为同步编程的形式

async function task1() { // 异步请求
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(1)
    }, 1000)
  })
}

const res1 = await task1()
console.log(res1) // 1
const res2 = await task1()
console.log(res2) // 1
复制代码

在调用异步函数时,只需要在调用前加上await就可以让这段代码像书写同步代码一般,更加易于理解。 async/await在使用时有两个特点:

  1. 但是只要是写上了async关键字的函数,在调用时,都会返回一个promise,这使得不管我们这个函数内部是否是一个异步处理过程,都需要在调用前加上await
  2. 在使用await时,await所在的函数必须为async才可使用,否则报语法错误。这也造成了async语法污染。
// 专门针对语法污染进行解释demo
async function task1() { // 异步请求
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(1)
    }, 1000)
  })
}

async function getRes() {
  const res = await task1() // 比如我们在这里进行异步请求
  return res
}


function foo() {
  // 因为 getRes 经过了 async,所以这里就必须要使用 await
  // 但是使用 await,foo 就必须得加上 async
  // 所以如果有这种多重调用的话,就会形成一种污染
  const n = getRes()
  console.log(n) // 得到的是一个 Promise
}

const m = await getRes()
console.log(m)
foo()
复制代码

我们将Promiseasync/await一起总结: Promise:

  1. 解决了异步请求中的回调地狱问题。
  2. 提供了一种在 JavaScript 中异步编程的方式。

Async/Await:

  1. 解决了Promise中的嵌套问题。
  2. 将异步代码变成同步代码书写的形式。
  3. async/await必须配合使用,可能会造成语法污染。

这篇断断续续写了好久😂有点对不起自己,继续加油!

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改