机器人
AI摘要
WXZGPT
生成中...
It has been 509 days since the last update, the content of the article may be outdated.

ES6

ES6是 ECMAScript 2015 的简称,是 JavaScript 语言的一个重要版本,引入了许多新的语法和功能,使得 JavaScript 更加强大和灵活

let 和 const

let

  • 变量不能重复声明
js
1
2
let star = '罗志祥';
let star = '小猪'; // error
  • let 有块级作用域
js
1
2
3
4
{
let girl = '周扬青';
}
console.log(girl); // error

不仅仅针对花括号,例如 if() 里面

  • 不存在变量提前
js
1
2
console.log(song);   // error
let song = '恋爱达人';
  • 不影响作用域链
js
1
2
3
4
let school = 'abc';
function fn() {
console.log(school); // abc
}

const

  • const 用来定义常量,一旦声明,不可修改
js
1
const A = 'abc';
  • 一定要赋初始值
  • 一般常量使用大写(潜规则)
  • 也具有块级作用域
js
1
2
3
4
{
const pyaler = 'uzi';
}
console.log(player); // error
  • 对于数组和对象的元素修改,不算作对常量的修改
js
1
2
const team = ['uzi', 'MXLG', 'Ming', 'Letme'];
team.push('Meiko'); // 不报错,常量地址没有发生变化

解构赋值

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

  • 数组的解构:
js
1
2
3
const F4 = ['小沈阳', '刘能', '赵四', '宋小宝'];
let [xiao, liu, zhao, song] = F4;
console.log(xiao, liu, zhao, song);
  • 对象的解构:
js
1
2
3
4
5
6
7
8
9
10
const zhao = {
name : '赵本山',
age: '不详',
xiaopin: function(){
console.log("我可以演小品AAA");
}
}
let {name, age, xiaopin} = zhao;
console.log(name, age);
console.log(xiaopin);

模板字符串

模板字符串是一种使用反引号 `` 包裹字符串,并且支持插入变量或表达式的新语法。它可以避免使用 + 号连接字符串和变量,并且支持多行字符串和标签模板等特性

js
1
2
3
4
5
6
7
8
9
10
11
12
// 使用模板字符串插入变量或表达式,用 ${} 包裹即可(注意是反引号而不是单引号)
let name = "Bob";
let age = 19;
let message = `Hello ${name}, you are ${age} years old.`;
console.log(message);

// 使用模板字符串可以直接换行,不需要使用 \n 或者 + 号连接多行字符串
let poem =
`Do not go gentle into that good night,
Old age should burn and rave at close of day;
Rage, rage against the dying of the light.`;
console.log(poem);

对象的简化写法

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

js
1
2
3
4
5
6
7
8
9
10
11
12
let name = 'aaa';
let change = function() {
console.log('aaa');
}

const school = {
name,
change,
improve() {
consolg.log('bbb');
}
}

箭头函数

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

  • this 是静态的,this 始终指向函数声明时所在作用域下的 this 的值
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function A(){
console.log(this.name)
}

let B = () => {
console.log(this.name);
}

window.name = '尚硅谷';
const school = {
name: 'ATGUIGU'
}

// 直接调用
A() // 尚硅谷
B() // 尚硅谷

// call
A.call(school); // ATGUIGU
B.cal(school); // 尚硅谷
  • 不能作为构造实例化对象
js
1
2
3
4
5
6
let A(name,age) => {
this.name=name;
this.age=age;
}
let me = new A('xiao', 123);
console.me // error
  • 不能使用 arguments 变量
js
1
2
3
4
let fn = () => {
console.log(arguments);
}
fn(1, 2, 3) // error
  • 省略小括号,当形参有且只有一个的时候
js
1
2
3
let add = n => {
return n + 1;
}
  • 省略花括号,当代码体只有一条语句的时候,此时 return 也必须省略
js
1
let add = n => n+1;

默认参数

ES6 允许给函数参数赋值初始值

  • 可以给形参赋初始值,一般位置要靠后(潜规则)
js
1
2
3
4
5
function add(a, b, c = 12){
return a+b+c;
}
let result = add (1, 2);
console.log(result) // 15
  • 与解构赋值结合
js
1
2
3
4
5
6
7
8
function A({host = '127.0.0.1', username, password, port}){
console.log(host + username + password + port)
}
A({
username: 'ran',
password: '123456',
port: 3306
})

rest 参数

ES6 引入 rest 参数,用于获取函数的实参,用来代替 arguments

js
1
2
3
4
function date(...args){
console.log(args);
}
date('aaa', 'bbb', 'ccc');

扩展运算符

扩展运算符是能将数组转换为逗号分隔的参数序列

