《ES6入门》学习笔记

ES6和ECMAScript2015的关系

标准委员会最终决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。接下来的时间,就在这个版本的基础上做改动,直到下一年的 6 月份,草案就自然变成了新一年的版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。

ES6 的第一个版本,就这样在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)。2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布,这个版本可以看作是 ES6.1 版,因为两者的差异非常小(只新增了数组实例的includes方法和指数运算符),基本上是同一个标准。根据计划,2017 年 6 月发布 ES2017 标准。


Babel转码器

Babel是一个广泛使用的ES6转码器,可以将ES6代码转为ES5代码,从而在现有的环境执行

// 转码前
input.map(item => item + 1);

// 转码后
input.map(function (item) {
  return item + 1;
});

Babel的配置文件是.babelrc,存放在根目录下,该文件来设置转码规则和插件

{
  "presets": [],
  "plugins": []
}

命令行转码babel-cli

安装命令如下

$ npm install --global babel-cli

 改写package.json

{
  // ...
  "devDependencies": {
    "babel-cli": "^6.0.0"
  },
  "scripts": {
    "build": "babel src -d lib"
  },
}

babel-node命令,提供一个支持ES6的REPL环境,支持Node的REPL环境的所有功能,而且可以直接执行ES6 。

babel-register模块改写require命令,加上一个钩子。每当使用require加载.js .jsx .es .es6后缀名的文件就会先用Babel转码。

babel-core模块,对需要调用Babel的API进行转码。

babel-polyfill为当前环境提供一个垫片,比如IteratorGeneratorSetMapProxyReflectSymbolPromise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)进行转码。


let和const命令

ES6新增了let命令用来声明变量,用法类似与var,但所生命的变量在let命令所在的代码块内有效。

for循环的计数器模式和使用let命令。

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10


var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

不存在变量提升

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

暂时性死区

在代码块内,使用lei命令声明变量之前,该变量都是不可用的

if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

 

let不允许在相同作用域内,重复声明同一个变量 。

function func(arg) {
  let arg; // 报错
}

function func(arg) {
  {
    let arg; // 不报错
  }
}

块级作用域

ES5只有全局作用域和函数作用域,没有块级作用域会导致内层变量可能覆盖外层变量,用来计数的循环变量泄露为全局变量。

块级作用域的出现使得广泛应用的立即执行函数表达式不必要了IIFE。

// IIFE 写法
(function () {
  var tmp = ...;
  ...
}());

// 块级作用域写法
{
  let tmp = ...;
  ...
}

考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

// 函数声明语句
{
  let a = 'secret';
  function f() {
    return a;
  }
}

// 函数表达式
{
  let a = 'secret';
  let f = function () {
    return a;
  };
}

const命令

const声明一个只读的常量,一旦声明,常量的值就不能改变

const的作用域与let命令相同:只在声明所在的块级作用域内有效。

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动

const a = [];
a.push('Hello'); // 可执行
a.length = 0;    // 可执行
a = ['Dave'];    // 报错

可以使用Object。freeze方法将对象冻结

const foo = Object.freeze({});

// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;

 变量的解构赋值用途

1.交换变量的值

let x = 1;
let y = 2;

[x, y] = [y, x];

2.从函数返回多个值

// 返回一个数组

function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一个对象

function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();

3.函数参数的定义

解构赋值可以方便地将一组参数与变量名对应起来

// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);

// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});

4.提取JSON数据

let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
};

let { id, status, data: number } = jsonData;

console.log(id, status, number);
// 42, "OK", [867, 5309]

5.函数参数默认值

jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {},
  cache = true,
  complete = function () {},
  crossDomain = false,
  global = true,
  // ... more config
} = {}) {
  // ... do stuff
};

 6.遍历Map结构

const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
  console.log(key + " is " + value);
}
// first is hello
// second is world

// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}

 7.输入模块的指定方法

const { SourceMapConsumer, SourceNode } = require("source-map");

 字符串的扩展

JS允许采用\uxxxx形式表示一个字符。这种表示法只限于码点在\u0000~\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。

"\uD842\uDFB7"
// "?"

"\u20BB7"
// " 7"

ES6提供了codePointAt方法,能够正确处理4个字节储存的字符,返回一个字符。

let s = '?a';

s.codePointAt(0) // 134071
s.codePointAt(1) // 57271

s.codePointAt(2) // 97

codePointAt方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。

function is32Bit(c) {
  return c.codePointAt(0) > 0xFFFF;
}

is32Bit("?") // true
is32Bit("a") // false

ES6 提供了String.fromCodePoint方法,可以识别大于0xFFFF的字符,弥补了String.fromCharCode方法的不足。在作用上,正好与codePointAt方法相反。

String.fromCodePoint(0x20BB7)
// "?"
String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'
// true

字符串的遍历器接口

这个遍历器最大的优点是可以识别大于0xFFFF的码点,传统的for循环无法识别这样的码点。

