ES6/ES7/ES8/ES9/ES10/ES11/ES12/ES13语法

228 阅读12分钟

ECMAScript是一种由Ecma国际(前身为欧洲计算机制造商协会,European Computer Manufacturers Association)通过ECMA-262标准化的脚本程序设计语言。这种语言在万维网上应用广泛,它往往被称为JavaScriptJScript,所以它可以理解为是JavaScript的一个标准,但实际上后两者是ECMA-262标准的实现和扩展。

ES6(ECMAScript2015)语法

新的声明方式

作用域

什么是作用域?

几乎所有编程语言就是在变量中存储值,并且能读取和修改此值。事实上,在变量中存储值和取出值的能力,给程序赋予了状态。 如果没有这样的概念,一个程序虽然可以执行一些任务,但是它们将会受到极大的限制而且不会非常有趣。 但是这些变量该存储在哪,又给如何读取?为了完成这个目标,需要制定一些规则,这个规则就是:作用域。

常见的作用域主要分为几个类型:全局作用域、函数作用域、块状作用域、动态作用域。

对象类型
global/window全局作用域
function函数作用域(局部作用域)
{}块状作用域
this动态作用域
全局作用域

变量在函数或者代码块 {} 外定义,即为全局作用域。不过,在函数或者代码块 {} 内未定义的变量也是拥有全局作用域的(不推荐)。

var course = "es"

// 此处可调用 course 变量
function myFunction() {
    // 函数内可调用 course 变量
}

上述代码中变量 course 就是在函数外定义的,它是拥有全局作用域的。这个变量可以在任意地方被读取或者修改,当然如果变量在函数内没有声明(没有使用 var 关键字),该变量依然为全局变量。

// 此处可调用 course 变量

function myFunction() {
    course = "es"
    // 此处可调用 course 变量
}
函数作用域

在函数内部定义的变量,就是局部作用域。函数作用域内,对外是封闭的,从外层的作用域无法直接访问函数内部的作用域!

function bar() {
    var testValue = 'inner'
}

console.log(testValue) // 报错:ReferenceError: testValue is not defined

如果想读取函数内的变量,必须借助 return 或者闭包。

function bar(value) {
    var testValue = 'inner'

    return testValue + value
}

console.log(bar('fun')) // "innerfun"

这是借助 return 的方式,下面是闭包的方式:

function bar(value) {
    var testValue = 'inner'

    var rusult = testValue + value

    function innser() {
        return rusult
    }

    return innser()
}

console.log(bar('fun')) // "innerfun"

通俗的讲,return 是函数对外交流的出口,而 return 可以返回的是函数,根据作用域的规则,函数内部的子函数是可以获取函数作用域内的变量的。

块状作用域

在其他编程语言中,块状作用域是很熟悉的概念,但是在JavaScript中不被支持,就像上述知识一样,除了全局作用域就是函数作用域,一直没有自己的块状作用域。在 ES6 中已经改变了这个现象,块状作用域得到普及。关于什么是块,只要认识 {} 就可以了。

if (true) {
    let a = 1
    console.log(a)
}

在这个代码中, if 后 {} 就是“块”,这个里面的变量就是拥有这个块状作用域,按照规则, {} 之外是无法访问这个变量的。

动态作用域

在 JavaScript 中很多同学对 this 的指向时而清楚时而模糊,其实结合作用域会对 this 有一个清晰的理解。不妨先来看下这段代码:

window.a = 3

function test() {
    console.log(this.a)
    console.log(a); //2
}

test.bind({
    a: 2
})() // 2
test() // 3
console.log(this.a) //3

在这里 bind 已经把作用域的范围进行了修改指向了 { a: 2 },而 this 指向的是当前作用域对象,是不是可以清楚的理解了呢?

接下来我们再思考另一个问题:作用域是在代码编写的时候就已经决定了呢,还是在代码执行的过程中才决定的?

var course = " es"

// 此处可调用 course 变量
function myFunction() {
    // 函数内可调用 course 变量
}

在看看这段代码,写代码的时候就知道 course 就是全局作用域,函数内部的用 var 定义的变量就是函数作用域。这个也就是专业术语:词法作用域。 通俗的讲变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。 相反,只能在执行阶段才能决定变量的作用域,那就是动态作用域。

let

ES6 新增了let命令,用来声明变量。

1. let 声明的全局变量不是全局对象window的属性

这就意味着,你不可以通过 window. 变量名 的方式访问这些变量,而 var 声明的全局变量是 window 的属性,是可以通过 window. 变量名 的方式访问的。