js
1
2
3
4
5
const tfboys=['AA', 'BB', 'CC'];
function chunwan(){
console.log(arguments);
}
chunwan(...tfboys); // 0: 'AA' 1: 'BB' 2: 'CC'

应用场景

  • 数组的合并
js
1
2
3
4
const A = ['aa', 'bb'];
const B = ['cc', 'dd'];
const C = [...A, ...B];
console.log(C) // [aa, bb, cc, dd]
  • 数组的克隆
js
1
2
3
const A = ['a', 'b', 'c'];
const B = [...A];
console.log(B) // [a, b, c]
  • 将伪数组转换为真正的数组
js
1
2
3
const A = documents.querySelectorAll('div');
const B = [...A];
console.log(B) // [div, div, div]

Symbol

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,是一种类似于字符串的数据类型。Symbol特点:

  1. Symbol 的值是唯一的,用来解决命名冲突的问题
  2. Symbol 值不能与其他数据进行运算
  3. Symbol 定义的对象属性不能使用 for ... in 循环遍历,但是可以使用 Reflect.ownKeys 来获取对象的所有键名
js
1
2
3
4
5
6
7
let s = Symbol('aa');
let s2= Symbol('aa');
console.log(s === s2) // false

let s3 = Symbol.for('bb');
let s4 = Symbol.for('bb');
comsole.log(s3 === s4) // true

注意,Symbol() 函数前不能使用 new 命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象,所以不能使用 new 命令来调用。另外,由于 Symbol 值不是对象,所以也不能添加属性。基本上,它是一种类似于字符串的数据类型

Symbol.prototype.description

前面说过,Symbol() 函数创建 Symbol 值时,可以用参数添加一个描述

js
1
const sym = Symbol('foo');

上面代码中,sym 这个值的描述就是字符串 foo。但是,读取这个描述需要将 Symbol 显式转为字符串,即下面的写法:

js
1
2
3
4
const sym = Symbol('foo');

String(sym) // "Symbol(foo)"
sym.toString() // "Symbol(foo)"

上面的用法不是很方便。ES2019 提供了一个 Symbol 值的实例属性 description,直接返回 Symbol 值的描述

js
1
2
3
const sym = Symbol('foo');

sym.description // "foo"

作为属性名的 Symbol

由于每一个 Symbol 值都是不相等的,这意味着只要 Symbol 值作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let mySymbol = Symbol();

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

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

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

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

上面代码通过方括号结构和 Object.defineProperty() 方法,将对象的属性名指定为一个 Symbol 值。注意,Symbol 值作为对象属性名时不能用点运算符

js
1
2
3
4
5
6
const mySymbol = Symbol();
const a = {};

a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"

上面代码中,因为点运算符后面总是字符串,所以不会读取 mySymbol 作为标识名所指代的那个值,导致 a 的属性名实际上是一个字符串,而不是一个 Symbol 值。同理,在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中

js
1
2
3
4
5
6
7
let s = Symbol();

let obj = {
[s]: function (arg) { ... }
};

obj[s](123);

上面代码中,如果 s 不放在方括号中,该属性的键名就是字符串 s而不是 s 所代表的那个 Symbol 值。Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的:

js
1
2
3
4
5
6
7
8
9
const log = {};

log.levels = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn')
};
console.log(log.levels.DEBUG, 'debug message');
console.log(log.levels.INFO, 'info message');

还有一点需要注意,Symbol 值作为属性名时,该属性还是公开属性,不是私有属性

消除魔术字符串

魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替

js
1
2
3
const shapeType = {
triangle: Symbol()
};

上面代码中,除了将 shapeType.triangle 的值设为一个 Symbol,其他地方都不用修改

属性名的遍历

Symbol 值作为属性名,遍历对象的时候,该属性不会出现在 for ... infor ... of 循环中,也不会被 Object.keys()Object.getOwnPropertyNames()JSON.stringify() 返回

但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols() 方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值

js
1
2
3
4
5
6
7
8
9
10
11
const obj = {};
const foo = Symbol('foo');

obj[foo] = 'bar';

for (let i in obj) {
console.log(i); // 无输出
}

Object.getOwnPropertyNames(obj) // []
Object.getOwnPropertySymbols(obj) // [Symbol(foo)]

上面代码中,使用 for ... in 循环和 Object.getOwnPropertyNames() 方法都得不到 Symbol 键名,需要使用 Object.getOwnPropertySymbols() 方法。另一个新的 API,Reflect.ownKeys() 方法可以返回所有类型的键名,包括常规键名和 Symbol 键名

js
1
2
3
4
5
6
7
let obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
};

