ES6基础(内含Promise、Symbol、Set、Map等基本使用)

1、let 和 const 命令
1、let 命令

基本用法

let 用于变量声明,类似于var,但其声明的变量只在let命令所在的代码块内有效

{
  var a = 10;
	let b = 5;
}

console.log(a); // 10
console.log(b); // b is not defined;

for循环的计数器

for (let i = 0; i < 10; i ++) {
	// todo
}
console.log(i); // i is not defined;

由此可见,i只在for循环体内有效,循环外引用报错

如下代码,如果使用var ,输出的会是10;

var a = [];
for(var i = 0; i < 10; i ++) {
	a[i] = function() {
		console.log(i);
	}
}
a[6](); // 10; var定义的变量为全局变量,console.log(i)即为全局的i,最后一次循环完后i的值为10;

但如果使用let定义,输出的会是6;

var a = [];
for(let i = 0; i < 10; i ++) {
	a[i] = function() {
		console.log(i);
	}
}
a[6](); // 6 let定义的i,每次循环都只在本轮循环中生效

另外,for循环有个特别之处,设置循环变量的部分是一个父级作用域,而循环体内的部分是一个单独的子作用域

for (let i = 0; i < 3; i ++) {
	let i = 'abc';
  console.log(i);
}
// 输出3遍 abc, 说明循环变量i和循环体内i在不同的作用域,有各自单独的作用域

不存在变量提升

var 定义变量会存在变量提升

var i = 10;
function fn() {
	console.log(i);
  var i = 5;
};
fn(); // undefined; 存在变量提升 => var i; console.log(i); i = 5;

let 命令改变了这种语法,let声明的变量一定要在声明后使用,否则报错

let i = 10;
function fn() {
	console.log(i);
  let i = 5;
};
fn(); // 直接报错

暂时性死区

只要块级作用域存在let命令,它所声明的变量就绑定这个区域,不再受外部影响;

var a = 123;
function fn() {
	a = 456; 
	let a;
}
fn(); // 直接报错,块级作用域中let声明变量之前进行赋值导致报错

总之,在let声明变量之前,该变量都是不可用的,这在语法上,称为“暂时性死区(TDZ)”;

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

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

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

不允许重复声明

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

function fn() {
	let a = 10;
  var a = 5;  // 报错
}

function fn() {
  let a = 10;
  let a = 5; // 报错
}
function func(arg) {
  let arg;
}
func() // 报错

function func(arg) {
  {
    let arg;
  }
}
func() // 不报错,声明变量不在同一作用域内
2、块级作用域

为什么需要块级作用域?

ES5只有全局作用域和函数作用域,没有块级作用域,带来许多不合理的场景

第一种场景,内层变量可能会覆盖外层变量

var tmp = new Date();

function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}

f(); // undefined 原因在于变量提升,内层的变量覆盖了外层的变量

第二种场景,用来计数的循环变量泄漏为全局变量

var s = 'hello';
for(var i = 0; i < s.length; i ++) {
  console.log(s[i]);
}
console.log(i); // 5

ES6的块级作用域

let实际上为Javascript提供了块级作用域

function fn () {
	let a = 10;
  if (true) {
      let a = 5;
  }
  console.log(i);// 5 外层代码块不受内层代码块影响,如果用var 声明,则会输出10;
}

内层作用域可以定义外层作用域的同名变量。

{{{{
  let insane = 'Hello World';
  {let insane = 'Hello World'}
}}}};

块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。

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

// 块级作用域写法
{
  let tmp = ...;
  ...
}
3、const 命令

基本用法

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

const PI = 3.1415;
console.log(PI); // 3.1415

PI = 4; // 报错

const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。

const foo;
// SyntaxError: Missing initializer in const declaration

const 的作用域与let一样,只在定义变量所在的作用域内有效

if (true) {
	const a = 7;
}
console.log(a); // 报错

const 声明的变量同样不存在变量提升,存在暂时性死区,只能声明后使用,不可重复声明;

本质

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

const foo = {};
foo.prop = 'abc'; // 为其添加属性,成功

foo = {}; // 将foo指向另外一个对象,报错

另一个例子

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

如果真的想把对象冻结,可以使用Object.freeze()方法

const foo = Object.freeze({});

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

除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

var constantize = (obj) => {
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, i) => {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  });
};
4、顶层对象的属性

顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。

window.a = 1;
a // 1

a = 2;
window.a // 2

ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined
2、变量的解构赋值
1、数组的解构赋值

基本用法

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

以前,为变量赋值,只能直接指定值

let a = 1;
let b = 2;
let c = 3;

ES6允许写成下边这样

let [a, b, c] = [1, 2, 3]

本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,等式左边就会被赋予相应的值

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  如果解构不成功,值就为undefined
z // []

另一种情况是不完全解构,即等号左边的模式,只匹配一部分等号右边的值数组,也可解构成功

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

let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4

默认值

解构赋值允许指定默认值

let [foo = true] = [];
foo; // true

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

注意⚠️:ES6内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined时,默认值才会生效

let [x = 1] = [undefined] // x = 1

let [x = 1] = [null] // null, 因为null 不严格等于 undefined

如果默认值是一个表达式,那么这个表达式是惰性求值的,只有在用到的时候,才会求值

function fn() {
	console.log('aaa');
}
let [a = fn()] = [1]  // a = 1,因为 a 能取到值,所以fn()不会求值

默认值可以引用解构赋值的其他变量,但该变量必须已经声明

let [x = 1, y = x] = [];     // x=1; y=1
let [x = 1, y = x] = [2];    // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = [];     // ReferenceError: y is not defined 当时y还没有声明
2、对象的解构赋值

基本用法

对象的解构与数组有个很重要的不同。数组的元素是按照次序排列的,元素的取值是由他的位置决定;而对象的属性没有次序,变量与属性必须同名,才能取到正确的值

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

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

对象的解构赋值,可以很方便的把现有的方法,赋值到某个变量

let {sin, cos, log} = Math;

const {log} = console;
log('hello'); // hello

如果变量名与属性名不一致

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

这实际说明,对象的解构赋值是下面形式的简写

let {foo: foo, bar: bar} = {foo: 'aaa', bar: 'bbb'};
// 也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

下面是赋值嵌套的例子

let obj = {};
let arr = [];

({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });

obj // {prop:123}
arr // [true]

注意,对象的解构赋值可以取到继承的属性

const obj1 = {};
const obj2 = {foo: 'aaa'};
Object.setPrototypeOf(obj1, obj2);

const {foo} = obj1;
foo; // aaa

默认值

默认值生效的条件是,对象的属性值严格等于undefined

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

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

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

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

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

注意点

1、如果要将一个已经声明的变量用于解构赋值,要非常小心

// 错误的写法
let a;
{a} = {a: 'hhh'}; // Javascript会将{a}认为是一个代码块,只有不将大括号写在行首,才会避免错误

// 正确的写法
let a;
({a} = {a: 'hhh'})

2、解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。

({} = [true, false]);
({} = 'abc');
({} = []);

3、由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。

let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
3、字符串的解构赋值

字符串也可以解构赋值。因为此时,字符串被转换成了类似数组的对象

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。

let {length : len} = 'hello';
len // 5
4、数值和布尔值的解构赋值

解构赋值时,如果等号右边是数值或布尔值,都会先转化为对象

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

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

//数值和布尔值的包装对象都有toString属性,因此变量s都能取到值

解构赋值的规则是,只要等号右边的值不为对象或数组,都先转换为对象。由于undefined和null不能转换为对象,所以无法解析赋值

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
5、函数参数的解构赋值

函数的参数也可以进行解构赋值

function fn([a, b]) {
	return a + b;
}
fn([1,2]); // 3

