前端点滴(ES6+)(二)

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.padStartString.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();
})
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章