Reflect.ownKeys(obj) // ["enum", "nonEnum", Symbol(my_key)]

由于以 Symbol 值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let size = Symbol('size');

class Collection {
constructor() {
this[size] = 0;
}

add(item) {
this[this[size]] = item;
this[size]++;
}

static sizeOf(instance) {
return instance[size];
}
}

let x = new Collection();
Collection.sizeOf(x) // 0

x.add('foo');
Collection.sizeOf(x) // 1

Object.keys(x) // ['0']
Object.getOwnPropertyNames(x) // ['0']
Object.getOwnPropertySymbols(x) // [Symbol(size)]

上面代码中,对象 x 的 size 属性是一个 Symbol 值,所以 Object.keys(x)Object.getOwnPropertyNames(x) 都无法获取它。这就造成了一种非私有的内部方法的效果

Symbol.for()

有时,我们希望重新使用同一个 Symbol 值,Symbol.for() 方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局

js
1
2
3
4
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true

上面代码中,s1 和 s2 都是 Symbol 值,但是它们都是由同样参数的 Symbol.for 方法生成的,所以实际上是同一个值

Symbol.for()Symbol() 这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for() 不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的 key 是否已经存在,如果不存在才会新建一个值。比如,如果你调用 Symbol.for("cat") 30 次,每次都会返回同一个 Symbol 值,但是调用 Symbol("cat") 30 次,会返回 30 个不同的 Symbol 值

js
1
2
Symbol("bar") === Symbol("bar") // false
Symbol.for("bar") === Symbol.for("bar") // true

Symbol.keyFor()

Symbol.keyFor() 方法返回一个已登记的 Symbol 类型值的 key

js
1
2
3
4
5
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

上面代码中,变量 s2 属于未登记的 Symbol 值,所以返回 undefined。注意,Symbol.for() 为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行

内置的 Symbol 值

除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法

  • Symbol.hasInstance

对象的 Symbol.hasInstance 属性,指向一个内部方法。当其他对象使用 instanceof 运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo 在语言内部,实际调用的是 Foo[Symbol.hasInstance](foo)

js
1
2
3
4
5
6
7
class MyClass {
[Symbol.hasInstance](foo) {
return foo instanceof Array;
}
}

[1, 2, 3] instanceof new MyClass() // true
  • Symbol.isConcatSpreadable

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

js
1
2
3
4
5
6
7
let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr1[Symbol.isConcatSpreadable] // undefined

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

对象的 Symbol.species 属性,指向一个构造函数。创建衍生对象时,会使用该属性

js
1
2
3
4
5
6
7
8
9
class MyArray extends Array {
}

const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);

b instanceof MyArray // true
c instanceof MyArray // true

上面代码中,子类 MyArray 继承了父类 Array,a 是 MyArray 的实例,b 和 c 是 a 的衍生对象。你可能会认为,b 和 c 都是调用数组方法生成的,所以应该是数组(Array 的实例),但实际上它们也是 MyArray 的实例。Symbol.species 属性就是为了解决这个问题而提供的。现在,我们可以为 MyArray 设置 Symbol.species 属性

js
1
2
3
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
}

上面代码中,a.map(x => x) 生成的衍生对象,就不是 MyArray 的实例,而直接就是 Array 的实例

总之,Symbol.species 的作用在于,实例对象在运行过程中,需要再次调用自身的构造函数时,会调用该属性指定的构造函数。它主要的用途是,有些类库是在基类的基础上修改的,那么子类使用继承的方法时,作者可能希望返回基类的实例,而不是子类的实例

  • Symbol.match

对象的 Symbol.match 属性,指向一个函数。当执行 str.match(myObject) 时,如果该属性存在,会调用它,返回该方法的返回值

js
1
2
3
4
5
6
7
8
9
10
11
String.prototype.match(regexp)
// 等同于
regexp[Symbol.match](this)

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

'e'.match(new MyMatcher()) // 1
  • Symbol.replace

对象的 Symbol.replace 属性,指向一个方法,当该对象被 String.prototype.replace 方法调用时,会返回该方法的返回值

js
1
2
3
4
const x = {};
x[Symbol.replace] = (...s) => console.log(s);

'Hello'.replace(x, 'World') // ["Hello", "World"]

Symbol.replace 方法会收到两个参数,第一个参数是 replace 方法正在作用的对象,上面例子是 Hello,第二个参数是替换后的值,上面例子是 World

  • Symbol.search

对象的 Symbol.search 属性,指向一个方法,当该对象被 String.prototype.search 方法调用时,会返回该方法的返回值

js
1
2
3
4
5
6
7
8
9
10
11
12
13
String.prototype.search(regexp)
// 等同于
regexp[Symbol.search](this)

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

