ES2015学习笔记

ECMA6学习笔记

参考资料

ECMAScript6入门:http://es6.ruanyifeng.com/

官方文档:https://babeljs.io/learn-es2015/

开发软件:WebStorm 开源地址:https://coding.net/u/chenxygx/p/CodeSave/git/tree/master/ECMAScript2015

npm install

Settings - Keymap : Main menu - Code - Reformat Code (配置格式化文件)

babel软件需要WebStorm配置一下

需要全局安装 babel,

npm install babel-preset-env --save-dev
npm install --save-dev babel-cli
npm install -g babel-cli
npm install --save-dev babel-plugin-transform-es2015-modules-commonjs

然后需要添加.babelrc文件,用来控制生成es2015

{
"presets": ["env"]
}

然后package.json添加build,script用来控制编译目录

"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "babel Script --watch --out-dir lib"
},

ES6声明变量的6种方式

ES5只有两种声明变量方式:var命令 和 function 命令。

ES6还有四种声明变量方式:let命令 、Const命令、import命令、class命令

let命令

用来声明变量,类似于var,声明的变量只在代码快({}表示代码块)内有效。并且不会受到变量提升的影响。

如果区块中存在let和const命令,就会形成封闭作用域,在声明之前使用变量就会报错。这种行为称为:暂时性死区

var tmp = 123;
if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

let不允许重复声明,不能在函数内部重新声明参数。有块级作用域,就不再需要 立即执行函数表达式(IIFE)了

Const命令

const声明一个只读的常量,一旦声明,常量就不能改变。必须在声明的同时,进行初始化。

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

const变量也不会提升,同样存在 暂时性死区

const本质上是指变量指向的内存地址不得改动。但对象和数组是可以进行变动的。

const x = {};
x.prop = 1;
x = {};

上面代码,可以对x进行添加属性,但不能重新进行赋值改变地址。

如果想让对象或数组完全冻结,可以使用object.freeze方法。

const x = Object.freeze({ prop : 2 });
x.prop = 1;
console.log(x.prop); // 2

顶级对象

顶级对象在浏览器环境指 window对象,在node指的是global对象。

因为顶级对象在各种实现中不统一,一般使用this变量,但是会有一些局限性。

全局环境中,this会返回顶层对象。但是node模块和ES2015模块中,this返回的是当前模块。

函数里的this,如果不是作为对象运行,而是单纯的函数,this会指向顶层对象。

针对this指向,可以查看javascript知识点记录

综上所述,可以在两种情况下都获取顶层对象的方法有两种

// 方法一
!(function () {
    (typeof window !== 'undefined'
        ? window
        : (typeof process === 'object' &&
        typeof require === 'function' &&
        typeof global === 'object')
            ? global
            : this);
    this.a = 1;
})()
console.log(a);

// 方法二
var getGlobal = function () {
    if (typeof self !== 'undefined') { return self; }
    if (typeof window !== 'undefined') { return window; }
    if (typeof global !== 'undefined') { return global; }
    throw new Error('unable to locate global object');
};
getGlobal().b = 2;
console.log(b);

数组的解构赋值 

ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,被称为解构。

let [a,b,c] = [1,2,3]; console.log(a+b+c); // 6

上面代码表示,可以从数组中提取值,按照对应次序位置,对变量进行赋值。

如果解构不成功,变量的值就等于 undefined

let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

let [x, , y] = [1, 2, 3];
x // 1
y // 3

let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []

对象的解构赋值 

对象的解构变量必须与属性同名,才可以取到值。次序不一致是没有影响的,如取不到值返回undefined

let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"

let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined

如果希望变量名与属性名不一致,必须写成下面这样。此时foo 和 bar是匹配模式,f是变量。

let { foo: f, bar: b } = { foo: "aaa", bar: "bbb" };

采用解构的写法,变量不能重新声明,所以如果有赋值的变量重新声明就会报错。

解构也可以用于嵌套结构的对象。此时p是模式不会赋值

let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};

let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"

对于结构嵌套对象,也是同样的操作。

var node = {
    loc: {
        start: {
            line: 1,
            column: 5
        }
    }
};
let { loc:{start:{line:l,column:c}} } = node;
console.log(l + c);
let obj = {};
let arr = [];
({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });
obj // {prop:123}
arr // [true]

对象的解构也可以设定默认值,使用 = 可以在目标值的属性等于undefined的时候,进行赋初始化值

var {x = 3} = {};
x // 3

var {x, y = 5} = {x: 1};
x // 1
y // 5

var {x:y = 3} = {};
y // 3

var {x:y = 3} = {x: 5};
y // 5

var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"

如果要使用一个已经声明的变量,需要将内容嵌套在一个大括号里,告诉js不当做代码段处理

let x;
({x} = {x: 1});

字符串的解构赋值

字符串也可以进行解构,将字符串拆分成数组对象。

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
let {length : len} = 'hello';
len // 5

数值和布尔值的解构赋值

如果等号右边是数值或布尔值,则会转换为对象。也就是说赋值的变量与等号右边类型相同。

赋值的规则是只要右边不是对象和数组,就会转换成对象。undefined和null是无法进行赋值的。

let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

函数参数的解构赋值

函数的参数也可以进行解构赋值,并且可以使用默认值。

function move({x = 0, y = 0} = {}) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

但是注意,如果

move({x, y} = { x: 0, y: 0 })

则不会给参数赋默认值,因为上面代码是给函数的参数给默认值,而不是给变量赋默认值。

move({x: 3, y: 8}); // [3, 8] move({x: 3}); // [3, undefined] move({}); // [undefined, undefined] move(); // [0, 0]

undefined也会触发函数参数的默认值。

[1, undefined, 3].map((x = 'yes') => x); // [ 1, 'yes', 3 ]

圆括号问题

ES6的规则是,只要有可能导致解构的歧义,就不得使用圆括号。

可以使用圆括号的只有一种场景:赋值语句的非模式部分。

[(b)] = [3]; // 正确 ({ p: (d) } = {}); // 正确 [(parseInt.prop)] = [3]; // 正确

实际用途

1. 交换两个变量的值

let [x,y] = [1,2];
[x,y] = [y,x];
console.log(x+","+y); // 2,1

2. 从函数返回多个值

// 返回数组
function example() {
    return [1,2,3];
}
let [a,b,c] = example(); // 1,2,3

// 返回对象
function exampleObj(){
    return{
        foo:1,
        bar:2
    }
}
let{foo,bar} = exampleObj(); // 1,2

3. 函数参数的定义,可以将一组参数与变量名对应起来。

// 参数数组
function  f([a,b,c]) {
    console.log(a+","+b+","+c);
}
f([1,2,3]);

// 参数对象
function ff({a,b,c}){
    console.log(a+","+b+","+c);
}
ff({a:1,b:2,c:3});

4. 提取JSON数据

let jsonData = {
    "Name":"A",
    "Old":12
}
let {Name,Old} = jsonData;
console.log(Name+Old);

5. 函数参数的默认值

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

6. 遍历Map结构

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

for (let [key, value] of map) {
  console.log(key + " is " + value);
}
//仅获取键 let[key]
//仅获取值 let[,value]

7. 输入模块的指定方法

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

字符串的Unicode表示法

Js允许使用\uxxxxx形式表示一个字符,其中xxx表示字符的Unicode码点

单一码点只能在\u0000-\uffff之间。如果超过这个范围需要使用两个字符。

ES6对此进行了改进,只要将码点放进大括号,就能正确解读该字符。

"\u{20BB7}"
// "?"

"\u{41}\u{42}\u{43}"
// "ABC"

String.codePointAt() 会返回字符串的码点。

String.fromCodePoint() 会从码点返回字符。

字符串的遍历器接口

ES6为字符串添加了for...of循环遍历

for(let codePoint of 'hello')
{
console.log(codePoint);
}

for...of循环,可以识别大于0xFFFF的码点。也就是可以识别汉字

includes('') //返回布尔值,表示是否找到了参数字符串

startsWith('') //返回布尔值,表示是否在字符串开头

endsWith('')  //返回布尔值,表示是否在字符串结尾

'hello'.repeat(2) //返回一个新字符串,表示将原字符串重复n次,如果是小数则取整,负数会报错

'x'.padStart(2,'x') //如果某个字符串不足两位长度,就在头补全

'x'.padEnd() //尾部补全

模板字符串