函数参数的解构也可设置默认值

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指定默认值,所以会得到与前一种写法不同的结果。
function move({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

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 ]
6、圆括号问题

不能使用圆括号的情况

1、变量声明语句

// 全部报错
let [(a)] = [1];

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

let { o: ({ p: p }) } = { o: { p: 2 } };

2、函数参数

// 报错
function f([(z)]) { return z; }
// 报错
function f([z,(x)]) { return x; }

3、赋值语句的格式

// 全部报错
({ p: a }) = { p: 42 };
([a]) = [5];

可以使用圆括号的情况

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

[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确
7、用途 ⭐️

1、交换变量的值

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

2、从函数返回多个值

// 返回一个数组
function fn () {
	return [1, 2, 3]
}
let [x, y, z] = fn()

// 返回一个对象
function fn () {
  return {
    foo: 'abc',
    bar: 'efd'
  }
}
let {foo, bar} = fn();

3、函数参数的定义

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

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

4、提取json数据

let jsonData = {
	id: 888,
  str: 'hhh',
  data: [123,456]
}
let {id, str:name, data:number} = jsonData
console.log(id, name, number) // 888, hhh, [123,456]

5、函数参数的默认值

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

6、遍历map结构

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

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

// 只获取键名
for (let [key] of map) {
  console.log(key)
}
// 只获取值
for (let [,value] of map) {
  console.log(value)
}

7、输入模块的指定方法

const { SourceMapConsumer, SourceNode } = require("source-map");
3、字符串模版及新增方法
1、字符串模版

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

// 普通字符串
let a = `I am a China`;

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

// 字符串嵌入变量
let name = 'Jason';
let age = 18;
let str = `他的名字叫${name}, 年龄是${age}岁`;
console.log(str); // 他的名字叫Jason, 年龄是18岁
2、字符串新增方法

字符串查找

ES5 只有indexOf()方法,用来判断一个字符串是否包含在另一个字符串中

ES6 又提供了3种方法:

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

startWith(): 返回布尔值,表示参数字符串是否在原字符串的开头;

endsWith(): 返回布尔值,表示参数字符串是否在原字符串的结尾;

let str = 'apple banana pear';
// 传统方法 indexOf 返回索引 没找到返回-1
console.log(str.indexOf('banana')) // 6
console.log(str.indexOf('orange')) // -1
// includes 返回true/false
console.log(str.includes('apple')) // true
console.log(str.includes('orange')) // false

// startWith
let str = 'https://study.163.com/course/courseLearn.htm?courseId=1005211046';
let str1 = 'http://www.baidu.com/';
console.log(str.startsWith('https')) // true
console.log(str1.startsWith('https')) // false

// endWith
let imageUrl = 'hhh.png';
let imageUrl1 = 'hhh.jpg';
console.log(imageUrl.endsWith('png')) // true
console.log(imageUrl1.endsWith('png')) // false

这三个方法都支持第二个参数,表示开始搜索的位置。

let s = 'Hello world!';

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

使用第二个参数n时,endsWith与其他两个不同,它针对前n个字符,而其他两个针对从第n个到字符串结束的字符。

重复字符串repeat

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

'x'.repeat(3) // "xxx"

参数如果是小数,会被取整

'na'.repeat(2.9) // "nana"

参数是负数或者Infinity,会报错

'na'.repeat(Infinity)
// RangeError
'na'.repeat(-1)
// RangeError

参数NaN等同于0

'na'.repeat(NaN) // ""

参数是字符串,会先转换成数字

'na'.repeat('3') //nanana

填充字符串 padStart、padEnd

字符串自动补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。

'x'.padStart(5, 'ab'); //ababx

'x'.padEnd(3, 'ab'); // xab

如果原字符串大于或等于最大长度,则字符串补全不生效,返回原字符串

`xxx`.padStart(2, 'ab'); // xxx

如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位置的补全字符串

'abc'.padStart(10, '0123456789')
// '0123456abc'

如果省略第二个参数,默认用空格补全长度

'abc'.padStart(6); // 'abc   '

padStart常见用途

// 为数值补全指定位数
'1'.padStart(10, '0') // "0000000001"

// 提示字符串格式
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"

如果不确定填充字符串的长度

let a = 'apple';
let str = 'xxx';
console.log(a.padStart(a.length+str.length, str))
console.log(a.padEnd(a.length+str.length, str))

去除空格方法 trimStart、trimEnd

trimStarttrimEnd行为与trim()一致,去除字符串空格。trimStart消除字符串头部的空格,trimEnd消除字符串尾部的空格。他们返回的都是新字符串

const s = '  abc  ';

s.trim() // "abc"
s.trimStart() // "abc  "
s.trimEnd() // "  abc"

除了空格键,这两个方法对字符串头部(或尾部)的 tab 键、换行符等不可见的空白符号也有效。

4、函数的扩展
1、函数参数的默认值

基本用法

ES6之前,不能为函数的参数直接定义默认值,只能采用变通的方法

function log(x,y) {
	y = y || 'world'
  console.log(x, y)
}

log('hello') // hello
log('hello', 'china') // hello china
log('hello', '') // hello world

ES6允许为函数的参数定义默认值

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

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

函数的参数默认声明,不能再使用let或const再次声明

function fn (a = 18) {
  // let a = 29; // 报错 Identifier 'a' has already been declared,因为函数的参数默认已经定义了。在函数内部不能再使用let或const声明
  console.log(a)
}
fn()

使用参数默认值时,函数不能有同名参数

function fn(x,x,y=1) {
	console.log(x,y); // 报错 SyntaxError: Duplicate parameter name not allowed in this context
}

另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。

let x = 99;
function foo(p = x + 1) {
  console.log(p);
}

foo() // 100

x = 100;
foo() // 101

与解构赋值默认值结合使用

参数默认值可以与解构赋值默认值结合使用

function fn({x = 0, y = 0}) {
	console.log(x, y)
}
fn({x: 1, y: 2}); // 1, 2
fn({x: 1}); // 1 0
fn({}); // 0 0
fn(); // TypeError: Cannot read property 'x' of undefined

// 如果没有提供参数,函数foo的参数默认为一个空对象
function fn({x = 0, y = 0} = {}) {
	console.log(x, y)
}
fn(); // 0 0

参数默认值的位置

通常情况下,定义参数默认值的位置,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数

function f(x = 1, y) {
  return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1]

函数的length属性

指定了默认值之后,函数的length属性,将返回没有指定参数默认值的参数个数,即指定默认值后,length属性将失真

(function(a) {}).length // 1
(function(a = 1) {}).length // 0

因为length属性的含义是,该函数预期传入的参数个数。rest参数也不会记入length属性

(function(...args) {}).length // 0

如果设置了参数默认值的不是尾参数,那么也不会记入length属性

(function(a = 1, b, c) {}).length // 0

作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。

var x = 1;

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

f(2) // 2 形成一个单独的作用域,x在函数内已经定义,所以不会再指向外部全局的x
let x = 1;

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

f() // 1  x本身没有定义,所以指向外部全局的x,不会指向函数内部的局部变量x,如果此时全局没有定义x,则会报错

应用

利用参数默认值,可以指定某个参数不得省略,如果省略就抛出一个错误

function throwIfMissing() {
  throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided;
}

foo()
// Error: Missing parameter

另外,可以将参数设置为undefined,说明参数是可以省略的

function fn(optional = undefined){
  ...
}
2、rest参数

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

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

rest参数既可以展开数组,也可以重置数组

function fn (...x) {
	console.log(x)
}
fn(1,8,0); // [1, 8, 0]

function fn1 (a, b, c) {
  console.log(a, b, c)
}
fn1(...[2,3,5]); // 2,3,5

注意,rest参数之后不能有其他参数,即只能是最后一个参数,否则会报错

function fn2 (a, b, ...c) {
  console.log(a, b); // 1 2
  console.log(c); // [3, 4, 5]
}
fn2(1,2,3,4,5)
3、严格模式

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

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

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

// 报错
const doSomething = (...a) => {
  'use strict';
  // code
};

const obj = {
  // 报错
  doSomething({a, b}) {
    'use strict';
    // code
  }
};
4、name属性

函数的name属性,返回该函数的函数名。如果将一个匿名函数赋给一个变量,ES5的name会返回一个空字符串,而ES6会返回实际的函数名

var f = function() {
  ...
}
// ES5
f.name; // ''

// ES6
f.name; // f

如果将一个具名函数赋给一个变量,ES5和ES6的name都会返回函数原本的名字

const bar = function baz() {};

// ES5
bar.name // "baz"

// ES6
bar.name // "baz"
5、箭头函数

基本用法

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

let x = (a, b) => (a+b)
console.log(x(5,9)) // 14

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

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

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

另一个用处是

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

// 箭头函数写法
var result = values.sort((x, y) => (x - y))

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命令,因此箭头函数不能用作 Generator 函数。

上面四点中,第一点尤其注意⚠️,this对象的指向是可变的,但是在箭头函数中,它是固定的

var id = 123;
function fn() {
  setTimeout(() => {
    console.log(this.id)
  }, 1000)
}
fn.call({id: 10}); // 10 this指向函数定义时所在的对象 即{id: 10}
function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭头函数
  setInterval(() => this.s1++, 1000);
  // 普通函数
  setInterval(function () {
    this.s2++;
  }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3 绑定定义时所在的作用域,即Timer函数,所以会执行3次
// s2: 0 指向全局对象

this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:argumentssupernew.target

function foo() {
  setTimeout(() => {
    console.log('args:', arguments); // 此arguments实际上是箭头函数foo的arguments变量
  }, 100);
}

foo(2, 4, 6, 8)
// args: [2, 4, 6, 8]

另外,由于箭头函数没有自己的this,所以当然也就不能用call()apply()bind()这些方法去改变this的指向。

(function() {
  return [
    (() => this.x).bind({ x: 'inner' })()
  ];
}).call({ x: 'outer' });
// ['outer']

不适用场合

第一个场合是定义对象的方法,且该方法中包含this

let cat = {
  lives: 9,
  jumps: ()=> {
    this.lives --;
  }
}
// NAN 因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域

第二个场合是需要动态this的时候,也不应使用箭头函数

var button = document.getElementById('press');
button.addEventListener('click', () => {
  this.classList.toggle('on');
});
// button的监听函数是一个箭头函数,导致里面的this就是全局对象

嵌套的箭头函数

箭头函数内部,还可以再使用箭头函数

6、尾调用

什么是尾调用

尾调用就是指一个函数的最后一步是调用另一个函数

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

以下3种情况中,都不属于尾调用

// 情况1
function fn(x) {
  let a = g(x);
  return a; // 调用函数之后,还有赋值操作,不属于尾调用
}

// 情况2
function fn(x) {
  return g(x) + 1; // 调用后还有操作,不属于尾调用
}

// 情况3
function fn(x) {
  g(x); // 等同于 g(x); return undefined;
}

尾调用不一定在函数尾部,只要在最后一步调用即可

function fn(x) {
  if (x > 0) {
      return a(x)
  }
  return b(x)
}
// 函数 a 和 b都属于尾调用,都是函数fn 的最后一步操作

尾调用优化

function f() {
  let m = 1;
  let n = 2;
  return g(m + n);
}
f();

// 等同于
function f() {
  return g(3);
}
f();

// 等同于
g(3);

上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量mn的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除f(x)的调用帧,只保留g(3)的调用帧。

这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。

尾递归

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

递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

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

factorial(5) // 120

上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n)

如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

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

factorial(5, 1) // 120

严格模式

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

这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。

  • func.arguments:返回调用时函数的参数。
  • func.caller:返回调用当前函数的那个函数。

尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。

function restricted() {
  'use strict';
  restricted.caller;    // 报错
  restricted.arguments; // 报错
}
restricted();
7、函数参数的尾逗号

ES2017允许函数的最后一个参数有尾逗号

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

clownsEverywhere(
  'foo',
  'bar',
);
8、Function.prototype.toString()

toString()方法返回函数代码本身,以前会省略注释和空格

function /* foo comment */ foo () {}

foo.toString()
// function foo() {}

修改后的toString()方法,明确要求返回一模一样的原始代码。

function /* foo comment */ foo () {}

foo.toString()
// "function /* foo comment */ foo () {}"
9、catch命令的参数省略

JavaScript 语言的try...catch结构,以前明确要求catch命令后面必须跟参数,接受try代码块抛出的错误对象。

try {
  // ...
} catch (err) {
  // 处理错误
}

ES2019 做出了改变,允许catch语句省略参数。

try {
  // ...
} catch {
  // ...
}
5、数组的循环
1、forEach()

代替普通for循环,接收两个参数 (循环回调函数,this的指向)

arr.forEach(function(val, index, arr) {
  console.log(val, index, arr);
  //apple 0 (3) ["apple", "orange", "banana"]
  //forEach.html:13 orange 1 (3) ["apple", "orange", "banana"]
  //forEach.html:13 banana 2 (3) ["apple", "orange", "banana"]
  console.log(this) // window 此时的this指向window
})

arr.forEach(function(val, index, arr) {
  console.log(val, index, arr);
  //apple 0 (3) ["apple", "orange", "banana"]
  //forEach.html:13 orange 1 (3) ["apple", "orange", "banana"]
  //forEach.html:13 banana 2 (3) ["apple", "orange", "banana"]
  console.log(this) // Number {123} 此时的this指向传入的 123
}, 123)

// 箭头函数 forEach
arr.forEach((val, index, arr) => {
  console.log(this, val, index, arr)
  // this 指window,加参数不变,因为箭头函数this指向函数定义所在的对象 为windows
}, 123)
2、arr.map()

正常情况下,需要配合return使用,返回一个新数组,如果不加return,相当于forEach

主要用于整理后台返回数据

let arr = [
  { title: 'aaa', read: 10, hot: true },
  { title: 'bbb', read: 11, hot: true },
  { title: 'ccc', read: 12, hot: false },
  { title: 'ddd', read: 13, hot: true }
]

let newArr = arr.map((val, index, arr) => {
  let json = {}
  json.t = `^_^${val.title}`
  json.read = val.read + 100
  json.hot = val.hot == true ? '真棒' : '笨蛋'
  return json;
})
console.log(newArr) //  ["aaa", "bbb", "ccc", "ddd"]
3、arr.filter()

用于过滤某些不符合条件的元素

let arr = [
  { title: 'aaa', read: 10, hot: true },
  { title: 'bbb', read: 11, hot: true },
  { title: 'ccc', read: 12, hot: false },
  { title: 'ddd', read: 13, hot: true }
]

let newArr = arr.filter((val, index, arr) => {
  return val.hot;
})
console.log(newArr) // 返回 arr 中为true的对象
4、arr.some()

类似查找,数组中只要某一个元素符合条件,返回true

let arr = ['apple', 'orange', 'banana']

let b = arr.some((val, index, arr) => {
	return val == 'banana'
})
console.log(b) // true
5、arr.every()

数组里必须每个元素都符合条件才会返回true

let arr = [1, 3, 5, 7, 9]
let a = arr.every((val, index, arr) => {
  return val % 2 == 1;
})
console.log(a) // true
6、arr.reduce() 和 arr.reduceRight()

注意:接收的参数与其他不同

arr.reduce()

// 求数组的和
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// prev 代表前一个值,cur代表当前值
let sum = arr.reduce((prev, cur, index, arr) => {
  return prev + cur
})
console.log(sum) // 55

let arr1 = [2, 2, 4]
let num = arr1.reduce((prev, cur, index, arr) => {
  // return Math.pow(prev, cur)  Math.pow(2, 3) 表示2的3次方 
  return prev**cur // ES2017新增 幂 运算符 2**3
})
console.log(num) // 256

console.log(Math.pow(2,3)) // 8
console.log(2**3) // 8

arr.reduceRight()

从右向左依次执行

let arr = [2, 2, 3] // 3的2次方的2次方
let num = arr.reduceRight((prev, cur, index, arr) => {
  // return Math.pow(prev, cur)
  return prev**cur
})
console.log(num) // 81
7、for…of()
let arr = ['apple', 'orange', 'banana']
for (let val of arr) {
  console.log(val) // apple orange banana
}
// arr.keys() 数组下标
for (let key of arr.keys()) {
  console.log(key) // 0 1 2
}

// arr.entries() 数组某一项
for (let item of arr.entries()) {
  console.log(item) // [0, "apple"]  [1, "orange"]  [2, "banana"]
}

for (let [value, key] of arr.entries()) {
  console.log(value, key) // 0 "apple"  1 "orange"  2 "banana"
}
6、数组的扩展
1、扩展运算符

含义

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

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

该运算符主要用于函数调用

function add(x, y) {
	return x + y
}
const numbers = [4,6]
add(...numbers); // 10

替代函数的apply方法

由于扩展运算符可以展开数组,所以不需要apply方法,将数组转为函数的参数了

// ES5
function fn(x,y,z) {
  ...
}
var args = [1,2,3]
fn.apply(null, args)
  
// ES6
function fn(x,y,z) {
  ...
}
var args = [1,2,3]
fn(...args)

另一个例子,通过push函数,将一个数组添加到另一个数组的尾部

// ES5
let arr1 = [0,1,2];
let arr2 = [3,4,5];
Array.prototype.push.apply(arr1, arr2);

//ES6
let arr1 = [0,1,2];
let arr2 = [3,4,5];
arr1.push(...arr2);

扩展运算符的应用

(1)、复制数组

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

(2)、合并数组

扩展运算符提供了数组合并的新写法

const a1 = [1,2];
const a2 = [3];
const a3 = [4,5];
// ES5
a1.concat(a2, a3); // [1,2,3,4,5]

// ES6
[...a1,...a2,...a3]; // [1,2,3,4,5]

(3)、与解构赋值结合

扩展运算符可以与解构赋值结合起来,用于生成数组;如果将扩展运算符用于数组赋值,只能放在参数的最后一位

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

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

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

(4)、字符串

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

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

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

任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。

let nodeList = document.querySelectorAll('div'); // nodeList不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于NodeList对象实现了 Iterator 。
let array = [...nodeList];

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

let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

let arr = [...map.keys()]; // [1, 2, 3]

Generator 函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符。

const go = function*(){
  yield 1;
  yield 2;
  yield 3;
};

[...go()] // [1, 2, 3]
2、Array.from()

Array.from()方法用于将两类对象转换为真正的数组:类似数组的对象和可遍历的对象(包括ES6新增的数据结构Set和Map)

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

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

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

实际应用中,最常见的类似数组的对象是DOM操作返回的NodeList集合,以及函数内部的arguments对象,Array.from都可将他们转换为数组

// NodeList对象
let ps = document.querySelectorAll('p');
Array.from(ps).filter(p => {
  return p.textContent.length > 100;
});

function show() {
  let args = Array.from(arguments)
  console.log(args) // [1, 2, 3, 4, 5]
}
show(1,2,3,4,5)

字符串转数组

let str = 'jason'
// ES5
// let strArr = str.split('');
// ES6
let strArr = Array.from(str)
console.log(strArr) // ["j", "a", "s", "o", "n"]

如果参数是一个真正的数组,Array.from()会返回一个一模一样的数组

let arr  = [1,2,3];
let arr1 = [...arr];
let arr2 = Array.from(arr)
console.log(arr2) // [1,2,3]

Array.from方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。

let json = {
  0: 'apple',
  1: 'banana',
  2: 'orange'
}
let jsonArr = Array.from(json)
console.log(jsonArr) // []

let json1 = {
  0: 'apple',
  1: 'banana',
  2: 'orange',
  length: 3
}
let jsonArr1 = Array.from(json1)
console.log(jsonArr1) // ["apple", "banana", "orange"]

下面的例子将数组中布尔值为false的成员转为0

Array.from([1, , 2, , 3], (n) => n || 0)
// [1, 0, 2, 0, 3]
Array.from({ length: 2 }, () => 'jack')
// ['jack', 'jack']
3、Array.of()

Array.of()方法用于将一组值转换为数组

Array.of(1,2,3); // [1,2,3]
Array.of(); // [] 如果没有参数,就返回一个空数组
Array.of(undefined); // [undefined]
4、数组实例的copyWithin()

数组实例的copyWithin()方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组,也就是说,使用这个方法,会修改当前数组

Array.prototype.copyWithin(target, start = 0, end = this.length)

它接受三个参数。

  • target(必需):从该位置开始替换数据。如果为负值,表示倒数。
  • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。

这三个参数都应该是数值,如果不是,会自动转为数值。

[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5] 从3号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2。
5、数组实例的find()和findIndex()

数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有成员依次执行该回调函数,直到找出第一个返回true的成员,然后返回该成员;如果没有符合条件的成员,返回undefined

let arr = [10,30,78,69,90]
let res = arr.find((val, index, arr) => {
  return val > 70;
})
console.log(res) // 78

findIndex()方法与find()方法类似,返回第一个符合条件的数组成员的位置,如果没有,返回-1

let arr = [10,30,78,69,90]
let idnex = arr.findIndex((val, index, arr) => {
	return val > 70;
})
console.log(idnex) // 2
6、数组实例的fill()

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

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

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

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

let arr = new Array(10);
arr.fill('默认值', 1, 3)
console.log(arr) //  [empty, "默认值", "默认值", empty × 7]
7、数组实例的 entries(),keys()和values()

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

for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1

for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"
8、数组实例的includes()

Array.prototype.includes()方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes相似

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

该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。

[1, 2, 3].includes(3, -1); // true

没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。

indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。

// ES5
[NaN].indexOf(NaN); // -1

// ES6
[NaN].includes(NaN); // true

另外,Map 和 Set 数据结构有一个has方法,需要注意与includes区分。

  • Map 结构的has方法,是用来查找键名的,比如Map.prototype.has(key)WeakMap.prototype.has(key)Reflect.has(target, propertyKey)
  • Set 结构的has方法,是用来查找值的,比如Set.prototype.has(value)WeakSet.prototype.has(value)
9、数组实例的flat(),flatMap()

Array.prototype.flat用于将嵌套的数组‘拉平’,变成一维的数组。该方法返回一个新数组,对原数据没有影响

[1,2,[3,4]];
// [1,2,3,4]

flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1。

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

如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。

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

如果原数组有空位,flat()方法会跳过空位。

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

flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组

// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
// 只能展开一层数组

flatMap()方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。

arr.flatMap(function callback(currentValue[, index[, array]]) {
  // ...
}[, thisArg])
10、数组的空位

数组的空位指,数组的某一个位置没有任何值

Array(3); // [,,,]

注意,空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。

0 in [undefined,undefined,undefined]; // true
0 in [,,,]; // false 

ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。

  • forEach(), filter(), reduce(), every()some()都会跳过空位。
  • map()会跳过空位,但会保留这个值
  • join()toString()会将空位视为undefined,而undefinednull会被处理成空字符串。
// forEach方法
[,'a'].forEach((x,i) => console.log(i)); // 1

// filter方法
['a',,'b'].filter(x => true) // ['a','b']

// every方法
[,'a'].every(x => x==='a') // true

// reduce方法
[1,,2].reduce((x,y) => x+y) // 3

// some方法
[,'a'].some(x => x !== 'a') // false

// map方法
[,'a'].map(x => 1) // [,1]

// join方法
[,'a',undefined,null].join('#') // "#a##"

// toString方法
[,'a',undefined,null].toString() // ",a,,"

ES6则是明确将空位转为undefined

Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。

Array.from(['a',,'b'])
// [ "a", undefined, "b" ]

扩展运算符(...)也会将空位转为undefined

[...['a',,'b']]
// [ "a", undefined, "b" ]

copyWithin()会连空位一起拷贝

[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]

fill()会将空位视为正常的数组位置。

new Array(3).fill('a') // ["a","a","a"]

for...of循环也会遍历空位

let arr = [, ,];
for (let i of arr) {
  console.log(1);
}
// 1
// 1

entries()keys()values()find()findIndex()会将空位处理成undefined

// entries()
[...[,'a'].entries()] // [[0,undefined], [1,"a"]]

// keys()
[...[,'a'].keys()] // [0,1]

// values()
[...[,'a'].values()] // [undefined,"a"]

// find()
[,'a'].find(x => true) // undefined

// findIndex()
[,'a'].findIndex(x => true) // 0
11、Array.prototype.sort()的排序稳定性

排序稳定性(stable sorting)是排序算法的重要属性,指的是排序关键字相同的项目,排序前后的顺序不变。

const arr = [
  'peach',
  'straw',
  'apple',
  'spork'
];

const stableSorting = (s1, s2) => {
  if (s1[0] < s2[0]) return -1;
  return 1;
};

arr.sort(stableSorting)
// ["apple", "peach", "straw", "spork"]
7、对象的扩展
1、对象的简洁语法

ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法

let name = 'Jason';
let age = 30;
let json = {
  name,  // name: name,
  age,    // age: age
  // showA: function() {
  //     return this.name;
  // }
  showA() { // 一定注意,不要使用箭头函数
    return this.name
  }
}
console.log(json.showA())


module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
  getItem: getItem,
  setItem: setItem,
  clear: clear
};

注意,简写的对象方法不能用做构造函数,会报错

const obj = {
  f() {
    this.foo = 'bar';
  }
};

new obj.f() // 报错
2、属性名表达式

ES6 允许字面量定义对象时,把表达式放在方括号内。

let lastWord = 'last word';

const a = {
  'first word': 'hello',
  [lastWord]: 'world'
};

a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"

// 还可以用于定义方法名
let obj = {
  ['h' + 'ello']() {
    return 'hi';
  }
};

obj.hello() // hi

注意,属性名表达式与简洁表示法,不能同时使用,会报错。

// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };

// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};
3、方法的name属性

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

const person = {
  sayName() {
    console.log('hello!');
  },
};

person.sayName.name   // "sayName"

如果对象的方法使用了取值函数(getter)和存值函数(setter),则name属性不是在该方法上面,而是该方法的属性的描述对象的getset属性上面,返回值是方法名前加上getset

const obj = {
  get foo() {},
  set foo(x) {}
};

obj.foo.name
// TypeError: Cannot read property 'name' of undefined

const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');

descriptor.get.name // "get foo"
descriptor.set.name // "set foo"
4、属性的可枚举性和遍历

可枚举性

对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
//  {
//    value: 123,
//    writable: true,
//    enumerable: true,
//    configurable: true
//  }

描述对象的enumerable属性,称为“可枚举性”,如果该属性为false,就表示某些操作会忽略当前属性。

目前,有四个操作会忽略enumerablefalse的属性。

  • for...in循环:只遍历对象自身的和继承的可枚举的属性。
  • Object.keys():返回对象自身的所有可枚举的属性的键名。
  • JSON.stringify():只串行化对象自身的可枚举的属性。
  • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。

属性的遍历

ES6 一共有 5 种方法可以遍历对象的属性。

(1)for…in

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

(2)Object.keys(obj)

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

(3)Object.getOwnPropertyNames(obj)

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

(4)Object.getOwnPropertySymbols(obj)

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

(5)Reflect.ownKeys(obj)

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

以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。

  • 首先遍历所有数值键,按照数值升序排列。
  • 其次遍历所有字符串键,按照加入时间升序排列。
  • 最后遍历所有 Symbol 键,按照加入时间升序排列。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()] 首先是数值属性2和10,其次是字符串属性b和a,最后是 Symbol 属性。
5、super关键字

ES6新增了一个关键字super,指向当前对象的原型对象

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto); // setPrototypeOf() 设置一个指定对象的原型
obj.find() // "hello"

注意,super关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。

// 报错
const obj = {
  foo: super.foo
}

// 报错
const obj = {
  foo: () => super.foo
}

// 报错
const obj = {
  foo: function () {
    return super.foo
  }
}
const proto = {
  x: 'hello',
  foo() {
    console.log(this.x);
  },
};

const obj = {
  x: 'world',
  foo() {
    super.foo();
  }
}

Object.setPrototypeOf(obj, proto);

obj.foo() // "world"
// super.foo指向原型对象proto的foo方法,但是绑定的this却还是当前对象obj,因此输出的就是world。
6、对象的扩展运算符

解构赋值

对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。

let {x, y, ...z} = {x: 1, y: 2, c: 3, d: 4}
console.log(x, y, z) // 1 2 {c: 3, d: 4} 解构赋值必须为最后一个参数,否则会报错

由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefinednull,就会报错,因为它们无法转为对象。

let { ...z } = null; // 运行时错误
let { ...z } = undefined; // 运行时错误

注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。

let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2

扩展运算符

对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。

let json = {a: 1, b: 2}
let json2 = {...json}
delete(json2.a)
console.log(json2) // {b: 2}
console.log(json) // {a: 1, b: 2}

