ES6简介

  1. ES6既是一个历史名词,也是一个泛指,含义是5.1版以后的JavaScript的下一代标准,涵盖了ES2015、ES2016、ES2017等等;
  2. ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 Jscript 和 ActionScript);

let和const命令

  1. let和const声明的变量只在自己的块作用域里有效:
var a = [];
//每一轮循环,i都是相同的变量
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

//每一轮循环,i都是一个新的变量
var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6
  1. let和const不存在变量提升:
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况,提前使用后面声明的变量会报错
console.log(bar); // 报错ReferenceError
let bar = 2;
  1. let和const的暂时性死区:
var tmp = 123;
if (true) {
  //只要使用const和let声明的变量,在该作用域里形成了封闭作用域,不受外部同名变量的影响
  tmp = 'abc'; // ReferenceError
  let tmp;
}

// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError: x is not defined
  1. let和const声明的变量不容许重复声明:
// 报错
function () {
  let a = 10;
  var a = 1;
}

// 报错
function () {
  let a = 10;
  let a = 1;
}

5.const声明一个只读的变量,声明时必须赋值,const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址不得改动

const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

6.ES6中,var命令和function命令声明的全局变量,依旧是顶层对象(global,window)的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从ES6开始,全局变量将逐步与顶层对象的属性脱钩。

7.global对象:

  • 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window。
  • 浏览器和 Web Worker 里面,self也指向顶层对象,但是Node没有self。
  • Node 里面,顶层对象是global,但其他环境都不支持。

解构赋值

  1. 数组的解构赋值
//普通解构
let [a, b, c] = [1, 2, 3];

//嵌套解构
let [foo, [[bar], baz]] = [1, [[2], 3]];

//部分解构
let [head, ...tail] = [1, 2, 3, 4];
=>  head // 1
=>  tail // [2, 3, 4]

//只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值
function* fibs() {
  let a = 0;
  let b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

let [first, second, third, fourth, fifth, sixth] = fibs();
=>  sixth // 5

//不具有Iterator接口的数据结构,使用数组解构会报错
let [foo] = NaN;
let [foo] = undefined;

//Set结构也可以使用解构
let [x, y, z] = new Set(['a', 'b', 'c']);

//解构时可以添加默认值
let [foo = true] = [];
=>  foo // true
let [x = 'c', y = 'b'] = ['a']; 
=>  // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; 
=>  // x='a', y='b'
  1. 对象的解构赋值
//普通解构,变量名字必须与对象的属性名字保持一致
let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"

//变量重命名,真正被赋值的是变量baz,而此时的foo只是一个匹配模式而已,而非变量
var { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"

//解决变量重名问题,表达式2必须用(),否则解析器会将其理解成代码块
let baz;
({bar: baz} = {bar: 1}); // 成功

//嵌套解构,此时的p不是变量,而是匹配模式
let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"

//解构时可以添加默认值,默认值生效的条件是严格等于undefined
var {x = 3} = {};
x // 3
var {x, y = 5} = {x: 1};
x // 1
y // 5
  1. 字符串的解构
//字符串可以当作数组进行解构
const [a, ...b] = 'hello';
a // "h"
b // ["e", "l", "l", "o"]
  1. 其它类型数据解构
//解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象,然后解构
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true

//由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
  1. 函数参数的解构
//函数参数的解构就当作普通传值就可以了
function add([x, y]){
  return x + y;
}
add([1, 2]); // 3 相当于 let [x, y] = [1, 2];

//{x = -1, y = -1}表示有参数的情况下,默认值解构,{ x: 0, y: 0 }表示默认参数
function move({x = -1, y = -1} = { x: 0, y: 0 }) {
  return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, -1]
move({}); // [-1, -1]
move(); // [0, 0]
  1. 解构的圆括号问题
//变量声明语句中,不能带有圆括号,以下全部报错
let [(a)] = [1];
let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};

//赋值语句中,不能将整个模式,或嵌套模式中的一层,放在圆括号之中
({ p: a }) = { p: 42 };
([a]) = [5];

//可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号
[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确

字符串的扩展

  1. 字符的 Unicode 表示法
//javascript采用Unicode表示法(\uxxxx)表示一个字符,但是该表示法只能表示\u0000~\uFFFF之间的字符,超过这个范围的字符必须用两个字节表示:
"\uD842\uDFB7"
// "?"
//用一个字节表示,识别错误
"\u20BB7"
// " 7"

//ES6对此作了改进,可以放在大括号里表示:
"\u{20BB7}"
// "?"
"\u{41}\u{42}\u{43}"
// "ABC"

//ES6中的字符串支持for...of循环遍历,并且可以正确识别大于0xFFFF的码点
var text = String.fromCodePoint(0x20BB7);
//普通的for循环无法识别
for (let i = 0; i < text.length; i++) {
  console.log(text[i]);
}
// " "  
// " "
//可以识别
for (let i of text) {
  console.log(i);
}
// "?"
  1. includes/startsWith/endsWith 函数
  • includes:返回布尔值,表示是否找到了参数字符串。
  • startsWith:返回布尔值,表示参数字符串是否在源字符串的头部。
  • endsWith:返回布尔值,表示参数字符串是否在源字符串的尾部。
//这三个方法都支持第二个参数,表示开始搜索的位置
var s = 'Hello world!';

s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true endsWith的行为与其他两个方法有所不同,它针对前n个字符
s.includes('Hello', 6) // false
  1. repeat 函数

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

'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""
  1. 模板字符串
//模板字符串中嵌入变量,需要将变量名写在${}之中
var x = 1;
var y = 2;

`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"

`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"

var obj = {x: 1, y: 2};
`${obj.x + obj.y}`
// 3

正则的扩展

  1. 正则的修饰符
  • g修饰符:全局匹配
  • i修饰符:不区分大小写
  • m修饰符:区分换行符,^和$可以匹配每行的开始和结尾
  • u修饰符(ES6):用来正确处理大于\uFFFF的Unicode字符
  • y修饰符: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
  1. 新的属性
//sticky属性:表示是否设置了y修饰符
var r = /hello\d/y;
r.sticky // true

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

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

数值的扩展

  1. 二进制和八进制表示法:
    ES6 提供了二进制和八进制数值的新写法,分别用前缀0b(或0B)和0o(或0O)表示
0b111110111 === 503 // true
0o767 === 503 // true
  1. Number.isFinite(), Number.isNaN()

它们与传统的全局方法isFinite()和isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,Number.isFinite()对于非数值一律返回false, Number.isNaN()只有对于NaN才返回true,非NaN一律返回false

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.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
  1. Number.parseInt(), Number.parseFloat()

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

Number.parseInt === parseInt // true
Number.parseFloat === parseFloat // true
  1. Number.isInteger()

Number.isInteger()用来判断一个值是否为整数。需要注意的是,在JavaScript内部,整数和浮点数是同样的储存方法,所以3和3.0被视为同一个值

Number.isInteger(25) // true
Number.isInteger(25.0) // true
  1. Number.isSafeInteger()

JavaScript能够准确表示的整数范围在-253到253之间(不含两个端点),超过这个范围,无法精确表示这个值,Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内

Number.isSafeInteger(3) // true
Number.isSafeInteger(1.2) // false
Number.isSafeInteger(9007199254740990) // true
Number.isSafeInteger(9007199254740992) // false
  1. Math增加的函数:
//Math.trunc方法用于去除一个数的小数部分,返回整数部分
Math.trunc(4.1) // 4
Math.trunc(4.9) // 4
Math.trunc(-4.1) // -4

//Math.sign方法用来判断一个数到底是正数、负数、还是零
// 参数为正数,返回+1;
// 参数为负数,返回-1;
// 参数为0,返回0;
// 参数为-0,返回-0;
// 其他值,返回NaN

//Math.cbrt方法用于计算一个数的立方根
Math.cbrt(-1) // -1
Math.cbrt(0)  // 0
Math.cbrt(1)  // 1
Math.cbrt(2)  // 1.2599210498948734

//Math.hypot方法返回所有参数的平方和的平方根
Math.hypot(3, 4);        // 5
Math.hypot(3, 4, 5);     // 7.0710678118654755
Math.hypot();            // 0

//Math.expm1(x)返回ex - 1,即Math.exp(x) - 1

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

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

//Math.log2(x)返回以2为底的x的对数。如果x小于0,则返回NaN
  1. 指数运算符
2 ** 2 // 4
2 ** 3 // 8

数组的扩展

  1. Array.from函数

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map),用于替代[].slice.call()方法

// arguments对象
function foo() {
  var args = Array.from(arguments);
  // ...
}

//只要部署了Iterator接口的数据结构,都可以使用该函数
Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']
let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']

//类数组的转换,必须有length属性的对象
Array.from({ length: 3 });
// [ undefined, undefined, undefined ]

//Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组
Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);
  1. Array.of函数

Array.of总是返回参数值组成的数组。如果没有参数,就返回一个空数组

Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]

//与Array构造函数的差别:
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
  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]
  1. 数组实例的find()和findIndex()

这两个函数接受一个判断函数,find返回第一个满足条件的元素,findIndex返回第一个满足条件元素的位置

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

[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) 
// 2
  1. 数组实例的fill()

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

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

//从第二个元素开始,到2号位置之前结束
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
  1. 数组实例的entries(),keys()和values()

ES6提供三个新的方法——entries(),keys()和values()——用于遍历数组。它们都返回一个遍历器对象,可以用for…of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历

  1. 数组实例的includes()

Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。该方法属于ES7,但Babel转码器已经支持

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

函数的扩展

  1. 函数参数默认值
//函数参数的默认值是惰性求值的
let x = 99;
function foo(p = x + 1) {
  console.log(p);
}
foo() // 100
x = 100;
foo() // 101

//函数的length属性:
//指定了默认值,函数的length属性将失效,length是参数第一个指定默认值之前的参数个数:
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
//rest参数也不会加入length长度:
(function(...args) {}).length // 0

//利用参数默认值可以指定某个参数不能省略:
function throwIfMissing() {
  throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided;
}
foo()
// Error: Missing parameter
  1. rest参数

ES6 引入 rest 参数(形式为“…变量名”),用于获取函数的多余参数

function push(array, ...items) {
  items.forEach(function(item) {
    array.push(item);
    console.log(item);
  });
}

var a = [];
push(a, 1, 2, 3)
  1. 扩展运算符

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

//rest参数和扩展运算符相结合使用
function push(array, ...items) {
  array.push(...items);  //扩展运算符
}

// 替代函数的apply方法
function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];
f(...args);

//数组合并新写法:
let newArr = [...arr1, ...arr2, ...arr3];

//解构赋值:
let [a, ...rest] = list

//扩展运算符内部调用的是数据结构的Iterator接口,因此只要具有Iterator接口的对象,都可以使用扩展运算符,比如Map结构
let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]

//generator函数的扩展运算符快
var go = function*(){
  yield 1;
  yield 2;
  yield 3;
};
[...go()] // [1, 2, 3]
  1. 严格模式

ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错

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

// 报错
const doSomething = function ({a, b}) {
  'use strict';
  // code
};
  1. name属性

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

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

//将匿名函数赋值给变量,返回变量名字
var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"

//将具名函数赋值给变量,返回该具名函数的名字
const bar = function baz() {};
bar.name // "baz"

//Function构造函数返回的函数实例,name属性的值为anonymous。
(new Function).name // "anonymous"

//bind返回的函数,name属性值会加上bound前缀。
function foo() {};
foo.bind({}).name // "bound foo"
  1. 箭头函数
//返回对象的两种方法
var getTempItem1 = id => ({ id: id, name: "Temp" });
var getTempItem2 = id => {return { id: id, name: "Temp" }};

//不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误
//不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用rest参数代替
//不可以使用yield命令,因此箭头函数不能用作Generator函数
  1. 尾调用和尾递归优化

ES6 的尾调用优化只在严格模式下开启,正常模式是无效的

//尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了
//m(x)和n(x)都属于尾部调用,可以优化
function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}

//保留n次调用记录
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
  1. 函数参数尾逗号

ES2017 允许函数的最后一个参数有尾逗号(trailing comma)

function clownsEverywhere(
  param1,
  param2,
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar',
);

对象的扩展

  1. 对象属性的简洁表示方法:
var birth = '2000/01/01';
var Person = {
  name: '张三',
  //等同于birth: birth
  birth,
  // 等同于hello: function ()...
  hello() { console.log('我的名字是', this.name); }
};
  1. Object.is

ES5比较两个值是否相等,只有两个运算符:相等运算符()和严格相等运算符(=)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0
Object.is大部分行为和===一致,但是会解决NaN和+0/—0的问题:

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

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

Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)
Object.assign不能复制源对象继承的值

//为对象指定默认值:
const DEFAULTS = {
  logLevel: 0,
  outputFormat: 'html'
};
function processContent(options) {
  options = Object.assign({}, DEFAULTS, options);
  console.log(options);
  // ...
}
  1. 属性遍历
  • for…in

for…in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。

  • Object.keys(obj)

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

  • Object.getOwnPropertyNames(obj)

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

  • Object.getOwnPropertySymbols(obj)

Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有Symbol属性。

  • Reflect.ownKeys(obj)

Reflect.ownKeys返回一个数组,包含对象自身的所有属性,不管属性名是 Symbol 或字符串,也不管是否可枚举。

  1. Object.keys(),Object.values(),Object.entries()

ES2017在Object.keys的基础上提供了Object.values(),Object.entries()函数,这三个函数都返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性,不包含Symbol值的属性

Symbol

  1. 基本概念:
//Symbol不能使用new操作符,返回的是一种新的类型
let s = Symbol();
typeof s   // 'symbol'

//Symbol可以作为对象的属性,可以保证每个属性的名字独一无二
let s1 = Symbol();
let s2 = Symbol();
let obj = {
    [s1]: "1",
    [s2]: "2"
}
obj[s1] // "1"
obj[s2] // "2"

//Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分
var s1 = Symbol('foo');
var s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"

//独一无二
// 没有参数的情况
var s1 = Symbol();
var s2 = Symbol();
s1 === s2 // false
// 有参数的情况
var s1 = Symbol('foo');
var s2 = Symbol('foo');
s1 === s2 // false

//Symbol不能与其它类型进行运算
var sym = Symbol('My symbol');
"your symbol is " + sym
// TypeError: can't convert symbol to string
`your symbol is ${sym}`
// TypeError: can't convert symbol to string

//Symbol的值可以显示转换字符串,或者转换为布尔值
var sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'

var sym = Symbol();
Boolean(sym) // true
!sym  // false
  1. Symbol的应用:
//Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的。
log.levels = {
  DEBUG: Symbol('debug'),
  INFO: Symbol('info'),
  WARN: Symbol('warn')
};
  1. Object.getOwnPropertySymbols:

Symbol 作为属性名,该属性不会出现在for…in、for…of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有Symbol属性名

  1. Reflect.ownKeys

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

  1. Symbol.for(),Symbol.keyFor

Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值

Symbol.for("bar") === Symbol.for("bar")
// true

Symbol("bar") === Symbol("bar")
// false

Symbol.keyFor方法返回一个已登记的 Symbol 类型值的key:(只有Symbol.for才会登记)

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

var s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
  1. Symbol.hasInstance

对象的Symbol.hasInstance属性,指向一个内部方法。当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法

class Even {
  static [Symbol.hasInstance](obj) {
    return Number(obj) % 2 === 0;
  }
}
//相当于调用EVEN[Symbol.hasInstance](1)
1 instanceof Even // false
2 instanceof Even // true
12345 instanceof Even // false
  1. Symbol.isConcatSpreadable

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

//undefined表示可展开
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']
  1. Symbol.species

对象的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);

//修改对象的构造函数后,指向Array
mapped instanceof MyArray // false
mapped instanceof Array // true
  1. Symbol.match/Symbol.replace/Symbol.search/Symbol.split

这几个属性分别在调用match,replace,search,split函数时,如果对象有该属性,会优先调用该属性,返回对应函数的返回值:

String.prototype.search(regexp)
// 等同于
regexp[Symbol.search](this)
class MySearch {
  constructor(value) {
    this.value = value;
  }
  [Symbol.search](string) {
    return string.indexOf(this.value);
  }
}
'foobar'.search(new MySearch('foo')) // 0
  1. Symbol.iterator
    对象的Symbol.iterator属性,指向该对象的默认遍历器方法:
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};
[...myIterable] // [1, 2, 3]
  1. Symbol.toPrimitive

对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值
Symbol.toPrimitive被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式:

  • Number:该场合需要转成数值
  • String:该场合需要转成字符串
  • 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' //当作default用
obj == 'default' // true  //当作default用
String(obj) // 'str' //显式调用
  1. Symbol.toStringTag

对象的Symbol.toStringTag属性,指向一个方法。在该对象上面调用Object.prototype.toString方法时,如果这个属性存在,它的返回值会出现在toString方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object]或[object Array]中object后面的那个字符串:

// 例一
({[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和Map数据结构

  1. set的基本概念

set类似于数组,但是其中的元素不重复,使用严格(===)判断,NaN和NaN是同一个元素,所有对象都是不同的元素

//set构造函数:
const set = new Set([1, 2, 3, 4, 4]);
const s = new Set();

//add函数,添加元素,返回set本身
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
s.add(1).add(2).add(2);
//delete 删除某个元素,返回是否删除成功
s.delete(2);
//has函数,返回是否包含某个元素
s.has(2) // false

//size属性返回set的长度
s.size //4
  1. set支持的方法:(set中键名和键值相等)
  • keys():返回键名的遍历器
  • values():返回键值的遍历器
  • entries():返回键值对的遍历器
  • forEach():使用回调函数遍历每个成员
  • map():返回一个新的数组
  • filter():返回一个新的数组

以上方法都不会改变set结构本身,想在遍历过程中同步set,可以通过以下方式实现:

// 方法一
let set = new Set([1, 2, 3]);
set = new Set([...set].map(val => val * 2));
// set的值是2, 4, 6

// 方法二
let set = new Set([1, 2, 3]);
set = new Set(Array.from(set, val => val * 2));
// set的值是2, 4, 6
  1. WeakSet概念

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与Set的区别:

  • WeakSet 的成员只能是对象,而不能是其他类型的值
  • WeakSet的成员是对象的弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用
  • WeakSet不存在size属性,因为它的元素随时都可能消失
  • WeakSet不存在for,filter,map函数
  1. Map的基本概念

Map是键值对的集合,比普通对象更适合作为键值,Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题。

同名属性碰撞是指:只有对同一对象的引用(引用类型),Map结构才会视为同一个键(键是复杂类型的值),而如果Map的键是一个简单类型的值(数字,字符串,布尔值),只要两个值严格相等,就视为同一个键

let arr1 = [1], arr2 = [1];
let map = new Map();
map.set(arr1, 1);
map.set(arr2, 2); //后者不会覆盖前者

let obj = {};
obj[arr1] = 1;
obj[arr2] = 2; //后者会覆盖前者


//键值对的数组构造函数
const map1 = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);

//无参构造函数:
const map2 = new Map();
map2.set('name', 'zhangsan');
map2.set({a: 1}, "lisi");

//Map的函数和属性
map1.size  // 2
map1.set('a': 1) //返回set对象本身
map1.get('a') //返回属性对应的值
map1.has("a") //表示是否属性是否存在
map1.clear() //清除所有键值
map1.delete('a') //删除对应的键
  1. Map的遍历器:
  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。
  • forEach():遍历 Map 的所有成员。

Map不支持map,filter等函数

//value,key分别是键和值,map是map本身
map.forEach(function(value, key, map) {
  console.log("Key: %s, Value: %s", key, value);
});

const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
  1. Map与JSON之间的转换

因为JSON中key不容许是对象,所以将map转换为JSON之前,最好先将Map转换为数组,然后再转换为JSON;

//map -> json
function mapToArrayJson(map) {
  return JSON.stringify([...map]);
}
// json -> map
function jsonToMap(jsonStr) {
  return new Map(JSON.parse(jsonStr));
}
  1. WeakMap的基本概念
  • WeakMap与WeakSet类似,WeakMap弱引用的只是键名,而不是键值。键值依然是正常引用,键值只能是对象
  • WeakMap只有四个方法可用:get()、set()、has()、delete()

Proxy(代理)

  1. 概述

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

Proxy构造函数:Proxy接受两个参数。第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有Proxy的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作

//比配置对象有一个get方法,用来拦截对目标对象属性的访问请求。get方法的两个参数分别是目标对象和所要访问的属性
let proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});

//proxy属性可以实现继承
let obj = Object.create(proxy);
obj.time // 35

常用代理拦截器:

  • get(target, propKey, receiver)
  • set(target, propKey, value, receiver)
  • has(target, propKey) 拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey) 拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target) 拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy),返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getPrototypeOf(target) 拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target) 拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto) 拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。
  • apply(target, object, args) 拦截Proxy实例作为函数调用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。
  • construct(target, args) 拦截Proxy实例作为构造函数调用的操作,比如new proxy(…args)。

如果一个属性不可配置(configurable)和不可写(writable),则该属性不能被代理,通过 Proxy 对象访问该属性会报错

const target = Object.defineProperties({}, {
  foo: {
    value: 123,
    writable: false,
    configurable: false
  },
});

const handler = {
  get(target, propKey) {
    return 'abc';
  }
};

const proxy = new Proxy(target, handler);

proxy.foo
// TypeError: Invariant check failed
  1. Proxy.revocable 方法返回一个可取消的 Proxy 实例

Proxy.revocable方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例:

//当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误
let target = {};
let handler = {};

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

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

revoke();
proxy.foo // TypeError: Revoked
  1. this问题

虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理:

const _name = new WeakMap();

class Person {
  constructor(name) {
    _name.set(this, name);
  }
  get name() {
    return _name.get(this);
  }
}

const jane = new Person('Jane');
jane.name // 'Jane'

const proxy = new Proxy(jane, {});
proxy.name // undefined

Reflect

  1. 概述

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

  • 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上
  • Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础
  1. 静态方法

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

  • Reflect.apply(target,thisArg,args)
  • Reflect.construct(target,args)
  • Reflect.get(target,name,receiver)
  • Reflect.set(target,name,value,receiver)
  • Reflect.defineProperty(target,name,desc)
  • Reflect.deleteProperty(target,name)
  • Reflect.has(target,name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)
  1. 是用Reflect实现观察者模式:
//只要修改了属性,就会调用queuedObservers中的所有函数
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

  1. 缺点:
  • 首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消;
  • 其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部;
  • 第三,当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成);
  1. Promise新建后立即执行
//Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以Resolved最后输出
let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('Resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// Resolved

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

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

console.log('one');

// one
// two
// three
  1. Promise的resolve函数的参数可以是另外一个Promise
//上面代码中,p1是一个Promise,3秒之后变为rejected。p2的状态在1秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了2秒,p1变为rejected,导致触发catch方法指定的回调函数
var p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000)
})

var p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000)
})

p2
  .then(result => console.log(result))
  .catch(error => console.log(error))
  1. 未捕获异常的处理

Node 有一个unhandledRejection事件,专门监听未捕获的reject错误:

//第二个参数是报错的Promise实例
process.on('unhandledRejection', function (err, p) {
  console.error(err.stack)
});
  1. Promise.resolve函数

将现有对象转为Promise对象,有以下两个特殊情况:

//1.如果参数是Promise实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例

//2.如果参数是一个thenable对象,方法会将这个对象转为Promise对象,然后就立即执行thenable对象的then方法
let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

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

Iterator

  1. 在ES6中,有三类数据结构原生具备Iterator接口:数组、某些类似数组的对象、Set和Map结构
  2. 自己定义iterator接口:
var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++], done: false} :
        {value: undefined, done: true};
    }
  };
}
  1. Iterator 试用场所:
  • for…of循环
  • 解构赋值
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'];
  • 扩展运算符
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']
  • 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 }
  • 计算生成的数据结构

有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6的数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。

- entries() 返回一个遍历器对象,用来遍历[键名, 键值]组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 Iterator 接口,默认就是调用entries方法。
- keys() 返回一个遍历器对象,用来遍历所有的键名。
- values() 返回一个遍历器对象,用来遍历所有的键值。
  1. 遍历器对象的return方法

return方法的使用场合是,如果for…of循环提前退出(通常是因为出错,或者有break语句或continue语句),就会调用return方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法:

