ES2015,2016,2017 特性 及使用

669 阅读10分钟

概述

ECMAScript 和 JavaScript 到底是什么关系?

1996 年 11 月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给标准化组织 ECMA,希望这种语言能够成为国际标准。该标准从一开始就是针对 JavaScript 语言制定的,但是JavaScript已经被Netscape公司注册为商标。再者想体现这门语言的制定者是 ECMA,不是Netscape,故取名ECMAScript,这样也有利于保证这门语言的开放性和中立性。 简而言之,ECMAScript和JavaScript的关系是,前者是后者的规格,后者是前者的一种实现。

ECMAScript版本 发布时间 新增特性
ECMAScript 2009(ES5) 2009年11月 扩展了Object、Array、Function的功能等
ECMAScript 2015(ES6) 2015年6月 类,模块化,箭头函数,函数参数默认值等
ECMAScript 2016(ES7) 2016年3月 includes,指数操作符
ECMAScript 2017(ES8) 2017年6月 sync/await,Object.values(),Object.entries(),String padding等

ES6的特性(2015)

2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

列举几个经常使用的:

  • 模块化
  • 箭头函数
  • 函数参数默认值
  • 模板字符串
  • 解构赋值
  • 延展操作符
  • 对象属性简写
  • Promise
  • Let与Const

类(class)

class Example {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
 static classMethod() {
    return 'hello';
  }
  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
var p1 = new Example(2,3);
var p2 = new Example(3,2);

p1.__proto__ === p2.__proto__
//true

Example.classMethod() // 'hello'

var example = new Example();
example.classMethod() // TypeError: foo.classMethod is not a function

class Bar extends Example {
  static classMethodBar() {
    return super.classMethod() + ', too';
  }
}
Bar.classMethodBar() // "hello, too"

Bar.classMethod() // 'hello'

这里定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5 的构造函数Point,对应 ES6 的Point类的构造方法。

Point类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。

ES6 的类,完全可以看作构造函数的另一种写法。与ES5一样,类的所有实例共享一个原型对象。

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。父类的静态方法,可以被子类继承。静态方法也是可以从super对象上调用的。

class A {
    print() {
    console.log(this.x);
  }
}
A.prototype.x = 1;

class B extends A {
  constructor() {
    super();
    console.log(super.x) // 1
    this.x = 2;
  }
  m() {
    super.print();
  }
}

let b = new B();
b.m() // 2

上面代码中,属性x是定义在A.prototype上面的,所以super.x可以取到它的值。super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()内部的this指向子类B的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)。

模块化(Module)

Es6 之前,社区的模块加载方案最主要的有 CommonJS 和 AMD 两种。

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

// CommonJS模块
let { stat, exists, readFile } = require('fs');
// ES6模块
import { stat, exists, readFile } from 'fs';

模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

export 命令
// person.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export { firstName, lastName, year };
export default firstName
import 命令
import { firstName, lastName, year } from './person.js';
import firstName from './person.js'; // defauls 默认导出不需要括号

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}

export { firstName, lastName } from 'person.js'; // 复合写法

箭头(Arrow)函数

example = function (name) {
}
example = name => {
}

箭头函数不但易书写,更大的好处在于 融合了 上下文环境,不改变方法内部 this 指向。不再需要var self = this;

ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。

function person(firstName = 'david', age = 25)
{
    // ...
}
person('',0) // age = 0 ,firstName = ''

function person(firstName, lastName)
{
    var firstName = firstName || 'david';
    var age = age || 25;
    //...
}

person('',0) // age = 25 ,firstName = 'david'

因为0的布尔值为false,这样age的取值将是25。firstName的取值为‘david’。可以看出Es6 允许为函数的参数设置默认值后不仅能是代码变得更加简洁而且能规避一些问题。

模板字符串

Hello ${name}, how are you ${time}? 

可以很畅快的拼接字符串。

解构赋值 与 延展操作符

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

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

let [head, ...tail] = [1, 2, 3, 4]; // 这里用到 另一个新语法延展操作符
head // 1
tail // [2, 3, 4]

let [x, y, ...z] = ['a']; 
x // "a"
y // undefined
z // []

let [x, y = 'b'] = ['a']; // x='a', y='b'

let x = 1;
let y = 2;

[x, y] = [y, x]; // 快速交换变量值


let { foo, bar } = { foo: 'aaa', bar: 'bbb' }; 
foo // "aaa"
bar // "bbb"

let {foo} = {bar: 'baz'};
foo // undefined

let与const

  var a = 1,b = 2;
{
  let c = a;
  var d = b;
}

c // ReferenceError: a is not defined.
d // 2

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

const PI = 3.1415;
PI // 3.1415

PI = 3;
// TypeError: Assignment to constant variable.

变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。若是var声明的,里面的i指向的就是全局的i。导致运行时输出的是最后一轮的i的值,也就是 10。。

const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。const的作用域与let命令相同:只在声明所在的块级作用域内有效。

Promise,对象属性简写。

Promise对象有以下两个特点。

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

ES7的特性(2016)

  • Array.prototype.includes()
  • 指数操作符

ES6 由于间隔数年才发布 可以看到 更新较为多,而之后ES的发布频率较为频繁,基本一年一次,所以更新 较少。

Array.prototype.includes()

includes() 函数用来判断一个数组是否包含一个指定的值,如果包含则返回 true,否则返回false。

arr.includes(x)  // ES7
arr.indexOf(x) >= 0 // 之前的做法

指数操作符

console.log(2**2);// 输出4 ES7 
Math.pow(2, 2); //  之前的做法

ES8的特性(2017)

  • Object.values() 和 Object.entries()
  • String.prototype.padEnd() 和 String.prototype.padStart()
  • Object.getOwnPropertyDescriptors()
  • async/await

Object.values() 和 Object.entries()

Object.values()返回一个数组,其元素是在对象上找到的可枚举属性值。属性的顺序与通过手动循环对象的属性值所给出的顺序相同。

需要注意的是 如果可枚举对象的key 为数字,会根据键以数字顺序返回值。

var obj = { foo: 'bar', baz: 42 };
console.log(Object.values(obj)); // ['bar', 42]

var an_obj = { 100: 'a', 2: 'b', 7: 'c' };
console.log(Object.values(an_obj)); // ['b', 'c', 'a']

if (!Object.values) Object.values = function(obj) { // 向下兼容仿写
    if (obj !== Object(obj))
        throw new TypeError('Object.values called on a non-object');
    var val=[],key;
    for (key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj,key)) {
            val.push(obj[key]);
        }
    }
    return val;
}

Object.entries()返回一个数组,其元素是与直接在object上找到的可枚举属性键值对相对应的数组。属性的顺序与通过手动循环对象的属性值所给出的顺序相同。

需要注意的是 如果可枚举对象的key 为数字,会根据键以数字顺序返回值。

const obj = { foo: 'bar', baz: 42 };
console.log(Object.entries(obj)); // [ ['foo', 'bar'], ['baz', 42] ]

const anObj = { 100: 'a', 2: 'b', 7: 'c' };
console.log(Object.entries(anObj)); // [ ['2', 'b'], ['7', 'c'], ['100', 'a'] ]

String.prototype.padEnd() 和 String.prototype.padStart()

padStart() 从左侧添加字符以达到给定长度,若填充后的字符串长度超过了目标长度,则只保留最左侧的部分,其他部分会被截断。

'abc'.padStart(10);         // "       abc" 填充到指定长度
'abc'.padStart(6,"123465"); // "123abc" 保留左侧
'abc'.padStart(8, "0");     // "00000abc" 重复填充入参到指定长度
'abc'.padStart(1);          // "abc" 保留左侧
// 如果原生环境不支持该方法,在其他代码之前先运行下面的代码,将创建 String.prototype.padStart() 方法。
if (!String.prototype.padStart) {
    String.prototype.padStart = function padStart(targetLength,padString) {
        targetLength = targetLength>>0; //floor if number or convert non-number to 0;
        padString = String((typeof padString !== 'undefined' ? padString : ' '));
        if (this.length > targetLength) {
            return String(this);
        }
        else {
            targetLength = targetLength-this.length;
            if (targetLength > padString.length) {
                padString += padString.repeat(targetLength/padString.length); //append to original to ensure we are longer than needed
            }
            return padString.slice(0,targetLength) + String(this);
        }
    };
}

padEnd() 特性与padStart 差之不大,只不过是从尾部填充至指定长度。

'abc'.padEnd(10);         // "abc       " 填充到指定长度
'abc'.padEnd(6,"123465"); // "abc123" 保留左侧
'abc'.padEnd(8, "0");     // "abc00000" 重复填充入参到指定长度
'abc'.padEnd(1);          // "abc" 保留左侧
// 如果原生环境不支持该方法,在其他代码之前先运行下面的代码,将创建 String.prototype.padEnd() 方法。
if (!String.prototype.padEnd) {
    String.prototype.padEnd = function padEnd(targetLength,padString) {
        targetLength = targetLength>>0; //floor if number or convert non-number to 0;
        padString = String((typeof padString !== 'undefined' ? padString: ''));
        if (this.length > targetLength) {
            return String(this);
        }
        else {
            targetLength = targetLength-this.length;
            if (targetLength > padString.length) {
                padString += padString.repeat(targetLength/padString.length); //append to original to ensure we are longer than needed
            }
            return String(this) + padString.slice(0,targetLength);
        }
    };
}

getOwnPropertyDescriptors

Object.getOwnPropertyDescriptors()方法返回给定对象的所有自己的属性描述符。如果没有属性,则可能是一个undefined。

const object1 = {
  property1: 42
};

const descriptors1 = Object.getOwnPropertyDescriptors(object1);
console.log(descriptors1.property1)
// {
//    configurable: true
//    enumerable: true
//    value: 42
//    writable: true
//    __proto__: Object
//}

async/await

异步函数可以包含await指令,该指令会暂停异步函数的执行,并等待Promise执行,然后继续执行异步函数,并返回结果(异步转同步)。

await 关键字只在异步函数内有效。如果你在异步函数外使用它,会抛出语法错误。

function resolveAfter2Seconds() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('slow promise is done');
    }, 2000);
  });
}
function resolveAfterSeconds() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('fast promise is done');
    }, 1000);
  });
}

async function asyncCall() {
  console.log('calling');
  var slow = await resolveAfter2Seconds();
  console.log(slow);
  var fast = await resolveAfterSeconds();
  console.log(fast);
  console.log(1);
}

asyncCall();

// "calling"
// Promise {<pending>}
// "slow promise is done"
// "fast promise is done"
// 1
async函数返回一个 Promise对象。async函数内部return语句返回的值,会成为then方法回调函数的参数。
async function f() {
 // 等同于
  // return 'hello world';
  return await 'hello world';
}

f().then(v => console.log(v))
// "hello world"

错误处理
async function main() {
  try {
    const val1 = await firstStep();
    const val2 = await secondStep(val1);
    const val3 = await thirdStep(val1, val2);
    
    return val1
  }
  catch (err) {
    return err
  }
}
main().then(e=>{console.log(e)})
//ReferenceError: firstStep is not defined
//  at main 

参考

MDN

ECMAScript 6 入门