let text = String.fromCodePoint(0x20BB7);

for (let i = 0; i < text.length; i++) {
  console.log(text[i]);
}
// " "
// " "

for (let i of text) {
  console.log(i);
}
// "?"

JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
let s = 'Hello world!';

s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false

 repeat()方法返回一个新的字符串,表示将原字符串重复n次。

'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""

ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。

'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'

'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

// 普通字符串
`In JavaScript '\n' is a line-feed.`

// 多行字符串
`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

 上面代码中,所有模板字符串的空格和换行,都是被保留的,比如<ul>标签前面会有一个换行。如果你不想要这个换行,可以使用trim方法消除它。

$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`.trim());

实例:模板编译

let template = `
<ul>
  <% for(let i=0; i < data.supplies.length; i++) { %>
    <li><%= data.supplies[i] %></li>
  <% } %>
</ul>
`;

然后,将template封装在一个函数里面返回,就可以了。

let script =
`(function parse(data){
  let output = "";

  function echo(html){
    output += html;
  }

  ${ template }

  return output;
})`;

return script;

将上面的内容拼装成一个模板编译函数compile

function compile(template){
  const evalExpr = /<%=(.+?)%>/g;
  const expr = /<%([\s\S]+?)%>/g;

  template = template
    .replace(evalExpr, '`); \n  echo( $1 ); \n  echo(`')
    .replace(expr, '`); \n $1 \n  echo(`');

  template = 'echo(`' + template + '`);';

  let script =
  `(function parse(data){
    let output = "";

    function echo(html){
      output += html;
    }

    ${ template }

    return output;
  })`;

  return script;
}

String.raw()

ES6为还原原生的String对象,提供了一个raw方法。

String.raw方法,往往用来充当模板字符串的处理函数,返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,对应于替换变量后的模板字符串。

String.raw`Hi\n${2+3}!`;
// 返回 "Hi\\n5!"

String.raw`Hi\u000A!`;
// 返回 "Hi\\u000A!"

正则的扩展

RegExp构造函数

在ES5中RegExg构造函数的参数有两种情况。

第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。

var regex = new RegExp('xyz', 'i');
// 等价于
var regex = /xyz/i;

第二种情况是,参数是一个正则表示式,这时会返回一个原有正则表达式的拷贝。

var regex = new RegExp(/xyz/i);
// 等价于
var regex = /xyz/i;

但是,ES5 不允许此时使用第二个参数添加修饰符,否则会报错。

ES6 改变了这种行为。如果RegExp构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。

new RegExp(/abc/ig, 'i').flags
// "i"

字符串的正则方法

字符串对象有4个方法,可以使用郑子表达式:match()replace()search()split()

ES6 将这 4 个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。

  • String.prototype.match 调用 RegExp.prototype[Symbol.match]
  • String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
  • String.prototype.search 调用 RegExp.prototype[Symbol.search]
  • String.prototype.split 调用 RegExp.prototype[Symbol.split]

y修饰符

ES6 还为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。

y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。

var s = 'aaa_aa_a';
var r1 = /a+/g;
var r2 = /a+/y;

r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]

r1.exec(s) // ["aa"]
r2.exec(s) // null

下面是字符串对象的replace方法的例子。

const REGEX = /a/gy;
'aaxa'.replace(REGEX, '-') // '--xa'

上面代码中,最后一个a因为不是出现在下一次匹配的头部,所以不会被替换。

单单一个y修饰符对match方法,只能返回第一个匹配,必须与g修饰符联用,才能返回所有匹配。

'a1a2a3'.match(/a\d/y) // ["a1"]
'a1a2a3'.match(/a\d/gy) // ["a1", "a2", "a3"]

旦出现非法字符,两者的行为就不一样了。

tokenize(TOKEN_Y, '3x + 4')
// [ '3' ]
tokenize(TOKEN_G, '3x + 4')
// [ '3', '+', '4' ]

上面代码中,g修饰符会忽略非法字符,而y修饰符不会,这样就很容易发现错误

ES6 为正则表达式新增了flags属性,会返回正则表达式的修饰符。

// ES5 的 source 属性
// 返回正则表达式的正文
/abc/ig.source
// "abc"

// ES6 的 flags 属性
// 返回正则表达式的修饰符
/abc/ig.flags
// 'gi'

s修饰符:dotAll模式

ES2018 引入s修饰符,使得.可以匹配任意单个字符。

/foo.bar/s.test('foo\nbar') // true

后行断言

先行断言”指的是,x只有在y前面才匹配,必须写成/x(?=y)/。比如,只匹配百分号之前的数字,要写成/\d+(?=%)/。”先行否定断言“指的是,x只有不在y前面才匹配,必须写成/x(?!y)/。比如,只匹配不在百分号之前的数字,要写成/\d+(?!%)/

/\d+(?=%)/.exec('100% of US presidents have been male')  // ["100"]
/\d+(?!%)/.exec('that’s all 44 of them')                 // ["44"]