function readLinesSync(file) {
  return {
    next() {
      if (file.isAtEndOfFile()) {
        file.close();
        return { done: true };
      }
    },
    return() {
      file.close();
      return { done: true };
    },
  };
}

Generator函数

  1. 概述
  • 书写规则
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· } //常用写法
function*foo(x, y) { ··· }

//yield只能是用在generator函数中:
var flat = function* (a) {
  a.forEach(function (item) {
    if (typeof item !== 'number') {
      yield* flat(item);  //普通函数中是用forEach函数会报错,forEach应该是用
    } else {
      yield item;
    }
  });
};

//yield表达式如果用在另一个表达式之中,必须放在圆括号里面
function* demo() {
  console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError
  console.log('Hello' + (yield)); // OK
  console.log('Hello' + (yield 123)); // OK
}

//Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身
function* gen(){
  // some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
  • 何时执行:只有调用next函数才会执行函数体,遇到最近一个yield暂停
  • next方法参数:

yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值

function* f() {
  for(var i = 0; true; i++) {
    var reset = yield i;
    if(reset) { i = -1; }
  }
}

var g = f();

g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
  1. Generator.prototype.throw2()

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

throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法:

var gen = function* gen(){
  try {
    yield console.log('a');
  } catch (e) {
    // ...
  }
  yield console.log('b');
  yield console.log('c');
}

var g = gen();
g.next() // a
g.throw() // b
g.next() // c

Generator函数体内抛出的错误,也可以被函数体外的catch捕获:

function* foo() {
  var x = yield 3;
  var y = x.toUpperCase();
  yield y;
}

var it = foo();

it.next(); // { value:3, done:false }

try {
  it.next(42);
} catch (err) {
  console.log(err);
}

一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next方法,将返回一个value属性等于undefined、done属性等于true的对象:

function* g() {
  yield 1;
  console.log('throwing an exception');
  throw new Error('generator broke!');
  yield 2;
  yield 3;
}

function log(generator) {
  var v;
  console.log('starting generator');
  try {
    v = generator.next();
    console.log('第一次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误1', v);
  }
  try {
    v = generator.next();
    console.log('第二次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误2', v);
  }
  try {
    v = generator.next();
    console.log('第三次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误3', v);
  }
  console.log('caller done');
}

log(g());
// starting generator
// 第一次运行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉错误2 { value: 1, done: false }
// 第三次运行next方法 { value: undefined, done: true }
// caller done
  1. Generator.prototype.return()

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

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

g.next()        // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next()        // { value: undefined, done: true }
  1. yield*

如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据

function *foo() {
  yield 2;
  yield 3;
  return "foo";
}

function *bar() {
  yield 1;
  var v = yield *foo();
  console.log( "v: " + v );
  yield 4;
}

var it = bar();

it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
  1. co库

被co包装的generator函数只能yield 函数,promise, generator,数组,和对象,不能yield 单个数值
如何使用co库:

let co = require('co');
function gen(a, b){
    yield c = Promise.resolve(a + b);
    return c + 5;
}

//方法1:
co(gen, 3, 4);

//方法2:
let newGeo = co.wrap(gen);
newGeo(3, 4);

async

async是generator函数的语法糖,是generator函数的改进:

//普通函数前加上async,所有的异步调用前加上await,该函数直接返回一个promise
var asyncReadFile = async function () {
  var f1 = await readFile('/etc/fstab');
  var f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
//co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)

yield和await并发执行:

//同步执行,效率低
let foo = await getFoo();
let bar = await getBar();
//如果两个函数执行不分前后顺序,可以使用下面两种方法,实现异步执行:
// 方法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 方法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

异步generator函数:

//yield (await file.readLine()),有yield的地方就是next函数停下来的地方,yield后面加了await关键字,只有等待这行完这个promise,next才会返回
async function* readLines(path) {
  let file = await fileOpen(path);

  try {
    while (!file.EOF) {
      yield await file.readLine();
    }
  } finally {
    await file.close();
  }
}

Generator函数与async函数的区别:

  • 将*替换为了async且async提到了function的前面,表示函数中有异步操作
  • 将yield替换为了await,表示紧跟后面的表达式需要等待结果
  • async函数自带了执行器,不需要像Generator一样需要next方法才能执行
  • yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)
  • async返回的是一个Promise对象,Generator函数返回的是一个遍历器对象

  1. 类内部定义的所有方法是不可枚举的:
class Point {
  constructor(x, y) {
    // ...
  }

  toString() {
    // ...
  }
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
  1. constructor函数

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加

class Point {
}
// 等同于
class Point {
  constructor() {}
}
//类必须使用new调用,否则会报错
class Foo {
  constructor() {
    return Object.create(null);
  }
}
Foo()
// TypeError: Class constructor Foo cannot be invoked without 'new'
  1. 类不存在变量提升
new Foo(); // ReferenceError
class Foo {}
  1. 私有方法定义

可以将私有函数的名字定义为一个Symbol,因为Symbol的唯一性,这样第三方无法获取到:

const bar = Symbol('bar');
const snaf = Symbol('snaf');

export default class myClass{

  // 公有方法
  foo(baz) {
    this[bar](baz);
  }

  // 私有方法
  [bar](baz) {
    return this[snaf] = baz;
  }
  // ...
};
  1. 类name属性返回类名本身
  2. 取值和存值函数:

改变类内部属性的存取行为

class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'
  1. 静态方法

静态方法只能在通过类名调用,不会被类的实例继承

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

Foo.classMethod() // 'hello'

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

ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数

//可以确保构造函数只能通过new调用
function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 生成实例');
  }
}