对象的 Symbol.split 属性,指向一个方法,当该对象被 String.prototype.split 方法调用时,会返回该方法的返回值

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MySplitter {
constructor(value) {
this.value = value;
}
[Symbol.split](string) {
let index = string.indexOf(this.value);
if (index === -1) {
return string;
}
return [
string.substr(0, index),
string.substr(index + this.value.length)
];
}
}

'foobar'.split(new MySplitter('foo')) // ['', 'bar']
'foobar'.split(new MySplitter('bar')) // ['foo', '']
'foobar'.split(new MySplitter('baz')) // 'foobar'

上面方法使用 Symbol.split 方法,重新定义了字符串对象的 split 方法的行为

  • Symbol.iterator

对象的 Symbol.iterator 属性,指向该对象的默认遍历器方法

js
1
2
3
4
5
6
7
8
const myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};

[...myIterable] // [1, 2, 3]
  • Symbol.toPrimitive

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

  1. Number:该场合需要转成数值
  2. String:该场合需要转成字符串
  3. Default:该场合可以转成数值,也可以转成字符串
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 123;
case 'string':
return 'str';
case 'default':
return 'default';
default:
throw new Error();
}
}
};

2 * obj // 246
3 + obj // '3default'
obj == 'default' // true
String(obj) // 'str'
  • Symbol.toStringTag

对象的 Symbol.toStringTag 属性,用来设定一个字符串(设为其他类型的值无效,但不报错)。在目标对象上面调用 Object.prototype.toString() 方法时,如果 Symbol.toStringTag 属性存在,该属性设定的字符串会出现在 toString() 方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制 [object Object][object Array] 中 object 后面的那个大写字符串

js
1
2
3
4
5
6
7
8
9
10
11
12
// 例一
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"

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

对象的 Symbol.unscopables 属性,指向一个对象。该对象指定了使用 with 关键字时,哪些属性会被 with 环境排除

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 没有 unscopables 时
class MyClass {
foo() { return 1; }
}

var foo = function () { return 2; };

with (MyClass.prototype) {
foo(); // 1
}

// 有 unscopables 时
class MyClass {
foo() { return 1; }
get [Symbol.unscopables]() {
return { foo: true };
}
}

var foo = function () { return 2; };

with (MyClass.prototype) {
foo(); // 2
}

上面代码通过指定 Symbol.unscopables 属性,使得 with 语法块不会在当前作用域寻找 foo 属性,即 foo 将指向外层作用域的变量

迭代器

迭代器是一种遵循迭代协议的对象,可以按照一定的顺序访问一个集合中的元素。迭代器有一个 next 方法,每次调用返回一个包含 value 和 done 属性的对象,value 表示当前元素的值,done 表示是否还有更多元素。ES6 提供了一种新的语法 for ... of循环,可以方便地遍历迭代器

js
1
2
3
4
5
6
7
const xiyou=['AA', 'BB', 'CC', 'DD'];
for(let v of xiyou){
console.log(v) // 'AA', 'BB', 'CC', 'DD' for in 保存的是键名,for of 保存的是键值
}
let iterator = xiyou[Symbol.iterator]();
console.log(iterator.next()); // {{value:'唐僧', done:false}}
console.log(iterator.next()); // {{value:'孙悟空', done:false}}

生成器

生成器是一种特殊的函数,可以返回一个迭代器对象,并且可以在函数体内使用 yield 关键字暂停和恢复执行。生成器使用 function* 关键字声明,并且可以接收参数

js
1
2
3
4
5
6
7
8
9
function * gen (){    // 函数名和 function 中间有一个 * 
yield '耳朵'; // yield 是函数代码的分隔符
yield '尾巴';
yield '真奇怪';
}
let iterator = gen();
console.log(iteretor.next()); // {value:'耳朵', done:false} next() 执行第一段,并且返回 yield 后面的值
console.log(iteretor.next()); // {value:'尾巴', done:false}
console.log(iteretor.next()); // {value:'真奇怪', done:false}

用生成器函数的方式解决 回调地狱 问题:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function one(){
setTimeout(() => {
console.log('111')
iterator.next()
}, 1000)
}
function two(){
setTimeout(() => {
console.log('222')
iterator.next();
}, 2000)
}
function three(){
setTimeout(() => {
console.log('333')
iterator.next();
}, 3000)
}

function * gen(){
yield one();
yield two();
yield three();
}

let iterator = gen();
iterator.next();

Promise

