10-ES6+

6/22/2022

# ES6

# 1、let 关键字

# 1.1 基本用法

ES6中新增了let命令,用于变量的声明,基本的用法和var类似。例如:

<script>
    // 使用var使用声明变量
    var userName = "wc";
    console.log("userName=", userName);
    // 使用let声明变量
    let userAge = 18;
    console.log("userAge=", userAge);
</script>
1
2
3
4
5
6
7
8
  • 通过以上的代码,我们发现var和let的基本使用是类似的,但是两者还是有本质的区别,最大的区别就是:
  • 使用let所声明的变量只在let命令所在的代码块中有效。

# 1.2 let与var区别

下面我们通过一个for循环的案例来演示一下let和var的区别, 如下所示:

<script>
    for (var i = 1; i <= 10; i++) {
        console.log("i=", i)
    }
    console.log(" last=", i)
</script>
1
2
3
4
5
6
  • 通过以上的代码,我们知道在循环体中i的值输出的是1--10,最后i的值为11.

  • 但是如果将var换成let会出现什么问题呢?代码如下:

for (let i = 1; i <= 10; i++) {
    console.log("i=", i)
}
console.log("last=", i)
1
2
3
4
  • 在循环体中输出的i的值还是1--10, 但是循环体外部打印i的值时出现了错误,错误如下:

出错的原因是:通过let声明的变量只在其对应的代码块中起作用,所谓的代码块我们可以理解成就是循环中的这一对大括号。

  • 当然在这里我们通过这个提示信息,可以发现在ES6中默认是启动了严格模式的,严格模式的特征就是:变量未声明不能使用,否则报的错误就是变量未定义。
  • 那么在ES5中怎样开启严格模式呢?我们可以在代码的最开始加上:“use strict”
  • 刚才我们说到,let声明的变量只在代码块中起作用,其实就是说明了通过let声明的变量仅在块级作用域内有效

# 1.3 块级作用域

什么是块级作用域?

  • 有一段代码是用大括号包裹起来的,那么大括号里面就是一个块级作用域

也就是说,在我们写的如下的案例中:

for (let i = 1; i <= 10; i++) {
    console.log("i=", i)
}
console.log("last=", i)
// i 这个变量的作用域只在这一对大括号内有效,超出这一对大括号就无效了。
1
2
3
4
5

为什么需要块级作用域?

  • ES5 只有全局作用域和函数作用域,没有块级作用域,这样就会带来一些问题
  • 第一:内层变量可能会覆盖外层变量
  • 代码如下:
var temp = new Date();

function show() {
    console.log("temp=", temp)
    if (false) {
        var temp = "hello world";
    }
}
show();
// 执行上面的代码,输出的结果为 *temp=undefined*  ,原因就是变量由于提升导致内层的temp变量覆盖了外层的temp变量
1
2
3
4
5
6
7
8
9
10
  • 第二: 用来计数的循环变量成为了全局变量
  • 关于这一点,在前面的循环案例中,已经能够看到。在这里,可以再看一下
<script>
    for (var i = 1; i <= 10; i++) {
        console.log("i=", i)
    }
    console.log("last=", i)

    // 在上面的代码中,变量i的作用只是用来控制循环,但是循环结束后,它并没有消失,而是成了全局的变量,这不是我们希望的,我们希望在循环结束后,该变量就要消失。
</script>
1
2
3
4
5
6
7
8

以上两点就是,在没有块级作用域的时候,带来的问题。

下面使用let来改造前面的案例。

let temp = new Date();

function show() {
    console.log("temp=", temp)
    if (false) {
        let temp = "hello world";
    }

}
show();
// 通过上面的代码,可以知道let不像var那样会发生“变量提升”的现象。
1
2
3
4
5
6
7
8
9
10
11

ES6块级作用域

  • let实际上为JavaScript新增了块级作用域,下面再看几个案例,通过这几个案例,巩固一下关于“块级作用域”这个知识点的理解,同时进一步体会块级作用域带来的好处
<script>
    function test() {
        let num = 5;
        if (true) {
            let num = 10;
        }
        console.log(num)
    }
    test()
    // 上面的函数中有两个代码块,都声明了变量num,但是输出的结果是5.这表示外层的代码不受内层代码块的影响。如果使用var定义变量num,最后的输出的值就是10.
</script>
1
2
3
4
5
6
7
8
9
10
11

说一下,下面程序的输出结果是多少?

if (true) {
    let b = 20;
    console.log(b)
    if (true) {
        let c = 30;
    }
    console.log(c);
}

// 输出的结果是:b的值是20,在输出c的时候,出现了错误。
// 导致的原因,两个if就是两个块级作用域,c这个变量在第二个if中,也就是第二个块级作用域中,所以在外部块级作用域中无法获取到变量c.
// 块级作用域的出现,带来了一个好处以前获得广泛使用的立即执行匿名函数不再需要了。
1
2
3
4
5
6
7
8
9
10
11
12

下面首先定义了一个立即执行匿名函数:

(function text() {
    var temp = 'hello world';
    console.log('temp=', temp);
})()

// 匿名函数的好处:通过定义一个匿名函数,创建了一个新的函数作用域,相当于创建了一个“私有”的空间,该空间内的变量和方法,不会破坏污染全局的空间 。
1
2
3
4
5
6

但是以上的写法是比较麻烦的,有了“块级作用域”后就编的比较简单了,代码如下:

{
    let temp = 'hello world';
    console.log('temp=', temp);
}
// 通过以上的写法,也是创建了一个“私有”的空间,也就是创建了一个封闭的作用域。同样在该封闭的作用域中的变量和方法,不会破坏污染全局的空间。
// 但是以上写法比立即执行匿名函数简单很多。
1
2
3
4
5
6

现在问你一个问题,以下代码是否可以:

let temp = '你好'; {
    let temp = 'hello world';
}

// 答案是可以的,因为这里有两个“块级作用域”,一个是外层,一个是内层,互不影响。
1
2
3
4
5

但是,现在修改成如下的写法:

let temp = '你好'; {
    console.log('temp=', temp);
    let temp = 'hello world';
}

// 出错了,也是变量未定义的错误,造成错误的原因还是前面所讲解的let 不存在“变量提升”。
1
2
3
4
5
6

块级作用域还带来了另外一个好处,我们通过以下的案例来体会一下:

// 该案例希望不同时间打印变量i的值。
for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log('i=', i);
    }, 1000)
}

// 输出的都是 i=3,造成的原因就是i为全局的。
1
2
3
4
5
6
7
8

那么可以怎样解决呢?相信这一点对你来说很简单,在前面ES5课程中也讲过。

for (var i = 0; i < 3; i++) {
    (function(i) {
        setTimeout(function() {
            console.log('i=', i);
        }, 1000)
    })(i)
}

// 通过以上的代码其实就是通过自定义一个函数,生成了函数的作用域,i变量就不是全局的了。
1
2
3
4
5
6
7
8
9

这种使用方式很麻烦,有了let命令后,就变的非常的简单了。

代码如下:

for (let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log('i=', i);
    }, 1000)
}
1
2
3
4
5

# 1.4 let命令注意事项

不存在变量提升

  • let不像var那样会发生“变量提升”现象。所以,变量一定要在声明后使用,否则会出错。
console.log(num);
let num = 2;
1
2

暂时性死区

  • 什么是暂时性死区呢?
// 先来看一个案例:
var num = 123;
if (true) {
    num = 666;
    let num;
}

// 上面的代码中存在全局的变量num,但是在块级作用域内使用了let又声明了一个局部的变量num,导致后面的num绑定到这个块级作用域,所以在let声明变量前,对num进行赋值操作会出错。

// 所以说,只要在块级作用域中存在let命令,它所声明的变量就被“绑定”在这个区域中,不会再受外部的影响。
1
2
3
4
5
6
7
8
9
10

如果在区域中存在let命令,那么在这个区域中通过let命令所声明的变量从一开始就生成了一个封闭的作用域,只要在声明变量前使用,就会出错

所谓的“暂时性死区”指的就是,在代码块内,使用let命令声明变量之前,该变量都是不可用的。

不允许重复声明

// let 不允许在相同的作用域内重复声明一个变量,
// 如果使用var声明变量是没有这个限制的。

function test() {
    var num = 12;
    var num = 20;
    console.log(num)
}
test()
1
2
3
4
5
6
7
8
9
// 以上代码没有问题,但是如果将var换成let,就会出错。如下代码所示:
function test() {
    let num = 12;
    let num = 20;
    console.log(num)
}
test()
1
2
3
4
5
6
7
// 当然,以下的写法也是错误的。
function test() {
    var num = 12;
    let num = 20;
    console.log(num)
}
test()
1
2
3
4
5
6
7
// 同时,还需要注意,不能在函数内部声明的变量与参数同名,如下所示:
function test(num) {
    let num = 20;
    console.log(num)
}
test(30)
1
2
3
4
5
6

# 2、const命令

# 2.1 基本用法

const用来声明常量,常量指的就是一旦声明,其值是不能被修改的。

