- 一、ES6 新特性(2015)
- 二、ES7 新特性(2016)
- 三、ES8 新特性(2017)
- 四、ES9 新特性(2018)
- 五、ES10 新特性(2019)
- 1. trimStart() 和 trimEnd()
- 2. flat()和 flatMap()
- 3. Object.fromEntries()
- 4. Symbol 描述
- 5. toString()
- 6. catch
- 六、ES11 新特性(2020)
- 七、ES12 新特性(2021)
- 结语
明天就是国庆节了,在这里祝小伙伴们国庆 7 天玩的开心,毕竟这是一年最长的假期了,也是最后一个长假了。不知道大家是不是有这样的状态,越是到临近假期越是没有上班的状态,即使在上班也是“身在曹营心在汉”状态,甚至大家都归心似箭了。我也不例外,心思早已不在状态。但是呢还是要摸鱼等到下班,既然坐着也是坐着,索性就重新梳理一下 ES6-ES12 的一些前端开发技巧,很多特性在开发中还是很实用的,希望对你有一点点帮助!这也是国庆节前最后一篇了,因为假期我也要去旅游啦,玩耍啦,哈哈。
文章内容较多,建议先收藏在学习呢!
下面就来看看 ECMAScript 各版本有哪些使用技巧吧。
一、ES6 新特性(2015)
ES6 的更新主要是体现在以下方面:
- 表达式:变量声明,解构赋值
- 内置对象:字符串拓展、数值拓展、对象拓展、数组拓展、函数拓展、正则拓展、
Symbol
、Set
、Map
、Proxy
、Reflect
- 语句与运算:
Class
、Module
、Iterator
- 异步编程:
Promise
、Generator
、Async
。
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; }
(2)不绑定 this
箭头函数不会创建自己的this
, 所以它没有自己的this
,它只会在自己作用域的上一层继承this
。所以箭头函数中this
的指向在它在定义时已经确定了,之后不会改变。
var id = 'GLOBAL'; var obj = { id: 'OBJ', a: function(){ console.log(this.id); }, b: () => { console.log(this.id); } }; obj.a(); // 'OBJ' obj.b(); // 'GLOBAL' new obj.a() // undefined new obj.b() // Uncaught TypeError: obj.b is not a constructor
对象 obj 的方法 b 是使用箭头函数定义的,这个函数中的this
就永远指向它定义时所处的全局执行环境中的 this,即便这个函数是作为对象 obj 的方法调用,this 依旧指向 Window 对象。需要注意,定义对象的大括号{}
是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。
同样,使用call()
、apply()
、bind()
等方法也不能改变箭头函数中this
的指向:
var id = 'Global'; let fun1 = () => { console.log(this.id) }; fun1(); // 'Global' fun1.call({id: 'Obj'}); // 'Global' fun1.apply({id: 'Obj'}); // 'Global' fun1.bind({id: 'Obj'})(); // 'Global'
相关文章推荐阅读:实例讲解 js 中的 call() apply() bind()的用法和区别
(3)不可作为构造函数
构造函数 new
操作符的执行步骤如下:
- 创建一个对象
- 将构造函数的作用域赋给新对象(也就是将对象的
__proto__
属性指向构造函数的prototype
属性) - 指向构造函数中的代码,构造函数中的
this
指向该对象(也就是为这个对象添加属性和方法) - 返回新的对象
实际上第二步就是将函数中的this
指向该对象。 但是由于箭头函数时没有自己的 this 的,且 this 指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。
(4)不绑定 arguments
箭头函数没有自己的arguments
对象。在箭头函数中访问arguments
实际上获得的是它外层函数的arguments
值。
6. 扩展运算符
扩展运算符:...
就像是rest
参数的逆运算,将一个数组转为用逗号分隔的参数序列,对数组进行解包。
spread 扩展运算符有以下用途:
(1)将数组转化为用逗号分隔的参数序列:
function test(a,b,c){ console.log(a); // 1 console.log(b); // 2 console.log(c); // 3 } var arr = [1, 2, 3]; test(...arr);
(2)将一个数组拼接到另一个数组:
var arr1 = [1, 2, 3,4]; var arr2 = [...arr1, 4, 5, 6]; console.log(arr2); // [1, 2, 3, 4, 4, 5, 6]
(3)将字符串转为逗号分隔的数组:
var str='JavaScript'; var arr= [...str]; console.log(arr); // ["J", "a", "v", "a", "S", "c", "r", "i", "p", "t"]
7. Symbol
ES6 中引入了一个新的基本数据类型Symbol
,表示独一无二的值。它是一种类似于字符串的数据类型,它的特点如下:
Symbol
的值是唯一的,用来解决命名冲突的问题Symbol
值不能与其他类型数据进行运算Symbol
定义的对象属性不能使用for...in
遍历循环,但是可以使用Reflect.ownKeys
来获取对象的所有键名
let s1 = Symbol(); console.log(typeof s1); // "symbol" let s2 = Symbol('hello'); let s3 = Symbol('hello'); console.log(s2 === s3); // false
基于以上特性,Symbol 属性类型比较适合用于两类场景中:常量值和对象属性。
(1)避免常量值重复
getValue 函数会根据传入字符串参数 key 执行对应代码逻辑:
function getValue(key) { switch(key){ case 'A': ... case 'B': ... } } getValue('B');
这段代码对调用者而言非常不友好,因为代码中使用了魔术字符串(Magic string,指的是在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值),导致调用 getValue 函数时需要查看函数代码才能找到参数 key
的可选值。所以可以将参数 key
的值以常量的方式声明:
const KEY = { alibaba: 'A', baidu: 'B', } function getValue(key) { switch(key){ case KEY.alibaba: ... case KEY.baidu: ... } } getValue(KEY.baidu);
但这样也并非完美,假设现在要在 KEY
常量中加入一个 key
,根据对应的规则,很有可能会出现值重复的情况:
const KEY = { alibaba: 'A', baidu: 'B', tencent: 'B' }
这就会出现问题:
getValue(KEY.baidu) // 等同于 getValue(KEY.tencent)
所以在这种场景下更适合使用 Symbol,不需要关心值本身,只关心值的唯一性:
const KEY = { alibaba: Symbol(), baidu: Symbol(), tencent: Symbol() }
(2)避免对象属性覆盖
函数 fn
需要对传入的对象参数添加一个临时属性 user
,但可能该对象参数中已经有这个属性了,如果直接赋值就会覆盖之前的值。此时就可以使用 Symbol
来避免这个问题。创建一个 Symbol
数据类型的变量,然后将该变量作为对象参数的属性进行赋值和读取,这样就能避免覆盖的情况:
function fn(o) { // {user: {id: xx, name: yy}} const s = Symbol() o[s] = 'zzz' }
8. 集合 Set
ES6 提供了新的数据结构Set
(集合)。它类似于数组,但是成员的值都是唯一的,集合实现了iterator
接口,所以可以使用扩展运算符和 for…of
进行遍历。
Set 的属性和方法:
属性和方法 | 概述 |
---|---|
size | 返回集合的元素个数 |
add | 增加一个新的元素,返回当前的集合 |
delete | 删除元素,返回布尔值 |
has | 检查集合中是否包含某元素,返回布尔值 |
clear | 清空集合,返回 undefined |
//创建一个空集合 let s = new Set(); //创建一个非空集合 let s1 = new Set([1,2,3,1,2,3]); //返回集合的元素个数 console.log(s1.size); // 3 //添加新元素 console.log(s1.add(4)); // {1,2,3,4} //删除元素 console.log(s1.delete(1)); //true //检测是否存在某个值 console.log(s1.has(2)); // true //清空集合 console.log(s1.clear()); //undefined
由于集合中元素的唯一性,所以在实际应用中,可以使用set
来实现数组去重:
let arr = [1,2,3,2,1] Array.from(new Set(arr)) // {1, 2, 3}
这里使用了Array.form()
方法来将数组集合转化为数组。
可以通过set
来求两个数组的交集和并集:
// 模拟求交集 let intersection = new Set([...set1].filter(x => set2.has(x))); // 模拟求差集 let difference = new Set([...set1].filter(x => !set2.has(x)));
用以下方法可以进行数组与集合的相互转化:
// Set 集合转化为数组 const arr = [...mySet] const arr = Array.from(mySet) // 数组转化为 Set 集合 const mySet = new Set(arr)
9. Map
ES6 提供了Map
数据结构,它类似于对象,也是键值队的集合,但是它的键值的范围不限于字符串,可以是任何类型(包括对象)的值,也就是说, Object 结构提供了“字符串—值” 的对应, Map
结构提供了“ 值—值” 的对应, 是一种更完善的 Hash 结构实现。 如果需要“ 键值对” 的数据结构, Map
比 Object 更合适。Map 也实现了 iterator 接口,所以可以使用扩展运算符和 for…of
进行遍历。
Map
的属性和方法:
属性和方法 | 概述 |
---|---|
size | 返回 Map 的元素个数 |
set | 增加一个新的元素,返回当前的 Map |
get | 返回键名对象的键值 |
has | 检查 Map 中是否包含某元素,返回布尔值 |
clear | 清空 Map,返回 undefined |
//创建一个空 map let m = new Map(); //创建一个非空 map let m2 = new Map([ ['name', 'hello'], ]); //获取映射元素的个数 console.log(m2.size); // 1 //添加映射值 console.log(m2.set('age', 6)); // {"name" => "hello", "age" => 6} //获取映射值 console.log(m2.get('age')); // 6 //检测是否有该映射 console.log(m2.has('age')); // true //清除 console.log(m2.clear()); // undefined
需要注意, 只有对同一个对象的引用, Map 结构才将其视为同一个键:
let map = new Map(); map.set(['a'], 555); map.get(['a']) // undefined
上面代码的set
和get
方法, 表面是针对同一个键, 但实际上这是两个值, 内存地址是不一样的, 因此get
方法无法读取该键, 所以会返回undefined
。
由上可知, Map
的键实际上是跟内存地址绑定的, 只要内存地址不一样, 就视为两个键。 这就解决了同名属性碰撞( clash) 的问题,在扩展库时, 如果使用对象作为键名, 就不用担心自己的属性与原来的属性同名。
如果 Map
的键是一个简单类型的值( 数字、 字符串、 布尔值), 则只要两个值严格相等, Map
将其视为一个键, 包括0
和 -0
。 另外, 虽然NaN
不严格相等于自身, 但 Map
将其视为同一个键。
let map = new Map(); map.set(NaN, 123); map.get(NaN) // 123 map.set(-0, 123); map.get(+0) // 123
10. 模块化
ES6 中首次引入模块化开发规范 ES Module,让 Javascript 首次支持原生模块化开发。ES Module 把一个文件当作一个模块,每个模块有自己的独立作用域,那如何把每个模块联系起来呢?核心点就是模块的导入与导出。
(1)export 导出模块
- 正常导出:
// 方式一 export var first = 'test'; export function func() { return true; } // 方式二 var first = 'test'; var second = 'test'; function func() { return true; } export {first, second, func};
as
关键字:var first = 'test'; export {first as second};
as 关键字可以重命名暴露出的变量或方法,经过重命名后同一变量可以多次暴露出去。
export default
export default
会导出默认输出,即用户不需要知道模块中输出的名字,在导入的时候为其指定任意名字。// 导出 export default function () { console.log('foo'); } // 导入 import customName from './export-default';
注意: 导入默认模块时不需要大括号,导出默认的变量或方法可以有名字,但是对外无效。export default
只能使用一次。
(2)import 导入模块
- 正常导入:
import {firstName, lastName, year} from './profile'; 复制代码
导入模块位置可以是相对路径也可以是绝对路径,.js 可以省略,如果不带路径只是模块名,则需要通过配置文件告诉引擎查找的位置。
as
关键字:import { lastName as surname } from './profile';
import
命令会被提升到模块头部,所以写的位置不是那么重要,但是不能使用表达式和变量来进行导入。- 加载整个模块(无输出)
import 'lodash'; //仅仅是加载而已,无法使用
- 加载整个模块(有输出)
import * as circle from './circle'; console.log('圆面积:' + circle.area(4)); console.log('圆周长:' + circle.circumference(14));
注意: import *
会忽略default
输出
(3)导入导出复合用法
- 先导入后导出
export { foo, bar } from 'my_module'; // 等同于 import { foo, bar } from 'my_module'; export { foo, boo};
- 整体先导入再输出以及
default
// 整体输出 export * from 'my_module'; // 导出 default,正如前面所说,export default 其实导出的是 default 变量 export { default } from 'foo'; // 具名接口改 default export { es6 as default } from './someModule';
(4)模块的继承
export * from 'circle'; export var e = 2.71828182846; export default function(x) { return Math.exp(x); }
注意: export *
会忽略default
。
二、ES7 新特性(2016)
1. Array.prototype.includes
includes()
方法用来判断一个数组是否包含一个指定的值,如果包含则返回 true,否则返回 false。该方法不会改变原数组。其语法如下:
arr.includes(searchElement, fromIndex)
该方法有两个参数:
- searchElement:必须,需要查找的元素值。
- fromIndex:可选,从 fromIndex 索引处开始查找目标值。如果为负值,则按升序从 array.length + fromIndex 的索引开始搜 (即使从末尾开始往前跳 fromIndex 的绝对值个索引,然后往后搜寻)。默认为 0。
[1, 2, 3].includes(2); // true [1, 2, 3].includes(4); // false [1, 2, 3].includes(3, 3); // false [1, 2, 3].includes(3, -1); // true
在 ES7 之前,通常使用 indexOf
来判断数组中是否包含某个指定值。但 indexOf
在语义上不够明确直观,同时 indexOf
内部使用 ===
来判等,所以存在对 NaN
的误判,includes
则修复了这个问题:
[1, 2, NaN].indexOf(NaN); // -1 [1, 2, NaN].includes(NaN); // true
注意:使用 includes()
比较字符串和字符时是区分大小写。
2. 指数操作符
ES7 还引入了指数操作符 ,用来更为方便的进行指数计算,它与 Math.pow()
等效:
Math.pow(2, 10)); // 1024 2**10; // 1024
三、ES8 新特性(2017)
ES8 中引入了异步函数的解决方案async/await
,这里不在多介绍,参考文章:
1. padStart()和 padEnd()
padStart()
和padEnd()
方法用于补齐字符串的长度。如果某个字符串不够指定长度,会在头部或尾部补全。
(1)padStart()
padStart()
用于头部补全。该方法有两个参数,其中第一个参数是一个数字,表示字符串补齐之后的长度;第二个参数是用来补全的字符串。
如果原字符串的长度,等于或大于指定的最小长度,则返回原字符串:
'x'.padStart(1, 'ab') // 'x'
如果用来补全的字符串与原字符串,两者的长度之和超过了指定的最小长度,则会截去超出位数的补全字符串:
'x'.padStart(5, 'ab') // 'ababx' 'x'.padStart(4, 'ab') // 'abax'
如果省略第二个参数,默认使用空格补全长度:
'x'.padStart(4, 'ab') // 'a '
padStart()
的常见用途是为数值补全指定位数,笔者最近做的一个需求就是将返回的页数补齐为三位,比如第 1 页就显示为 001,就可以使用该方法来操作:
"1".padStart(3, '0') // 输出结果: '001' "15".padStart(3, '0') // 输出结果: '015'
(2)padEnd()
padEnd()
用于尾部补全。该方法也是接收两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串:
'x'.padEnd(5, 'ab') // 'xabab' 'x'.padEnd(4, 'ab') // 'xaba'
2. Object.values()和 Object.entries()
在 ES5 中就引入了Object.keys
方法,在 ES8 中引入了跟Object.keys
配套的Object.values
和Object.entries
,作为遍历一个对象的补充手段,供 for…of 循环使用。它们都用来遍历对象,它会返回一个由给定对象的自身可枚举属性(不含继承的和Symbol
属性)组成的数组,数组元素的排列顺序和正常循环遍历该对象时返回的顺序一致,这个三个元素返回的值分别如下:
Object.keys()
:返回包含对象键名的数组;Object.values()
:返回包含对象键值的数组;Object.entries()
:返回包含对象键名和键值的数组。
let obj = { id: 1, name: 'hello', age: 18 }; console.log(Object.keys(obj)); // 输出结果: ['id', 'name', 'age'] console.log(Object.values(obj)); // 输出结果: [1, 'hello', 18] console.log(Object.entries(obj)); // 输出结果: [['id', 1], ['name', 'hello'], ['age', 18]
注意
Object.keys()
方法返回的数组中的值都是字符串,也就是说不是字符串的 key 值会转化为字符串。- 结果数组中的属性值都是对象本身可枚举的属性,不包括继承来的属性。
3. 函数扩展
ES2017 规定函数的参数列表的结尾可以为逗号:
function person( name, age, sex, ) {}
该特性的主要作用是方便使用git
进行多人协作开发时修改同一个函数减少不必要的行变更。
四、ES9 新特性(2018)
1. for await…of
for await...of
方法被称为异步迭代器,该方法是主要用来遍历异步对象。
for await...of
语句会在异步或者同步可迭代对象上创建一个迭代循环,包括 String,Array,类数组,Map, Set 和自定义的异步或者同步可迭代对象。这个语句只能在 async function
内使用:
function Gen (time) { return new Promise((resolve,reject) => { setTimeout(function () { resolve(time) },time) }) } async function test () { let arr = [Gen(2000),Gen(100),Gen(3000)] for await (let item of arr) { console.log(Date.now(),item) } } test()
输出结果:
2. Promise.prototype.finally()
ES2018 为 Promise
添加了 finally()
方法,表示无论 Promise
实例最终成功或失败都会执行的方法:
const promise = new Promise((resolve, reject) => { setTimeout(() => { const one = '1'; reject(one); }, 1000); }); promise .then(() => console.log('success')) .catch(() => console.log('fail')) .finally(() => console.log('finally'))
finally()
函数不接受参数,finally()
内部通常不知道 promise
实例的执行结果,所以通常在 finally()
方法内执行的是与 promise
状态无关的操作。
3. 对象的扩展运算符
在 ES6 中就引入了扩展运算符,但是它只能作用于数组,ES2018 中的扩展运算符可以作用于对象:
(1)将元素组织成对象
const obj = {a: 1, b: 2, c: 3}; const {a, ...rest} = obj; console.log(rest); // 输出 {b: 2, c: 3} (function({a, ...obj}) { console.log(obj); // 输出 {b: 2, c: 3} }({a: 1, b: 2, c: 3}));
(2)将对象扩展为元素
const obj = {a: 1, b: 2, c: 3}; const newObj ={...obj, d: 4}; console.log(newObj); // 输出 {a: 1, b: 2, c: 3, d: 4}
(3)可以用来合并对象
const obj1 = {a: 1, b:2}; const obj2 = {c: 3, d:4}; const mergedObj = {...obj1, ...obj2}; console.log(mergedObj); // 输出 {a: 1, b: 2, c: 3, d: 4}
五、ES10 新特性(2019)
1. trimStart() 和 trimEnd()
在 ES10 之前,JavaScript 提供了trim()
方法,用于移除字符串首尾空白符。在 ES9 中提出了trimStart()
和 trimEnd() 方法用于移除字符串首尾的头尾空白符,空白符包括:空格、制表符 tab、换行符等其他空白符等。
(1)trimStart()
trimStart()
方法的的行为与trim()
一致,不过会返回一个从原始字符串的开头删除了空白的新字符串,不会修改原始字符串:
const s = ' abc '; s.trimStart() // "abc "
(2)trimEnd()
trimEnd()
方法的的行为与trim()
一致,不过会返回一个从原始字符串的结尾删除了空白的新字符串,不会修改原始字符串:
const s = ' abc '; s.trimEnd() // " abc"
注意,这两个方法都不适用于null
、undefined
、Number
类型。
2. flat()和 flatMap()
(1)flat()
在 ES2019 中,flat()
方法用于创建并返回一个新数组,这个新数组包含与它调用flat()
的数组相同的元素,只不过其中任何本身也是数组的元素会被打平填充到返回的数组中:
[1, [2, 3]].flat() // [1, 2, 3] [1, [2, [3, 4]]].flat() // [1, 2, [3, 4]]
在不传参数时,flat()
默认只会打平一级嵌套,如果想要打平更多的层级,就需要传给flat()
一个数值参数,这个参数表示要打平的层级数:
[1, [2, [3, 4]]].flat(2) // [1, 2, 3, 4]
如果数组中存在空项,会直接跳过:
[1, [2, , 3]].flat()); // [1, 2, 3]
如果传入的参数小于等于 0,就会返回原数组:
[1, [2, [3, [4, 5]]]].flat(0); // [1, [2, [3, [4, 5]]]] [1, [2, [3, [4, 5]]]].flat(-10); // [1, [2, [3, [4, 5]]]]
(2)flatMap()
flatMap()
方法使用映射函数映射每个元素,然后将结果压缩成一个新数组。它与 map
和连着深度值为 1 的 flat
几乎相同,但 flatMap
通常在合并成一种方法的效率稍微高一些。该方法会返回一个新的数组,其中每个元素都是回调函数的结果,并且结构深度 depth
值为 1。
[1, 2, 3, 4].flatMap(x => x * 2); // [2, 4, 6, 8] [1, 2, 3, 4].flatMap(x => [x * 2]); // [2, 4, 6, 8] [1, 2, 3, 4].flatMap(x => [[x * 2]]); // [[2], [4], [6], [8]] [1, 2, 3, 4].map(x => [x * 2]); // [[2], [4], [6], [8]]
3. Object.fromEntries()
Object.fromEntries()
方法可以把键值对列表转换为一个对象。该方法相当于 Object.entries()
方法的逆过程。 Object.entries()
方法返回一个给定对象自身可枚举属性的键值对数组,而Object.fromEntries()
方法把键值对列表转换为一个对象。
const object = { key1: 'value1', key2: 'value2' } const array = Object.entries(object) // [ ["key1", "value1"], ["key2", "value2"] ] Object.fromEntries(array) // { key1: 'value1', key2: 'value2' }
使用该方法主要有以下两个用途:
(1)将数组转成对象
const entries = [ ['foo', 'bar'], ['baz', 42] ] Object.fromEntries(entries) // { foo: "bar", baz: 42 }
(2)将 Map 转成对象
const entries = new Map([ ['foo', 'bar'], ['baz', 42] ]) Object.fromEntries(entries) // { foo: "bar", baz: 42 }
4. Symbol 描述
通过 Symbol()
创建符号时,可以通过参数提供字符串作为描述:
let dog = Symbol("dog"); // dog 为描述
在 ES2019 之前,获取一个 Symbol 值的描述需要通过 String 方法 或 toString 方法:
String(dog); // "Symbol(dog)" dog.toString(); // "Symbol(dog)"
ES2019 补充了属性 description
,用来直接访问描述:
dog.description; // dog
5. toString()
ES2019 对函数的 toString()
方法进行了扩展,以前这个方法只会输出函数代码,但会省略注释和空格。ES2019 的 toString()
则会保留注释、空格等,即输出的是原始代码:
function sayHi() { /* dog */ console.log('wangwang'); } sayHi.toString(); // 将输出和上面一样的原始代码
6. catch
在 ES2019 以前,catch
会带有参数,但是很多时候 catch
块是多余的。而现在可以不带参数:
// ES2019 之前 try { ... } catch(error) { ... } // ES2019 之后 try { ... } catch { ... }
六、ES11 新特性(2020)
1. BigInt
在 JavaScript 中,数值类型 Number
是 64 位浮点数**,所以计算精度和表示范围都有一定限制。ES2020 新增了 BigInt
数据类型,这也是 JavaScript 引入的**第八种基本类型。BigInt
可以表示任意大的整数。其语法如下:
BigInt(value);
其中 value
是创建对象的数值。可以是字符串或者整数。
在 JavaScript 中,Number
基本类型可以精确表示的最大整数是 253。因此早期会有这样的问题:
let max = Number.MAX_SAFE_INTEGER; // 最大安全整数 let max1 = max + 1 let max2 = max + 2 max1 === max2 // true
有了BigInt
之后,这个问题就不复存在了:
let max = BigInt(Number.MAX_SAFE_INTEGER); let max1 = max + 1n let max2 = max + 2n max1 === max2 // false
可以通过typeof
操作符来判断变量是否为BigInt
类型(返回字符串”bigint”):
typeof 1n === 'bigint'; // true typeof BigInt('1') === 'bigint'; // true
还可以通过Object.prototype.toString
方法来判断变量是否为BigInt
类型(返回字符串”[object BigInt]”):
Object.prototype.toString.call(10n) === '[object BigInt]'; // true
注意,BigInt
和 Number
不是严格相等的,但是宽松相等:
10n === 10 // false 10n == 10 // true
Number
和 BigInt
可以进行比较:
1n < 2; // true 2n > 1; // true 2 > 2; // false 2n > 2; // false 2n >= 2; // true
2. 空值合并运算符(??)
在编写代码时,如果某个属性不为 null
和 undefined
,那么就获取该属性,如果该属性为 null 或 undefined,则取一个默认值:
const name = dogName ? dogName : 'default';
可以通过 ||
来简化:
const name = dogName || 'default';
但是 ||
的写法存在一定的缺陷,当 dogName 为 0 或 false 的时候也会走到 default 的逻辑。所以 ES2020 引入了 ??
运算符。只有 ??
左边为 null 或 undefined 时才返回右边的值:
const dogName = false; const name = dogName ?? 'default'; // name = false;
3. 可选链操作符(?.)
在开发过程中,我们经常需要获取深层次属性,例如 system.user.addr.province.name。但在获取 name 这个属性前需要一步步的判断前面的属性是否存在,否则并会报错:
const name = (system && system.user && system.user.addr && system.user.addr.province && system.user.addr.province.name) || 'default';
为了简化上述过程,ES2020 引入了「链判断运算符」?.
,可选链操作符( ?.
)允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?.
操作符的功能类似于 .
链式操作符,不同之处在于,在引用为 null 或 undefined 的情况下不会引起错误,该表达式短路返回值是 undefined。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined。
const name = system?.user?.addr?.province?.name || 'default';
当尝试访问可能不存在的对象属性时,可选链操作符将会使表达式更短、更简明。在探索一个对象的内容时,如果不能确定哪些属性必定存在,可选链操作符也是很有帮助的。
可选链有以下三种形式:
a?.[x] // 等同于 a == null ? undefined : a[x] a?.b() // 等同于 a == null ? undefined : a.b() a?.() // 等同于 a == null ? undefined : a()
在使用 TypeScript 开发时,这个操作符可以解决很多问题。
七、ES12 新特性(2021)
1. String.prototype.replaceAll()
replaceAll()
方法会返回一个全新的字符串,所有符合匹配规则的字符都将被替换掉,替换规则可以是字符串或者正则表达式。
let string = 'hello world, hello ES12' string.replace(/hello/g,'hi') // hi world, hi ES12 string.replaceAll('hello','hi') // hi world, hi ES12
注意的是,replaceAll()
在使用正则表达式的时候,如果非全局匹配(/g),会抛出异常:
let string = 'hello world, hello ES12' string.replaceAll(/hello/,'hi') // Uncaught TypeError: String.prototype.replaceAll called with a non-global
2. 数字分隔符
数字分隔符可以在数字之间创建可视化分隔符,通过 _ 下划线来分割数字,使数字更具可读性,可以放在数字内的任何地方:
const money = 1_000_000_000 //等价于 const money = 1000000000
该新特性同样支持在八进制数中使用:
const number = 0o123_456 //等价于 const number = 0o123456
3. Promise.any
Promise.any
是是 ES2021 新增的特性,它接收一个 Promise
可迭代对象(例如数组),只要其中的一个 promise
成功,就返回那个已经成功的 promise
如果可迭代对象中没有一个 promise 成功(即所有的 promises 都失败/拒绝),就返回一个失败的 promise
和 AggregateError
类型的实例,它是 Error 的一个子类,用于把单一的错误集合在一起
const promises = [ Promise.reject('ERROR A'), Promise.reject('ERROR B'), Promise.resolve('result'), ] Promise.any(promises).then((value) => { console.log('value: ', value) }).catch((err) => { console.log('err: ', err) }) // 输出结果:value: result
如果所有传入的 promises 都失败:
const promises = [ Promise.reject('ERROR A'), Promise.reject('ERROR B'), Promise.reject('ERROR C'), ] Promise.any(promises).then((value) => { console.log('value:', value) }).catch((err) => { console.log('err:', err) console.log(err.message) console.log(err.name) console.log(err.errors) })
输出结果:
err:AggregateError: All promises were rejected All promises were rejected AggregateError ["ERROR A", "ERROR B", "ERROR C"]
结语
又是一顿爆肝,又是一篇万字长文,以上就是重新梳理了一下 ES6——ES12 的常用新特性,很多特性在开发中还是很实用的,希望对你有一点点帮助!我发现咸鱼妹也总结了一篇,大家可以看看,其实都差不多,好了,今天就到这里摸会儿鱼下班,爽歪歪。