手记

An easy guide to object rest/spread properties in JavaScript

变一个魔术,将这只猫变成一只狗,注意 .sound 属性值如何变化。

Run demo

const dog = {  
  ...cat,  ...{
    sound: 'woof' // `<----- Overwrites cat.sound
  }
};
console.log(dog); // =>` { sound: 'woof', legs: 4 }

后面声明的 ·woof· 属性值覆盖了前面的在 cat 对象声明的属性值 'meow' , 符合之前所说的规则: 对于同名属性,后声明的值覆盖先声明的值。

这个规则同样适用于对象的初始化

Run demo

const anotherDog = {  
  ...cat,
  sound: 'woof' // `<---- Overwrites cat.sound
};console.log(anotherDog); // =>` { sound: 'woof', legs: 4 }

上面代码里,sound: 'woof' 同样覆盖了之前声明的 ' meow' 值。

现在,交换一下扩展对象的位置,输出了不同的结果。

Run demo

const stillCat = {  
  ...{
    sound: 'woof' // `<---- Is overwritten by cat.sound
  },  ...cat
};
console.log(stillCat); // =>` { sound: 'meow', legs: 4 }

cat 对象仍然是 cat 对象。虽然第一个源对象内的 .sound 属性值是 'woof' ,但是被之后 cat 对象的 'meow' 覆盖。

普通属性和对象扩展的相对位置非常重要,这将直接影响到对象克隆,对象合并,以及填充默认属性的结果。

下面分别详细介绍。

2.2 克隆对象

用对象扩展符克隆一个对象非常简洁,下面的代码克隆了一个 bird 对象。

Run demo

const bird = {  
  type: 'pigeon',
  color: 'white'};

const birdClone = {  
  ...bird
};

console.log(birdClone); // => { type: 'pigeon', color: 'white' }  
console.log(bird === birdClone); // => false

...birdbird 对象的自有和可枚举属性复制到目标对象 birdClone 内。

虽然克隆看起来很简单,但仍然要注意其中的几个细微之处。

浅复制

对象扩展只是对对象进行了 浅复制, 只有对象自身被复制,而嵌套的对象结构 没有被复制

laptop 对象有一个嵌套对象 laptop.screen。现在我们来克隆 laptop对象来看看其内部的嵌套对象怎么变化。

Run demo

const laptop = {    name: 'MacBook Pro',  screen: {    size: 17,    isRetina: true
  }};const laptopClone = {  
  ...laptop};console.log(laptop === laptopClone);               // => false  console.log(laptop.screen === laptopClone.screen); // => true

第一个比较语句 laptop === laptopClone 的值为 false, 说明主对象被正确克隆。

然而 laptop.screen === laptopClone.screen 的计算结果为 true ,说明 laptopClone.screen 没有被复制,而是 laptop.screenlaptopClone.screen 引用了同一个嵌套对象。

好的一点是,你可以在对象的任何一层使用对象扩展符,只需要再多做一点工作就同样可以克隆一个嵌套对象。

Run demo

const laptopDeepClone = {  
  ...laptop,  screen: {
     ...laptop.screen
  }
};

console.log(laptop === laptopDeepClone);               // => false  console.log(laptop.screen === laptopDeepClone.screen); // => false

使用 ...laptop.screen 使嵌套对象也被克隆,现在 laptopDeepClone 完全克隆了 laptop

原型失去了

下面的代码声明了一个 Game 类,并创造了一个 doom实例。

Run demo

class Game {  
  constructor(name) {    this.name = name;
  }

  getMessage() {    return `I like ${this.name}!`;
  }
}const doom = new Game('Doom');  
console.log(doom instanceof Game); // => true  console.log(doom.name);            // => "Doom"  console.log(doom.getMessage());    // => "I like Doom!"

现在我们克隆一个通过构造函数创建的 doom 实例,结果可能与你想的不同。

Run demo

const doomClone = {  
  ...doom
};console.log(doomClone instanceof Game); // => false  console.log(doomClone.name);            // => "Doom"  console.log(doomClone.getMessage());  
// TypeError: doomClone.getMessage is not a function

...doom 将自有属性 .name 属性复制到 doomClone 内。

doomClone 现在只是一个普通的 JavaScript 对象,它的原型是 Object.prototype 而不是预想中的Game.prototype对象扩展不保留源对象的原型。

因此调用 doomClone.getMessage() 方法会抛出一个 TypeError 错误,因此 doomClone 没有继承 getMessage() 方法。

当然我们可以手动在克隆对象上加上 __proto__ 属性来结局这个问题。

Run demo