这一点与变量是不一样的,而变量指的是在程序运行中,是可以改变的量。

let num = 12;
num = 30;
console.log(num)
1
2
3

以上的代码输出结果为:30

但是通过const命令声明的常量,其值是不允许被修改的。

const PI = 3.14;
PI = 3.15;
console.log(PI)
1
2
3

以上代码会出错。

在以后的编程中,如果确定某个值后期不需要更改,就可以定义成常量,例如: PI, 它的取值就是3.14,后面不会改变。所以可以将其定义为常量。

# 2.2 const命令注意事项

# 2.2.1 不存在常量提升

以下代码是错误的

console.log(PI);
const PI = 3.14
1
2

# 2.2.2 只在声明的块级作用域内有效

const命令的作用域与let命令相同:只在声明的块级作用域内有效

如下代码所示:

if (true) {
    const PI = 3.14;
}
console.log(PI);
1
2
3
4

以上代码会出错

# 2.2.3 暂时性死区

const命令与let指令一样,都有暂时性死区的问题,如下代码所示:

if (true) {
    console.log(PI);
    const PI = 3.14;
}
1
2
3
4

以上代码会出错

# 2.2.4 不允许重复声明

let PI = 3.14;
const PI = 3.14;
console.log(PI);
1
2
3

以上代码会出错

# 2.2.5 常量声明必须赋值

使用const声明常量,必须立即进行初始化赋值,不能后面进行赋值。

如下代码所示:

const PI;
PI = 3.14;
console.log(PI);
1
2
3

以上代码会出错

# 3、解构赋值

# 3.1、数组解构赋值基本用法

所谓的解构赋值,就是从数组或者是对象中提取出对应的值,然后将提取的值赋值给变量。

首先通过一个案例,来看一下以前是怎样实现的。

let arr = [1, 2, 3];
let num1 = arr[0];
let num2 = arr[1];
let num3 = arr[2];
console.log(num1, num2, num3);
1
2
3
4
5

在这里定义了一个数组arr, 并且进行了初始化,下面紧跟着通过下标的方式获取数组中的值,然后赋值给对应的变量。

虽然这种方式可以实现,但是相对来说比较麻烦,ES6中提供了解构赋值的方式,代码如下:

let arr = [1, 2, 3];
let [num1, num2, num3] = arr;
console.log(num1, num2, num3);
1
2
3

将arr数组中的值取出来分别赋值给了,num1, num2和num3.

通过观察,发现解构赋值等号两侧的结构是类似。

下面再看一个案例:

let arr = [{
        userName: 'zs',
        age: 18
    },
    [1, 3], 6
];
let [{
        userName,
        age
    },
    [num1, num2], num3
] = arr;
console.log(userName, age, num1, num2, num3);
1
2
3
4
5
6
7
8
9
10
11
12
13

定义了一个arr数组,并且进行了初始化,arr数组中有对象,数组和数值。

现在通过解构赋值的方式,将数组中的值取出来赋给对应的变量,所以等号左侧的结构和数组arr的结构是一样的。

但是,如果不想获取具体的值,而是获取arr数组存储的json对象,数组,那么应该怎样写呢?

let arr = [{
        userName: 'zs',
        age: 18
    },
    [1, 3], 6
];
let [jsonResult, array, num] = arr;
console.log(jsonResult, array, num);
1
2
3
4
5
6
7
8

# 3.2、注意事项

# 3.2.1 如果解析不成功,对应的值会为undefined.
let [num1, num2] = [6]
console.log(num1, num2);
1
2

以上的代码中,num1的值为6,num2的值为undefined.

# 3.2.2 不完全解构的情况

所谓的不完全解构,表示等号左边只匹配右边数组的一部分。

代码如下:

let [num1, num2] = [1, 2, 3];
console.log(num1, num2);
1
2

以上代码的执行结果:num1=1, num2 = 2

也就是只取了数组中的前两个值。

// 如果只取第一个值呢?
let [num1] = [1, 2, 3];
console.log(num1);
1
2
3
//只取第二个值呢?
let [, num, ] = [1, 2, 3];
console.log(num);
1
2
3
// 只取第三个值呢?
let [, , num] = [1, 2, 3];
console.log(num);
1
2
3

# 3.4、对象解构赋值基本使用

解构不仅可以用于数组,还可以用于对象。

let {
    userName,
    userAge
} = {
    userName: 'ls',
    userAge: 20
};
console.log(userName, userAge);
1
2
3
4
5
6
7
8

在对 对象进行解构赋值的时候,一定要注意:变量名必须与属性的名称一致,才能够取到正确的值。

如下所示:

let {
    name,
    age
} = {
    userName: 'ls',
    userAge: 20
};
console.log(name, age);
1
2
3
4
5
6
7
8

输出的结果都是undefined.

那么应该怎样解决上面的问题呢?

let {
    userName: name,
    userAge: age
} = {
    userName: 'ls',
    userAge: 20
}
console.log(name, age);
1
2
3
4
5
6
7
8

通过以上的代码解决了对应的问题,那么这种方式的原理是什么呢?

先找到同名属性,然后再赋值给对应的变量。

把上面的代码,改造成如下的形式,更容易理解:

let obj = {
    userName: 'ls',
    userAge: 21
};
let {
    userName: name,
    userAge: age
} = obj;
console.log(name, age)
1
2
3
4
5
6
7
8
9

如果按照ES5的方式:

let name = obj.userName
let age = obj.userAge
1
2

# 3.5、对象解构赋值注意事项

# 3.5.1 默认解构

所谓的默认解构,指的是取出来值就用取出来的值,如果取不出来就用默认的值。

演示默认解构之前,先来看如下的代码:

let obj = {
    name: 'zs'
};
let {
    name,
    age
} = obj;
console.log(name, age);
1
2
3
4
5
6
7
8

你想一下输出结果是什么呢?

输出的结果是:zs undefined

也就是name变量的值为:‘zs’, age变量的值为:'undefined'.

由于没有给age变量赋值所以该变量的值为'undefined'.

现在修改一下上面的程序

let obj = {
    name: 'zs'
};
let {
    name,
    age = 20
} = obj;
console.log(name, age);
1
2
3
4
5
6
7
8

现在给age这个变量赋了一个默认值为20,所以输出的结果为:zs 20

这也就是刚才所说到的默认解构,也就是取出来值就用取出来的值,如果取不出来就用默认的值。

现在再问你一个问题:如果在对应中有age属性,那么对应的等号左侧的age这个变量的值是多少呢?

如下代码所示:

let obj = {
    name: 'zs',
    age: 26
};
let {
    name,
    age = 20
} = obj;
console.log(name, age);
1
2
3
4
5
6
7
8
9

输出的结果为: zs 26

这就是,取出来值就用取出来的,取不出来就用默认值。

# 3.5.2 嵌套结构对象的解构

解构也可以用于对嵌套结构的对象,如下代码所示:

let obj = {
    arr: [
        "Hello", {
            msg: 'World'
        }
    ]
}
let {
    arr: [str, {
        msg
    }]
} = obj;
console.log(str, msg);
1
2
3
4
5
6
7
8
9
10
11
12
13

在上面的代码中要注意的是:arr只是一种标志或者是一种模式,不是变量,因此不会被赋值。

再看一个案例:

let obj = {
    local: {
        start: {
            x: 20,
            y: 30
        }
    }
};
let {
    local: {
        start: {
            x,
            y
        }
    }
} = obj;
console.log(x, y);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在该案例中创建了一个obj对象,在该对象中又嵌套了一个local对象,该对象可以认为是一个表示位置的坐标对象,在该对象中又嵌套了一个start对象,start对象可以认为是一个位置的起始坐标点,所以在该对象中有两个属性为x, y,分别表示横坐标和纵坐标。

所以说obj对象是一个比较复杂的嵌套结构的对象,现在对该对象进行解构,那么在等号的左侧的结构要和obj对象的结构一致,最后输出打印x, y的值。

问题:如果现在要打印等号左侧的local和start,那么输出的结果是什么呢?

会出错,原因就是在等号的左侧,只有x和y是变量,local和start都是一种标识,一种模式,所以不会被赋值。

# 3.6、字符串的解构赋值

字符串也可以进行解构赋值,这是因为字符串被转换成了一个类似于数组的对象。

let [a, b, c, d, e, f] = 'wangcai';
console.log(a, b, c, d, e, f);
1
2

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

let {
    length: len
} = 'wangcai';
console.log('len=', len);
1
2
3
4

# 3.7、函数参数的解构赋值

函数的参数也能够进行解构的赋值,如下代码所示:

function test([x, y]) {
    return x + y;
}
console.log(test([3, 6]));
1
2
3
4

上面的代码中,函数test的参数不是一个数组,而是通过解构得到的变量x和y.

函数的参数的解构也可以使用默认的值。

function test({
    x = 0,
    y = 0
} = {}) {
    return [x, y];

}
console.log(test({
    x: 3,
    y: 6
}));
1
2
3
4
5
6
7
8
9
10
11

当然可以进行如下的调用

test({
    x: 3
})
test({})
1
2
3
4

