每一个JavaScript开发人员都应该知道的新ES2018功能(译文)

前言

本文首发于个人我的网站: Timbok.top

正文

ECMAScript标准的第九版,官方称为ECMAScript 2018(或简称ES2018),于2018年6月发布。从ES2016开始,ECMAScript规范的新版本每一年发布而不是每几年发布一次,而且添加的功能少于主要版本之前。该标准的最新版本经过添加四个新RegExp功能,rest/spread属性,asynchronous iteration,和Promise.prototype.finally。此外,ES2018从标记模板中删除了转义序列的语法限制。javascript

这些新变化将在后面的小节中解释。css

rest/spread属性

ES2015最有趣的功能之一是点差运算符。该运算符使复制和合并数组变得更加简单。您可使用运算符...,而不是调用concat()or slice()方法:java

const arr1 = [10, 20, 30];

// make a copy of arr1
const copy = [...arr1];

console.log(copy);    // → [10, 20, 30]

const arr2 = [40, 50];

// merge arr2 with arr1
const merge = [...arr1, ...arr2];

console.log(merge);    // → [10, 20, 30, 40, 50]

在必须做为函数的单独参数传入数组的状况下,扩展运算符也派上用场。例如:git

const arr = [10, 20, 30]

// equivalent to
// console.log(Math.max(10, 20, 30));
console.log(Math.max(...arr));    // → 30

ES2018经过向对象文字添加扩展属性来进一步扩展此语法。使用spread属性,您能够将对象的自身可枚举属性复制到新对象上。请考虑如下示例:程序员

const obj1 = {
  a: 10,
  b: 20
};

const obj2 = {
  ...obj1,
  c: 30
};

console.log(obj2);    // → {a: 10, b: 20, c: 30}

在此代码中,...运算符用于检索属性obj1并将其分配给obj2。在ES2018以前,尝试这样作会引起错误。若是有多个具备相同名称的属性,则将使用最后一个属性:github

const obj1 = {
  a: 10,
  b: 20
};

const obj2 = {
  ...obj1,
  a: 30
};

console.log(obj2);    // → {a: 30, b: 20}

Spread属性还提供了一种合并两个或多个对象的新方法,能够将其用做方法的替代Object.assign()方法:正则表达式

const obj1 = {a: 10};
const obj2 = {b: 20};
const obj3 = {c: 30};

// ES2018
console.log({...obj1, ...obj2, ...obj3});    // → {a: 10, b: 20, c: 30}

// ES2015
console.log(Object.assign({}, obj1, obj2, obj3));    // → {a: 10, b: 20, c: 30}

但请注意,spread属性并不老是产生相同的结果Object.assign()。请考虑如下代码:编程

Object.defineProperty(Object.prototype, 'a', {
  set(value) {
    console.log('set called!');
  }
});

const obj = {a: 10};

console.log({...obj});    
// → {a: 10}

console.log(Object.assign({}, obj));    
// → set called!
// → {}

在此代码中,该Object.assign()方法执行继承的setter属性。相反,传播属性彻底忽略了setter。ubuntu

重要的是要记住,spread属性只复制可枚举的属性。在如下示例中,type属性不会显示在复制的对象中,由于其enumerable属性设置为false数组

const car = {
  color: 'blue'
};

Object.defineProperty(car, 'type', {
  value: 'coupe',
  enumerable: false
});

console.log({...car});    // → {color: "blue"}

即便它们是可枚举的,也会忽略继承的属性:

const car = {
  color: 'blue'
};

const car2 = Object.create(car, {
  type: {
    value: 'coupe',
    enumerable: true,
  }
});

console.log(car2.color);                      // → blue
console.log(car2.hasOwnProperty('color'));    // → false

console.log(car2.type);                       // → coupe
console.log(car2.hasOwnProperty('type'));     // → true

console.log({...car2});                       // → {type: "coupe"}

在此代码中,car2继承color属性car。由于spread属性只复制对象的本身的属性,color因此不包含在返回值中。

请记住,spread属性只能生成对象的浅表副本。若是属性包含对象,则仅复制对象的引用:

const obj = {x: {y: 10}};
const copy1 = {...obj};    
const copy2 = {...obj}; 

console.log(copy1.x === copy2.x);    // → true

这里copy1copy2的x是指在内存中的同一对象,因此全等运算返回true

ES2015中添加的另外一个有用功能是rest参数,它使JavaScript程序员可使用它...来表示值做为数组。例如:

const arr = [10, 20, 30];
const [x, ...rest] = arr;

