ES6 开始,js 新增了剩余参数语法、展开语法等,它们有个共同之处就是都以 ... 这么个符号为前缀,好像很多地方都可以用到,但实际上又不是同一回事,容易让初学者晕头转向。本篇就对目前笔者已知的 3 种关于 ... 用法的总结归纳。

用法一:函数的剩余参数(Rest parameters)

定义函数的时候,形参当中,最后一个参数(theArgs)如果以 ... 作为前缀,那么 theArgs 就会变为一个数组,调用该函数时,如果传入的实参数量大于等于定义函数时形参的数量,比如下面的例 1.1,形参定义了 3 个,却传入了 5 个实参,那么从 3 开始(包括),之后的实参都会被放入到 theArgs 中:

// 例 1.1
function fn(a, b, ...theArgs) {
  console.log(theArgs)
}
fn(1, 2, 3, 4, 5) // [ 3, 4, 5 ]

请注意,...theArgs 必须作为最后一个形参,不然会报语法错误:“SyntaxError: Rest parameter must be last formal parameter”。 除箭头函数外,函数内都有个 arguments 可以用来获取传递给函数的参数。那么它和剩余参数有什么区别呢?

  • 剩余参数,比如例 1.1 中的 theArgs 是个真数组,可以直接使用数组的方法,而 arguments 是一个类数组(array-like)对象,想要运用数组方法还得转成数组;
  • 剩余参数只包含那些没有对应形参的实参,比如例 1.1 中的 3、4 和 5,而 arguments 包含了传给函数的所有实参。
  • arguments 可以通过 callee 属性获取当前 arguments 所在的函数。

其实 arguments 是早期的规范中为了方便获取传入函数的参数而存在的,ES6 之后,我们应当尽量使用剩余参数这种语法而不是 arguments。

用法二:展开语法(Spread syntax)

展开语法又可以在 3 个地方使用:

1)调用函数时

剩余参数是在函数定义时形参里使用的,展开语法的用处之一则是在函数调用时,传入的实参里使用。具体说就是将数组表达式或者 string 在语法层面展开作为实参传给函数。比如有如下代码:

// 例 2.1
const arr = [1, 2, 3]
const string = 'Jay'
function fn(a, b, c) {
  console.log(a, b, c)
}

fn 接收 3 个参数,如果我们想把数组 arr 中的每个元素传给 fn,在 ES6 之前可以利用 apply 实现:

// 例 2.1.1
fn.apply(null, arr) // 1 2 3

有了展开语法,则可以直接将作为参数的数组 arr 展开,这样可以提高代码的可读性 :

// 例 2.1.2
fn(...arr) // 1 2 3

展开语法还可以将字符串展开:

// 例 2.1.3
fn(...string) // J a y

2)构造数组时

通过字面量方式构造数组时,也可以使用展开语法:

// 例 2.2
const arr1 = [1, 2]
const arr2 = [3, 4, 5]
// 构造字面量数组
const newArr = [...arr1, ...arr2]
console.log(newArr) // [ 1, 2, 3, 4, 5 ]

3)构造字面量对象时

除了构造数组,ES2018(ES9) 添加了新的特性使得构造对象的时候也可以运用展开语法,被展开的对象表达式将按 key-value 的方式展开 :

// 例 2.3.1
const obj = { name: 'Jay' }
const newObj = { ...obj, age: 40 }
console.log(newObj) // { name: 'Jay', age: 40 }

在构造对象字面量时,还可以把数组也放进去展开,比如在 MDN 上关于展开语法的说明中有这么个例子:

// 例 2.3.2
const obj1 = { foo: 'bar', x: 42 }
const obj2 = { foo: 'baz', y: 13 }
const merge = (...objects) => ({ ...objects })
const mergedObj = merge(obj1, obj2)
console.log(mergedObj) // { '0': { foo: 'bar', x: 42 }, '1': { foo: 'baz', y: 13 } }

乍看上去迷迷糊糊的,可能的难点在于第 4 行代码的理解,它定义了一个函数 const merge = (...objects) => ({ ...objects }, 其中=> 左边的 ...objects 为函数的剩余参数语法,所以 objects 就是一个由 obj1 和 obj2 这两个对象组成的数组:[ { foo: 'bar', x: 42 }, { foo: 'baz', y: 13 } ]=>右边的 ...objects 用到的是展开语法,用于构造字面量对象,只不过因为 objects 是个数组,所以展开数组得到的对象的 key 为数组的下标,即 { ...objects } 得到的对象为:{ '0': { foo: 'bar', x: 42 }, '1': { foo: 'baz', y: 13 } }

注意

在调用函数或构造数组时使用展开语法只能用于可迭代对象,比如不能在构造数组时把一个对象放进去使用展开语法。

展开语法其实是一种浅拷贝

在 MDN 文档还能看到这么一句话:

实际上, 展开语法和 Object.assign() 行为一致, 执行的都是浅拷贝(只遍历一层)。

也就是说类似下面的例 2.4:

// 例 2.4
const obj = {
  a: 'a',
  b: {
    c: 'd'
  }
}
const newObj = { ...obj }
newObj.b.c = '我变了'
console.log(obj.b.c) // 我变了

我们改变通过展开语法得到的 newObj 的深层属性 newObj.b.c = '我变了' 时,会导致 obj.b.c 的值一同被修改。原因可以通过画内存表现图说明:

const newObj = { ...obj } 生成的 newObj,只是把堆内存中 obj 指向的地址为 0x100 这个对象整个复制了一份,生成了地址为 0x300 的新对象(注意这个新对象里,b 的值是一个内存地址),然后让 newObj 指向它。如果我们更改了 newObj 的 a 属性或 b 属性的值,当然不会影响到 obj 对象,但是如果我们改变的是 newObj.b.c,就会找到 0x200 这个对象并修改,自然会影响到 obj 对象。

用法三:解构数组时

在对数组进行解构赋值时,我们可以使用剩余模式,将剩余数组赋值给一个变量,效果上类似于函数的剩余参数。比如下面例 3.1 中的 c

// 例 3.1
const arr = [1, 2, 3, 4, 5]
const [a, b, ...c] = arr
console.log(c) // [ 3, 4, 5 ]

以上就是js 中以 ... 为前缀的几种用法详解的详细内容,更多关于js ... 前缀用法的资料请关注阿兔在线工具其它相关文章!

点赞(0)

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部