七、ES6
从这一章节开始,我们来看一下关于ES6
的重点内容。
1、let 关键字
1.1 基本用法
ES6中新增了let命令,用于变量的声明,基本的用法和var类似。例如:
<script>
// 使用var使用声明变量
var userName = "bxg";
console.log("userName=", userName);
// 使用let声明变量
let userAge = 18;
console.log("userAge=", userAge);
</script>
通过以上的代码,我们发现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>
通过以上的代码,我们知道在循环体中i的值输出的是1–10,最后i的值为11.
但是如果将var换成let会出现什么问题呢?代码如下:
for (let i = 1; i <= 10; i++) {
console.log("i=", i)
}
console.log("last=", i)
在循环体中输出的i的值还是1–10,但是循环体外部打印i的值时出现了错误,错误如下:
出错的原因是:通过let声明的变量只在其对应的代码块中起作用,所谓的代码块我们可以理解成就是循环中的这一对大括号。
当然在这里我们通过这个提示信息,可以发现在ES6中默认是启动了严格模式的,严格模式的特征就是:变量未声明不能使用,否则报的错误就是变量未定义。
那么在ES5中怎样开启严格模式呢?我们可以在代码的最开始加上:“use strict”
刚才我们说到,let声明的变量只在代码块中起作用,其实就是说明了通过let声明的变量仅在块级作用域内有效
1.3 块级作用域
1.3.1 什么是块级作用域?
在这里告诉大家一个最简单的方法: **有一段代码是用大括号包裹起来的,那么大括号里面就是一个块级作用域**
也就是说,在我们写的如下的案例中:
for (let i = 1; i <= 10; i++) {
console.log("i=", i)
}
console.log("last=", i)
i 这个变量的作用域只在这一对大括号内有效,超出这一对大括号就无效了。
1.3.2 为什么需要块级作用域?
ES5 只有全局作用域和函数作用域,没有块级作用域,这样就会带来一些问题,
第一:内层变量可能会覆盖外层变量
代码如下:
var temp = new Date();
function show() {
console.log("temp=", temp)
if (false) {
var temp = "hello world";
}
}
show();
执行上面的代码,输出的结果为 temp=undefined ,原因就是变量由于提升导致内层的temp变量覆盖了外层的temp变量
第二: 用来计数的循环变量成为了全局变量
关于这一点,在前面的循环案例中,已经能够看到。在这里,可以再看一下
<script>
for (var i = 1; i <= 10; i++) {
console.log("i=", i)
}
console.log("last=", i)
</script>
在上面的代码中,变量i的作用只是用来控制循环,但是循环结束后,它并没有消失,而是成了全局的变量,这不是我们希望的,我们希望在循环结束后,该变量就要消失。
以上两点就是,在没有块级作用域的时候,带来的问题。
下面使用let来改造前面的案例。
let temp = new Date();
function show() {
console.log("temp=", temp)
if (false) {
let temp = "hello world";
}
}
show();
通过上面的代码,可以知道let不像var那样会发生“变量提升”的现象。
第二个问题前面已经讲解过。
1.3.3 ES6块级作用域
let实际上为JavaScript新增了块级作用域,下面再看几个案例,通过这几个案例,巩固一下关于“块级作用域”这个知识点的理解,同时进一步体会块级作用域带来的好处
<script>
function test() {
let num = 5;
if (true) {
let num = 10;
}
console.log(num)
}
test()
</script>
上面的函数中有两个代码块,都声明了变量num,但是输出的结果是5.这表示外层的代码不受内层代码块的影响。如果使用var定义变量num,最后的输出的值就是10.
说一下,下面程序的输出结果是多少?
if (true) {
let b = 20;
console.log(b)
if (true) {
let c = 30;
}
console.log(c);
}
输出的结果是:b的值是20,在输出c的时候,出现了错误。
导致的原因,两个if就是两个块级作用域,c这个变量在第二个if中,也就是第二个块级作用域中,所以在外部块级作用域中无法获取到变量c.
块级作用域的出现,带来了一个好处以前获得广泛使用的立即执行匿名函数不再需要了。
下面首先定义了一个立即执行匿名函数:
;(function text() {
var temp = 'hello world';
console.log('temp=', temp);
})()
匿名函数的好处:通过定义一个匿名函数,创建了一个新的函数作用域,相当于创建了一个“私有”的空间,该空间内的变量和方法,不会破坏污染全局的空间 。
但是以上的写法是比较麻烦的,有了“块级作用域”后就编的比较简单了,代码如下:
{
let temp = 'hello world';
console.log('temp=', temp);
}
通过以上的写法,也是创建了一个“私有”的空间,也就是创建了一个封闭的作用域。同样在该封闭的作用域中的变量和方法,不会破坏污染全局的空间。
但是以上写法比立即执行匿名函数简单很多。
现在问你一个问题,以下代码是否可以:
let temp = '你好';
{
let temp = 'hello world';
}
答案是可以的,因为这里有两个“块级作用域”,一个是外层,一个是内层,互不影响。
但是,现在修改成如下的写法:
let temp = '你好';
{
console.log('temp=', temp);
let temp = 'hello world';
}
出错了,也是变量未定义的错误,造成错误的原因还是前面所讲解的let 不存在“变量提升”。
块级作用域还带来了另外一个好处,我们通过以下的案例来体会一下:
该案例希望不同时间打印变量i的值。
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log('i=', i);
}, 1000)
}
那么上面程序的执行结果是多少?
对了,输出的都是 i=3
造成的原因就是i为全局的。
那么可以怎样解决呢?相信这一点对你来说很简单,在前面ES5课程中也讲过。
for (var i = 0; i < 3; i++) {
(function(i) {
setTimeout(function() {
console.log('i=', i);
}, 1000)
})(i)
}
通过以上的代码其实就是通过自定义一个函数,生成了函数的作用域,i变量就不是全局的了。
这种使用方式很麻烦,有了let命令后,就变的非常的简单了。
代码如下:
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log('i=', i);
}, 1000)
}
1.4 let命令注意事项
1.4.1 不存在变量提升
let不像var那样会发生“变量提升”现象。所以,变量一定要在声明后使用,否则会出错。
关于这一点,前面的课程也多次强调。
console.log(num);
let num = 2;
1.4.2 暂时性死区
什么是暂时性死区呢?
先来看一个案例:
var num = 123;
if (true) {
num = 666;
let num;
}
上面的代码中存在全局的变量num,但是在块级作用域内使用了let又声明了一个局部的变量num,导致后面的num绑定到这个块级作用域,所以在let声明变量前,对num进行赋值操作会出错。
所以说,只要在块级作用域中存在let命令,它所声明的变量就被“绑定”在这个区域中,不会再受外部的影响。
关于这一点,ES6明确规定,如果在区域中存在let命令,那么在这个区域中通过let命令所声明的变量从一开始就生成了一个封闭的作用域,只要在声明变量前使用,就会出错。
所以说,所谓的“暂时性死区”指的就是,在代码块内,使用let命令声明变量之前,该变量都是不可用的。
1.4.3 不允许重复声明
let 不允许在相同的作用域内重复声明一个变量,
如果使用var声明变量是没有这个限制的。
如下面代码所示:
function test() {
var num = 12;
var num = 20;
console.log(num)
}
test()
以上代码没有问题,但是如果将var换成let,就会出错。如下代码所示:
function test() {
let num = 12;
let num = 20;
console.log(num)
}
test()
当然,以下的写法也是错误的。
function test() {
var num = 12;
let num = 20;
console.log(num)
}
test()
同时,还需要注意,不能在函数内部声明的变量与参数同名,如下所示:
function test(num) {
let num = 20;
console.log(num)
}
test(30)
2、const命令
2.1 基本用法
const用来声明常量,常量指的就是一旦声明,其值是不能被修改的。
这一点与变量是不一样的,而变量指的是在程序运行中,是可以改变的量。
let num = 12;
num = 30;
console.log(num)
以上的代码输出结果为:30
但是通过const命令声明的常量,其值是不允许被修改的。
const PI = 3.14;
PI = 3.15;
console.log(PI)
以上代码会出错。
在以后的编程中,如果确定某个值后期不需要更改,就可以定义成常量,例如:PI,它的取值就是3.14,后面不会改变。所以可以将其定义为常量。
2.2 const命令注意事项
2.2.1 不存在常量提升
以下代码是错误的
console.log(PI);
const PI = 3.14
2.2.2 只在声明的块级作用域内有效
const命令的作用域与let命令相同:只在声明的块级作用域内有效
如下代码所示:
if (true) {
const PI = 3.14;
}
console.log(PI);
以上代码会出错
2.2.3 暂时性死区
const命令与let指令一样,都有暂时性死区的问题,如下代码所示:
if (true) {
console.log(PI);
const PI = 3.14;
}
以上代码会出错
2.2.4 不允许重复声明
let PI = 3.14;
const PI = 3.14;
console.log(PI);
以上代码会出错
2.2.5 常量声明必须赋值
使用const声明常量,必须立即进行初始化赋值,不能后面进行赋值。
如下代码所示:
const PI;
PI = 3.14;
console.log(PI);
以上代码会出错
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);
在这里定义了一个数组arr,并且进行了初始化,下面紧跟着通过下标的方式获取数组中的值,然后赋值给对应的变量。
虽然这种方式可以实现,但是相对来说比较麻烦,ES6中提供了解构赋值的方式,代码如下:
let arr = [1, 2, 3];
let [num1, num2, num3] = arr;
console.log(num1, num2, num3);
将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);
定义了一个arr数组,并且进行了初始化,arr数组中有对象,数组和数值。
现在通过解构赋值的方式,将数组中的值取出来赋给对应的变量,所以等号左侧的结构和数组arr的结构是一样的。
但是,如果不想获取具体的值,而是获取arr数组存储的json对象,数组,那么应该怎样写呢?
let arr = [{
userName: 'zs',
age: 18
},
[1, 3], 6
];
let [jsonResult, array, num] = arr;
console.log(jsonResult, array, num);
3.2、注意事项
3.2.1 如果解析不成功,对应的值会为undefined.
let [num1, num2] = [6]
console.log(num1, num2);
以上的代码中,num1的值为6,num2的值为undefined.
3.2.2 不完全解构的情况
所谓的不完全解构,表示等号左边只匹配右边数组的一部分。
代码如下:
let [num1, num2] = [1, 2, 3];
console.log(num1, num2);
以上代码的执行结果:num1=1,num2 = 2
也就是只取了数组中的前两个值。
// 如果只取第一个值呢?
let [num1] = [1, 2, 3];
console.log(num1);
//只取第二个值呢?
let [, num, ] = [1, 2, 3];
console.log(num);
// 只取第三个值呢?
let [, , num] = [1, 2, 3];
console.log(num);
3.4、对象解构赋值基本使用
解构不仅可以用于数组,还可以用于对象。
let {
userName,
userAge
} = {
userName: 'ls',
userAge: 20
};
console.log(userName, userAge);
在对 对象进行解构赋值的时候,一定要注意:变量名必须与属性的名称一致,才能够取到正确的值。
如下所示:
let {
name,
age
} = {
userName: 'ls',
userAge: 20
};
console.log(name, age);
输出的结果都是undefined.
那么应该怎样解决上面的问题呢?
let {
userName: name,
userAge: age
} = {
userName: 'ls',
userAge: 20
}
console.log(name, age);
通过以上的代码解决了对应的问题,那么这种方式的原理是什么呢?
先找到同名属性,然后再赋值给对应的变量。
把上面的代码,改造成如下的形式,更容易理解:
let obj = {
userName: 'ls',
userAge: 21
};
let {
userName: name,
userAge: age
} = obj;
console.log(name, age)
如果按照ES5的方式:
let name = obj.userName
let age = obj.userAge
3.5、对象解构赋值注意事项
3.5.1 默认解构
所谓的默认解构,指的是取出来值就用取出来的值,如果取不出来就用默认的值。
演示默认解构之前,先来看如下的代码:
let obj = {
name: 'zs'
};
let {
name,
age
} = obj;
console.log(name, age);
你想一下输出结果是什么呢?
输出的结果是:zs undefined
也就是name变量的值为:‘zs’, age变量的值为:‘undefined’.
由于没有给age变量赋值所以该变量的值为’undefined’.
现在修改一下上面的程序
let obj = {
name: 'zs'
};
let {
name,
age = 20
} = obj;
console.log(name, age);
现在给age这个变量赋了一个默认值为20,所以输出的结果为:zs 20
这也就是刚才所说到的默认解构,也就是取出来值就用取出来的值,如果取不出来就用默认的值。
现在再问你一个问题:如果在对应中有age属性,那么对应的等号左侧的age这个变量的值是多少呢?
如下代码所示:
let obj = {
name: 'zs',
age: 26
};
let {
name,
age = 20
} = obj;
console.log(name, age);
输出的结果为: zs 26
这就是,取出来值就用取出来的,取不出来就用默认值。
3.5.2 嵌套结构对象的解构
解构也可以用于对嵌套结构的对象,如下代码所示:
let obj = {
arr: [
"Hello", {
msg: 'World'
}
]
}
let {
arr: [str, {
msg
}]
} = obj;
console.log(str, msg);
在上面的代码中要注意的是:arr只是一种标志或者是一种模式,不是变量,因此不会被赋值。
再看一个案例:
let obj = {
local: {
start: {
x: 20,
y: 30
}
}
};
let {
local: {
start: {
x,
y
}
}
} = obj;
console.log(x, y);
在该案例中创建了一个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] = 'itcast';
console.log(a, b, c, d, e, f);
类似于数组的对象都有length属性,因此也可以对这个属性进行解构赋值。
let {
length: len
} = 'itcast';
console.log('len=', len);
3.7、函数参数的解构赋值
函数的参数也能够进行解构的赋值,如下代码所示:
function test([x, y]) {
return x + y;
}
console.log(test([3, 6]));
上面的代码中,函数test的参数不是一个数组,而是通过解构得到的变量x和y.
函数的参数的解构也可以使用默认的值。
function test({
x = 0,
y = 0
} = {}) {
return [x, y];
}
console.log(test({
x: 3,
y: 6
}));
当然可以进行如下的调用
test({x:3})
test({})
3.8、解构赋值的好处
3.8.1 交换变量的值
let num1 = 3;
let num2 = 6;
[num1, num2] = [num2, num1];
console.log(num1, num2);
3.8.2 函数可以返回多个值
function test() {
return [1, 2, 3];
}
let [a, b, c] = test();
console.log(a, b, c);
在上面的代码中,返回了三个值,当然在实际的开发过程中,你可以根据自己的实际情况确定返回的数据的个数。
如果,我只想接收返回中的一部分值呢?
// 接收第一个值
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);
3.8.3 函数返回一个对象
可以将函数返回的多个值封装到一个对象中。
function test() {
return {
num1: 3,
num2: 6
}
}
let {
num1,
num2
} = test();
console.log(num1, num2);
3.8.4 提取JSON对象中的数据
解构赋值对提取JSON对象中的数据也非常有用。
let userData = {
id: 12,
userName: 'zhangsan',
userAge: 20
}
let {
id,
userName,
userAge
} = userData;
console.log(id, userName, userAge);
以上的代码可以快速提取JSON中的数据。
4、扩展运算符与rest
运算符
4.1 扩展运算符
扩展运算符的表现形式是三个点(…),可以将一个数组转换为用逗号分隔的序列。
下面通过一个案例看一下基本的应用,案例的要求是将两个数组合并为一个数组。
先采用传统的做法:
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let arr3 = [].concat(arr1, arr2);
console.log(arr3);
下面使用扩展运算符来完成上面的案例
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let arr3 = [...arr1, ...arr2];
console.log(arr3);
通过以上的代码,发现也实现了我们最终想要的结果,...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);
发现和我们最开始实现的是一样的。
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);
这种方式非常麻烦,所以可以使用Math对象中的max方法来完成.
先来看一下Math.max的基本用法
console.log(Math.max(1, 5, 12, 67));
如果是用Math.max来计算数组中的最大值。(ES5的写法)
let arr = [12, 23, 11, 56];
console.log(Math.max.apply(null, arr));
虽然可以使用Math.max.apply
来实现,但是感觉还是很麻烦,
这里就可以使用扩展运算符
let arr = [12, 23, 11, 56];
console.log(Math.max(...arr));
在上面的代码中(不管ES5还是ES6),由于JavaScript不提供求数组中最大值的函数,所以只能将数组转换成一个参数的列表,然后再进行相应的求值。
4.2.2 用于函数调用
在函数调用的时候,需要进行参数的传递,在某些情况下,通过扩展运算符,更有利于参数的传递。
function test(num1, num2) {
return num1 + num2;
}
let array = [23, 56];
console.log(test(...array));
通过扩展运算符,将array这个数组中的值取出来,然后23赋值给了num1,56赋值给了num2.
下面,再看一个使用扩展运算符处理函数参数的案例。
把一组数据添加到数组中。
function test(array, ...items) {
array.push(...items);
console.log(array)
}
let array = [23, 56];
test(array, 90, 78, 98);
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));
在上面定义的函数中,传递了三个参数,第一个参数:是一个‘+’号,后面两个参数,表示进行加法运算的数据。
但是,问题是如果参与运算的数据比较多,那么定义的参数也就比较多,这样比较麻烦。这时可以使用rest参数形式。
如下所示:
function add(...values) {
console.log(values);
}
add(2, 3);
通过以上的代码输出发现,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));
上面的循环方式,使用的是传统的模式,也可以使用 forEach
的形式来进行循环,如下所示:
function add(...values) {
let sum = 0;
values.forEach(function(item) {
sum += item
})
return sum;
}
console.log(add(2, 3));
下面我们再来看一个rest
运算符的基本使用
解构会将相同数据结构对应的值赋给对应的变量,但是当我们想将其中的一部分值统一赋值给一个变量的时候,可以使用rest
运算符。
如下代码:
<script>
let arr = [1, 2, 3, 4, 5, 6];
let [arr1, ...arr2] = arr; //进行解构处理
console.log(arr1); // 1
console.log(arr2); // [2,3,4,5,6]
</script>
在上面的代码中,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));
下面使用 rest的方式
function sortFunc(...values) {
return values.sort()
}
console.log(sortFunc(23, 12, 67));
因为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);
以上代码会出错,要求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));
通过上面的代码,可以发现传统方式来定义函数的时候,比较麻烦。
箭头函数的使用
let f = (x, y) => {
return x + y
};
console.log(f(9, 8));
在调用f这个函数的时候,将9和8传递给了x,y这两个参数,然后进行加法运算。
如果参数只有一个,可以省略小括号。
let f = num => {
return num / 2;
}
console.log(f(6));
如果没有参数,只需要写一对小括号就可以。
let f = () => {
return 9 / 3;
}
console.log(f());
上面我们写的代码中,发现函数体中只有一条语句,那么这时是可以省略大括号的。
let f = (x, y) => x + y;
console.log(f(3, 6));
把上面的代码转换成ES5的写法,发现和我们前面写的代码是一样的。
var f = function f(x, y) {
return x + y;
};
console.log(f(3, 6));
5.2 箭头函数注意事项
5.2.1 直接返回对象
如果希望箭头函数直接返回一个对象,应该怎样写呢?
你可能认为很简单,可以采用如下的写法
let f = (id, name) => {
id: id,
userName: name
};
console.log(f(1, 'zs'));
但是上面的写法是错误的,因为这时大括号被解释为代码块,解决的办法是:在对象外面加上小括号,
所以,正确的写法如下:
let f = (id, name) => ({
id: id,
userName: name
});
console.log(f(1, 'zs'));
通过打印,发现输出的是一个对象。
当然也可以采用如下的写法
let f = (id, name) => {
return {
id: id,
userName: name
}
};
console.log(f(1, 'zs'));
5.2.2 箭头函数中this的问题
下面定义一个对象,来理解this的应用。
看一下,如下代码:
let person = {
userName: 'ls',
getUserName() {
console.log(this.userName)
}
}
person.getUserName();
以上代码执行的结果为:‘ls’,并且在该程序中this
为当前的person对象。
现在,将上面的代码修改一下,要求延迟1秒钟以后,再输出用户名的名称。
let person = {
userName: 'ls',
getUserName() {
setTimeout(function() {
console.log(this.userName)
}, 1000)
}
}
person.getUserName();
上面的输出结果为:undefined
,因为在setTimeout中this指的是window,而不是person对象。
为了解决上面的问题,可以将代码进行如下的修改:
let person = {
userName: 'ls',
getUserName() {
let that = this;
setTimeout(function() {
console.log(that.userName)
}, 1000)
}
}
person.getUserName();
在进入setTimeout这个方法之前,提前将this赋值给that变量,然后在setTimeout中使用that,那么这时that指的就是person对象。
上面的解决方法比较麻烦,可以修改成箭头函数的形式,代码如下所示:
let person = {
userName: 'wangwu',
getUserName() {
setTimeout(() => {
console.log(this.userName);
},1000)
}
}
person.getUserName();
通过上面的代码,可以发现在箭头函数中直接使用this是没有问题的。
你可以这样理解:在箭头函数中是没有this的,如果在箭头函数中使用了this,那么实际上使用的是外层代码块的this. 箭头函数不会创建自己的**this,它只会从自己的作用域链的上一层继承this**
或者通俗的理解:找出定义箭头函数的上下文(即包含箭头函数最近的函数或者是对象),那么上下文所处的父上下文即为this.
那么在我们这个案例中,setTimeout
函数中使用了箭头函数,箭头函数中用了this,
而这时this
指的是外层代码块也就是person
,所以箭头函数中使用的this指的就是person
(包含箭头函数最近的函数是setTimeout
,那么包含setTimeout
这个函数的最近的函数或者是对象是谁呢?对了,是getUserName
这个函数,而getUserName
这个函数是属于哪个对象呢?是person
,所以this
为person
)
下面,再看一个案例:(可以将下面的代码转换成ES5的代码)
let person = {
userName: 'zhangsan',
getUserName: () => {
console.log(this.userName)
}
}
person.getUserName();
输出结果为:undefined
因为这时包含getUserName
这个箭头函数最近的对象是person(这里也就是说getUserName
这个箭头函数的上下文为person
),那么person
对象所处的父上下文(也就是包含person这个对象最近的对象),是谁呢?对了,就是window
。
下面再看一个案例:
let person = {
userName: 'zhangsan',
getUserName() {
return () => {
console.log(this.userName);
}
}
}
person.getUserName()();
根据上面总结的规律是,这段代码输出的结果是:‘zhangsan’.
在这里还需要注意一个问题就是:
由于箭头函数没有自己的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))
上面代码执行的结果为:2
也就是说,箭头函数不能使用call( )
来改变this的指向,本意是想让this指向b这个对象,但是实际上this还是adder这个对象。
5.3.3 箭头函数不适合的场景
第一:不能作为构造函数,不能使用new
操作符
构造函数是通过new
操作符生成对象实例的,生成实例的过程也是通过构造函数给实例绑定this
的过程,而箭头函数没有自己的this
,因此不能使用箭头函数作为构造函数。
如下代码:
function Person(name) {
this.name = name;
}
var p = new Person("zhangsan"); //正常
以上是我们前面经常使用的一种方式,没有问题
下面看一下使用箭头函数作为构造函数的情况
let Person = (name) => {
this.userName = name;
};
let p = new Person("lisi");
当执行上面的程序的时候,会出现错误
第二:没有prototype
属性
因为在箭头函数中没有this
,也就不存在自己的作用域,因此箭头函数是没有prototype
属性的。
let Person = (name) => {
this.userName = name;
};
console.log(Person.prototype); // undefined
第三:不适合将原型函数定义成箭头函数
在给构造函数添加原型函数时,如果使用箭头函数,其中的this
会指向全局作用域window
,而不会指向构造函数。
因此并不会访问到构造函数本身,也就无法访问到实例属性,失去了原型函数的意义。
function Person(name) {
this.userName = name;
}
Person.prototype.sayHello = () => {
console.log(this); // window
console.log(this.userName); // undefined
};
let p = new Person("zhangsan");
p.sayHello();
6、对象的扩展
1、属性与方法的简洁表示方式
以前创建对象的方式:
let userName = 'zhangsan';
let userAge = 18;
let person = {
userName: userName,
userAge: userAge
}
console.log(person);
通过上面的代码,可以发现对象中的属性名和变量名是一样的,像这种情况,在ES6中是可以简化如下形式:
let userName = 'zhangsan';
let userAge = 18;
let person = {
userName,
userAge
}
console.log(person);
通过以上代码可以发现:在ES6中,如果对象的属性名和变量名是一样的,那么两者可以合二为一。
当然,除了属性可以简写,方法也可以简写,以前定义方法的形式如下:
let userName = 'zhangsan';
let userAge = 18;
let person = {
userName,
userAge,
sayHello: function() {
console.log('你好')
}
}
person.sayHello();
在ES6中可以简化成如下的形式:
let userName = 'zhangsan';
let userAge = 18;
let person = {
userName,
userAge,
sayHello() {
console.log('Hello');
}
}
person.sayHello();
所以在以后的编程中,会经常看到或者是用到这种ES6
的表示形式。
2、Object.assign( )方法
2.1 基本使用
现在,有一个需求,将一个对象的属性拷贝给另外一个对象,应该怎样处理?
你可能会说,很简单,可以通过循环的方式来来实现。
如下代码所示:
let obj1 = {
name: 'zhangsan'
};
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);
虽然通过循环的方式,可以实现对象属性的拷贝,但是很麻烦。下面讲解一个简单的方法:Object.assign( )
方法.
Object.assign( )
方法用来源对象的所有可枚举的属性复制到目标对象。该方法至少需要两个对象作为参数,第一个参数是目标对象,后面的参数都是源对象。只要有一个参数不是对象,就会抛出异常。
示例代码如下:
let target = {
a: 1,
b: 2
};
let source = {
c: 3,
d: 4
};
Object.assign(target, source);
console.log(target);
最终的结果:将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);
通过上面的代码,将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);
上面的代码中对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);
2.3 注意事项
1、如果目标对象与源对象有同名属性,那么后面的属性会覆盖前面的属性。
let target = {
a: 1,
b: 2
};
let source = {
b: 3,
d: 4
};
Object.assign(target, source);
console.log(target);
上面的代码,将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);
在上面的代码中,通过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"]);
通过以上的代码,可以发现在对象中定义的属性的名称本身就是字符串,而且发现“num 1”中间是有空格的,所以在访问该属性的时候,通过[ ]的形式来进行访问。
那么为什么说容易造成属性名的冲突呢?
举例说明:你用了一个别人提供的对象,但是又想为这个对象添加新的方法或者是属性,新方法或者是新属性的名称有可能与现有对象中的属性名称或者是方法的名称产生冲突。如果有一种机制,能够保证每个属性的名字都是唯一的,那么就能够从根本上防止属性名称的冲突问题。这也就是ES6引入Symbol的原因。
Symbol是一种数据类型,是JavaScript语言的第7种数据类型,前6种分别是:undefined,null,布尔值,字符串,数值和对象。
Symbol类型的值是通过Symbol函数生成的。它的值是独一无二的,也就是唯一的,可以保证对象中属性名称的唯一。
可以通过如下的代码测试类型
let s = Symbol();
console.log(typeof s);
对应的输出类型为"symbol"
下面创建Symbol类型的变量,然后进行打印输出。
let s = Symbol();
let s1 = Symbol();
console.log(s);
console.log(s1);
发现输出的结果都是:Symbol( )。
输出的结果都是Symbol( ),那么无法区分,哪个Symbol( )是s变量的,哪个是s1变量的。
为了解决这个问题,Symbol( )函数可以接受一个字符串作为参数,这个参数表示对Symbol的描述,主要是为了在控制台进行输出打印的时候,能够区分开,Symbol最终是属于哪个变量的。
let s = Symbol('s');
let s1 = Symbol('s1');
console.log(s);
console.log(s1);
输出的结果为:Symbol(s)和Symbol(s1)
注意:Symbol函数的参数只表示对当前Symbol值(结果)的描述,因此相同参数的Symbol函数的返回值是不相等的。代码如下:
let s = Symbol('s');
let s1 = Symbol('s');
console.log(s === s1);
以上结果为:false.
7.2、Symbol应用场景
7.2.1 作为属性名的Symbol
在前面的课程中,讲解过由于Symbol的值是唯一的,并且能够保证对象中不会出现同名的属性。下面,先来讲解一下,怎样使用Symbol作为属性名,然后再看一下怎样保证对象中不会出现同名属性。
第一种添加属性的方式:
let mySymbol = Symbol();
let obj = {}
// 第一种添加属性的方式
obj[mySymbol] = 'hello';
console.log(obj[mySymbol]);
第二种添加属性的方式:
let mySymbol = Symbol();
let obj = {
[mySymbol]: 'world' // 注意mySymbol必须加上方括号,否则为字符串而不是Symbol类型。
}
console.log(obj[mySymbol]);
第三种添加属性的方式
let mySymbol = Symbol();
let obj = {};
Object.defineProperty(obj, mySymbol, {
value: '你好'
})
console.log(obj[mySymbol]);
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);
在上面的代码中,有两个函数分别是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);
通过上面的代码可以发现,通过Symbol解决了属性名称冲突的问题。
8、Proxy
1.Proxy简介
Proxy可以理解成在对象前添加了一个“拦截”层,外界在对该对象进行访问时,必须先通过这个拦截层。因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy这个词的意思是代理的意思,也就是说由Proxy来“代理”某些操作。所以又称之为“代理器”。
Proxy的使用
let proxy=new Proxy(target,handler)
target:表示所要拦截的目标对象(原来要访问的对象)
handler:也是一个对象,表示拦截的行为和规则。
要想使用Proxy来完成对象的拦截,除了创建对象以外,还需要指定对应的拦截的方法。
下面使用一下get()
这个拦截方法,来体会一下Proxy拦截器的使用。
get( )
方法用于拦截某个属性的读取操作。
下面先看如下的代码
let student = {
userName: '张三'
}
console.log(student.userName);
console.log(student.userAge);
这段代码非常的简单,定义了一个student对象,在该对象中添加了一个userName,下面可以直接通过对象名加上点的方式来获取对应的属性的值。但是,问题是,在student这个对象中,只是定义了userName这个属性,并没有定义userAge,但是当通过student.userAge这个方式来获取的时候,发现得到的结果是undefined.
那么,在这里我们希望如果访问对象中不存在的属性的时候,应该给出相应的错误提示,要想实现这个需求,就要用到Proxy中的get()
方法,也就是在访问某个对象的属性之前,先拦截一下,看一下所访问的对象是否有对应的属性,如果有,继续访问,如果没有给用户一个错误的提示。
let student = {
userName: '张三'
}
let proxy = new Proxy(student, {
get: function(target, property) {
if (property in target) {
return target[property];
} else {
// (引用错误) 对象代表当一个不存在的变量被引用时发生的错误。
throw new ReferenceError('访问的属性' + property + "不存在")
}
}
})
console.log(proxy.userName);
console.log(proxy.userAge);
get()
有两个参数,`第一个参数表示的是目标对象,第二个参数表示的是要访问的属性。
通过上面的代码,可以体会出所谓的 "拦截"的含义了,也就是在访问某个属性的值之前,先判断一下该对象是否有对应的属性,如果有就返回属性的值,没有就给出相应的错误提示。
注意:要使Proxy起作用,必须针对Proxy对象进行操作,不是针对目标对象进行操作(上面的是student对象)。
1.2 set( )方法
set()
方法用于拦截某个属性的赋值操作。
假如,Student对象有一个age属性,表示学生的年龄,在这里我们要求对年龄进行限制,如果大于60岁,给出错误提示,这样在这里可以使用Proxy对象保证age属性的取值是符合要求的。
let student = {
name: 'zs',
age: 20
};
let proxy = new Proxy(student, {
set: function(obj, prop, value) {
console.log('obj=', obj);
console.log('prop=', prop);
console.log('value=', value);
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('年龄不是整数!')
}
if (value > 60) {
throw new RangeError('年龄太大了')
}
}
}
})
proxy.age = '80';
console.log(proxy.age);
set()
方法
第一个参数:表示拦截的对象。
第二个参数:表示操作的属性名称。
第三个参数:表示属性的值。
1.3 apply( )方法
apply()
方法拦截函数的调用,函数调用包括直接调用,call函数调用和apply函数调用,三种调用操作方式。
下面先看一下apply()
函数的语法
let handler={
apply(target,ctx,args){
}
}
apply( )
函数可以有三个参数,
第一个参数:表示目标对象,也就是要拦截的函数
第二个参数:表示目标对象的上下文对象(this)
第三个参数:表示目标对象的参数数组。
let target = function(msg) {
return '你好' + msg;
}
let handler = {
apply: function(target, ctx, args) {
console.log('target=', target);
console.log('ctx=', ctx === obj);
console.log('args=', args);
return 'hello'
}
}
let proxy = new Proxy(target, handler);
let obj = {
proxy,
};
console.log(obj.proxy('张三'))
在执行target这个方法之前,会被Proxy所拦截。
1.4 has( ) 方法
has()
可以隐藏某些属性,不被in操作符发现。
let user = {
_name: 'zhangsan',
age: 20
}
let handler = {
has(target, key) {
console.log('target=', target);
console.log('key=', key);
}
}
let proxy = new Proxy(user, handler);
'_name' in proxy; // 自动调用 has 方法
通过上面的代码,可以发现:has方法有两个参数,第一个参数:表示目的对象,第二个参数:表示操作的属性。
下面的例子,把user
对象中 _name
属性隐藏起来,也就是无法通过in
运算符发现该属性。
let user = {
_name: 'zhangsan',
age: 20
}
let handler = {
has(target, key) {
// console.log('target=', target);
// console.log('key=', key);
if (key[0] === '_') {
return false
}
return key in target;
}
}
let proxy = new Proxy(user, handler);
console.log('_name' in proxy);
上面的代码中,如果对象中的属性名第一个字符是下画线,那么has方法会返回false,从而不会被 in
运算符发现,但是如果将 console.log('_name' in proxy);
换成 console.log('age' in proxy);
返回为true.
2、应用场景
2.1 数据校验
在前面的课程中,已经讲解过基本的数据校验,下面看一个比较完整的数据校验的案例。
// 创建一个对象
class Person {
constructor() {
this.name = '';
this.age = 19;
return validator(this, personValidators);
}
}
// 定义校验规则
const personValidators = {
name(val) {
return typeof val === 'string';
},
age(val) {
return typeof val === 'number' && val > 18
}
}
// 完成校验
function validator(target, validator) {
return new Proxy(target, {
_validator: validator,
set(target, key, value) {
if (target.hasOwnProperty(key)) {
let v = this._validator[key]; //根据key获取具体的校验规则
if (v(value)) {
return Reflect.set(target, key, value);
} else {
throw Error(`不能给${key}属性设置${value}`);
}
} else {
throw Error(`${key} 不存在`)
}
}
})
}
// 注意这里返回的是Proxy对象
let person = new Person();
person.name = 'zhangsan';
person.age = 19;
// person.name = 90;
// person.age = 15;
console.log(person);
通过上面的案例,将对象的创建,数据验证的规则,以及具体的验证方式都进行了分离,整体结构更加的清晰,代码更加的容易维护。
2.2 简单模拟双向数据绑定
大家都知道在V
ue`中,是有双向绑定功能的,下面通过Proxy来模拟一下。
let input = document.getElementById('txtInput');
let p = document.getElementById('txtP');
let obj = {
text: ''
};
let newObj = new Proxy(obj, {
set: function(target, key, value) {
if (target.hasOwnProperty(key)) {
input.value = value;
p.innerHTML = value;
}
return Reflect.set(target, key, value);
},
});
input.addEventListener('keyup', function(e) {
newObj.text = e.target.value;
});
在文本框中输入值以后,对应的会在p标签中进行展示,通过在浏览器的控制台中,如果给 newObj中的text属性赋值,对应的文本框和P标签内容也会发生变化。
2.3 实现真正的私有属性
<script>
const userInfo = {
_id: 123,
getAllUsers: function () {
console.log("获取所有用户的信息");
},
getUserById: function (userId) {
console.log("根据用户的编号,查询指定的信息" + userId);
},
saveUser: function (user) {
console.log("保存用户信息");
},
};
const proxy = new Proxy(userInfo, {
get: function (target, prop) {
if (prop[0] === "_") {
return undefined;
}
return target[prop];
},
set: function (target, prop, value) {
if (prop[0] !== "_") {
target[prop] = value;
}
},
has: function (target, prop) {
if (prop[0] === "_") {
return false;
}
return prop in target;
},
});
console.log(proxy._id); //undefined
proxy.getAllUsers(); // 获取所有用户的信息
proxy.getUserById(123); // 根据用户的编号,查询指定的信息123
console.log("_id" in proxy); //false
console.log("saveUser" in proxy); // true
</script>
9、Set结构
Set结构与数组类似,但是成员的值都是唯一的,没有重复值。
1.1 常用的操作方法
关于常用的操作方法,这里会讲解如下4个。
add(value)
: 添加某个值,返回Set结构本身。
delete(value)
: 删除某个值,返回一个布尔值,表示删除是否成功
has(value)
: 返回一个布尔值,表示参数是否为Set的成员.
clear()
: 清除所有成员,没有返回值
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);
在上面的代码中,用到了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);
在上面的代码中,又添加了一个数字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.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.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);
Set结构是一个类似数组的结构,那么怎样转换成一个真正的数据呢?
可以通过前面学习的 Arrray.from
方法。
let s = new Set();
s.add(1);
s.add(2);
s.add(3);
let array = Array.from(s);
console.log(array);
在使用数组编程的时候,经常会用到一个功能,就是清除数组中的重复的数据,那么在这里可以借助于Set结构来完成。
// 清除数组中的重复数据.
// Set函数可以接受一个数组或者是类似数组的对象,作为参数。
let array = [1, 2, 3, 3, 5, 6];
let s = new Set(array);
console.log(Array.from(s));