由于数组是特殊的对象,所以对象的扩展运算符可以用于数组

let foo = { ...['a', 'b', 'c'] };
foo
// {0: "a", 1: "b", 2: "c"}

对象的扩展运算符等同于使用Object.assign()方法。

let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);
7、链判断运算符

编程实务中,如果读取对象内部的某个属性,往往需要判断一下该对象是否存在。比如,要读取message.body.user.firstName,安全的写法是写成下面这样。

const firstName = (message
  && message.body
  && message.body.user
  && message.body.user.firstName) || 'default';

或者使用三元运算符?:,判断一个对象是否存在。

const fooInput = myForm.querySelector('input[name=foo]')
const fooValue = fooInput ? fooInput.value : undefined

ES2020 引入了“链判断运算符”(optional chaining operator)?.,简化上面的写法。

const firstName = message?.body?.user?.firstName || 'default';
const fooValue = myForm.querySelector('input[name=foo]')?.value

链判断运算符有三种用法。

  • obj?.prop // 对象属性
  • obj?.[expr] // 同上
  • func?.(...args) // 函数或对象方法的调用
a?.b
// 等同于
a == null ? undefined : a.b

a?.[x]
// 等同于
a == null ? undefined : a[x]

a?.b()
// 等同于
a == null ? undefined : a.b()

a?.()
// 等同于
a == null ? undefined : a()
8、Null 判断运算符

读取对象属性的时候,如果某个属性的值是nullundefined,有时候需要为它们指定默认值。常见做法是通过||运算符指定默认值。

const headerText = response.settings.headerText || 'Hello, world!';
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;

// 但是存在一个问题,开发者的意愿是当等号左边的值为undefined或null时,才使用默认值,但是现在这种写法,等号左边的值为0或者空字符串时,也会使用默认值

为了避免这种情况,ES2020引入了新的运算符??,它的行为类似||,但是只有等号左边的值为undefinednull时,才会生效

与链判断运算符?.一起使用

const animationDuration = response.settings?.animationDuration ?? 300
8、对象新增方法
1、Object.is()

用来比较两个值是否严格相等

console.log(Object.is('foo', 'foo')); // true

console.log(NaN == NaN); // false

console.log(Number.isNaN(NaN)); // true

console.log(Object.is(NaN, NaN)) // true

console.log(+0 == -0); // true

console.log(Object.is(+0, -0)); // false
2、Object.assign()

基本用法

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

let json1 = {a: 1};
let json2 = {b: 2};
let json3 = {c: 3}

// let 新的对象 = Object.assign(target, source1, source1)
let obj = Object.assign({}, json1, json2, json3)
// {a: 1, b: 2, c: 3}

注意,如果目标对象与源对象有同名属性,或者多个源有同名属性,则后面的属性会覆盖前面的属性

let json1 = {a: 1};
let json2 = {b: 2, a: 2};
let json3 = {c: 3}

let obj = Object.assign({}, json1, json2, json3)
console.log(obj) // {a: 2, b: 2, c: 3} 

由于undefinednull无法转换成对象,所以如果他们作为参数,就会报错

Object.assign(undefined); // 报错
Object.assign(null); // 报错

如果非对象参数出现在源对象的位置,这些参数都会先转换为对象,如果无法转换成对象,则跳过,这意味着,如果undefinednull不在首参数,就不会报错

let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true

其他类型的值(即数组、字符串和布尔值)不在首参数,也不会报错,但是,除了字符串会以数组形式,拷贝到目标对象,其他值不会产生效果

const v1 = 'abc'
const v2 = true
const v3 = 10

const obj = Object.assign({},v1,v2,v3)
console.log(obj); // { "0": "a", "1": "b", "2": "c" }

注意点

(1)浅拷贝

Object.assign方法实行的是浅拷贝,而不是深拷贝,也就是说,如果源对象的某个属性值是对象,那么目标对象拷贝得到的是这个对象的引用

const obj1 = {a:{b: 1}};
const obj2 = Object.assign({},obj1);
obj1.a.b = 2;
console.log(obj2.a.b); // 2

(2)同名属性的替换

遇到同名属性,Object.assign的方法是替换,而不是添加

const target = { a: { b: 'c', d: 'e' } }
const source = { a: { b: 'hello' } }
Object.assign(target, source)
// { a: { b: 'hello' } }

(3)数组的处理

Object.assign可以用来处理数组,但是会把数组转换为对象

Object.assign([1,2,3], [4,5]);
// 4,5,3  Object.assign把数组视为属性名为 0、1、2 的对象,因此源数组的 0 号属性4覆盖了目标数组的 0 号属性1

(4)取值函数的处理

Object.assign只能进行值的复制,如果要复制的是一个取值函数,那么将求值后再复制

const source = {
  get foo() { return 1 }
};
const target = {};

Object.assign(target, source)
// { foo: 1 }

常见用途

(1)为对象添加属性

class Point {
  constructor(x, y) {
    Object.assign(this, {x, y}); // 将x属性和y属性添加到Point类的对象实例
  }
}

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

(4)合并多个对象

将多个对象合并到某个对象

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

(5)为属性指定默认值

const DEFAULTS = {
  logLevel: 0,
  outputFormat: 'html'
};

function processContent(options) {
  options = Object.assign({}, DEFAULTS, options);
  console.log(options);
  // ...
}
3、Object.keys()、Object.values()、Object.entries()
let {keys, values, entries} = Object
let json = {
  a: 1,
  b: 2,
  c: 3
}

for (let key of keys(json)) { // 相当于(let key of Object.keys(json))
  console.log(key) // a b c
}

for (let value of values(json)) {
  console.log(value) // 1 2 3 
}

for (let item of entries(json)) {
  console.log(item) // ["a", 1] ["b", 2] ["c", 3]
}

for (let [key, val] of entries(json)) {
  console.log(key, val) // a 1  b 2  c 3
}
4、Object.fromEntries()

Object.fromEntries方法是Object.entries的逆操作,用于将一个键值对数组转化为对象

Object.fromEntries([
  ['foo', 'bar'],
  ['baz', 42]
])
// { foo: "bar", baz: 42 }
9、Promise 对象
1、Promise的含义

Promise是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和强大

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

2、基本用法

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

const promise = new Promise(function(resolve, reject) {
  if (/*异步操作成功*/) {
      resolve(value)
  } else {
    	reject(error)
  }
})

Promise构造函数接收一个函数作为参数,该函数接收两个参数分别是resolvereject

Promise实例生成后,可以用then方法指定resolvedrejected状态的回调函数

promise.then(function(res) {
  // success
}, function(error) {
  // fail
})
// 等同于
promise.then((res) => {
  // success
},(error) => {
  // fail
})

下面是异步加载图片的例子

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    const image = new Image();
    image.onload = function() {
      resolve(image)
    }
    image.onerror = function() {
      reject(new Error('Could not load image at ' + url))
    }
    image.src = url;
  })
}

下面是用Promise对象实现Ajax的例子

const getJSON = function(url) {
  const promise = new Promise(function(resolve, reject){
    const handler = function() {
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    const client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();

  });

  return promise;
};

getJSON("/posts.json").then(function(json) {
  console.log('Contents: ' + json);
}, function(error) {
  console.error('出错了', error);
});

注意,调用resolvereject并不会终结Promise函数的执行

new Promise((resolve, reject) => {
  resolve(1);
  console.log(2);
}).then(r => {
  console.log(r);
});
// 2
// 1

一般来说,调用resolvereject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolvereject的后面。所以,最好在它们前面加上return语句,这样就不会有意外

new Promise((resolve, reject) => {
  return resolve(1);
  // 后面的语句不会执行
  console.log(2);
})
3、Promise.prototype.then()

Promise实例具有then方法,then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数

promise.then(res=> {
	console.log(res) 
}, err => {
	console.log(err)
})

then方法返回的是一个新的Promise实例,因此可以采用链式写法,即then方法后面再调用一个then方法

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});
4、Promise.prototype.catch()

Promise.prototype.catch用于指定发生错误时的回调函数

getJson('/posts.json').then(function(res) {
  //
}).catch(function(error) {
  console.log('发生错误' + error)
})

reject方法的作用,等同于抛出错误

如果Promise状态已经变成resolved,再抛出错误是无效的

const promise = new Promise(function(resolve, reject) {
  resolve('ok');
  throw new Error('test');
});
promise
  .then(function(value) { console.log(value) })
  .catch(function(error) { console.log(error) });
// ok

Promise对象的错误具有‘冒泡’属性,会一直向后传递,直到被捕获为止,也就是说,错误总是被下一个catch捕获

getJson('/posts.json').then(function(post) {
  return getJson(post.commentUrl)
}).then(function(comments) {
  // ...
}).catch(function(error) {
  // 处理前面3个Promise的错误   两个then和getJson
})

一般来说,不要在then方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法

promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

跟传统的try/catch语法不同,如果没有使用catch方法指定错误处理的函数,Promise对象抛出的错误不会传递到外层代码,即不会有任何反应

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  console.log('everything is great');
});

setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123

一般总是建议,Promise 对象后面要跟catch方法,这样可以处理 Promise 内部发生的错误。catch方法返回的还是一个 Promise 对象,因此后面还可以接着调用then方法。

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};

someAsyncThing()
.catch(function(error) {
  console.log('oh no', error);
})
.then(function() {
  console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on
// 如果没有报错,则会跳过catch
5、Promise.prototype.finally()

finnly方法用于指定不管Promise对象最后状态如何,都会执行的操作

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

下面是一个例子,服务器使用Promise处理请求,然后使用finally关掉服务器

sever.listen(port).then(function() {
  // ...
}).finally(sever.stop)

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果

// finally 本质上是then方法的特例
promise
.finally(() => {
  // 语句
});

// 等同于
promise
.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);

finally总是会返回原来的值

// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})

// resolve 的值是 2
Promise.resolve(2).finally(() => {})

// reject 的值是 undefined
Promise.reject(3).then(() => {}, () => {})

// reject 的值是 3
Promise.reject(3).finally(() => {})
6、Promise.all()

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

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

p的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

let pro1 = Promise.resolve('aaa')
let pro2 = Promise.resolve('bbb')
let pro3 = Promise.resolve('ccc')
Promise.all([pro1, pro2, pro3]).then(res => {
  console.log(res) // ["aaa", "bbb", "ccc"]

  let [res1, res2, res3] = res
  console.log(res1, res2, res3) // aaa bbb ccc
})

注意,如果作为参数的Promise实例,自己定义了catch方法,那么它一旦被rejected,并不会出发Promise.allcatch方法

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error('报错了');
})
.then(result => result)
.catch(e => e);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]  
// 如果p2没有自己的catch方法,就会调用Promise.all的catch方法
7、Promise.race()

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

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

上面代码中,只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数

下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject,否则变为resolve

const p = Promise.race([
  fetch('/resource-that-may-take-a-while'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);

p
.then(console.log)
.catch(console.error);
8、Promise.allSettled()

Promise.allSettled方法接受一组Promise实例作为参数,包装成一个新的Promise实例。只有等到这些实例都返回结果,不管是fullfilled还是rejected,包装实例才会结束

const promises = [
  fetch('/api-1'),
  fetch('/api-2'),
  fetch('/api-3'),
];

await Promise.allSettled(promises);
removeLoadingIndicator(); // 对服务器发出三个请求,等到三个请求都结束,不管请求成功还是失败,加载的滚动图标就会消失。

该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()的 Promise 实例。

const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);

const allSettledPromise = Promise.allSettled([resolved, rejected]);

allSettledPromise.then(function (results) {
  console.log(results);
});
// [
//    { status: 'fulfilled', value: 42 },
//    { status: 'rejected', reason: -1 }
// ]
// fulfilled时,对象有value属性,rejected时有reason属性,对应两种状态的返回值

下面是返回值用法的例子

const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);

// 过滤出成功的请求
const successfulPromises = results.filter(p => p.status === 'fulfilled');

// 过滤出失败的请求,并输出原因
const errors = results
  .filter(p => p.status === 'rejected')
  .map(p => p.reason);
9、Promise.any()

Promise.any()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态

Promise.any()Promise.race()方法很像,只有一点不同,就是不会因为某个 Promise 变成rejected状态而结束

const promises = [
  fetch('/endpoint-a').then(() => 'a'),
  fetch('/endpoint-b').then(() => 'b'),
  fetch('/endpoint-c').then(() => 'c'),
];
try {
  const first = await Promise.any(promises);
  console.log(first);
} catch (error) {
  console.log(error);
}
// 只要有一个变成fulfilled,Promise.any()返回的 Promise 对象就变成fulfilled。如果所有三个操作都变成rejected,那么就会await命令就会抛出错误
10、Promise.resolve()

Promise.resolve方法用于将现有对象转换为Promise对象

const p = Promise.resolve($.ajax('/posts.json'))

Promise.resolve()等价于下面的写法

Promise.resolve('foo');
// 等价于
new Promise(resolve => resolve('foo'));

Promise.resolve() 方法的参数分为四种情况

(1)参数是一个Promise实例

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

(2)参数是一个thenable对象

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

let thenable = {
	then: function(resolve, reject) {
    resolve(20)
  }
}
let p1 = Promise.resolve(thenable)
p1.then((value) => {
  console.log(value); // 20
})

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

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

const p = Promise.resolve('hello');
p.then(function(res) {
  console.log(res); // hello
})

(4)不带有任何参数

Promise.resolve方法允许调用时不带参数,直接返回一个resolved状态的Promise对象

const p = Promise.resolve();
p.then((res) => {
  // ...
})

需要注意的是,立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。

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

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

console.log('one')
// one
// two
// three
11、Promise.reject()

Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected

const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function(err) {
  console.log(err) // 出错了
})

注意,Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数

const thenable = {
  then(resolve, reject) {
    reject('出错了');
  }
};

Promise.reject(thenable)
.catch(e => {
  console.log(e === thenable)
})
// true
12、应用

加载图片

const loadImage = function (path) {
  return new Promise(function(resolve, reject) {
    const image = new Image();
    image.onload = resolve();
    image.onerror = reject();
    image.src = path;
  })
}

Generator 函数与 Promise 的结合

function getFoo () {
  return new Promise(function (resolve, reject){
    resolve('foo');
  });
}

const g = function* () {
  try {
    const foo = yield getFoo();
    console.log(foo);
  } catch (e) {
    console.log(e);
  }
};

function run (generator) {
  const it = generator();

  function go(result) {
    if (result.done) return result.value;

    return result.value.then(function (value) {
      return go(it.next(value));
    }, function (error) {
      return go(it.throw(error));
    });
  }

  go(it.next());
}

run(g);
13、Promise.try()

有两种写法,让同步函数同步执行,异步函数异步执行,并且具有统一的API

第一种写法是async函数

const f = ()=> console.log('now');
(asyns() => f())();  // f是同步的会得到同步的结果

(async() => f())()   // f是异步的就可以使用`then`方法指定下一步
.then(...)
      
console.log('next');
// now
// next

第二种写法是使用new Promise

const f = () => console.log('now');
(
  () => new Promise(
    resolve => resolve(f())
  )
)();
console.log('next');
// now
// next

鉴于这是一个很常见的需求,所以提供Promise.try()方法替代上边的写法

const f = () => console.log('now')
Promise.try(f);
console.log('next')
10、Module的语法
1、概述

JavaScript 一直没有模块体系,在ES6之前,社区制定了一套模块规范,主要的是CommonJS和AMD两种。前者主要用于服务端,后者用于浏览器。

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

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
// 实质是整体加载fs模块,生成一个对象,然后再读取这三个方法,这种加载称为‘运行时加载’,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
// ES6模块
import { stat, exists, readFile } from 'fs';
// 实质是从fs模块加载这3个方法,其他方法不加载,这种加载称为’编译时加载‘或静态加载

除了静态加载带来的各种好处,ES6 模块还有以下好处。

  • 不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
  • 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
  • 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。
2、严格模式

ES6的模块自动采用严格模式

严格模式主要有以下限制

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protectedstaticinterface
3、export 命令

模块功能主要有两个命令组成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

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

// profile.js
export let firstName = 'Jason';
export let lastName = 'Jie';

// 还有一种写法(优先考虑这种写法)
let firstName = 'Jason';
let lastName = 'Jie';
export {
	firstName,
  lastName
}

export命令除了输出变量,还可以输出函数或类

export function sum (x, y) {
  return x + y;
}

通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名

function v1() {
	// ...
}
let str = 'hhh'

export {
	v1 as f1,
  str as s
}

需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一对一的对应关系

// 报错
export 1;
// 报错
var m = 1;
export m;
// 报错
function f() {}
export f;

// 正确写法
// 变量
export let m = 1;
// 
let m = 1;
export {m};
// 
let m = 1;
export {m as n};
// 方法
export function f () {}
// 
function f() {}
export {f}

另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以获取到模块内部实时的值

export let foo = 'hhh';
setTimeout(() => {
  foo = 'xxx'; // 1s后变为xxx
}, 1000)
4、import 命令

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

// 大括号里的变量名,必须与被导入模块对外接口的名称相同
import {firstName, lastName} from './profile.js'
// 使用
function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}

如果想为输入的变量重新起名字,import命令要使用as关键字,将输入的变量重名

import {lastName as surName} from './profile.js'

import输入的变量都是只读的,也就是说,不允许在加载模块的脚本里,改写接口,但是改写对象的属性是允许的

// 变量
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;

// 对象
import {a} from './xxx.js'
a.foo = 'hello'; // 合法操作,由于此写法很难查错,建议凡是输入的变量,都当作完全可读,不要轻易改变它的属性

import后边的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js文件名可省略

注意,import命令具有提升效果,会提升到整个模块的顶部,首先执行

foo();
import { foo } from './profile';

由于import是静态执行,所以不能使用变量和表达式

// 报错
import { 'f' + 'oo' } from 'my_module';

// 报错
let module = 'my_module';
import { foo } from module;

// 报错
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}

如果多次重复执行同一条import语句,那么只会执行一次,不会执行多次

// 只执行一次
import 'lodash'; 
import 'lodash';
5、模块的整体加载

