ES6

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

let 和 const

let

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

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

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

const

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

解构赋值

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

  • 数组的解构:
1
2
3
const F4 = ['小沈阳', '刘能', '赵四', '宋小宝'];
let [xiao, liu, zhao, song] = F4;
console.log(xiao, liu, zhao, song);
  • 对象的解构:
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);

模板字符串

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

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 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法,这样的书写更加简洁

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 的值
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); // 尚硅谷
  • 不能作为构造实例化对象
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 变量
1
2
3
4
let fn = () => {
console.log(arguments);
}
fn(1, 2, 3) // error
  • 省略小括号,当形参有且只有一个的时候
1
2
3
let add = n => {
return n + 1;
}
  • 省略花括号,当代码体只有一条语句的时候,此时 return 也必须省略
1
let add = n => n+1;

默认参数

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

  • 可以给形参赋初始值,一般位置要靠后(潜规则)
1
2
3
4
5
function add(a, b, c = 12){
return a+b+c;
}
let result = add (1, 2);
console.log(result) // 15
  • 与解构赋值结合
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

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

扩展运算符

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

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

应用场景

  • 数组的合并
1
2
3
4
const A = ['aa', 'bb'];
const B = ['cc', 'dd'];
const C = [...A, ...B];
console.log(C) // [aa, bb, cc, dd]
  • 数组的克隆
1
2
3
const A = ['a', 'b', 'c'];
const B = [...A];
console.log(B) // [a, b, c]
  • 将伪数组转换为真正的数组
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 来获取对象的所有键名
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 值时,可以用参数添加一个描述

1
const sym = Symbol('foo');

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

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

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

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

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

sym.description // "foo"

作为属性名的 Symbol

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

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 值作为对象属性名时不能用点运算符

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 值必须放在方括号之中

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

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

obj[s](123);

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

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 值作为属性名时,该属性还是公开属性,不是私有属性

消除魔术字符串

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

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

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

属性名的遍历

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

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

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 键名

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

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 值,并将其注册到全局

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 值

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

Symbol.keyFor()

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

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)

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() 时,是否可以展开

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 属性,指向一个构造函数。创建衍生对象时,会使用该属性

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 属性

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) 时,如果该属性存在,会调用它,返回该方法的返回值

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 方法调用时,会返回该方法的返回值

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 方法调用时,会返回该方法的返回值

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 方法调用时,会返回该方法的返回值

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 属性,指向该对象的默认遍历器方法

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:该场合可以转成数值,也可以转成字符串
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 后面的那个大写字符串

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 环境排除

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循环,可以方便地遍历迭代器

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* 关键字声明,并且可以接收参数

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}

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

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 对象

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 方法

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) 的别名,用于指定发生错误时的回调函数

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 引入标准的

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

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

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 实例

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 实例

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

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 对象才会发生状态变更

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 实例返回

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() 方法就起到这个作用

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

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

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

Promise.reject()

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

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 值
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 的属性和方法

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 关键字实现

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.

数值扩展

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

对象方法扩展

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命令用于输入其他模块提供的功能

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

暴露语法

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

引入语法

  • 通用导入方式
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"
  • 解构赋值方式
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"

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

模块化方式 2

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

ES7

  1. Array.prototype.includes:用来检测数组中是否包含某个元素,返回布尔类型值
  2. 在 ES7 中引入指数操作符 **,用来实现幂运算,功能与 Math.pow 结果相同
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 函数执行的返回值决定
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 捕获处理
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() // 失败了

对象方法扩展

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 参数

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"}
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

对象扩展方法

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

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 添加了私有属性,方法是在属性名之前使用 # 表示

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)。如果在类的外部使用,就会报错

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

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

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

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 是一个不存在的私有属性,不管在函数内部或外部,读取该属性都会导致报错

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

可选链操作符

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

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

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 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示

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

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

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

import

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

1
import(specifier)

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

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 命令

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