“后行断言”正好与“先行断言”相反,x只有在y后面才匹配,必须写成/(?<=y)x/。比如,只匹配美元符号之后的数字,要写成/(?<=\$)\d+/。“后行否定断言”则与“先行否定断言”相反,x只有不在y后面才匹配,必须写成/(?<!y)x/。比如,只匹配不在美元符号后面的数字,要写成/(?<!\$)\d+/

/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill')  // ["100"]
/(?<!\$)\d+/.exec('it’s is worth about €90')                // ["90"]

Unicide 属性类

ES2018引入了\p{...} \P{...},允许正则表达式匹配复合Unicode某种属性的所有字符

const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test('π') // true

\P{…}\p{…}的反向匹配,即匹配不满足条件的字符。

具名组匹配

正则表达式使用圆括号进行组匹配

const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;

使用exec方法,就可以将这三组匹配结果提取出来

const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;

const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj[1]; // 1999
const month = matchObj[2]; // 12
const day = matchObj[3]; // 31

组匹配的问题是,组的顺序变了,引用就要修改序号

ES2018引入了具名组匹配,允许为每一个组指定一个名字

const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;

const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj.groups.year; // 1999
const month = matchObj.groups.month; // 12
const day = matchObj.groups.day; // 31

字符串替换时,使用$<组名>引用具名组。

let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;

'2015-01-02'.replace(re, '$<day>/$<month>/$<year>')
// '02/01/2015'

数值的扩展

ES6在Number对象上,新提供Number.isFinite()和Numbet.isNaN()

Number.isFinite(15); // true
Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite(-Infinity); // false
Number.isFinite('foo'); // false
Number.isFinite('15'); // false
Number.isFinite(true); // false

注意,如果参数类型不是数值,Number.isFinite一律返回false

Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9/NaN) // true
Number.isNaN('true' / 0) // true
Number.isNaN('true' / 'true') // true

如果参数类型不是NaNNumber.isNaN一律返回false

Number.parseInt(),Number.parseFloat()

ES6 将全局方法parseInt()parseFloat(),移植到Number对象上面,行为完全保持不变。

// ES5的写法
parseInt('12.34') // 12
parseFloat('123.45#') // 123.45

// ES6的写法
Number.parseInt('12.34') // 12
Number.parseFloat('123.45#') // 123.45

这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。

 Number.isInteger()

 Number.isInteger()用来判断一个数值是否为整数。

Number.isInteger(25) // true
Number.isInteger(25.1) // false

 如果对数据精度的要求较高,不建议使用Number.isInteger()判断一个数值是否为整数。

Number.ERSILON

Number.EPSILON的实质是一个可以接受的最小误差范围。

function withinErrorMargin (left, right) {
  return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}

0.1 + 0.2 === 0.3 // false
withinErrorMargin(0.1 + 0.2, 0.3) // true

1.1 + 1.3 === 2.4 // false
withinErrorMargin(1.1 + 1.3, 2.4) // true

安全整数和Number.isSafeInteger()

JS能够准确表示的整数范围在-2^53到2^53之间

ES6 引入了Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。

Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。

Math对象的扩展

ES6在Math对象上新增了17个与数学相关的方法

Math.trunc():用于去除一个数的小数部分,返回整数部分。对于非数值,Math.trunc内部使用Number方法将其先转为数值。

Math.sign():用于判断一个数到底是正数、负数、还是零。

Math.cbrt():用于计算一个数的立方根。

Math.clz32():将参数转为32位无符号整数的形式。

Math.imul():返回两个数以32位带符号整数形式相乘的结果。

Math.fround():返回一个数的32位单精度浮点数。

Math.hypot():返回所有参数的平方和的平方根。

Math.expm1():Math.expm1(x)返回 ex - 1

Math.log1p():返回1 + x的自然对数,即Math.log(1 + x)。如果x小于-1,返回NaN

Math.log10():返回以 10 为底的x的对数。如果x小于 0,则返回 NaN。

Math.log2():返回以 2 为底的x的对数。如果x小于 0,则返回 NaN。

ES2016 新增了一个指数运算符(**)。


函数的扩展

ES6允许为函数的参数设置默认值,直接卸载参数定义的后面。

function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。

rest参数

ES6引入rest参数(...变量名),用于获取函数的多于参数,这样就不用使用argumens对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

function add(...values) {
    let sum = 0;
    
    for (var val of value) {
        sum += val;
    }
    
    return sum;
}


add (1, 2, 3, 4)   //  10

rest参数之后不能再有其他参数。

严格模式

ES5开始,函数内部可以设定为严格模式。

function doSomething(a, b) {
  'use strict';
  // code
}

ES2016做出修改,函数参数使用了默认值,解构赋值,或者扩展运算符,函数内部就不能显式设定为严格模式。

原因是函数内部的严格模式,同时适用于函数体与函数参数。

箭头函数

ES6允许使用“箭头”(=>)定义函数

