对象扩展符简易指南

时间:2021-1-8 作者:admin

合并多个 Javascript 对象是常见的工作,但不好的是,至少到目前,Javascript 仍然没有一种方便的方法来完成这个工作。至少现在是这样。

在 ES5 时代,你可以使用 Lodash 的 _.extend(target, [sources]) 方法,而 ES2015 则引入了 Object.assign(target, [sources]) 方法。

幸运的是,对象扩展符 (an ECMASript proposal at stage 3) 是一个很大的进步,提供了简单方便的如下简介方便的语法。

Run demo

const cat = {   
  legs: 4,
  sound: 'meow'
};
const dog = {  
  ...cat,
  sound: 'woof'
};

console.log(dog); // => { legs: 4, sounds: 'woof' }  

上面的例子中,...cat 复制 cat 的属性到一个新对象 dog 中,cat 中原来的属性 sound 被覆盖,最终值为 woof

本篇文章将介绍对象 spread 和 rest 语法。包括如何实现对象克隆,对象合并,以及如何覆盖属性值。

下面是关于可枚举属性的简单和概括,以及如何分辨对象自有属性和继承属性。

1. 可枚举以及自有属性

Javascript 里的对象是键值对的组合。

键名通常是一个字符串或者一个 symbol 。值可以是一个原始类型的值(string, boolean, number, undefined 或者 null),一个对象或者一个函数。

下面这个例子将通过对象字面量语法创建一个对象。

const person = {  
  name: 'Dave',
  surname: 'Bowman'
};

person 这个对象描述了一个人的名和姓氏。

1.1 可枚举属性

描述一个属性有几种描述符,如 writable、enumerable 和configurable 。你可以看这篇文章Object properties in JavaScript了解更多细节。

Enumerable 描述符是一个布尔值,表示这个属性是否可以被枚举。

你可以通过 Object.keys() 方法来枚举一个对象的自有属性和可枚举属性,通过 for..in 语句来枚举所有可枚举的属性。

以对象字面量的形式创建对象 { prop1: 'val1', prop2: 'val2' } 时显式声明的属性都是可枚举的。接下来看看之前创建的 person 对象有哪些可枚举的属性。

Run demo

const keys = Object.keys(person);  
console.log(keys); // => ['name', 'surname']  

.name.surnameperson 对象的可枚举属性

接下来是有趣的一部分。对象扩展符复制了原对象的所有可枚举属性。

Run demo

console.log({ ...person }; // => { name: 'Dave', surname: 'Bowman' }  

下面我们在 person 对象上创建一个不可枚举属性 .age

Run demo

Object.defineProperty(person, 'age', {  
  enumerable: false, // Make the property non-enumerable
  value: 25
});
console.log(person['age']); // => 25

const clone = {  
  ...person
};
console.log(clone); // => { name: 'Dave', surname: 'Bowman' }  

目标对象 clone 从源对象 person 上创建了可枚举属性 .name.surname ,但是不可枚举属性 .age 则被忽略掉。

1.2 自有属性

Javascript 内有原型继承机制,因此一个对象上既有 自有属性,也有从继承属性。

对象字面量显式声明的属性都是自有属性,存在于原型链上的属性都是 继承 属性。

下面将创建一个 personB 对象,并将 person 对象设置成其原型对象。

Run demo

const personB = Object.create(person, {  
  profession: {
    value: 'Astronaut',
    enumerable: true
  }
});

console.log(personB.hasOwnProperty('profession')); // => true  
console.log(personB.hasOwnProperty('name'));       // => false  
console.log(personB.hasOwnProperty('surname'));    // => false  

现在 personB 上有自有的 .profession 属性,以及从原型对象 person 上继承来的 .name.surname 属性。

对象扩展符从源对象上复制自有属性,而会忽略继承的属性

Run demo

const cloneB = {  
  ...personB
};
console.log(cloneB); // => { profession: 'Astronaut' }  

...personB 只从 personB 上复制了 .profession 这个自有属性,而继承属性 .name.surname 则被忽略。

总结: 对象扩展符号只会从源对象上复制 自有和可枚举属性,这和 Object.keys() 方法的返回值一样。

2. 对象扩展属性

在对象字面量里,对象扩展符源对象里的自有属性和可枚举属性复制进目标对象内。

const targetObject = {  
  ...sourceObject,
  property: 'Value'
};

顺便说一下,很多时候,对象扩展符与 Object.assign() 等价,上面的代码也可以用这样实现:

const targetObject = Object.assign(  
  { }, 
  sourceObject,
  { property: 'Value' }
);

一个对象字面量里可以使用多个对象扩展符,与普通的属性声明同时使用:

const targetObject = {  
  ...sourceObject1,
  property1: 'Value 1',
  ...sourceObject2,
  ...sourceObject3,
  property2: 'Value 2'
};

2.1 对象扩展规则:后面的属性会覆盖前面的

当同时扩展多个对象时,这个对象内可能会存在同名属性,那么最终生成的对象的属性值是怎么计算的,规则很简单:后扩展的属性会覆盖之前扩展的属性

来看一些简单的例子。下面的代码会实例化一只 cat 。

const cat = {  
  sound: 'meow',
  legs: 4
};

现在我们要变一个魔术,将这只猫变成一只狗,注意 .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 操作符在解构赋值时可以收集剩余的属性。

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。