# 3.8、解构赋值的好处

# 3.8.1 交换变量的值

let num1 = 3;
let num2 = 6;
[num1, num2] = [num2, num1];
console.log(num1, num2);
1
2
3
4

# 3.8.2 函数可以返回多个值

function test() {
    return [1, 2, 3];
}
let [a, b, c] = test();
console.log(a, b, c);
1
2
3
4
5

在上面的代码中,返回了三个值,当然在实际的开发过程中,你可以根据自己的实际情况确定返回的数据的个数。

如果,我只想接收返回中的一部分值呢?

// 接收第一个值
function test() {
    return [1, 2, 3];
}
let [a] = test();
console.log(a);

// 接收前两个值
function test() {
    return [1, 2, 3];
}
let [a, b] = test();
console.log(a, b);

// 只接收第一个值和第三个值。
function test() {
    return [1, 2, 3];
}
let [a, , b] = test();
console.log(a, b);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 3.8.3 函数返回一个对象

可以将函数返回的多个值封装到一个对象中。

function test() {
    return {
        num1: 3,
        num2: 6
    }
}
let {
    num1,
    num2
} = test();
console.log(num1, num2);
1
2
3
4
5
6
7
8
9
10
11

# 3.8.4 提取JSON对象中的数据

解构赋值对提取JSON对象中的数据也非常有用。

let userData = {
    id: 12,
    userName: 'wangcai',
    userAge: 20
}
let {
    id,
    userName,
    userAge
} = userData;
console.log(id, userName, userAge);
1
2
3
4
5
6
7
8
9
10
11

以上的代码可以快速提取JSON中的数据。

# 4、扩展运算符与 rest 运算符

# 4.1 扩展运算符

扩展运算符的表现形式是三个点(...), 可以将一个数组转换为用逗号分隔的序列。

下面通过一个案例看一下基本的应用,案例的要求是将两个数组合并为一个数组。

先采用传统的做法:

let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let arr3 = [].concat(arr1, arr2);
console.log(arr3);
1
2
3
4

下面使用扩展运算符来完成上面的案例

let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let arr3 = [...arr1, ...arr2];
console.log(arr3);
1
2
3
4

通过以上的代码,发现也实现了我们最终想要的结果, ...arr1 是将 arr1 这个数组中的所有的元素取出来,然后组成 '1, 2, 3'这个形式,放到 ...arr1 这个位置,同理arr2也是一样。

通过扩展运算符实现起来发现更加的简单。

当然我们可以将上面使用扩展运算符实现的案例,转成ES5看一下。

转成ES5的代码如下:

var arr1 = [1, 2, 3];
var arr2 = [4, 5, 6];
var arr3 = [].concat(arr1, arr2);
console.log(arr3);
1
2
3
4

发现和我们最开始实现的是一样的。

# 4.2 扩展运算符应用场景

# 4.2.1 代替数组中的apply方法

现在求数组中的最大值。

求最大值,我们想到的第一种方法就是:

通过循环的方式来完成,如下面的代码:

let arr = [12, 23, 11, 56];
let max = arr[0]
for (let index = 0; index < arr.length; index++) {
    if (arr[index] > max) {
        max = arr[index];
    }

}
console.log("max=", max);
1
2
3
4
5
6
7
8
9

这种方式非常麻烦,所以可以使用Math对象中的max方法来完成.

先来看一下Math.max的基本用法

console.log(Math.max(1, 5, 12, 67));
1

如果是用Math.max来计算数组中的最大值。(ES5的写法)

let arr = [12, 23, 11, 56];
console.log(Math.max.apply(null, arr));
1
2

虽然可以使用 Math.max.apply 来实现,但是感觉还是很麻烦,

这里就可以使用扩展运算符

let arr = [12, 23, 11, 56];
console.log(Math.max(...arr));
1
2

在上面的代码中(不管ES5还是ES6),由于JavaScript不提供求数组中最大值的函数,所以只能将数组转换成一个参数的列表,然后再进行相应的求值。

# 4.2.2 用于函数调用

在函数调用的时候,需要进行参数的传递,在某些情况下,通过扩展运算符,更有利于参数的传递。

function test(num1, num2) {
    return num1 + num2;
}
let array = [23, 56];
console.log(test(...array));
1
2
3
4
5

通过扩展运算符,将array这个数组中的值取出来,然后23赋值给了num1, 56赋值给了num2.

下面,再看一个使用扩展运算符处理函数参数的案例。

把一组数据添加到数组中。

function test(array, ...items) {
    array.push(...items);
    console.log(array)
}
let array = [23, 56];
test(array, 90, 78, 98);
1
2
3
4
5
6

test这个函数的作用是:把90, 78, 98这三个数添加到array这个数组中,

在这里要注意的是:90, 78, 98 这三个数据给了items这个参数,这里我们用到了后面所讲解的rest参数,所以items这个参数实际上是一个数组,然后在test这个函数体内,又通过扩展运算符将items这个数组中的数据取出来给了array这个数组。

# 4.3 rest 运算符

# 4.3.1 rest参数基本使用

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

function add(s, num1, num2) {
    return s + (num1 + num2);

}
console.log(add('+', 2, 3));
1
2
3
4
5

在上面定义的函数中,传递了三个参数,第一个参数:是一个‘+’号,后面两个参数,表示进行加法运算的数据。

但是,问题是如果参与运算的数据比较多,那么定义的参数也就比较多,这样比较麻烦。这时可以使用rest参数形式。

如下所示:

function add(...values) {
    console.log(values);
}
add(2, 3);
1
2
3
4

通过以上的代码输出发现,values这个参数是一个数组,所传递的数据都存储到这个数组中,下面可以将数据从这个数组中取出来,进行运算。

function add(...values) {
    // console.log(values);
    let sum = 0;
    for (let index = 0; index < values.length; index++) {
        sum += values[index];
    }
    return sum;
}
console.log(add(2, 3));
1
2
3
4
5
6
7
8
9

上面的循环方式,使用的是传统的模式,也可以使用 forEach 的形式来进行循环,如下所示:

function add(...values) {
    let sum = 0;
    values.forEach(function(item) {
        sum += item
    })
    return sum;
}
console.log(add(2, 3));
1
2
3
4
5
6
7
8

下面我们再来看一个 rest 运算符的基本使用

解构会将相同数据结构对应的值赋给对应的变量,但是当我们想将其中的一部分值统一赋值给一个变量的时候,可以使用 rest 运算符。

如下代码:

let arr = [1, 2, 3, 4, 5, 6];
let [arr1, ...arr2] = arr; //进行解构处理
console.log(arr1); // 1
console.log(arr2); // [2,3,4,5,6]
1
2
3
4

在上面的代码中, arr 经过解构后,变量 arr1 的值为1,而通过 rest 运算符会将后面所有的值都统一赋值给 arr2 变量,得到的 arr2 为一个数组。

# 4.3.2 rest参数的好处

在以前的案例中,我们都是使用 arguments .

那么这种用法比 arguments 有什么样的好处呢?

对数据进行排序,使用的是 arguments

function sortFunc() {
    return Array.prototype.slice.call(arguments).sort()
}
console.log(sortFunc(23, 12, 67));
1
2
3
4

下面使用 rest的方式

function sortFunc(...values) {
    return values.sort()
}
console.log(sortFunc(23, 12, 67));
1
2
3
4

因为values这个参数本身就是数组,所以可以直接使用sort函数,进行数据的排序操作。

通过以上的对比,发现使用rest这种参数的写法更简洁。

# 4.3.3 rest参数注意问题

在使用rest这种参数的时候,一定要注意: rest参数之后不能再有其他的参数,也就是说rest参数只能是最后一个参数,否则会报错。

如下代码所示:

function test(a, ...b, c) {
    console.log(a);
    console.log(b);
    console.log(c);
}
test(1, 23, 2, 5);
1
2
3
4
5
6

以上代码会出错,要求rest参数只能是最后一个参数。

通过前面对扩展运算符和 rest 运算符的讲解,我们知道两者是互为逆运算,扩展运算符是将数组分隔成独立的序列,而 rest 运算符是将独立的序列合并成一个数组。

既然两者都是通过3个点(...)来表示,那么如何判断这3个点属于哪一种运算符呢?我们可以遵循如下的规则:

第一:当3个点(...)出现在函数的形参上或者出现在赋值号的左侧,则表示的就是 rest 运算符

第二:当3个点(...)出现在函数的实参上或者出现在赋值号的右侧,则表示它为扩展运算符。

# 5、什么是箭头函数

# 5.1 箭头函数基本使用

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

先使用传统的方式定义一个函数。

示例代码如下:

// 使用传统方式定义函数
let f = function(x, y) {
    return x + y;
}
console.log(f(3, 6));
1
2
3
4
5

通过上面的代码,可以发现传统方式来定义函数的时候,比较麻烦。

箭头函数的使用

let f = (x, y) => {
    return x + y
};
console.log(f(9, 8));
1
2
3
4

在调用f这个函数的时候,将9和8传递给了x, y这两个参数,然后进行加法运算。