除了指定加载某个输出值,还可以使用整体加载,即用星号*指定一个对象,所有输出值都加载在这个对象上

// circle.js
export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}

// 加载模块
// main.js
import * as circle from './circle.js';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14))
6、export default 命令

从上面可以看出,使用import命令的时候,用户必须要知道所要加载的变量名或者函数名,对于用户来说不方便,为了使他们不阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

// export-default.js
export default function() {
  console.log('foo');
}

// 其他模块加载该模块时,import 命令可以为该模块指定任意名字
import customName from './export-default.js'; // 需要注意的是,这时import 后边,不需要大括号
customName(); // foo

export default用在非匿名函数前,也是可以的

export default function foo() {
  // ...
}

// 或者
function foo() {
  // ...
}
export default foo;

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

// 默认输出
export default function foo() {
  // ...
}
import foo from 'foo';

// 正常输出
export function foo() {
  // ...
}
import {foo} from 'foo';

export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import后边才不用加大括号,因为只可能唯一对应export default命令。

本质上,export default就是输出一个叫default的变量或方法,然后系统允许你给他起任意名字。

// modules.js
function add(x, y) {
  return x * y;
}
export {add as default};
// 等同于
// export default add;

// app.js
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';

正是因为export default命令其实是输出一个叫default的变量,所以他后面不能跟变量声明语句。

// 正确
export var a = 1;

// 正确
var b = 10;
export default b;

// 错误
export default var c = 8;

// 正确
export default 7;

// 报错
export 7

如果想在一条import语句中,同时输入默认方法和其他接口,可以写成下面这样

import _, {foo, bar} from 'xxx';

// 对应如下
export default function() {
  // ...
}
export function foo() {}
export function bar() {}

export default也可以用来输出类

// MyClass.js
export default class { ... }

// main.js
import MyClass from 'MyClass';
let o = new MyClass();
7、export 和 import 的复合写法

如果在一个模块中,先输入后输出同一个模块,import语句可以与export语句写在一起

export {foo, bar} from './profile.js';

// 可以简单理解为
import {foo, bar} from './profile.js';
export {foo, bar};

// 需要注意的是,写成一行后,foo 和 bar实际上并没有被导入到当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo 和 bar
// 接口改名
export { foo as myFoo } from 'my_module';

// 整体输出
export * from 'my_module';

// 默认接口
export {default} from 'my_module';

// 具名接口改为默认接口的写法
export { es6 as default } from './someModule';
// 等同于
import { es6 } from './someModule';
export default es6;

// 默认接口也可以改为具名接口
export { default as es6 } from './someModule';
8、模块的继承

模块之间也可以继承

假设有一个circleplus模块,继承了circle模块。

// circleplus.js
export * from 'circle'; // export * 命令会忽略circle里的默认方法
export var e = 2.71828182846;
export default function(x) {
  return Math.exp(x);
}

这时,也可以将circle的属性或方法,改名后再输出

// circleplus.js
export { area as circleArea } from 'circle';

加载上面模块的写法如下

// main.js

import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));
9、跨模块常量

const声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法

// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;

// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3

// test2.js 模块
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3

如果要使用的常量非常多,可以建一个专门的constants目录,将各种常量写在不同的文件里,保存在该目录下

// constants/db.js
export const db = {
  url: 'http://my.couchdbserver.local:5984',
  admin_username: 'admin',
  admin_password: 'admin password'
};

// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];

然后,将这些文件输出的常量,合并在index.js里面。

// constants/index.js
export {db} from './db';
export {users} from './users';

使用的时候,直接加载index.js就可以了。

// script.js
import {db, users} from './constants/index';
10、import()

简介

前面介绍过,import命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行(import命令叫做“连接” binding 其实更合适)。所以,下面的代码会报错。

// 报错
if (x === 2) {
  import MyModual from './myModual';
}

import()函数,支持动态加载模块。

import('./modules/1.js').then((res) => {
  console.log(res) 
})

import()返回一个Promise对象

const main = document.querySelector('main');

import(`./section-modules/${someVariable}.js`)
  .then(module => {
    module.loadPageInto(main);
  })
  .catch(err => {
    main.textContent = err.message;
  });

适用场合

(1)按需加载

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

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

(2)条件加载

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

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

(3)动态的模块路径

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

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

注意点

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

import('xxx.js').then(({export1, export2}) => {
  console.log(export1, export2)
})

如果模块有default输出接口,可以用参数直接获得

import('xxx.js').then((res) => {
  console.log(res.default)
})

也可以使用具名输入的形式

import('xxx.js').then(({default: theDefault}) => {
  console.log(theDefault)
})

如果想同时加载多个模块,可以采用下面的方法

Promise.all([
  import('./modules/1.js'),
  import('./modules/2.js')
]).then(([mod1, mod2]) => {
  console.log(mod1, mod2)
})

import()也可用在async函数之中

async function main() {    
  const modOne = await import('./modules/1.js');
  const modTwo = await import('./modules/2.js');
  console.log(modOne, modTwo);
  // 等同于

  const [m1, m2] = await Promise.all([
    import('./modules/1.js'),
    import('./modules/2.js')
  ])
  console.log(m1, m2)
}
main()
11、Module的加载实现
1、浏览器加载

传统方法

HTML网页中,浏览器通过<script>标签加载Javascript脚本

<!-- 页面内嵌的脚本 -->
<script type="application/javascript">
  // module code
</script>

<!-- 外部脚本 -->
<script type="application/javascript" src="path/to/myModule.js">
</script>

// 默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间

如果脚本体积很大,下载和执行时间就会很长,就会造成浏览器阻塞,所以浏览器允许脚本异步加载

<script src="path/to/myModule.js" defer></script> // defer 是'渲染完执行'
<script src="path/to/myModule.js" async></script> // async 是'下载完执行'

加载规则

浏览器加载ES6模块,也使用script标签,但是要加入type = 'module'属性

<script type='module' src='./foo.js'></script>

浏览器对于带有type="module"script,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了标签的defer属性

<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>
// 如果网页有多个 `type='module'`,会按照页面出现的顺序依次执行

<script>标签的async属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再继续渲染。

<script type="module" src="./foo.js" async></script>
// 一旦使用了async属性,<script type="module">就不会按照页面出现的顺序执行,而是只要该模块加载完成,就执行该模块

ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。

<script type="module">
  import utils from "./utils.js";

  // other code
</script>

对于外部的模块脚本,有几点需要注意。

  • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
  • 模块脚本自动采用严格模式,不管有没有声明use strict
  • 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。
  • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。
  • 同一个模块如果加载多次,将只执行一次
import utils from 'https://example.com/js/utils.js';

const x = 1;

console.log(x === window.x); //false
console.log(this === undefined); // true
2、ES6模块与CommonJS模块的差异

它们有两个重大差异

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

第二个差异是因为CommonJS加载的是一个对象(即 module.export属性),该对象只有在脚本运行完才会生成。

而ES6模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成

下面重点解释第一个差异

CommonJS模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值

// lib.js
var counter = 3;
function incCounter() {
  counter ++;
}
module.export = {
  counter: counter,
  incCounter: incCounter
}

// main.js 加载这个模块
var mod = require('./lib.js');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
// 上面代码说明,模块加载以后,它的内部变化影响不到输出的mod.counter,因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变化的值


// lib.js
var counter = 3;
function incCounter() {
  counter ++;
}
module.export = {
  get counter() {
    return counter;
  },
  incCounter: incCounter
}
// main.js
var mod = require('./lib.js');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 4

而ES6模块是动态引用,并不会缓存值,模块里面的变量绑定所在的模块

// lib.js
export let counter = 3;
export function incCounter() {
  counter ++;
}

// main.js
import {counter, incCounter} from './lib.js'
console.log(counter); // 3
incCounter();
console.log(counter); // 4

ES6输入的模块变量是只读的,所以对他重新赋值,会报错

// lib.js
export let obj = {};

// main.js
import { obj } from './lib';

obj.prop = 123; // OK 可以添加属性
obj = {}; // TypeError 但是重新赋值会报错

export通过接口,输出的都是同一个值。不同的脚本加载这个模块,得到的都是同一个实例

// mod.js
function C() {
  this.sum = 0;
  this.add = function () {
    this.sum += 1;
  };
  this.show = function () {
    console.log(this.sum);
  };
}

export let c = new C();

// x.js
import {c} from './mod';
c.add();

// y.js
import {c} from './mod';
c.show();

// main.js
import './x';
import './y';

// 执行main.js,输出结果是1
12、Class的基本语法
1、简介

类的由来

JavaScript中,生成实例对象的方法是通过构造函数

function Person(name, age) {
  this.name = name
  this.age = age
}
// Person.prototype.showName = function() {
//     return `名字为 ${this.name}`
// }
// Person.prototype.showAge = function() {
//     return `年龄为 ${this.age}`
// }
// 等同于下面这种写法
Object.assign(Person.prototype, {
  showName() {
    return `名字为 ${this.name}`
  },
  showAge() {
    return `年龄为 ${this.age}`
  }
})

let p1 = new Person('jason', 10)
console.log(p1.showName(), p1.showAge()) // 名字为 jason 年龄为 10

ES6提供了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类

let aaa = 'fn'
let bbb = 'method'
class Person{  // 也可以用 const Person = class {}
  constructor(name, age) { // 构造函数(方法),只要执行new,就会自动调用
    // console.log(`构造函数执行了,${name}, ${age}`) // 构造函数执行了,Jason, 10
    // 属性
    this.name = name;
    this.age = age;
  }
  // 方法直接写在class里,不需要加逗号
  showName() {
    return `名字为:${this.name}`
  }
  showAge() {
    return `年龄为:${this.age}`
  }
  // 方法名可以用表达式定义
  [aaa + bbb]() {
    return '方法名自定义'
  }
}
let p1 = new Person('Jason', 10)
console.log(p1.showName(), p1.showAge()) // 名字为:Jason 年龄为:10

console.log(p1[aaa + bbb]()) // 方法名自定义
console.log(p1.fnmethod()) // 方法名自定义
// 类的数据类型就是函数,类本身就指向构造函数

使用的时候,也是直接对类使用new命令

class Bar{
  doStuff() {
    console.log('stuff')
  }
}

let a = new Bar();
console.log(a.doStuff()); // stuff

类的所有方法都定义在类的prototype属性上面

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

// 等同于
Point.prototype = {
  constructor(){},
  toString(){},
  toValue(){}
}

Object.assign()可以方便地一次向类添加多个方法

class Point{
  constructor() {
    // ...
  }
}
Object.assign(Point.prototype, {
  toString() {},
  toValue() {}
})

另外,类的内部定义的所有方法,都是不可枚举的

class Point {
  constructor(x, y) {
    // ...
  }

  toString() {
    // ...
  }
}

Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]

constructor 方法

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

class Point{
  
}
// 等同于
class Point{
  constructor() {}
}

constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象

class Foo{
	constructor() {
    return Object.create(null);
  }
}
new Foo() instanceof Foo
// false

类的实例

生成类的实例的写法,与ES5一样,使用new命令生成。

class Point {
  // ...
}

let p1 = Point(2,4); // 报错
let p2 = new Point(2,4); // 正确

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

class Point{
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  toString() {
    return `(${this.x}, ${this.y})`
  }
}
let point = new Point(2,3);
point.toString(); // (2,3)

point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false 是原型对象的属性
point.__proto__.hasOwnProperty('toString') // true

与ES5一样,类的所有实例对象共享一个原型对象

var p1 = new Point(1, 1);
var p2 = new Point(2, 3);

p1.__proto__ === p2.__proto__; // true

取值函数(getter) 和 存值函数(setter)

在类的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为

class Person{
  constructor() {

  }
  // 获取值
  get aaa() {
    return `获取aaa的值`
  }
  // 设置值
  set aaa(val) {
    console.log(`设置aaa的值,为${val}`) // 设置aaa的值,为hhh
  }
}

let p1 = new Person();
p1.aaa = 'hhh'
console.log(p1.aaa) // 获取aaa的值

属性表达式

类的属性名,可以采用表达式

let methodName = 'getArea';
class Square{
  constructor() {
    // ...
  }
  [methodName]() {
    // ...
  }
}

Class表达式

与函数一样,类也可以使用表达式的方式定义

const MyClass = class Me{
  getClassName() {
    return Me.name
  }
}

let inst = new MyClass();
inst.getClassName(); // Me
Me.getClassName(); // ReferenceError: Me is not defined Me只在Class内部有定义
// 注意,这个类的名字是 Me, 但是Me只在Class的内部可用,指代当前类。在这个类外部,只能用MyClass引用

如果类的内部没有用到的话,可以省略Me,也就是可以写成下面的形式

const MyClass = class {
  // ...
}

采用Class表达式。可以写出立即执行的Class

let person = class {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(this.name)
  }
}('Jason')
person.sayName(); // Jason

注意点

(1)严格模式

类和模块的内部,默认就是严格模式

(2)不存在提升

类不存在变量提升

new Point(); // ReferenceError
class Point{}

(3)name属性

存在name属性

class Point{}
Point.name; // Point

(4)Generator 方法

如果在某个方法前加上星号(*),就表示方法是个Generator方法

class Foo {
  constructor(...args) {
    this.args = args;
  }
  * [Symbol.iterator]() {
    for (let arg of this.args) {
      yield arg;
    }
  }
}

for (let x of new Foo('hello', 'world')) {
  console.log(x);
}
// hello
// world

(5)this 的指向

类的方法内部如果含有this,它默认指向类的实例

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

  print(text) {
    console.log(text);
  }
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
// 这时,this指向方法运行时所在的环境(由于class内部是严格模式,所以this指向undefined)

一个比较简单的方法是,在构造方法中绑定this

class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }
}

另一种方法是使用箭头函数

class Obj {
  constructor() {
    this.getThis = () => this;
  }
}

const myObj = new Obj();
myObj.getThis() === myObj // true
//箭头函数内部的this总是指向定义时所在的对象。上面代码中,箭头函数位于构造函数内部,它的定义生效的时候,是在构造函数执行的时候。这时,箭头函数所在的运行环境,肯定是实例对象,所以this会总是指向实例对象。

❓还有一种解决方法是使用Proxy,获取方法的时候,自动绑定this

function selfish (target) {
  const cache = new WeakMap();
  const handler = {
    get (target, key) {
      const value = Reflect.get(target, key);
      if (typeof value !== 'function') {
        return value;
      }
      if (!cache.has(value)) {
        cache.set(value, value.bind(target));
      }
      return cache.get(value);
    }
  };
  const proxy = new Proxy(target, handler);
  return proxy;
}

const logger = selfish(new Logger());
2、静态方法

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

class Person{
  constructor() {
		// ...
  }
  showName() {
    return '这是showName方法'
  }
  static aaa() {
    return '这是静态方法'
  }
}
let p1 = new Person();
console.log(p1.showName()) // 这是showName方法
console.log(Person.aaa()) // 这是静态方法
console.log(p1.aaa()) // TypeError: p1.aaa is not a function

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

class Foo{
  static bar () {
    this.baz();  // this 值的是 Foo类
  }
  static baz() {
    console.log('hello')
  }
  baz() {   // 还可以看出静态方法可以与非静态方法重名
    console.log('world')
  }
}
Foo.bar(); // hello

父类的静态方法,可以被子类继承

class Foo{
  static sayName() {
    console.log('hello')
  }
}
class Bar extends Foo{
  
}
Bar.sayName(); // hello

静态方法也是可以从super对象上调用的

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

class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}

Bar.classMethod() // "hello, too"
3、实例属性的新写法

实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层

class IncreasingCounter {
  constructor() {
    this._count = 0;
  }
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

// 也可以写成下面这种方式
class IncreasingCounter {
  _count = 0;
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}
4、静态属性

静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象this上的属性

写法是在实例属性的前面,加上static关键字

// 老写法
class Foo{
  // ...
}
Foo.prop = 1;

// 新写法
class Foo{
  static prop = 1;
}
❓5、私有方法和私有属性

现有的解决方案

私有方法和私有属性,是只能在类的内部访问的方法和属性,外部无法访问。这是常见需求,有利于代码的封装,但是ES6不提供,只能通过变通方法模拟实现。

1、在命名上加以区别 (不保险,在类的外部还是可以调用到)

class Foo{
  bar() {
    console.log('bar')
  }
  _baz() {
    console.log('baz')
  }
}

2、将私有方法移出模块(因为模块内部的所有方法都是对外可见的)

class Widget {
  foo (baz) {
    bar.call(this, baz);
  }

  // ...
}

function bar(baz) {
  return this.snaf = baz;
}

3、利用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;
  }

  // ...
};

私有属性的提案

目前,有一个提案,为class添加私有属性。方法是在属性名之前,使用#表示

class IncreasingCounter {
  #count = 0;
  get value() {
    console.log('Getting the current value!');
    return this.#count;
  }
  increment() {
    this.#count++;
  }
}

// 私有属性,只能在类的内部使用,如果在类的外部调用,会报错
const counter = new IncreasingCounter();
counter.#count // 报错
counter.#count = 42 // 报错

这种写法还可以用来写私有方法。

class Foo {
  #a;
  #b;
  constructor(a, b) {
    this.#a = a;
    this.#b = b;
  }
  #sum() {
    return #a + #b;
  }
  printSum() {
    console.log(this.#sum());
  }
}

另外,私有属性也可以设置gettersetter方法

class Counter {
  #xValue = 0;

  constructor() {
    super();
    // ...
  }

  get #x() { return #xValue; }
  set #x(value) {
    this.#xValue = value;
  }
}

私有属性和私有方法前面,也可以加static关键字,表示这是一个静态的私有方法或私有属性

class FakeMath {
  static PI = 22 / 7;
  static #totallyRandomNumber = 4;

  static #computeRandomNumber() {
    return FakeMath.#totallyRandomNumber;
  }

  static random() {
    console.log('I heard you like random numbers…')
    return FakeMath.#computeRandomNumber();
  }
}

FakeMath.PI // 3.142857142857143
FakeMath.random()
// I heard you like random numbers…
// 4
FakeMath.#totallyRandomNumber // 报错
FakeMath.#computeRandomNumber() // 报错
6、new.target 属性

ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。

