介绍
Rest参数(剩余参数), arguments变量, Spread语法(展开语法) 深浅拷贝
# Rest参数(剩余参数)和Spread语法(展开语法)
当我们在代码中看到 ...
时,它要么是 rest 参数,要么是 spread 语法。
有一个简单的方法可以区分它们:
- 若
...
出现在函数参数列表的最后,那么它就是 rest 参数,它会把参数列表中剩余的参数收集到一个数组中。 - 若
...
出现在函数调用或类似的表达式中,那它就是 spread 语法,它会把一个数组展开为列表。
使用场景:
- Rest (opens new window)参数用于创建可接受任意数量参数的函数。
- Spread (opens new window) 语法用于将数组传递给通常需要含有许多参数的函数。
我们可以使用这两种语法轻松地互相转换列表与参数数组。
旧式的 arguments (opens new window)(类数组且可迭代的对象)也依然能够帮助我们获取函数调用中的所有参数。
# Rest 参数 (opens new window) 剩余参数
我们可以在函数定义中声明一个数组来收集参数。语法是这样的:...变量名
,这将会声明一个数组并指定其名称,其中存有剩余的参数。这三个点的语义就是“收集剩余的参数并存进指定数组中”。
- Rest参数,它会把参数列表中剩余的参数收集到一个数组中。
例如,我们需要把所有的参数都放到数组 args
中:
function sumAll(...args) { // 数组名为 args
let sum = 0;
for (let arg of args) sum += arg;
return sum;
}
alert( sumAll(1) ); // 1
alert( sumAll(1, 2) ); // 3
alert( sumAll(1, 2, 3) ); // 6
我们也可以选择将第一个参数获取为变量,并将剩余的参数收集起来。
下面的例子把前两个参数获取为变量,并把剩余的参数收集到 titles
数组中:
function showName(firstName, lastName, ...titles) {
alert( firstName + ' ' + lastName ); // Julius Caesar
// 剩余的参数被放入 titles 数组中
// i.e. titles = ["Consul", "Imperator"]
alert( titles[0] ); // Consul
alert( titles[1] ); // Imperator
alert( titles.length ); // 2
}
showName("Julius", "Caesar", "Consul", "Imperator");
**Rest 参数必须放到参数列表的末尾**
Rest 参数会收集剩余的所有参数,因此下面这种用法没有意义,并且会导致错误:
function f(arg1, ...rest, arg2) { // arg2 在 ...rest 后面?!
// error
}
...rest
必须写在参数列表最后。
# arguments 变量 (opens new window)
有一个名为arguments (opens new window) 的特殊类数组对象可以在函数中被访问,该对象以参数在参数列表中的索引作为键,存储所有参数。
例如:
function showName() {
alert( arguments.length );
alert( arguments[0] );
alert( arguments[1] );
// 它是可遍历的
// for(let arg of arguments) alert(arg);
}
// 依次显示:2,Julius,Caesar
showName("Julius", "Caesar");
// 依次显示:1,Ilya,undefined(没有第二个参数)
showName("Ilya");
在过去,JavaScript 中不支持 rest 参数语法,而使用 arguments
是获取函数所有参数的唯一方法。现在它仍然有效,我们可以在一些老代码里找到它。
但缺点是,尽管 arguments
是一个类数组,也是可迭代对象,但它终究不是数组。它不支持数组方法,因此我们不能调用 arguments.map(...)
等方法。
此外,它始终包含所有参数,我们不能像使用 rest 参数那样只截取参数的一部分。
因此,当我们需要这些功能时,最好使用 rest 参数。
**箭头函数没有 arguments**
如果我们在箭头函数中访问 arguments
,访问到的 arguments
并不属于箭头函数,而是属于箭头函数外部的“普通”函数。
举个例子:
function f() {
let showArg = () => alert(arguments[0]);
showArg();
}
f(1); // 1
我们已经知道,箭头函数没有自身的 this
。现在我们知道了它们也没有特殊的 arguments
对象。
# Spread 语法 (opens new window) 展开运算符
展开运算符
或 扩展运算符
,是在ES6中新增加的内容,它可以在函数调用/数组构造时,将数组表达式或者String
在语法层面展开;还可以在构造字面量对象时将对象表达式按照key
-value
的方式展开。
- 只要能被for...in (opens new window)迭代(渲染)的内容 都可以使用 展开运算符 (Symbol (opens new window)唯一的标识符 和 Number (opens new window)数字类型除外)
- 被展开的东西,就是一段用逗号分隔的代码,那么从
语法层面
,展开的内容可以放在任何它可以存在的地方。 - 展开运算符是浅拷贝
# 字符串展开
- 字符串可以用
...
展开,展开成逗号分割的元素集合。
const str = "abc"
console.log(...str) // a b c
// 等同于
console.log('a', 'b', 'c');
# 数组展开
- 数组也支持for...in (opens new window) 迭代那也能被展开
const arr = [1, 2, 3]
const Json = [
{
name: '小王',
old: '13'
},
{
name: '小绿',
old: '16'
}
]
console.log(...arr) // 1 2 3
console.log(...Json) // {name: '小王', old: '13'} {name: '小绿', old: '16'}
# 展开对象内容
- 如果对象通过以上数组 字符串 方式直接展开会报错 不能使用扩展用算符展开 对象的
key
:value
的形式 - 在展开运算符之前 我们通常是使用ES5的arguments (opens new window) 进行手动展开的
const Obj = {
name : 'keke',
age: 12
}
// 直接扩展对象
console.log(...Obj)
// 相当于这样写
console.log(name : 'keke',age: 12) // 哪里有这样写的变量 肯定会报错
- 会被浏览器报错
- 正确写法 需要用
{}
包起来key
:value
的形式
// 正确的写法
console.log({...Obj})
// 相当于
console.log({name : 'keke',age: 12})
- 对象结构常用于Vue中的赋值 某些数据直接赋值可能会导致数据出现双向绑定问题(比如父子传值) 因为是浅拷贝
# 展开对象并且只用值
- 对象的展开 好像并没有什么用 但是可以通过Object.values() (opens new window) 先把对象的值转成数组集合 让后通过
...
展开运算符 把值展开
let user = {
myName: 'John',
age: 30,
}
const { myName, age } = user // 通过赋值的方式解构对象
console.log(...Object.values(user)) // 通过...运算符解构对象
# 拼接对象和数组(浅拷贝)
- 通过
...
展开运算符 可以混入多个对象 - 如果存在相同
key
键名(属性名) 取新添加的key
键名(属性名) - 混入对象的时候 外层一定要有
{}
从展开的层面理解为{key: value, key: value}
- 展开运算符是浅拷贝
单个对象的混入
const Obj = {
name: '小刘',
old: '18',
}
// 单个对象的混入
console.log({ ...Obj, ...{ hobby: '学习' } }) // {name: '小刘', old: '18', hobby: '学习'}
多个对象的混入
const Obj = {
name: '小刘',
old: '18',
}
const Obj2 = {
hobby: '学习',
girl: '没有',
}
const ObjAll = {
...Obj,
...Obj2,
}
// 多个对象的混入
console.log(ObjAll) // {name: '小刘', old: '18', hobby: '学习', girl: '没有'}
数组之间的混入
const ret = [1, 2, 3]
const ret1 = [3, 2, 1]
const retAll = [...ret, ...ret1]
console.log(retAll) // [1, 2, 3, 3, 2, 1]
# 展开内容的存放
- 展开的内容可以放在任何它可以存在的地方,这是展开运算符的精华所在。
- 上面我们已经说了,被
...
展开的东西,就是一段用逗号分隔的代码,那么从语法层面,展开的内容可以放在任何它可以存在的地方。
展开后存放在 数组中
- 字符串通过
...
展开后 存放在数组中
const str = "abc"
const strArr = [...str]
// 通过数组 储存展开运算符的数据
console.log(strArr) // ['a', 'b', 'c']
展开后存放在 对象中
- 该语法是Rest参数(剩余参数), 请查看上面的队Rest参数的记录
# 报错参考
- 当你语法错误或 展开了无法展开的内容(Symbol (opens new window)唯一的标识符) 会报
- 当你试图展开
Number
类型的数据 或 直接展开对象 会报
# 深拷贝浅拷贝
JavaScript
中存在两大数据类型:
- 基本类型
- 基本类型数据保存在在栈内存中
- 引用类型 (opens new window)
- 引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中
浅拷贝和深拷贝的区别
浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址
即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址
深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
# 对象浅拷贝的例子
如果两个对象, 都不存在子级, 自身是一个扁平化数据, 没有多层级, 上面的展开运算符中的浅拷贝就是一个例子
举个浅拷贝的例子
- 下面的
Obj
和Obj2
都是扁平化对象, 那么这个时候浅拷贝生效: 拷贝一层,深层次的引用类型则共享内存地址
const Obj = {
name: '小刘',
}
const Obj2 = {
hobby: '学习',
}
const ObjAll = {
...Obj,
...Obj2,
}
Obj2.hobby = '打游戏'
console.log(ObjAll.hobby) // 学习
- 那么深层次的对象修改, 浅拷贝就会直接对深层次的对象进行共享内存地址
- 面对这种深层次树形结构的对象修改, 就需要用到深拷贝来实现了,比如lodash的.cloneDeep(value) (opens new window)
const Obj = {
name: '小刘',
}
const Obj2 = {
myHobby: {
hobby: '学习',
},
}
const ObjAll = {
...Obj,
...Obj2,
}
Obj2.myHobby.hobby = '打游戏' // 深层次的对象是引用相同的内存地址
console.log(ObjAll.myHobby.hobby) // 打游戏
# const/let引用
浅拷贝和深拷贝不要和const
和let
的赋值引用关系 (opens new window)搞混
array
和object
都存在内存地址的, 所以这两个类型支持引用,string
和number
他们是简单数据类型, 不存在引用关系
const user = [2, 3, 4]
const admin = user
admin[0] = 1
console.log(user) // [1, 3, 4] // 通过 "user" 引用也会看到这个修改
let user = { name: 'John' }
let admin = user
admin.name = 'Pete' // 通过 "admin" 引用来修改
console.log(user.name); // 'Pete', 通过 "user" 引用也会看到这个修改