如果参数只有一个,可以省略小括号。

let f = num => {
    return num / 2;
}
console.log(f(6));
1
2
3
4

如果没有参数,只需要写一对小括号就可以。

let f = () => {
    return 9 / 3;
}
console.log(f());
1
2
3
4

上面我们写的代码中,发现函数体中只有一条语句,那么这时是可以省略大括号的。

let f = (x, y) => x + y;
console.log(f(3, 6));
1
2

把上面的代码转换成ES5的写法,发现和我们前面写的代码是一样的。

var f = function f(x, y) {
    return x + y;
};
console.log(f(3, 6));
1
2
3
4

# 5.2 箭头函数注意事项

# 5.2.1 直接返回对象

如果希望箭头函数直接返回一个对象,应该怎样写呢?

你可能认为很简单,可以采用如下的写法

let f = (id, name) => {
    id: id,
    userName: name
};
console.log(f(1, 'zs'));
1
2
3
4
5

但是上面的写法是错误的,因为这时大括号被解释为代码块,解决的办法是:在对象外面加上小括号,

所以,正确的写法如下:

let f = (id, name) => ({
    id: id,
    userName: name
});
console.log(f(1, 'zs'));
1
2
3
4
5

通过打印,发现输出的是一个对象。

当然也可以采用如下的写法

let f = (id, name) => {
    return {
        id: id,
        userName: name
    }
};
console.log(f(1, 'zs'));
1
2
3
4
5
6
7

# 5.2.2 箭头函数中this的问题

下面定义一个对象,来理解this的应用。

看一下,如下代码:

let person = {
    userName: 'ls',
    getUserName() {
        console.log(this.userName)
    }
}
person.getUserName();
1
2
3
4
5
6
7

以上代码执行的结果为:'ls', 并且在该程序中 this 为当前的person对象。

现在,将上面的代码修改一下,要求延迟1秒钟以后,再输出用户名的名称。

let person = {
    userName: 'ls',
    getUserName() {
        setTimeout(function() {
            console.log(this.userName)
        }, 1000)
    }
}
person.getUserName();
1
2
3
4
5
6
7
8
9

上面的输出结果为: undefined ,因为在setTimeout中this指的是window, 而不是person对象。

为了解决上面的问题,可以将代码进行如下的修改:

let person = {
    userName: 'ls',
    getUserName() {
        let that = this;
        setTimeout(function() {
            console.log(that.userName)
        }, 1000)
    }
}
person.getUserName();
1
2
3
4
5
6
7
8
9
10

在进入setTimeout这个方法之前,提前将this赋值给that变量,然后在setTimeout中使用that, 那么这时that指的就是person对象。

上面的解决方法比较麻烦,可以修改成箭头函数的形式,代码如下所示:

let person = {
    userName: 'wangwu',
    getUserName() {
        setTimeout(() => {
            console.log(this.userName);
        }, 1000)
    }
}
person.getUserName();
1
2
3
4
5
6
7
8
9

通过上面的代码,可以发现在箭头函数中直接使用this是没有问题的。

你可以这样理解:**在箭头函数中是没有this的,如果在箭头函数中使用了this, 那么实际上使用的是外层代码块的this. 箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承this **

或者通俗的理解:找出定义箭头函数的上下文(即包含箭头函数最近的函数或者是对象),那么上下文所处的父上下文即为this.

那么在我们这个案例中, setTimeout 函数中使用了箭头函数,箭头函数中用了 this, 而这时 this 指的是外层代码块也就是 person , 所以箭头函数中使用的this指的就是 person (包含箭头函数最近的函数是 setTimeout , 那么包含 setTimeout 这个函数的最近的函数或者是对象是谁呢?对了,是 getUserName 这个函数,而 getUserName 这个函数是属于哪个对象呢?是 person , 所以 thisperson )

下面,再看一个案例:(可以将下面的代码转换成ES5的代码)

let person = {
    userName: 'wangcai',
    getUserName: () => {
        console.log(this.userName)
    }
}
person.getUserName();
1
2
3
4
5
6
7

输出结果为:undefined

因为这时包含 getUserName 这个箭头函数最近的对象是person(这里也就是说 getUserName 这个箭头函数的上下文为 person ), 那么 person 对象所处的父上下文(也就是包含person这个对象最近的对象),是谁呢?对了,就是 window

下面再看一个案例:

let person = {
    userName: 'wangcai',
    getUserName() {
        return () => {
            console.log(this.userName);
        }
    }
}
person.getUserName()();
1
2
3
4
5
6
7
8
9

根据上面总结的规律是,这段代码输出的结果是:'wangcai'.

在这里还需要注意一个问题就是:

由于箭头函数没有自己的this, 所以不能使用 call()apply()bind() 这些方法来改变this的指向。

let adder = {
    base: 1,
    add: function(a) {
        let f = v => v + this.base;
        let b = {
            base: 3
        };
        return f.call(b, a);
    }
};
console.log(adder.add(1))
1
2
3
4
5
6
7
8
9
10
11

上面代码执行的结果为:2

也就是说,箭头函数不能使用 call( ) 来改变this的指向,本意是想让this指向b这个对象,但是实际上this还是adder这个对象。

# 5.3.3 箭头函数不适合的场景

第一:不能作为构造函数,不能使用 new 操作符

构造函数是通过 new 操作符生成对象实例的,生成实例的过程也是通过构造函数给实例绑定 this 的过程,而箭头函数没有自己的 this ,因此不能使用箭头函数作为构造函数。

如下代码:

function Person(name) {
    this.name = name;
}
var p = new Person("wangcai"); //正常
1
2
3
4

以上是我们前面经常使用的一种方式,没有问题

下面看一下使用箭头函数作为构造函数的情况

let Person = (name) => {
    this.userName = name;
};
let p = new Person("lisi");
1
2
3
4

当执行上面的程序的时候,会出现错误

第二:没有 prototype 属性

因为在箭头函数中没有 this , 也就不存在自己的作用域,因此箭头函数是没有 prototype 属性的。

let Person = (name) => {
    this.userName = name;
};
console.log(Person.prototype); // undefined
1
2
3
4

第三:不适合将原型函数定义成箭头函数

在给构造函数添加原型函数时,如果使用箭头函数,其中的 this 会指向全局作用域 window , 而不会指向构造函数。

因此并不会访问到构造函数本身,也就无法访问到实例属性,失去了原型函数的意义。

function Person(name) {
    this.userName = name;
}
Person.prototype.sayHello = () => {
    console.log(this); // window
    console.log(this.userName); // undefined
};
let p = new Person("wangcai");
p.sayHello();
1
2
3
4
5
6
7
8
9

# 6、对象的扩展

# 1、属性与方法的简洁表示方式

以前创建对象的方式:

let userName = 'wangcai';
let userAge = 18;
let person = {
    userName: userName,
    userAge: userAge
}
console.log(person);
1
2
3
4
5
6
7

通过上面的代码,可以发现对象中的属性名和变量名是一样的,像这种情况,在ES6中是可以简化如下形式:

let userName = 'wangcai';
let userAge = 18;
let person = {
    userName,
    userAge
}
console.log(person);
1
2
3
4
5
6
7

通过以上代码可以发现:在ES6中,如果对象的属性名和变量名是一样的,那么两者可以合二为一。

当然,除了属性可以简写,方法也可以简写,以前定义方法的形式如下:

let userName = 'wangcai';
let userAge = 18;
let person = {
    userName,
    userAge,
    sayHello: function() {
        console.log('你好')
    }
}
person.sayHello();
1
2
3
4
5
6
7
8
9
10

在ES6中可以简化成如下的形式:

let userName = 'wangcai';
let userAge = 18;
let person = {
    userName,
    userAge,
    sayHello() {
        console.log('Hello');
    }
}
person.sayHello();
1
2
3
4
5
6
7
8
9
10

所以在以后的编程中,会经常看到或者是用到这种 ES6 的表示形式。

# 2、Object.assign( )方法

# 2.1 基本使用

现在,有一个需求,将一个对象的属性拷贝给另外一个对象,应该怎样处理?

你可能会说,很简单,可以通过循环的方式来来实现。

如下代码所示:

let obj1 = {
    name: 'wangcai'
};
let obj2 = {
    age: 20
};
let obj3 = {};
for (let key in obj1) {
    obj3[key] = obj1[key];
}
for (let key in obj2) {
    obj3[key] = obj2[key];
}
console.log('obj3=', obj3);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

虽然通过循环的方式,可以实现对象属性的拷贝,但是很麻烦。下面讲解一个简单的方法: Object.assign( ) 方法.

Object.assign( ) 方法用来源对象的所有可枚举的属性复制到目标对象。该方法至少需要两个对象作为参数,第一个参数是目标对象,后面的参数都是源对象。只要有一个参数不是对象,就会抛出异常。

示例代码如下:

let target = {
    a: 1,
    b: 2
};
let source = {
    c: 3,
    d: 4
};
Object.assign(target, source);
console.log(target);
1
2
3
4
5
6
7
8
9
10