function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error('必须使用 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, '张三');  // 报错

Class 内部调用new.target,返回当前Class

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    this.length = length;
    this.width = width;
  }
}

var obj = new Rectangle(3, 4); // 输出 true

需要注意的是,子类继承父类时,new target会返回子类

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    // ...
  }
}

class Square extends Rectangle {
  constructor(length, width) {
    super(length, width);
  }
}

var obj = new Square(3); // 输出 false

利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。

class Shape { // 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);  // 正确
13、Class 的继承
1、简介

Class 可以通过extends关键字实现继承

// 父类
class Person {
  constructor(name) {
    this.name = name;
  }
  showName() {
    console.log('父类的showName方法')
  }
}
// 子类  
class Student extends Person {
  constructor(name, skill) {
    super(name); // 使用super关键字继承父类的属性
    this.skill = skill;
  }
  showName() {
    super.showName(); // 调用父类的方法
    // TODO
    console.log('子类的showName方法')
  }
}

子类必须在 constructor 方法中调用 super方法,否则新建实例会报错

class Person{
  // ...
}

class Student extends Person{
  constructor() {
    
  }
}
let stu = new Student(); // 报错

如果子类没有定义constructor方法,会被默认添加

class Student extends Person {
  // ...
}
// 等同于
class Student extends Person{
  constructor(...args) {
    super(args)
  }
}

另外,在子类的构造函数中,只有调用super方法之后,才能使用this,否则会报错

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

class Student extends Person {
  constructor(name, age, skill) {
    this.skill = skill; // 报错
    super(name, age);
    this.skill = skill; // 正确
  }
}

父类的静态方法也会被子类继承

class Person {
  static sayHello() {
    console.log('hello')
  }
}

class Student extends Person{
  
}
Student.sayHello(); // hello
2、Object.getPrototypeOf()

Object.getPrototypeOf()方法可以用来在子类上获取父类

Object.getPrototypeOf(Student) === Person
// true
// 因此,可以使用这个方法判断,一个类是否继承了另一个类
3、super关键字

super关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同

第一种情况,super作为函数调用时,代表父类的构造函数。ES6要求,子类的构造函数必须执行一次super函数

class A {
  
}

class B extends A {
  constructor() {
    super();
  }
}

super()内部的this指向子类的实例,因此super()在这里相当于A.prototype.constructor.call(this)

class A {
  console.log(new.target.name); // new.target指向当前正在执行的函数
}

class B extends A {
  constructor() {
    super();
  }
}
new A(); // A
new B(); // B

作为函数时,super只能用在子类的构造函数中,用在其他地方会报错

class A {
  
}

class B extends A {
  m() {
    super(); // 报错
  }
}

第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类

class A{
  p() {
    return 2;
  }
}

class B extends A{
  constructor() {
    super();
    console.log(super.p());
  }
}
let b = new B(); // 2

这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的

class A {
  constructor() {
    this.a = 1;
  }
}

class B extends A{
  get m() {
    return super.a
  }
}
let b = new B();
b.m; // undefined

如果属性定义在父类的原型对象上,super就可以取到

class A{}
A.prototype.a = 1;

class B extends A {
  constructor() {
    super();
    console.log(super.a);
  }
}
let b = new B();

ES6规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例

class A{
  constructor() {
    this.x = 1
  }
  print() {
    console.log(this.x)
  }
}

class B extends A{
  constructor() {
    super();
    this.x = 2;
  }
  bPrint() {
    super.print();
  }
}
let b = new B();
b.bPrint(); // 2

由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性

class A{
  constructor() {
    this.x = 1;
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined
    console.log(this.x); // 3
  }
}
let b = new B();

如果super作为对象,用在静态方法之中,这时super指向父类,而不是父类的原型对象

class Parent{
  static myMethod(msg) {
    console.log(`static ${msg}`)
  }
  myMethod(msg) {
    console.log(`instance ${msg}`)
  }
}

class Child extends Parent{
  static myMethod(msg) {
    super.myMethod(msg);
  }
  myMethod(msg) {
    super.myMethod(msg);
  }
}
Child.myMethod(1); // static 1
let child = new Child();
child.myMethod(2); // instance 2

另外,在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例

class A {
  constructor() {
    this.x = 1;
  }
  static print() {
    console.log(this.x);
  }
}

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

注意,使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错

class A{}

class B extends A{
  constructor() {
    super();
    console.log(super); // 报错
  }
}

由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字

let obj = {
  toString() {
    return `myObject: ${super.toString()}`
  }
}
console.log(obj.toString()) // MyObject: [object Object]
4、类的prototype属性和__ proto__属性

Class 作为构造函数的语法糖,同时具有prototype__proto__两个属性,因此同时存在两条继承链

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

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

class A {}

class B extends A{}
B.__proto__ === A; // true
B.prototype.__proto__ === A.prototype; // true

这样的结果是因为,类的继承是按照下面这种模式实现的

class A{}
class B{}

// B的实例继承A的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype

// B继承A的静态属性
Object.setPrototypeOf(B, A);
B.__proto__ = A;

实例的__ proto__属性

子类实例的__proto__属性的__proro__属性,指向父类的__proto__属性,也就是说,子类的原型的原型,是父类的原型

var p1 = new Point(1,2);
var p2 = new ColorPoint(1,2,'red');

p2.__proto__ === p1.__proto; // false
p2.__proto__.__proto__ === p1.__proto; // true

因此,可以通过子类实例的__proto__属性的__proro__属性,修改父类实例的行为

p2.__proto__.__proto__.printName = function() {
  console.log('Ha');
}

p1.printName(); // Ha
5、原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript的原生构造函数大致有下面这些

  • Boolean()
  • Number()
  • String()
  • Array()
  • Date()
  • Function()
  • RegExp()
  • Error()
  • Object()

以前,这些原生构造函数是无法继承的

ES6允许继承原生构造函数定义子类

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

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

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

// extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数
6、Mixin 模式的实现

Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。

const a = {
  a: 'a'
}
const b = {
  b: 'b'
}
const c = {...a, ...b}; // {a: 'a', b: 'b'}

下面是一个更完备的实现,将多个类的接口“混入”(mix in)另一个类。

function mix(...mixins) {
  class Mix {
    constructor() {
      for (let mixin of mixins) {
        copyProperties(this, new mixin()); // 拷贝实例属性
      }
    }
  }

  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) {
  // ...
}
14、Symbol
1、概述

ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。它是JavaScript语言的第七种数据类型,前六种是:undefinednull、布尔值(Boolean)、字符串(String)、对象(Object)、数值(Number)。

Symbol值通过Symbol函数生成。这就是说,对象的属性名现在有两种形式,一种是原来就有的字符串,另一种就是新增的Symbol类型。凡是属性名是Symbol类型的,就都是独一无二的

let s = Symbol(); // 不能使用new命令

typeof s; // symbol

Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

let s1 = Symbol('foo');
let s2 = Symbol('bar');

s1 // Symbol(foo)
s2 // Symbol(bar)

s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"

注意,Symbol函数的参数只是对当前Symbol值的描述,因此相同参数的Symbol函数的返回值是不相等的

// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();

s1 === s2; // false

let s1 = Symbol('abc');
let s2 = Symbol('abc');

s1 === s2; // false

Symbol值不能与其他类型的值进行运算,会报错

let 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可以显示转换为字符串

let sym = Symbol('My symbol');

sym.toString(); // 'Symbol(My symbol)'
String(sym); // 'Symbol(My symbol)'

另外,Symbol值也可以转为布尔值,但不能转为数值

let sym = Symbol();
Boolean(sym) // true
!sym  // false

if (sym) {
  // ...
}

Number(sym) // TypeError
sym + 2 // TypeError
2、Symbol.prototype.Description

创建Symbol的时候,可以添加一个描述

let s1 = Symbol('foo')

ES2019提供了一个实例属性description,直接返回Symbol 值的描述

let s1 = Symbol('foo')
s1.description; // foo
3、作为属性名的Symbol

由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖

let mySymbol = Symbol();

// 第一种写法
let a = {}
a[mySymbol] = 'hello'

// 第二种写法
let a = {
  [mySymbol]: 'hello'
}

// 第三种写法
let a = {}
Object.defineProperty(a, mySymbol, {value: 'hello'})

// 以上写法得到同样结果
a[mySymbol]; // 'hello'

注意,Symbol值作为对象属性名时,不能用点运算符

const mySymbol = Symbol();
const a = {};

a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"

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

let s = Symbol();

let obj = {
  [s]:function(args) {
    // ...
  }
}
// 采用增强的对象写法,可以写的更简洁
let obj = {
  [s](args){
    // ...
  }
}
obj[s](123)

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

const log = {};

log.levels = {
  DEBUG: Symbol('debug'),
  INFO: Symbol('info'),
  WARN: Symbol('warn')
};
console.log(log.levels.DEBUG, 'debug message');
console.log(log.levels.INFO, 'info message');

还有一点需要注意,Symbol值作为属性名时,该属性还是公有属性,不是私有属性。

4、属性名的遍历

Symbol作为属性名,进行遍历的时候,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringfy()返回

但是,它也不是私有属性,有一个Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

const obj = {};
let a = Symbol('a');
let b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

const objectSymbols = Object.getOwnPropertySymbols(obj);

objectSymbols
// [Symbol(a), Symbol(b)]

另一个新的 API,Reflect.ownKeys()方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。

let obj = {
  [Symbol('my_key')]: 1,
  enum: 2,
  nonEnum: 3
};

Reflect.ownKeys(obj)
//  ["enum", "nonEnum", Symbol(my_key)]
5、Symbol.for(), Symbol.keyFor()

有时,我们希望重新使用同一个Symbol 值,Symbol.for()方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建一个以该参数作为名称的Symbol值,并将其注册到全局。

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

s1 === s2; // true

Symbol.for()Symbol()两种写法,都会生成新的Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。

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

Symbol.for('foo') === Symbol.for('foo');
// true

Symbol('foo') === Symbol('foo');
// false

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

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

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

注意,Symbol.for()为Symbol值登记的名字,是全局环境的,不管有没有在全局环境运行

function foo() {
  return Symbol.for('bar');
}

const x = foo();
const y = Symbol.for('bar');
console.log(x === y); // true
6、内置的Symbol值

除了定义自己使用的Symbol值以外,ES6还提供了11个内置的Symbol值,指向语言内部使用的方法。

Symbol.hasInstance

对象的Symbol.hasInstance属性,指向一个内部方法。当其他对象使用instanceof运算符,判读是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)

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

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

Symbol.isConcatSpreadable

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

let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e'); // ['a', 'b', 'c', 'd', 'e'] 数组的默认行为是可以展开
arr1[Symbol.isConcatSpreadable]; // undefined 默认等于undefined

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

类似数组的对象正好相反,默认不展开。它的Symbol.isConcatSpreadable属性设为true时,才可以展开。

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']

Symbol.species

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

class MyArray extends Array {
}

const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);

b instanceof MyArray // true
c instanceof MyArray // true
// a是MyArray的实例,b和c是a的衍生对象。你可能会认为,b和c都是调用数组方法生成的,所以应该是数组(Array的实例),但实际上它们也是MyArray的实例

Symbol.species属性就是为了解决这个问题而提供的。现在,我们可以为MyArray设置Symbol.species属性

class MyArray extends Array {
  static get [Symbol.species]() { return Array; }
}
class MyArray extends Array {
  static get [Symbol.species]() { return Array; }
}

const a = new MyArray();
const b = a.map(x => x);

b instanceof MyArray // false
b instanceof Array // true
// a.map(x => x)生成的衍生对象,就不是MyArray的实例,而直接就是Array的实例。

Symbol.match

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

String.prototype.match(regexp)
// 等同于
regexp[Symbol.match](this)

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

'e'.match(new MyMatcher()) // 1

Symbol.replace

对象的Symbol.replace属性,指向一个方法,当该对象被String.prototype.replace方法调用时,会返回该方法的返回值。

String.prototype.replace(searchValue, replaceValue)
// 等同于
searchValue[Symbol.replace](this, replaceValue)

Symbol.search

对象的Symbol.search属性,指向一个方法,当该对象被String.prototype.search方法调用时,会返回该方法的返回值

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

Symbol.split

对象的Symbol.split属性,指向一个方法,当该对象被String.prototype.split方法调用时,会返回该方法的返回值。

String.prototype.split(separator, limit)
// 等同于
separator[Symbol.split](this, limit)

class MySplitter {
  constructor(value) {
    this.value = value;
  }
  [Symbol.split](string) {
    let 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

对象的Symbol.iterator属性,指向该对象的默认遍历器方法。

const myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable] // [1, 2, 3]

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'
obj == 'default' // true
String(obj) // 'str'

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';
  }
}
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"

ES6 新增内置对象的Symbol.toStringTag属性值如下。

  • JSON[Symbol.toStringTag]:‘JSON’
  • Math[Symbol.toStringTag]:‘Math’
  • Module 对象M[Symbol.toStringTag]:‘Module’
  • ArrayBuffer.prototype[Symbol.toStringTag]:‘ArrayBuffer’
  • DataView.prototype[Symbol.toStringTag]:‘DataView’
  • Map.prototype[Symbol.toStringTag]:‘Map’
  • Promise.prototype[Symbol.toStringTag]:‘Promise’
  • Set.prototype[Symbol.toStringTag]:‘Set’
  • %TypedArray%.prototype[Symbol.toStringTag]:'Uint8Array’等
  • WeakMap.prototype[Symbol.toStringTag]:‘WeakMap’
  • WeakSet.prototype[Symbol.toStringTag]:‘WeakSet’
  • %MapIteratorPrototype%[Symbol.toStringTag]:‘Map Iterator’
  • %SetIteratorPrototype%[Symbol.toStringTag]:‘Set Iterator’
  • %StringIteratorPrototype%[Symbol.toStringTag]:‘String Iterator’
  • Symbol.prototype[Symbol.toStringTag]:‘Symbol’
  • Generator.prototype[Symbol.toStringTag]:‘Generator’
  • GeneratorFunction.prototype[Symbol.toStringTag]:‘GeneratorFunction’

Symbol.unscopables

对象的Symbol.unscopables属性,指向一个对象。该对象指定了使用with关键字时,哪些属性会被with环境排除。

Array.prototype[Symbol.unscopables]
// {
//   copyWithin: true,
//   entries: true,
//   fill: true,
//   find: true,
//   findIndex: true,
//   includes: true,
//   keys: true
// }

Object.keys(Array.prototype[Symbol.unscopables])
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'includes', 'keys']
15、Generator 函数的语法
1、简介

基本概念

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

Generator函数有多种理解角度。语法上,首先可以把它理解成,Generator函数是一个状态机,封装了多个内部状态。

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

形式上,Generator函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有个星号;二是,函数体内部使用yield表达式,定义不同的内部状态

// 定义
function * gen(){
  yield 'Welcome';
  yield 'to';
  return 'China';
}

// 调用
let gen1 = gen();
console.log(gen1) // 对象

// 使用next()
// value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束
console.log(gen1.next()); // {value: "Welcome", done: false}
console.log(gen1.next()); // {value: "to", done: false}
console.log(gen1.next()); // {value: "China", done: true}

// 如果已经为true value 就会为undefined
console.log(gen1.next()); // {value: undefined, done: true}

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

ES6没有规定,function关键字与函数名之间的星号,写在哪个位置。这导致下面方法都能通过

function * foo(x, y) { ... }
function* foo(x, y) { ... } // 一般写法
function *foo(x, y) { ... }
function*foo(x, y) { ... }

yield表达式

yield表达式就是暂停标志。

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

(1)遇到yield表达式,就暂停执行后边的操作,并将紧跟在yield表达式后边的值,作为返回对象的value属性值;

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

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

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

需要注意的是,yield后面的表达式,只有当调用next方法,内部指针指向该语句时才会执行

function* gen(){
	yield 123 + 456;
}

// 该函数不会立即求值,而是当调用next方法时,才会求值

yield表达式与return语句有相似之处,也有不同。

相似之处:

  • 都能返回紧跟在语句后面的表达式的值

不同之处:

  • 每次遇到yield,函数暂停执行,下次再从该位置向后执行,而return语句不具备位置记忆的功能
  • 一个函数里,最多只能执行一次return,但是可以执行多次yield表达式

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

function* fn() {
	console.log('执行')
}
var gen = fn();
setTimeout(() => {
  gen.next();
}, 1000)

另外,yield表达式只能用在Generator 函数中,用在其他地方会报错

(function() {
	yield 1;
})()
// SyntaxError: Unexpected number

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
}

yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。

function* demo() {
  foo(yield 'a', yield 'b'); // OK
  let input = yield; // OK
}

与 Iterator接口的关系

上一章说过,任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。

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

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable] // [1, 2, 3]
2、next方法的参数

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

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 } yield(x+1)的返回值为12  y=24
b.next(13) // { value:42, done:true } z=13

注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的

function* dataConsumer() {
  console.log('Started');
  console.log(`1. ${yield}`);
  console.log(`2. ${yield}`);
  return 'result';
}

let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a')
// 1. a
genObj.next('b')
// 2. b
3、for…of循环

for...of循环可以自动遍历Generator函数运行时生成的Iterator对象,且此时不再需要调用next方法

function * gen(){
  yield 'Welcome';
  yield 'to';
  return 'China';
}

let gen1 = gen();
for (let val of gen1) {
  console.log(val) // Welcome to
}
// 一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象

除了for...of以外,扩展运算符(...)、解构赋值和Array.from方法内部调用的,都是遍历器接口。这意味着,它们都可以将Generator函数返回的Iterator对象作为参数

function* numbers() {
  yield 1;
  yield 2;
  return 3;
  yield 4;
}

// 扩展运算符
[...numbers()] // [1,2]

// Array.from 方法
Array.from(numbers()); // [1, 2]

// 解构赋值
let [x, y] = numbers();
x; // 1
y; // 2

// for...of 循环
for(let n of numbers()) {
  console.log(n);
}
// 1
// 2
4、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

throw方法接收一个参数,该参数会被catch语句接收,建议抛出Error对象的实例

