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网页中,浏览器通过

你可能感兴趣的:(ES6基础(内含Promise、Symbol、Set、Map等基本使用))