最终的结果:将source对象中的属性拷贝到target对象上。

在上面的定义中,可以看出参数不仅两个,可以有多个,但是要注意的是第一个参数一定是目标对象,下面再看一个多个参数的案例:

let target = {
    a: 1,
    b: 2
};
let source = {
    c: 3,
    d: 4
};
let source1 = {
    e: 5,
    f: 6
};

Object.assign(target, source, source1);
console.log(target);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

通过上面的代码,将source和 source1 这两个对象的属性都拷贝给了target对象。

# 2.2 深浅拷贝问题

通过 Object.assign( ) 方法,实现的拷贝只拷贝了属性的值,属于浅拷贝。

如下代码所示:

let obj1 = {
    name: '张三',
    address: {
        city: '北京'
    }
}
let obj2 = {};
Object.assign(obj2, obj1);
obj2.address.city = "上海";
console.log("obj1=", obj1);
console.log("obj2=", obj2);
1
2
3
4
5
6
7
8
9
10
11

上面的代码中对 obj2 这个对象的city属性的值进行了修改,发现对应的 obj1 对象中的city属性的值也发生了改变。

但是,在某些情况下,我们不希望这样,我们希望修改一个对象的属性值时,不会影响到另外一个对象的属性值。

那么对应的要实现相应的深拷贝。

关于深拷贝,实现方式比较多,下面简单的说一种方式:(这里只是简单的模拟)

function clone(source) {
    let newObj = {};
    for (let key in source) {
        // 由于address属性为对象,所以执行递归。
        if (typeof source[key] === 'object') {
            newObj[key] = clone(source[key]);
        } else {
            // 如果是name属性直接赋值
            newObj[key] = source[key];
        }
    }
    return newObj;
}
let obj1 = {
    name: '张三',
    address: {
        city: '北京'
    }
}
let obj2 = clone(obj1);
obj2.address.city = "上海";
console.log("obj1=", obj1);
console.log("obj2=", obj2);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 2.3 注意事项

1、如果目标对象与源对象有同名属性,那么后面的属性会覆盖前面的属性。

let target = {
    a: 1,
    b: 2
};
let source = {
    b: 3,
    d: 4
};
Object.assign(target, source);
console.log(target);
1
2
3
4
5
6
7
8
9
10

上面的代码,将source对象中的属性拷贝给了target对象,但是source对象中有b这个属性,而且target对象上也有b属性,那么最终的结果是:source对象中的b属性覆盖掉target对象中的b属性。

2、不可枚举的属性不会被复制。

let obj = {};
Object.defineProperty(obj, 'b', {
    enumerable: false,
    value: 'world'
})
let obj1 = {
    a: 'hello'
}
Object.assign(obj1, obj);
console.log('obj1=', obj1);
1
2
3
4
5
6
7
8
9
10

在上面的代码中,通过 Object.defineProperty() 方法为obj对象添加了一个属性b, 这个属性的值为 'world', 并且指定了enumerable这个属性的值为false. 也就是不可以被枚举。也就是该属性不可以通过for in来进行遍历。

最终,通过 Object.assign 这个方法进行拷贝,发现 obj1 对象中没有obj对象的b这个属性。

# 7、Symbol

# 7.1、Symbol简介

在具体讲解Symbol之前,先来看一个问题。

ES5 的对象属性名都是字符串,这样容易造成属性名的冲突。

代码如下所示:

let obj = {
    num: 10,
    "num 1": 20
}
console.log(obj.num);
console.log(obj["num 1"]);
1
2
3
4
5
6

通过以上的代码,可以发现在对象中定义的属性的名称本身就是字符串,而且发现“num 1”中间是有空格的,所以在访问该属性的时候,通过[ ]的形式来进行访问。

那么为什么说容易造成属性名的冲突呢?

举例说明:你用了一个别人提供的对象,但是又想为这个对象添加新的方法或者是属性,新方法或者是新属性的名称有可能与现有对象中的属性名称或者是方法的名称产生冲突。如果有一种机制,能够保证每个属性的名字都是唯一的,那么就能够从根本上防止属性名称的冲突问题。这也就是ES6引入Symbol的原因。

Symbol是一种数据类型,是JavaScript语言的第7种数据类型,前6种分别是:undefined, null, 布尔值,字符串,数值和对象。

Symbol类型的值是通过Symbol函数生成的。它的值是独一无二的,也就是唯一的,可以保证对象中属性名称的唯一。

可以通过如下的代码测试类型

let s = Symbol();
console.log(typeof s);
1
2

对应的输出类型为"symbol"

下面创建Symbol类型的变量,然后进行打印输出。

let s = Symbol();
let s1 = Symbol();
console.log(s);
console.log(s1);
1
2
3
4

发现输出的结果都是:Symbol( )。

输出的结果都是Symbol( ), 那么无法区分,哪个Symbol( )是s变量的,哪个是s1变量的。

为了解决这个问题,Symbol( )函数可以接受一个字符串作为参数,这个参数表示对Symbol的描述,主要是为了在控制台进行输出打印的时候,能够区分开,Symbol最终是属于哪个变量的。

let s = Symbol('s');
let s1 = Symbol('s1');
console.log(s);
console.log(s1);
1
2
3
4

输出的结果为:Symbol(s)和Symbol(s1)

注意:Symbol函数的参数只表示对当前Symbol值(结果)的描述,因此相同参数的Symbol函数的返回值是不相等的。代码如下:

let s = Symbol('s');
let s1 = Symbol('s');
console.log(s === s1);
1
2
3

以上结果为:false.

# 7.2、Symbol应用场景

# 7.2.1 作为属性名的Symbol

在前面的课程中,讲解过由于Symbol的值是唯一的,并且能够保证对象中不会出现同名的属性。下面,先来讲解一下,怎样使用Symbol作为属性名,然后再看一下怎样保证对象中不会出现同名属性。

第一种添加属性的方式:

let mySymbol = Symbol();
let obj = {}
// 第一种添加属性的方式
obj[mySymbol] = 'hello';
console.log(obj[mySymbol]);
1
2
3
4
5

第二种添加属性的方式:

let mySymbol = Symbol();
let obj = {
    [mySymbol]: 'world' // 注意mySymbol必须加上方括号,否则为字符串而不是Symbol类型。
}
console.log(obj[mySymbol]);
1
2
3
4
5

第三种添加属性的方式

let mySymbol = Symbol();
let obj = {};
Object.defineProperty(obj, mySymbol, {
    value: '你好'
})
console.log(obj[mySymbol]);
1
2
3
4
5
6

# 7.2.2 防止属性名称冲突

在前面,已经讲解了怎样使用Symbol作为属性名了,下面看一下怎样通过Symbol来防止属性名的冲突。

下面先定义一个对象,然后动态的向对象中添加一个id属性。

let obj = {
    name: 'zs',
    age: 18
}

function test1(obj) {
    obj.id = 42;
}

function test2(obj) {
    obj.id = 369;
}
test1(obj);
test2(obj);
console.log(obj);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在上面的代码中,有两个函数分别是test1和test2向obj这个对象中动态添加id属性,在这里可以把这两个函数想象成两个不同的模块,或者是两个不同开发人员来实现的功能。

但是问题是,由于test2( )这个函数后执行,所以会将test1( )这个函数创建的id属性的值覆盖掉。那么这是我们不希望看到的,为了解决这个问题,可以使用Symbol作为属性名来解决。

let obj = {
    name: 'zs',
    age: 18
}
let mySymbol = Symbol('lib1');

function test1(obj) {
    obj[mySymbol] = 42;

}
let mySymbol2 = Symbol('lib2');

function test2(obj) {
    obj[mySymbol2] = 369;
}
test1(obj);
test2(obj);
console.log(obj);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

通过上面的代码可以发现,通过Symbol解决了属性名称冲突的问题。

# 8、Proxy

# 1. Proxy简介

有这样一个需求:有一个对象,我们需要监听这个对象的属性被设置了或被获取了。

直接上代码:

<script>
    let obj = {
        name: "wc",
        age: 18
    }

    Object.defineProperty(obj, "name", {
        get: function() {
            console.log("监听到了obj对象的name属性被访问了");

            return "666";
        },
        set: function() {
            console.log("监听到了obj对象的name属性被设置了");
        }
    })

    // 在你获取或设置时,能不能监听到
    console.log(obj.name); // 获取obj中的属性
    obj.name = "xq"; // 设置obj中的name属性
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

上面的代码是监听到了name属性的,我想监听一个对象上N个属性,如何做:

<script>
    let obj = {
        name: "wc",
        age: 18,
        adress: "bj",
    }
    Object.keys(obj).forEach(key => {
        let value = obj[key];
        Object.defineProperty(obj, key, {
            get: function() {
                console.log(`监听到了obj对象的${key}属性被访问了`);
                return value;
            },
            set: function() {
                console.log(`监听到了obj对象的${key}属性被设置了`);
            }
        })
    })
    console.log(obj.name);
    obj.name = "wc666";
    console.log(obj.adress);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