var g = function* () {
  try{
    yield;
  }catch(e){
    console.log(e)
  }
}

var i = g();
i.next();
i.throw(new Error('出错了!'))
// Error: 出错了!(…)

注意,不要混淆遍历器对象的throw方法和全局的throw命令。

var g = function* () {
  while (true) {
    try {
      yield;
    } catch (e) {
      if (e != 'a') throw e;
      console.log('内部捕获', e);
    }
  }
};

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

try {
  throw new Error('a');
  throw new Error('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 外部捕获 [Error: a]
// 之所以只捕获了a,是因为函数体外的catch语句块,捕获了抛出的a错误以后,就不会再继续try代码块里面剩余的语句了

如果Generator 函数内部没有部署try...catch代码块,那么throw方法抛出的错误,将被外部try...catch代码块捕获。

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

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

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

如果Generator函数内部外部都没有定义try...catch代码块,那么程序将不错,直接中断执行。

var gen = function* gen(){
  yield console.log('hello');
  yield console.log('world');
}

var g = gen();
g.next();
g.throw();
// hello
// Uncaught undefined

throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。

var g = function* () {
  try{
    yield 1;
  }catch(e) {
    console.log('内部捕获', e)
  }
}
let i = g();
i.throw(1);
// Uncaught 1

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

另外,throw命令与g.throw方法是无关的,两者互不影响

var gen = function* gen(){
  yield console.log('hello');
  yield console.log('world');
}

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

try {
  throw new Error();
} catch (e) {
  g.next();
}
// hello
// world

Generator 函数体外抛出的错误,可以在函数体内捕获;反过来,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属性等于undefineddone属性等于true的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。

5、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 }

如果return方法调用时,不提供参数,则返回值的value属性为undefined

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

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

如果 Generator 函数内部有try...finally代码块,且正在执行try代码块,那么return方法会导致立刻进入finally代码块,执行完以后,整个函数才会结束。

function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
6、next()、throw()、return()的共同点

next()throw()return()作用都是让Generator函数恢复执行,并且使用不同的语句替换yield表达式

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

const g = function* (x, y) {
  let result = yield x + y;
  return result;
};

const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}

gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;

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

gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));

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

gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
7、yield* 表达式

如果在Generator函数体内部,调用另一个Generator函数。需要在前者的函数体内部,手动完成遍历。

function* foo() {
  yield 'a';
  yield 'b';
}

function* bar() {
  yield 'x';
  // 手动遍历 foo()
  for (let i of foo()) {
    console.log(i);
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// x
// a
// b
// y

ES6 提供了yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。

function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}

// 等同于
function* bar() {
  yield 'x';
  yield 'a';
  yield 'b';
  yield 'y';
}

// 等同于
function* bar() {
  yield 'x';
  for (let i of foo()) {
    yield i;
  }
  yield 'y';
}
for (let v of bar()) {
  console.log(v);
}

从语法角度上来看,如果yield表达式后边跟的是一个遍历器对象,需要在yield表达式后加星号,表明它返回的是一个遍历器对象。

let delegatedIterator = (function* () {
  yield 'Hello!';
  yield 'Bye!';
}());

let delegatingIterator = (function* () {
  yield 'Greetings!';
  yield* delegatedIterator;
  yield 'Ok, bye.';
}());

for(let value of delegatingIterator) {
  console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."

yield*后面的Generator函数(没有return语句时),等同于在其内部,部署一个for...of循环

function* concat(item1, item2){
  yield* item1;
  yield* item2;
}
// 等同于
function* concat(item1, item2) {
  for (let value of item1) {
    yield value;
  }
  for (let value of item2) {
    yield value;
  }
}

任何数据结构只要有 Iterator 接口,就可以被yield*遍历。

// 数组
function* gen(){
  yield* ["a", "b", "c"];
}

gen().next() // { value:"a", done:false }

// 字符串
let read = (function* () {
  yield 'hello';
  yield* 'hello';
})();

read.next().value // "hello"
read.next().value // "h"

如果被代理的 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}

yield*命令可以很方便地取出嵌套数组的所有成员

function* iterTree(tree) {
  if (Array.isArray(tree)) {
    for(let i=0; i < tree.length; i++) {
      yield* iterTree(tree[i]);
    }
  } else {
    yield tree;
  }
}

const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];

for(let x of iterTree(tree)) {
  console.log(x);
}
// a
// b
// c
// d
// e
8、作为对象属性的Generator函数

如果一个对象的属性是Generator函数,可以写成下面这种形式

let obj = {
  * gen() {
    // ...
  }
}

// 等同于
let obj = {
  gen: function* () {
    // ...
  }
}
9、Generator函数的this

Generator 函数总是返回一个遍历器,ES6规定这个遍历器是Generator函数的实例,也继承了Generator函数的prototype上的方法

function* g() {}

g.prototype.log = function() {
  console.log('hello')
}

let obj = g();
obj instanceof g; // true
obj.log(); // hello

如果把g当作普通的构造函数,并不会生效,因为g返回的是遍历器对象,而不是this

function* g() {
  this.a = 1;
}
let obj = g();
obj.next();
obj.a; // undefined

Generator函数也不能和new一起使用,会报错

function* F() {
  yield this.x = 2;
  yield this.y = 3;
}

new F()
// TypeError: F is not a constructor

那么,有没有办法让 Generator 函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this

下面是一个变通方法。首先,生成一个空对象,使用call方法绑定 Generator 函数内部的this。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var obj = {};
var f = F.call(obj);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

obj.a // 1
obj.b // 2
obj.c // 3

上面代码中,执行的是遍历器对象f,但是生成的对象实例是obj,有没有办法将这两个对象统一呢

一个办法就是将obj换成F.prototype

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var f = F.call(F.prototype);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

再将F改成构造函数,就可以对它执行new命令了。

function* gen() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}

function F() {
  return gen.call(gen.prototype);
}

var f = new F();

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3
16、Generator函数的异步应用
1、传统方法

ES6诞生以前,异步编程的方法,大概有以下四种

  • 回调函数
  • 事件监听
  • 发布/订阅
  • Promise 对象
2、基本概念

异步

所谓"异步",简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。

回调函数

JavaScript语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段写在一个函数里,等到重新执行这个任务的时候,直接调用这个函数。

读取文件进行处理,是这样写的

fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
  if (err) throw err;
  console.log(data);
});
// readFile函数的第三个参数就是回调函数

Promise

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

Primise最大的问题是代码冗余

3、Generator函数

协程

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

协程有点像函数,又有点像线程,它的运行流程大致如下

第一步,协程A开始执行;

第二步,协程A执行到一半,进入暂停,执行权转移给协程B;

第三步,(一段时间后),协程B交还执行权;

第四步,协程A恢复执行。

举例来说,读取文件的协程写法如下。

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

协程的Generator函数实现

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

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

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

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

Generator函数的数据交换和错误处理

Generator函数能暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整方案:函数体外的数据交换和错误处理机制。

next返回值的 value 属性,是Generator 函数向外输出数据;next方法还可以接收参数,向Generator函数体内输入数据

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

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

Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。

function* gen(x) {
  try{
    var y = yield x + 1;
  }catch(e) {
    console.log(e)
  }
}

let g = gen(1);
g.next();
g.throw('出错了')
// 出错了

异步任务的封装

使用Generator函数,执行一个真正的异步任务。

var fetch = require('node-fetch');

function* gen(){
  var url = 'https://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){
  return data.json();
}).then(function(data){
  g.next(data);
});
4、Thunk 函数

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

参数的求值策略

传名调用,即直接将表达式传入函数体,只在用到它的时候求值。

var x = 1;

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

f(x + 5)
// return (x + 5) * 2

Thunk函数的含义

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

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

f(x + 5)

// 等同于
var thunk = function() {
  return x + 5;
}
function f(thunk) {
  return thunk() * 2
}

JavaScript 语言的 Thunk 函数

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

// 正常版本的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);

任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。下面是一个简单的 Thunk 函数转换器。

// ES6版本
const Thunk = function(fn) {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback);
    }
  };
};

function f(a, cb) {
  cb(a);
}
const ft = Thunk(f);

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

Thunkify模块

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

首先是安装

$ npm install thunkify

使用方式如下

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

var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
  // ...
});

Generator函数的流程管理

Thunk函数现在可以用户Generator函数的自动流程管理。

Generator函数可以自动执行。

function* gen() {
  // ...
}

var g = gen();
var res = g.next();

while(!res.done){
  console.log(res.value);
  res = g.next();
}

但是,这不适合异步操作。

Thunk 函数的自动流程管理

Thunk 函数真正的威力,在于可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器

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

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

  next();
}

function* g() {
  // ...
}

run(g);
5、co模块

基本用法

用于 Generator 函数的自动执行

下面是一个 Generator 函数,用于依次读取两个文件。

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

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

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

co函数返回一个Promise对象,因此可以用then方法添加回调函数。

co(gen).then(function (){
  console.log('Generator 函数执行完成');
});
17、async 函数
1、含义

async函数是什么?一句话,它就是Generator函数的语法糖。

使用Generator 函数读取文件

const fs = require('fs');

// fs封装成一个promise
const readFile = function(fileName) {
    return new Promise((resolve, reject) => {
        fs.readFile(fileName, (err, data) => {
            if(err) reject(err);
            resolve(data)
        })
    })
}

// generator
function* gen() {
    yield readFile('data/a.txt');
    yield readFile('data/b.txt');
    yield readFile('data/c.txt');
}

let g1 = gen();
g1.next().value.then(res => {
    console.log(res.toString()) // aaaaa
    return g1.next().value;
}).then(res => {
    console.log(res.toString()) // bbbbb
    return g1.next().value;
}).then(res => {
    console.log(res.toString()) // ccccc
})

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

async function fn() {
  let f1 = await readFile('data/a.txt')  // 表示后面结果需要等待
  console.log(f1.toString()) // aaaaa
  let f2 = await readFile('data/b.txt')
  console.log(f2.toString()) // bbbbb
  let f3 = await readFile('data/c.txt')
  console.log(f3.toString()) // ccccc
}
fn();

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

(1)内置执行器

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

asyncReadFile();

(2)更好的语义

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

(3)更广的适应性

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

(4)返回值是Promise对象

async函数的返回值是Promise对象,可以用then方法指定下一步操作。

2、基本用法

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

下面是一个例子,指定多少毫秒后输出值。

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms)
  })
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}
asyncPrint('Hello World', 10)
// 指定10毫秒输出Hello World

由于async返回的是Promise对象,可以作为await命令的参数,以上代码可写成下面这种形式

async function timeout(ms) {
  await new Promise((resolve, reject) => {
    setTimeout(resolve, ms)
  })
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}
asyncPrint('Hello World', 10)
// 指定10毫秒输出Hello World

async函数有多种使用形式

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

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

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

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

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

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

// 箭头函数
const fn = async() => {}
3、语法

返回Promise对象

async函数返回一个Promise对象。

async函数内部return返回的值,会成为then方法回调函数的参数。

async function f() {
  return 'hello world';
}

f().then(res => {
  console.log(res); // 'hello world'
})

async函数内部抛出错误,会导致返回的Promise对象变为reject状态。抛出的错误对象会被catch方法的回调函数接收到。

async function f() {
  throw new Error('出错了')
}

f().then(res => {
  console.log(res);
}).catch(error => {
  console.log(error); 
})
// 出错了

Promise对象的状态变化

async函数返回的Promise对象,必须等到内部所有await命令后面的Promise对象执行完,才会发生状态改变,除非遇到return语句或抛出错误。也就是说,只有async函数内部的所有异步操作执行完,才会执行then方法指定的回调函数。

async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.text();
  return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"

await命令

正常情况下,await命令后面跟的是一个Promise对象,返回该对象的结果。如果不是Promise对象,返回相应的值。

async function f() {
  return await 123;
  // 相当于
  // return 123
}
f().then(res => {
  console.log(res);
})
// 123

另一种情况是,await命令后面是一个thenable对象(即定义then方法的对象),那么await会将其等同于 Promise 对象。

class Sleep {
  constructor(timeout) {
    this.timeout = timeout;
  }
  then(resolve, reject) {
    const startTime = Date.now();
    setTimeout(
      () => resolve(Date.now() - startTime),
      this.timeout
    );
  }
}

(async () => {
  const sleepTime = await new Sleep(1000);
  console.log(sleepTime);
})();
// 1000

await命令后面的Promise对象如果变为reject状态,则reject的参数就会被catch方法的回调函数接收到

async function fn() {
  await Promise.reject('出错了')
}

fn().then(res => {
  console.log(res)
}).catch(err= > {
  console.log(err)
})
// 出错了

任何一个await语句后面的Promise对象的状态变为reject,那么整个async函数就会中断执行。

async function f() {
  await Promise.reject('出错了');
  await Promise.resolve('hello world'); // 不会执行
}

有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这是可以将第一个await语句放在try...catch里面,这样不管第一个异步操作是否成功,都会执行第二个await

async function f() {
  try {
    await Promise.reject('出错了');
  } catch(e) {
  }
  return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// hello world

另一种方法是await后面的Promise对象再跟一个catch方法,处理前面可能出现的错误。

async function f() {
  await Promise.reject('出错了')
    .catch(e => console.log(e));
  return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// 出错了
// hello world

错误处理

如果await后面的异步操作出错,就意味着async函数返回的Promise对象被reject

async function f() {
  await new Promise((resolve, reject) => {
    throw new Error('出错了')
  })
}

f().then(res => {
  console.log(res)
}).catch(error => {
  console.log(error)
})
// 出错了

防止出错的方法,可以将其放在try...catch

如果有多个await语句,可以统一放在try...catch

async function main() {
  try {
    const val1 = await firstStep();
    const val2 = await secondStep(val1);
    const val3 = await thirdStep(val1, val2);

    console.log('Final: ', val3);
  }
  catch (err) {
    console.error(err);
  }
}

使用注意点

第一点,await命令后面的Promise对象,状态可能会为reject,所以最好把await命令放在try...catch代码块中。

async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err);
  }
}

// 另一种写法
async function myFunction() {
  await somethingThatReturnsAPromise()
  .catch(function (err) {
    console.log(err);
  });
}

第二点,多个await命令,如果不存在继发关系,最好让他们同时触发

// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

第三点,await命令只能用在await函数中,用在普通函数中会报错

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  // 报错
  docs.forEach(function (doc) {
    await db.post(doc);
  });
}

// 正确的写法可以采用for循环
async function dbFuc(db) {
  let docs = [{}, {}, {}];

  for (let doc of docs) {
    await db.post(doc);
  }
}

// 另一种方法是使用数组的reduce方法
async function dbFuc(db) {
  let docs = [{}, {}, {}];

  await docs.reduce(async (_, doc) => {
    await _;
    await db.post(doc);
  }, undefined);
}

第四点,async函数可以保留运行堆栈

const a = async () => {
  await b();
  c();
};
// b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()或c()报错,错误堆栈将包括a()。
4、async函数的实现原理

async函数的实现原理,就是将Generator函数和自动执行器,包装在一个函数里

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

// 等同于

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

下面给出spawn函数的实现

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}
5、与其他异步处理方法的比较

通过node的读取文件为例,来看 async 函数与 Promise、Generator 函数的比较。

const fs = require('fs');

// fs封装成一个promise
const readFile = function(fileName) {
    return new Promise((resolve, reject) => {
        fs.readFile(fileName, (err, data) => {
            if(err) reject(err);
            resolve(data)
        })
    })
}

// Promise 写法
readFile('data/a.txt').then(res => {
    console.log(res.toString()) // aaaaa
    return readFile('data/b.txt')
}).then(res => {
    console.log(res.toString()) // bbbbb
    return readFile('data/c.txt')
}).then(res => {
    console.log(res.toString()) // ccccc
})

// Generator函数写法
function* gen() {
    yield readFile('data/a.txt');
    yield readFile('data/b.txt');
    yield readFile('data/c.txt');
}

let g1 = gen();
g1.next().value.then(res => {
    console.log(res.toString()) // aaaaa
    return g1.next().value;
}).then(res => {
    console.log(res.toString()) // bbbbb
    return g1.next().value;
}).then(res => {
    console.log(res.toString()) // ccccc
})

// async 函数写法
async function fn() {
  let [a, b, c] = await Promise.all([
    readFile('data/a.txt'),
    readFile('data/b.txt'),
    readFile('data/c.txt')
  ])
  console.log(a.toString())
  console.log(b.toString())
  console.log(c.toString())
}
6、实例:按顺序完成异步操作
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}

但是问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。我们需要的是并发发出远程请求。

async function logInOrder(urls) {
  // 并发读取远程URL
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });

  // 按次序输出
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}
18、Set和Map数据结构
1、Set

基本用法

ES6提供了新的数据结构Set。它类似于数组,但成员的值都是唯一的,没有重复值。

Set本身是一个构造函数,用于生成Set数据结构。

Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。

// Set ES6新增数据结构,类似数组
let setArr = new Set(['a', 'b', 'c']);
console.log(setArr) // {"a", "b", "c"}

// 但是set里不能有重复的值
let setArr1 = new Set(['a', 'b', 'c', 'b']);
console.log(setArr1) // {"a", "b", "c"}

// 接受类似数组的对象作为参数
const set = new Set(document.querySelectorAll('div'));
set.size // 56

可以用来去重数组

let arr = [1,2,4,5,7,4,3,6,3,2,5];
let setArr = new Set(arr)
console.log(setArr) // {1, 2, 4, 5, 7, 3, 6}

let newArr = [...setArr]
console.log(newArr) // [1, 2, 4, 5, 7, 3, 6]

Set内部判断两个值是否相等,类似于精确相等运算符(===),主要的区别是向Set加入值时认为NaN等于自身,而精确相等运算符认为NaN不等于自身。

let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}
// 这表明,在 Set 内部,两个NaN是相等的。

另外,两个对象是不相等的

let set = new Set();

set.add({});
set.size // 1

set.add({});
set.size // 2

Set实例的属性和方法