Promise 是一种用于处理异步操作的对象,它表示一个未来可能完成或失败的事件。Promise 有三种状态:pending(等待)、fulfilled(成功)、rejected(失败)。Promise 可以通过 then 方法添加成功或失败时执行的回调函数,也可以通过 catch 方法添加失败时执行的回调函数。Promise 还可以通过 all 和 race 方法组合多个 Promise 对象

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const p =new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('用户数据');
})
});

let result = p.then(value => {
console.log(value)
// return 123;
// return new Promise((resolve, reject) => {
// resolve('ok')
// })
throw 123
},reason => {
console.log(reason)
})
console.log(result);

Promise.prototype.then()

Promise 实例具有 then 方法,也就是说,then 方法是定义在原型对象 Promise.prototype 上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then 方法的第一个参数是 resolved 状态的回调函数,第二个参数是 rejected 状态的回调函数,它们都是可选的

then 方法返回的是一个新的 Promise 实例(注意,不是原来那个 Promise 实例)。因此可以采用链式写法,即 then 方法后面再调用另一个 then 方法

js
1
2
3
4
5
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});

Promise.prototype.catch()

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

js
1
2
3
4
5
6
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});

Promise.prototype.finally()

finally() 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的

js
1
2
3
4
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

不管 promise 最后的状态,在执行完 then 或 catch 指定的回调函数以后,都会执行 finally 方法指定的回调函数

js
1
2
3
4
5
6
7
8
9
10
11
// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})

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

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

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

Promise.all()

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

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

上面代码中,Promise.all() 方法接受一个数组作为参数,p1、p2、p3 都是 Promise 实例,如果不是,就会先调用下面讲到的 Promise.resolve 方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all() 方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。p的状态由 p1、p2、p3 决定,分成两种情况:

  1. 只有 p1、p2、p3 的状态都变成 fulfilled,p 的状态才会变成 fulfilled,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数
  2. 只要 p1、p2、p3 之中有一个被 rejected,p 的状态就变成 rejected,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数

Promise.race()

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

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

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

Promise.race() 方法的参数与 Promise.all() 方法一样,如果不是 Promise 实例,就会先调用下面讲到的 Promise.resolve() 方法,将参数转为 Promise 实例,再进一步处理

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

js
1
2
3
4
5
6
7
8
9
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);

上面代码中,如果 5 秒之内 fetch 方法无法返回结果,变量 p 的状态就会变为 rejected,从而触发 catch 方法指定的回调函数

Promise.allSettled()

有时候,我们希望等到一组异步操作都结束了,不管每一个操作是成功还是失败,再进行下一步操作。但是,现有的 Promise 方法很难实现这个要求。Promise.all() 方法只适合所有异步操作都成功的情况,如果有一个操作失败,就无法满足要求。

为了解决这个问题,ES2020 引入了 Promise.allSettled() 方法,用来确定一组异步操作是否都结束了(不管成功或失败)。所以,它的名字叫做"Settled",包含了 "fulfilled"和"rejected"两种情况

Promise.allSettled() 方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是 fulfilled 还是 rejected),返回的 Promise 对象才会发生状态变更

js
1
2
3
4
5
6
7
8
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];

await Promise.allSettled(promises);
removeLoadingIndicator();

Promise.any()

ES2021 引入了 Promise.any() 方法。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回

js
1
2
3
4
5
6
7
8
9
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);
});

只要参数实例有一个变成 fulfilled 状态,包装实例就会变成 fulfilled 状态;如果所有参数实例都变成 rejected 状态,包装实例就会变成 rejected 状态

Promise.resolve()

有时需要将现有对象转为 Promise 对象,Promise.resolve() 方法就起到这个作用

js
1
const jsPromise = Promise.resolve($.ajax('/whatever.json'));

上面代码将 jQuery 生成的deferred对象,转为一个新的 Promise 对象。Promise.resolve() 等价于下面的写法

js
1
2
3
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

Promise.reject()

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

js
1
2
3
4
5
6
7
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

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

集合

Set

ES6 提供了新的数据结构 set,它类似于数组,但成员的值都是唯一的,集合实现了 iterator 接口,所以可以使用扩展运算符和 for ... of 进行遍历,集合的属性和方法:

  • size 返回集合的元素个数
  • add 增加一个新元素,返回当前集合
  • delete 删除元素,返回 boolean 值 has 检测集合中是否包含某个元素,返回 boolean 值
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let s = new Set();
let s2 = new Set(['A', 'B', 'C', 'D'])

// 元素个数
console.log(s2.size);

// 添加新的元素
s2.add('E');

// 删除元素
s2.delete('A')

// 检测
console.log(s2.has('C'));

// 清空
s2.clear()

console.log(s2);