上面监听属性的变化,有什么不足?

  1. Object.defineProperty刚开始设计初衷,并不是用来监听对象中的属性
  2. 如果对象非常复杂,需要递归去监听,一旦递归,性能非常差
  3. 有些操作监听不了,如添加属性,删除属性....

基于上面的缺点,在ES6中,引出了Proxy类,从这个类名上可以看出,它是一个代理。 和日常生活中的代理是一个意思,就是中介。也就是说,如果我们监听一个对象的相关操作,那么我们需要创建一个代理类,之后该对象的所有操作,都通过代码对象来完成,代理对象可以监听到我们对原始对象的操作。

使用Proxy监听对对象中属性的操作,代码如下:

<script>
    let obj = {
        name: "wc",
        age: 18
    }

    // Proxy是ES6中的一个类
    // objProxy 是上面obj的代理对象
    // obj 叫原始对象
    // {} handler 处理对象
    let objProxy = new Proxy(obj, {
        // key 表示你访问的属性名
        // target 表示原始对象
        get: function(target, key) {
            console.log(`监听到了obj对象的${key}属性被访问了`, target);
            // .....
            return target[key]
        },
        set: function(target, key, newValue) {
            console.log(`监听到了obj对象的${key}属性被设置了`, target);
            target[key] = newValue;
        }
    })
    console.log(objProxy.name);
    objProxy.name = "wc666"
    console.log(objProxy.name);
</script>
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

当new Proxy时,第1个参数是原始对象,第2个参数是处理对象,处理对象中放捕获器,上面的的get和set其实就是捕获器,proxy中有13的捕获器:

<script>
    let obj = {
        name: "wc",
        age: 18
    }
    // 代理对象就可以监听到你对对象中属性的操作
    let objProxy = new Proxy(obj, {
        // 获取值时的捕获器
        get: function(target, key) {
            console.log(`监听到了obj对象的${key}属性被访问了`, target);
            return target[key]
        },
        // 设置值时的捕获器
        set: function(target, key, newValue) {
            console.log(`监听到了obj对象的${key}属性被设置了`, target);
            target[key] = newValue;
        },
        // 监听in的捕获器
        has: function(target, key) {
            console.log(`监听到了obj对象的${key}属性in操作`, target);
            return key in target;
        },
        // 监听delete的捕获器
        deleteProperty: function(target, key) {
            console.log(`监听到了obj对象的${key}属性delete操作`, target);
            delete target[key]
        }
    })
    // 判断name是否是objProxy的属性
    console.log("name" in objProxy);
    delete objProxy.name;
    console.log(objProxy.name);
</script>
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

注意:要使Proxy起作用,必须针对Proxy对象进行操作,不是针对目标对象进行操作(上面的是student对象)。

# 9、Set和Map结构

在ES6之前,我们存储数据的结构主要有两种:数组、对象。在ES6中新增了另外两种数据结构:Set、Map,以及它们的另外形式WeakSet、WeakMap。


Set结构与数组类似,但是成员的值都是唯一的,没有重复值。创建Set我们需要通过Set构造函数(暂时没有字面量创建的方式), 我们可以发现Set中存放的元素是不会重复的,那么Set有一个非常常用的功能就是给数组去重

# 1.1 常用的操作方法

关于常用的操作方法,这里会讲解如下4个。

add(value) : 添加某个值,返回Set结构本身。

delete(value) : 删除某个值,返回一个布尔值,表示删除是否成功

has(value) : 返回一个布尔值,表示参数是否为Set的成员.

clear() : 清除所有成员,没有返回值

遍历 : 另外Set是支持for of的遍历的

# 1.1.1 add( )方法

下面先看一下 add( ) 方法的使用

let s = new Set();
s.add(1);
s.add(2);
s.add(3);
console.log(s);
console.log(s.size);
1
2
3
4
5
6

在上面的代码中,用到了size属性,这个属性返回的是Set结构中的成员总数。

Set结构中的成员是不允许出现重复值的,下面测试一下。

let s = new Set();
s.add(1);
s.add(2);
s.add(3);
s.add(3)
console.log(s);
console.log(s.size);
1
2
3
4
5
6
7

在上面的代码中,又添加了一个数字3,但是在输出的时候,发现3这个数值只出现了一次,并且总数的个数也没有发生变化,所以Set是不允许出现重复的。

# 1.1.2 has( )方法

let s = new Set();
s.add(1);
s.add(2);
s.add(3);
s.add(3);
console.log(s);
console.log(s.size);
console.log(s.has(3))
console.log(s.has(5))
1
2
3
4
5
6
7
8
9

# 1.1.3 delete( )方法

let s = new Set();
s.add(1);
s.add(2);
s.add(3);
s.add(3);
console.log(s);
console.log(s.size);
console.log(s.delete(3)) //删除成功返回true.
console.log(s.has(3))
console.log(s.has(5))
1
2
3
4
5
6
7
8
9
10

# 1.1.4 clear()方法

let s = new Set();
s.add(1);
s.add(2);
s.add(3);
s.add(3);
console.log(s);
console.log(s.size);
console.log(s.delete(3))
console.log(s.has(3))
console.log(s.has(5))
s.clear(); // 清除所有项
console.log(s);
1
2
3
4
5
6
7
8
9
10
11
12

Set结构是一个类似数组的结构,那么怎样转换成一个真正的数据呢?

可以通过前面学习的 Arrray.from 方法。

let s = new Set();
s.add(1);
s.add(2);
s.add(3);
let array = Array.from(s);
console.log(array);
1
2
3
4
5
6

在使用数组编程的时候,经常会用到一个功能,就是清除数组中的重复的数据,那么在这里可以借助于Set结构来完成。

// 清除数组中的重复数据.
// Set函数可以接受一个数组或者是类似数组的对象,作为参数。
let array = [1, 2, 3, 3, 5, 6];
let s = new Set(array);
console.log(Array.from(s));
1
2
3
4
5

# 2.1 WeakSet使用

和Set类似的另外一个数据结构称之为WeakSet,也是内部元素不能重复的数据结构。

那么和Set有什么区别呢?

  • 区别一:WeakSet中只能存放对象类型,不能存放基本数据类型;
  • 区别二:WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收;
  • 区别三:WeakSet不能遍历,因为WeakSet只是对对象的弱引用,如果我们遍历获取到其中的元素,那么有可能造成对象不能正常的销毁,所以存储到WeakSet中的对象是没办法获取的;

WeakSet常见的方法:

  • add(value):添加某个元素,返回WeakSet对象本身
  • delete(value):从WeakSet中删除和这个值相等的元素,返回boolean类型
  • has(value):判断WeakSet中是否存在某个元素,返回boolean类型
 // 1.Weak Reference(弱引用)和Strong Reference(强引用)
 let obj1 = {
     name: "wc"
 }
 let obj2 = {
     name: "xq"
 }
 let obj3 = {
     name: "z3"
 }

 // let arr = [obj1, obj2, obj3]
 // obj1 = null
 // obj2 = null
 // obj3 = null

 // const set = new Set(arr)
 // arr = null

 // 2.WeakSet的用法
 // 2.1.和Set的区别一: 只能存放对象类型
 const weakSet = new WeakSet()
 weakSet.add(obj1)
 weakSet.add(obj2)
 weakSet.add(obj3)

 // 2.2.和Set的区别二: 对对象的引用都是弱引用

 // 3.WeakSet的应用
 const pWeakSet = new WeakSet()
 class Person {
     constructor() {
         pWeakSet.add(this)
     }

     running() {
         if (!pWeakSet.has(this)) {
             console.log("Type error: 调用的方式不对")
             return
         }
         console.log("running~")
     }
 }

 let p = new Person()
 // p = null
 p.running()
 const runFn = p.running
 runFn()
 const obj = {
     run: runFn
 }
 obj.run()
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
43
44
45
46
47
48
49
50
51
52
53

# 3.1 Map结构

另外一个新增的数据结构是Map,用于存储映射关系。但是我们可能会想,在之前我们可以使用对象来存储映射关系,他们有什么区别呢?

  • 事实上我们对象存储映射关系只能用字符串(ES6新增了Symbol)作为属性名(key)
  • 某些情况下我们可能希望通过其他类型作为key,比如对象,这个时候会自动将对象转成字符串来作为key,那么我们就可以使用Map