console.log(x);       // → 10
console.log(rest);    // → [20, 30]

这里,arr的第一个值被分配给对应的x,而剩余的元素被分配给rest变量。这种称为阵列解构的模式变得如此受欢迎,以致于Ecma技术委员会决定为对象带来相似的功能:

const obj = {
  a: 10,
  b: 20,
  c: 30
};

const {a, ...rest} = obj;

console.log(a);       // → 10
console.log(rest);    // → {b: 20, c: 30}

此代码使用解构赋值中的其他属性将剩余的自身可枚举属性复制到新对象中。请注意,rest属性必须始终出如今对象的末尾,不然会引起错误:

const obj = {
  a: 10,
  b: 20,
  c: 30
};

const {...rest, a} = obj;    // → SyntaxError: Rest element must be last element

还要记住,在对象中使用多个rest会致使错误,除非它们是嵌套的:

const obj = {
  a: 10,
  b: {
    x: 20,
    y: 30,
    z: 40
  }
};

const {b: {x, ...rest1}, ...rest2} = obj;    // no error

const {...rest, ...rest2} = obj;    // → SyntaxError: Rest element must be last element

Support for Rest/Spread

Chrome Firefox Safari Edge
60 55 11.1 No
Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
60 55 11.3 No 8.2 60

Node.js

  • 8.0.0(运行时须要加-harmony
  • 8.3.0(彻底支持)

Asynchronous Iteration(异步迭代)

迭代数据集是编程的重要部分。此前ES2015,提供的JavaScript语句如forfor...inwhile,和方法map()filter()以及forEach()都用于此目的。为了使程序员可以一次一个地处理集合中的元素,ES2015引入了迭代器接口。

若是对象具备Symbol.iterator属性,则该对象是可迭代的。在ES2015中,字符串和集合对象(如Set,Map和Array)带有Symbol.iterator属性,所以能够迭代。如下代码给出了如何一次访问可迭代元素的示例:

const arr = [10, 20, 30];
const iterator = arr[Symbol.iterator]();
  
console.log(iterator.next());    // → {value: 10, done: false}
console.log(iterator.next());    // → {value: 20, done: false}
console.log(iterator.next());    // → {value: 30, done: false}
console.log(iterator.next());    // → {value: undefined, done: true}

Symbol.iterator是一个众所周知的符号,指定一个返回迭代器的函数。与迭代器交互的主要方法是next()方法。此方法返回具备两个属性的对象:valuedonevalue属性为集合中下一个元素的值。done属性的值为truefalse表示集合是否迭代完成。

默认状况下,普通对象不可迭代,但若是在其上定义Symbol.iterator属性,则它能够变为可迭代,以下例所示:

const collection = {
  a: 10,
  b: 20,
  c: 30,
  [Symbol.iterator]() {
    const values = Object.keys(this);
    let i = 0;
    return {
      next: () => {
        return {
          value: this[values[i++]],
          done: i > values.length
        }
      }
    };
  }
};

const iterator = collection[Symbol.iterator]();
  
console.log(iterator.next());    // → {value: 10, done: false}
console.log(iterator.next());    // → {value: 20, done: false}
console.log(iterator.next());    // → {value: 30, done: false}
console.log(iterator.next());    // → {value: undefined, done: true}

此对象是可迭代的,由于它定义了一个Symbol.iterator属性。迭代器使用该Object.keys()方法获取对象属性名称的数组,而后将其分配给values常量。它还定义了一个计数器变量i,并给它一个初始值0.当执行迭代器时,它返回一个包含next()方法的对象。每次调用next()方法时,它都返回一对{value, done}value保持集合中的下一个元素并done保持一个布尔值,指示迭代器是否已达到集合的须要。

虽然这段代码天衣无缝,但却没必要要。使用生成器函数能够大大简化过程:

const collection = {
  a: 10,
  b: 20,
  c: 30,
  [Symbol.iterator]: function * () {
    for (let key in this) {
      yield this[key];
    }
  }
};

const iterator = collection[Symbol.iterator]();
  
console.log(iterator.next());    // → {value: 10, done: false}
console.log(iterator.next());    // → {value: 20, done: false}
console.log(iterator.next());    // → {value: 30, done: false}
console.log(iterator.next());    // → {value: undefined, done: true}

在这个生成器中,for...in循环用于枚举集合并产生每一个属性的值。结果与前一个示例彻底相同,但它大大缩短了。

迭代器的缺点是它们不适合表示异步数据源。ES2018的补救解决方案是异步迭代器和异步迭代。异步迭代器与传统迭代器的不一样之处在于,它不是以形式返回普通对象{value, done},而是返回履行的承诺{value, done}。异步迭代定义了一个返回异步迭代器的Symbol.asyncIterator方法(而不是Symbol.iterator)。

一个例子让这个更清楚:

const collection = {
  a: 10,
  b: 20,
  c: 30,
  [Symbol.asyncIterator]() {
    const values = Object.keys(this);
    let i = 0;
    return {
      next: () => {
        return Promise.resolve({
          value: this[values[i++]], 
          done: i > values.length
        });
      }
    };
  }
};

const iterator = collection[Symbol.asyncIterator]();
  
console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 10, done: false}
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 20, done: false} 
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 30, done: false} 
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: undefined, done: true} 
}));

请注意,不可以使用promises的迭代器来实现相同的结果。虽然普通的同步迭代器能够异步肯定值,但它仍然须要同步肯定done的状态。

一样,您可使用生成器函数简化过程,以下所示:

const collection = {
  a: 10,
  b: 20,
  c: 30,
  [Symbol.asyncIterator]: async function * () {
    for (let key in this) {
      yield this[key];
    }
  }
};

const iterator = collection[Symbol.asyncIterator]();
  
console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 10, done: false}
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 20, done: false} 
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 30, done: false} 
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: undefined, done: true} 
}));

一般,生成器函数返回带有next()方法的生成器对象。当调用next()时,它返回一个{value,done},其value属性保存了yield值。异步生成器执行相同的操做,除了它返回一个履行{value,done}的promise。

迭代可迭代对象的一种简单方法是使用for...of语句,可是for...of不能与async iterables一块儿使用,由于valuedone不是同步肯定的。所以,ES2018提供了for...await...of。咱们来看一个例子:

const collection = {
  a: 10,
  b: 20,
  c: 30,
  [Symbol.asyncIterator]: async function * () {
    for (let key in this) {
      yield this[key];
    }
  }
};

(async function () {
  for await (const x of collection) {
    console.log(x);
  }
})();

// logs:
// → 10
// → 20
// → 30

在此代码中,for...await...of语句隐式调用Symbol.asyncIterator集合对象上的方法以获取异步迭代器。每次循环时,都会调用迭代器的next()方法,它返回一个promise。一旦解析了promise,就会将结果对象的value属性读取到x变量中。循环继续,直到返回的对象的done属性值为true

请记住,该for...await...of语句仅在异步生成器和异步函数中有效。违反此规则会致使一个SyntaxError报错。

next()方法可能会返回拒绝的promise。要优雅地处理被拒绝的promise,您能够将for...await...of语句包装在语句中try...catch,以下所示:

const collection = {
  [Symbol.asyncIterator]() {
    return {
      next: () => {
        return Promise.reject(new Error('Something went wrong.'))
      }
    };
  }
};

(async function() {
  try {
    for await (const value of collection) {}
  } catch (error) {
    console.log('Caught: ' + error.message);
  }
})();

// logs:
// → Caught: Something went wrong.

Support for Asynchronous Iterators

Chrome Firefox Safari Edge
63 57 12 No
Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
63 57 12 No 8.2 63

Node.js

  • 8.0.0(运行时须要加-harmony
  • 8.3.0(彻底支持)

Promise.prototype.finally

ES2018的另外一个使人兴奋的补充是finally()方法。一些JavaScript库以前已经实现了相似的方法,这在许多状况下证实是有用的。这鼓励了Ecma技术委员会正式添加finally()到规范中。使用这个方法,程序员将能无论promise的结果如何,都能执行一个代码块。咱们来看一个简单的例子:

fetch('https://www.google.com')
  .then((response) => {
    console.log(response.status);
  })
  .catch((error) => { 
    console.log(error);
  })
  .finally(() => { 
    document.querySelector('#spinner').style.display = 'none';
  });

finally()不管操做是否成功,当您须要在操做完成后进行一些清理时,该方法会派上用场。在此代码中,该finally()方法只是在获取和处理数据后隐藏加载微调器。代码不是在then()catch()方法中复制最终逻辑,而是在promise被fulfilled或rejected后注册要执行的函数。

你可使用promise.then(func,func)而不是promise.finally(func)来实现相同的结果,但你必须在fulfillment处理程序和rejection处理程序中重复相同的代码,或者为它声明一个变量:

fetch('https://www.google.com')
  .then((response) => {
    console.log(response.status);
  })
  .catch((error) => { 
    console.log(error);
  })
  .then(final, final);

function final() {
  document.querySelector('#spinner').style.display = 'none';
}

then()catch()同样,finally()方法老是返回一个promise,所以能够连接更多的方法。一般,您但愿使用finally()做为最后一个链,但在某些状况下,例如在发出HTTP请求时,最好连接另外一个catch()以处理finally()中可能发生的错误。

Support for Promise.prototype.finally

Chrome Firefox Safari Edge
63 58 11.1 18
Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
63 58 11.1 No 8.2 63

Node.js

  • 10.0.0(彻底支持)

新的RegExp功能

ES2018为该RegExp对象增长了四个新功能,进一步提升了JavaScript的字符串处理能力。这些功能以下:

  • S(DOTALL)标志
  • Named Capture Groups(命名捕获组)
  • Lookbehind Assertions(后向断言)
  • Unicode Property Escapes(Unicode属性转义)

S(DOTALL)标志

点(.)是正则表达式模式中的特殊字符,它匹配除换行符以外的任何字符,例如换行符(\n)或回车符(\r)。匹配全部字符(包括换行符)的解决方法是使用具备两个相反短字的字符类,例如[\d\D]。此字符类告诉正则表达式引擎找到一个数字(\d)或非数字(\D)的字符。所以,它匹配任何字符:

console.log(/one[\d\D]two/.test('one\ntwo'));    // → true

ES2018引入了一种模式,其中点可用于实现相同的结果。可使用s标志在每一个正则表达式的基础上激活此模式:

console.log(/one.two/.test('one\ntwo'));     // → false
console.log(/one.two/s.test('one\ntwo'));    // → true

使用标志来选择新行为的好处是向后兼容性。所以,使用点字符的现有正则表达式模式不受影响。

Named Capture Groups(命名捕获组)

在一些正则表达式模式中,使用数字来引用捕获组可能会使人困惑。例如,使用/(\d{4})-(\d{2})-(\d{2})/与日期匹配的正则表达式。因为美式英语中的日期符号与英式英语不一样,所以很难知道哪一个组指的是哪一天,哪一个组指的是月份:

const re = /(\d{4})-(\d{2})-(\d{2})/;
const match= re.exec('2019-01-10');

console.log(match[0]);    // → 2019-01-10
console.log(match[1]);    // → 2019
console.log(match[2]);    // → 01
console.log(match[3]);    // → 10

ES2018引入了使用(?<name>...)语法的命名捕获组。所以,匹配日期的模式能够用不那么模糊的方式编写:

const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = re.exec('2019-01-10');

console.log(match.groups);          // → {year: "2019", month: "01", day: "10"}
console.log(match.groups.year);     // → 2019
console.log(match.groups.month);    // → 01
console.log(match.groups.day);      // → 10

您可使用\k<name>语法在模式中稍后调用命名的捕获组。例如,要在句子中查找连续的重复单词,您可使用/\b(?<dup>\w+)\s+\k<dup>\b/

const re = /\b(?<dup>\w+)\s+\k<dup>\b/;
const match = re.exec('Get that that cat off the table!');        

console.log(match.index);    // → 4
console.log(match[0]);       // → that that

要将命名的捕获组插入到方法的替换字符串中replace(),您须要使用$<name>构造。例如:

const str = 'red & blue';

console.log(str.replace(/(red) & (blue)/, '$2 & $1'));    
// → blue & red

console.log(str.replace(/(?<red>red) & (?<blue>blue)/, '$<blue> & $<red>'));    
// → blue & red

Lookbehind Assertions(后向断言)

ES2018为JavaScript带来了后向性断言,这些断言已经在其余正则表达式实现中可用多年。之前,JavaScript只支持超前断言。后向断言用表示(?<=...),并使您可以匹配基于模式以前的子字符串的模式。例如,若是要在不捕获货币符号的状况下以美圆,英镑或欧元匹配产品的价格,则可使用/(?<=\$|£|€)\d+(\.\d*)?/

const re = /(?<=\$|£|€)\d+(\.\d*)?/;

console.log(re.exec('199'));     
// → null

console.log(re.exec('$199'));    
// → ["199", undefined, index: 1, input: "$199", groups: undefined]

console.log(re.exec('€50'));     
// → ["50", undefined, index: 1, input: "€50", groups: undefined]

还有一个lookbehind的否认版本,用(?<!...),只有当模式前面没有lookbehind中的模式时,负lookbehind才容许您匹配模式。例如,模式/(?<!un)available/匹配没有“un”前缀的可用词

这段翻译的很差,放上原文

There is also a negative version of lookbehind, which is denoted by (?<!...). A negative lookbehind allows you to match a pattern only if it is not preceded by the pattern within the lookbehind. For example, the pattern /(?<!un)available/ matches the word available if it does not have a "un" prefix:

Unicode Property Escapes(Unicode属性转义)

ES2018提供了一种称为Unicode属性转义的新类型转义序列,它在正则表达式中提供对完整Unicode的支持。假设您要在字符串中匹配Unicode字符㉛。虽然㉛被认为是一个数字,可是你不能将它与\d速记字符类匹配,由于它只支持ASCII [0-9]字符。另外一方面,Unicode属性转义可用于匹配Unicode中的任何十进制数:

const str = '㉛';

console.log(/\d/u.test(str));    // → false
console.log(/\p{Number}/u.test(str));     // → true

一样,若是要匹配任何Unicode字母字符,你可使用\p{Alphabetic}

const str = 'ض';

console.log(/\p{Alphabetic}/u.test(str));     // → true

// the \w shorthand cannot match ض
  console.log(/\w/u.test(str));    // → false

还有一个否认版本\p{...},表示为\P{...}

console.log(/\P{Number}/u.test('㉛'));    // → false
console.log(/\P{Number}/u.test('ض'));    // → true

console.log(/\P{Alphabetic}/u.test('㉛'));    // → true
console.log(/\P{Alphabetic}/u.test('ض'));    // → false

除了字母和数字以外,还有几个属性能够在Unicode属性转义中使用。您能够在当前规范提案中找到支持的Unicode属性列表。

Support for New RegExp

  • | Chrome | Firefox | Safari | Edge
S(DOTALL)标志 62 No 11.1 No
命名捕获组 64 No 11.1 No
后向断言 62 No No No
Unicode属性转义 64 No 11.1 No
  • | Chrome Android | Firefox Android | iOS Safari | Edge Mobile | Samsung Internet | Android Webview
S(DOTALL)标志 62 No 11.3 No 8.2 62
命名捕获组 64 No 11.3 No No 64
后向断言 62 No No No 8.2 62
Unicode属性转义 64 No 11.3 No No 64

Node.js

  • 8.3.0 (运行时须要加-harmony)
  • 8.10.0 (support for s (dotAll) flag and lookbehind assertions)
  • 10.0.0 (彻底支持)

模板字符串

当模板字符串紧跟在表达式以后时,它被称为标记模板字符串。当您想要使用函数解析模板文字时,标记的模板会派上用场。请考虑如下示例:

function fn(string, substitute) {
  if(substitute === 'ES6') {
    substitute = 'ES2015'
  }
  return substitute + string[1];
}

const version = 'ES6';
const result = fn`${version} was a major update`;

console.log(result);    // → ES2015 was a major update

在此代码中,调用标记表达式(它是常规函数)并传递模板文字。该函数只是修改字符串的动态部分并返回它。

在ES2018以前,标记的模板字符串具备与转义序列相关的语法限制。反斜杠后跟某些字符序列被视为特殊字符:\x解释为十六进制转义符,\u解释为unicode转义符,\后跟一个数字解释为八进制转义符。其结果是,字符串,例如"C:\xxx\uuu"或者"\ubuntu"被认为是由解释无效转义序列,并会抛出SyntaxError。

ES2018从标记模板中删除了这些限制,而不是抛出错误,表示无效的转义序列以下undefined

function fn(string, substitute) {
  console.log(substitute);    // → escape sequences:
  console.log(string[1]);     // → undefined
}

const str = 'escape sequences:';
const result = fn`${str} \ubuntu C:\xxx\uuu`;

请记住,在常规模板文字中使用非法转义序列仍会致使错误:

const result = `\ubuntu`;
// → SyntaxError: Invalid Unicode escape sequence

Support for Template Literal Revision

Chrome Firefox Safari Edge
62 56 11 No
Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
62 56 11 No 8.2 62

Node.js

  • 8.3.0 (运行时须要加-harmony
  • 8.10.0(彻底支持)

总结

咱们已经仔细研究了ES2018中引入的几个关键特性,包括异步迭代,rest/spread属性Promise.prototype.finally()以及RegExp对象的添加。虽然其中一些浏览器供应商还没有彻底实现其中一些功能,但因为像Babel这样的JavaScript转换器,它们今天仍然可使用。

ECMAScript正在迅速发展,而且每隔一段时间就会引入新功能,所以请查看已完成提案的列表,了解新功能的所有内容。

第一次翻译文章,能力有限,水平通常,翻译不妥之处,还望指正。感谢。

相关文章
相关标签/搜索