Map

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合。但是"键"的范围不限于字符串,各种类型的值(包括对象)都可以当作键。Map 也实现了 iterator 接口,所以可以使用扩展运算符和 for ... of 进行遍历 Map 的属性和方法

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let m = new Map();
m.set('name', 'ran');
m.set('change', () => {
console.log('改变!')
})
let key={
school: 'atguigu'
}
m.set(key, ['成都', '西安']);

// size
console.log(m.size);

// 删除
m.delete('name');

// 获取
console.log(m.get('change'));

// 清空
m.clear()

// 遍历
for(let v of m){
console.log(v);
}

Class

ES6 提供了一种新的语法,可以让 JavaScript 支持类和继承这两个面向对象编程的重要概念。类是一种定义对象属性和方法的模板,可以通过 new 关键字创建类的实例。继承是一种让子类拥有父类属性和方法的机制,可以通过 extends 和 super 关键字实现

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 定义一个类:使用 class 关键字,并且提供一个 constructor 方法作为构造函数(初始化实例属性)
class Person {
constructor(name,age) {
this.name = name; // this 指向实例对象
this.age = age;
}

// 定义类的方法:直接在类中写函数名和函数体(不需要使用 function 关键字)
sayHi() {
console.log(`Hello ${this.name}, you are ${this.age} years old.`);
}
}

// 创建类的实例:使用 new 关键字,并且传入构造函数所需的参数
let alice = new Person("Alice", 18);
alice.sayHi(); // Hello Alice, you are 18 years old.

// 定义一个子类:使用 extends 关键字继承父类,并且可以重写或新增属性和方法
class Student extends Person {
constructor(name, age, grade) {
super(name, age); // 使用 super 关键字调用父类的构造函数(必须在子类构造函数中第一行执行)
this.grade = grade; // 子类可以新增自己的属性
}

// 子类可以重写或新增父类的方法
sayHi() {
console.log(`Hello ${this.name}, you are ${this.age} years old and in grade ${this.grade}.`);
}

study() {
console.log(`${this.name} is studying hard.`);
}
}

// 创建子类的实例:使用 new 关键字,并且传入构造函数所需的参数(包括父类和子类的参数)
let bob = new Student("Bob", 19, 12);
bob.sayHi(); // Hello Bob, you are 19 years old and in grade 12.
bob.study(); // Bob is studying hard.

数值扩展

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Number.EPSILON 是 JavaScript 的最小精度,属性的值接近于 2.22044...E-16
function equal(a, b){
if(Math.abs(a - b) < Number.EPSILON) {
return true;
} else {
return false;
}
}

console.log(equal(0.1 + 0.2 === 0.3)) // false
console.log(equal(0.1 + 0.2, 0.3)) // true

// 二进制和八进制
let b = 0b1010; // 2进制
let o = 0o777; // 8进制
let d = 100; // 10进制
let x = 0xff; // 16进制
console.log(x) // 255

// 检测一个数是否为有限数
console.log(Number.isFinite(100)); // true
console.log(Number.isFinite(100 / 0)); // false
console.log(Number.isFinite(Infinity)); // false

// 检测一个数值是否为 NaN
console.log(Number.isNaN(123)) // false

// 字符串转整数
console.log(Number.parseInt('5213123love')); // 5213123
console.log(Number.parseFloat('5.123123神器')); // 5.123123

// 判断是否为整数
console.log(Number.isInteger(5)); // true
console.log(Number.isInteger(2.5)); // false

// 将小数部分抹除
console.log(Math.trunc(3.45345345345)) // 3

// 检测一个数到底是正数、负数、还是 0
console.log(Math.sign(100)) // 1
console.log(Math.sign(0)) // 0
console.log(Math.sign(-123)) // -1

对象方法扩展

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1.Object.is 判断两个值是否完全相等
console.log(Object.is(120, 120)) // true
console.log(Object.is(NaN, NaN)) // false

// 2.Object.assign 对象的合并
const a = {
name: 'ran',
age: 12
}
const b = {
pass: 'i love you'
}
console.log(Object.assign(a, b)) // {name: 'ran', age: '12', pass: 'i love you'}

// 3.Object.setPrototypeOf 设置原型对象 Object.getPrototypeof
const school = {
name: '尚硅谷'
}
const cities = {
xiaoqu: ['北京', '上海']
}
Object.setPrototypeOf(school, cities)
console.log(Object.getPrototypeOf(school)) // {xiaoqu: Array(2)}
console.log(school) // {name: "尚硅谷"}

模块化

基本介绍

  • 模块化是指将一个大的程序文件,拆分成许多小的文件,然后将小文件组合起来
  • 模块化的好处:
    1. 防止命名冲突
    2. 代码复用
    3. 高维护性
  • 模块化规范产品
    1. CommonJS ====> NodeJS、Browserify
    2. AMD ====> requireJS
    3. CMD ====> seaJS

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