Map的常用属性和方法

  • 常见的属性之size:返回Map中元素的个数;
  • 常见的方法之set(key, value):在Map中添加key、value,并且返回整个Map对象
  • 常见的方法之get(key):根据key获取Map中的value;
  • 常见的方法之has(key):判断是否包括某一个key,返回Boolean类型;
  • 常见的方法之delete(key):根据key删除一个键值对,返回Boolean类型
  • 常见的方法之clear():清空所有的元素;
  • 常见的方法之forEach(callback, [, thisArg]):通过forEach遍历Map;
  • Map也可以通过for of进行遍历。
 const info = {
     name: "wc"
 }
 const info2 = {
     age: 18
 }

 // 1.对象类型的局限性: 不可以使用复杂类型作为key
 // const obj = {
 //   address: "bj",
 //   [info]: "haha",
 //   [info2]: "hehe"
 // }
 // console.log(obj)

 // 2.Map映射类型
 const map = new Map()
 map.set(info, "wc")
 map.set(info2, "xq")
 console.log(map)

 // 3.Map的常见属性和方法
 // console.log(map.size)
 // 3.1. set方法, 设置内容
 map.set(info, "z3")
 console.log(map)
 // 3.2. get方法, 获取内容
 // console.log(map.get(info))
 // 3.3. delete方法, 删除内容
 // map.delete(info)
 // console.log(map)
 // 3.4. has方法, 判断内容
 // console.log(map.has(info2))
 // 3.5. clear方法, 清空内容
 // map.clear()
 // console.log(map)
 // 3.6. forEach方法
 // map.forEach(item => console.log(item))

 // 4.for...of遍历
 for (const item of map) {
     const [key, value] = item
     console.log(key, value)
 }
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
43
44

# 4.1 WeakMap的使用

和Map类型的另外一个数据结构称之为WeakMap,也是以键值对的形式存在的。

和Map有什么区别:

  • 区别一:WeakMap的key只能使用对象,不接受其他的类型作为key;
  • 区别二:WeakMap的key对对象想的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象
  • 区别三:WeakMap也是不能遍历的,没有forEach方法,也不支持通过for of的方式进行遍历;

WeakMap常见的方法有四个:

  • set(key, value):在Map中添加key、value,并且返回整个Map对象;
  • get(key):根据key获取Map中的value;
  • has(key):判断是否包括某一个key,返回Boolean类型;
  • delete(key):根据key删除一个键值对,返回Boolean类型;
 let obj1 = {
     name: "wc"
 }
 let obj2 = {
     name: "xq"
 }

 // 1.WeakMap的基本使用
 const weakMap = new WeakMap()
 // weakMap.set(123, "aaa")
 weakMap.set(obj1, "aaa")
 weakMap.set(obj2, "bbb")

 obj1 = null
 obj2 = null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 10、class定义类

前面,可以把函数当成一个类,函数当成一个类,在其它编程语言中,我没见过,ES6中,为了和其它编程语言保持一致,提出一个class关键,通过class去定义类,但是class仅仅是语法糖,最终还需要转化成函数。

使用class定义一个类,如下:

<script>
    // Person是类名
    class Person {

    }
    let p = new Person();
    console.log(p); // 对象
</script>
1
2
3
4
5
6
7
8
<script>
    // Person是类名
    // 类表达式
    let Person = class {

    }
    let p = new Person();
    console.log(p); // 对象
</script>
1
2
3
4
5
6
7
8
9

通过类创建出来的对象和通过构造器创建出来的对象,有什么区别? 它和我们前面使用构造器创建类的特性基本上是一样的。

<script>
    function Student() {}
    console.log(typeof Student); // function

    class Person {

    }
    let p = new Person();
    // 通过class创建的类,也有prototype
    console.log(Person.prototype);
    console.log(p.__proto__);
    console.log(Person.prototype == p.__proto__);
    console.log(typeof Person);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

通过class创建的对象,如何赋值私有属性和公有属性,如下:

<script>
    class Person {
        // 一个类只有一个constructor
        // 1)内部创建一个对象  nm = {}
        // 2)将Person的原型prototype赋值给创建出来的对象  nm.__proto__ = Person.prototype
        // 3)将对象赋值给this  将this指向对象   new绑定  this = mn;
        // 4)执行constructor中的代码
        // 5)返回创建出来的对象  return nm
        constructor(name, age) {
            this.name = name;
            this.age = age;
        }
    }
    // p的私有属性:name  age
    let p = new Person("wc", 18);
    console.log(p.hasOwnProperty("name"));
    console.log(p.hasOwnProperty("age"));
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

对象中的方法或公有属性写法如下:

<script>
    class Person {
        constructor(name, age) {
            this.name = name;
            this.age = age;
        }
        // running是公有属性还是私有属性
        running() {
            console.log(this.name + " running...");
        }
    }
    let p = new Person("wc", 18);
    p.running();
    console.dir(p)
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

类中的设置器和访问器,使用如下:

<script>
    class Person {
        constructor(name, age) {
            this.name = name;
            this.age = age;
            // _address 表示不建议在类的外面访问
            // 当时就可以使用get 和 set
            this._address = "bj"
        }
        get address() {
            console.log("getter调用了~");
            // getter返回什么,address属性就是什么
            return this._address;
        }
        set address(value) {
            this._address = value;
        }
    }
    let p = new Person("wc", 18);
    console.log(p.address); // 自动调用上面的get address(){}
    p.address = "gz" // 自动调用上面的set address(){}
    console.log(p.address);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

之前,使用函数创建创建,函数有多种角色,函数可以是类,也可以是对象,可以在函数这个对象上添加属性,如下:

<script>
    function Fn() {

    }
    let f = new Fn();
    Fn.age = 110;
    Fn.address = "bj";
    console.log(Fn.age);
    console.log(Fn.address);
</script>
1
2
3
4
5
6
7
8
9
10

能不能在class对应的类名上添加一些属性呢?答:可以的,这样的属性,叫静态属性,如下:

<script>
    // 一切都是对象
    class Person {
        constructor(name, age) {
            this.name = name;
            this.age = age;
        }
        running() {
            console.log(this.name + " running...");
        }
        // 静态属性 只能通过类名来访问
        static eating() {
            console.log("eating...");
        }
    }
    Person.eating();
    let p = new Person("wc", 10);
    p.eating();
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

通过class创建的类,如何玩继承呢,代码如下:

<script>
    class Person {
        constructor(name, age) {
            this.name = name;
            this.age = age;
        }
        running() {
            console.log(this.name + " running...");
        }
        static eating() {
            console.log("eating...");
        }
    }
    // extends表示继承  Student类继承了Person类
    class Student extends Person {
        constructor(name, age, sno) {
            // 由于Student继承了Person,当new Student时,还需要走Person的constructor
            // super(); // super表示调用Persion的constructor
            // this.name = name;  // name表示Student的私有属性
            // this.age = age;  // age表示Student的私有属性
            // this.sno = sno;   // sno表示Student的私有属性

            super(name, age);
            this.sno = sno;
        }
    }
    let stu = new Student("wc", 18, 110);
    console.log(stu.name);
    console.log(stu.age);
    stu.running();
    Student.eating(); // 静态属性也可以继承到
</script>
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

父类有的公有属性,子类是可以重写,代码如下:

<script>
    class Person {
        constructor(name, age) {
            this.name = name;
            this.age = age;
        }
        running() {
            console.log(this.name + " running...");
        }
    }
    class Student extends Person {
        constructor(name, age, sno) {
            super(name, age);
            this.sno = sno;
        }
        // 父类有的方法,子类也可以有同名方法,重写
        // super.running();表示调用父类的runing
        running() {
            super.running();
            console.log("Student running....");
        }
    }
    let stu = new Student("wc", 18, 110);
    stu.running(); // 如果自已有,就调用自己的
</script>
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

我们自己写的类,能不能继承,JS中内置的类呢?如下:

<script>
    // 自己实现的类,去继承JS中的内置的类
    class MyArray extends Array {
        // 你的push,覆盖了Array中的push
        // 重写
        push() {
            console.log("....");
        }
    }
    let marr = new MyArray();
    marr.push(1)
    marr.push(2)
    console.log(marr);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 11、ES7 - Array Includes

在ES7之前,如果我们想判断一个数组中是否包含某个元素,需要通过 indexOf 获取结果,并且判断是否为 -1。在ES7中,我们可以通过includes来判断一个数组中是否包含一个指定的元素,根据情况,如果包含则返回 true,否则返回false。

let names = ["wc", "xq", "z3"];
if (names.includes("wc")) {
    console.log("包含wc")
}

console.log(names.indexOf(NaN)); // -1 
console.log(names.includes(NaN)); // true
1
2
3
4
5
6
7

# 12、ES7 –指数exponentiation运算符

在ES7之前,计算数字的乘方需要通过 Math.pow 方法来完成。在ES7中,增加了 ** 运算符,可以对数字来计算乘方

const res = Math.pow(2, 2);
const res2 = 3 ** 3;
console.log(res, res2)
1
2
3

# 13、ES8 Object values 和 Object entries

之前我们可以通过 Object.keys 获取一个对象所有的key,在ES8中提供了 Object.values 来获取所有的value值。

const obj = {
    name: "wc",
    age: 18,
    height: 1.88,
    address: "bj"
}

// 1.获取所有的key
const keys = Object.keys(obj)
console.log(keys)

// 2.ES8 Object.values
const values = Object.values(obj)
console.log(values)

// 3.ES8 Object.entries
// 3.1. 对对象操作
const entries = Object.entries(obj)
console.log(entries)
for (const entry of entries) {
    const [key, value] = entry
    console.log(key, value)
}

// 3.2. 对数组/字符串操作
console.log(Object.entries(["wc", "xq"]))
console.log(Object.entries("Hello"))
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