var f = v => v;

// 等同于
var f = function (v) {
    return v;
};

箭头函数的一个用处就是简化回调函数。

// 正常函数写法
[1,2,3].map(function x {}
    return x*x;
));

// 箭头函数写法
[1,2,3].map(x => x*x);

使用注意点:

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
  • 不可以当作构造函数,也就是说,不可以使用new命令
  • 不可以使用arguments对象,该对象在函数体内不存在。可以用rest参数代替
  • 并不刻意使用yield命令

嵌套的箭头函数

function insert(value) {
  return {into: function (array) {
    return {after: function (afterValue) {
      array.splice(array.indexOf(afterValue) + 1, 0, value);
      return array;
    }};
  }};
}

insert(2).into([1, 3]).after(1); //[1, 2, 3]



let insert = (value) => ({into: (array) => ({after: (afterValue) => {
  array.splice(array.indexOf(afterValue) + 1, 0, value);
  return array;
}})});

insert(2).into([1, 3]).after(1); //[1, 2, 3]

双冒号运算符

箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(call,bind,apply)。所以提出了“函数绑定”运算符,用来取代call、apply、bind调用。

foo::bar;
// 等同于
bar.bind(foo);

foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);

const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
  return obj::hasOwnProperty(key);
}

尾调用优化

尾调用时函数式编程的一个重要概念。指某个函数的最后一步是调用另一个函数。

function f(x){
    return g(x);
} 

尾递归

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

递归非常耗费内存,对于尾递归只存在一个调用帧,不会发生栈溢出的错误

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120



function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

函数式编程有一个概念:柯里化,是将多参数的函数转换成单参数的形式

function currying(fn, n) {
  return function (m) {
    return fn.call(this, m, n);
  };
}

function tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}

const factorial = currying(tailFactorial, 1);

factorial(5) // 120

上面代码通过柯里化,将尾递归函数tailFactorial变为只接受一个参数的factorial

采用ES6的函数默认值

function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5) // 120

尾递归优化

function sum(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1);
  } else {
    return x;
  }
}

sum(1, 100000)
// Uncaught RangeError: Maximum call stack size exceeded(…)

sum是一个递归函数,超出了调用栈的最大次数。

蹦床函数可以将递归执行转为循环执行,他接受一个函数f作为参数,只要f执行后返回一个函数,就继续执行

function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}
function sum(x, y) {
  if (y > 0) {
    return sum.bind(null, x + 1, y - 1);
  } else {
    return x;
  }
}

数组的扩展

扩展运算符

扩展运算符是三个点  ... 好比rest参数的逆运算,将一个数组转为逗号分隔的参数序列。

console.log(...[1, 2, 3])
// 1 2 3

console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5

[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]

替代函数的apply方法

// ES5 的写法
function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];
f.apply(null, args);

// ES6的写法
function f(x, y, z) {
  // ...
}
let args = [0, 1, 2];
f(...args);

扩展运算符的应用:

(1)复制数组:数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新数组

                           ES5只能通过变通方法来复制数组

const a1 = [1, 2];
const a2 = a1.concat();

a2[0] = 2;
a1 // [1, 2]

                            扩展运算符提供了复制数组的简便写法。

const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;

(2)合并数组

const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];

// ES5 的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6 的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]

(3)与解构赋值结合

const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest  // [2, 3, 4, 5]

const [first, ...rest] = [];
first // undefined
rest  // []

const [first, ...rest] = ["foo"];
first  // "foo"
rest   // []

(4)字符串

扩展运算符还可以将字符串转为真正的数组

[...'hello']
// [ "h", "e", "l", "l", "o" ]

(5)实现了Iterator接口的对象

(6)Map和Set结构,Generator函数

Array.from()

Array.from方法用于将两类对象转为真正的数组:类似数组的对象和可遍历的对象

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};

// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);

Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]

Array.from()的另一个应用是,将字符串转为数组,然后返回字符串的长度。因为它能正确处理各种 Unicode 字符,可以避免 JavaScript 将大于\uFFFF的 Unicode 字符,算作两个字符的 bug。

Array.of()

用于将一组值转换为数组

Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1

数组实例的copyWithin()

在当前数组内部,将指定位置的成员复制到其他位置覆盖原有成员,然后返回当前数组。

Array.prototype.copyWithin(target, start = 0, end = this.length)
  • target(必需):从该位置开始替换数据。如果为负值,表示倒数。
  • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。
// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)
// [4, 2, 3, 4, 5]

// -2相当于3号位,-1相当于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)
// [4, 2, 3, 4, 5]

// 将3号位复制到0号位
[].copyWithin.call({length: 5, 3: 1}, 0, 3)
// {0: 1, 3: 1, length: 5}

// 将2号位到数组结束,复制到0号位
let i32a = new Int32Array([1, 2, 3, 4, 5]);
i32a.copyWithin(0, 2);
// Int32Array [3, 4, 5, 4, 5]