var a = 5
console.log(window.a) // 5
let a = 5
console.log(window.a) // undefined

2. 用let定义变量不允许重复声明

这个很容易理解,使用 var 可以重复定义,使用 let 却不可以。

var a = 5
var a = 6

console.log(a) // 6

如果是 let ,则会报错

let a = 5
let a = 6
// VM131:1 Uncaught SyntaxError: Identifier 'a' has already been declared
//   at <anonymous>:1:1

3. let声明的变量不存在变量提升

function foo() {
    console.log(a)
    var a = 5
}

foo() //undefined

上述代码中, a 的调用在声明之前,所以它的值是 undefined,而不是 Uncaught ReferenceError。实际上因为 var 会导致变量提升,上述代码和下面的代码等同:

function foo() {
    var a
    console.log(a)
    a = 5
}

foo() //undefined

而对于 let 而言,变量的调用是不能先于声明的,看如下代码:

function foo() {
    console.log(a)
    let a = 5
}

foo()
// Uncaught ReferenceError: Cannot access 'a' before initialization

在这个代码中, a 的调用是在声明之前,因为 let 没有发生变量提升,所有读取 a 的时候,并没有找到,而在调用之后才找到 let 对 a 的定义,所以按照 tc39 的定义会报错。

4. let声明的变量具有暂时性死区

只要块级作用域内存在 let 命令,它所声明的变量就绑定在了这个区域,不再受外部的影响

var a = 5
if (true) {
    a = 6
    let a
}
// Uncaught ReferenceError: Cannot access 'a' before initialization

上面代码中,存在全局变量 a ,但是块级作用域内 let 又声明了一个局部变量 a ,导致后者绑定这个块级作用域,所以在let声明变量前,对 a 赋值会报错。

ES6 明确规定,如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”

有时“暂时性死区”比较隐蔽,比如:

function foo(b = a, a = 2) {
    console.log(a, b)
}
foo()
// Uncaught ReferenceError: Cannot access 'a' before initialization

5. let 声明的变量拥有块级作用域

let实际上为 JavaScript 新增了块级作用域

{
    let a = 5
}
console.log(a) // undefined

a 变量是在代码块 {} 中使用 let 定义的,它的作用域是这个代码块内部,外部无法访问。

我们再看一个项目中很常见的 for 循环:

for (var i = 0; i < 3; i++) {
    console.log('循环内:' + i) // 0、1、2
}
console.log('循环外:' + i) // 3

如果改为 let 会怎么样呢?

for (let i = 0; i < 3; i++) {
    console.log('循环内:' + i) // 0、1、2
}
console.log('循环外:' + i) // ReferenceError: i is not defined

继续看下面两个例子的对比,这时 a 的值又是多少呢?

if (false) {
    var a = 5
}
console.log(a) // undefined
if (false) {
    let a = 5
}
console.log(a)
// Uncaught ReferenceError: a is not defined

总结

使用let声明的变量:

  • 不属于顶层对象window
  • 不允许重复声明
  • 不存在变量提升
  • 暂时性死区
  • 块级作用域

const

不能被改变的叫做常量,请大家思考在 ES5 中如何定义一个常量呢? ES5 中可以使用 Object.defineProperty() 来实现定义常量:

Object.defineProperty(window, 'PI', {
    value: 3.14,
    writable: false //是否可写
})
console.log(PI)
PI = 5
console.log(PI)

const 除了具有 let 的块级作用域和不会变量提升外,还有就是它定义的是常量,在用 const 定义变量后,我们就不能修改它了,对变量的修改会抛出异常。

const PI = 3.1415

console.log(PI)

PI = 5

console.log(PI)
// Uncaught TypeError: Assignment to constant variable.

这个代码块中因为对PI尝试修改,导致浏览器报错,这就说明 const 定义的变量是不能被修改的,它是只读的。聪明的同学一定会发现只读属性是不是一定要进行初始化呢?

const PI

PI = 3.1415
// Uncaught SyntaxError: Missing initializer in const declaration

const 声明的变量必须进行初始化,不然会抛出异常 Uncaught SyntaxError: Missing initializer in const declaration。

重点

const obj = {
    name: 'tony',
    age: 34
}
obj.school = '广州大学'
console.log(obj)
// {name: "xiecheng", age: 34, school: "广州大学"}

大家会发现 const 定义的 obj 竟然被改变了... 这到底是为什么呢?有点懵啊...

这时我们就需要了解JS中的变量是如何存储的,见下图: js有两种内存形式,一种是栈内存(stack),另一种是堆内存(heap)