模板字符串是增强版的字符串,使用 ` 反引号标识。用来定义多行字符串,或者在字符串中嵌入变量。

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

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

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

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

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

如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出中。如果不想保留,可以在末尾使用trim()删除

模板字符串中嵌套变量,可以使用 ${变量名},括号内可以放任何计算和表达式,还可以调用函数

正则的扩展

http://es6.ruanyifeng.com/#docs/regex

数值的扩展

http://es6.ruanyifeng.com/#docs/number

函数的扩展

ES6允许为函数的参数设置默认值,直接写在参数定义的后面使用=号

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

参数的变量都是默认声明的,所以不能使用let 或 const再次声明。

如果参数默认值是变量,则每次都重新计算默认表达式的值。

函数默认值必须在函数的尾部,非尾部定义是没有办法省略的。

作用域

一旦设置了参数的默认值,参数会形成一个单独的作用域。

等初始化结束以后,这个作用域就会消失。

var x = 1;

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

f(2) // 2

rest参数

ES6引用rest参数,用于获取函数的多余参数,这样就不需要使用arguments对象了。

rest参数是一个数组变量,该变量将多余的数组存放进去。

function add(...values) {
  let sum = 0;
  for (var val of values) {
    sum += val;
  }
  return sum;
}
add(2, 5, 3) // 10

扩展运算符

扩展运算符是三个点(...),将一个数组转为用逗号分隔的参数序列。

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

扩展运算符可以展开数组,也就是说不需要apply方法了。

// ES5的写法
Math.max.apply(null, [14, 3, 77])  // ES6的写法 Math.max(...[14, 3, 77])

在看一个把一个数组添加到另一个数组尾部的例子

// ES5的写法
var arr1 = [0, 1, 2]; var arr2 = [3, 4, 5]; Array.prototype.push.apply(arr1, arr2);  // ES6的写法 var arr1 = [0, 1, 2]; var arr2 = [3, 4, 5]; arr1.push(...arr2);

扩展运算符的应用

1. 合并数组

// ES5
[1, 2].concat(more)
// ES6
[1, 2, ...more]

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

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

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

2. 与解构赋值结合。如果扩展运算符用于给数组赋值,只能放在参数最后一位

// ES5
a = list[0], rest = list.slice(1)
// ES6
[a, ...rest] = list

3. 函数的返回值

Javascript函数只能返回一个值,如果需要返回多个,就只能使用数组或对象。

扩展运算符提供了解决这个问题的一个变通方法。

var dateFields = readDateFields(database); var d = new Date(...dateFields);

4. 扩展运算符可以将字符串转为数组

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

name属性

函数的 name 属性,返回该函数的函数名。

function foo() {} foo.name // "foo"

箭头函数

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

var a = v => console.log(v);
// 等同于
var b = function(v){
    console.log(v);
}
var f = () => 5;
// 等同于
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
  return num1 + num2;
};

如果箭头函数的代码块部分多于一条语句,就需要使用大括号将他们括起来。并且使用return返回。

var sum = (num1, num2) => { return num1 + num2; }

大括号会被解析为代码块,如果箭头函数直接返回一个对象,则必须在对象外面加上括号

var getTempItem = id => ({ id: id, name: "Temp" });

箭头函数可以与变量解构一起使用

const full = ({ first, last }) => first + ' ' + last;  // 等同于 function full(person) { return person.first + ' ' + person.last; }

也可以使得表达式更加简洁一些

const isEven = n => n % 2 == 0; const square = n => n * n;

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

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

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

// 正常函数写法
var result = values.sort(function (a, b) {
  return a - b;
});

// 箭头函数写法
var result = values.sort((a, b) => a - b);

箭头函数还可以与rest结合使用

const numbers = (...nums) => nums;

numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]

const headAndTail = (head, ...tail) => [head, tail];

headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]

箭头函数注意点

1. 函数内的this对象,就是定义时所在的对象,而不是运行时所在的对象

2. 不可以当做构造函数,不可以使用new

3. 不可以使用 arguments对象,如果需要可以使用rest替代

4. 不可以使用 yield 命令

箭头函数可以嵌套使用

const plus1 = a => a + 1;
const mult2 = a => a * 2;

mult2(plus1(5))
// 12

箭头函数可以让this固定化,因为箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。

也正是因为没有this,所以不能做为构造函数。

function foo() {
  return () => {
    return () => {
      return () => {
        console.log('id:', this.id);
      };
    };
  };
}

var f = foo.call({id: 1});

var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1

上面代码中,只有一个this,就是函数foo的this。因为所有的内层函数都是箭头函数,都没有自己的this。

属性简洁表达式

ES6允许直接写入变量和函数,作为对象的属性和方法

var foo = 'bar'; var baz = {foo}; baz // {foo: "bar"}  // 等同于 var baz = {foo: foo};

属性表达式

ES6允许字面量定义对象,将变量用作对象的属性名、方法名。两个表达式不能同时使用

let propKey = 'foo'; let obj = { [propKey]: true, ['a' + 'bc']: 123 };

方法的name属性

函数的name属性,返回函数名。对象方法也是函数,也有name属性。

Object.is()

Object.is用来比较两个值是否严格相等,与严格比较运算符(===)一致。

Object.assign() 

Object.assign 用于将对象合并,将源对象所有可枚举的属性,复制到目标对象中。

object.assign 使用的是浅拷贝。

常见途径

1. 为对象添加属性(将x和y拷贝到对象实例上)

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

2. 为对象添加方法

Object.assign(SomeClass.prototype, {
  someMethod(arg1, arg2) {
    ···
  },
  anotherMethod() {
    ···
  }
});

// 等同于下面的写法
SomeClass.prototype.someMethod = function (arg1, arg2) {
  ···
};
SomeClass.prototype.anotherMethod = function () {
  ···
};

3. 克隆对象

function clone(origin) {
  return Object.assign({}, origin);
}

上面的克隆代码,只能克隆原始对象值,不能克隆他的继承值。如果想保持继承链,可以用下面代码

function clone(origin) {
  let originProto = Object.getPrototypeOf(origin);
  return Object.assign(Object.create(originProto), origin);
}

4. 合并多个对象

const merge =
  (target, ...sources) => Object.assign(target, ...sources);

如果希望合并成一个空的对象,可以用下面的代码

const merge =
  (...sources) => Object.assign({}, ...sources);

属性的遍历

ES6一共有5中方式可以遍历对象的属性

for ... in:循环遍历自身和继承的可枚举属性(不含Symbol属性

Object.keys(obj):返回一个数组,包括对象自身的所有可枚举属性。(不含继承、不含Symbol属性)

Object.getOwnPropertyNames(obj):返回一个数组,包括对象自身的所有属性,包括不可枚举的属性。(不含继承、不含Symbol属性)

Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的Symbol属性。

Reflect.ownKeys(obj):返回一个数组,包含对象自身的属性。(不含继承、含Symbol属性)

以上5中遍历方式,都会按照以下顺序。

1. 遍历所有属性名为数值的属性,按照数字排序

2. 遍历所有属性名为字符串的属性,按照生成时间排序

3. 遍历所有属性名为Symbol值的属性,按照生成时间排序

__proto__属性

用来读取或设置当前对象的 prototype 对象。

它实际上是一个内部属性,不对外开放。所以尽量不要使用这个属性。

__proto__ 和 prototype的区别

prototype是函数的一个属性。注意是函数,对象中是没有的。这个属性指向对象的原型的属性。

__proto__是一个对象拥有的内置属性,是JS内部使用寻找原型链的属性。

当我们需要使用prototype的时候,下面例子进行new时分为三步。

var Person = function(){};
var p = new Person();

1. var p = {}; 初始化一个对象p

2. p.__proto__ = Person.prototype;

3. Person.call(p) 构造p,初始化p

Object.setPrototypeOf()

与__proto__属性相同,用来设置一个对象的prototype对象,返回参数对象本身。

这是ES6推荐的设置对象的方法。左侧是待设置对象,右侧是设置继承对象。

var obj1 = {
    Name: "C"
};
var obj2 = {
    Old : 12
};
Object.setPrototypeOf(obj2,obj1);
console.log(obj2.Old); //12
console.log(obj2.Name); //C

如果第一个参数不是对象,则会给他自动转为对象,但由于返回的还是第一个参数,所以不会产生效果。

由于undefined 和 null 无法转为对象,所以如果当做第一个参数,则会报异常。

Object.getPrototypeOf()

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

Object.getPrototypeOf(obj);
let obj1 = { Name: "C" };
let obj2 = { Old: 12 };
let obj3 = () => ({Name: "CC"});
let obj4 = function () {};
Object.setPrototypeOf(obj2, obj1);
Object.setPrototypeOf(obj4, obj3);
console.log(Object.getPrototypeOf(obj4) == obj4.__proto__); //true
console.log(Object.getPrototypeOf(obj2) === obj2.__proto__); //true
console.log(Object.getPrototypeOf(new obj4()) == obj4.prototype); //true

如果参数不是对象,会被自动转为对象。如果参数是undefined或null,无法转为对象,会报错。

Object.keys()

返回一个数组,成员是参数对象自身的键名(不含继承)

let obj1 = {Name: "C"};
let obj2 = {Old: 12, Old2 : 13};
Object.setPrototypeOf(obj2, obj1);
console.log(Object.keys(obj2)); // [ 'Old', 'Old2' ]

Object.values()

返回一个数组,成员是参数对象自身的键值(不含继承)

let obj1 = {Name: "C"};
let obj2 = {Old: 12, Old2 : 13};
Object.setPrototypeOf(obj2, obj1);
console.log(Object.values(obj2)); // [ 12, 13 ]

Object.entries()

返回一个数组,成员是参数对象自身的键值对(不含继承)

let obj1 = {Name: "C"};
let obj2 = {Old: 12, Old2 : 13};
Object.setPrototypeOf(obj2, obj1);
console.log(Object.entries(obj2)); // [ [ 'Old', 12 ], [ 'Old2', 13 ] ]

Object.getOwnPropertyDescriptors()

返回某个对象属性的描述对象(不含继承)

let obj1 = {Name: "C"};
let obj2 = {Old: 12, Old2 : 13};
Object.setPrototypeOf(obj2, obj1);
console.log(Object.getOwnPropertyDescriptor(obj2,"Old"));
//{ value: 12,
// writable: true,
//    enumerable: true,
//    configurable: true }

Symbol属性

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

Symbol值通过Symbol函数生成。凡是属性名属于Symbol类型,都是独一无二的,可以保证不会产生冲突。

let s = Symbol(); typeof s // "symbol"

Symbol函数前不能使用new命令,因为生成Symbol是一个原始类型的值,不是对象。

Symbol可以接受一个字符串作为参数,用来表示描述或区分。如果Symbol参数是一个对象,则会调用该对象的toString方法。

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

作为属性名Symbol

由于每个Symbol值都是不相等的,用于对象的属性名,就能保证不会出现同名的属性。

var mySymbol = Symbol();
// 第一种
var obj = {};
obj[mySymbol] = "C";
//第二种
var obj2 = {
    [mySymbol]:"C2"
}
//第三种
var obj3 = {};
Object.defineProperty(obj3,mySymbol,{value:"C3"});
console.log(obj[mySymbol]);

Symbol作为属性名的时候,不能用点运算符。因为点运算符后面总是字符串,不会读取标示; 

同理,在对象内部使用Symbol定义属性时,必须放在方括号[]内。如果不放在方括号内,则当做字符串处理。

Symbol还可以定义一组常量,保证这组常量的值都是不相等的。

log.levels = {
  DEBUG: Symbol('debug'), INFO: Symbol('info'), WARN: Symbol('warn') }; log(log.levels.DEBUG, 'debug message'); log(log.levels.INFO, 'info message');
const COLOR_RED    = Symbol(); const COLOR_GREEN = Symbol(); ---------------------------------------------- function getComplement(color) { switch (color) { case COLOR_RED: return COLOR_GREEN; case COLOR_GREEN: return COLOR_RED; default: throw new Error('Undefined color'); } }

常量使用Symbol值最大好处是,其他任何值都不可能有相同的值了。

Symbol值作为属性名时,该属性还是公开属性,不是私有属性。

常用实例:消除魔法数

魔法数指,在代码中多次出现的字符串或数值。会与代码进行强耦合,应该尽量消除魔法数。

如果魔法数的值不重要,只是为了区分,就可以使用Symbol来进行修改。

function getAreaBefore(shape,options)
{
    var area = 0;
    switch(shape)
    {
        case 'Triangle':
            area = .5 * options.width * options.height;
            break;
    }
    return area;
}
console.log(getAreaBefore('Triangle',{width:100,height:100}));

var shareType = {
    Triangle:Symbol()
}
function getAreaAfter(shape,options)
{
    var area = 0;
    switch(shape){
        case shareType.Triangle:
            area = .5 * options.width * options.height;
            break;
    }
    return area;
}
console.log(getAreaAfter(shareType.Triangle,{width:100,height:100}));

属性名的遍历

Symbol作为属性名,不会出现在 for...in、for...of循环中,也不会在Object.keys、Object.getOwnPropertyNames、Json.stringify中

有一个Object.getOwnPropertySymbols方法,可以获取指定对象所有Symbol属性名。

Reflect.ownKeys方法也可以返回所有类型的键名,包括常规的和Symbol的。

Symbol.for

可以接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有就返回,没有就新建。

var s1 = Symbol.for('foo'); var s2 = Symbol.for('foo'); s1 === s2 // true

Symbol.keyFor

可以返回一个已登记的Symbol类型值的key。

var s1 = Symbol.for("foo"); Symbol.keyFor(s1) // "foo" var s2 = Symbol("foo"); Symbol.keyFor(s2) // undefined

常用实例:单例模式

单例模式指一个类,任何时候都是返回同一个实例。我们为了防止这个类的全局变量修改,可以使用Symbol

// mod.js
const FOO_KEY = Symbol.for('foo');
function A() {
    this.foo = 'hello';
}
if (!global[FOO_KEY]) {
    global[FOO_KEY] = new A();
}
module.exports = global[FOO_KEY];
require('./mod.js');
console.log(global[Symbol.for('foo')].foo); //hello

Symbol.hasInstance

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

class MyClass{
    [Symbol.hasInstance](foo){
        return foo instanceof Array;
    }
}
var result = [1,2,3] instanceof new MyClass();
console.log(result); //true

Symbol.isConcatSpreadable

布尔值属性,表示该对象使用Array.prototype.concat() 时,是否可以展开。默认为可以展开

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

类似数组的对象也可以展开,但他的属性默认值为false,必须手动打开。

let obj = {length: 2, 0: 'c', 1: 'd'}; ['a', 'b'].concat(obj, 'e') // ['a', 'b', obj, 'e'] obj[Symbol.isConcatSpreadable] = true; ['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']

对于一个类来说,此属性必须写成实例的属性。

class A1 extends Array {
  constructor(args) {
    super(args);
    this[Symbol.isConcatSpreadable] = true;
  }
}
class A2 extends Array {
  constructor(args) {
    super(args);
    this[Symbol.isConcatSpreadable] = false;
  }
}

Symbol.species

创建实例时,默认会调用这个方法,使用这个属性返回的函数当做构造函数,来创建新的实例对象。

class MyArray extends Array {
  static get [Symbol.species]() { return Array; }
}
var a = new MyArray(1,2,3);
var mapped = a.map(x => x * x);

mapped instanceof MyArray // false
mapped instanceof Array // true

Symbol.match

当执行str.match(myObject)时,如果该属性存在,会调用它,返回该方法的返回值。

class MyMatcher {
  [Symbol.match](string) { return 'hello world'.indexOf(string); } } 'e'.match(new MyMatcher()) // 1

Symbol.replace

当该对象被string.prototype.replace方法调用时,返回该方法的返回值。

const x = {}; x[Symbol.replace] = (...s) => console.log(s); 'Hello'.replace(x, 'World') // ["Hello", "World"]

Symbol.search

该对象被String.prototype.search方法调用时,会返回该方法的返回值。

class MySearch {
  constructor(value) { this.value = value; } [Symbol.search](string) { return string.indexOf(this.value); } } 'foobar'.search(new MySearch('foo')) // 0

Symbol.split

该对象被String.prototype.split方法调用时,会返回该方法的返回值

class MySplitter {
  constructor(value) { this.value = value; } [Symbol.split](string) { var index = string.indexOf(this.value); if (index === -1) { return string; } return [ string.substr(0, index), string.substr(index + this.value.length) ]; } } 'foobar'.split(new MySplitter('foo')) // ['', 'bar'] 'foobar'.split(new MySplitter('bar')) // ['foo', ''] 'foobar'.split(new MySplitter('baz')) // 'foobar'

Symbol.iterator

指向对象的默认遍历器方法。对象进行for...of循环时,会调用此方法。

class Collection {
  *[Symbol.iterator]() { let i = 0; while(this[i] !== undefined) { yield this[i]; ++i; } } } let myCollection = new Collection(); myCollection[0] = 1; myCollection[1] = 2; for(let value of myCollection) { console.log(value); } // 1 // 2

Symbol.toPrimitive

对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。

Symbol.toPrimitive被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式。

1. Number:该场合需要转成数值

2. String : 该场合需要转成字符串

3. Default:该场合可以转成数值或字符串

let obj = {
  [Symbol.toPrimitive](hint) { switch (hint) { case 'number': return 123; case 'string': return 'str'; case 'default': return 'default'; default: throw new Error(); } } }; 2 * obj // 246 3 + obj // '3default' obj == 'default' // true String(obj) // 'str'

Symbol.toStringTag

在该对象上面调用Object.prototype.toString方法时,如果这个属性存在,他的返回值会出现在方法返回值中。

// 例一
({[Symbol.toStringTag]: 'Foo'}.toString()) // "[object Foo]"  // 例二 class Collection { get [Symbol.toStringTag]() { return 'xxx'; } } var x = new Collection(); Object.prototype.toString.call(x) // "[object xxx]"

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有以下属性

constructor:构造函数,默认就是Set函数

size:返回set实例的成员总数

实例方法分为两大类,操作方法、遍历方法

操作方法:

add(value):添加某个值,返回Set结构本身

delete(value):删除某个值,返回一个布尔值,表示删除是否成功

has(value):返回一个布尔值,表示该值是否为Set成员

clear():清除所有成员,没有返回值

Array.form:可以将Set结构转为数组

这样去重复,也可以写成如下格式

function dedupe(array) { return Array.from(new Set(array)); } dedupe([1, 1, 2, 3]) // [1, 2, 3]

遍历操作:

keys():返回键名遍历器

values() :返回键值遍历器

entries() : 返回键值对遍历器

forEach():使用回调函数遍历每个成员。参数依次为键值、键名、集合本身

let set = new Set([1, 2, 3]); set.forEach((value, key) => console.log(value * 2) )

运用

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}

WeakSet

WeakSet与Set类似,也是不重复的集合,WeakSet不能进行遍历。与Set有两个区别

1. WeakSet成员只能是对象,如果是其他值会报错。

2. WeakSet中的对象都是弱引用,垃圾回收机制不考虑WeakSet对该对象的引用。

如果其他对象都不在引用该对象,那么会直接回收,不会考虑WeakSet。

由于上述特点,WeakSet成员不适合引用,因为会随时消失。

const a = [[1, 2], [3, 4]]; const ws = new WeakSet(a); // WeakSet {[1, 2], [3, 4]}

WeakSet结构有三个方法

add(value):添加一个新成员

delete(value):删除指定成员

has(value):是否包含指定成员

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

Map结构的实例有以下属性和操作方法。

Size:返回Map结构的成员总数

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

get(key):读取key的键值,找不到返回undefined

has(key):返回一个布尔值,表示某个键是否存在

delete(key):删除某个键,返回true,如果失败返回false

clear():清除所有成员,没有返回值

遍历方法

keys():返回键名遍历器

values() :返回键值遍历器

entries() : 返回键值对遍历器

forEach():使用回调函数遍历每个成员。参数依次为键值、键名、集合本身

与其他数据结构转换

Map转数组

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

数组转Map

new Map([ [true, 7], [{foo: 3}, ['abc']] ]) // Map { // true => 7, // Object {foo: 3} => ['abc'] // }

Map转对象

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}

Map转JSON

function strMapToJson(strMap) { return JSON.stringify(strMapToObj(strMap)); } let myMap = new Map().set('yes', true).set('no', false); strMapToJson(myMap) // '{"yes":true,"no":false}'

JSON转Map

function jsonToStrMap(jsonStr) { return objToStrMap(JSON.parse(jsonStr)); } jsonToStrMap('{"yes": true, "no": false}') // Map {'yes' => true, 'no' => false}

WeakMap

WeakMap结构与Map结构类似,用于生成键值对的集合

WeakMap与Map的区别有两点

1. WeakMap只接受对象作为键名,不接受其他的值作为键名

2. WeakMap的键名所指的对象,不计入垃圾回收机制

Proxy

用来修改某些操作的默认行为,在目标对象之前架设一层拦截,外界对对象的访问都必须先通过这层拦截。

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

var proxy = new Proxy(target, handler);
var obj = new Proxy({},{
   get:function(target,key,receiver){
        console.log(`get ${key}`);
        return Reflect.get(target,key,receiver);
   },
   set:function(target,key,value,receiver){
       console.log(`set ${key}`);
       return Reflect.set(target,key,value,receiver);
   }
});
obj.count = 1; // set count
obj.count++; //get count  set count

Proxy接受两个参数,

第一个参数是所代理的目标,及如果没有Proxy介入,原来访问的对象

第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数。

可以将Proxy对象,设置到object.proxy属性,从而可以在object对象上调用

var object = { proxy: new Proxy(target, handler) };

proxy实例也可以作为其他对象的原型对象

同一个拦截器函数,可以设置拦截多个操作。

下面是Proxy支持的拦截操作

get(target,propKey,receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']

set(target,propKey,value,receiver):拦截对象属性的设置,返回一个布尔值

has(target,propKey):拦截propKey in proxy的操作,返回一个布尔值

用来拦截HasProperty操作,即判断对象是否具有某个属性时。典型的操作就是in运算符

var handler = {
  has(target,key){
      if(key[0] === '_'){
          return false;
      }
      return key in target;
  }
};
var target = { _prop:'foo',prop:'foo' };
var proxy = new Proxy(target,handler);
console.log('_prop' in proxy); // false
console.log('prop' in proxy); // true

deleteProperty(target,propKey):拦截delete proxy[propKey]的操作,返回一个布尔值

deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性无法被delete删除。

var handler = {
  deleteProperty (target, key) {
    invariant(key, 'delete');
    return true;
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}

var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property

ownKeys(target)

拦截Object.getOwnPropertyNames(proxy)\Object.getOwnPropertySymblos(proxy)\Object.keys(proxy)

返回一个数组,该方法返回目标对象所有自身的属性和属性名,而Object.keys的返回值结果仅包括目标对象自身的可遍历属性

let target = {
  _bar: 'foo',
  _prop: 'bar',
  prop: 'baz'
};

let handler = {
  ownKeys (target) {
    return Reflect.ownKeys(target).filter(key => key[0] !== '_');
  }
};

let proxy = new Proxy(target, handler);
for (let key of Object.keys(proxy)) {
  console.log(target[key]);
}
// "baz"

getOwnPropertyDescriptor(target,propKey)

拦截Object.getOwnPropertyDescriptor(proxy,propkey),返回属性的描述对象

var handler = {
  getOwnPropertyDescriptor (target, key) {
    if (key[0] === '_') {
      return;
    }
    return Object.getOwnPropertyDescriptor(target, key);
  }
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'wat')
// undefined
Object.getOwnPropertyDescriptor(proxy, '_foo')
// undefined
Object.getOwnPropertyDescriptor(proxy, 'baz')
// { value: 'tar', writable: true, enumerable: true, configurable: true }

defineProperty(target,propKey,proDesc)

拦截Object.defineProperty(proxy,propkey,propDesc)/Object.defineProperties(proxy,proDesc)返回一个布尔值

var handler = {
  defineProperty (target, key, descriptor) {
    return false;
  }
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar'

preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值

var p = new Proxy({}, {
  preventExtensions: function(target) {
    console.log('called');
    Object.preventExtensions(target);
    return true;
  }
});

Object.preventExtensions(p)
// "called"
// true

getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象

var proto = {};
var p = new Proxy({}, {
  getPrototypeOf(target) {
    return proto;
  }
});
Object.getPrototypeOf(p) === proto // true

isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值

var p = new Proxy({}, {
  isExtensible: function(target) {
    console.log("called");
    return true;
  }
});

Object.isExtensible(p)
// "called"
// true

setPrototypeOf(target,proto):拦截Object.setPrototypeOf(proxy,proto),返回一个布尔值

var handler = {
  setPrototypeOf (target, proto) {
    throw new Error('Changing the prototype is forbidden');
  }
};
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden

apply(target,object,args):拦截proxy实例作为函数调用的操作,比如proxy(...args)/proxy.call(object,...args)/proxy.apply(...) 

apply方法拦截函数的调用,call和apply操作。方法可以接受三个参数,分别是目标对象、上下文对象、参数数组。

var twice = {
  apply(target,ctx,args){
      return Reflect.apply(...arguments) * 2;
      //return Reflect.apply(target,ctx,args) * 2;
  }
};
function sum(left,right){
    return left+right;
};
var proxy = new Proxy(sum,twice);
console.log(proxy(1,2)); // 6
console.log(proxy.apply(null,[1,2])); // 6
console.log(proxy.call(null,1,2)); // 6

construct(target,args):拦截Proxy实例作为构造函数调用的操作,比如new Proxy(...args)

construct方法可以接受两个参数

1. target:目标对象

2. args:构建函数的参数对象

var p = new Proxy(function () {}, {
    construct: function(target, args) {
        console.log('called: ' + args.join(', '));
        return { value: args[0] * 10 };
    }
});

new p(1).value; // called: 1

Proxy.revocable()

Proxy.revocable方法返回一个可取消的Proxy实例

let target = {};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked

this问题

Proxy代理的情况下,目标对象内部的this关键字会指向Proxy代理

const target = {
  m: function () {
    console.log(this === proxy);
  }
};
const handler = {};

const proxy = new Proxy(target, handler);

target.m() // false
proxy.m()  // true

Reflect

Reflect对象与Proxy对象一样,也是ES6为了操作对象而提供的新API

Reflect对象的设计目的有这样几个

1. 将Object对象的一些明显属于语言内部的方法,放到Reflect对象上。

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

// 老写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

3. 让Object操作都变成函数行为。某些Object操作是命令式的,比如name in obj

// 老写法
'assign' in Object // true // 新写法 Reflect.has(Object, 'assign') // true

4. Reflect对象的方法与Proxy对象的方法一一对应,主要Proxy对象的方法,Reflect就有

var loggedObj = new Proxy(obj, {
  get(target, name) {
    console.log('get', target, name);
    return Reflect.get(target, name);
  },
  deleteProperty(target, name) {
    console.log('delete' + name);
    return Reflect.deleteProperty(target, name);
  },
  has(target, name) {
    console.log('has' + name);
    return Reflect.has(target, name);
  }
});

静态方法

Reflect对象一共有13个静态方法

Reflect.apply(target,thisArg,args)

用于绑定this对象后执行给定函数

const ages = [11, 33, 12, 54, 18, 96];
// 旧写法
const youngest = Math.min.apply(Math, ages);
const oldest = Math.max.apply(Math, ages);
const type = Object.prototype.toString.call(youngest);
// 新写法
const youngest = Reflect.apply(Math.min, Math, ages);
const oldest = Reflect.apply(Math.max, Math, ages);
const type = Reflect.apply(Object.prototype.toString, youngest, []);

Reflect.construct(target,args)

方法等同于new target(...args),提供了一种不使用new,来调用构造函数的方法

function Greeting(name) {
  this.name = name;
}
// new 的写法
const instance = new Greeting('张三');
// Reflect.construct 的写法
const instance = Reflect.construct(Greeting, ['张三']);

Reflect.get(target,name,receiver)

查找并返回target对象的name属性,如果有读取函数,则读取函数的this绑定receiver

var myObject = {
  foo: 1,
  bar: 2,
  get baz() {
    return this.foo + this.bar;
  },
};

var myReceiverObject = {
  foo: 4,
  bar: 4,
};

Reflect.get(myObject, 'baz', myReceiverObject) // 8

Reflect.set(target,name,value,receiver)

设置target对象的name属性等于value,如果设置了赋值函数,则赋值函数this绑定receiver

var myObject = {
  foo: 4,
  set bar(value) {
    return this.foo = value;
  },
};

var myReceiverObject = {
  foo: 0,
};

Reflect.set(myObject, 'bar', 1, myReceiverObject);
myObject.foo // 4
myReceiverObject.foo // 1

Reflect.defineProperty(target,name,desc)

用来为对象定义属性。如果第一个参数不是对象,就会抛出错误

function MyDate() {
  /**/
}
// 旧写法
Object.defineProperty(MyDate, 'now', {
  value: () => Date.now()
});
// 新写法
Reflect.defineProperty(MyDate, 'now', {
  value: () => Date.now()
});

Reflect.deleteProperty(target,name)

等同于delete obj[name],用于删除对象的属性。

如果删除成功,或者删除属性不存在,返回true。

删除失败,或者被删除的属性依然存在,返回false。

const myObj = { foo: 'bar' };
// 旧写法
delete myObj.foo;
// 新写法
Reflect.deleteProperty(myObj, 'foo');

Reflect.has(target,name)

Reflect.has方法对应name in obj里面的in运算符

var myObject = {
  foo: 1,
};
// 旧写法
'foo' in myObject // true
// 新写法
Reflect.has(myObject, 'foo') // true

Reflect.ownKeys(target)

方法用于返回对象的所有属性

var myObject = {
  foo: 1,
  bar: 2,
  [Symbol.for('baz')]: 3,
  [Symbol.for('bing')]: 4,
};

// 旧写法
Object.getOwnPropertyNames(myObject)
// ['foo', 'bar']

Object.getOwnPropertySymbols(myObject)
//[Symbol.for('baz'), Symbol.for('bing')]

// 新写法
Reflect.ownKeys(myObject)
// ['foo', 'bar', Symbol.for('baz'), Symbol.for('bing')]

Reflect.isExtensible(target)

返回布尔值,表示当前对象是否可以扩展

const myObject = {};
// 旧写法
Object.isExtensible(myObject) // true
// 新写法
Reflect.isExtensible(myObject) // tru 

Reflect.preventExtensions(target)

用于让一个对象变为不可扩展,返回一个布尔值,表示是否操作成功

var myObject = {};
// 旧写法
Object.isExtensible(myObject) // true
// 新写法
Reflect.preventExtensions(myObject) // true

Reflect.getOwnPropertyDescriptor(target, name)

用于得到指定属性的描述对象,如果第一个参数不是对象,不报错返回undefined。

var myObject = {};
Object.defineProperty(myObject, 'hidden', {
  value: true,
  enumerable: false,
});
// 旧写法
var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');
// 新写法
var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden');

Reflect.getPrototypeOf(target)

方法用于读取对象的__prop__的属性,对应Object.getPrototypeOf

const myObj = new FancyThing();
// 旧写法
Object.getPrototypeOf(myObj) === FancyThing.prototype;
// 新写法
Reflect.getPrototypeOf(myObj) === FancyThing.prototype;

Reflect.setPrototypeOf(target, prototype)

方法用于设置对象的__prop__属性,返回第一个参数对象

const myObj = new FancyThing();
// 旧写法
Object.setPrototypeOf(myObj, OtherThing.prototype);
// 新写法
Reflect.setPrototypeOf(myObj, OtherThing.prototype);

Promise

异步编程的一种解决方案,比回调函数和事件,更合理和强大。ES6将其写入语言标准,提供了Promise对象

Promise就是一个容器,保存某个未来才会结束的事件(通常是一个异步操作)的结果。

从语法上来说,Promise是一个对象,可以获取异步信息,提供统一API,各种操作都可以用同样的方法进行处理。

Promise对象有两个特点

1. 对象的状态不受外界影响,Promise对象代表一个异步操作,三种状态:Pending进行中,Resolved已完成,Rejected已失败

只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能

从Pending改变为Resolved

从Pending改变为Rejected

只要这两种情况发生了,状态就凝固了,不会再变了。会一直保持这个结果。

有了Promise对象,可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。提供了统一的接口,操作更方便

Promise缺点有三个

无法取消,一旦新建他就会立即执行。

如果不设置回调,异常则不会反应到外部。

当处于Pending状态时,无法得知进展到哪一个阶段

基本用法

Promise对象是一个构造函数,用来生成Promise实例。

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

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

允许接收一个函数作为参数,函数的两个参数分别是resolve,reject,这两个参数由js引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从Pending变为Resolved。在成功时调用,并将异步操作结果作为参数传递。

reject函数的作用是,将Promise对象的状态从Pending编程Rejected,在操作失败时调用,并将异步操作报出的错误作为参数传递出去。

实例生成后,可以用then方法分别制定Resolved状态和Rejected状态的回调函数。

promise.then(function(value) {  // success }, function(error) {  // failure });

then方法可以接受两个回调函数作为参数,第一个回调是状态为Resolved,第二个回调是状态为Rejected时调用。

其中第二个函数是可选的,两个函数都接受Promise对象传出的值作为参数。

function timeout(ms){
    return new Promise((resolve,reject) => {
        if(ms > 1000)
            resolve("--成功");
        else
            reject("--失败");
    });
}
timeout(1000).then((value)=>{
    console.log("成功"+value);
},(value)=>{
    console.log("失败"+value);
});
timeout(1001).then((value)=>{
    console.log("成功"+value);
},(value)=>{
    console.log("失败"+value);
});

上面例子表示,返回一个Promise实例,如果参数大于1000则改变状态为Resolved,触发then绑定事件。

Promise新建后,就会立即执行。then方法指定的回调将在当前脚本所有同步任务完成后执行,所以Resolved最后输出。

let promise = new Promise((resolve,reject)=>{
    console.log("promise");
    resolve();
});
promise.then(()=>console.log("Resolved."));
console.log("Hi");
// promise > Hi > Resolved

如果调用resolve函数和reject函数时带有参数,那么他们的参数会传递给回调函数。

reject函数的参数通常是Error对象的实例

resolve函数的参数除了正常值以外,还可能是另一个Promise实例,表示异步操作的结果有可能是一个值,也有可能是另一个操作。

如果p1 p2都是Promise实例,但是p2的resolve方法将p1作为参数,一个异步操作的结果是返回另一个异步操作。

此时p1状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1状态时Pending,那么p2的回调就会等待。

如果p1的状态时Resolve或Rejected,那么p2的回调就会立即执行。

var p1 = new Promise((resolve,reject)=>{
    setTimeout(()=>reject(new Error('fail')),3000);
});
var p2 = new Promise((resolve,reject)=>{
    setTimeout(()=>resolve(p1),1000);
});
p2.then(result=>console.log(result)).catch(error=>console.log(error.toString()));
//Error : fail

Promise.prototype.then

为Promise实例添加状态改变时的回调函数。第一个参数是Resolved状态的回调,第二个是Rejected状态的回调

then方法返回的是新的Promise实例,可以采用链式的then,来指定一组按照次序调用的回调函数。

var promise = new Promise((resolve,reject)=>resolve());
promise.then(()=>console.log(1)).then(()=>console.log(2));

Promise.prototype.catch

catch方法是.then(null,rejection)的别名,用于指定发生错误时的回调函数。

针对错误的抛出可以有两种写法

var promise = new Promise((resolve,reject) => {
    throw "Error";
});
var promise2 = new Promise((resolve,reject) =>{
    reject("Error");
})
promise.catch(error=>console.log(error)); //Error
promise2.catch(error=>console.log(error)); //Error

如果在reject后面,在执行throw是没有效果的。

Promise.all 

用于将多个Promise实例,包装成一个新的Promise实例。

var p = Promise.all([p1, p2, p3]);

接受一个数组作为参数,参数必须是Promise对象的实例,如果不是会进行转换。

状态由p1 p2 p3来决定,分为两种情况

1. 只有状态都变成 Resolved ,p的状态才会变成 Resolved

2. 只有有一个状态为 Rejected,p的状态就会变成 Rejected

var p1 = new Promise((resolve,reject)=>resolve());
var p2 = new Promise((resolve,reject)=>resolve());
var p = Promise.all([p1,p2]).then(()=>console.log("成功")).catch(()=>console.log("失败"));

Promise.race

同样是将多个Promise实例,包装成一个新的Promise实例。

var p = Promise.race([p1, p2, p3]);

有一个实例率先改变状态,p的状态就跟着改变,并执行回调函数。

Promise.resolve

将现有的对象转为Promise对象,就需要使用Promise.resolve。例如把ajax方法,转为Promise

var jsPromise = Promise.resolve($.ajax('/whatever.json'));

Promise.resolve参数分为四种情况

1. 参数是一个Promise实例

如果参数是Promise实例,那么不做任何修改原封不动的返回这个实例

2. 参数是一个thenable对象

thenable对象指具有then方法的对象,比如

let thenable = {
  then: function(resolve, reject) { resolve(42); } };

Promise.resolve会将这个对象转为Promise对象,然后就立即执行thenable对象的then方法

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value);  // 42
});

3. 参数不具有then方法的对象,或根本就不是对象

如果参数是一个原始值或不具有then方法的对象,则返回一个新的Promise对象,状态为 d

var p  = Promise.resolve("Hello");
p.then(result=>console.log(result)); //Hello

字符串不具有then方法,返回的Promise实例的状态一开始就是Resolved,回调会立即执行,同时将参数传递给回调。

4. 不带任何参数

调用时不带参数,直接返回一个Resolved状态的Promise对象。

立即resolve的Promise对象,是在本轮事件循环的结束时。

setTimeout(function () {
  console.log('three');
}, 0);

Promise.resolve().then(function () {
  console.log('two');
});

console.log('one');
// one
// two
// three

setTimeout是下一轮事件循环的开始执行

Promise.reject

会返回一个新的Promise实例,状态为rejected

var p = Promise.reject('出错了');
// 等同于
var p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s) {
  console.log(s)
});
// 出错了

done()

Promise对象的回调链,不管以then方法或catch方法结尾,最后一个方法抛出的错误,都无法捕获

因为可以提供一个done方法,总是处于回调链尾端,保证抛出任何可能出现的错误。

Promise.prototype.done = function (onFulfilled, onRejected) {
    this.then(onFulfilled, onRejected)
        .catch(function (reason) {
            // 抛出一个全局错误
            setTimeout(() => { throw reason }, 0);
}); }; var p = Promise.resolve("Hello"); p.then(result=>{ throw new Error(result); }).done();

finally()

用于指定不管Promise对象最后状态如何,都会执行的操作。接受一个普通回调作为参数。

Promise.prototype.finally = function (callback) {
    let P = this.constructor;
    return this.then(
        value  => P.resolve(callback()).then(() => value),
        reason => P.resolve(callback()).then(() => { throw reason })
    );
};
var p  = Promise.resolve("Hello");
p.then(result=>{
    throw new Error(result);
}).finally(function(){
    console.log("finally");
});

应用-加载图片

const preloadImage = function (path) {
    return new Promise(function (resolve, reject) {
        var image = new Image();
        image.onload  = resolve;
        image.onerror = reject;
        image.src = path;
    });
};
preloadImage("images/0.jpg")
    .then(()=>console.log("图片加载成功"))
    .catch(result=>console.log("图片加载失败"));

Iterator和for...of循环

JS原有表示集合的数据结构,主要是数组和对象,ES6又添加了Map和Set。

需要一种统一的接口机制,来处理所有不同的数据结构。

遍历器(Iterator)就是这样的一个机制,它是一种接口,为不同的数据结构提供统一访问机制。

任何数据结构只要不输Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)

Iterator作用有三种

1. 为各种数据结构,提供一个统一的、简单的访问接口

2. 使得数据结构的成员能够按照某种次序排列

3. ES6创造了一种新的遍历命令for...of循环,Iterator接口主要提供for...of使用

Iterator遍历过程是这样的

1. 创建一个指针对象,指向当前数据结构的起始位置。遍历器对象本身就是一个指针对象

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

3. 第二次调用next方法,可以指向第二个成员

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

每一次调用next方法,都会返回数据结构的当前成员的信息。返回一个包含value和done两个属性的对象。

其中value属性是当前成员的值,done属性是一个布尔值,代表遍历是否结束。如果done是true,则不会再进行循环。

var it = makeIterator(['a', 'b']);
console.log(it.next()) // { value: 'a' }
console.log(it.next()) // { value: 'b' }
console.log(it.next()) // { done: true }

function makeIterator(array) {
    var nextIndex = 0;
    return {
        next: function() {
            return nextIndex < array.length ?
                {value: array[nextIndex++]} :
                {done: true};
        }
    };
}

这就是一个遍历器生成函数,作用就是返回一个遍历器对象。对数组执行这个函数,会返回该数组的遍历器对象。

在ES6中,有些数据结构原生具备Iterator接口(比如数组),即不用处理,就可以被for...of循环遍历,有些不行(比如对象)。

原因在于,这些数据结构原生部署了Symbol.iterator属性,另外一些数据结构没有。

凡是部署了Symbol.iteraotr属性的数据结构,就称为部署了遍历器的接口。调用这个接口,就会返回一个遍历器对象。

默认Iterator接口

ES6规定,默认的Iterator接口部署在数据结构的Symbol.iterator属性。

一个数据结构只要具有Symbol.iterator属性,就可以认为是可遍历的。

Symbol.iterator属性本身就是一个函数,就是当前数据结构默认的遍历器生成函数。

执行这个函数,就会返回一个遍历器,至于属性名Symbol.iterator,它是一个表达式,返回symbol对象的iterator属性

这是一个预定好的,类型为Symbol的特殊值,所以要放在括号内。

const obj = {
    // [Symbol.iterator] : function () {
    //     return {
    //         next: function () {
    //             return {
    //                 value: 1,
    //                 done: true
    //             };
    //         }
    //     };
    // }
    [Symbol.iterator]: () => ({
        next: () => ({
            value: 1, done: true
        })
    })
};

var objs = obj[Symbol.iterator]();
console.log(objs.next()); //{ value: 1, done: true }

对象obj是可遍历的,因为具有Symbol.iterator属性。执行这个属性会返回一个遍历器对象。

这个对象根本特征就是具有next方法,每次调用next方法,都会返回一个代表当前成员的信息对象。

ES6,有三类数据结构原生具有Iterator接口:数组、类似数组的对象、Set/Map结构

let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

console.log(iter.next()) // { value: 'a', done: false }

上面代码中,arr是一个数组,原生就具有遍历器接口,部署在arr的Symbol.iterator属性上面。

对象之所以没有默认部署Iterator接口,是因为对象的那个属性先遍历,那个后遍历是不确定的,需要手动指定。

遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。

不过,对象部署遍历器接口并不是很必要,这是对象实际上被当做Map结构使用,ES6原生提供了。

一个对象如果要有可被for...of循环调用的Iterator接口,就必须在Symbol.iterator的属性上部署遍历器生成方法

class RangeIterator {
    constructor(start, stop) {
        this.value = start;
        this.stop = stop;
    }

    [Symbol.iterator]() {
        return this;
    }

    next() {
        let val = this.value;
        if (val < this.stop) {
            this.value++;
            return {value: val}
        }
        return {done: true};
    }
}
function range(start,stop)
{
    return new RangeIterator(start,stop);
}
for(let item of range(0,3))
{
    console.log(item);
}

代码是一个类部署Iterator接口的写法,Symbol.iterator属性对应一个函数,执行后返回当前对象的遍历器对象。

下面是通过遍历器实现指针结构的例子

function Obj(value) {
  this.value = value;
  this.next = null;
}

Obj.prototype[Symbol.iterator] = function() {
  var iterator = {
    next: next
  };

  var current = this;

  function next() {
    if (current) {
      var value = current.value;
      current = current.next;
      return {
        done: false,
        value: value
      };
    } else {
      return {
        done: true
      };
    }
  }
  return iterator;
}

var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);

one.next = two;
two.next = three;

for (var i of one){
  console.log(i);
}
// 1
// 2
// 3

调用该方法会返回遍历器对象iterator,调用该对象的next方法,返回一个值的同时,自动将内部指针移到下一个实例

下面是为对象添加Iterator接口的例子

const obj = {
    data:['hello','world'],
    [Symbol.iterator](){
        const self = this;
        let index = 0;
        return{
            next(){
                if(index<self.data.length)
                {
                    return{value:self.data[index++]};
                }
                else{
                    return{done:true};
                }
            }
        }
    }
}
for(let item of obj)
{
    console.log(item);
}

对于类似数组对象存在数组键名和length属性,部署Iterator接口,有一个简便方法。

就是Symbol.iterator方法直接引用,数组的Iterator接口。

NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; // 或者 NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
const obj1 = {
    0:'a',
    1:'b',
    2:'c',
    length:3,
    [Symbol.iterator]:Array.prototype[Symbol.iterator]
};
for(let item of obj1)
{
    console.log(item);
}

注意的是,必须是 0,1,2 这种属性。普通属性部署Symbol.iterator是无效的。

调用Iterator接口的场合 

1. 解构赋值

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

const obj1 = {
    0:'a',
    1:'b',
    2:'c',
    length:3,
    [Symbol.iterator]:Array.prototype[Symbol.iterator]
};
const obj2 = new Set().add('a').add('b').add('c');
let [x,y] = obj1;
let [a,b] = obj2;
let [c,...rest] = obj2;
console.log(x+","+y); // a,b
console.log(a+","+b); // a,b
console.log(c+","+rest); // a,b,c

2. 扩展运算符

使用扩展运算符,也会调用默认的Iterator接口 

const str ='hello';
console.log([...str]); // [ 'h', 'e', 'l', 'l', 'o' ]

const arr = ['b','c'];
console.log(['a',...arr,'d']); // [ 'a', 'b', 'c', 'd' ]

上面代码的扩展运算符,就调用内部Iterator接口

这提供了一种简便机制,可以将任何部署Iterator接口的数据结构,转为数组。

只要某个数据结构部署了Iterator接口,就可以对它使用扩展运算符,将其转为数组

const obj1 = {
    0:'a',
    1:'b',
    2:'c',
    length:3,
    [Symbol.iterator]:Array.prototype[Symbol.iterator]
};
let arr = [...obj1];
console.log(arr); // [ 'a', 'b', 'c' ]

3. yield*

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

const obj1 = {
    0:'a',
    1:'b',
    2:'c',
    length:3,
    [Symbol.iterator]:Array.prototype[Symbol.iterator]
};
let generator = function* () {
    yield 1;
    yield* obj1;
    yield 5;
};

for(let item of generator())
{
    console.log(item); // 1 a b c 5
}

4. 其他场合

数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口

  • for...of
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]])
  • Promise.all()
  • Promise.race()

字符串的Iterator接口

字符串是一个类似的数组对象,也原生具有Iterator接口。

var someString = "hi";
typeof someString[Symbol.iterator]
// "function"

var iterator = someString[Symbol.iterator]();

iterator.next()  // { value: "h", done: false }
iterator.next()  // { value: "i", done: false }
iterator.next()  // { value: undefined, done: true }

上面代码中,调用Symbol.iterator方法返回一个遍历器对象,在这个遍历器对象上可以调用next实现遍历。

可以覆盖原生的Symbol.iterator方法,达到修改遍历器行为的目的。

let str = new String("hi");
console.log([...str]); // [ 'h', 'i' ]

str[Symbol.iterator] = ()=>({
    next(){
        if (this._first) {
            this._first = false;
            return { value: "bye" };
        } else {
            return { done: true };
        }
    },
    _first:true
})
console.log([...str]); // [ 'bye' ]
console.log(str); // [String: 'hi']

上面代码中,字符串str的Symbol.iterator方法被修改了,所以扩展运算符返回的值变成bye,而字符串本身还是hi

Iterator接口与Generator函数

Symbol.iterator方法的最简单实现,还是使用Generator函数。

let obj = {
    * [Symbol.iterator]() {
        yield 'hello';
        yield 'world';
    }
};

for (let x of obj) {
    console.log(x);
}
// hello
// world

遍历器对象的return(),throw()

遍历器对象除了具有next方法,还可以有return方法和throw方法。

如果自己写遍历器对象生成函数,那么next方法是必须部署的,return方法和throw方法是可选部署的。

如果for...of循环提前退出(break,continue),就会调用return方法。

let str = "hello";
function readLinesSync() {
    let index=0;
    return {
        [Symbol.iterator]:()=>({
            next() {
                if(index<str.length)
                    return {value:str[index++]};
                else
                    return{done:true};
            },
            return() {
                console.log("执行return方法");
                return { done: true };
            }
        })
    };
};
for(let item of readLinesSync())
{
    console.log(item); // h 执行return方法
    break;
}

for...of

一个数据结构只要部署了Symbol.iterator属性,就被视为具有iterator接口,就可以用for...of循环遍历它的成员。

也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。

for...of循环可以使用的范围包括数组、Set、Map,某些类似数组的对象、Generator对象、字符串。

数组

数组原生具备iterator接口,for...of循环本质上就是调用这个接口产生的遍历器。

for...in循环,只能获得对象的键名,不能直接获取键值。

for...of循环,允许遍历获得键值。数组的遍历器接口只返回具有数字索引的属性。

let arr = [3, 5, 7];
arr.foo = 'hello';

for (let i in arr) {
  console.log(i); // "0", "1", "2", "foo"
}

for (let i of arr) {
  console.log(i); //  "3", "5", "7"
}

Generator

Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数不同。

Generator函数有多种理解,从语法上可以理解成一个状态机,封装了多个内部状态。

执行Generator函数会返回一个遍历器对象,除了状态机,还是一个遍历器对象生成函数。

返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态。

形式上,Generator函数是一个普通函数,但有两个特征

1. function关键字与函数名之间有*号

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

function* hello(){
    yield "hello";
    yield "world";
    return "ending";
}
var ho = hello();
console.log(ho.next()); // { value: 'hello', done: false }
console.log(ho.next()); // { value: 'world', done: false }
console.log(ho.next()); // { value: 'ending', done: true }

上面代码定义了一个Generator函数,它内部有两个yield表达式,该函数有三个状态。

Generator函数的调用方式和普通函数一样,是在函数名后面加上一对圆括号。

不同的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结束结果,而是一个指向内部状态的指针对象。

下一步,必须调用next方法,使得指针移动到下一个状态。

每次调用next方法, 内部指针就从函数头部或上一次停止的位置开始执行,直到遇见另一个yield表达式或return语句

换言之,Generator函数是分段执行,yield表达式是暂停执行的标记,而next方法可以恢复执行。

ES6没有规定function关键字和函数名之间的星号的位置,所以以下写法都可以通过。

function * foo(x, y) { ··· } function *foo(x, y) { ··· } function* foo(x, y) { ··· } function*foo(x, y) { ··· }

yield表达式

Generator函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,提供了一种可暂停的函数。yie表达式就是暂停标志。

遍历器对象的next方法运行逻辑如下

1. 遇到yield表达式,就暂停执行后面的操作,并返回对象属性value是,紧跟在yield后面的表达式的值。 

2. 下一次调用next方法时,再继续执行下去,直到遇到下一个yield表达式

3. 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式,作为返回对象的value属性。

4. 如果该函数没有return语句,则返回的对象的value属性值为undefined。

需要注意的是,yield表达式后面的表达式,只有当调用next方法时,才会执行。

return语句不具备位置记忆的功能,一个函数里,只能执行一次return语句,但可以执行多个yield。

function* f() {
  console.log('执行了!')
}

var generator = f();

setTimeout(function () {
  generator.next()
}, 2000);

Generator函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。

上面代码中,如果函数f是普通函数,在赋值的时候就会执行。但是f是Generator函数,只有调用next方法时,才会执行。

注意:yield表达式只能在Generator函数里面使用,在其他地方使用就会报错。

Generator与Iterator接口

Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性,从而使得对象具有Iterator接口

var myIterable = {};
myIterable[Symbol.iterator] = function* (){
    yield 1;
    yield 2;
    yield 3;
};
var result = [...myIterable];
console.log(result); // [1,2,3]

上面代码就是把Generator函数赋值给Symbol.iterator属性,让对象有了接口,就可以被...运算符遍历了。

Generator函数执行后,返回一个遍历器对象,该对象本身就具有Symbol.iterator属性,执行后返回自身。 

next方法参数

yield表达式本身没有返回值,next方法可以带一个参数,这个参数就被当做上一个yield的返回值。

function* f() {
    for (var i = 0; i < 5; i++) {
        var reset = yield i;
        if (reset) {
            break;
        }
    }
}
var g = f();
console.log(g.next()); //{ value: 0, done: false }
console.log(g.next(true)); //{ value: undefined, done: true }

这个功能的语法意义很重要,Generator函数从暂停状态到恢复执行,他的上下文是不变的。

通过next方法参数,就有办法在Generator函数开始运行之后,继续向函数体内注入值。

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

由于next方法的参数表示上一个yield表达式的返回值,所以第一次使用next方法时,不能带有参数。

V8引擎直接忽略第一次使用next方法的参数。

for...of循环

循环可以自动遍历Generator函数时生成的Iterator对象,不再需要调用next方法。

function* foo() {
    for (let i = 0; i < 5; i++) {
        yield i;
    }
}
for (let i of foo()) {
    console.log(i);
}

利用Generator函数和for...of循环,可以实现斐波那契数列

function* feibo() {
    let [prev, curr] = [0, 1];
    while (true) {
        [prev, curr] = [curr, prev + curr];
        yield curr;
    }
}
for (let n of feibo()) {
    if (n > 20)
        break;
    console.log(n);
}

利用for...of循环,可以写出遍历任意对象的方法,原生Javascript对象没有遍历接口,无法使用for...of,通过Generator就可以为他加上。

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

或者,我们可以使用Generator函数加到对象的Symbol.iterator属性上面

function* objectEntries() {
    let propKeys = Object.keys(this);
    for (let propKey of propKeys) {
        yield [propKey, this[propKey]];
    }
}
let jane = {first: "Jane", last: "Doe"};
jane[Symbol.iterator] = objectEntries;
for (let [key, value] of jane) {
    console.log(`${key}-${value}`);
}

除了for...of以外,扩展运算符 ... ,解构赋值 ,Array.from方法内部调用的,都是遍历器接口。

function* objectEntries() {
    let propKeys = Object.keys(this);
    for (let propKey of propKeys) {
        yield [propKey, this[propKey]];
    }
}
let jane = {first: "Jane", last: "Doe"};
jane[Symbol.iterator] = objectEntries;
for (let [key, value] of jane) {
    console.log(`${key}-${value}`);
}

Generator.prototype.throw()

Generator函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在Generator函数体内捕获

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log('内部捕获', e);
  }
};

var i = g();
i.next();

try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b

对象i连续抛出两个错误,第一个错误被Generator内部异常捕获,第二次因为内部的异常已经执行过了,所以就抛出外部的

Generator.prototype.return()

Generator函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历Generator函数

function* gen() {
    yield 1
    yield 2
}
const g = gen();
console.log(g.return("c")); // { value: 'c', done: true }
console.log(g.next()); // { value: undefined, done: true }

如果return不提供参数,则返回值value为undefined。

如果Generator函数里面有try...finally,则return方法会推迟到finally代码块执行完再执行。

yield * 表达式

如果在Generator函数里面调用另一个Generator函数,是没有效果的。

这个时候就需要使用yield * 表达式,用来在一个Generator函数里面执行另一个函数。

function * foo() {
    yield 1
    yield 2
}
function * bar() {
    yield 3
    yield* foo();
    yield 4
}
for (let v of bar()) {
    console.log(v);
}
// 3 1 2 4

作为对象属性的Generator函数

如果一个对象的一个属性是Generator函数,可以使用星号来表示函数,一下两个对象是等效的。

let obj = {
    * myGeneratorMethod(){
        yield 1
        yield 2
    }
}
let obj2 = {
    myGeneratorMethod: function*() {
        yield 1
        yield 2
    }
}
for (let v of obj.myGeneratorMethod()) {
    console.log(v);
}
for (let v of obj2.myGeneratorMethod()) {
    console.log(v);
}

Generator函数的this

Generator函数的返回的遍历器对象,是函数的实例,继承了prototype方法。

Generator应用

异步操作的同步化表达

Generator函数的暂停执行效果,意味着可以把异步操作写在yield表达式里面,等调用next方法时再往后执行。

相当于不需要写回调函数,异步操作的后续操作可以放在yield表达式下面,要等到调用next方法再执行。

所以Generator函数的一个重要实际意义就是用来处理异步操作,改写回调函数。

可以通过Generator函数逐行读取文本文件。下面代码使用yield表达式可以手动逐行读取文件。

function* numbers() {
  let file = new FileReader("numbers.txt");
  try {
    while(!file.eof) {
      yield parseInt(file.readLine(), 10);
    }
  } finally {
    file.close();
  }
}

控制流管理

如果一个多步操作非常耗时,可以使用Generator按次序自动执行所有步骤。

let steps = [step1Func, step2Func, step3Func];

function *iterateSteps(steps){
  for (var i=0; i< steps.length; i++){
    var step = steps[i];
    yield step();
  }
}

steps封装了一个任务的多个步骤,iterateSteps依次给这些步骤添加上yield命令。

分解完步骤后,还可以将项目分解成多个依次执行的任务。

let jobs = [job1, job2, job3];

function* iterateJobs(jobs){
  for (var i=0; i< jobs.length; i++){
    var job = jobs[i];
    yield* iterateSteps(job.steps);
  }
}

数组jobs封装了一个项目的多个任务,interateJobs则依次为这些任务添加上yield命令

最后用for...of循环一次性执行所有的步骤

for (var step of iterateJobs(jobs)){
  console.log(step.id);
}

Generator函数异步应用

异步就是一个任务不是连续完成的,先执行第一段,等做好准备,在执行第二段。

比如,任务是读取文件进行处理,第一段就是发送请求读取文件,等系统返回文件,在执行第二段处理文件。

这种不连续的操作,就称为异步操作。

回调函数

Javascript语言对异步编程的实现,就是用回调函数。把任务的第二部分单独写在一个函数里面。

重新执行这个任务,就直接调用这个函数。比如读取文件的操作。

fs.readFile('/etc/passwd', 'utf-8', function (err, data) { if (err) throw err; console.log(data); });

 redFile第三个参数,就是回调函数,也就是任务的第二段

Promise

回调函数本身并没有什么问题,但是如果出现多个回调函数嵌套,则会变成横向发展无法管理。

因为多个异步操作形成了强耦合,只要有一个操作需要为修改,它的上下层函数,就都要跟着改变。

这种情况,被称为回调函数地狱“callback hell”

使用Promise就是为了解决这个问题,允许将回调函数的嵌套,改为链式调用,连续读取多个文件。

var readFile = require('fs-readfile-promise');

readFile(fileA)
    .then(function (data) {
        console.log(data.toString());
    })
    .then(function () {
        return readFile(fileB);
    })
    .then(function (data) {
        console.log(data.toString());
    })
    .catch(function (err) {
        console.log(err);
    });

Promise写法是回调函数的改进版,使用then以后,异步任务会变得更加清晰。

Promise最大的缺点就是代码冗余,任务都被Promise包装了,语以变得不清晰。

Generator函数 

协程

协程,意思是多个线程相互协作,完成异步任务。

协程大致的流程如下:

1. 协程A开始执行。

2. 协程A执行到一半,进入暂停,执行权转移到协程B。

3. (一段时间后)协程B交还执行权。

4. 协程A恢复执行。

读取文件的协程写法

function *asyncJob() {
  // ...其他代码
  var f = yield readFile(fileA);
  // ...其他代码
}

asyncJob是一个协程,其中yield是关键。表示执行到此处,执行权将交给其他线程。yield是异步两个阶段的分界线。

协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。

它的最大优点,就是代码的写法非常像同步操作。

协程的Generator函数实现

Generator函数协程在ES6实现,最大特点就是交出函数的执行权(暂停执行)

整个Generator函数就是一个封装的异步函数,操作中需要暂停的部分都用yield注明。

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

调用Generator函数,返回一个内部指针g。调用g的next方法,会移动内部指针,指向第一个遇到的yield语句。

next方法是分阶段执行Generator函数,每次调用会返回一个对象。value表示yield后面表达式的值,done表示是否执行完毕。

异步任务的封装

function* gen(){
    var url = 'http://api.github.com/users/github';
    var result = yield fetch(url);
    console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
    console.log(data.json);
}).then(function(data){
    g.next(data)
}).catch(function(data){
    console.log(data);
});

Generator封装一个异步操作,先读取一个接口数据,然后用JSON返回。 

上面操作就是,执行Generator函数,返回一个遍历器对象,使用next方法,执行异步的第一个阶段。

Fetch返回的是一个Promise对象,因此then方法调用下一个next方法。

虽然上面的Generator函数将异步操作表示的很简洁,但是流程管理却不是很方便。

Thunk函数

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

求值策略,函数的参数到底应该何时求值。分为两种意见,示例如下:

var x = 1;
function f(m) {
  return m * 2;
}
f(x + 5)

1.传值调用,在进入函数体之前,就计算x+5的值(等于6),再将6传入函数里面。C语言采用这种策略

2.传名调用,将表达式x+5传入函数体,只有在用到它的时候求值。Haskell语言采用这种策略。

编译器的传名调用实现,往往是将参数放到一个临时函数中,再将这个临时函数传入函数体。这个函数就是Thunk函数。

function f(m) {
    return m * 2;
}

var thrun = function () {
    return x + 5;
}

function f(thrun) {
    return thrun() * 2;
}

Javascript语言中Thrun函数替换的不是表达式,而是多参函数,将其替换成一个只接受回调函数作为参数的单参数函数

// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);

fs模块的readFile方法是一个多参函数,参数分别为文件名和回调函数。经过转换它变成了一个单参函数,只接受回调函数作为参数。

这个单参版本就叫做Thunk函数。任何函数只要有回调函数,就能写成Thunk函数的形式。例如:

// ES5版本
var Thunk = function(fn){
  return function (){
    var args = Array.prototype.slice.call(arguments);
    return function (callback){
      args.push(callback);
      return fn.apply(this, args);
    }
  };
};

// ES6版本
  

使用上面的转换器,生成fs.readFile的Thunk函数。

var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);

//完整示例
function f(a, cb) {
  cb(a);
}
const ft = Thunk(f);

ft(1)(console.log) // 1

生产环境的转换器,建议使用Thunkify模块。

首先安装:npm install thunkify

使用方法如下:

var thunkify = require("thunkify");
var fs = require("fs");

var read = thunkify(fs.readFile);
read("package.json")(function(err,str){
    console.log(err);
    console.log(str.toString());
})

Thunk函数可以用于Generator函数的自动流程管理,可以自动执行

如果需要异步操作中,保证前一步执行完,才能执行后一步,就需要Thunk函数。

Generator函数封装了两个异步操作,yield命令用于将程序的执行权移出Generator函数。

Thunk函数可以在回调函数里面,将执行权交还给Generator函数。

var thunkify = require("thunkify");
var fs = require("fs");
var readFileThunk = thunkify(fs.readFile);

var gen = function* () {
    var r1 = yield readFileThunk("package.json");
    console.log(r1.toString());
    var r2 = yield readFileThunk("demo1.html");
    console.log(r2.toString());
}

var g = gen();
var r1 = g.next();
r1.value(function (err, data) {
    if (err) throw err;
    var r2 = g.next(data);
    r2.value(function (err, data) {
        if (err) throw err;
        g.next(data);
    });
})

g是Generator函数的内部指针,表示目前执行到哪一步。next方法负责将指针下移,返回该步的信息(value属性和done属性)

Generator函数的执行过程,其实就是将同一个回调函数,反复传入next方法的value属性。变成递归来自动完成这个过程。

Thunk函数真正的用途,在于自动执行Generator函数。下面就是基于Thunk函数的Generator执行器

var thunkify = require("thunkify");
var fs = require("fs");
var readFileThunk = thunkify(fs.readFile);

function run(fn) {
    var gen = fn();

    function next(err, data) {
        if (data) console.log(data.length);
        var result = gen.next(data);
        if (result.done) return;
        result.value(next);
    }

    next();
}

var g = function* () {
    var r1 = yield readFileThunk("package.json");
    var r2 = yield readFileThunk("demo1.html");
}

run(g);

run函数就是一个Generator函数的自动执行器,内部的next函数就是Thunk函数的回调函数。

next函数先将指针移到Generator函数的下一步gen.next,然后判断Generator函数是否结束result.done

如果没有结束,就将next函数再传入Thunk函数result.value,否则就直接退出。 

上面代码中,函数g封装了n个异步操作的读取文件操作,只要执行run函数,这些操作就会自动完成。

Co模块

co模块是TJ Holowaychuk开发的小工具,用于Generator函数的自动执行。co模块可以让你不用写Generator函数的执行器

Generator函数只要传入co函数,就会自动执行。co函数返回Promise对象,因此可以使用then方法添加回调函数。

var thunkify = require("thunkify");
var fs = require("fs");
var readFile = thunkify(fs.readFile);
var co = require("co");

var gen = function* () {
    var f1 = yield readFile("demo1.html");
    var f2 = yield readFile("package.json");
    console.log(f1.length);
    console.log(f2.length);
}

co(gen).then(function () {
    console.log('Generator 函数执行完成');
});

Co模块原理

Generator就是一个异步操作的容器,自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。

两种方法可以做到

1.回调函数,将异步操作包装成Thunk函数,在回调函数中交回执行权

2.Promise对象,将异步操作包装秤Promise对象,用then方法交回执行权

co模块就是将两种自动执行器包装成一个模块,使用co的前提条件是,Generator函数的yield命令后面,只能是Thunk函数或Promise对象

async函数

ES2017标准引入async函数

http://es6.ruanyifeng.com/#docs/async

Class基本语法

ES6引入了Class的概念,作为对象的模板可以使用class关键字定义类。

class相当于ES5的语法糖。下面示例来展示ES6写法的不同

//ES5写法
function Point(x, y) {
    this.x = x;
    this.y = y;
}

Point.prototype.toString = function () {
    return "重构后的" + this.x + "," + this.y;
}

var P = new Point(1, 2);
console.log(P.toString());

//ES6写法
class Point2 {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    toString() {
        return "重构后的" + this.x + "," + this.y;
    }
}

var P2 = new Point2(1, 2);
console.log(P2.toString());

上面代码定义了类,constructor是构造方法,this就是实例对象。定义类的方法时,不需要添加function关键字,方法之间不需要分隔符

使用的时候,直接对类进行new命令就可以了。prototype属性在class上面继续存在,所有的方法都定义在类的prototype属性上面。

在类的实例上调用方法,其实就是调用原型上的方法。prototype对象的constructor属性指向类本身。

类的内部所有定义的方法,都是不可枚举的,ES5定义的方法可以枚举。

Object.assign方法可以向类添加多个方法

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

Object.assign(Point.prototype, {
    toString() {
        console.log("ToString");
    },
    toValue() {
        console.log("ToValue");
    }
})

var P = new Point(1, 2);
P.toString();
P.toValue();

类的属性名,可以采用变量来定义。

names = "toString";

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

Object.assign(Point.prototype, {
    [names]() {
        console.log("ToString");
    },
    toValue() {
        console.log("ToValue");
    }
})

var P = new Point(1, 2);
P.toString();
P.toValue();

严格模式

类和模块的内部就是严格模式,不需要使用use strict。ES6整个语言都是严格模式

constructor方法

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

一个必须拥有constructor方法,如果没有显示定义,会默认添加一个空的。

constructor方法默认返回实例对象this,可以指向返回另一个对象。

类的实例对象

类必须使用new,否则会报错。属性除非显式定义在其本身(this)上,否则都会定义在原型上

类的表达式

和函数一样,类也可以使用表达式来定义。name属性是函数默认特性

const MyClass = class Me {
    getClassName() {
        return Me.name;
    }
}
let inst = new MyClass();
console.log(inst.getClassName());  //Me

上述代码中Me只在Class内部使用,指当前类。如果类的内部没有用到的话,可以省略Me写成

const MyClass = class { /* ... */ };

使用表达式可以写出立即执行的Class,person就是立即执行的类的实例

let person = new class{
    constructor(name){
        this.name = name;
    }
    sayName(){
        console.log(this.name);
    }
}("cxy");
person.sayName(); //cxy

类不存在变量提升的问题

私有方法和私有属性

通过命名来区分,_person是私有方法,Peroson是公有方法

this指向

类的方法内部如果含有this,默认指向类的实例。但是如果单独使用该方法很可能报错。

可以在构造方法中绑定this,这样就不会找不到了

或者直接使用箭头函数

class Logger {
    constructor() {
        this.printName = this.printName.bind(this);
        this.printName = (name = 'there') => {
            this.print(`Hello ${name}`);
        }
    }
}

Class的Generator方法

如果某个方法之前加上星号(*),就表示该方法是一个Generator函数。

const Foo = class{
    constructor(...args){
        this.args = args;
    }
    *[Symbol.iterator](){
        for(let arg of this.args){
            yield arg;
        }
    }
}
for(let item of new Foo(1,2,3)){
    console.log(item); //1,2,3
}

Class静态方法和静态属性

如果在一个方法前面加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用。称为静态方法

class Foo{
    static classMethod(){
        return "Hello";
    }
}
console.log(Foo.classMethod()); //Hello

如果静态方法包含this关键字,这个this指向类而不是实例 

Foo.prop = 1; 直接类后面.就可以创建静态属性

new.target属性 

如果构造函数不是通过new命令调用,new.target会返回undefined。类内部调用new.target,返回当前正在运行的Class。

function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

需要注意的是,子类继承父类时,new.target会返回子类。可以利用这个特点来写出抽象类

class Shape {
    constructor() {
        if (new.target == Shape) {
            throw new Error("本类不能实例化");
        }
    }
}

Class继承

class通过extends关键字实现继承

class Point {
}

class ColorPoint extends Point {
}

super表示父类的构造函数,可以新建父类的this对象。并且可以调用父类方法。类似于Net中的base关键字

class Shape {
    constructor(x, y) {
        if (new.target == Shape) {
            throw new Error("本类不能实例化");
        }
        this.x = x;
        this.y = y;
    }

    toString() {
        return `Shape x=${this.x},y=${this.y}`;
    }
}

class Point extends Shape {
    constructor(x, y, z) {
        super(x, y);
        this.z = z;
    }

    toString() {
        console.log(`Point z=${this.z}-${super.toString()}`);
    }
}

new Point(1, 2, 3).toString(); //Point z=3-Shape x=1,y=2

子类必须在构造函数中调用super方法,否则新建实例会报错。因为子类没有自己的this对象,是继承父类的this对象

不调用super就得不到this对象。

如果子类没有定义constructor方法,在继承状态下会默认添加如下代码。

class ColorPoint extends Point {
}

// 等同于
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }
}

需要注意,必须在使用this之前,调用super。否则会报错

Object.getPrototypeOf方法可以用来从子类上获取父类

可以用来判断是否继承

super关键字

super关键字既可以当做函数,也可以当做对象

当做函数的时候,表示父类的构造函数。ES6要求子类的构造函数必须执行一次super函数。

当做对象的时候,在普通方法中表示父类原型对象,在静态方法中指向父类。

class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

let b = new B();
b.m // undefined

定义在父类实例上的方法或属性是无法通过super调用的。定义在原型是可以的

class A {}
A.prototype.x = 2;

class B extends A {
  constructor() {
    super();
    console.log(super.x) // 2
  }
}

let b = new B();

ES6规定,通过super调用父类的方法时,方法内部的this指向子类。

Mixin模式的实现

Mixin指多个对象合成一个新的对象,新对象具有各个组成成员的接口,

function mix(...mixins) {
  class Mix {}

  for (let mixin of mixins) {
    copyProperties(Mix, mixin); // 拷贝实例属性
    copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
  }

  return Mix;
}

function copyProperties(target, source) {
  for (let key of Reflect.ownKeys(source)) {
    if ( key !== "constructor"
      && key !== "prototype"
      && key !== "name"
    ) {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}

上面的mix函数可以将多个对象合成一个类,使用的时候只要继承这个类即可

class DistributedEdit extends mix(Loggable, Serializable) {
  // ...
}

修饰器

提案阶段 http://es6.ruanyifeng.com/#docs/decorator

Module语法

ES6之前制定了一些模块加载方案,最主要的有CommonJS和AMD两种,前者用于服务器,后者用于浏览器。

ES6在语言层面上,实现了模块功能。完全可以取代CommonJS和AMD

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

CommonJS模块就是对象,输入时必须查找对象属性

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

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

上面代码的实质是整体加载fs模块,生成一个对象_fs,然后从这个对象上读取三个方法。

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

注意:使用import无法再js中进行调试代码,只能调试babel转换后的代码。

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

上面代码的实质是从fs模块中加载三个方法,其他不加载。这种加载称为“编译时加载”或静态加载,即ES6编译时就完成模块加载。

效率比CommonJS模块的加载高出很多,这也导致了没法引用ES6模块本身,因为他不是对象。

export命令

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

一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取。

如果你希望外部能够获取某个变量,必须使用export关键字输出该变量或者使用export{}输出变量,{}不可省略

//Demo2
export let firstName = "Demo2"
export let lastName = "js"
or
let firstName = "Demo2"
let lastName = "js"
export {firstName, lastName}

//Demo1
import {firstName,lastName} from "./Demo2";
console.log(`${firstName}-${lastName}`); //Demo2-js

优先推荐export{}这种输出方式,可以直接在脚本尾部清楚的看出。

export命令除了输出变量,还可以输出函数或类,例如

export function multiply(x, y) {
  return x * y;
};

可以使用as关键字对接口进行重命名。

// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};

// 写法三
var n = 1;
export {n as m};

另外,export语句输出的接口,与其对应的值是动态绑定的,通过接口可以取得模块内部实时的值

//Demo2
let foo = 'bar';
setTimeout(() => foo = 'baz', 500)
export {foo}

//Demo1
import {foo} from './Demo2'

setTimeout(() => console.log(foo), 100); //bar
setTimeout(() => console.log(foo), 501); //baz

这一点与CommonJS不同,CommonJS模块输出的值得缓存,不存在动态更新。

export命令只能存在于模块顶层,不能再函数里面写。

import命令

export定义模块对外接口后,其他JS文件就可以通过import加载

import命令接受一对大括号,指定其他模块需要导入的变量名,大括号的变量名必须与对外接口相同。

如果想设置别名,需要使用as关键字。

import { lastName as surname } from './profile';

impor的from是指定模块文件的位置,可以是相对路径,也可以是绝对路径。.js后缀可以省略。

如果只是模块名,不带有路径,那么必须有配置文件,告诉js引用模块的位置。

import命令有提升效果,会自动提升到模块头部,首先执行。

import语句会执行所有加载的模块,因此可以写成。但是不会输入任何值

import 'lodash';

目前,通过Babel转码,CommonJS模块的require命令和import命令可以共存

但是最好不这样做,因为import在静态解析阶段执行。是模块中最早执行的,会有可能出问题。

模块整体加载

可以通过*号来加载整个模块,所有输出值都在这个对象上面。

//Demo2
let firstName = "Demo2"
let lastName = "js"
export {firstName, lastName};

//Demo1
import * as Demo2 from './Demo2'

console.log(`${Demo2.firstName}.${Demo2.lastName}`); //Demo2.js

export default命令

export default命令,可以为模块指定默认输出。import可以为该模块提供任何名字。export default也可以用于非匿名函数 export default foo;

//Demo2
export default function(){
    console.log("foo");
}

//Demo1
import foo from './Demo2'
foo(); //foo

一个模块只能有一个默认输出。在import的时候注意不要加大括号

export与import复合写法

如果在一个模块中,先输入后输出同一个模块。import语句可以与export写在一起。

export { foo, bar } from 'my_module';

// 等同于
import { foo, bar } from 'my_module';
export { foo, bar };

模块的接口改名和整体输出,也可以这么写

// 接口改名
export { foo as myFoo } from 'my_module';

// 整体输出
export * from 'my_module';

修改默认接口的写法如下

export { es6 as default } from './someModule';

// 等同于
import { es6 } from './someModule';
export default es6;

也可以修改默认接口为显式接口

export { default as es6 } from './someModule';

模块的继承

模块之间也可以进行继承,

//Demo3
export function foo(){
    console.log("foo");
}

//Demo2
export * from './Demo3'
export default function fo() {
    console.log("fo");
}

//Demo1
import Demo2,* as Demo from './Demo2'
Demo2();
Demo.foo();

Module的加载实现

浏览器加载ES6模块,也使用<script>标签,但是必须加入type="module"属性。注意:只有谷歌浏览器支持ES6写法,import这种只有使用webpack进行打包才可以使用。

<script type="module" src="dist/Demo1.js"></script>

浏览器对于带有type="module"的script都是异步加载,不会造成堵塞浏览器,等同于打开了defer属性

如果页面有多个moudle会按照页面出现顺序依次加载。使用async属性,不会按照出现顺序加载。

ES6模块与CommonJS模块的差异

他们有两大重要差异

1.CommonJS模块输出的是一个值得拷贝,ES6模块输出的是值得引用

2.CommonJS模块是运行时加载,ES6模块时编译时输出接口

编程风格

let取代var,因为没有副作用

全局常量优先使用const

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

数组成员给变量赋值时,优先使用解构赋值

const arr = [1, 2, 3, 4];
// good
const [first, second] = arr;

函数的参数如果是对象成员,优先使用解构赋值

// good
function getFullName(obj) {
  const { firstName, lastName } = obj;
}

// best
function getFullName({ firstName, lastName }) {
}

如果函数返回多个值,优先使用对象的解构赋值,而不是数组赋值。这样便于以后添加返回值,以及更改返回值的顺序

// good
function processInput(input) {
  return { left, right, top, bottom };
}

const { left, right } = processInput(input);

单行定义对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。

// good
const a = { k1: v1, k2: v2 };
const b = {
  k1: v1,
  k2: v2,
};

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

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

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

如果属性名是动态的,可以在创建对象时,使用属性表达式定义。

// good
const obj = {
  id: 5,
  name: 'San Francisco',
  [getKey('enabled')]: true,
};

对象属性和方法尽量采用简洁写法,编译描述和书写。

使用扩展运算符拷贝数组

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

使用Array.from方法,将类似数组的对象转为数组。

const foo = document.querySelectorAll('.foo');
const nodes = Array.from(foo);

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

(() => {
  console.log('Welcome to the Internet.');
})();

需要使用函数表达式的场合,尽量用箭头函数替代。因为更简洁并且绑定了this

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

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

箭头函数取代了Function.prototype.bind不应再用self/_this/that绑定this

// bad
const self = this;
const boundMethod = function(...params) {
  return method.apply(self, params);
}

// acceptable
const boundMethod = method.bind(this);

// best
const boundMethod = (...params) => method.apply(this, params);

简单的、单行的、不会复用的函数,建议采用箭头函数。否则还是应该采用传统函数的写法

所有配置项都应集中在一个对象,放在最后一个参数,布尔值不可直接作为参数

// good
function divide(a, b, { option = false } = {}) {
}

不要再函数体内使用arugments变量,使用rest运算符(...)代替。

因为rest运算符显示表明你想要获取参数,而arugments是一个类似数组的对象,rest运算符可以提供一个真正的数组。

// good
function concatenateAll(...args) {
  return args.join('');
}

使用默认值语法设置函数参数的默认值 

// good
function handleThings(opts = {}) {
  // ...
}

如果需要key:value的数据结构,使用Map结构。因为Map内置遍历机制

let map = new Map(arr);

for (let key of map.keys()) {
  console.log(key);
}

for (let value of map.values()) {
  console.log(value);
}

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}

使用Class取代需要prototype的操作,因为Class的写法更简洁,更易于理解。

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

使用extends实现继承,这样更简单,不会有被破坏instanceof运算的危险。

// good
class PeekableQueue extends Queue {
  peek() {
    return this._queue[0];
  }
}

Module语法是JavaScript模块的标准写法,坚持使用这种写法。使用import取代require,使用export取代module.exports

// commonJS的写法
var React = require('react');

var Breadcrumbs = React.createClass({
  render() {
    return <nav />;
  }
});

module.exports = Breadcrumbs;

// ES6的写法
import React from 'react';

class Breadcrumbs extends React.Component {
  render() {
    return <nav />;
  }
};

export default Breadcrumbs;

如果模块只有一个输出值,就是用export default,如果有多个输出值就不使用。不要同时使用两个

不要在模块中使用通配符,这样可以确保你的模块中有一个默认输出

// bad
import * as myObject from './importModule';

// good
import myObject from './importModule';

如果模块默认输出一个函数,首字母小写

如果模块默认输出一个对象,首字母大写

ESLint 的使用

语法规则和代码风格检测工具,确保语法正确,风格统一

安装:npm i -g eslint

安装插件:

npm i -g eslint-config-airbnb
npm i -g eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react

根目录新建.eslintrc文件配置

{
  "extends": "eslint-config-airbnb" }

创建不符合规则的js

var unusued = 'I have no purpose!';

function greet() {
    var message = 'Hello, World!';
    alert(message);
}

greet();
index.js

使用ESLint检查:eslint index.js

$

转载于:https://www.cnblogs.com/chenxygx/p/6509564.html

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值