ES6 常用语法是一个面试中经常被问到的问题,我们来聊聊在工作用一般比较常用的ES6语法
一、模版字符串
es6 新增的模版字符串中可以使用${}
用来执行JS表达式。
在括号中可以放:变量、算术计算、三元表达式、对象属性、创建对象、调用函数、访问数组元素以及表达式。
不可以放分支/判断、循环等程序结构。
let count = 1
let str = `我是一条字符串,后面的\${}里面可以放一个表达式${ count }`
复制代码
二、let与const
这里我会占用少量篇幅来解释一下变量提升以帮助理解var
有什么问题,以及let
和const
解决了什么问题。
变量提升
在聊let
与const
解决了什么问题之前我们先搞懂什么是变量提升(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 这一句抛错了
复制代码
我们有以下结论:
- JS 在执行过程中,使用了未声明的变量,JS 执行会报错
- 在一个变量定义之前使用时不会报错,其值为
undefined
- 在一个
function
函数定义之前使用可以正常运行这个函数
所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。
我们用代码来描述一下上面的示例是怎样在 JS 引擎中执行的
// 编译阶段
var myname = undefined
function say() {
console.log('say hello')
}
// 执行阶段
say()
console.log(myname) // undefined
myname = "俊酱"
复制代码
通过上面的示例,简单的理解了一下 JS 中的变量提升,我们接下来聊var
、let
和const
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 中新增的let
与const
两种定义变量的方式解决了上面var
的问题。
let 的特性:
- 不能在声明前使用该变量
- 在相同的作用域内,不能声明两个同名的变量
- 全局作用域下声明的变量不会挂载到 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
的特点以外,还有以下特点:
- 在定义时必须要有赋值
- 赋值以后不能改变其值,如果是引用类型可以修改值内部的属性。
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的区别
区别 | var | let | const |
---|---|---|---|
是否产生“块级作用域” | ❌ | ✅ | ✅ |
是否有变量提升 | ✅ | ❌ | ❌ |
是否保存到 window 上 | ✅ | ❌ | ❌ |
相同作用域能否重复声明变量 | ✅ | ❌ | ❌ |
是否能提前使用 | ✅ | ❌ | ❌ |
是否必须设置初始值 | ❌ | ❌ | ✅ |
能否修改实际保存在变量中的原始类型值或引用类型地址 | ✅ | ✅ | ❌ |
三、箭头函数
- 箭头函数内的 this 与函数外作用域的 this 保持一致。(?点击这篇理解为什么
getMyname
能指向xiaoming
这个对象中的name
属性)
const xiaoming = {
name: '小明',
getName: () => {
console.log(this.name)
},
getMyname: function() {
console.log(this.name)
},
}
xiaoming.getName() // undefined
xiaoming.getMyname() // '小明'
复制代码
- 箭头函数不绑定
arguments
(使用剩余参数是更好的选择)。
const foo = () => {
console.log(arguments)
}
foo(1, 2) // ❌ 抛错 arguments is not defined
const foo1 = (...args) => {
console.log(args)
}
foo1(1, 2) // ✅ [1, 2]
复制代码
- 没有
super
、new.target
所以不能作为构造函数使用,通过new调用会抛错。
const Foo = (name) => {
this.name = name
}
const f = new Foo('小明') // ❌ 抛错 Foo is not a constructor
复制代码
- 由于箭头函数没有自己的this指针(或者说箭头函数被永久的绑定为外部作用域),所以通过
call
、apply
方法调用时第一个参数会被忽略,后面的参数有效。
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 做一个对比:
for | forEach | for of | for 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
有三个优点
- 支持箭头函数
- 生成的数组是纯正的数组类型
- 自定义命名
六、展开运算符
展开运算符的语法与剩余参数一模一样,但其使用场景是在非函数形参处使用时为展开运算符。
展开运算符的使用场景非常的广泛,我们简单举两个?
- 取最大值
Math.max(...[1, 2, 36, 21, 3]) // 36
复制代码
- 浅拷贝
const arr = [1, 2, 3, 4, 5]
const arr1 = [...arr]
console.log(arr === arr1) // false
const obj = {
name: '小明',
age: 16
}
const obj1 = { ...obj }
复制代码
- 数据合并
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}`)
}
复制代码
继承
我们以一个简易的飞机大战游戏来举例
飞机大战中从屏幕上方向下方移动的有敌机和空降补给(降落伞)两种类型对象。它们有共同属性
x
,y
表示当前位置,和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
有没有从父类那边继承过来(能否访问)
在子类中通过原型链查找是能够找到fly
方法,这说明继承是成功的。我们再来尝试new
一个敌机试试看
根据报错提示,我们需要在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
在使用时有两个特点:
- 但是只要是写上了
async
关键字的函数,在调用时,都会返回一个promise
,这使得不管我们这个函数内部是否是一个异步处理过程,都需要在调用前加上await
。 - 在使用
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()
复制代码
我们将Promise
与async/await
一起总结:
Promise:
- 解决了异步请求中的回调地狱问题。
- 提供了一种在 JavaScript 中异步编程的方式。
Async/Await:
- 解决了Promise中的嵌套问题。
- 将异步代码变成同步代码书写的形式。
- async/await必须配合使用,可能会造成语法污染。
这篇断断续续写了好久?有点对不起自己,继续加油!