// 对于没有部署 TypedArray 的 copyWithin 方法的平台
// 需要采用下面的写法
[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
// Int32Array [4, 2, 3, 4, 5]

数组实例的find()和findIndex()

数组实例的find方法,用于找出第一个符合条件的数组成员。

[1, 4, -5, 10].find((n) => n < 0)
// -5

数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1

[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2

数组实例的fill()

fill方法使用给定值,填充一个数组。

['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]

fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。

数组实例的entries(),keys()和values()

ES6提供这三个新的方法用于遍历数组。他们都返回一个遍历器对象,可以用for...of循环进行遍历。

区别就是他们分别时对键值对,键名和键值的遍历。

数组实例的includes()

返回一个布尔值,表示某个数组是否包含给定的值

[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true

数组实例的flat(),flatMap()

flat()用于将嵌套的数组“拉平”,变成一个一维的数组,不影响原数组

[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]

如果是多层的嵌套数组,可以将flat()方法的参数写成一个整数 默认为1  eg:flat(2)

不管有多少层嵌套都要转成一维数组,可以flat(infinity)

flatMap()方法对原数组的每个成员执行一个函数,然后对返回值组成的数组执行flat()方法。

// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]

对象的扩展

Object.is()

ES5比较两个值是否相等,相等运算符(==)和严格相等运算符(===)。前者会自动转换数据类型,后者NAN不等于自身,+0会等于-0。

Object.is('foo', 'foo')
// true
Object.is({}, {})
// false

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

Object.assign()

用于对象的合并,将原对象的所有可枚举属性,复制到目标对象。

const target = { a: 1 };

const source1 = { b: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

如果目标对象与原对象有同名属性,后面的属性会覆盖前面的属性。此方法实行的是浅拷贝。

Object.getOwnPropertyDescripttors()

返回某个对象属性的描述对象

const obj = {
  foo: 123,
  get bar() { return 'abc' }
};

Object.getOwnPropertyDescriptors(obj)
// { foo:
//    { value: 123,
//      writable: true,
//      enumerable: true,
//      configurable: true },
//   bar:
//    { get: [Function: get bar],
//      set: undefined,
//      enumerable: true,
//      configurable: true } }

Object.setPrototypeOf()

此方法作用与__proto__相同,用来设置一个对象的prototype对象,返回参数对象本身。

// 格式
Object.setPrototypeOf(object, prototype)

// 用法
const o = Object.setPrototypeOf({}, null);

let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);

proto.y = 20;
proto.z = 40;

obj.x // 10
obj.y // 20
obj.z // 40

Object.getPrototypeOf()

此方法与Object.setPrototypeOf()配套,用于读取一个对象的原型对象。


Symbol

ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。

Symbol值通过Symbol函数生成。

let s = Symbol();

typeof s
// "symbol"

Symbol值不能与其他类型的值进行运算。但是,Symbol值可以显示转为字符串,也可以转为布尔值。

作为属性名的Symbol

由于每一个Symbol值都不相等,意味着Symbol值可以作为标识符,用于对象的属性名,就能保证不会出项同名的属性。

在对象内部,使用Symbol值定义属性时,Symbol值必须放在方括号之中。

实例:消除魔术字符串

魔术字符串指的是,在代码中多次出现,与代码形成强耦合的某一个具体字符串或者数值。

function getArea(shape, options) {
  let area = 0;

  switch (shape) {
    case 'Triangle': // 魔术字符串
      area = .5 * options.width * options.height;
      break;
    /* ... more code ... */
  }

  return area;
}

getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串

上面代码中,字符串'Triangle'就是一个魔术字符串。他多次出现,不利于将来的修改和维护。

const shapeType = {
  triang: Symbol();
};

function getArea(shape, options) {
  let area = 0;
  switch (shape) {
    case shapeType.triangle:
      area = .5 * options.width * options.height;
      break;
  }
  return area;
}

getArea(shapeType.triangle, { width: 100, height: 100 });

属性名的遍历

Object.getOwnPropertySymbols方法返回一个数组,成员是当前对象的用作属性名的Symbol值

另一个新的API,Reflect.ownKeys方法可以返回所有类型的键名。

const shapeType = {
  triangle: 'Triangle'
};

function getArea(shape, options) {
  let area = 0;
  switch (shape) {
    case shapeType.triangle:
      area = .5 * options.width * options.height;
      break;
  }
  return area;
}

getArea(shapeType.triangle, { width: 100, height: 100 });

Symbol.for(), Symbol.keyFor()

Symbol.for()可以重新使用同一个Symbol值。

let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true

Symbol.for()Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。

Symbol.keyFor方法返回一个已登记的 Symbol 类型值的key

let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

内置的Symbol值

1.Symbol.hasInstance

                  当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。

class MyClass {
  [Symbol.hasInstance](foo) {
    return foo instanceof Array;
  }
}

[1, 2, 3] instanceof new MyClass() // true

2.Symbol.isConcatSpreadable

              对象的Symbol.isConcatSpreadable属性等于一个布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开。

let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr1[Symbol.isConcatSpreadable] // undefined

let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']

3.Symbol.species

      对象的Symbol.species属性,指向一个构造函数。创建衍生对象时,会使用该属性。

4.Symbol.match

      对象的这个属性,指向一个函数。当执行str.match(myObject)时,如果该属性存在,会调用它,返回该方法的返回值

5.Symbol.replace


Set和Map数据结构

1.Set

ES6提供了新的数据Set。类似于数组,但成员的值都是唯一的。

const s = new Set();

[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

for (let i of s) {
  console.log(i);
}
// 2 3 5 4

Set实例的方法分为两大类:操作方法和遍历方法

  • add(value):添加某个值,返回Set结构本身
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功
  • has(value):返回一个布尔值,表示该值是否为Set的成员
  • clear():清除所有成员,没有返回值
s.add(1).add(2).add(2);
// 注意2被加入了两次

s.size // 2

s.has(1) // true
s.has(2) // true
s.has(3) // false

s.delete(2);
s.has(2) // false

Array.from方法可以将Set结构转为数组,这可以去除数组重复成员的方法

function dedupe(array) {
  return Array.from(new Set(array));
}

dedupe([1, 1, 2, 3]) // [1, 2, 3]

使用Set可以很容易实现并集、交集和差集

let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}

// 差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}

2.Map

Map数据结构类似与对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值都可以当作键。

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false

(1)size属性:size属性返回Map结构的成员总数。

const map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2

(2)set(key, value):set方法设置键名key对应的键值value,然后返回整个Map结构。

const m = new Map();

m.set('edition', 6)        // 键是字符串
m.set(262, 'standard')     // 键是数值
m.set(undefined, 'nah')    // 键是 undefined

(3)get(key):get方法读取key对应的键值

const m = new Map();

const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // 键是函数

m.get(hello)  // Hello ES6!

(4)has(key)

(5)delete(key)

(6)clear()

Map与其他数据结构的互相转换

1.转为数组,使用扩展运算符(...)

const myMap = new Map()
  .set(true, 7)
  .set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]

2.与对象互转

// 转为对象
function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}

const myMap = new Map()
  .set('yes', true)
  .set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }

// 对象转Map
function objToStrMap(obj) {
  let strMap = new Map();
  for (let k of Object.keys(obj)) {
    strMap.set(k, obj[k]);
  }
  return strMap;
}

objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}