Set结构的实例有以下属性

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。

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

  • Set.prototype.add(value): 添加某个值,返回Set结构本身
  • Set.prototype.delete(value): 删除某个值,返回一个布尔值,表示是否删除成功
  • Set.prototype.has(value): 返回一个布尔值,表示该值是否为Set的成员
  • Set.prototype.clear(): 清除所有成员,没有返回值
let setArr = new Set();
setArr.add('a');
setArr.add('b');
// 也可以采用链式写法
setArr.add('c').add('d')

setArr.delete('b');

console.log(setArr.has('b'))  // true

setArr.clear();

console.log(setArr); // {}

Array.from方法可以将 Set 结构转为数组。

const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);

遍历操作

Set 结构的实例有四个遍历方法,可以用于遍历成员。

  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach():使用回调函数遍历每个成员

Set的遍历顺序就是插入顺序。

(1)keys()values()entries()

let setArr = new Set(['a', 'b', 'c', 'd']);
for (let item of setArr) { // 默认是value
  console.log(item) // a b c d
}

for (let key of setArr.keys()) {
  console.log(key) // a b c d
}

for (let value of setArr.values()) {
  console.log(value) // a b c d
}

for (let item of setArr.entries()) {
  console.log(item) // ["a", "a"] ["b", "b"] ["c", "c"] ["d", "d"]
}

for (let [k,v] of setArr.entries()) {
  console.log(k, v) // a a   b b   c c   d d
}

(2)forEach()

Set 结构的实例与数组一样,也拥有forEach方法,用于对每个成员执行某种操作,没有返回值。

let setArr = new Set(['a', 'b', 'c', 'd']);
setArr.forEach((key, value) => {
  console.log(key,value) // a a   b b   c c   d d
})

(3)遍历的应用

扩展运算符(...)内部使用for...of循环,所以也可用于Set结构

let set = new Set(['red', 'green', 'blue']);
let arr = [...set];
// ['red', 'green', 'blue']

扩展运算符与Set结构相结合,就可以去除数组的重复成员

let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)];
// [3, 5, 2]

而且,数组的mapfilter方法也可以用于Set了

let set = new Set([1, 2, 3]);
set = new Set([...set].map(x => x * 2));
// 返回Set结构:{2, 4, 6}

let set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter(x => (x % 2) == 0));
// 返回Set结构:{2, 4}

因此使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。

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}

如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用Array.from方法。

// 方法一
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
2、WeakSet

含义

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

首先,WeakSet 的成员只能是对象,而不能是其他类型的值

const ws = new WeakSet();
ws.add(1)
// TypeError: Invalid value used in weak set
ws.add(Symbol())
// TypeError: invalid value used in weak set

语法

WeakSet 是一个构造函数,可以使用new命令,创建WeakSet数据结构。

const ws = new WeakSet();

WeakSet 结构有以下三个方法

  • WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
  • WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
  • WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
const ws = new WeakSet();
const obj = {};
const foo = {};

ws.add(window);
ws.add(obj);

ws.has(window); // true
ws.has(foo);    // false

ws.delete(window);
ws.has(window);    // false

WeakSet 没有size属性,无法遍历它的成员。

并且,WeakSet没有clear()方法。

3、Map

含义和基本用法

JavaScript的对象(Object),本质上是键值对的集合,但是传统上只能用字符串当作键。

为了解决这个问题,ES6提供了Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以用做键。也就是说,Object结构提供了“字符串-值”的对应,而Map结构提供了“值-值”的对应

const ws = new WeakSet();
const obj = {};
const foo = {};

ws.add(window);
ws.add(obj);

ws.has(window); // true
ws.has(foo);    // false

ws.delete(window);
ws.has(window);    // false

作为构造函数,Map也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。

const map = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);

map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"

SetMap都可以用来生成新的 Map。

const set = new Set([
  ['foo', 1],
  ['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1

const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3

如果对同一个键多次赋值,后面的值将覆盖前面的值。

const map = new Map();

map
.set(1, 'aaa')
.set(1, 'bbb');

map.get(1) // "bbb"

注意,只有对同一个对象的引用,Map结构才将其视为同一个键。

const map = new Map();

map.set(['a'], 555);
map.get(['a']) // undefined 
// 实际上这是两个不同的数组实例,内存地址不一样

同理,同样的值的两个实例,在 Map 结构中被视为两个键。

const map = new Map();

const k1 = ['a'];
const k2 = ['a'];

map
.set(k1, 111)
.set(k2, 222);

map.get(k1) // 111
map.get(k2) // 222

如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefinednull也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。

let map = new Map();

map.set(-0, 123);
map.get(+0) // 123

map.set(true, 1);
map.set('true', 2);
map.get(true) // 1

map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3

map.set(NaN, 123);
map.get(NaN) // 123

实例的属性和操作方法

(1)size属性

size属性返回Map结构的成员总数

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

map.size // 2

(2)Map.prototype.set(key, value)

set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。

const m = new Map();

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

// 同样,也可以使用链式写法
let map = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');

(3)Map.prototype.get(key)

get方法读取key对应的键值,如果找不到key,返回undefined

const m = new Map();

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

m.get(hello)  // Hello ES6!

(4)Map.prototype.has(key)

has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。

const m = new Map();

m.set('edition', 6);
m.set(262, 'standard');
m.set(undefined, 'nah');

m.has('edition')     // true
m.has('years')       // false
m.has(262)           // true
m.has(undefined)     // true

(5)Map.prototype.delete(key)

delete方法删除某个键,返回true。如果删除失败,返回false

const m = new Map();
m.set(undefined, 'nah');
m.has(undefined)     // true

m.delete(undefined)
m.has(undefined)       // false

(6)Map.prototype.clear()

clear方法清除所有成员,没有返回值

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

map.size // 2
map.clear()
map.size // 0

遍历方法

Map 结构原生提供三个遍历器生成函数和一个遍历方法。

  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
  • Map.prototype.forEach():遍历 Map 的所有成员
const map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries()
for (let [key, value] of map) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...

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']]

结合数组的map方法、filter方法,可以实现 Map 的遍历和过滤(Map 本身没有mapfilter方法)。

const map0 = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');

const map1 = new Map(
  [...map0].filter(([k, v]) => k < 3)
);
// 产生 Map 结构 {1 => 'a', 2 => 'b'}

const map2 = new Map(
  [...map0].map(([k, v]) => [k * 2, '_' + v])
    );
// 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'}

此外,Map 还有一个forEach方法,与数组的forEach方法类似,也可以实现遍历。

map.forEach(function(value, key, map) {
  console.log("Key: %s, Value: %s", key, value);
});

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

(1)Map转为数组

Map转数组最方便的方法,就是使用扩展运算符(...)

const myMap = new Map()
.set(true, 10)
.set(undefined, 'hello')

[...myMap] // [[true, 10], [undefined, 'hello']]

(2)数组转Map

将数组传入 Map 构造函数,就可以转为 Map

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

(3)Map转对象

如果所有 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 }

(4)对象转Map

对象转为 Map 可以通过Object.entries()

let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));

(5)Map转JSON

Map 转为 JSON 要区分两种情况。一种情况是,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}'

另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。

function mapToArrayJson(map) {
  return JSON.stringify([...map]);
}

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

(6)JSON转Map

JSON 转为 Map,正常情况下,所有键名都是字符串。

function jsonToStrMap(jsonStr) {
  return objToStrMap(JSON.parse(jsonStr));
}

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

但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。

function jsonToMap(jsonStr) {
  return new Map(JSON.parse(jsonStr));
}

jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
4、WeakMap

含义

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

// WeakMap 可以使用 set 方法添加成员
const wm1 = new WeakMap();
const key = {foo: 1};
wm1.set(key, 2);
wm1.get(key) // 2

// WeakMap 也可以接受一个数组,
// 作为构造函数的参数
const k1 = [1, 2, 3];
const k2 = [4, 5, 6];
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
wm2.get(k2) // "bar"

WeakMapMap的区别有两点。

首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。

const map = new WeakMap();
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key

WeakMap的语法

WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操作(即没有keys()values()entries()方法),也没有size属性。

WeakMap只有四个方法可用:get()set()has()delete()

const wm = new WeakMap();

// size、forEach、clear 方法都不存在
wm.size // undefined
wm.forEach // undefined
wm.clear // undefined
19、数值的扩展
1、二进制和八进制表示法

ES6提供二进制和八进制新的表示法,分别用前缀0b(或oB)和 0o(或0O)表示

0b111110111 === 503 // true
0o767 === 503 // true
2、Number.isFinite(), Number.isNaN()

ES6在Number对象上,新增了Number.isFinite()Number.isNaN()两个方法

Number.isFinite()用来检查一个数值是否为有限的,即不是Infinity

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

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

Number.isNaN()用来检查一个值是不是NaN

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

注意,如果参数类型不是NaN,一律返回false

3、Number.parseInt()、Number.parseFloat()

ES6将全局方法parseInt()parseFloat(),移植到Number上面,行为完全保持不变。这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。

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

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

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

Number.isInteger(25) // true
Number.isInteger(25.1) // false
Number.isInteger(25.0) // true
Number.isInteger('abc') // false
Number.isInteger(true) // false
Number.isInteger(null) // false
5、Number.EPSILON

ES6 在Number对象上,新增了一个极小的常量Number.EPSILON。根据规则,它表示1与大于1的最小浮点数之间的差。

对于 64 位浮点数来说,大于 1 的最小浮点数相当于二进制的1.00..001,小数点后面有连续 51 个零。这个值减去 1 之后,就等于 2 的 -52 次方

Number.EPSILON === Math.pow(2, -52)
// true
Number.EPSILON
// 2.220446049250313e-16
Number.EPSILON.toFixed(20)
// "0.00000000000000022204"

Number.EPSILON实际上是 JavaScript 能够表示的最小精度。

6、安全整数和Number.isSafeInteger

JavaScript 能够准确表示的整数范围在-2^532^53之间(不含两个端点),超过这个范围,无法精确表示这个值。

Math.pow(2, 53) // 9007199254740992

9007199254740992  // 9007199254740992
9007199254740993  // 9007199254740992

Math.pow(2, 53) === Math.pow(2, 53) + 1
// true

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

Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
// true
Number.MAX_SAFE_INTEGER === 9007199254740991
// true

Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER
// true
Number.MIN_SAFE_INTEGER === -9007199254740991
// true

Number.isSafeInteger()用来判断一个整数是否在安全范围内。

Number.isSafeInteger('a') // false
Number.isSafeInteger(null) // false
Number.isSafeInteger(NaN) // false
Number.isSafeInteger(Infinity) // false
Number.isSafeInteger(-Infinity) // false

Number.isSafeInteger(3) // true
Number.isSafeInteger(1.2) // false
Number.isSafeInteger(9007199254740990) // true
Number.isSafeInteger(9007199254740992) // false

Number.isSafeInteger(Number.MIN_SAFE_INTEGER - 1) // false
Number.isSafeInteger(Number.MIN_SAFE_INTEGER) // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER) // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1) // false
7、Math对象的扩展

ES6 在 Math 对象上新增了 17 个与数学相关的方法。所有这些方法都是静态方法,只能在 Math 对象上调用。

Math.trunc()

Math.trunc方法用于去除一个数值的小数部分,返回整数部分。

Math.trunc(4.1) // 4
Math.trunc(4.9) // 4
Math.trunc(-4.1) // -4
Math.trunc(-4.9) // -4
Math.trunc(-0.1234) // -0

对于非数值,Math.trunc内部使用Number方法先将其转为数值。

Math.trunc('123.456') // 123
Math.trunc(true) //1
Math.trunc(false) // 0
Math.trunc(null) // 0

对于空值和无法截取的值,返回NaN

Math.trunc(NaN);      // NaN
Math.trunc('foo');    // NaN
Math.trunc();         // NaN
Math.trunc(undefined) // NaN

Math.sign()

Math.sign方法用来判断一个值是正数、负数还是0。对于非数值,会将其先转换为数值。

它会返回五种值。

  • 参数为正数:返回+1
  • 参数为负数:返回-1
  • 参数为 0:返回0
  • 参数为 -0:返回-0
  • 其他值:返回NaN
Math.sign(-5) // -1
Math.sign(5) // +1
Math.sign(0) // +0
Math.sign(-0) // -0
Math.sign(NaN) // NaN

Math.sign('')  // 0
Math.sign(true)  // +1
Math.sign(false)  // 0
Math.sign(null)  // 0
Math.sign('9')  // +1
Math.sign('foo')  // NaN
Math.sign()  // NaN
Math.sign(undefined)  // NaN

Math.cbrt()

Math.cbrt方法用于计算一个值的立方根。对于非数值,先将其转换为数值。

Math.cbrt(-1) // -1
Math.cbrt(0)  // 0
Math.cbrt(1)  // 1
Math.cbrt(2)  // 1.2599210498948734

Math.cbrt('8') // 2
Math.cbrt('hello') // NaN

Math.clz32()

Math.clz32()方法将参数转为 32 位无符号整数的形式,然后返回这个 32 位值里面有多少个前导 0。对于非数值,先将其转换为数值。

Math.clz32(0) // 32
Math.clz32(1) // 31
Math.clz32(1000) // 22
Math.clz32(0b01000000000000000000000000000000) // 1
Math.clz32(0b00100000000000000000000000000000) // 2

// 对于小数,只考虑整数部分
Math.clz32(3.2) // 30
Math.clz32(3.9) // 30

// 非数值,先转换
Math.clz32() // 32
Math.clz32(NaN) // 32
Math.clz32(Infinity) // 32
Math.clz32(null) // 32
Math.clz32('foo') // 32
Math.clz32([]) // 32
Math.clz32({}) // 32
Math.clz32(true) // 31

上面代码中,0 的二进制形式全为 0,所以有 32 个前导 0;1 的二进制形式是0b1,只占 1 位,所以 32 位之中有 31 个前导 0;1000 的二进制形式是0b1111101000,一共有 10 位,所以 32 位之中有 22 个前导 0。

Math.imul()

Math.imul方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。

Math.imul(2, 4)   // 8
Math.imul(-1, 8)  // -8
Math.imul(-2, -2) // 4
// 如果只考虑最后 32 位,大多数情况下,Math.imul(a, b)与a * b的结果是相同的,即该方法等同于(a * b)|0的效果(超过 32 位的部分溢出)

等等其他方法,一般情况用不到。

8、指数运算符

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

2**3 // 8

这个运算符的一个特点是右运算,即多个指数运算符连用时,从右边开始计算。

// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2
// 512

指数运算符可以与等号结合,形成一个新的赋值运算符(**=)。

let a = 1.5;
a **= 2;
// 等同于 a = a * a;

let b = 4;
b **= 3;
// 等同于 b = b * b * b;
9、BigInt 数据类型

ES2020 引入了一种新的数据类型 BigInt(大整数)。BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。

为了与 Number 类型区别,BigInt 类型的数据必须添加后缀n

1234 // 普通整数
1234n // BigInt

// BigInt 的运算
1n + 2n // 3n

BigInt 同样可以使用各种进制表示,都要加上后缀n

0b1101n // 二进制
0o777n // 八进制
0xFFn // 十六进制

BigInt 与普通整数是两种值,它们之间并不相等。

42n === 42 // false

typeof运算符对于 BigInt 类型的数据返回bigint

typeof 123n // 'bigint'

BigInt 可以使用负号(-),但是不能使用正号(+),因为会与 asm.js 冲突。

-42n // 正确
+42n // 报错

BigInt对象

JavaScript 原生提供BigInt对象,可以用作构造函数生成 BigInt 类型的数值。转换规则基本与Number()一致,将其他类型的值转为 BigInt。

BigInt(123) // 123n
BigInt('123') // 123n
BigInt(false) // 0n
BigInt(true) // 1n

// 必须有参数,并且参数可以正常转换为数值
new BigInt() // TypeError
BigInt(undefined) //TypeError
BigInt(null) // TypeError
BigInt('123n') // SyntaxError
BigInt('abc') // SyntaxError

// 如果参数是小数,也会报错
BigInt(1.5) // RangeError
BigInt('1.5') // SyntaxError

BigInt 对象继承了 Object 对象的两个实例方法。

  • BigInt.prototype.toString()
  • BigInt.prototype.valueOf()

它还继承了 Number 对象的一个实例方法。

  • BigInt.prototype.toLocaleString()

此外,还提供了三个静态方法。

  • BigInt.asUintN(width, BigInt): 给定的 BigInt 转为 0 到 2width - 1 之间对应的值。
  • BigInt.asIntN(width, BigInt):给定的 BigInt 转为 -2width - 1 到 2width - 1 - 1 之间对应的值。
  • BigInt.parseInt(string[, radix]):近似于Number.parseInt(),将一个字符串转换成指定进制的 BigInt。

转换规则

可以使用Boolean()Number()String()这三个方法,将 BigInt 可以转为布尔值、数值和字符串类型。

Boolean(0n) // false
Boolean(1n) // true
Number(1n)  // 1
String(1n)  // "1"

另外,取反运算符(!)也可以将 BigInt 转为布尔值。

!0n // true
!1n // false

数学运算

数学运算方面,BigInt 类型的+-***这四个二元运算符,与 Number 类型的行为一致。除法运算/会舍去小数部分,返回一个整数。

9n / 5n
// 1n

几乎所有的数值运算符都可以用在 BigInt,但是有两个例外。

  • 不带符号的右移位运算符>>>
  • 一元的求正运算符+

BigInt 不能与普通数值进行混合运算。

1n + 1.3 // 报错

其他运算

BigInt 对应的布尔值,与 Number 类型一致,即0n会转为false,其他值转为true

if (0n) {
  console.log('if');
} else {
  console.log('else');
}
// else
20、ES2018新增(正则扩展及标签函数)
1、正则的扩展

s 修饰符:dotAll模式

正则表达式中,点(.)是一个特殊字符,代表任意的单个字符,但是有两个例外。一个是四个字节的 UTF-16 字符,这个可以用u修饰符解决;另一个是行终止符(line terminator character)。

所谓行终止符,就是该字符表示一行的终结。以下四个字符属于“行终止符”。

  • U+000A 换行符(\n
  • U+000D 回车符(\r
  • U+2028 行分隔符(line separator)
  • U+2029 段分隔符(paragraph separator)
/foo.bar/.test('foo\nbar')
// false

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

/foo.bar/s.test('foo\nbar') // true
let reg = /^\w+.\w+$/;
let reg1 = /^\w+.\w+$/s;
let str = 'welcome$toChina';
let str1 = 'welcome\ntoChina';

console.log(reg.test(str)) // true
console.log(reg.test(str1)) // false
console.log(reg1.test(str1)) // true

这被称为dotAll模式,即点(dot)代表一切字符。所以,正则表达式还引入了一个dotAll属性,返回一个布尔值,表示该正则表达式是否处在dotAll模式。

const re = /foo.bar/s;
// 另一种写法
// const re = new RegExp('foo.bar', 's');

re.test('foo\nbar') // true
re.dotAll // true
re.flags // 's'

/s修饰符和多行修饰符/m不冲突,两者一起使用的情况下,.匹配所有字符,而^$匹配每一行的行首和行尾。

具名组匹配

简介

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

let reg = /(\d{4})-(\d{2})-(\d{2})/;

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

let str = '2020-04-03';
let reg = /(\d{4})-(\d{2})-(\d{2})/;
console.log(reg.exec(str)) //  ["2020-04-03", "2020", "04", "03", index: 0, input: "2020-04-03", groups: {…}]

let dateArr = reg.exec(str)
let year = dateArr[1]
let month = dateArr[2]
let day = dateArr[3]

组匹配的一个问题是,每一组的匹配含义不容易看出来,而且只能用数字序号引用,要是组的顺序变了,引用的时候就必须修改序号。

ES2018引入了具名组匹配,允许为每一个组匹配指定一个名字,既便于阅读代码,也便于引用。

let str = '2020-04-03';
let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
console.log(str.match(reg).groups) // {year: "2020", month: "04", day: "03"}
let {year, month, day} = str.match(reg).groups
console.log(year, month, day)  // 2020 04 03

如果具名组没有匹配,那么对应的groups属性会是undefined

const RE_OPT_A = /^(?<as>a+)?$/;
const matchObj = RE_OPT_A.exec('');

matchObj.groups.as // undefined
'as' in matchObj.groups // true

解构赋值和替换

有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值。

let str = '2020-04-03';
let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
console.log(str.match(reg).groups) // {year: "2020", month: "04", day: "03"}
let {year, month, day} = str.match(reg).groups
console.log(year, month, day)  // 2020 04 03

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

let str = '2020-04-03';
let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
str = str.replace(reg, '$<year>/$<month>/$<day>')
console.log(str) // 2020/04/03

replace方法的第二个参数也可以是一个回调函数

let str = '2020-04-03';
let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
str1 = str.replace(reg, (...args) => {
  console.log(args) // ["2020-04-03", "2020", "04", "03", 0, "2020-04-03", {…}]
  let {year, month, day} = args[args.length - 1];
  return `${year}/${month}/${day}`
})
console.log(str1) // 2020/04/03

引用

如果要在正则表达式内部引用某个“具名组匹配”,可以使用\k<组名>的写法。

let reg = /^(?<str>welcome)-\k<str>$/;

let str2 = 'str-str';
let str3 = 'welcome-welcome'

console.log(reg.test(str2)) // false
console.log(reg.test(str3)) // true

数字引用(\1)依然有效。

const RE_TWICE = /^(?<word>[a-z]+)!\1$/;
RE_TWICE.test('abc!abc') // true
RE_TWICE.test('abc!ab') // false

这两种引用语法还可以同时使用。

const RE_TWICE = /^(?<word>[a-z]+)!\k<word>!\1$/;
RE_TWICE.test('abc!abc!abc') // true
RE_TWICE.test('abc!abc!ab') // false
21、Proxy
1、概述

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

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写

let obj = {
  name: 'Jason'
}

let newObj = new Proxy(obj, {
  get(target, property) {
    console.log(target, property) // {name: "Jason"} "name"
    return `您访问了${property}属性`
  }
})
console.log(newObj.name) // 您访问了name属性

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

var proxy = new Proxy(target, handler)

Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy生成一个Proxy实例,target表示所要拦截的对象,handler也是一个对象,用来定制拦截行为。

var proxy = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35

作为构造函数,Proxy接收两个参数,第一个参数是所要代理目标的对象,第二个参数是配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。

注意,要使得Proxy起作用,必须对Proxy实例进行操作,而不是针对目标对象进行操作。

如果handler没有设置任何拦截,那就等同于直接通向原对象。

var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"

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

var proxy = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
});

let obj = Object.create(proxy);
obj.time // 35
2、Proxy实例的方法

get()

get方法用于拦截某个属性的读取操作,可以接收三个参数,依次为目标对象,属性名和proxy实例本身,最后一个参数可选。

let obj = {
  name: 'Jason'
}

let newObj = new Proxy(obj, {
  get(target, property) {
    if (property in target) {
      return target[property]
    } else {
      // throw new ReferenceError(`${property}属性不在此对象内`)
      console.warn(`${property}属性不在此对象内`)
      return '警告'
    }

  }
})

console.log(newObj.name) // Jason
console.log(newObj.age) // ⚠️ age属性不在此对象内 警告

get方法可以继承。

let proto = new Proxy({}, {
  get(target, propertyKey, receiver) {
    console.log('GET ' + propertyKey);
    return target[propertyKey];
  }
});

let obj = Object.create(proto);
obj.foo // "GET foo"

下面的例子则是利用get拦截,实现一个生成各种 DOM 节点的通用函数dom

const DOM = new Proxy({}, {
  get(target, property) {
    console.log(target, property) // {} "div"
    return function(attr, ...children) {
      console.log(attr, children) // {id: "div1", class: "box"} (2) ["我是一个div", "这是我的内容"]

      const el = document.createElement(property)

      // 添加属性
      for (let attrKey of Object.keys(attr)) {
        el.setAttribute(attrKey, attr[attrKey])
      }

      // 添加内容
      for (let child of children) {
        if (typeof child == 'string') {
          child = document.createTextNode(child)
        }
        el.appendChild(child)
      }
      return el;
    }
  }
})

// 可以创建各种原生标签
let oDiv = DOM.div(
  {id: 'div1', class: 'box'}, '我是一个div', '这是我的内容',
  DOM.a({href: 'https://www.baidu.com/'}, '我是百度链接')
)

console.log(oDiv)

window.onload = function() {
  document.body.appendChild(oDiv)
}

set()

set方法用来拦截某个属性的赋值操作,可以接收四个参数,依次为目标对象、属性名、属性值和Proxy实例本身,最后一个参数可选。

假定Person对象有一个age属性,该属性应该是一个不大于 200 的整数,那么可以使用Proxy保证age的属性值符合要求

let validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }

    // 对于满足条件的 age 属性以及其他属性,直接保存
    obj[prop] = value;
  }
};

let person = new Proxy({}, validator);

person.age = 100;

person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错

有时,我们会在对象上面设置内部属性,属性名的第一个字符使用下划线开头,表示这些属性不应该被外部使用。结合getset方法,就可以做到防止这些内部属性被外部读写。

const handler = {
  get (target, key) {
    invariant(key, 'get');
    return target[key];
  },
  set (target, key, value) {
    invariant(key, 'set');
    target[key] = value;
    return true;
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}
const target = {};
const proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property

apply()

apply方法拦截函数的调用、callapply操作。

apply方法接收三个参数,依次为目标对象、目标对象的上下文对象(即this)、目标对象的参数数组。

var target = function () { return 'I am the target'; };
var handler = {
  apply: function () {
    return 'I am the proxy';
  }
};

var p = new Proxy(target, handler);

p()
// "I am the proxy"

下面是另外一个例子。

function sum(a, b) {
  return a + b;
}

let newSum = new Proxy(sum, {
  apply(target, context, args) {
    // console.log(target, context, args)
    console.log(...arguments)
    return Reflect.apply(...arguments)
  }
})

console.log(newSum(2,4)) // 6

has()

has方法用来拦截hasProperty操作,即判断对象是否有某个属性时,这个方法会生效。典型的操作就是in运算符。

has方法接收两个参数,依次为目标对象、需查询的属性名。

let obj = {
  a: 1,
  b: 2
}

let newObj = new Proxy(obj, {
  deleteProperty(target, property) {
    // console.log(target, property)
    console.log(`您要删除${property}属性`)
    delete target[property]
  },
  has(target, property) {
    console.log('判断是否存在,调用has方法')
    return property in target
  }
})

// delete newObj.a;
// console.log(newObj) // {b: 2}

console.log('a' in obj) // true

construct()

construct方法用于拦截new命令。

construct方法接收三个参数,依次是目标对象、构造函数的参数对象(args)、创建实例对象时,new命令作用的构造函数

var handler = {
  construct (target, args, newTarget) {
    return new target(...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"
// 10

construct方法返回的必须是一个对象,否则报错

var p = new Proxy(function() {}, {
  construct: function(target, argumentsList) {
    return 1;
  }
});

new p() // 报错
// Uncaught TypeError: 'construct' on proxy: trap returned non-object ('1')

deleteProperty()

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

var handler = {
  deleteProperty (target, key) {
    invariant(key, 'delete');
    delete target[key];
    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

defineProperty()

defineProperty方法拦截了Object.defineProperty操作。

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

上面代码中,defineProperty方法返回false,导致添加新属性总是无效。

注意,如果目标对象不可扩展(non-extensible),则defineProperty不能增加目标对象上不存在的属性,否则会报错。另外,如果目标对象的某个属性不可写(writable)或不可配置(configurable),则defineProperty方法不得改变这两个设置。

**getOwnPropertyDescriptor() **

getOwnPropertyDescriptor方法拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined

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 }

getPrototypeOf()

getPrototypeOf方法主要用来拦截获取对象原型。具体来说,拦截下面这些操作。

  • Object.prototype.__proto__
  • Object.prototype.isPrototypeOf()
  • Object.getPrototypeOf()
  • Reflect.getPrototypeOf()
  • instanceof
var proto = {};
var p = new Proxy({}, {
  getPrototypeOf(target) {
    return proto;
  }
});
Object.getPrototypeOf(p) === proto // true

注意,getPrototypeOf方法的返回值必须是对象或者null,否则报错。另外,如果目标对象不可扩展(non-extensible), getPrototypeOf方法必须返回目标对象的原型对象。

isExtensible()

isExtensible方法拦截Object.isExtensible操作。

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

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

注意,该方法只能返回布尔值,否则返回值会被自动转为布尔值。

这个方法有一个强限制,它的返回值必须与目标对象的isExtensible属性保持一致,否则就会抛出错误。

Object.isExtensible(proxy) === Object.isExtensible(target)
var p = new Proxy({}, {
  isExtensible: function(target) {
    return false;
  }
});

Object.isExtensible(p)
// Uncaught TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is 'true')

ownKeys()

ownKeys方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作。

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
  • for...in循环
let target = {
  a: 1,
  b: 2,
  c: 3
};

let handler = {
  ownKeys(target) {
    return ['a'];
  }
};

let proxy = new Proxy(target, handler);

Object.keys(proxy)
// [ 'a' ]

注意,使用Object.keys方法时,有三类属性会被ownKeys方法自动过滤,不会返回。

  • 目标对象上不存在的属性
  • 属性名为 Symbol 值
  • 不可遍历(enumerable)的属性

preventExtensions()

preventExtensions方法拦截Object.preventExtensions()。该方法必须返回一个布尔值,否则会被自动转为布尔值。

这个方法有一个限制,只有目标对象不可扩展时(即Object.isExtensible(proxy)false),proxy.preventExtensions才能返回true,否则会报错。

var proxy = new Proxy({}, {
  preventExtensions: function(target) {
    return true;
  }
});

Object.preventExtensions(proxy)
// Uncaught TypeError: 'preventExtensions' on proxy: trap returned truish but the proxy target is extensible

为了防止出现这个问题,通常要在proxy.preventExtensions方法里面,调用一次Object.preventExtensions

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

Object.preventExtensions(proxy)
// "called"
// Proxy {}

setPrototypeOf()

setPrototypeOf方法主要用来拦截Object.setPrototypeOf方法。

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

注意,该方法只能返回布尔值,否则会被自动转为布尔值。另外,如果目标对象不可扩展(non-extensible),setPrototypeOf方法不得改变目标对象的原型。

3、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

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

Proxy.revocable的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。

4、this问题

虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 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

上面代码中,一旦proxy代理target.m,后者内部的this就是指向proxy,而不是target

22、Reflect
1、概述

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。

(1)将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。

(2)修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false

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

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

(3) 让Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为

// 老写法
'assign' in Object // true

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

(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

Proxy(target, {
  set: function(target, name, value, receiver) {
    var success = Reflect.set(target, name, value, receiver);
    if (success) {
      console.log('property ' + name + ' on ' + target + ' set to ' + value);
    }
    return success;
  }
});
2、静态方法

这些静态方法与Proxy对象的方法是一一对应的。

Reflect.get(target, name, receiver)

Reflect.get方法查找并返回target对象的name属性,如果没有该属性,则返回undefined

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

Reflect.get(myObject, 'foo') // 1
Reflect.get(myObject, 'bar') // 2
Reflect.get(myObject, 'baz') // 3

如果name属性部署了读取函数(getter),则读取函数的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.get方法会报错。

Reflect.get(1, 'foo') // 报错
Reflect.get(false, 'foo') // 报错

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

Reflect.set方法设置target对象的name属性等于value

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

myObject.foo // 1

Reflect.set(myObject, 'foo', 2);
myObject.foo // 2

Reflect.set(myObject, 'bar', 3)
myObject.foo // 3

注意,如果 Proxy对象和 Reflect对象联合使用,前者拦截赋值操作,后者完成赋值的默认行为,而且传入了receiver,那么Reflect.set会触发Proxy.defineProperty拦截。

let p = {
  a: 'a'
};

let handler = {
  set(target, key, value, receiver) {
    console.log('set');
    Reflect.set(target, key, value, receiver)
  },
  defineProperty(target, key, attribute) {
    console.log('defineProperty');
    Reflect.defineProperty(target, key, attribute);
  }
};

let obj = new Proxy(p, handler);
obj.a = 'A';
// set
// defineProperty

Reflect.has(obj, name)

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

var myObject = {
  foo: 1,
};

// 旧写法
'foo' in myObject // true

// 新写法
Reflect.has(myObject, 'foo') // true

Reflect.deleteProperty(obj, name)

Reflect.deleteProperty方法等同于delete obj[name],用于删除对象的属性。

const myObj = { foo: 'bar' };

// 旧写法
delete myObj.foo;

// 新写法
Reflect.deleteProperty(myObj, 'foo');

该方法返回一个布尔值。如果删除成功,或者被删除的属性不存在,返回true;删除失败,被删除的属性依然存在,返回false

Reflect.construct(target, args)

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

function Greeting(name) {
  this.name = name;
}

// new 的写法
const instance = new Greeting('张三');

// Reflect.construct 的写法
const instance = Reflect.construct(Greeting, ['张三']);

Reflect.getPrototypeOf(obj)

Reflect.getPrototypeOf方法用于读取对象的__proto__属性,对应Object.getPrototypeOf(obj)

const myObj = new FancyThing();

// 旧写法
Object.getPrototypeOf(myObj) === FancyThing.prototype;

// 新写法
Reflect.getPrototypeOf(myObj) === FancyThing.prototype;

Reflect.getPrototypeOfObject.getPrototypeOf的一个区别是,如果参数不是对象,Object.getPrototypeOf会将这个参数转为对象,然后再运行,而Reflect.getPrototypeOf会报错。

Object.getPrototypeOf(1) // Number {[[PrimitiveValue]]: 0}
Reflect.getPrototypeOf(1) // 报错

Reflect.setPrototypeOf(obj, newProto)

Reflect.setPrototypeOf方法用于设置目标对象的原型(prototype),对应Object.setPrototypeOf(obj, newProto)方法。它返回一个布尔值,表示是否设置成功。

const myObj = {};

// 旧写法
Object.setPrototypeOf(myObj, Array.prototype);

// 新写法
Reflect.setPrototypeOf(myObj, Array.prototype);

myObj.length // 0

Reflect.apply(func, thisArg, args)

Reflect.apply方法等同于Function.prototype.apply.call(func, 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.defineProperty(target, propertyKey, attributes)

Reflect.defineProperty方法基本等同于Object.defineProperty,用来为对象定义属性。未来,后者会被逐渐废除,请从现在开始就使用Reflect.defineProperty代替它。

function MyDate() {
  /*…*/
}

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

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

这个方法可以与Proxy.defineProperty配合使用。

const p = new Proxy({}, {
  defineProperty(target, prop, descriptor) {
    console.log(descriptor);
    return Reflect.defineProperty(target, prop, descriptor);
  }
});

p.foo = 'bar';
// {value: "bar", writable: true, enumerable: true, configurable: true}

p.foo // "bar"

Reflect.getOwnPropertyDescriptor(target, propertyKey)

Reflect.getOwnPropertyDescriptor基本等同于Object.getOwnPropertyDescriptor,用于得到指定属性的描述对象,将来会替代掉后者。

var myObject = {};
Object.defineProperty(myObject, 'hidden', {
  value: true,
  enumerable: false,
});

// 旧写法
var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');

// 新写法
var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden');

Reflect.isExtensible (target)

Reflect.isExtensible方法对应Object.isExtensible,返回一个布尔值,表示当前对象是否可扩展。

const myObject = {};

// 旧写法
Object.isExtensible(myObject) // true

// 新写法
Reflect.isExtensible(myObject) // true

Reflect.preventExtensions(target)

Reflect.preventExtensions对应Object.preventExtensions方法,用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操作成功。

var myObject = {};

// 旧写法
Object.preventExtensions(myObject) // Object {}

// 新写法
Reflect.preventExtensions(myObject) // true

Reflect.ownKeys (target)

Reflect.ownKeys方法用于返回对象的所有属性,基本等同于Object.getOwnPropertyNamesObject.getOwnPropertySymbols之和。

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

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

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

// 新写法
Reflect.ownKeys(myObject)
// ['foo', 'bar', Symbol(baz), Symbol(bing)]
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值