# 14、ES8 - String Padding

某些字符串我们需要对其进行前后的填充,来实现某种格式化效果,ES8中增加了 padStart 和 padEnd 方法,分别是对字符串的首尾进行填充的。应用场景:比如需要对身份证、银行卡的前面位数进行隐藏:

// padStart和padEnd
// 1.应用场景一: 对时间进行格式化
// const minute = "15".padStart(2, "0")
// const second = "6".padStart(2, "0")

// console.log(`${minute}:${second}`)

// 2.应用场景二: 对一些敏感数据格式化
let cardNumber = "410883199898764665"
const sliceNumber = cardNumber.slice(-4)
cardNumber = sliceNumber.padStart(cardNumber.length, "*")
console.log(cardNumber)
1
2
3
4
5
6
7
8
9
10
11
12

# 15、ES8 - Trailing Commas

在ES8中,我们允许在函数定义和调用时多加一个逗号

 function foo(num1, num2, ) {
     console.log(num1, num2)
 }

 foo(10, 20, )
1
2
3
4
5

# 16、ES8 - Object Descriptors

  • Object.getOwnPropertyDescriptors

# 17、ES8 - Async Function

  • 之前已讲过

# 18、ES9新增知识点

ES9新增知识点

  • Async iterators:迭代器
  • Object spread operators:之前讲过了
  • Promise finally:之前讲过了

# 19、ES10 - flat flatMap

flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回

flatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组

  • 注意一:flatMap是先进行map操作,再做flat的操作;
  • 注意二:flatMap中的flat相当于深度为1;
// flat的使用: 将一个数组, 按照制定的深度遍历, 将遍历到的元素和子数组中的元素组成一个新的数组, 进行返回
const nums = [10, 20,
    [111, 222],
    [333, 444],
    [
        [123, 321],
        [231, 312]
    ]
]
const newNums1 = nums.flat(1)
console.log(newNums1)
const newNums2 = nums.flat(2)
console.log(newNums2)

// 2.flatMap的使用:对数组中每一个元素应用一次传入的map对应的函数
const messages = [
    "wc",
    "xq",
    "z3"
]

// 1.for循环的方式:
// const newInfos = []
// for (const item of messages) {
//   const infos = item.split(" ")
//   for (const info of infos) {
//     newInfos.push(info)
//   }
// }
// console.log(newInfos)

// 2.先进行map, 再进行flat操作
// const newMessages = messages.map(item => item.split(" "))
// const finalMessages = newMessages.flat(1)
// console.log(finalMessages)

// 3.flatMap
const finalMessages = messages.flatMap(item => item.split(" "))
console.log(finalMessages)
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

# 20、ES10 - Object fromEntries

在前面,我们可以通过 Object.entries 将一个对象转换成 entries,那么如果我们有一个entries了,如何将其转换成对象呢?ES10提供了 Object.formEntries来完成转换:

 // 1.对象
 const obj = {
     name: "wc",
     age: 18,
     height: 1.88
 }

 const entries = Object.entries(obj)
 const info = Object.fromEntries(entries)
 console.log(info)

 // 2.应用
 const searchString = "?name=wc&age=18&height=1.88"
 const params = new URLSearchParams(searchString)
 console.log(params.get("name"))
 console.log(params.get("age"))
 console.log(params.entries())

 for (const item of params.entries()) {
     console.log(item)
 }

 const paramObj = Object.fromEntries(params)
 console.log(paramObj)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 21、ES10 - trimStart trimEnd

去除一个字符串首尾的空格,我们可以通过trim方法,如果单独去除前面或者后面呢

  • ES10中给我们提供了trimStart和trimEnd
const message = "   Hello World    "
console.log(message.trim())
console.log(message.trimStart())
console.log(message.trimEnd())
1
2
3
4

# 22、ES11 - BigInt

在早期的JavaScript中,我们不能正确的表示过大的数字, 大于MAX_SAFE_INTEGER的数值,表示的可能是不正确的,如下:

let maxInt = Number.MAX_SAFE_INTEGER;
console.log(maxInt);
console.log(maxInt + 1);
console.log(maxInt + 2);
1
2
3
4

那么ES11中,引入了新的数据类型BigInt,用于表示大的整数, BitInt的表示方法是在数值的后面加上n

let bigInt = 9007199254740992n;
console.log(bigInt + 1n);
console.log(bigInt + 2n);
1
2
3

# 23、ES11 - Nullish Coalescing Operator

ES11,Nullish Coalescing Operator增加了空值合并操作符:

let info = undefined
// info = info || "默认值"
// console.log(info)

// ??: 空值合并运算符
info = info ?? "默认值"
console.log(info)
1
2
3
4
5
6
7

# 24、ES11 - Optional Chaining

可选链也是ES11中新增一个特性,主要作用是让我们的代码在进行null和undefined判断时更加清晰和简洁:

const obj = {
    name: "wc",
    friend: {
        name: "xq",
        // running: function() {
        //   console.log("running~")
        // }
    }
}

// 1.直接调用: 非常危险
// obj.friend.running()

// 2.if判断: 麻烦/不够简洁
// if (obj.friend && obj.friend.running) {
//   obj.friend.running()
// }

// 3.可选链的用法: ?.
obj?.friend?.running?.()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 25、ES11 - Global This

之前我们希望获取JavaScript环境的全局对象,不同的环境获取的方式是不一样的

  • 在浏览器中可以通过this、window来获取;
  • 在Node中我们需要通过global来获取;

在ES11中对获取全局对象进行了统一的规范:globalThis

console.log(globalThis)
1

# 26、ES11 - for..in标准化

在ES11之前,虽然很多浏览器支持for...in来遍历对象类型,但是并没有被ECMA标准化,在ES11中,对其进行了标准化,for...in是用于遍历对象的key的。

let obj = {
    name: "wc",
    age: 18,
    height: 1.88
}
for (let key in obj) {
    console.log(key)
}
1
2
3
4
5
6
7
8

# 27、ES12 - logical assignment operators

 // 赋值运算符
 // const foo = "xq"
 let counter = 100
 counter = counter + 100
 counter += 50

 // 逻辑赋值运算符
 function foo(message) {
     // 1.||逻辑赋值运算符
     // message = message || "默认值"
     // message ||= "默认值"

     // 2.??逻辑赋值运算符
     // message = message ?? "默认值"
     message ?? = "默认值"

     console.log(message)
 }

 foo("wc")
 foo()

 // 3.&&逻辑赋值运算符
 let obj = {
     name: "wc",
     running: function() {
         console.log("running~")
     }
 }

 // 3.1.&&一般的应用场景
 // obj && obj.running && obj.running()
 // obj = obj && obj.name
 obj && = obj.name
 console.log(obj)
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

# 28、ES13 - method .at()

字符串、数组的at方法,它们是作为ES13中的新特性加入的。

# 29、ES13 - Object.hasOwn(obj, propKey)

Object中新增了一个静态方法(类方法): hasOwn(obj, propKey), 方法用于判断一个对象中是否有某个自己的属性;

和之前学习的Object.prototype.hasOwnProperty有什么区别呢

  • 区别一:防止对象内部有重写hasOwnProperty
  • 区别二:对于隐式原型指向null的对象, hasOwnProperty无法进行判断
 const obj = {
     name: "wc",
     age: 18,
     // 防止对象中也有一个自己的hasOwnProperty方法
     hasOwnProperty: function() {
         return "ok"
     },
     __proto__: {
         address: "bj"
     }
 }

 console.log(obj.name, obj.age)
 console.log(obj.address)

 console.log(obj.hasOwnProperty("name"))
 console.log(obj.hasOwnProperty("address"))

 console.log(Object.hasOwn(obj, "name"))
 console.log(Object.hasOwn(obj, "address"))

 // 和hasOwnProperty的区别二:
 const info = Object.create(null)
 info.name = "wc"
 // console.log(info.hasOwnProperty("name"))
 console.log(Object.hasOwn(info, "name"))
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

# 30、ES13 - New members of classes

在ES13中,新增了定义class类中成员字段(field)的其他方式:

  • Instance public fields
  • Static public fields
  • Instance private fields
  • static private fields
  • static block
 class Person {
     // 实例属性
     // 对象属性: public 公共 -> public instance fields
     height = 1.88

     // 对象属性: private 私有: 潜规则
     // _intro = "name is wc"

     // ES13对象属性: private 私有: 潜规则
     #intro = "name is wc"

     // 2.类属性(static)
     // 类属性: public
     static totalCount = "1000万"

     // 类属性: private
     static #maleTotalCount = "1000万"

     constructor(name, age) {
         // 对象中的属性: 在constructor通过this设置
         this.name = name
         this.age = age
         this.address = "bj"
     }

     // 3.静态代码块
     static {
         console.log("Hello World")
         console.log("Hello Person")
     }
 }

 const p = new Person("wc", 18)
 console.log(p)
 console.log(p.name, p.age, p.height, p.address, p.#intro)
 console.log(Person.#maleTotalCount)
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
Last Updated: 12/25/2022, 10:02:14 PM