js
1
2
3
4
export let school = '尚硅谷'
export function teach() {
console.log('教技能')
}
js
1
2
import * as m1 from "./src/js/m1.js";
console.log(m1);

暴露语法

  • 统一暴露
js
1
2
3
4
5
6
// 统一暴露
let school = '尚硅谷';
function findjob(){
console.log('找工作吧');
}
export {school, findjob}
  • 默认暴露
js
1
2
3
4
5
6
7
//默认暴露
export default {
school: 'ATGUIGU',
change:function() {
console.log('我们可以改变你')
}
}

引入语法

  • 通用导入方式
js
1
2
3
import * as m1 from "./src/js/m1.js"
import * as m2 from "./src/js/m2.js"
import * as m3 from "./src/js/m3.js"
  • 解构赋值方式
js
1
2
3
4
import {school, teach} from "./src/js/m1.js"
import {school as guigu, findJob} from "./src/js/m2.js"
import {default as m3 } from "./src/js/m3.js"

  • 简便形式(只针对默认暴露)
js
1
import m3 from "./src/js/m3.js"

模块化方式 2

js
1
<script src="./src//js/app.js" type=modeule></script>

ES7

  1. Array.prototype.includes:用来检测数组中是否包含某个元素,返回布尔类型值
  2. 在 ES7 中引入指数操作符 **,用来实现幂运算,功能与 Math.pow 结果相同
js
1
2
3
4
5
6
7
// include
const mingzhu = ['西游记', '红楼梦', '水浒传', '三国演义']
console.log(mingzhu.includes('西游记')) // true
console.log(mingzhu.includes('金瓶梅')) // false

// **
console.log(2**10) // 1024

ES8

async

async 和 await 两种语法结合可以让异步代码像同步代码一样

  • async 函数的返回值为 promise 对象
  • async 返回的 promise 对象的结果值由 async 函数执行的返回值决定
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function fn(){
// 1.如果返回的是一个非 Promise 的对象,则 fn() 返回的结果就是成功状态的 Promise 对象,值为返回值
// 2.如果返回的是一个Promise对象,则 fn() 返回的结果与内部 Promise 对象的结果一致
// 3.如果返回的是抛出错误,则 fn() 返回的就是失败状态的 Promise 对象
return new Promise((resolve, reject) => {
resolve('成功的数据');
});
}
const result = fn();
result.then(value => {
console.log(value) // 成功的数据
},reason => {
console.log(reason)
})

await

  1. await 必须放在 async 函数中
  2. await 右侧的表达式一般为 promise 对象
  3. await 可以返回的是右侧 promise 成功的值
  4. await 右侧的 promise 如果失败了,就会抛出异常,需要通过 try ... catch 捕获处理
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建 Promise 对象
const p = new Promise((resolve, reject) => {
// resolve("成功的值")
reject("失败了")
})

// await 必须放在 async 函数中
async function main() {
try {
let res = await p;
console.log(res);
} catch (e) {
console.log(e);
}
}

main() // 失败了

对象方法扩展

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const school = {
name: '尚硅谷',
cities: ['北京', '上海', '深圳'],
xueke: ['前端', 'Java', '大数据', '运维']
};

// 获取对象所有的键
console.log(Object.keys(school));

// 获取对象所有的值
console.log(Object.values(school));

// entries,用来创建 map
console.log(Object.entries(school));
console.log(new Map(Object.entries(school)))

// 对象属性的描述对象
console.log(Object.getOwnPropertyDescriptor(school))

const obj = Object.create(null, {
name:{
value: '尚硅谷',
// 属性特性
writable:true,
configurable:true,
enumerable:true,
}
})

ES9

运算扩展符与 rest 参数

js
1
2
3
4
5
6
7
8
9
10
11
12
function connect({host, port, ...user}){
console.log(host);
console.log(port);
console.log(user)
}
connect({
host: '127.0.0.1',
port: 3306,
username: 'root',
password: 'root',
type: 'master'
}) // 127.0.0.1 3306 {username: "root", password: "root", type: "master"}
js
1
2
3
4
5
6
7
8
9
10
11
const AA = {
username: 'ran'
}
const BB = {
password: 'lyyrhf'
}
const CC = {
job: 'Java'
}
const D = {...AA, ...BB, ...CC};
console.log(D) // {username: "ran", password: "lyyrhf", job: "Java"}

ES10

对象扩展方法

