每个 JavaScript 开发者都应该知道的 ES2018 新特性

Avatar of Faraz Kelhini
Faraz Kelhini

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元免费额度!

ECMAScript 标准的第九版,正式名称为 ECMAScript 2018(或简称为 ES2018),于 2018 年 6 月发布。 从 ES2016 开始,ECMAScript 规范的新版本每年发布一次,而不是每隔几年发布一次,并且添加的功能少于以前的主要版本。 该标准的最新版本继续年度发布周期,添加了四个新的 RegExp 功能、剩余/扩展属性、异步迭代和 Promise.prototype.finally。 此外,ES2018 取消了标记模板中转义序列的语法限制。

以下各节将解释这些新的更改。

剩余/扩展属性

添加到 ES2015 的最有趣的功能之一是扩展运算符。 此运算符使复制和合并数组变得更加简单。 您无需调用 concat()slice() 方法,而是可以使用 ... 运算符

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]

扩展运算符在必须将数组作为单独参数传递给函数的情况下也派上用场。 例如

const arr = [10, 20, 30]

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

ES2018 通过向对象字面量添加扩展属性来进一步扩展此语法。 使用扩展属性,您可以将对象的自身可枚举属性复制到新对象上。 考虑以下示例

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

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

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

在此代码中,... 运算符用于检索 obj1 的属性并将其分配给 obj2。 在 ES2018 之前,尝试这样做会引发错误。 如果有多个属性具有相同的名称,则将使用最后出现的属性

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

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

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

扩展属性还提供了一种合并两个或多个对象的新方法,可以用作 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}

但是请注意,扩展属性并不总是产生与 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。

重要的是要记住,扩展属性只复制可枚举属性。 在以下示例中,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"}

在此代码中,car2car 继承 color 属性。 由于扩展属性只复制对象的自身属性,因此 color 不包含在返回值中。

请记住,扩展属性只能创建对象的浅拷贝。 如果属性包含对象,则只会复制该对象的引用

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

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

copy1 中的 x 属性引用内存中与 copy2 中的 x 相同的对象,因此严格相等运算符返回 true

ES2015 添加的另一个有用功能是剩余参数,它使 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}

此代码在解构赋值中使用剩余属性将剩余的自身可枚举属性复制到新对象中。 请注意,剩余属性必须始终出现在对象的末尾,否则会引发错误

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

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

还要记住,除非嵌套,否则在对象中使用多个剩余语法会导致错误

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

对剩余/扩展属性的支持

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

Node.js

  • 8.0.0(需要 --harmony 运行时标志)
  • 8.3.0(完全支持)

异步迭代

迭代数据集合是编程的重要组成部分。 在 ES2015 之前,JavaScript 提供了诸如 forfor...inwhile 之类的语句,以及诸如 map()filter()forEach() 之类的方法来实现此目的。 为了使程序员能够一次处理集合中的一个元素,ES2015 引入了迭代器接口。

如果对象具有 Symbol.iterator 属性,则该对象是可迭代的。 在 ES2015 中,字符串和集合对象(如 SetMapArray)带有 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 常量。 它还定义了一个计数器变量并将其初始值设置为 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} 
}));

请注意,无法使用承诺迭代器来实现相同的结果。 虽然普通同步迭代器可以异步确定值,但它仍然需要同步确定“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 属性持有生成的 value。 异步生成器做同样的事情,只是它返回一个承诺,该承诺将实现为 {value, done}

迭代可迭代对象的一种简单方法是使用 for...of 语句,但 for...of 不适用于异步可迭代对象,因为 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() 方法,该方法返回一个承诺。 一旦承诺被解决,就会将结果对象的 value 属性读入 x 变量。 循环持续到返回对象的 done 属性的值为 true

请记住,for...await...of 语句只在异步生成器和异步函数中有效。 违反此规则会导致 SyntaxError

next() 方法可能会返回一个被拒绝的承诺。 为了优雅地处理被拒绝的承诺,您可以将 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.