Proxy

Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,属于一种“元编程”。可以理解成在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。

var proxy = new Proxy(target, handler);
方法描述
handler.apply()拦截Proxy实例作为函数调用的操作
handler.construct()拦截Proxy实例作为函数调用的操作
handler.defineProperty()拦截 Object.defineProperty() 的操作
handler.deleteProperty()拦截 Proxy 实例删除属性操作
handler.get()拦截 读取属性的操作
handler.set()拦截 属性赋值的操作
handler.getOwnPropertyDescriptor()拦截 Object.getOwnPropertyDescriptor() 的操作
handler.getPrototypeOf()拦截 获取原型对象的操作
handler.has()拦截 属性检索操作
handler.isExtensible()拦截 Object.isExtensible()操作
handler.ownKeys()拦截 Object.getOwnPropertyDescriptor() 的操作
handler.preventExtension()拦截 Object().preventExtension() 操作
handler.setPrototypeOf()拦截Object.setPrototypeOf()操作
Proxy.revocable()创建一个可取消的 Proxy 实例

Reflect

Reflect对象的设计目的:

1.将Object对象的一些明显属于语言内部的方法,放到Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法

2.修改某些Object方法的返回结果,让其变得更合理。

3.让Object操作都变成函数行为。

4.Reflect对象的方法与Proxy对此昂的方法一一对应。

实例使用Proxy实现观察者模式

观察者模式指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行。

const person = observable({
  name: '张三',
  age: 20
});

function print() {
  console.log(`${person.name}, ${person.age}`)
}

observe(print);
person.name = '李四';
// 输出
// 李四, 20

下面,使用 Proxy 写一个观察者模式的最简单实现,即实现observableobserve这两个函数。思路是observable函数返回一个原始对象的 Proxy 代理,拦截赋值操作,触发充当观察者的各个函数。

const queuedObservers = new Set();

const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});

function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver);
  queuedObservers.forEach(observer => observer());
  return result;
}

Promise 对象

Promise是异步编程的一种解决方案。简单来说就是一个容器,里面保存着某个和未来才会结束的事件的结果。

Promise对象有两个特点:1.对象的状态不受外界影响。  2.一旦状态改变,就不会在变,任何时候都可以得到这个结果。

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

异步加载图片的例子

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    const image = new Image();

    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(new Error('Could not load image at ' + url));
    };

    image.src = url;
  });
}

Iterator 和 for...of 循环

