ECMAScript6(ES6学习笔记)

134 阅读33分钟

🥷let & const

let命令

var声明的变量在全局范围内有效,let声明的变量只在它所在的代码块有效(适用于for循环的计数器)

{
    let a = 10;
    var b = 1;
}
console.log(a); // Uncaught ReferenceError: a is not defined
console.log(b); // 1

for (let i = 0; i < 10; i++) { // 注意这里的i是一个父作用域
    console.log(i);
}

var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。而let命令所声明的变量一定要在声明后使用,否则报错。

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

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

如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。这在语法上,称为“暂时性死区”。暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

🎯const命令

const声明一个只读的常量。一旦声明,常量的值就不能改变,且必须立即初始化。

const foo; // 'const' declarations must be initialized
const PI = 3.14;
PI = 5.14; // Inline Babel script: "PI" is read-only

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

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

ES6声明变量的6种方法:var命令、function命令、let命令、const命令、import命令和class命令

🥷变量的解构赋值

数组的解构赋值

从数组和对象中提取值,对变量进行赋值,这被称为解构

let [a, b, c] = [1, 2, 3];
let [x, y, ...z] = ['a']; // x="a"  y=undefined  z=[]

解构赋值允许指定默认值(只有当一个数组成员严格等于undefined,默认值才会生效)

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

对象的解构赋值

对象的解构中,对象的属性没有次序,变量必须与属性同名,才能取到正确的值。如果解构失败,变量的值等于undefined

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

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

let { log, sin, cos } = Math;

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

对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。如下代码,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined

对象的解构也可以指定默认值,但是默认值生效的条件是对象的属性值严格等于undefined

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

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

字符串的解构赋值

let {length : len} = 'hello';   // len=5

数值布尔值的解构赋值

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

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

函数参数的解构赋值

[[1, 2], [3, 4]].map(([a, b]) => a + b);   // [ 3, 7 ]

undefined就会触发函数参数的默认值:
[1, undefined, 3].map((x = 'yes') => x);  // [ 1, 'yes', 3 ]

变量的解构赋值用途

交换变量的值

let x = 1;
let y = 2;

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

从函数返回多个值

函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。

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

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

函数参数的定义

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

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

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

🎯提取JSON数据

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

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

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

函数参数的默认值

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

遍历Map结构

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

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

🎯输入模块的指定方法

加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。

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

字符串的扩展

字符的Unicode表示法

ES6允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的Unicode码点

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

🎯模板字符串

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

模板字符串中嵌入变量,需要将变量名写在 ${} 之中。

注意,所有模板字符串的空格和换行都是被保留的。如果不想要这个换行,可以使用trim() 方法消除它

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

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

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

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

如果需要引用模板字符串本身,在需要时执行,可以写成函数。模板字符串写成一个函数的返回值,执行这个函数,就相当于执行这个模板字符串了。

let func = (name) => `Hello ${name}!`;
func('Jack') // "Hello Jack!"

字符串新增方法

  • String.fromCodePoint()
    • 此方法定义在String对象上,用于从Unicode码点返回对应字符
console.log(String.fromCharCode(0x20BB7)); // 错误:ஷ
console.log(String.fromCodePoint(0x20BB7)); // 正确:𠮷
  • String.raw()
    • 该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法
String.raw`Hi\n` === "Hi\\n" // true
  • 实例方法:codePointAt()
    • 正确处理4个字节储存的字符,返回一个字符的码点
  • 实例方法:normalize()
    • 将字符的不同表示方法统一为同样的形式,称为Unicode正规化
  • 实例方法:includes(), startsWith(), endsWith()
    • includes() :返回布尔值,表示是否找到了参数字符串
    • startsWith() :返回布尔值,表示参数字符串是否在原字符串的头部
    • endsWith() :返回布尔值,表示参数字符串是否在原字符串的尾部
    • 这三个方法都支持第二个参数,表示开始搜索的位置(使用第二个参数n时,endsWith的行为与其他两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束)
let s = 'Hello world!';

s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false
  • 实例方法:repeat()
    • 返回一个新字符串,表示将原字符串重复n次
'x'.repeat(3) // "xxx"
  • 实例方法:padStart(),padEnd()
    • 如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。
    • 一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'

'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
  • 实例方法:trimStart(),trimEnd()
    • trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。
const s = '  abc  ';