js
1
2
3
4
5
6
7
8
9
10
11
12
// 二维数组
const res = Object.fromEntries([
['name', 'RHF'],
['cities', '成都', '武汉']
])
console.log(res) // {name: "RHF", cities: "成都"}

// Map
const m = new Map();
m.set('name', 'ranhaifeng')
const result = Object.fromEntries(m)
console.log(result); // {name: "ranhaifeng"}

flat 与 flatMap

js
1
2
3
4
5
6
const arr = [1, 2, 3, [4, 5, 6, [7, 8, 9]]]
// 参数为深度,是一个数字
console.log(arr.flat(2)) // [1, 2, 3, 4, 5, 6, 7, 8, 9]

const arr2=[1, 2, 3, 4]
const result = arr2.flatmap(item => [item * 10]); // 如果 map 的结果是一个多维数组可以进行 flat 是两个操作的结合

私有属性

ES2022 正式为 class 添加了私有属性,方法是在属性名之前使用 # 表示

js
1
2
3
4
5
6
7
8
9
10
class IncreasingCounter {
#count = 0;
get value() {
console.log('Getting the current value!');
return this.#count;
}
increment() {
this.#count++;
}
}

上面代码中,#count 就是私有属性,只能在类的内部使用(this.#count)。如果在类的外部使用,就会报错

js
1
2
3
const counter = new IncreasingCounter();
counter.#count // 报错
counter.#count = 42 // 报错

注意,从 Chrome 111 开始,开发者工具里面可以读写私有属性,不会报错,原因是 Chrome 团队认为这样方便调试

另外,不管在类的内部或外部,读取一个不存在的私有属性,也都会报错。这跟公开属性的行为完全不同,如果读取一个不存在的公开属性,不会报错,只会返回 undefined

js
1
2
3
4
5
6
7
8
9
10
11
12
13
class IncreasingCounter {
#count = 0;
get value() {
console.log('Getting the current value!');
return this.#myCount; // 报错
}
increment() {
this.#count++;
}
}

const counter = new IncreasingCounter();
counter.#myCount // 报错

上面示例中,#myCount 是一个不存在的私有属性,不管在函数内部或外部,读取该属性都会导致报错

注意:私有属性的属性名必须包括 #,如果不带 #,会被当作另一个属性

可选链操作符

相当于一个判断符,如果前面的有,就进入下一层级

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function main(config){
const dbHost = config?.db?.host
console.log(dbHost) // 192.168.1.100
}

main({
db: {
host:'192.168.1.100',
username:'root'
},
cache: {
host:'192.168.1.200',
username:'admin'
}
})

BigInt

JavaScript 所有数字都保存成 64 位浮点数,这给数值的表示带来了两大限制。一是数值的精度只能到 53 个二进制位(相当于 16 个十进制位),大于这个范围的整数,JavaScript 是无法精确表示,这使得 JavaScript 不适合进行科学和金融方面的精确计算。二是大于或等于 2 的 1024 次方的数值,JavaScript 无法表示,会返回Infinity

js
1
2
3
4
5
// 超过 53 个二进制位的数值,无法保持精度
Math.pow(2, 53) === Math.pow(2, 53) + 1 // true

// 超过 2 的 1024 次方的数值,无法表示
Math.pow(2, 1024) // Infinity

ES2020 引入了一种新的数据类型 BigInt(大整数),来解决这个问题,这是 ECMAScript 的第八种数据类型。BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示

js
1
2
3
4
5
6
7
8
const a = 2172141653n;
const b = 15346349309n;

// BigInt 可以保持精度
a * b // 33334444555566667777n

// 普通整数无法保持精度
Number(a) * Number(b) // 33334444555566670000

为了与 Number 类型区别,BigInt 类型的数据必须添加后缀n

js
1
2
3
4
5
1234 // 普通整数
1234n // BigInt

// BigInt 的运算
1n + 2n // 3n

import

ES2020 提案 引入 import() 函数,支持动态加载模块

js
1
import(specifier)

上面代码中,import 函数的参数 specifier,指定所要加载的模块的位置。import 命令能够接受什么参数,import() 函数就能接受什么参数,两者区别主要是后者为动态加载。import() 返回一个 Promise 对象。下面是一个例子:

js
1
2
3
4
5
6
7
8
9
const main = document.querySelector('main');

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

import() 函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import() 函数与所加载的模块没有静态连接关系,这点也是与 import 语句不相同。import() 类似于 Node.js 的 require() 方法,区别主要是前者是异步加载,后者是同步加载

由于 import() 返回 Promise 对象,所以需要使用 then() 方法指定处理函数。考虑到代码的清晰,更推荐使用 await 命令

js
1
2
3
4
5
6
7
8
9
10
11
12
13
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();