遍历器是一种接口,来处理所有不同的数据结构。任何数据结构只要部署Iterator接口,就可以完成遍历操作。

Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排    列;三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。

Iterator 的遍历过程是这样的。

(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

原生具备Iterator接口的数据结构如下:Array、Map、Set、String、TypedArray、函数的arguments对象、NodeList对象

调用 Iterator 接口的场合

(1) 解构赋值 :对数组和Set结构进行解构赋值,会默认调用Symbol.iterator方法。

let set = new Set().add('a').add('b').add('c');

let [x,y] = set;
// x='a'; y='b'

let [first, ...rest] = set;
// first='a'; rest=['b','c'];

(2) 扩展运算符

// 例一
var str = 'hello';
[...str] //  ['h','e','l','l','o']

// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']

(3) yield*

yield*后面跟的是一个可遍历的结构,他会调用该结构的遍历器结构。

let generator = function* () {
  yield 1;
  yield* [2,3,4];
  yield 5;
};

var iterator = generator();

iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }

JS原有的for...in循环,只能获得对象的键名,不能直接过去键值

var arr = ['a', 'b', 'c', 'd'];

for (let a in arr) {
  console.log(a); // 0 1 2 3
}

for (let a of arr) {
  console.log(a); // a b c d
}

Generator 函数的语法

Generator 函数是ES6提供的一种异步编程解决方案。Generator 函数是一个状态机,封装了多个内部状态。

它还是一个遍历器对象生成函数。返回的遍历器对象可以依次遍Generator 函数内部的每一个状态。

两个特征:1.function关键字与函数名之间有一个星号

                  2.函数体内部使用yield表达式,定义不同的内部状态

yield 表达式

由于 Generator 函数返回的是遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一个可以暂停执行的函数。yield表达式就是暂停标志。

利用for...of循环。可以写出遍历任意对象的方法

function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

for (let [key, value] of objectEntries(jane)) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

next()throw()return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。

next()是将yield表达式替换成一个值。

throw()是将yield表达式替换成一个throw语句。

return()是将yield表达式替换成一个return语句。

应用

(1)异步操作的同步化表达

Generator 函数的暂停执行的效果,意味着可以把异步操作写在yield表达式里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield表达式下面,反正要等到调用next方法时再执行。所以,Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。

function* loadUI() {
  showLoadingScreen();
  yield loadUIDataAsynchronously();
  hideLoadingScreen();
}
var loader = loadUI();
// 加载UI
loader.next()

// 卸载UI
loader.next()

(2)控制流管理

如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。

step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // Do something with value4
      });
    });
  });
});

Generator 函数可以进一步改善代码运行流程。

function* longRunningTask(value1) {
  try {
    var value2 = yield step1(value1);
    var value3 = yield step2(value2);
    var value4 = yield step3(value3);
    var value5 = yield step4(value4);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}

(3)部署Iterator接口

利用 Generator 函数,可以在任意对象上部署 Iterator 接口。

function* iterEntries(obj) {
  let keys = Object.keys(obj);
  for (let i=0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
  }
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value);
}

// foo 3
// bar 7

(4)作为数据结构

Generator 可以看作是数据结构,更确切地说,可以看作是一个数组结构,因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口。

function* doStuff() {
  yield fs.readFile.bind(null, 'hello.txt');
  yield fs.readFile.bind(null, 'world.txt');
  yield fs.readFile.bind(null, 'and-such.txt');
}

Generator 函数的异步应用

Generator 函数是协程再ES6的实现,最大特点就是可以交出函数的执行权。

整个Generator 函数就是一个封装的异步任务。异步操作需要暂停的地方,都用yield语句注明。

function* gen(x) {
  var y = yield x + 2;
  return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }

Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。

两个特性使它作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。

Thunk 函数

Thunk 函数是自动执行Generator 函数的一种方法

JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。

co 模块

co 模块用于Generator 函数的自动执行

var gen = function* () {
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

co 模块可以让你不用编写 Generator 函数的执行器。

var co = require('co');
co(gen);

async 函数

async 函数就是Generator 函数的语法糖

前文有一个 Generator 函数,依次读取两个文件。

const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

上面代码的函数gen可以写成async函数,就是下面这样。

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

async函数对Generator 函数的改进体现在以下四点

(1)内置执行器

Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

asyncReadFile();

上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。

(2)更好的语义

asyncawait,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

(3)更广的适用性

co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

(4)返回值是Promise

async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。

基本用法

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

async function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name);
  const stockPrice = await getStockPrice(symbol);
  return stockPrice;
}

getStockPriceByName('goog').then(function (result) {
  console.log(result);
});

async 函数有多种使用形式。

// 函数声明
async function foo() {}

// 函数表达式
const foo = async function () {};

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

// 箭头函数
const foo = async () => {};

async 函数的实现原理

将Generator 函数和自动之行器包装在一个函数里

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

Class的基本用法