var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三');  // 报错

//子类继承父类时,new.target会返回子类
class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error('本类不能实例化');
    }
  }
}

class Rectangle extends Shape {
  constructor(length, width) {
    super();
    // ...
  }
}

var x = new Shape();  // 报错
var y = new Rectangle(3, 4);  // 正确
  1. 继承
  • 子类继承父类必须有super函数调用:
//子类的constructor方法没有调用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; // 正确
  }
}
  • 继承关系的判断:
Object.getPrototypeOf(ColorPoint) === Point
// true
  • super
//super()当作函数调用,只能使用在子类的构造函数中,并且super内部的this指针指向子类:
class A {
  constructor() {
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A() // A
new B() // B

//super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

let b = new B();
b.m // undefined 上面代码中,p是父类A实例的属性,super.p就引用不到它

//ES6 规定,通过super调用父类的方法时,super会绑定子类的this
class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  m() {
    super.print();
  }
}

let b = new B();
b.m() // 2
  • 继承的实质:
class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
  • ES6允许继承原生构造函数定义子类,因为ES6是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承
    Boolean() Number() String() Array() Date() Function() RegExp() Error() Object()
//这意味着,ES6 可以自定义原生数据结构(比如Array、String等)的子类,这是 ES5 无法做到的
class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1

arr.length = 0;
arr[0] // undefined

Decorator:

修饰器(Decorator)是一个函数,用来修改类的行为,修饰器本质就是编译时执行的函数

@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

//修饰器的应用1:修饰类
function testable(isTestable) {
  return function(target) {
    target.isTestable = isTestable;
  }
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

//修饰器的应用2:修饰类的方法:
//第一个参数是所要修饰的目标对象,第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象
function readonly(target, name, descriptor){
  descriptor.writable = false;
  return descriptor;
}

class Person {
  @readonly
  name() { return `${this.first} ${this.last}` }
}

//如果同一个方法有多个修饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行
function dec(id){
    console.log('evaluated', id);
    return (target, property, descriptor) => console.log('executed', id);
}

class Example {
    @dec(1)
    @dec(2)
    method(){}
}
// evaluated 1
// evaluated 2
// executed 2
// executed 1

//修饰器不能修饰函数,因为存在函数提升
var counter = 0;
var add = function () {
  counter++;
};
@add
function foo() {
}
//相当于下面的定义:
@add
function foo() {
}
var counter;
var add;
counter = 0;
add = function () {
  counter++;
};

模块

  1. CommonJS模块和ES6模块的却别

差异:

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用;
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口;
  • ES6输入的模块变量只是一个“符号链接”,所以这个变量是只读的,不能对其进行重新赋值;