ES6+新特性说明
(补充)一、ES6新特性(2015)
1. 新的基本类型 symbol
symbol 是一种基本数据类型 (primitive data type)。Symbol()
函数会返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()
"。
每个从Symbol()
返回的symbol值都是唯一的。(所以不能进行比较)一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的。
const symbol1 = Symbol();
const symbol2 = Symbol(42);
const symbol3 = Symbol('foo');
console.log(typeof symbol1);
//output: "symbol"
console.log(symbol3.toString());
//output: "Symbol(foo)"
console.log(Symbol('foo') === Symbol('foo'));
//output: false
应用场景
使用Symbol来作为对象属性名(key)
在这之前,我们通常定义或访问对象的属性时可以使用字符串,比如下面的代码:
let obj = {
abc: 123,
"hello": "world"
}
console.log(obj["abc"]); // 123
console.log(obj["hello"]); // 'world'
obj["sym"] = "sym";
console.log(obj); // {abc: 123, hello: "world", sym: "sym"}
console.log(obj.abc);// 123
ES6后, Symbol
可同样用于对象属性的定义和访问:
const SYM_NAME = Symbol()
const SYM_AGE = Symbol()
let obj = {
[SYM_NAME]: "chen"
}
obj[SYM_AGE] = 18
console.log(obj[SYM_NAME]); // "chen"
console.log(obj[SYM_AGE]); // 18
console.log(obj); // {Symbol(): "chen", Symbol(): 18}
随之而来的是另一个非常值得注意的问题:就是当使用了Symbol作为对象的属性key后,在对该对象进行key的枚举时,会有什么不同?在实际应用中,我们经常会需要使用Object.keys()
或者for...in
来枚举对象的属性名,那在这方面,Symbol类型的key表现的会有什么不同之处呢?来看以下示例代码:
let obj = {
[Symbol('SYM_NAME')]: 'chen',
age: 18,
sex:'male'
}
Object.keys(obj) // ['age', 'sex']
for (let p in obj) {
console.log(p) // 分别会输出:'age' 和 'sex'
}
Object.getOwnPropertyNames(obj) // ['age', 'sex']
由上可知,Symbol类型的key是不能通过Object.keys()
或者for...in
来枚举的,它未被包含在对象自身的属性名集合(property names)之中。所以,利用该特性,我们可以把一些不需要对外操作和访问的属性使用Symbol来定义。也正因为这样一个特性,当使用JSON.stringify()
将对象转换成JSON字符串的时候,Symbol属性也会被排除在输出内容之外:
JSON.stringify(obj) // {"age":18,"sex":"male"}
我们可以利用这一特点来更好的设计我们的数据对象,让“对内限制性操作”和“对外选择性输出”变得更加优雅。
限制性操作:
简而言之就是如果有需要,硬是要获取,还是有方法的,只不过有条件:
// 使用Object的API
Object.getOwnPropertySymbols(obj) // [Symbol(name)]
// 使用新增的反射API,将symbol反射回去
Reflect.ownKeys(obj) // [Symbol(name), 'age', 'sex']
使用Symbol定义类的私有属性/方法
我们知道在JavaScript中,是没有如Java等面向对象语言的访问控制关键字private
的,类上所有定义的属性或方法都是可公开访问的。因此这对我们进行API的设计时造成了一些困扰。
而有了Symbol以及模块化机制,类的私有属性和方法才变成可能。例如:
- a.js
/* 声明一个唯一的常量 */
const PASSWORD = Symbol();
class Login {
constructor(username, password) {
this.username = username
/* 私有化 */
this[PASSWORD] = password
}
checkPassword(pwd) {
return this[PASSWORD] === pwd
}
}
export default Login
- b.js
import Login from './a.js';
const login = new Login('admin', '123456');
login.checkPassword('123456');
console.log(login.PASSWORD); //undefined
console.log(login[PASSWORD]); //undefined
console.log(login["PASSWORD"]); //undefined
由于Symbol常量PASSWORD
被定义在a.js所在的模块中,外面的模块获取不到这个Symbol,也不可能再创建一个一模一样的Symbol出来(因为Symbol是唯一的),因此这个PASSWORD
的Symbol只能被限制在a.js内部使用,所以使用它来定义的类属性是没有办法被模块外访问到的,达到了一个私有化的效果。
扩展
symbol共享
通常情况下,我们在一个浏览器窗口中(window),使用Symbol()
函数来定义和Symbol实例就足够了。但是,如果你的应用涉及到多个window(最典型的就是页面中使用了 <iframe>
),并需要这些window中使用的某些Symbol是同一个,那就不能使用Symbol()函数了,因为用它在不同window中创建的Symbol实例总是唯一的,而我们需要的是在所有这些window环境下保持一个共享的Symbol。这种情况下,我们就需要使用另一个API来创建或获取Symbol,那就是Symbol.for()
,它可以注册或获取一个window间全局的Symbol实例:
let gs1 = Symbol.for('global_symbol_1') //注册一个全局Symbol
let gs2 = Symbol.for('global_symbol_1') //获取全局Symbol
gs1 === gs2 // true
这样一个Symbol不光在单个window中是唯一的,在多个相关window间也是唯一的了。
可以参考: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol
二、ES7新特性(2016)
ES2016添加了两个小的特性来说明标准化过程:
- Array.prototype.includes()
- 指数操作符
1. Array.prototype.includes()
includes()
函数用来判断一个数组是否包含一个指定的值,如果包含则返回 true
,否则返回false
。
includes
函数与 indexOf
函数很相似,下面两个表达式是等价的:
arr.includes(x)
arr.indexOf(x) >=0
接下来我们来判断数字中是否包含某个元素:
在ES7之前的做法
使用indexOf()
验证数组中是否存在某个元素,这时需要根据返回值是否为-1来判断:
let arr = ['1', '2', '3'];
if (arr.indexOf('1') !== -1){
console.log('1存在');
}
使用ES7的includes()
使用includes()验证数组中是否存在某个元素,这样更加直观简单:
let arr = ['1', '2', '3'];
if (arr.includes('1')){
console.log('1存在');
}
2. 指数操作符
在ES7中引入了指数运算符**
,**
具有与Math.pow(..)
等效的计算结果。
不使用指数操作符
使用自定义的递归函数calculateExponent或者Math.pow()进行指数运算:
function calculateExponent(base, exponent){
if (exponent === 1){
return base;
}else{
return base * calculateExponent(base, exponent - 1);
}
}
console.log(calculateExponent(2, 10)); // 输出1024
/* 使用 Math.pow() */
console.log(Math.pow(2, 10)); // 输出1024
使用指数操作符
使用指数运算符**,就像+、-等操作符一样:
console.log(2**10); //=> 1024
三、ES8新特性(2017)
-
async/await
-
Object.values()
-
Object.entries()
-
string padding
-
Object.getOwnPropertyDescriptors()
1. async/await(重点)
ES2018引入异步迭代器(asynchronous iterators),这就像常规迭代器,除了next()
方法返回一个Promise。因此await
可以和for...of
循环一起使用,以串行的方式运行异步操作。例如:
/* 模板 */
async function process(array) {
for await (let i of array) {
doSomething(i);
}
}
实例:
迭代异步可迭代对象
/* 可迭代对象 */
var iter = {
[Symbol.iter]() {
return {
i: 0,
next() {
if (this.i < 3) {
return Promise.resolve({ value: this.i++, done: false });
}else{
return Promise.resolve({ done: true }); /* 结束迭代 */
}
}
};
}
};
/* 异步迭代器 */
/* 自调用函数 */
(async function() {
for await (num of iter) {
console.log(num);
}
})();
//输出:
//0
//1
//2
迭代异步生成器
/* 生成器 */
async function* asyncGenerator() {
var i = 0;
while (i < 3) {
yield i++;
}
}
/* 异步迭代器 */
/* 自调用函数 */
(async function() {
for await (num of asyncGenerator()) {
console.log(num);
}
})();
//输出:
//0
//1
//2
深入理解async/await
async/await其实是Promise的语法糖,它能实现的效果都能用then链来实现,这也和我们之前提到的一样,它是为优化then链而开发出来的。从字面上来看,async是“异步”的简写,await译为等待,所以我们很好理解async声明function是异步的,await等待某个操作完成。当然语法上强制规定await只能出现在asnyc函数中,我们先来看看async函数返回了什么:
async function test(){
return 'hello world';
}
let result = test();
console.log(result)
这个async声明的异步函数把return后面直接量通过Promise.resolve()返回Promise对象,所以如果这个最外层没有用await调用的话,是可以用原来then链的方式来调用的:
async function test(){
return 'hello world'
}
let result = test()
console.log(result)
result.then(v=>{
console.log(v) //hello world
})
联想一下Promise特点——异步无等待,所以当没有await语句执行async函数,它就会立即执行,返回一个Promise对象,非阻塞,与普通的Promise对象函数一致。
重点就在await,它等待什么呢?
按照语法说明,await等待的是一个Promise对象,或者是其他值(也就是说可以等待任何值),如果等待的是Promise对象,则返回Promise的处理结果;如果是其他值,则返回该值本身。并且await会暂停当前async function的执行,等待Promise的处理完成。若Promise正常处理(fulfillded),其将回调的resolve函数参数作为await表达式的值,继续执行async function;若Promise处理异常(rejected),await表达式会把Promise异常原因抛出;另外如果await操作符后面的表达式不是一个Promise对象,则返回该值本身。
实例:
function Asy(x){
return new Promise(resolve=>{setTimeout(() =>resolve(x), 2000)})
}
async function Awa(){
let res = await Asy(1);
console.log(res); // 2秒钟之后出现1,再次等待3s,处理完后再处理下一条
console.log(3) // 2秒钟之后出现3
}
Awa();//异步,不进入主线程
console.log(4);//同步任务
//输出:
//4
//1
//3
关于: EvenLoop
2. Object.values()
Object.values()
是一个与Object.keys()
类似的新函数,但返回的是Object自身属性的所有值,不包括继承的值。
假设我们要遍历如下对象obj
的所有值:
const obj = {a: 'chen', b: 'yao', c: 'dao'};
不使用Object.values()
/* 通过键值对来获取obj中的属性值 */
const vals = Object.keys(obj).map(key=>obj[key]);
console.log(vals); //=> ["chen", "yao", "dao"]
使用Object.values()
const values = Object.values(obj);
console.log(values); //=> ["chen", "yao", "dao"]
从上述代码中可以看出Object.values()
为我们省去了遍历key,并根据这些key获取value的步骤。 简单明了~
3. Object.entries()
Object.entries()
函数返回一个给定对象自身可枚举属性的键值对的数组。
假设我们要获取如下对象obj
可枚举属性的键值对的数组:
const obj = {a: 0, b: 1, c: 2};
不使用Object.values()
/* 通过键值对来获取obj中的属性值 */
Object.keys(obj).map(key=>{
console.log('keys:'+key+' value:'+obj[key])
});
//keys:a value:0
//keys:b value:1
//keys:c value:2
使用Object.entries()
for(let [key,value] of Object.entries(obj)){
console.log(`key: ${key} value:${value}`)
}
//keys:a value:0
//keys:b value:1
//keys:c value:2
4. string padding
在ES8中String新增了两个实例函数String.prototype.padStart
和String.prototype.padEnd
,允许将空字符串或其他字符串添加到原始字符串的开头或结尾。
语法:
-
字符串.padStart(targetLength,[padString]) ;
-
字符串.padEnd(targetLength,[padString]) ;
说明与定义:
-
targetLength:当前字符串需要填充到的目标长度。如果这个数值小于当前字符串的长度,则返回当前字符串本身。
-
padString:(可选)填充字符串。如果字符串太长,使填充后的字符串长度超过了目标长度,则只保留最左侧的部分,其他部分会被截断,此参数的缺省值为 " "。
实例:
/* padStart */
console.log('0.0'.padStart(4,'10')) //10.0
console.log('0.0'.padStart(8,'10')) //101010.0
console.log('0.0'.padStart(7,'10')) //10100.0
console.log('0.0'.padStart(1,'10')) //0.0
/* padEnd */
console.log('0.0'.padEnd(4,'10')) //0.01
console.log('0.0'.padEnd(8,'10')) //0.010101
console.log('0.0'.padEnd(7,'10')) //0.01010
console.log('0.0'.padEnd(1,'10')) //0.0
5. Object.getOwnPropertyDescriptors()
Object.getOwnPropertyDescriptors()
函数用来获取一个对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。
语法:
- Object.getOwnPropertyDescriptors(obj)
返回obj
对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。
const obj2 = {
name: 'yaodao',
get age() { return '20' }
};
Object.getOwnPropertyDescriptors(obj2)
// {name: {…}, age: {…}}
//
//{
// name:{
// value: "yaodao",
// writable: true, //可写
// enumerable: true, //可枚举
// configurable: true, //可配置
// __proto__: Object,
// },
// age:{
// get: ƒ age(),
// set: undefined,
// enumerable: true,
// configurable: true,
// __proto__: Object
// __proto__: Object
// }
//}
在 vue 响应式原理中起重要的作用。
四、ES9新特性(2018)
- 异步迭代
- Promise.finally()
- Rest/Spread 属性
1. 异步迭代
在async/await
的某些时刻,你可能尝试在同步循环中调用异步函数。例如:
function Asy(x){
return new Promise(resolve=>{setTimeout(() =>resolve(x), 2000)})
}
async function Awa(){
let res = await Asy(1);
console.log(res); // 2秒钟之后出现1,再次等待3s,处理完后再处理下一条
console.log(3) // 2秒钟之后出现3
}
Awa();//异步,不进入主线程
console.log(4);//同步任务
//输出:
//4
//1
//3
ES2018引入异步迭代器(asynchronous iterators),这就像常规迭代器,除了next()
方法返回一个Promise。因此await
可以和for...of
循环一起使用,以串行的方式运行异步操作。例如:
async function process(array) {
for await (let i of array) {
doSomething(i);
}
}
2. Promise.finally()
一个Promise调用链要么成功到达最后一个.then()
,要么失败触发.catch()
。在某些情况下,你想要在无论Promise运行成功还是失败,运行相同的代码,例如清除,删除对话,关闭数据库连接等。
.finally()
允许你指定最终的逻辑:
function doSomething() {
doSomething1()
.then(doSomething2)
.then(doSomething3)
.catch(err => {
console.log(err);
})
.finally(() => {
// finish here!
});
}
3. Rest/Spread 属性
ES2015引入了Rest参数(…)和扩展运算符(…)。三个点(…)仅用于数组。Rest参数语法允许我们将一个不定数量的参数表示为一个数组。
function rest(p1, p2, ...p3) {
console.log(p1); //1
console.log(p2); //2
console.log(p3); //[3,4,5]
}
rest(1, 2, 3, 4, 5);
ES2018为对象解构提供了和数组一样的Rest参数()和展开操作符,一个简单的例子:
const myObject = {
a: 1,
b: 2,
c: 3
};
const { a, ...x } = myObject;
console.log(a); //=> 1
console.log({...x}); //=> {b: 2, c: 3}
或者你可以使用它给函数传递参数:
function rest({ a, ...x }) {
console.log(a);
console.log({...x})
}
rest({a: 1,b: 2,c: 3});
//输出:
//1
//{b: 2, c: 3}
五、ES10新特性(2019)
-
新增了Array的
flat()
方法和flatMap()
方法 -
新增了String的
trimStart()
方法和trimEnd()
方法 -
Object.fromEntries()
-
新的基本数据类型
BigInt
-
标准化 globalThis 对象
-
修改
catch
绑定 -
动态导入
1. 新增了Array的flat()
方法和flatMap()
方法
flat()
和flatMap()
本质上就是是归纳(reduce) 与 合并(concat)的操作。
Array.prototype.flat()
flat()
方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。 可用于数组降维,以及去除数组的空项 。参数:n(降维深度)。
/* 数组维数请从左到右数"[" */
/* 降维就从深维度开始往浅维度按照降维深度降维,极限剩下一维 */
var arr1 = [1, 2, [3, 4]];
arr1.flat();
// [1, 2, 3, 4]
var arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();
// [1, 2, 3, 4, [5, 6]]
var arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2);
// [1, 2, 3, 4, 5, 6]
//使用 Infinity 作为深度,展开任意深度的嵌套数组
arr3.flat(Infinity);
// [1, 2, 3, 4, 5, 6]
还可以利用flat()
方法的特性来去除数组的空项
var arr4 = [1, 2, , 4, 5];
arr4.flat();
// [1, 2, 4, 5]
Array.prototype.flatMap()
flatMap()
方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。它与 map 和 深度值1的 flat 几乎相同,但 flatMap 通常在合并成一种方法的效率稍微高一些。 这里我们拿map方法与flatMap方法做一个比较。
var arr1 = [1, 2, 3, 4];
arr1.map(x => [x * 2]);
// [[2], [4], [6], [8]]
arr1.flatMap(x => [x * 2]);
// [2, 4, 6, 8]
/* 会按照flatMap()构建数组维度 */
arr1.flatMap(x => [[x * 2]]);
// [[2], [4], [6], [8]]
2. 新增了String的trimStart()
方法和trimEnd()
方法
分别去除字符串首尾空白字符
let str = " Space around ";
console.log(str.trimEnd()); //=> " Space around"
console.log(str.trimStart()); //=> "Space around "
console.log(str.trim()); //=> "Space around"
3. Object.fromEntries()
Object.entries()
方法的作用是返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for…in 循环遍历该对象时返回的顺序一致(区别在于 for-in 循环也枚举原型链中的属性)。
而Object.fromEntries()
则是 Object.entries()
的反转。
let obj = { apple : 10, orange : 20, banana : 30 };
/* 获取键值对 */
let entries = Object.entries(obj);
console.log(entries);
/* 通过键值对反转回obj */
let fromEntries = Object.fromEntries(entries);
console.log(fromEntries);
结果:
4. 新的基本数据类型BigInt
参考blog: https://segmentfault.com/a/1190000019912017?utm_source=tag-newest
5. 标准化 globalThis
对象
这在ES10之前, globalThis
还没有标准化。
在产品代码中,你可以自己编写这个怪物,在多个平台上“标准化”它:
var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};
但即使这样也不总是奏效。因此,ES10 添加了 globalThis 对象,从现在开始,该对象用于在任何平台上访问全局作用域:
// 访问全局数组构造函数
var arr = globalThis.Array(0, 1, 2);
console.log(arr);
//=> [0, 1, 2]
// 类似于 ES5 之前的 window.v = { flag: true }
globalThis.v = { flag: true };
console.log(window.v);
//=> { flag: true }
6. 修改 catch
绑定
在过去,try/catch 语句中的 catch 语句需要一个变量。 try/catch 语句帮助捕获终端级别的错误:
try {
//...
}
catch(error) {
//...
console.log( error );
}
在某些情况下,所需的错误变量是未使用的。
在 ES10 中,捕获错误的变量是可选的
现在可以跳过错误变量:
try {
//...
return true;
}
catch{
//...
return false;
}
7. 动态导入
现在可以将导入分配给变量:
element.addEventListener('click', async() => {
/* 导入给一个常量 */
const module = await import(`./api-scripts/button-click.js`);
module.clickEvent();
})