ES6提供了更接近传统语言的写法,引入了Class 的概念,作为对象的模板,通过class关键字可以定义类。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

ES6 的类,完全可以看作构造函数的另一种写法。

class Point {
  // ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true

构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。

class Point {
  constructor() {
    // ...
  }

  toString() {
    // ...
  }

  toValue() {
    // ...
  }
}

// 等同于

Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

prototype对象的constructor属性,直接指向“类”的本身,这与 ES5 的行为是一致的。

Point.prototype.constructor === Point // true

constructor 方法

constructor 方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。

类的实例

class Point {
  // ...
}

// 报错
var point = Point(2, 3);

// 正确
var point = new Point(2, 3);

与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。

//定义类
class Point {

  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }

}

var point = new Point(2, 3);

point.toString() // (2, 3)

point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true

静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前加上static关键字。就表示该方法不会被实例继承,这就被称为“静态方法”。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

Class 的继承

Class可以通过extends关键字实现继承,比ES5的通过修改原型链实现继承要方便。

class Point {
}

class ColorPoint extends Point {
}

在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // 正确
  }
}

类的 prototype 属性和__proto__属性

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

Module 的语法

ES6模块的设计思想时尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

// ES6模块
import { stat, exists, readFile } from 'fs';

// CommonJS模块
let { stat, exists, readFile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

export 命令

模块功能主要由两个命令构成: export和import。export命令用于规定模块的对外接口。import命令用于输入其他模块提供的功能。

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。

// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
 
// 或者
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

import 命令

使用export命令定义了模块的对外接口,其他JS文件就可以通过import命令加载这个模块。

// main.js
import {firstName, lastName, year} from './profile.js';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}

模块的整体加载

// main.js

import { area, circumference } from './circle';

console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));

上面写法是逐一指定要加载的方法,整体加载的写法如下。

import * as circle from './circle';

console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

export default 命令

为了用户方便你,不阅读文档就能加载模块,可以用export default 命令,为模块指定默认输出

下面比较一下默认输出和正常输出。

// 第一组
export default function crc32() { // 输出
  // ...
}

import crc32 from 'crc32'; // 输入

// 第二组
export function crc32() { // 输出
  // ...
};

import {crc32} from 'crc32'; // 输入

import()使用场合

(1)按需加载

import()可以在需要的时候,再加载某个模块。

button.addEventListener('click', event => {
  import('./dialogBox.js')
  .then(dialogBox => {
    dialogBox.open();
  })
  .catch(error => {
    /* Error handling */
  })
});

(2)条件加载

import()可以放在if代码块,根据不同的情况,加载不同的模块。

if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}

(3)动态的模块路径

import()允许模块路径动态生成。

import(f())
.then(...);

import()加载模块成功后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。

Module的加载实现

下面就是两种异步加载的语法。

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

defer是“渲染完再执行”,async是“下载完就执行”

浏览器加载ES6模块,加入type="module"属性,都是异步加载不会堵塞浏览器

ES6模块与ConmmonJS模块的差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块,这是两者的一个重大差异。


编程风格

块级作用域

(1)let 取代 var

(2)全局常量和线程安全

字符串

静态字符串一律使用单引号或反引号,不适用双引号。动态字符串使用反引号

// bad
const a = "foobar";
const b = 'foo' + a + 'bar';

// acceptable
const c = `foobar`;

// good
const a = 'foobar';
const b = `foo${a}bar`;

解构赋值

使用数据成员对变量赋值时,优先使用解构赋值。

const arr = [1, 2, 3, 4];

// bad
const first = arr[0];
const second = arr[1];

// good
const [first, second] = arr;

对象

对象尽量静态化,一旦定义,不随意添加新的属性。可以使用Object.assign方法添加属性

// bad
const a = {};
a.x = 3;

// if reshape unavoidable
const a = {};
Object.assign(a, { x: 3 });

// good
const a = { x: null };
a.x = 3;

数组

使用扩展运算符拷贝数组

// bad
const len = items.length;
const itemsCopy = [];
let i;

for (i = 0; i < len; i++) {
  itemsCopy[i] = items[i];
}

// good
const itemsCopy = [...items];

函数

立即执行函数可以写成箭头函数的形式。

// bad
[1, 2, 3].map(function (x) {
  return x * x;
});

// good
[1, 2, 3].map((x) => {
  return x * x;
});

// best
[1, 2, 3].map(x => x * x);

Map 结构

只有模拟现实世界的实体对象才使用Object。如果只是需要key:value的数据结构,使用Map结构。

Class

总是用Class取代需要prototype。

// bad
function Queue(contents = []) {
  this._queue = [...contents];
}
Queue.prototype.pop = function() {
  const value = this._queue[0];
  this._queue.splice(0, 1);
  return value;
}

// good
class Queue {
  constructor(contents = []) {
    this._queue = [...contents];
  }
  pop() {
    const value = this._queue[0];
    this._queue.splice(0, 1);
    return value;
  }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值