解构赋值-对象和函数参数

所谓解构赋值,就是一种模式匹配规则,当等号两边的模式相同时,就可以进行赋值操作。解构赋值的本质,就在于赋值二字。

对象的解构赋值

1. 变量名和属性名相同

对象的解构赋值要求被赋值变量名与赋值对象属性名相同,才可以得到正确的值。

const { name, age, job } = { name: "jaakko", age: 22 }

name    // "a"
age    // 10
job    // undefined

上面这个例子中,变量 nameage 都与赋值对象中属性名相同,故赋值成功,而变量 job 在赋值对象中没有对应属性名称,故赋值失败,值为 undefined

2. 变量名和属性名不同

如果不想使用原对象的属性名作为变量名,就要用到下面这种写法。

const { name: myName, age: myAge } = { name: "jaakko", age: 22 }

myName  // "a"
myAge   // 10

这也说明默认的赋值操作实际是下面这种写法的简写。

const { name: name, age: age } = { name: "jaakko", age: 22 }

3. 指定默认值

上面我们说到,当被赋值变量与赋值对象属性名没有对应时,变量会被赋值为 undefined,为了避免在赋值失败时变量变为 undefined,我们可以给它指定默认值。

当赋值失败时,变量的值就是我们指定的默认值;当赋值成功时,变量会忽略我们指定的默认值。

const { name="fake_name" } = { name: "jaakko" };

name    // "jaakko"

上面这个例子中,name 属性被解构成功,所以忽略了默认值。

注意,这里设置默认值使用的是 =,而不是 :

默认值生效的条件是,对象的属性值严格等于 undefined

const { name="fake_name", age = 22, job = "engineer" } = { 
    name: "jaakko",
    age: undefined,
    job: null
};

name    // "jaakko"
age    // 22
job    // null

上面这个例子中,name 由于解构成功而忽略默认值,age 得到了 undefined 所以使用默认值,而 job 属性的值为 null,不严格等于 undefined,所以算解构成功。

函数参数的解构赋值

1. 函数参数为数组时

function add([x, y]) {
    return x + y;
}

add([3, 4]);    // 7

传递给 add 方法的参数是一个数组,但对于 add 内部来说,它们能感受到的就是 xy 两个变量。开篇提到过,解构赋值的本质就是给变量赋值。

2. 函数参数为对象时

function introduce({name, age}) {
    console.log(`我叫 ${name}, 我 ${age} 岁!`);
}

introduce({name: 'jaakko', age: 22})    // "我叫 jaakko, 我 22 岁!"

和数组同理,函数内部接收变量。

给变量设定默认值

函数参数的解构也可以指定默认值。

function introduce({name = "fake_name", age = 99}) {
    console.log(`我叫 ${name}, 我 ${age} 岁!`);
}

introduce({})   // "我叫 fake_name, 我 99 岁!"

introduce 方法的参数为空对象时,nameage 解构失败,使用默认值。

下面这种情况会报错。

function introduce({name = "fake_name", age = 99}) {
    console.log(`我叫 ${name}, 我 ${age} 岁!`);
}

introduce();    // Uncaught TypeError: Cannot destructure property `name` of 'undefined' or 'null'.

introduce 的参数为空时,就会发生错误(注意!不是解构失败,是抛出错误)。造成错误的原因是,我们试图从一个不存在的值中去解构,这显然是错误的行为。

为了避免这种情况,我们需要给 introduce 的参数一个默认值。

function introduce({name = "fake_name", age = 99} = {}) {
    console.log(`我叫 ${name}, 我 ${age} 岁!`);   // "我叫 fake_name, 我 99 岁!"
}

introduce()     // "我叫 fake_name, 我 99 岁!"

现在,即使不给 introduce 传递参数也不会报错了,因为参数有一个默认值,是一个空对象。

给函数参数设定默认值

最后,要注意下面这种情况。

function introduce({name, age} = {name: "fake_name", age: 99}) {
    console.log(`我叫 ${name}, 我 ${age} 岁!`);   // "我叫 fake_name, 我 99 岁!"
}

introduce({ name: "jaakko", age: 22 });     // 我叫 jaakko, 我 22 岁!
introduce({});     // 我叫 undefined, 我 undefined 岁!
introduce();     // 我叫 fake_name, 我 99 岁!

注意看第二次调用 introduce 方法的结果,似乎与我们预期的不太一样,下面我们依次来分析这三种调用方式的不同。

(1) 带参调用

这个结果与我们的预期结果相符:传入参数,解构成功,忽略默认值,函数内部使用传入参数。

(2) 传入参数为空对象

这种情况就与我们的预期不符了,我们预期是:解构失败,使用参数默认值。这里明显是解构失败了,因为参数的值均为 undefined,但是却没有使用默认值,为什么呢?

还记得我们上一个例子吗?我们说给函数参数一个默认值(空对象),也就是说函数参数中 = 后面的部分应该是函数的默认参数

{name, age} = {name: "fake_name", age: 99}

现在函数的默认参数是这个带参数的对象 {name: "fake_name", age: "99"},所以,当我们不给函数传递参数时,函数才会使用这个默认参数。

现在的情况是,我们给函数传递了一个空对象 {} 作为参数,所以函数会从传递进来的参数中去解构 nameage 的值,而不是使用默认值。

(3) 不带参调用

理解了第 (2) 种情况,这种情况就好理解了。

不给函数传递参数时,会使用默认参数作为函数的参数,会从默认参数对象去解构变量,所以这里 nameage 解构出来的是函数默认参数的值。

总结

理解了上面三种情况产生的原因(重点是后两种),我们应该明白了,我们给函数设定默认值和给变量设定默认值是两种完全不同的行为,在使用的时候一定要注意。

推荐下面这种写法。

function introduce({name = "fake_name", age = 99} = {}) {
    console.log(`我叫 ${name}, 我 ${age} 岁!`);   // "我叫 fake_name, 我 99 岁!"
}

introduce({ name: "jaakko", age: 22 });     // 我叫 jaakko, 我 22 岁!
introduce({});     // 我叫 fake_name, 我 99 岁!
introduce();    // 我叫 fake_name, 我 99 岁!

这种写法得到的结果与我们预期相符。

简单分析一下:

introduce 方法设定的默认参数是一个空对象 {}

  • 当带参数调用时,忽略默认参数(空对象),变量解构成功,忽略变量默认值,使用解构的值。
  • 当传入参数为空对象时,忽略默认参数(传入参数和默认参数都是空对象),变量解构失败,使用变量默认值。
  • 当参数为空时(即不传参数),使用默认参数(空对象)作为参数,解构失败,使用变量默认值。

参考: