又是一顿爆肝,又是一篇万字长文,重新梳理了一下ES6——ES12的常用新特性,很多特性在开发中还是很实用的,希望对你有一点点帮助!文章内容较多,建议先收藏在学习呢!
ECMAScript 是一种由 Ecma 国际通过 ECMA-262 标准化的脚本程序设计语言,这种语言被称为 JavaScript 。简单来说,ECMAScript 是 JavaScript 的标准与规范,JavaScript 是 ECMAScript 标准的实现和扩展。
自2015年开始,ECMAScript发布的版本如下:
发布时间 | 正式名称 | 版本名称 | 名称缩写 |
2015 | ECMAScript2015 | ECMAScript6 | ES2015、ES6 |
2016 | ECMAScript2016 | ECMAScript7 | ES2016、ES7 |
2017 | ECMAScript2017 | ECMAScript8 | ES2017、ES8 |
2018 | ECMAScript2018 | ECMAScript9 | ES2018、ES9 |
2019 | ECMAScript2019 | ECMAScript10 | ES2019、ES10 |
2020 | ECMAScript2020 | ECMAScript11 | ES2020、ES11 |
2021 | ECMAScript2021 | ECMAScript12 | ES2021、ES12 |
下面就来看看ECMAScript各版本有哪些使用技巧吧。
一、ES6 新特性(2015)
ES6的更新主要是体现在以下方面:
- 表达式:变量声明,解构赋值
- 内置对象:字符串拓展、数值拓展、对象拓展、数组拓展、函数拓展、正则拓展、Symbol、Set、Map、Proxy、Reflect
- 语句与运算:Class、Module、Iterator
- 异步编程:Promise、Generator、Async。
这里主要介绍一些常用的新特性。还有一些特性,在之前文章中已经介绍过了,这里不在多说,直接上链接:
- Promise、Generator:《万字长文,重学JavaScript异步编程》
- 数组方法:《万字长文,重学JavaScript数组类型》
- 字符串方法:《JavaScript 28个常用字符串方法及使用技巧》
1. let和const
在ES6中,新增了let和const关键字,其中 let 主要用来声明变量,而 const 通常用来声明常量。let、const相对于var关键字有以下特点:
特性 | var | let | const |
变量提升 | ✔️ | × | × |
全局变量 | ✔️ | × | × |
重复声明 | ✔️ | × | × |
重新赋值 | ✔️ | ✔️ | × |
暂时性死区 | × | ✔️ | ✔️ |
块作用域 | × | ✔️ | ✔️ |
只声明不初始化 | ✔️ | ✔️ | × |
这里主要介绍其中的四点:
(1)重新赋值
const 关键字声明的变量是“不可修改”的。其实,const 保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。但对于引用类型的数据(主要是对象和数组),变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针是不变的,至于它指向的数据结构就不可控制了。
(2)块级作用域
在引入let和const之前是不存在块级作用域的说法的,这也就导致了很多问题,比如内层变量会覆盖外层的同名变量:
var a = 1; if (true) { var a = 2; } console.log(a); // 输出结果:2 复制代码
循环变量会泄漏为全局变量:
var arr = [1, 2, 3]; for (var i = 0; i < arr.length; i++) { console.log(arr[i]); // 输出结果:1 2 3 } console.log(i); // 输出结果:3 复制代码
而通过let和const定义的变量存在块级作用域,就不会产生上述问题:
let a = 1; if (true) { let a = 2; } console.log(a); // 输出结果:1 const arr = [1, 2, 3]; for (let i = 0; i < arr.length; i++) { console.log(arr[i]); // 输出结果:1 2 3 } console.log(i); // Uncaught ReferenceError: i is not defined 复制代码
(3)变量提升
我们知道,在ES6之前是存在变量提升的,所谓的变量提升就是变量可以在声明之前使用:
console.log(a); // 输出结果:undefined var a = 1; 复制代码
变量提升的本质是JavaScript引擎在执行代码之前会对代码进行编译分析,这个阶段会将检测到的变量和函数声明添加到 JavaScript 引擎中名为 Lexical Environment 的内存中,并赋予一个初始化值 undefined。然后再进入代码执行阶段。所以在代码执行之前,JS 引擎就已经知道声明的变量和函数。
这种现象就不太符合我们的直觉,所以在ES6中,let和const关键字限制了变量提升,let 定义的变量添加到 Lexical Environment 后不再进行初始化为 undefined 操作,JS 引擎只会在执行到词法声明和赋值时才进行初始化。而在变量创建到真正初始化之间的时间跨度内,它们无法访问或使用,ES6 将其称之为暂时性死区:
// 暂时性死区 开始 a = "hello"; // Uncaught ReferenceError: Cannot access 'a' before initialization let a; // 暂时性死区 结束 console.log(a); // undefined 复制代码
(4)重复声明
在ES6之前,var关键字声明的变量对于一个作用域内变量的重复声明是没有限制的,甚至可以声明与参数同名变量,以下两个函数都不会报错:
function funcA() { var a = 1; var a = 2; } function funcB(args) { var args = 1; } 复制代码
而let修复了这种不严谨的设计:
function funcA() { let a = 1; let a = 2; // Uncaught SyntaxError: Identifier 'a' has already been declared } function funcB(args) { let args = 1; // Uncaught SyntaxError: Identifier 'args' has already been declared } 复制代码
现在我们项目中已经完全放弃了var,而使用let来定义变量,使用const来定义常量。在ESlint开启了如下规则:
"no-var": 0; 复制代码
2. 解构赋值
ES6中还引入了解构赋值的概念,解构赋值遵循“模式匹配”,即只要等号两边的模式相等,左边的变量就会被赋予对应的值。不同类型数据的解构方式不同,下面就分别来看看不同类型数据的解构方式。
平时在开发中,我主要会用到对象的解构赋值,比如在React中解构porps值等,使用解构赋值来获取父组件传来的值;在React Hooks中的useState使用到了数组的解构赋值;
(1)数组解构
具有 Iterator 接口的数据结构,都可以采用数组形式的解构赋值。
const [foo, [[bar], baz]] = [1, [[2], 3]]; console.log(foo, bar, baz) // 输出结果:1 2 3 复制代码
这里,ES6实现了对数组的结构,并依次赋值变量foo、bar、baz。数组的解构赋值按照位置将值与变量对应。
数组还可以实现不完全解构,只解构部分内容:
const [x, y] = [1, 2, 3]; // 提取前两个值 const [, y, z] = [1, 2, 3] // 提取后两个值 const [x, , z] = [1, 2, 3] // 提取第一三个值 复制代码
如果解构时对应的位置没有值就会将变量赋值为undefined:
const [x, y, z] = [1, 2]; console.log(z) // 输出结果:undefined 复制代码
数组解构赋值可以使用rest操作符来捕获剩余项:
const [x, ...y] = [1, 2, 3]; console.log(x); // 输出结果:1 console.log(y); // 输出结果:[2, 3] 复制代码
在解构时还支持使用默认值,当对应的值为undefined时才会使用默认值:
const [x, y, z = 3] = [1, 2]; console.log(z) // 输出结果:3 复制代码
(2)对象解构
对象的解构赋值的本质其实是先找到同名的属性,在赋值给对应的变量:
let { foo, bar } = { foo: 'aaa', bar: 'bbb' }; console.log(foo, bar); // 输出结果:aaa bbb 复制代码
需要注意的是,在JavaScript中,对象的属性是没有顺序的。所以,在解构赋值时,变量必须与属性同名才能去取到值。
对象的解构赋值也是支持默认值的,当定义的变量在对象中不存在时,其默认值才会生效:
let { foo, bar, baz = 'ccc'} = { foo: 'aaa', bar: 'bbb', baz: null }; console.log(foo, bar, baz); // 输出结果:aaa bbb null let { foo, bar, baz = 'ccc'} = { foo: 'aaa', bar: 'bbb' }; console.log(foo, bar, baz); // 输出结果:aaa bbb ccc 复制代码
可以看到,只有定义的变量是严格的===undefined时,它的默认值才会生效。
除此之外,我们还需要注意,不能给已声明的变量进行赋值,因为当缺少 let、const、var 关键词时,将会把 {baz} 理解为代码块从而导致语法错误,所以下面代码会报错:
let baz; { baz } = { foo: 'aaa', bar: 'bbb', baz: 'ccc' }; 复制代码
可以使用括号包裹整个解构赋值语句来解决上述问题:
let baz; ({ baz } = { foo: 'aaa', bar: 'bbb', baz: 'ccc' }); console.log(baz) 复制代码
在对象的解构赋值中,可以将现有对象的方法赋值给某个变量,比如:
let { log, sin, cos } = Math; log(12) // 输出结果:2.4849066497880004 sin(1) // 输出结果:0.8414709848078965 cos(1) // 输出结果:0.5403023058681398 复制代码
(3)其他解构赋值
剩下的几种解构赋值,目前我在项目中应用的较少,来简单看一下。
- 字符串解构
字符串解构规则:只要等号右边的值不是对象或数组,就先将其转为类数组对象,在进行解构:
const [a, b, c, d, e] = 'hello'; console.log(a, b, c, d, e) // 输出结果:h e l l o 复制代码
类数组对象有 length 属性,因此可以给这个属性进行解构赋值:
let {length} = 'hello'; // 输出结果: 5 复制代码
由于字符串都是一个常量,所以我们通常是知道它的值是什么的,所以很少会使用变量的解构赋值。
- 数值和布尔值解构赋值
对数值和布尔值进行解构时,它们将会先被转为对象,然后再应用解构语法:
let {toString: s} = 123; s === Number.prototype.toString // 输出结果:true let {toString: s} = true; s === Boolean.prototype.toString // 输出结果:true 复制代码
注意null和undefined不能转换为对象,所以如果右边是这两个值,就会报错。
- 函数参数解构赋值
函数参数表面上是一个数组,在传入参数的那一刻,就会被解构为x和y。
function add([x, y]){ return x + y; } add([1, 2]); // 3 复制代码
除此之外,我们还可以解构函数的返回值:
function example() { return [1, 2, 3]; } let [a, b, c] = example(); 复制代码
3. 模板字符串
传统的JavaScript语言中,输出模板经常使用的是字符串拼接的形式,这样写相当繁琐,在ES6中引入了模板字符串的概念来解决以上问题。
模板字符串是增强版的字符串,用反引号``来标识,他可以用来定义单行字符串,也可以定义多行字符串,或者在字符串中嵌入变量。
// 字符串中嵌入变量 let name = "Bob", time = "today"; `Hello ${name}, how are you ${time}?` // 字符串中调用函数 ` ${fn()} 复制代码
在平时的开发中,除了上面代码中的应用,很多地方会用到模板字符串,比如拼接一个DOM串,在Emotion/styled中定义DOM结构等,都会用到模板字符串。不过在模板字符串中定义DOM元素就不会有代码提示了。
在使用模板字符串时,需要注意以下几点:
- 如果在字符串中使用反引号,需要使用\来转义;
- 如果在多行字符串中有空格和缩进,那么它们都会被保留在输出中;
- 模板字符串中嵌入变量,需要将变量名写在${}之中;
- 模板字符串中可以放任意的表达式,也可以进行运算,以及引用对象的属性,甚至可以调用函数;
- 如果模板字符中的变量没有声明,会报错。
4. 函数默认参数
在ES6之前,函数是不支持默认参数的,ES6实现了对此的支持,并且只有不传入参数时才会触发默认值:
function getPoint(x = 0, y = 0) { console.log(x, y); } getPoint(1, 2); // 1 2 getPoint() // 0 0 getPoint(1) // 1 0 复制代码
当使用函数默认值时,需要注意以下几点:
(1)函数length属性值
函数length属性通常用来表示函数参数的个数,当引入函数默认值之后,length表示的就是第一个有默认值参数之前的普通参数个数:
const funcA = function(x, y) {}; console.log(funcA.length); // 输出结果:2 const funcB = function(x, y = 1) {}; console.log(funcB.length); // 输出结果:1 const funcC = function(x = 1, y) {}; console.log(funcC.length); // 输出结果 0 复制代码
(2)参数作用域
当给函数的参数设置了默认值之后,参数在被初始化时将形成一个独立作用域,初始化完成后作用域消解:
let x = 1; function func(x, y = x) { console.log(y); } func(2); 复制代码
这里最终会打印出2。在函数调用时,参数 x, y 将形成一个独立的作用域,所以参数中的y会等于第一个参数中的x,而不是上面定义的1。
5. 箭头函数
ES6中引入了箭头函数,用来简化函数的定义:
const counter = (x, y) => x + y; 复制代码
相对于普通函数,箭头函数有以下特点:
(1)更加简洁
- 如果没有参数,就直接写一个空括号即可
- 如果只有一个参数,可以省去参数的括号
- 如果有多个参数,用逗号分割
- 如果函数体的返回值只有一句,可以省略大括
// 1. 不传入参数 const funcA = () => console.log('funcA'); // 等价于 const funcA = function() { console.log('funcA'); } // 2. 传入参数 const funcB = (x, y) => x + y; // 等价于 const funcB = function(x, y) { return x + y; } // 3. 单个参数的简化 const funcC = (x) => x; // 对于单个参数,可以去掉 (),简化为 const funcC = x => x; // 等价于 const funcC = function(x) { return x; } // 4. 上述代码函数体只有单条语句,如果有多条,需要使用 {} const funcD = (x, y) => { console.log(x, y); return x + y; } // 等价于 const funcD = function(x, y) { console.log(x, y); return x + y; }