s.trim() // "abc"
s.trimStart() // "abc  "
s.trimEnd() // "  abc"
  • 实例方法:matchAll()
    • matchAll()方法返回一个正则表达式在当前字符串的所有匹配
  • 实例方法:replaceAll()
    • replaceAll()的第二个参数replacement是一个字符串,表示替换的文本,其中可以使用一些特殊字符串。
      • $&:匹配的字符串。
      • $` :匹配结果前面的文本。
      • $':匹配结果后面的文本。
      • $n:匹配成功的第n组内容,n是从1开始的自然数。这个参数生效的前提是,第一个参数必须是正则表达式。
      • $$:指代美元符号$。
'aabbcc'.replaceAll('b', '_')   // 'aa__cc'
  • 实例方法:at()
    • at()方法接受一个整数作为参数,返回参数指定位置的字符,支持负索引(即倒数的位置)
const str = 'hello';
str.at(1) // "e"
str.at(-1) // "o"

数值的扩展

  • 二进制和八进制
    • ES6提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示
    • 如果要将0b和0o前缀的字符串数值转为十进制,要使用Number方法
0b111110111 === 503 // true
0o767 === 503 // true
Number('0b111')  // 7
Number('0o10')  // 8
  • 数值分隔符
    • 不能放在数值的最前面(leading)或最后面(trailing)
    • 不能两个或两个以上的分隔符连在一起
    • 小数点的前后不能有分隔符
    • 科学计数法里面,表示指数的e或E前后不能有分隔符
    • 分隔符不能紧跟着进制的前缀0b、0B、0o、0O、0x、0X
let budget = 1_000_000_000_000;
  • Number.isFinite(), Number.isNaN()
    • Number.isFinite()用来检查一个数值是否为有限的(finite),即不是Infinity。如果参数类型不是数值,Number.isFinite一律返回false
    • Number.isNaN()用来检查一个值是否为NaN。如果参数类型不是NaN,Number.isNaN一律返回false
  • Number.parseInt(), Number.parseFloat()
// ES6的写法
Number.parseInt('12.34') // 12
Number.parseFloat('123.45#') // 123.45
  • Number.isInteger()
    • 用来判断一个数值是否为整数
    • 如果对数据精度的要求较高,不建议使用Number.isInteger()判断一个数值是否为整数
Number.isInteger(25) // true
Number.isInteger(25.0) // true  25等同于25.0
Number.isInteger(25.1) // false
  • Number.EPSILON
    • 常量Number.EPSILON,表示1与大于1的最小浮点数之间的差
    • 它是JavaScript能够表示的最小精度,可以用来设置“能够接受的误差范围”
function withinErrorMargin (left, right) {
  return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}

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

1.1 + 1.3 === 2.4 // false
withinErrorMargin(1.1 + 1.3, 2.4) // true
  • 安全整数和Number.isSafeInteger()

    • ES6引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限
    • Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内
  • BigInt

    • 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示
    • BigInt类型的数据必须添加后缀n
1234 // 普通整数
1234n // BigInt

🥷函数的扩展

🎯函数参数的默认值

ES6可以为函数的参数设置默认值,直接写在参数定义的后面

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

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

参数默认值可以与解构赋值的默认值,结合起来使用

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

foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined
function fetch(url, { body = '', method = 'GET', headers = {} }) {
  console.log(method);
}

fetch('http://example.com', {})   // "GET"
fetch('http://example.com')  // 报错

// 双重默认值
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
  console.log(method);
}
fetch('http://example.com')  // "GET"

🎯rest参数

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

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

add(2, 5, 3) // 10
function push(array, ...items) {
  items.forEach(function(item) {
    array.push(item);
    console.log(item);
  });
}

var a = [];
push(a, 1, 2, 3)

注意,rest参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。函数的length属性,不包括rest参数。

严格模式

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

name属性

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

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

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

🎯箭头函数

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

var f = v => v;

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

如果箭头函数 不需要参数或需要多个参数,就使用一个圆括号代表参数部分

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

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

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

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

由于大括号被解释为代码块,所以如果箭头函数 直接返回一个对象,必须在对象外面加上括号,否则会报错

// 报错
let getTempItem = id => { id: id, name: "Temp" };

// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });

箭头函数可以与变量解构结合使用

const full = ({ first, last }) => first + ' ' + last;

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

箭头函数使得表达更加简洁

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

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

// 普通函数写法
[1,2,3].map(function (x) {
  return x * x;
});
// 箭头函数写法
[1,2,3].map(x => x * x);

// 普通函数写法
var result = values.sort(function (a, b) {
  return a - b;
});
// 箭头函数写法
var result = values.sort((a, b) => a - b);

rest参数与箭头函数结合

const numbers = (...nums) => nums;
numbers(1, 2, 3, 4, 5)   // [1,2,3,4,5]

const headAndTail = (head, ...tail) => [head, tail];
headAndTail(1, 2, 3, 4, 5)  // [1,[2,3,4,5]]

箭头函数有几个使用注意点:

(1)箭头函数没有自己的this对象

(2)不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用rest参数代替

(4)不可以使用yield命令,因此箭头函数不能用作Generator函数

尾调用优化

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

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

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。尾递归只存在一个调用帧,所以永远不会发生“栈溢出”错误。

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

// 采用函数默认值的写法:
function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}
factorial(5) // 120

函数参数的尾逗号

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

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

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

数组的扩展

🎯扩展运算符

扩展运算符(spread)是三个点 (...)。它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。该运算符主要用于函数调用。(注意,只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错)

function push(array, ...items) {
  array.push(...items);
}

function add(x, y) {
  return x + y;
}
add(...[4, 38]) // 42
// 将arr2数组添加到arr1数组的尾部
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);

扩展运算符的常用应用:

  • 复制数组(返回原数组的克隆)
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;
  • 合并数组
    • 注意:如果数组里是对象,则是浅拷贝(合并而成的新数组,但是成员是对原数组成员的引用,这就是浅拷贝。如果修改了引用指向的值,会同步反映到新数组。)
[...arr1, ...arr2, ...arr3]


const a1 = [{ foo: 1 }];
const a2 = [{ bar: 2 }];
const a3 = [...a1, ...a2];
a3[0] === a1[0] // true
  • 与解构赋值结合
    • 扩展运算符可以与解构赋值结合起来,用于生成数组。
[a, ...rest] = list

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   // []
  • 字符串
    • 扩展运算符还可以将字符串转为真正的数组
[...'hello']
// [ "h", "e", "l", "l", "o" ]
  • 实现了Iterator接口的对象
    • 任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组
let nodeList = document.querySelectorAll('div');
let array = [...nodeList];

类方法

  • Array.from()
    • 方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6新增的数据结构Set和Map)
    • Array.from()还可以接受一个函数作为第二个参数,作用类似于数组的map()方法,用来对每个元素进行处理,将处理后的值放入返回的数组。
let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};

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


// 返回一个具有三个成员的数组,每个位置的值都是undefined
Array.from({ length: 3 });
// [ undefined, undefined, undefined ]
Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);

Array.from([1, 2, 3], (x) => x * x)  // [1, 4, 9]
  • Array.of()
    • 用于将一组值,转换为数组
    • Array.of()基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载,它的行为非常统一
    • Array.of()总是返回参数值组成的数组。如果没有参数,就返回一个空数组
Array.of(3, 11, 8) // [3,11,8]
Array.of(undefined) // [undefined]

实例方法

  • 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]
  • find()、findIndex()、findLast()、findLastIndex()
  • 数组实例的find() 方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
    • find()方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。
[1, 4, -5, 10].find((n) => n < 0)  // -5

[1, 5, 10, 15].find(function(value, index, arr) {
  return value > 9;
}) // 10
  • 数组实例的findIndex() 方法,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
    • find()和findIndex()都是从数组的0号位,依次向后检查。ES2022新增了两个方法findLast()和findLastIndex() ,从数组的最后一个成员开始,依次向前检查,其他都保持不变。
[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2
  • fill()
    • fill() 方法使用给定值,填充一个数组,一般用于空数组的初始化
    • 还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置
    • 如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象
['a', 'b', 'c'].fill(7)   // [7, 7, 7]

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

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

let arr = new Array(3).fill({name: "Mike"});
arr[0].name = "Ben";
arr   // [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
  • 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"
  • includes()
    • 该方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似
    • 该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true

[1, 2, 3].includes(3, 3);  // false
[1, 2, 3].includes(3, -1); // true
  • flat()、flatMap()
    • 用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响
[1, 2, [3, 4]].flat()  // [1, 2, 3, 4]
[1, 2, [3, [4, 5]]].flat()  // [1, 2, 3, [4, 5]]
[1, 2, [3, [4, 5]]].flat(2)  // [1, 2, 3, 4, 5]
[1, [2, [3]]].flat(Infinity)  // [1, 2, 3]

// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
  • at()
    • 接受一个整数作为参数,返回对应位置的成员,并支持负索引。这个方法不仅可用于数组,也可用于字符串和类型数组(TypedArray)
const arr = [5, 12, 8, 130, 44];
arr.at(2) // 8
arr.at(-2) // 130
  • toReversed()、toSorted()、toSpliced()、with()

    • toReversed()对应reverse(),用来颠倒数组成员的位置
    • toSorted()对应sort(),用来对数组成员排序
    • toSpliced()对应splice(),用来在指定位置,删除指定数量的成员,并插入新成员
    • with(index, value)对应splice(index, 1, value),用来将指定位置的成员替换为新的值
    • 上面四个方法,允许对数组进行操作时,不改变原数组,而返回一个原数组的拷贝。
  • group()、groupToMap()

    • group()的参数是一个分组函数,原数组的每个成员都会依次执行这个函数,确定自己是哪一个组
    • group()的返回值是一个对象,该对象的键名就是每一组的组名,即分组函数返回的每一个字符串(如下是even和odd);该对象的键值是一个数组,包括所有产生当前键名的原数组成员。
const array = [1, 2, 3, 4, 5];

array.group((num, index, array) => {
  return num % 2 === 0 ? 'even': 'odd';
});
// { odd: [1, 3, 5], even: [2, 4] }
  • groupToMap()的作用和用法与group()完全一致,唯一的区别是返回值是一个 Map 结构,而不是对象
    • 使用原则:按照字符串分组就使用group(),按照对象分组就使用groupToMap()
  • 数组的空位
    • 数组的空位指的是,数组的某一个位置没有任何值
    • ES6 则是明确将空位转为undefined,扩展运算符(...)也会将空位转为undefined
Array(3) // [, , ,]
  • Array.prototype.sort() 的排序稳定性
    • 排序稳定性(stable sorting)是排序算法的重要属性,指的是排序关键字相同的项目,排序前后的顺序不变

对象的扩展

🎯属性的简洁表示法

  • 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法
    • 注意,简写的对象方法不能用作构造函数,会报错
const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}

const o = {
  method() {
    return "Hello!";
  }
};
// 等同于
const o = {
  method: function() {
    return "Hello!";
  }
};
module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
  getItem: getItem,
  setItem: setItem,
  clear: clear
};

属性名表达式

  • 允许字面量定义对象时,用表达式作为对象的属性名,即把表达式放在方括号内
let lastWord = 'last word';

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

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

🎯对象的扩展运算符(...)

  • 解构赋值
    • 对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。
    • 解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x=1  y=2   z={ a: 3, b: 4 }

function baseFunction({ a, b }) {
  // ...
}
function wrapperFunction({ x, y, ...restConfig }) {
  // 使用 x 和 y 参数进行操作
  // 其余参数传给原始函数
  return baseFunction(restConfig);
}
  • 扩展运算符
    • 对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中
    • 对象的扩展运算符,只会返回参数对象自身的、可枚举的属性(不会返回方法,因为方法定义在原型对象上)
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }

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

{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}

对象的新增方法

  • Object.is()
    • 它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致
Object.is('foo', 'foo')  // true
Object.is({}, {})  // false
Object.is(NaN, NaN) // true
  • Object.assign()
    • 用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。第一个参数是目标对象,后面的参数都是源对象
  • Object.getOwnPropertyDescriptors()
    • 返回指定对象所有自身属性(非继承属性)的描述对象
  • __proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()
    • __proto__属性(前后各两个下划线),用来读取或设置当前对象的原型对象
    • Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的原型对象(prototype),返回参数对象本身
    • 该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象
  • Object.keys()
    • 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名
let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 };

for (let key of keys(obj)) {
  console.log(key); // 'a', 'b', 'c'
}
  • Object.values()
    • 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值
const obj = { foo: 'bar', baz: 42 };
Object.values(obj)  // ["bar", 42]
  • Object.entries()
    • 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
  • Object.fromEntries()
    • 是Object.entries()的逆操作,用于将一个键值对数组转为对象
    • 该方法的主要目的,是将键值对的数据结构还原为对象,因此特别适合将Map结构转为对象
    • 还可以配合URLSearchParams对象,将查询字符串转为对象
// 例一
const entries = new Map([
  ['foo', 'bar'],
  ['baz', 42]
]);

Object.fromEntries(entries)
// { foo: "bar", baz: 42 }

// 例二
const map = new Map().set('foo', true).set('bar', false);
Object.fromEntries(map)
// { foo: true, bar: false }
Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))
// { foo: "bar", baz: "qux" }
  • Object.hasOwn()
    • 判断是否为自身的属性。接受两个参数,第一个是所要判断的对象,第二个是属性名

运算符的扩展

指数运算符(**)

// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2   // 512

let b = 4;
b **= 3; // 64
// 等同于 b = b * b * b;

🎯链判断运算符

“链判断运算符”(?.)

直接在链式调用的时候判断,左侧的对象是否为null或undefined。如果是的,就不再往下运算,而是返回undefined。

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

需要注意以下几点:

(1)短路机制:本质上,?.运算符相当于一种短路机制,只要不满足条件,就不再往下执行。(链判断运算符一旦为真,右侧的表达式就不再求值)

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

(2)括号的影响:如果属性链有圆括号,链判断运算符对圆括号外部没有影响,只对圆括号内部有影响

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

(3)报错场合

以下都是错误场景

// 构造函数
new a?.()
new a?.b()

// 链判断运算符的右侧有模板字符串
a?.`{b}`
a?.b`{c}`

// 链判断运算符的左侧是 super
super?.()
super?.foo

// 链运算符用于赋值运算符左侧
a?.b = c

(4)右侧不得为十进制数值

Null判断运算符

Null判断运算符(??)。它的行为类似“||”,但是只有运算符左侧的值为null或undefined时,才会返回右侧的值

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

这个运算符的一个目的,就是跟链判断运算符?.配合使用,为null或undefined的值设置默认值。

const animationDuration = response.settings?.animationDuration ?? 300;

适合判断函数参数是否赋值

function Component(props) {
  const enable = props.enabled ?? true;
  // …
}

逻辑赋值运算符

这三个运算符 ||=、&&=、??= 相当于先进行逻辑运算,然后根据运算结果,再视情况进行赋值运算。

// 或赋值运算符
x ||= y
// 等同于
x || (x = y)

// 与赋值运算符
x &&= y
// 等同于
x && (x = y)

// Null 赋值运算符
x ??= y
// 等同于
x ?? (x = y)

它们的一个用途是,为变量或属性设置默认值:

user.id ||= 1;

function example(opts) {
  opts.foo ??= 'bar';
  opts.baz ??= 'qux';
}

Symbol

ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。它属于 JavaScript 语言的原生数据类型之一,其他数据类型是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、大整数(BigInt)、对象(Object)。

Symbol值通过Symbol() 函数生成(注意,Symbol()函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的)

let s1 = Symbol('foo');
s1 // Symbol(foo)
s1.toString() // "Symbol(foo)"
s1.description // "foo"

Symbol值作为标识符,用于对象的属性名,就能保证不会出现同名的属性

let mySymbol = Symbol();

// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
let a = {
  [mySymbol]: 'Hello!'
};

// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
a[mySymbol] // "Hello!"

Set&Map数据结构

Set

Set成员值无重复的

const s = new Set();

const set = new Set([1, 2, 3, 4, 4]);
[...set]  // [1, 2, 3, 4]

两个便捷用法:

// 去除数组的重复成员
// 方法1:
[...new Set(array)]
// 方法2:
function dedupe(array) {
  return Array.from(new Set(array));
}
dedupe([1, 1, 2, 3]) // [1, 2, 3]

// 去除字符串里面的重复字符
[...new Set('ababbc')].join('')  // "abc"

Set实例的操作方法(用于操作数据):

  • Set.prototype.add(value) :添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value) :删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value) :返回一个布尔值,表示该值是否为Set的成员。
  • Set.prototype.clear() :清除所有成员,没有返回值。

Set实例的遍历方法(用于遍历成员):

  • Set.prototype.keys() :返回键名的遍历器
  • Set.prototype.values() :返回键值的遍历器
  • Set.prototype.entries() :返回键值对的遍历器
  • Set.prototype.forEach() :使用回调函数遍历每个成员

WeakSet

WeakSet结构与Set类似,也是不重复的值的集合。但是,WeakSet的成员只能是对象,而不能是其他类型的值;且WeakSet不可遍历

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

const b = [3, 4];
const ws = new WeakSet(b);  // Uncaught TypeError: Invalid value used in weak set(…)

Map

Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应

const map = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);

备注:Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

Map实例的属性和操作方法:

  • size属性:返回Map结构的成员总数
  • Map.prototype.set(key, value):设置键名key对应的键值为value,然后返回整个Map结构。如果key已经有值,则键值会被更新,否则就新生成该键
    • set方法返回的是当前的Map对象,因此可以采用链式写法:
let map = new Map().set(1, 'a').set(2, 'b').set(3, 'c');
  • Map.prototype.get(key):读取key对应的键值,如果找不到key,返回undefined
  • Map.prototype.has(key):返回一个布尔值,表示某个键是否在当前Map对象之中
  • Map.prototype.delete(key):删除某个键,返回true。如果删除失败,返回false
  • Map.prototype.clear():清除所有成员,没有返回值

Map实例的遍历方法:

  • Map.prototype.keys():返回键名的遍历器
  • Map.prototype.values():返回键值的遍历器
  • Map.prototype.entries():返回所有成员的遍历器
  • Map.prototype.forEach():遍历 Map 的所有成员

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

  • Map转为数组:最方便的方法,就是使用扩展运算符(...)
const myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
[...myMap]   // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
  • 数组转为Map:将数组传入Map构造函数,就可以转为Map
new Map([  [true, 7],
  [{foo: 3}, ['abc']]
])
// Map { true => 7, Object {foo: 3} => ['abc'] }
  • Map转为对象:如果所有Map的键都是字符串,它可以无损地转为对象;如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名
function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}
  • 对象转为Map:对象转为Map可以通过Object.entries()
let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));
  • 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}'
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"]]]'
  • 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']}

WeakMap

WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。其次,WeakMap的键名所指向的对象,不计入垃圾回收机制

const wm1 = new WeakMap();
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);

WeakRef

用于直接创建对象的弱引用(一大用处就是作为缓存,未被清除时可以从缓存取值,一旦清除缓存就自动失效)

let target = {};
let wr = new WeakRef(target);

let obj = wr.deref();
if (obj) { // target 未被垃圾回收机制清除
  // ...
}

🥷Promise对象

🎯含义和基本用法

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。Promise提供统一的API,各种异步操作都可以用同样的方法进行处理。

Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。

  • Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject
    • resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从pending变为resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去
    • reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从pending变为rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去
const promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});
  • Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数
    • then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数
promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

🎯Promise.prototype.then()

  • 它的作用是为Promise实例添加状态改变时的回调函数
  • then方法返回的是一个新的Promise实例(注意不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法
getJSON("/post/1.json").then(
  post => getJSON(post.commentURL)
).then(
  comments => console.log("resolved: ", comments),
  err => console.log("rejected: ", err)
);

Promise.prototype.catch()

  • Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数
p.then((val) => console.log('fulfilled:', val))
  .catch((err) => console.log('rejected', err));

// 等同于
p.then((val) => console.log('fulfilled:', val))
  .then(null, (err) => console.log("rejected:", err));
  • 一般来说,不要在then()方法里面定义Reject状态的回调函数(即then的第二个参数),总是使用catch方法。
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

Promise.prototype.finally()

  • finally()方法用于指定不管Promise对象最后状态如何,都会执行的操作
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

Promise.all()

  • Promise.all()方法用于将多个Promise实例,包装成一个新的Promise实例
const p = Promise.all([p1, p2, p3]);

// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
  return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
  // ...
}).catch(function(reason){
  // ...
});

Promise.race()

  • Promise.race()方法同样是将多个Promise实例,包装成一个新的Promise实例
const p = Promise.race([p1, p2, p3]);

Promise.allSettled()

  • Promise.allSettled()方法接受一个数组作为参数,数组的每个成员都是一个Promise对象,并返回一个新的Promise对象
  • 只有等到参数数组的所有Promise对象都发生状态变更(不管是fulfilled还是rejected),返回的Promise对象才会发生状态变更
const promises = [
  fetch('/api-1'),
  fetch('/api-2'),
  fetch('/api-3'),
];

await Promise.allSettled(promises);
removeLoadingIndicator(); // 只有等到这三个请求都结束了(不管请求成功还是失败),这里才会执行。

Promise.any()

  • 该方法接受一组Promise实例作为参数,包装成一个新的Promise实例返回
  • 只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态
Promise.any([
  fetch('https://v8.dev/').then(() => 'home'),
  fetch('https://v8.dev/blog').then(() => 'blog'),
  fetch('https://v8.dev/docs').then(() => 'docs')
]).then((first) => {  // 只要有一个 fetch() 请求成功
  console.log(first);
}).catch((error) => { // 所有三个 fetch() 全部请求失败
  console.log(error);
});

Promise.resolve()

  • 用于将现有对象转为Promise对象
const jsPromise = Promise.resolve($.ajax('/whatever.json'));

Promise.reject()

  • 返回一个新的Promise实例,该实例的状态为rejected
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

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

🎯Promise应用

  • 加载图片
    • 我们可以将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化
const preloadImage = function (path) {
  return new Promise(function (resolve, reject) {
    const image = new Image();
    image.onload  = resolve;
    image.onerror = reject;
    image.src = path;
  });
};
  • Generator函数与Promise的结合
    • 使用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);

Promise.try()

让同步函数同步执行,异步函数异步执行:

  • 第一种写法是用async函数来写
const f = () => console.log('now');

(async () => f())();
console.log('next');

(async () => f())()
.then(...)
.catch(...)
  • 第二种写法是使用Promise.try
const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next

🥷Generator函数语法

🎯简介

形式上,Generator函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号(一般是*紧跟在function后面);二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)

调用Generator函数,返回一个遍历器对象,代表Generator函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束

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

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

hw.next()  // { value: 'hello', done: false }
hw.next()  // { value: 'world', done: false }
hw.next()  // { value: 'ending', done: true }
hw.next()  // { value: undefined, done: true }

for...of 循环

function* numbers () {
  yield 1
  yield 2
  return 3
  yield 4
}

for (let v of numbers()) {
  console.log(v);
}
// 1 2   注意3不输出

// 扩展运算符
[...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

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 v of foo()) {
    yield v;
  }
  yield 'y';
}
for (let v of bar()){
  console.log(v);
}

🎯作为对象属性的Generator函数

如果一个对象的属性是Generator函数,可以简写成下面的形式

let obj = {
  * myGeneratorMethod() {
    ···
  }
};

// 等同于
let obj = {
  myGeneratorMethod: function* () {
    // ···
  }
};

Generator函数的异步应用

使用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);
}

🥷async函数

含义

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

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

async函数就是将Generator函数的星号(*)替换成async,将yield替换成await。但是有以下四点的改进:

(1)内置执行器

async函数自带执行器,执行方式与普通函数一样,如:asyncReadFile();

(2)更好的语义

async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果

(3)更广的适用性

async函数的await命令后面,可以是Promise对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即resolved的Promise对象)

(4)返回值是Promise

async函数的返回值是Promise对象,可以用then方法指定下一步的操作

进一步说,async函数完全可以看作多个异步操作包装成的一个Promise对象,而await命令就是内部then命令的语法糖

🎯基本用法

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

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

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

// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }
  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}
const storage = new Storage();
storage.getAvatar('jake').then(…);

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

🎯语法

返回Promise对象

async函数返回一个Promise对象。async函数内部return语句返回的值,会成为then方法回调函数的参数

async function f() {
  return 'hello world';
}

f().then(v => console.log(v))  // "hello world"
async function f() {
  throw new Error('出错了');
}

f().then(
  v => console.log('resolve', v),
  e => console.log('reject', e)
)
//reject Error: 出错了

Promise对象的状态变化

只有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)
// 只有三个操作全部完成,才会执行then方法里面的console.log。

await命令

正常情况下,await命令后面是一个Promise对象,返回该对象的结果。如果不是Promise对象,就直接返回对应的值

async function f() {
  // 等同于: return 123;
  return await 123;
}
f().then(v => console.log(v))// 123

错误处理

如果await后面的异步操作出错,那么等同于async函数返回的Promise对象被reject

防止出错的方法,最好是把await放在try...catch代码块之中

async function f() {
	// 防止出错的方法,也是将其放在try...catch代码块之中
  try {
    await new Promise(function (resolve, reject) {
      throw new Error('出错了');
    });
  } catch(e) {
  }
  return await('hello world');
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出错了
// hello world

Class

基本使用

类的数据类型就是函数,类本身就指向构造函数

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

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

var point = new Point(2, 3);

私有属性,是在属性名之前使用#表示,只能在类的内部使用。也可以用来表示私有方法

#foo = 0;

#sum() {
  return this.#a + this.#b;
}

类的继承

通过extends关键字实现继承,让子类继承父类的属性和方法(包括静态属性和静态方法,但是不包括私有属性和私有方法)

注意,静态属性是通过软拷贝实现继承的。如果父类的静态属性的值是一个对象,那么子类的静态属性也会指向这个对象,因为浅拷贝只会拷贝对象的内存地址。

class ColorPoint extends Point {}

🥷Module

概述与严格模式

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

import { stat, exists, readFile } from 'fs';

ES6的模块自动采用严格模式,严格模式主要有以下限制:

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

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

🎯export命令

如果希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量:

// profile.js
export var name = 'Michael';
export var year = 1958;

或者:(此方法推荐使用)
// profile.js
var name = 'Michael';
var year = 1958;
export { firstName, lastName, year };

export还可以输出函数或类:

export function multiply(x, y) {
  return x * y;
};

export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错

🎯import命令

使用export命令定义了模块的对外接口以后,其他JS文件就可以通过import命令加载这个模块。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同

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

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

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

import { lastName as surname } from './profile.js';

🎯import整体加载

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

// 逐一加载
import { area, circumference } from './circle';

// 整体加载
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

🎯export default 命令

export default 命令,为模块指定默认输出。 需要注意的是,这时import命令后面不使用大括号。

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

// import-default.js
import customName from './export-default';
customName(); // 'foo'

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

export与import的复合写法

如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。但是当前模块不能直接使用,只是相当于对外转发了

export { foo, bar } from 'my_module'; 

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

🎯跨模块常量

如果要使用的常量非常多,可以建一个专门的constants目录,将各种常量写在不同的文件里面,保存在该目录下。然后将这些文件输出的常量,合并在index.js里面。使用的时候,直接加载index.js就可以了。

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

// script.js
import {db, users} from './constants/index';

import()

import()函数,支持动态加载模块。import()类似于 Node.js 的require()方法,区别主要是前者是异步加载,后者是同步加载。

async function renderWidget() {
  const container = document.getElementById('widget');
  if (container !== null) {
    // 等同于
    // import("./widget").then(widget => {
    //   widget.render(container);
    // });
    const widget = await import('./widget.js');
    widget.render(container);
  }
}

renderWidget();

主要使用场景:

(1)按需加载:在需要的时候,再加载某个模块。如下是点击按钮后再加载

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

(2)条件加载:放在if代码块,根据不同的情况,加载不同的模块

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

(3)动态的模块路径

// 根据函数f的返回结果,加载不同的模块
import(f()).then(...);

🥷编程风格

🎯块级作用域

  • let取代var
    • let完全可以取代var,因为两者语义相同,而且let没有副作用
    • 且var命令存在变量提升效用,let命令没有这个问题
  • 全局常量使用const
    • 在let和const之间,建议优先使用const
    • 所有的函数都应该设置为常量

🎯字符串

  • 静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号
const a = 'foobar';
const b = `foo${a}bar`;

🎯解构赋值

  • 使用数组成员对变量赋值时,优先使用解构赋值
const arr = [1, 2, 3, 4];
const [first, second] = arr;
  • 函数的参数如果是对象的成员,优先使用解构赋值
function getFullName({ firstName, lastName }) {}
  • 如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。这样便于以后添加返回值,以及更改返回值的顺序
function processInput(input) {
  return { left, right, top, bottom };
}

const { left, right } = processInput(input);

🎯对象

  • 单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾
const a = { k1: v1, k2: v2 };
const b = {
  k1: v1,
  k2: v2,
};
  • 对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法
  • 如果对象的属性名是动态的,可以在创造对象的时候,使用属性表达式定义
const obj = {
  id: 5,
  name: 'San Francisco',
  [getKey('enabled')]: true,
};
  • 对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写

🎯数组

  • 使用扩展运算符(...)拷贝数组
const itemsCopy = [...items];
  • 使用 Array.from 方法,将类似数组的对象转为数组
const foo = document.querySelectorAll('.foo');
const nodes = Array.from(foo);

🎯函数

  • 立即执行函数可以写成箭头函数的形式
(() => { console.log('Welcome to the Internet.');})();
  • 那些 使用匿名函数当作参数的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了this
// good
[1, 2, 3].map((x) => {
  return x * x;
});

// best
[1, 2, 3].map(x => x * x);
  • 简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法
  • 使用默认值语法设置函数参数的默认值
function handleThings(opts = {}) {
  // ...
}

Map结构

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

Class

  • 用Class,取代需要prototype的操作
  • 使用extends实现继承

🎯模块

  • 使用import取代require()
import { func1, func2 } from 'moduleA';
  • 使用export取代module.exports
  • 如果模块 只有一个输出值,就使用export default,如果模块有多个输出值,除非其中某个输出值特别重要,否则建议不要使用export default,即多个输出值如果是平等关系,export default与普通的export就不要同时使用
import React from 'react';

class Breadcrumbs extends React.Component {
  render() {
    return <nav />;
  }
};

export default Breadcrumbs;
  • 如果模块默认输出一个函数,函数名的首字母应该小写,表示这是一个工具方法
function makeStyleGuide() {}
export default makeStyleGuide;
  • 如果模块默认输出一个对象,对象名的首字母应该大写,表示这是一个配置值对象
const StyleGuide = {
  es6: {}
};
export default StyleGuide;

ESLint的使用

ESLint是一个语法规则和代码风格的检查工具

1)在项目的根目录安装 ESLint:

$ npm install --save-dev eslint

2)安装Airbnb语法规则,以及 import、a11y、react 插件

$ npm install --save-dev eslint-config-airbnb
$ npm install --save-dev eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react

3)在项目的根目录下新建一个.eslintrc文件,配置ESLint

{
  "extends": "eslint-config-airbnb"
}

参考文档:

官方镜像:wangdoc.com/es6/

入门教程:es6.ruanyifeng.com/#README