ES2018新特性——每个JS开发者都需要了解

文章转载自众成翻译,原文链接https://www.zcfy.cc/article/new-es2018-features-every-javascript-developer-should-know

第9版ECMAScript标准于2018年6月发布,正式名称为ECMAScript 2018(简称ES2018)。从ES2016开始,ECMAScript规范的新版本每年发布一次,而不是每隔几年发布一次,相应的,每版增加的功能也更少一些。最新版本的标准通过添加4个新的RegExp特性、rest/spread属性、异步迭代和Promise.prototype.finally来延续每年的发布周期。此外,ES2018取消了标记模板转义序列的语法限制。

以下将逐一解释这些变动。

Rest/Spread 特性

ES2015中添加的最有趣的特性之一是spread操作符。你可以用它替换cancat()和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]

在数组必须以拆解的方式作为函数参数的情况下,spread操作符也很有用。例如:

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}

在上述代码中,spread操作符遍历obj1属性,并将其添加到obj2的属性中;而在之前的版本中,如此处理会抛出一个异常。需要注意的是,如果存在相同的属性名,只有最后一个会生效。

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属性;而spread操作忽略了setter。

划重点:spread只复制枚举属性。在下面的例子中,type属性不会出现在复制对象中,因为它的枚举属性被设置为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继承了car中的color属性。因为spread操作只会复制对象自身的属性,color并没有出现在新的对象中。

spread只会进行浅拷贝,如果属性的值是一个对象的话,只有对象的引用会被拷贝:

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

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

copy1.x 和 copy2.x 指向同一个对象的引用,所以他们严格相等。

ES2015增加的另一个有用特性是rest参数,它允许JS使用……将值表示为数组:

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属性将剩余的可枚举属性复制到一个新对象中。注意,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

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 (完全支持)

异步迭代

遍历是编程的一个重要部分。JS提供了for、for…in和while以及map()、filter()和forEach()等遍历数据的方法。在ES2015则引入了迭代器接口。

包含Symbol.iterator属性的对象是可迭代对象,如字符串和集合对象(如Set、Map和Array)。如下为迭代遍历的示例:

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为布尔值,表示遍历是否结束。

普通对象进行迭代需要定义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}

对象的迭代器通过Object.keys()方法获取属性名数组,将其赋值给values常量,同时定义一个默认值为0的计数器。当迭代器开始执行时,会返回一个包含next()方法的对象。该方法会返回包含value和done的对象,value为下一迭代值,done为布尔值,表示迭代器是否到达终点。

上述实现方式还是过于复杂,可以通过generator函数简化:

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}

在该generator函数中,利用for in循环枚举生成属性值。结果与前面的示例完全相同,但是要短得多。

迭代器的缺点是不适合表示异步数据源。ES2018的解决方案是异步迭代器和异步迭代。异步迭代器与传统迭代器的不同之处在于,它没有返回{value, done}形式的普通对象,而是返回一个Promise,其resolve返回{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} 
}));

注意,promise+迭代器并不能代替异步迭代器。虽然一个普通的同步迭代器可以异步地确定值,但是它仍然需要同步地确定“完成”的状态。

当然,您同样可以使用generator函数简化该过程,如下所示:

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对象

在可迭代对象上迭代的一个简单方法是使用for of,但由于异步迭代对象的value和done并不是同步指定的,因此for of并不适用。基于此,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()方法可能返回一个包含rejects的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.

异步迭代器支持

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.10.0 (需要--harmony\ async\ iteration标志)

  • 10.0.0 (全部支持)

Promise.prototype.finally

ES2018的另一个令人兴奋的新特性是finally()方法。几个JavaScript库以前实现过类似的方法,这在许多情况下都很有用。这鼓励Ecma技术委员会正式将finally()添加到规范中。无论promise的结果如何,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()方法在请求数据之后隐藏loading,无论请求是否成功。

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

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.finall支持

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 (全部支持)

新的正则表达式特性

ES2018为正则表达式添加了四个新特性,进一步提高了JavaScript的字符串处理能力。这些特点如下:

  • s (dotAll) 标志

  • 命名捕获组

  • Lookbehind 后行断言

  • 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引入了使用(?…)语法的命名捕获组。因此,匹配日期的模式可以用一种不那么模棱两可的方式来写:

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语法重复调用名称捕获组。例如,要在一个句子中找到连续重复的单词,可以使用/\b(?\w+)\s+\k\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()方法的替换字符串中,需要使用$构造。例如:

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后行断言

ES2018将lookbehind后行断言引入JavaScript,以前JavaScript只支持前行断言。后行断言由(?<=…)表示,代表字符串中的一个位置,紧接该位置之前的字符序列能够匹配pattern。例如,如果您想匹配以美元、英镑或欧元表示的产品的价格,而不需要捕获货币符号,您可以使用/(?<=$|£|€)\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]

还有一种负向后行断言,表示为(?<!…),代表字符串中的一个位置,紧接该位置之前的字符序列不能匹配pattern。例如,如果模式/(?<!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。你可以使用\p{Number}来匹配所有的Unicode数字,例如,假设你想匹配的Unicode字符㉛字符串:

const str = '㉛';

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

同样的,你可以使用\p{Alphabetic}来匹配所有的Unicode单词字符:

const str = 'ض';

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

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

同样有一个负向的Unicode属性转义模板 \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属性列表。

新正则表达式支持

Chrome Firefox Safari Edge
s (dotAll) Flag 62 No 11.1 No
Named Capture Groups 64 No 11.1 No
Lookbehind Assertions 62 No No No
Unicode Property Escapes 64 No 11.1 No
Chrome (Android) Firefox (Android) iOS Safari Edge Mobile Samsung Internet Android Webview
s (dotAll) Flag 62 No 11.3 No 8.2 62
Named Capture Groups 64 No 11.3 No No 64
Lookbehind Assertions 62 No No No 8.2 62
Unicode Property Escapes 64 No 11.3 No No 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、unicode转义的\u和八进制转义的\u。因此,像“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 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()以及正则表达式新特性的添加。尽管一些浏览器厂商还没有完全实现其中的一些特性,但是仍然可以用诸如Babel之类转义器进行使用。

ECMAScript正在快速发展,经常会有新特性被引入,有兴趣可以查询已完成提案列表,了解全部最新内容。有没有什么新功能让你特别兴奋?在评论中分享吧!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 原文链接:https://www.sitepoint.com/es2018-whats-new/在本文中,我将介绍...
    虚蕪面孔阅读 638评论 0 2
  • 在本文中,我将介绍通过ES2018(ES9)引入的JavaScript的新功能,以及它们的用途以及如何使用它们的示...
    猿学阅读 1,445评论 0 0
  • ECMAScript 2018(或者叫ES9)现在已经可用了。以下功能已经到达 stage 4,但是在撰写本文时在...
    努力与幸运阅读 199评论 0 0
  • Presto内存配置是开发者很关心的核心配置: 很难给出确切的数字,因为这些数字应该根据您的工作量来设置。所以,我...
    叫我小名阅读 6,535评论 0 0
  • 在寒风暴雨天气里没有什么比吃一碗妈妈炖的筒骨藕汤更暖和了。 趁着清晨赶集的热闹,妈妈在肉铺里挑了...
    茜多阅读 253评论 0 1