image.png 基本数据类型存储在 栈内存 中,引用数据类型存储在 堆内存 中然后在栈内存中保存 引用地址 。

const 实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。

冻结数据

Object.freeze() 只是浅层冻结,只会对最近一层的对象进行冻结,并不会对深层对象冻结。

Object.freeze(obj)
//彻底冻结对象的函数
/**
 * 利用forEach来实现ES6彻底冻结对象的函数
 * @param  {[type]} obj [description]
 * @return {[type]}     [description]
 */
/*ES6写法*/
var constantize = (obj) => {
    Object.freeze(obj);
    Object.keys(obj).forEach( (key,value) => {
        if( typeof obj[key] === 'object' ) {
            constantize( obj[key] );
        }
    });
};
/*ES5写法*/
var contantize = function (obj) {
    Object.freeze(obj);
    Object.keys(obj).forEach(function (key,value) {
        if( typeof obj[key] === 'object' ) {
            constantize ( obj[key] );
        }
    });
}

总结 使用const声明的常量:

  • 不属于顶层对象window
  • 不允许重复声明
  • 不存在变量提升
  • 暂时性死区
  • 块级作用域

解构赋值

在 ES6 中新增了变量赋值的方式:解构赋值。允许按照一定模式,从数组和对象中提取值,对变量进行赋值。如果对这个概念不了解,我们可以快速展示一个小示例一睹风采:

let arr = [1, 2, 3]
let a = arr[0]
let b = arr[1]
let c = arr[2]

想从数组中找出有意义的项要单独赋值给变量,在 ES6 中就可以这样写了:

let [a, b, c] = [1, 2, 3]

数组解构赋值

  • 赋值元素可以是任意可遍历的对象 赋值的元素不仅是数组,它可以是任意可遍历的对象
let [a, b, c] = "123" // ["1", "2", "3"]
console.log(a); //1
console.log(b); //2
console.log(c); //3

let [one, two, three] = new Set([1, 2, 3])
console.log(one); //1
console.log(two); //2
console.log(three); //3
  • 左边的变量 被赋值的变量还可以是对象的属性,不局限于单纯的变量。
let user = {};
[user.firstName, user.secondName] = 'Kobe Bryant'.split(' ');

console.log(user.firstName, user.secondName) // Kobe Bryant
  • 循环体 解构赋值在循环体中的应用,可以配合 entries 使用。
let user = {
  name: 'John',
  age: 30
}

// loop over keys-and-values
for (let [key, value] of Object.entries(user)) {
  console.log(`${key}:${value}`) // name:John, then age:30
}

当然,对于 map 对象依然适用:

let user = new Map()
user.set('name', 'John')
user.set('age', '30')

for (let [key, value] of user.entries()) {
  console.log(`${key}:${value}`) // name:John, then age:30
}
  • 可以跳过赋值元素 如果想忽略数组的某个元素对变量进行赋值,可以使用逗号来处理。
let [name, , title] = ['John', 'Jim', 'Sun', 'Moon']

console.log( title ) // Sun
  • rest 参数
let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]

console.log(name1) // Julius
console.log(name2) // Caesar

// Note that type of `rest` is Array.
console.log(rest[0]) // Consul
console.log(rest[1]) // of the Roman Republic
console.log(rest.length) // 2

注意

我们可以使用 rest 来接受赋值数组的剩余元素,不过要确保这个 rest 参数是放在被赋值变量的最后一个位置上。

  • 默认值 如果数组的内容少于变量的个数,并不会报错,没有分配到内容的变量会是 undefined。
let [firstName, surname] = []

console.log(firstName) // undefined
console.log(surname) // undefined

当然你也可以给变量赋予默认值,防止 undefined 的情况出现:

// default values
let [firstName = "Guest", surname = "Anonymous"] = ["Julius"]

console.log(firstName)    // Julius (from array)
console.log(surname) // Anonymous (default used)

对象解构赋值

  • 基本用法 解构赋值除了可以应用在 Array,也可以应用在 Object。基本的语法如下:
let {var1, var2} = {var1:…, var2…}

大致的意思是我们有一个 Object 想把里面的属性分别拿出来而无需通过调用属性的方式赋值给指定的变量。具体的做法是在赋值的左侧声明一个和 Object 结构等同的模板,然后把关心属性的 value 指定为新的变量即可。

let options = {
  title: "Menu",
  width: 100,
  height: 200
}

let {title, width, height} = options

console.log(title)  // Menu
console.log(width)  // 100
console.log(height) // 200