const doomFullClone = {  
  ...doom,  __proto__: Game.prototype
};console.log(doomFullClone instanceof Game); // => true  console.log(doomFullClone.name);            // => "Doom"  console.log(doomFullClone.getMessage());    // => "I like Doom!"

对象字面量内部的 __proto__ 属性确保了 doomFullClone 的原型为 Game.prototype

尽量不要尝试这种方法__proto__ 属性已经废弃,这里使用它只是为了论证前面的观点。

对象扩展的目的是以浅复制的方式扩展自有和可枚举属性,因此不保留源对象的原型似乎也说得过去。

例外,这里用 Object.assign() 来克隆 doom 更加合理。

Run demo

const doomFullClone = Object.assign(new Game(), doom);console.log(doomFullClone instanceof Game); // => true  console.log(doomFullClone.name);            // => "Doom"  console.log(doomFullClone.getMessage());    // => "I like Doom!"

这样,就保留了原型。

2.3 不可变对象更新

在一个应用里,同一个对象可能会用于多个地方,直接修改这个对象会带来意想不到的副作用,并且追踪这个修改及其困难。

一个好的方式是使操作不可变。不可变性使修改对象更为可控,更有利于书写。pure functions。即时是在复杂的应用场景,由于单向数据流,更容易确定对象的来源和改变的原因。

使用对象扩展能更方便的以不可变方式来修改一个对象。假设现在你有一个对象来描述一本书的信息。

const book = {  
  name: 'JavaScript: The Definitive Guide',
  author: 'David Flanagan',
  edition: 5,
  year: 2008
};

现在,书第六版即将出版,我们用对象扩展的处理这个场景。

Run demo

const newerBook = {  
  ...book,
  edition: 6,  // <----- Overwrites book.edition
  year: 2011   // <----- Overwrites book.year
};

console.log(newerBook);  
/*
{
  name: 'JavaScript: The Definitive Guide',
  author: 'David Flanagan',
  edition: 6,
  year: 2011
}
*/

newerBook 对象内的 ...book 扩展了 book 对象的属性。手动创建的可枚举属性 editon: 6year: 2011 更新了原有的同名属性。

重要的属性一般在末尾来指定,以便覆盖前面已经创建的同名属性。

newerBook 是一个更新了某些属性的新的对象,并且我们没有改变原有的 book 对象,满足了不可变性的要求。

2.4 合并对象

使用对象扩展符合并多个对象非常简单。

现在我们合并3个对象来创建一个“合成对象”。

Run demo

const part1 = {  
  color: 'white'};const part2 = {  
  model: 'Honda'};const part3 = {  
  year: 2005};const car = {  
  ...part1,
  ...part2,
  ...part3
};
console.log(car); // { color: 'white', model: 'Honda', year: 2005 }

上面的例子中,我们使用 part1part2part3 3个对象合并成了一个 car 对象。

另外,不要忘了之前讲的规则,后面的属性值会覆盖前面的同名属性值。这是我们合并有同名属性对象的计算依据。

现在我们稍微改变一下之前的代码。给 part1part3 增加一个 .configuration 属性。

Run demo

