本文我们介绍ES6的展开运算符。展开运算符(spread operator)允许一个表达式在某处展开。展开运算符在多个参数(用于函数调用)或多个元素(用于数组字面量)或者多个变量(用于解构赋值)的地方可以使用。
下面是简单的屏幕分享,不爱看文字的同道中人可以看视频。-_-
一、函数调用
在函数调用中可以使用展开运算符。 我们还是先看看在ES5中如何将一个数组展开成函数的多个参数,可以通过apply()方法实现,代码如下:
function hello(a, b, c) {
? ? console.info(a);
? ? console.info(b);
? ? console.info(c);
}
var args = ['光脚丫思考1', '光脚丫思考2', '光脚丫思考3'];
hello.apply(null, args);?
上述代码使用apply()方法来将args数组展开成多个函数参数,其中函数参数和数组的元素次序是一一对应的。
在ES6中实现上面的代码效果,则更加的简洁。代码如下:
function hello(a, b, c) {
? ? console.info(a);
? ? console.info(b);
? ? console.info(c);
}
var args = ['光脚丫思考1', '光脚丫思考2', '光脚丫思考3'];
hello(...args);
可以看到只需要在数组变量的前面增加展开运算符(...)就可以了,其输出结果和前面的结果完全相同。 前面这两段代码,数组的元素个数和函数的参数个数刚好一致,如果数组的元素比函数的参数多了?或者少于函数的参数个数呢?是否仍然可以使用展开运算符呢?下面我们尝试下。
1、多余
下面的示例代码数组的元素个数多余函数的参数个数。
function hello(a, b, c) {
? ? console.info(a);
? ? console.info(b);
? ? console.info(c);
}
var args = ['光脚丫思考1', '光脚丫思考2', '光脚丫思考3', '光脚丫思考4', '光脚丫思考5'];
hello(...args);
其中,函数的参数仍然是3个,但是数组的元素却有5个。这段代码在控制台输出的结果如下图所示:
可以看到,本着数组元素和函数参数按照顺序一一对应传递的原则,在上面的示例代码中,数组中的前3个元素按着次序分别传递给了函数的3个参数,而后续的2个数组元素则被忽略了。
2、少于
下面的示例代码数组的元素个数少余函数的参数个数。
function hello(a, b, c) {
? ? console.info(a);
? ? console.info(b);
? ? console.info(c);
}
var args = ['光脚丫思考1', '光脚丫思考2'];
hello(...args);
这次,函数的参数仍然是3个,但是数组的元素却只剩下了2个。这段代码在控制台输出的结果如下图所示:
可以看到,由于数组缺少第3个元素,因此传递个函数的第3个参数就是undefined了。
3、数学计算Math
到这里,我们已经初步看到了展开运算符在函数调用中的妙用,这一技术应用到Math对象中的一系列方法中,可以说是相当的合适,甚至于完美。比如:
let numbers = [3, 8, 2, 0, 5, 9, 6];
let result = Math.min(...numbers);
console.info(result);
再比如:
let numbers = [3, 8, 2, 0, 5, 9, 6];
let result = Math.max(...numbers);
console.info(result);
4、多个数组展开
截止目前为止,前面的示例中我们只演示了,在函数调用的时候,将1个数组展开传参给函数。如果是多个数组呢?是否也可以呢?如下代码所示:
function hello(a, b, c) {
? ? console.info(a);
? ? console.info(b);
? ? console.info(c);
}
var args1 = ['光脚丫思考1', '光脚丫思考2'];
var args2 = ['光脚丫思考3', '光脚丫思考4'];
hello(...args1, ...args2);?
运行上面的代码,在控制台输出了如下的结果:
可以看到,将多个数组使用展开运算符传递给函数的参数也是没有问题的。 代码的复杂性在提升一下,如果传递的参数中包含有普通参数呢?如下代码所示:
function hello(a, b, c) {
? ? console.info(a);
? ? console.info(b);
? ? console.info(c);
}
var args1 = ['光脚丫思考1', '光脚丫思考2'];
var args2 = ['光脚丫思考3', '光脚丫思考4'];
hello("光脚丫思考", ...args1, ...args2);
控制台的输出结果如下图:
输出结果和预期的一样,这样看来,函数调用的时候可以将普通的实参与展开运算符传参结合使用,传参的位置不做限制,大家可以自行尝试下。
5、动态参数
在我的另外一篇文章《04-ES6语法:默认参数和rest参数》中提到了rest参数。我们来回顾下其中的一个示例:
// ES6中的rest参数。
function createBox(width, height, color, ...args) {
? ? var message = `盒子的宽度=${width},高度=${height},颜色=${color}`;
? ? console.info(message);
? ? console.info(args);
}
createBox(500, 200, "red", "这里是盒子的描述信息1", "这里是盒子的描述信息2");
请留意函数的最后一个参数,使用了类似于展开运算符(...)的语法,在控制台的输出结果如下所示:
从输出结果来看,这里更像是把剩余的参数聚拢到args这个参数中形成一个数组,而前面我们演示的几个示例则是把数组中的元素“展开”为各个对应的参数,两者刚好是反着来的。 于是我们可以得出如下的结论:
- 在定义函数的时候使用运算符(...),会将传过来的多余参数聚拢到一起。
- 在调用函数的时候使用运算符(...),会把原本聚拢在一起的数据展开传递给各个参数。
6、默认参数
展开运算符可以用于设置函数参数的默认值,如下面的代码示例:
function introduce(data) {
? ? const defaultParams = {
? ? ? ? name: "光脚丫思考",
? ? ? ? blog: "https://blog.csdn.net/gjysk"
? ? };
? ? let actualParams = { ...defaultParams, ...data };
? ? console.info(actualParams);
}
introduce({ name: "光脚丫思考1" });
请留意变量actualParams实际上是合并了实际参数和函数内部定义的默认参数的值。控制台输出的结果如下图所示:
调用introduce()函数的时候,传递的参数对象中只包含了name属性值,经过函数内部的展开运算符与默认参数合并后,实际参数变量actualParams的name使用了传递的参数值,而blog则使用了默认参数中的值。为了确保最后是函数调用的实际参数覆盖默认参数,defaultParams要放置于data的前面进行合并,也就是下面这句代码。
let actualParams = { ...defaultParams, ...data };
如果调换了2个参数的位置,如下面的代码所示:
let actualParams = { ...data, ...defaultParams };?
这样的话,默认参数值就会覆盖函数调用时传递的实际参数值,这样会有问题,没有达到默认值想要达到的效果。
二、对象展开
1、对象合并
使用展开运算符(...)可以将多个对象的属性展开并合并为一个新的对象。如下代码:
let user1 = {
? ? name1: "光脚丫思考1",
? ? name2: "光脚丫思考2",
};
let user2 = {
? ? name3: "光脚丫思考3",
? ? name4: "光脚丫思考4",
};
let user3 = { ...user1, ...user2 };
console.info(user3);
创建对象user3的时候使用了展开运算符,可以形象地理解为将user1和user2两个对象的属性展开后合并到user3对象中。 运行上面的代码,在控制台输出的内容如下图所示:
可以清楚的看到,对象user3包含了对象user1和对象user2的所有属性。结论:新对象包含了合并的对象的所有属性。 超过2个对象的属性合并与2个对象的合并是相同的,有兴趣的话可以自行尝试下。 还有一种情况,我们想在尝试下:如果2个对象包含了相同名称的属性,合并的时候会发生什么事情呢?代码如下:
let user1 = {
? ? name1: "光脚丫思考1-1",
? ? name2: "光脚丫思考1-2",
};
let user2 = {
? ? name1: "光脚丫思考2-1",
? ? name3: "光脚丫思考2-3",
};
let user3 = { ...user1, ...user2 };
console.info(user3);
请留意上述代码中user1和user2同时包含了name1这个属性。运行代码后,控制台输出的内容如下图所示:
观察合并后的user3对象,存在name1属性,但是属性值是user2.name1的值。 这里有一个规则:合并的对象中包含有同名的属性,则后面对象中的属性值覆盖前面的同名的属性值。?
2、复制对象
使用展开运算符可以复制对象,代码如下:
let user = {
? ? name: "光脚丫思考",
? ? blog: "https://blog.csdn.net/gjysk"
};
let clonedUser = {...user};
user.name = "光脚丫思考1";
console.info(clonedUser);
上面的代码中,通过展开运算符(...)将user对象复制了一个新的对象clonedUser,然后修改user.name的值。 控制台的输出结果如下图所示:
从上述的代码和控制台输出的结果可以看到,新复制的对象clonedUser.name属性值并没有随着user.name的改变而改变。这可以证明2个对象并非引用的关系,而是互相独立的对象。但是,明显可以看到,我们只是修改了简单类型的属性值,如果修改的属性值是一个对象呢?继续尝试以下代码:
let user = {
? ? name: "光脚丫思考",
? ? blog: "https://blog.csdn.net/gjysk",
? ? address: {
? ? ? ? city: "中国北京"
? ? }
};
let clonedUser = { ...user };
user.address.city = "陕西安康";
console.info(clonedUser);
上面的代码中,我们给user增加了一个address属性,并且该属性是一个对象值。通过展开运算符(...)复制对象之后,紧接着就修改了user.address.city的值。 控制台输出的结果如下图所示:
可以看到,这次修改的值在拷贝对象中也被修改了。这说明对于引用类型的对象属性,只是做了一个浅拷贝,也就是把引用地址给复制了,而引用的对象并未复制,用的是同一个对象。 结论是:通过展开运算符(...)拷贝对象,值类型的属性被深度拷贝了,而引用类型的属性只是做了浅拷贝。
3、非对象展开
如果展开的不是对象,则会自动将其转为对象,但是新创建的对象可能并不包含任何属性。如下代码所示:
console.log({ ...1 });
console.log({ ...undefined });
console.log({ ...null });
console.log({ ...true });
以上的代码在控制台的输出结果如下图所示:
可以看到,以上4种情况都是创建了没有任何属性的空的对象{}。?
三、数组展开
1、参数调用中的数组展开
在《函数调用》中已经演示了调用函数的时候如何将数组元素展开作为参数传递给函数,基本用法如下:
function hello(a, b, c) {
? ? console.info(a);
? ? console.info(b);
? ? console.info(c);
}
var args = ['光脚丫思考1', '光脚丫思考2', '光脚丫思考3'];
hello(...args);
详细的内容可以回头看相关的内容。
2、合并数组
在项目开发中,一定会遇到将多个数组进行结合的情况,ES6之前的可以使用Array的concat()方法进行合并,如下代码:
let links1 = [
? ? "https://blog.csdn.net/gjysk/article/details/123508438",
? ? "https://blog.csdn.net/gjysk/article/details/123755633"
];
let links2 = [
? ? "https://blog.csdn.net/gjysk/article/details/123836530",
? ? "https://blog.csdn.net/gjysk/article/details/124382522"
];
let links3 = links1.concat(links2);
console.info(links3);
在控制台上输出的结果如下:
可以看到links1和links2两个数组中的元素并合并到了links3数组中。 使用ES6的展开运算符(...)可以实现相同的功能和效果,代码如下:
let links1 = [
? ? "https://blog.csdn.net/gjysk/article/details/123508438",
? ? "https://blog.csdn.net/gjysk/article/details/123755633"
];
let links2 = [
? ? "https://blog.csdn.net/gjysk/article/details/123836530",
? ? "https://blog.csdn.net/gjysk/article/details/124382522"
];
let links3 = [...links1, ...links2];
console.info(links3);
请注意:合并之后生成了一个新的数组,但如果数组中包含的元素是对象的话,仍然是引用关系。可以参考《复制对象》里面的代码自行尝试。 另外一点,上述的代码是在元素的末尾追加的另一个数组的元素,如果要在数组的开头,或者中间的任意位置追加另外一个数组的元素呢?下面分别演示。?
(1)开头插入数组
下面的代码演示如何在数组的开头插入另外一个数组的元素:
let links1 = [
? ? "数组1-元素1",
? ? "数组1-元素2"
];
let links2 = [
? ? "数组2-元素1",
? ? "数组2-元素2",
];
let links3 = [...links2, ...links1];
console.info(links3);
为了演示方便,我们将数组中的元素内容修改了下,这样合并后看到的结果会更直观些。 最关键的代码就是使用展开运算符(...)合并代码的那句,实际上就是调整了下两个数组所在的位置,这样就可以了。控制台输出的内容如下所示:
?
(2)中间插入数组
展开运算符(...)暂时没有提供一种更加方便的办法直接将一个数组的元素插入到另外一个数组的任意位置,但是,可以使用类似于如下代码的方式,在构造数组的时候,将另外一个数组插入到任意位置。
let links1 = [
? ? "数组1-元素1",
? ? "数组1-元素2"
];
let links2 = [
? ? "数组2-元素1",
? ? ...links1,
? ? "数组2-元素2",
];
console.info(links2);
上面的代码在创建links2数组的时候,中间的元素使用展开运算符(...)将links1的元素合并进去了。 控制台输出的结果如下图所示:
3、复制数组
前面我们演示过如何复制对象,对于数组而言,同样也可以进行复制。代码如下:
let numbers = [1, 2, 3, 4, 5, 6, 7];
let clonedNumbers = [...numbers];
console.info(clonedNumbers);
上面的代码就是通过展开运算符将数组numbers中的元素复制给了clonedNumbers数组。控制台输出的结果如下图所示:
和复制对象一样,我们接下来分别看下元素为值类型的数组和元素为引用类型的数组复制的效果。 对上面的代码演示的就是值类型的数组元素的复制,稍微改造下代码:
let numbers = [1, 2, 3, 4, 5, 6, 7];
let clonedNumbers = [...numbers];
numbers[0] = "first";
console.info(clonedNumbers);
上面的代码我们将原数组numbers的第1个元素修改为字符串first,然后继续输出拷贝的数组clonedNumbers,控制台输出的结果如下图所示:
和前面代码输出的结果没有任何的变化。这说明是真正的复制,两者之间没有引用关系了,互相不影响和干扰。 接下来,我们看看数组元素为引用类型的数组会是怎样的反应呢?代码如下:
let users = [
? ? {
? ? ? ? name: "光脚丫思考1",
? ? ? ? blog: "https://blog.csdn.net/gjysk"
? ? },
? ? {
? ? ? ? name: "光脚丫思考2",
? ? ? ? blog: "https://blog.csdn.net/gjysk"
? ? },
];
let clonedUsers = [...users];
users[0].name = "光脚丫思考1-已修改"
console.info(clonedUsers);
可以看到,上面的users数组包含了2个对象,通过展开运算符(...)复制生成新的数组clonedUsers之后,我们修改了原有数组users中第1个元素的属性值。 此时,控制台的输出结果如下图所示:
从输出结果可以看到,虽然修改的是原数组users第1个元素的属性值,但是,复制的新数组clonedUsers的第1个元素的对应属性值也跟着修改了。 因此,我们可以得出和对象复制几乎相同的结论:通过展开运算符(...)拷贝数组,值类型的元素被深度拷贝了,而引用类型的元素只是做了浅拷贝。 另外一种情况,我们需要区别对待,如下代码所示:
let users = [
? ? {
? ? ? ? name: "光脚丫思考1",
? ? ? ? blog: "https://blog.csdn.net/gjysk"
? ? },
? ? {
? ? ? ? name: "光脚丫思考2",
? ? ? ? blog: "https://blog.csdn.net/gjysk"
? ? },
];
let clonedUsers = [...users];
users[0] = {
? ? name: "光脚丫思考3",
? ? blog: "https://blog.csdn.net/gjysk"
};
console.info(clonedUsers);
上面的示例代码并非是修改了原数组users中的第一个元素的属性值,而是直接把第一个元素给替换成新的元素了。此时,原数组的内容会改变,而拷贝的数组clonedUsers不会随之改变。控制台输出的结果如下图所示:
四、数组+对象
使用展开运算符可以将数组与对象合并到一起,并返回返回新的数组。下面,我们先看一个简单的示例:
let users = [
? ? {
? ? ? ? name: "光脚丫思考1",
? ? ? ? blog: "https://blog.csdn.net/gjysk"
? ? },
? ? {
? ? ? ? name: "光脚丫思考2",
? ? ? ? blog: "https://blog.csdn.net/gjysk"
? ? },
];
let user3 = {
? ? name: "光脚丫思考3",
? ? blog: "https://blog.csdn.net/gjysk"
};
let combineUsers = [...users, user3];
console.info(combineUsers);
注意使用在定义新的数组combineUsers时,使用展开运算符将users数组的元素展开并添加到新的数组中,而user3则作为普通的一个元素添加到了数组的最后面。以上代码在控制台输出的结果如下图所示:
上面的示例较为简单,我们再来看一个比较复杂的情况。
let data1 = {
? ? dataList: [
? ? ? ? {
? ? ? ? ? ? name: "光脚丫思考1",
? ? ? ? ? ? blog: "https://blog.csdn.net/gjysk"
? ? ? ? }, {
? ? ? ? ? ? name: "光脚丫思考2",
? ? ? ? ? ? blog: "https://blog.csdn.net/gjysk"
? ? ? ? }
? ? ],
? ? pageSize: 15,
? ? pageNum: 1
}
let data2 = {
? ? dataList: [
? ? ? ? {
? ? ? ? ? ? name: "光脚丫思考3",
? ? ? ? ? ? blog: "https://blog.csdn.net/gjysk"
? ? ? ? },
? ? ? ? {
? ? ? ? ? ? name: "光脚丫思考4",
? ? ? ? ? ? blog: "https://blog.csdn.net/gjysk"
? ? ? ? }
? ? ],
? ? pageSize: 20,
? ? pageNum: 2
}
let combine = {
? ? ...data1,
? ? dataList: [
? ? ? ? ...data1.dataList,
? ? ? ? ...data2.dataList
? ? ]
}
console.log(combine);
上面的代码我们模拟接口返回的2个数据结果,data1和data2分别有不同的数据结果,以及分页的大小pageSize和当前的分页pageNum。通过展开运算符(...)将data1的数据直接拷贝给新的combine对象,紧接着将data1和data2的dataList用展开运算符(...)展开合并为新的数组。以上代码在控制台输出的内容如下图所示:
注意观察上面的输出结果,其中pageNum和pageSize的值是data1的,而dataList是data1和data2两个数据列表dataList合并后的结果。 虽然代码的复杂性提升了,但是基本的语法规则是一致的。
五、类数组对象
什么是类数组呢?形象点的理解就是类似数组的变量。比如下面的代码将会产生一个类数组的变量:
var list = document.getElementsByTagName("div");
展开运算符可以基于一个类数组变量生成新的数组对象,比如下面的代码:
<!DOCTYPE html>
<html lang="en">
?
<head>
? ? <meta charset="UTF-8" />
? ? <meta http-equiv="X-UA-Compatible" content="IE=edge" />
? ? <meta name="viewport" content="width=device-width, initial-scale=1.0" />
? ? <title>ES6语法:展开运算符</title>
</head>
?
<body>
? ? <div></div>
? ? <div></div>
? ? <div></div>
</body>
<script>
? ? let divList = document.getElementsByTagName("div");
? ? let arrayList = [...divList];
? ? console.info(divList);
? ? console.info(arrayList);
</script>
?
</html>
以上代码中的divList是类数组变量,通过展开运算符基于该类数组变量创建了新的数组arrayList,在控制台的输出结果如下图所示:
我们再来看另外一个示例,Map变量的的的展开。
var map = new Map([[1, 11], [2, 22], [3, 33]]);
console.log([...map.keys()]);
console.log([...map.values()]);
以上代码输出的结果如下图所示:
从输出结果可以看到,map对象的键和键值分别创建了2个数组,并且位置也是一一对应的关系。
六、字符串展开
如果展开运算符后面是字符串,则至少会有如下的三种展开方式。
1、以对象方式展开
下面的示例代码演示以对象方式展开字符串。
let message = "光脚丫思考";
console.info({...message});
输出结果如下图所示:
以上代码的展开方式创建了一个新的对象,其中字符串每个字符在字符串中的索引位置成为了该对象的属性名,而对应位置的字符则成为了属性值。
2、以数组方式展开
下面的示例代码演示以数组方式展开字符串。
let message = "光脚丫思考";
console.info([...message]);
输出结果如下图所示:
以上代码的展开方式创建了一个新的数组,其中字符串每个字符成为该数组中的一个元素,并且字符在数组中的索引位置与在字符串中的索引位置一致。?
3、直接展开
下面的示例代码演示直接展开字符串的用法。
let message = "光脚丫思考";
console.info(...message);
输出结果如下图所示:
上面的代码实际上是函数调用的时候,将字符串展开作为参数传递给了函数。
|