1
ECMAScript标准的第九版,官方称为ECMAScript2018(或简称ES2018),于2018年6月发布。从ES2016开始,ECMAScript规范的新版本每年发布而不是每几年发布,并且添加的功能少于主要版本以前。该标准的最新版本通过添加四个新RegExp功能,休息/传播属性,异步迭代和,继续每年发布周期Promise.prototype.finally。此外,ES2018从标记模板中删除了转义序列的语法限制。

其余/传播属性

ES2015最有趣的功能之一是传播运营商。该运算符使复制和合并数组变得更加简单。您可以使用运算符,而不是调用concat()or 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通过向对象文字添加扩展属性来进一步扩展此语法。使用spread属性,可以将对象的自身可枚举属性复制到新对象上。请考虑以下示例:

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}

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}

但请注意,传播属性并不总是产生相同的结果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属性。相反,传播属性完全忽略了设置者。

重要的是要记住,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

该x物业copy1是指在内存中的同一对象x中copy2是指,所以全等运算的回报true。

ES2015增加的另一个有用功能是休息参数,它使JavaScript程序员可以使用它...来表示值作为数组。例如:

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

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

这里,第一个项目arr被分配给x,而剩余的元素被分配给rest变量。这种称为阵列解构的模式变得如此受欢迎

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

clipboard.png

Node.js的:

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

异步迭代

迭代数据集是编程的重要部分。此前ES2015,提供的JavaScript语句如for,for...in和while,和如方法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()方法。此方法返回具有两个属性的对象:value和done。该value属性包含集合中下一个元素的值。该done属性包含true或false表示集合的结尾是否已到达。

默认情况下,普通对象不可迭代,但如果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}。异步iterable定义了一个返回异步迭代器的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的迭代器来实现相同的结果。虽然普通的同步迭代器可以异步确定值,但它仍然需要同步确定“完成”的状态。

同样,你可以使用生成器函数简化过程,如下所示:

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属性决定产生价值。异步生成器执行相同的操作,只是它返回一个履行的promise {value, done}。

一个简单的方法来遍历一个迭代的对象是使用for...of的语句,但for...of不与异步iterables的工作value和done不同步确定。因此,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语句仅在异步生成器和异步函数中有效。违反此规则会导致a SyntaxError。

该next()方法可以返回拒绝的承诺。要优雅地处理被拒绝的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.

clipboard.png

Node.js的:

8.10.0(需要--harmony_async_iteration标志)
10.0.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.then(func, func)而不是实现相同的结果promise.finally(func),但是必须在履行处理程序和拒绝处理程序中重复相同的代码,或者为它声明一个变量:

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()。

clipboard.png

新的RegExp功能

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

s(dotAll)标志
命名捕获组
Lookbehind断言
Unicode属性转义

S(DOTALL)标志

dot(.)是正则表达式模式中的特殊字符,它匹配除换行符之外的任何字符,例如换行符(n)或回车符(r)。匹配所有字符(包括换行符)的解决方法是使用具有两个相反短字的字符类,例如[dD]。此字符类告诉正则表达式引擎找到一个数字(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>...)语法的命名捕获组。因此,匹配日期的模式可以用不太模糊的方式编写:

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

后向断言

ES2018为JavaScript带来了后瞻性断言,这些断言已在其他正则表达式实现中提供多年。以前,JavaScript只支持超前断言。lookbehind断言用表示(?<=...),并能够根据模式之前的子字符串匹配模式。例如,如果要在不捕获货币符号的情况下以美元,英镑或欧元匹配产品的价格,您可以使用/(?<=&dollar;|£|€)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,用表示(?<!...)。负外观允许您仅在模式不在后面的模式之前匹配模式。例如,如果模式/(?<!un)available/没有“un”前缀,则模式匹配可用单词:

const re = /(?<!un)available/;

console.log(re.exec('We regret this service is currently unavailable'));    
// → null

console.log(re.exec('The service is available'));             
// → ["available", index: 15, input: "The service is available", groups: undefined]

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属性转义中使用。

模板文字修订

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

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:xxxuuu"或者"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

结束

ES2018中引入的几个关键特性,包括异步迭代,休息/扩展属性Promise.prototype.finally()以及RegExp对象的添加。虽然有些浏览器供应商尚未完全实现其中一些功能,但由于像Babel这样的JavaScript转换器,它们今天仍然可以使用。

ECMAScript正在迅速发展,并且每隔一段时间就会引入新功能,因此请查看完整提案列表,了解新功能的全部范围。你有什么特别兴奋的新功能吗?在评论中分享!


程序员阿宇
3.2k 声望791 粉丝

前端学习交流群:784783012 欢迎新手,进阶者