对异步迭代器的支持

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

Node.js

  • 8.10.0(需要 –harmony_async_iteration 标志)
  • 10.0.0(完全支持)

Promise.prototype.finally

ES2018 添加的另一个令人兴奋的功能是 finally() 方法。 一些 JavaScript 库之前已经实现了类似的方法,这在许多情况下都很有用。 这促使 Ecma 技术委员会将 finally() 添加到规范中。 使用此方法,程序员将能够执行代码块,无论承诺的命运如何。 让我们来看一个简单的例子

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() 方法只是在获取和处理数据后隐藏加载微调器。代码注册了一个函数,该函数在 promise fulfilled 或 rejected 时执行,而不是在 then()catch() 方法中复制最终逻辑。

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

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() 中发生的错误是一个好习惯。

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 8.2 63

Node.js

10.0.0(完全支持)

新的 RegExp 特性

ES2018 为 RegExp 对象添加了四个新特性,进一步提高了 JavaScript 的字符串处理能力。这些特性如下

  • s(dotAll)标志
  • 命名捕获组
  • 后顾断言
  • 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

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

命名捕获组

在某些正则表达式模式中,使用数字引用捕获组可能会令人困惑。例如,以正则表达式 /(\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>...)</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</day></month></year>

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

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</dup></dup>

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

const str = 'red &amp; blue';

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

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

后顾断言

ES2018 将后顾断言引入 JavaScript,这些断言多年来一直在其他正则表达式实现中使用。以前,JavaScript 只支持前瞻断言。后顾断言由 (?&lt;=...) 表示,并允许您根据模式之前的子字符串匹配模式。例如,如果要匹配美元、英镑或欧元的商品价格,但不捕获货币符号,可以使用 /(?&lt;=\$|£|€)\d+(\.\d*)?/

const re = /(?&lt;=\$|£|€)\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]

还存在后顾的否定版本,它由 (?!... 表示。否定后顾允许您仅当模式不在后顾中的模式之前时匹配模式。例如,模式 <code>/(? 匹配单词 available,如果它没有 "un" 前缀:

<code>
<pre rel="JavaScript"><code class="language-javascript">const re = /(?

<h4>Unicode 属性转义</h4>
ES2018 提供了一种称为 Unicode 属性转义的新型转义序列,它为正则表达式提供了对完整 Unicode 的支持。假设您要匹配字符串中的 Unicode 字符 ㉛。尽管 ㉛ 被认为是一个数字,但您不能使用 <code>\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

除了 Alphabetic 和 Number 之外,Unicode 属性转义中还可以使用更多属性。您可以在 当前规范提案 中找到支持的 Unicode 属性列表。

对新的 RegExp 特性的支持

Chrome Firefox Safari Edge
s(dotAll)标志 62 11.1
命名捕获组 64 11.1
后顾断言 62
Unicode 属性转义 64 11.1
Chrome(Android) Firefox(Android) iOS Safari Edge Mobile Samsung Internet Android Webview
s(dotAll)标志 62 11.3 8.2 62
命名捕获组 64 11.3 64
后顾断言 62 8.2 62
Unicode 属性转义 64 11.3 64

Node.js

  • 8.3.0(需要 –harmony 运行时标志)
  • 8.10.0(支持 s(dotAll)标志和后顾断言)
  • 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

对模板字面量修订的支持

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

Node.js

  • 8.3.0(需要 –harmony 运行时标志)
  • 8.10.0(完全支持)

总结

我们已经很好地了解了 ES2018 中引入的几个关键特性,包括异步迭代、rest/spread 属性、Promise.prototype.finally() 以及对 RegExp 对象的添加。尽管其中一些特性尚未被一些浏览器供应商完全实现,但由于 JavaScript 转译器(如 Babel),它们今天仍然可以使用。

ECMAScript 正在快速发展,新特性经常被引入,因此请查看 已完成的提案列表,了解所有新增内容。您对哪些新特性特别感兴趣?在评论中分享它们!