const part1 = {  
  color: 'white',
  configuration: 'sedan'};const part2 = {  
  model: 'Honda'};const part3 = {  
  year: 2005,
  configuration: 'hatchback'};const car = {  
  ...part1,
  ...part2,
  ...part3 // <--- part3.configuration overwrites part1.configuration};
console.log(car);  
/*
{ 
  color: 'white', 
  model: 'Honda', 
  year: 2005,
  configuration: 'hatchback'  `<--- part3.configuration
}
*/

...part1configuration 属性设置成了 'sedan'。然而之后的扩展符 ...part3 覆盖了之前的同名 .configuration,最终生成的对象值为 'hatchback'

2.5 给对象设置默认值

一个对象在程序运行时可能会有多套不同的属性值,有些属性可能会被设置,有些则可能被忽略。

这种情况通常发生在一个配置对象上。用户可以指定一个重要的属性值,不重要的属性则使用默认值。

现在我们来实现一个 multline(str, config) 方法,将str 按照给定的长度分割成多行。

config 对象接受下面3个可选的参数。

  • width: 分割的字符长度,默认是 10

  • newLine: 添加到每一行结尾的的字符, 默认是 \n

  • indent: 每一行开头的缩进符,默认是空字符串 ''

下面是一些 multline() 运行的例子。

Run demo

multiline('Hello World!');  
// =>` 'Hello Worl\nd!'multiline('Hello World!', { width: 6 });  
// => 'Hello \nWorld!'multiline('Hello World!', { width: 6, newLine: '*' });  
// => 'Hello *World!'multiline('Hello World!', { width: 6, newLine: '*', indent: '_' });  
// => '_Hello *_World!'

config 参数接受几套不同的属性值:你可以指定1,2或者3个属性值,甚至不指定任何一个属性。

使用对象扩展语法来填充配置对象非常简单,在对象字面量里,首先扩展默认值对象,然后是配置对象,如下所示:

Run demo

function multiline(str, config = {}) {  
  const defaultConfig = {    width: 10,    newLine: '\n',    indent: ''
  };  const safeConfig = {
    ...defaultConfig,
    ...config
  };  let result = '';  // Implementation of multiline() using
  // safeConfig.width, safeConfig.newLine, safeConfig.indent
  // ...
  return result;
}

我们来仔细了解一下 safeConfig 对象。

...defaultConfig 首先将默认对象的属性复制,随后,...config 里用户自定义的值覆盖了之前的默认属性值。

这样 safeConfig 值就拥有了所有 multiline() 需要的配置参数。无论调用 multiline() 函数时,输入的 config 是否缺失了某些属性,都可以保证 safeConfig 拥有所有的必备参数。

显而易见,对象扩展实现了我们想要的 给对象设置默认值。

2.6 更加深入

对象扩展更有用的一点是用于嵌套对象,当更新一个复杂对象时,更具有可读性,比 Object.assign() 更值得推荐。

下面的 box 对象定义一个盒子及盒子内的物品。

const box = {  
  color: 'red',  size: {    width: 200, 
    height: 100 
  },
  items: ['pencil', 'notebook']
};

box.size 描述了这个盒子的尺寸,box.items 列举了盒子内的物品。

为了使盒子看起来更高,我们增大 box.size.height 的值,只需要在嵌套对象上使用 对象扩展符

Run demo

const biggerBox = {  
  ...box,  size: {
    ...box.size,    height: 200
  }
};
console.log(biggerBox);  
/*
{
  color: 'red',
  size: {
    width: 200, 
    height: 200 <----- Updated value
  },
  items: ['pencil', 'notebook']
}
*/

...box 确保了 biggerBox 获得了 源对象 box 上的全部属性。

更新 box.size 的 height 值需要额外一个 {...box.size, height: 200} 对象,该对象接收 box.size 的全部属性,并将 height 值更新至 200

只需要一个语句就能更新对象的多处属性。

现在如果我们还想把颜色改成 black ,增加盒子的宽度到 400, 并且再放一把尺子到盒子内,应该怎么办?同样很简单。

Run demo

const blackBox = {  
  ...box,  color: 'black',  size: {
    ...box.size,    width: 400
  },
  items: [
    ...box.items,    'ruler'
  ]
};
console.log(blackBox);  
/*
{
  color: 'black', <----- Updated value
  size: {
    width: 400, <----- Updated value
    height: 100 
  },
  items: ['pencil', 'notebook', 'ruler'] `<----- A new item ruler
}
*/

2.7 扩展 undefinednull原始类型值

如果在 undefinednull原始类型值 上使用原始类型的值,不会复制任何属性,也不会抛出错误,只是简单的返回一个空对象。

Run demo

const nothing = undefined;  
const missingObject = null;  
const two = 2;console.log({ ...nothing });       // => { }  console.log({ ...missingObject }); // => { }  console.log({ ...two });           // => { }

如上所示:从 nothing, missingObjecttwo不会复制任何属性。

当然,这只是一个演示,毕竟根本没有理由在一个原始类型的值上面使用对象扩展符。

3. 剩余属性

当使用解构赋值将对象的属性值赋值给变量后,剩余的属性值将会被集合进一个剩余对象内。

下面的代码演示了怎么使用 rest 属性。

Run demo

const style = {  
  width: 300,
  marginLeft: 10,
  marginRight: 30};const { width, ...margin } = style;

console.log(width);  // => 300  console.log(margin); // => { marginLeft: 10, marginRight: 30 }

通过解构赋值,我们定义了一个新的变量 width ,并将它的值设置为 style.width。而解构赋值声明内的 ...margin 则获得了 style 对象的其余属性,margin 对象获取了 marginLeftmarginRight 属性。

rest 操作符同样只会获取自有属性和可枚举属性。

注意,在解构赋值内,rest 操作符只能放到最后,因此 const { ...margin , width } = style 无效,并会抛出一个 SyntaxError: Rest element must be last element 错误。

4. 结论

对象扩展需要以下几点:

  • 它只会提取对象的自有属性和可枚举属性

  • 后定义的属性值会覆盖之前定义过的同名属性值

同时,对象扩展使用上方便简洁,能更好的处理嵌套对象,保持不可变性,在实现对象克隆和填充默认属性值上也使用方便。

rest 操作符在解构赋值时可以收集剩余的属性。

原文出处


0人推荐
随时随地看视频
慕课网APP