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

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属性,能够将对象的自身可枚举属性复制到新对象上。请考虑如下示例:ubuntu

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()方法:promise

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

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正在迅速发展,而且每隔一段时间就会引入新功能,所以请查看完整提案列表,了解新功能的所有范围。你有什么特别兴奋的新功能吗?在评论中分享!

相关文章
相关标签/搜索