在这个结构赋值的过程中,左侧的“模板”结构要与右侧的 Object 一致,但是属性的顺序无需一致。

上述的赋值左侧是采用了对象简写的方式,类似于:

let {title: title, width: width, height: height} = options

如果不想这么写或者想使用其他的变量名,可以自定义的,如下:

let {width: w, height: h, title} = options
  • 默认值 当然,这个赋值的过程中也是可以指定默认值的,这样做:
let options = {
  title: "Menu"
}

let {width = 100, height = 200, title} = options

console.log(title)  // Menu
console.log(width)  // 100
console.log(height) // 200
  • rest 运算符 如果我们想象操作数组一样,只关心指定的属性,其他可以暂存到一个变量下,这就要用到 rest 运算符了
let options = {
    title: "Menu",
    height: 200,
    width: 100
  }
  
  let {title, ...rest} = options
  
  // now title="Menu", rest={height: 200, width: 100}
  console.log(rest.height)  // 200
  console.log(rest.width)   // 100
  • 嵌套对象 如果一个 Array 或者 Object 比较复杂,它嵌套了 Array 或者 Object,那只要被赋值的结构和右侧赋值的元素一致就好了。
let options = {
  size: {
    width: 100,
    height: 200
  },
  items: ["Cake", "Donut"],
  extra: true    // something extra that we will not destruct
}

// destructuring assignment on multiple lines for clarity
let {
  size: { // put size here
    width,
    height
  },
  items: [item1, item2], // assign items here
  title = 'Menu' // not present in the object (default value is used)
} = options

console.log(title)  // Menu
console.log(width)  // 100
console.log(height) // 200
console.log(item1)  // Cake
console.log(item2)  // Donut

这个原理其实很简单,如果不理解可以看下图:

image.png

字符串解构赋值

可以当做是数组的解构:

let str = 'imooc'

let [a, b, c, d, e] = str

console.log(a, b, c, d, e)//i m o o c

数组

ES5 中数组遍历方式

let arr = [1, 2, 3, 2, 4]

for循环

for (let i = 0; i < arr.length; i++) {
    console.log(arr[i])
}

forEach() 没有返回值,只是针对每个元素调用func

arr.forEach(function(elem, index, array) {
    if (arr[index] == 2) {
        continue //不支持
    }
    console.log(elem, index)
})

这个语法看起来要简洁很多,不需要通过索引去访问数组项,然而它的缺点也是很明显,不支持 break、continue 等。

[1, 2, 3, 4, 5].forEach(function(i) {
    if (i === 2) {
        return;
    } else {
        console.log(i)
    }
})

这段代码的"本意"是从第一个元素开始遍历,遇到数组项 2 之后就结束遍历,不然打印出所遍历过的数值项。可是,事实让你大跌眼镜,因为它的输出是 1, 3, 4, 5。

forEach 的代码块中不能使用 break、continue,它会抛出异常。

map() 返回新的数组,每个元素为调用func的结果

let result = arr.map(function(value) {
    value += 1
    console.log(value)
    return value
})
console.log(arr, result)

filter() 返回符合func条件的元素数组

let result = arr.filter(function(value) {
    console.log(value)
    return value == 2
})
console.log(arr, result)

some() 返回boolean,判断是否有元素符合func条件

let result = arr.some(function(value) {
    console.log(value)
    return value == 4
})
console.log(arr, result)

every() 返回boolean,判断每个元素都符合func条件

let result = arr.every(function(value) {
    console.log(value)
    return value == 2
})
console.log(arr, result)

同样完成刚才的目标,使用 every 遍历就可以做到 break 那样的效果,简单的说 return false 等同于 break,return true 等同于 continue。如果不写,默认是 return false。 every 的代码块中不能使用 break、continue,它会抛出异常。

reduce() 接收一个函数作为累加器

//累加
let sum = arr.reduce(function(prev, cur, index, array) {
    return prev + cur
}, 0)
console.log(sum) //12
//取最大值
let max = arr.reduce(function(prev, cur) {
    return Math.max(prev, cur)
})
console.log(max) //4
//去重
let res = arr.reduce(function(prev, cur) {
    prev.indexOf(cur) == -1 && prev.push(cur)
    return prev
}, [])
console.log(res) //[1,2,3,4]

ES6 中数组遍历方式

ES7(ECMAScript2016)语法

ES8(ECMAScript2017)语法

ES9(ECMAScript2018)语法

ES10(ECMAScript2019)语法

ES11(ECMAScript2020)语法

ES12(ECMAScript2021)语法

ES13(ECMAScript2022)语法