liushuangls / memo

用于记录
1 stars 0 forks source link

每个JavaScript开发人员都应该知道的ES2018新功能 #55

Open liushuangls opened 5 years ago

liushuangls commented 5 years ago

第九版ECMAScript标准,正式名称为ECMAScript 2018(简称ES2018),于2018年6月发布。从ES2016开始,ECMAScript规范的新版本每年发布一次,而不是每几年发布一次,并且添加的功能少于主要版本。ES2018添加了四个新的功能:新的 RegExp 功能 ,rest/spread 属性,asynchronous iterationPromise.prototype.finally 。此外,ES2018从 模板字符串 中删除了转义序列的语法限制。

Rest/Spread 属性

ES2015最有趣的功能之一是 扩展(spread)运算符。该运算符使复制和合并数组变得更加简单。可以使用运算符,来代替 concat()slice()方法:

const arr1 = [10, 20, 30];

// 复制 arr1
const copy = [...arr1];

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

const arr2 = [40, 50];

// 合并 arr2 与 arr1
const merge = [...arr1, ...arr2];

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

在必须传入数组参数的情况下,扩展运算符也能派上用场。例如:

const arr = [10, 20, 30]

// 等同于
// console.log(Math.max(10, 20, 30));
console.log(Math.max(...arr));    // → 30

ES2018通过向对象添加扩展属性来进一步扩展此语法。可以将对象的自身可枚举属性复制到新对象上。在ES2018之前,尝试这样做会引发错误。如果有多个具有相同名称的属性,则将使用最后一个属性。如下:

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

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

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

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

扩展运算符为浅复制。如果对象的属性也是对象,则仅复制属性的对象引用:

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

// copy1中的x属性的引用与copy2中的x属性的引用是相同的对象,因此 === 运算符返回true。
console.log(copy1.x === copy2.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
};

// rest属性必须始终出现在对象的末尾,否则会引发错误
// 在对象中使用多个 rest 语法会导致错误,除非它们是嵌套的
const {a, ...rest} = obj;

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

兼容性

浏览器兼容

Node: 8.3.0

异步迭代(Asynchronous Iteration)

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

如果对象具有Symbol.iterator属性,则该对象是可迭代的。在ES2015中,如SetMapArray具有Symbol.iterator属性,因此是可迭代的。以下代码给出了如何使用可迭代对象的示例:

const arr = [10, 20, 30];
const iterator = arr[Symbol.iterator]();

// value属性包含集合中元素的值。 done属性为布尔值,表示集合的结尾是否已到达。
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属性,则它可以变为可迭代,如下例所示:

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}

虽然以上代码没有问题,但却不必要地复杂化。幸运的是,使用 generator 函数可以大大简化过程:

const collection = {
  a: 10,
  b: 20,
  c: 30,
  [Symbol.iterator]: function * () {
    // 当没有yield返回值时,generator函数返回{value: undefined, done: true}
    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}

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

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} 
}));

迭代可迭代对象(iterable)的一种简单方法是使用for of语句,但是for of不能用于async 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 () {
   // 对于错误处理,使用try catch语句
  for await (const x of collection) {
    console.log(x);
  }
})();

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

兼容性

Node: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';
  });

then()catch()一样,finally()方法总是返回一个promise,因此您可以链接更多方法。通常,您希望使用finally()作为最后一个链,但在某些情况下,例如在发出HTTP请求时,将另一个catch()链接起来处理finally()中可能发生的错误是一种很好的做法。

兼容性

Node:10.0.0

新的RegExp功能

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

s (dotAll) 模式

.是正则表达式模式中的特殊字符,它匹配除换行符之外的任何字符。匹配所有字符(包括换行符)的解决方法是使用具有两个相反短字的字符类,例如[\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>...)语法的命名捕获组。因此,匹配日期的模式可以用不那么模糊的方式编写:

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只支持先行断言。 后行断言用(?<=...)表示,使您能够根据模式之前的子字符串匹配模式。例如,如果您想要以美元,英镑或欧元匹配产品的价格而不捕获货币符号,则可以使用/(?<=\$|£|€)\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/匹配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
// 如果要匹配任何Unicode字母字符,可以使用\p{Alphabetic}
// 还有一个相反版本为\P{...}
console.log(/\p{Number}/u.test(str));     // → true

兼容性

Node: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

兼容性

Node:8.10.0

总结

我们已经仔细研究了ES2018中引入的几个关键特性,包括asynchronous iterationrest/spread属性,Promise.prototype.finally()以及RegExp对象的新增功能。虽然其中一些浏览器供应商尚未完全实现其中一些功能,但由于像Babel这样的JavaScript转换器,它们今天仍然可以使用。

原文链接