深入浅出讲解ES6的解构

什么是解构?

解构与构造数据截然相反。 例如,它不是构造一个新的对象或数组,而是逐个拆分现有的对象或数组,来提取你所需要的数据。

ES6使用了一种新模式来匹配你想要提取的数值, 解构赋值就是采用了这种模式。 该模式会映射出你正在解构的数据结构,只有那些与该模式相匹配的数据,才会被提取出来。

被解构的数据项位于赋值运算符 = 的右侧,可以是任何数组和对象的组合,允许随意嵌套。用于给这些数据赋值的变量个数不限。

数组解构

数组解构 使用一个数组作为一个数据项,你可以根据 数组模式 (用于从数组中匹配你所需要的数值)从这个数组里面提取数值给一个或者多个变量赋值。

数组模式 是根据数值的位置来鉴别哪些值是你想要提取的。它必须能精确地映射数组的结构,来要让数组模式中的每个变量都被赋上 被解构数组中 位置与之相对应的值。

举几个例子来帮助我们理解吧:

数组模式示例

把数组中所有的数值赋给一个个单独的变量

   // 设置数组
  const avengers = ['Tony Stark', 'Steve Rogers', 'Natasha Romanoff'];

  // 把数组解构赋值给变量。数组模式位于赋值运算符 `=` 的左侧,被结构的数组在
  // 其右侧。
  const [ironMan, cap, blackWidow] = avengers;

  // ironMan = 'Tony Stark' 
  // cap = 'Steve Rogers'
  // blackWidow = 'Natasha Romanoff'

  // 输出 ironMan:
  ironMan;

提取除第一个外的所有数值

 const avengers = ['Tony Stark', 'Steve Rogers', 'Natasha Romanoff'];

  // 我们不用用到Tony
  const [, cap, blackWidow] = avengers;

  // ironMan = Error: undefined 
  // cap = 'Steve Rogers'
  // blackWidow = 'Natasha Romanoff'

  // 输出 cap:
  cap;

提取除第二个外的所有数值

 const avengers = ['Tony Stark', 'Steve Rogers', 'Natasha Romanoff'];

  // cap 缺失
  const [ironMan, , blackWidow] = avengers;

  // ironMan = 'Tony Stark' 
  // cap = Error: undefined
  // blackWidow = 'Natasha Romanoff'

  // 输出 blackWidow:
  blackWidow;

提取除最后一个外的所有数值

 const avengers = ['Tony Stark', 'Steve Rogers', 'Natasha Romanoff'];

  // ironMan vs cap
  const [ironMan, cap] = avengers;

  // ironMan = 'Tony Stark' 
  // cap = 'Steve Rogers'
  // blackWidow = Error: undefined

  // 输出 blackWidow:
  ironMan;

嵌套数组

这种匹配模式也支持嵌套数组,只要保证赋值运算符 = 左侧的数组模式与右侧的数组结构相匹配即可。再次说明一下,= 左边的变量都会被赋上 = 右侧数组中位置与之相对应的值。 无论你怎么深层次地嵌套,仍可以对它们进行解构。

解构嵌套的数组

 // Destructuring Nested Arrays
  const avengers = [
            'Natasha Romanoff', 
            ['Tony Stark', 'James Rhodes'], 
            ['Steve Rogers', 'Sam Wilson']
          ];

  // Avengers and their partners
  const [blackWidow, [ironMan, warMachine], [cap, falcon]] = avengers;

  // blackWidow = 'Natasha Romanoff'
  // ironMan = 'Tony Stark'
  // warMachine = 'James Rhodes'
  // cap = 'Steve Rogers'
  // falcon = 'Sam Wilson'

  // Output warMachine:
  warMachine;

从深层嵌套的数组中提取一个值

 // 从该数组中提取 Pepper Potts
  const avengers = [
            'Natasha Romanoff', 
            [['Tony Stark', 'Pepper Potts'], 'James Rhodes'], 
            ['Steve Rogers', 'Sam Wilson']
          ];

  // Destructure
  const [ , // 跳过 'Natasha Romanoff'
       [[ , // 跳过 'Tony Stark'
       hera // Pepper Potts 赋值给变量 'hera'
     ]]] = avengers;

  // 请注意:你也可以这样写
  // const [, [[, hera ]]] = avengers;

  // 输出 hera:
  hera;

  // hera = 'Pepper Potts'

运用rest操作符捕获所有剩余项

如果你想要获取特定的数组项,并且把剩余的项归在一个数组,那么你可以这样运用 rest操作符 来解构:

 // 通过rest操作符解构
  const avengers = ['Natasha Romanoff', 'Tony Stark', 'Steve Rogers'];

  const [blackWidow, ...theOthers] = avengers;

  theOthers;
  // blackWidow = 'Natasha Romanoff'
  // theOthers = ['Tony Stark', 'Steve Rogers']

  // 输出 theOthers:
  theOthers;

对象解构

对象解构就更神奇了,尤其是当你需要从一个复杂的、深层嵌套的对象中取值时,其作用更加明显。重申一下,对象解构与数组解构用的是同样的规则(即在赋值运算符左侧创建一个 对象模式, 使它的变量位置与 = 右侧对象的值位置相匹配)。

在对象解构中,你需要指明那些需要被提取值的属性名称,以及将要被赋值的变量名。跟数组解构一样,我们需要在赋值运算符左边先创建一个对象模式来映射被解构的对象。

尽管在这种情况下,我们想要提取的是 对象属性的值 (如:我们从 { prop: value } 中提取 value)。相应地,我们的对象模式必须有一个变量,这个变量的位置要跟我们即将提取的属性值所在的位置一致。

简单示例

提取一个简单的对象属性值

我们可以这样做,来将对象 { ironMan: 'Tony Stark' } 的属性 ironMan 的值 'Tony Stark' 赋值给变量 a:

 //解构对象的属性值,赋给单个变量 `a`:
 const { ironMan: a } = { ironMan: 'Tony Stark' };

 // 输出 a:
 a;  // a = 'Tony Stark '

提取多个属性值

我们只要拓展相同的模式,就可以从一个对象中提取多个属性值,如下:

 // Setup our object
 const avengers = {
  ironMan: 'Tony Stark', 
  cap: 'Steve Rogers', 
  blackWidow: 'Natasha Romanoff'
 };

 // Destructure object to individual variables
 const { 
  ironMan: a, 
  cap: b, 
  blackWidow: c 
 } = avengers;

 // a = 'Tony Stark '
 // b = 'Steve Rogers'
 // c ='Natasha Romanoff'

 // Output a:
 a;

观察一下这个解构模式是怎么确切地匹配 被解构对象 的。

嵌套的对象解构

像解构嵌套数组一样,我们可以对嵌套对象进行解构,不管它的层级多深。

 // Setup our object
 const avengers = {
  blackWidow: 'Natasha Romanoff',
  ironManCharacters: {
   couple: {
    ironMan: 'Tony Stark', 
    hera: 'Pepper Potts',
    },
    partner: {
       warMachine: 'James Brodie'
    }
  },
  capCharacters: {
   cap: 'Steve Rogers', 
   partner: {
    falcon: 'Sam Wilson'
   }
  }
 };

 // Destructure object to individual variables
 const { 
  blackWidow: a,
  ironManCharacters: { 
   couple: {
    ironMan: b,
    hera: c
  },
   partner: {
    warMachine: d
   }
  },
  capCharacters: {
   cap: e,
   partner: {
    falcon: f
   }
  }
 } = avengers;

 // a = 'Natasha Romanoff'
 // b = 'Tony Stark '
 // c = 'Pepper Potts'
 // d = 'James Brodie'
 // e = 'Steve Rogers'
 // f = 'Sam Wilson'

 // Output a:
 a;

给赋值的变量命名

当然,把变量名设为诸如 a, b, c 之类,是很糟糕的,变量名称应该是有意义的。

冗长式命名

 // Setup our object
 const avengers = {
  ironMan: 'Tony Stark', 
  cap: 'Steve Rogers', 
  blackWidow: 'Natasha Romanoff'
 };

 // Destructure object to individual variables with meaningful names
 const { 
  ironMan: ironMan,
  cap: cap, 
  blackWidow: blackWidow
 } = avengers;

 // blackWidow = 'Natasha Romanoff'
 // ironMan = 'Tony Stark '
 // cap = 'Steve Rogers'

 // Output blackWidow:
 blackWidow;

这种做法比上面用 a,b,c 命名好,但是仍然可以完善。 { ironMan: ironMan } 看起来有点丑而且不直观。

语法上命名捷径

如果你要把一个对象的属性值赋给一个变量,该变量的名称跟对象的属性名称一样,那么在 = 左侧的赋值模式里面,你只需要简单地写属性名即可,如下:

 // Setup our object
 const avenger = {
  ironMan: 'Tony Stark'
 };

 // Destructure object to individual variables with meaningful names
 const { 
  ironMan  // equivalent to 'ironMan: ironMan'
 } = avenger;

 // ironMan = 'Tony Stark '

 // Output ironMan:
 ironMan;

由于 被解构的对象属性名称 跟 被赋值的变量名称 相同,我们只需要把名称列出来一次即可。

语法简洁

我们稍微重新修整下前面的代码,就可以使它们看起来更加简洁明了:

 // Setup our object
 const avengers = {
  ironMan: 'Tony Stark', 
  cap: 'Steve Rogers', 
  blackWidow: 'Natasha Romanoff'
 };

 // Destructure object to individual variables with meaningful names
 const { ironMan, cap, blackWidow } = avengers;

 // Output ironMan:
 ironMan;

从对象中提取一个深层嵌套的属性

当我们要提取一个深层嵌套的对象属性时,事情就更有趣了:

 // Setup our object
const avengers = {
  blackWidow: 'Natasha Romanoff',
  ironManCharacters: {
   couple: {
     ironMan: 'Tony Stark',
     hera: 'Pepper Potts',
   },
   partner: {
     warMachine: 'James Brodie'
   }
  },
  capCharacters: {
   cap: 'Steve Rogers',
   partner: {
     falcon: 'Sam Wilson'
   }
  }
};

// Destructure a deeply nested object
const { ironManCharacters: { couple } } = avengers;

// couple = {
//  ironMan: 'Tony Stark', 
//  hera: 'Pepper Potts',
// }

// Output couple:
couple;

等等,你是怎么阅读这段代码的?couple 这个变量又是怎么被定义的呢?

通过这样拆分,我们就可以看出赋值运算符 = 左侧是被解构对象的一个映射:

 const avengers = {
  ironManCharacters: {
   couple: {
     ironMan: 'Tony Stark', 
     hera: 'Pepper Potts',
   }
  }
};

const { 
  ironManCharacters: { 
   couple 
  }
} = avengers;

// Output couple:
couple;

仅仅使用 const { couple } = avengers; 并没有办法提取出 couple 的值。只有把要提取的对象属性的 位置和名称映射出来,JS 编译器才能得到相应的信息,沿着对象的所有属性往下查找,并准确地提取我们想要的值。

这里也要注意到 couple 用了语法捷径给变量命名,实际上是这样的:

const { 
  ironManCharacters: { 
   couple: couple
  }
} = avengers;

couple 就是这样被定义的,它的值就是对象 avengers 中属性名为 couple 的值。

给对象的属性解构赋值

到目前为止,我们都是解构对象的值来给单个的变量赋值,其实还可以给另一个对象的属性赋值。

 const avengers = {
 blackWidow: 'Natasha Romanoff',
 ironManCharacters: {
  couple: {
   ironMan: 'Tony Stark',
   hera: 'Pepper Potts'
  }
 }
};

const ironManProperties = {
 family: {}
};

({
 ironManCharacters: {
  couple: ironManProperties.family
 }
} = avengers);

ironManProperties.family
// ironManProperties.family = {
//  ironMan: 'Tony Stark',
//  hera: 'Pepper Potts'
// }

// Output ironManProperties.family:
ironManProperties.family;

在这里我们把 ironManCharacters.couple 的值赋给了 ironManProperties.family 这个属性,这里有两点需要说明一下:

1.解构赋值必须被包含在 圆括号 内

当我们在对一个已存在的变量(如上面例子中的 ironManProperties)进行解构时,一定要这样做,而不是去声明一个新的变量。

2.模式仍然相匹配

{ ironManCharacters: { couple... } } 与对象 avengers 中的 ironManCharacters 相匹配。这样就能如你所愿,从 avengers 对象中提取出 ironManCharacters.couple 的值了。但是现在,couple 后面放置了一个新的对象ironManProperties 和它的属性 family,其实被赋值的就是这个对象的属性ironManProperties.family了。

当你尝试把这种情况解释清楚时,是否还有所困惑呢?在jsfiddle里面尝试上面的代码,一切就明了了。

如果你不清楚自己为什么要这样做,请参考下一篇文章的例子。这些例子会告诉你,为什么采用这种模式来解构API调用的 JSON 对象,让你领略解构的神奇之处!

默认值

解构时,你还可以给变量指定一个默认值:

 // Setup our object
 const avengers = {
  ironMan: 'Tony Stark', 
  cap: 'Steve Rogers', 
  blackWidow: 'Natasha Romanoff'
 };

 // Destructure using defaults
 const { ironMan, cap, blackWidow, theHulk='Bruce Banner' } = avengers;

 // ironMan = 'Tony Stark' 
 // cap = 'Steve Rogers'
 // blackWidow = 'Natasha Romanoff'
 // theHulk = 'Bruce Banner'

 // Output blackWidow:
 blackWidow;

解构时要避免出现这些问题

解构赋值时没有使用 const, let, var

在讲到对 对象属性 进行解构赋值时就已经提及了这一点,但这里还是有必要再重申一下,让大家有个深刻的印象。

不能对已经声明的变量进行解构

也就是说,你只能在对变量解构赋值的同时声明变量。

 // Setup our object
  const avengers = {
   ironMan: 'Tony Stark', 
   cap: 'Steve Rogers', 
   blackWidow: 'Natasha Romanoff',
   theHulk: 'Bruce Banner'
  };

  // Valid destructuring
  const { ironMan } = avengers;

  let { cap } = avengers;

  var { blackWidow } = avengers;

  // Invalid destructuring
  let theHulk;

  { theHulk } = avengers;
  // Error

  // Output theHulk:
  theHulk;

为何不能对一个已经声明的变量进行解构呢?那是因为这时如果你使用了花括号 { ,JavaScript会认为你是在声明一个 block

解决的办法就是把整个解构赋值用一对 圆括号 括起来。

如何对一个已声明的变量进行解构赋值

 // Setup our object
  const avengers = {
   ironMan: 'Tony Stark', 
   cap: 'Steve Rogers', 
   blackWidow: 'Natasha Romanoff',
   theHulk: 'Bruce Banner'
  };

  // A valid Hulk
  let theHulk;

  ({ theHulk } = avengers);
  // theHulk = 'Bruce Banner'

  // Output theHulk:
  theHulk;

现在我们不是以花括号开头,所以JS不会认为我们是在声明一个 block ,这样就可以达到预期的解构结果。

直接返回一个被解构的值

在没有先声明一个接下来要被返回的变量时,就直接返回一个被解构的值,这样是无法达到预期效果的。例如,下面的代码中,返回的将是整个 ironMan对象,而不是预期要的值 Tony Stark

 // Note: this doesn't work!
 function getTonyStark(avengers){
  return { ironMan: { realName } } = avengers;
  // return the avengers object, not the realName value
 }

 const avengers = {
  ironMan: {
   realName: 'Tony Stark'
  }
 };

 const tonyStark = getTonyStark(avengers);

 // tonyStark = {
 //  ironMan: {
 //   realName: 'Tony Stark'
 //  }
 // };

 // Output tonyStark:
 tonyStark;

要从一个被解构的对象中提取值,必须先把它赋值给一个变量,然后再把这个变量返回,如下代码所示:

 // Note: this DOES work!
 function getTonyStark(avengers){
  const { ironMan: { realName } } = avengers;
  return realName;
 }

 const avengers = {
  ironMan: {
   realName: 'Tony Stark'
  }
 };

 const tonyStark = getTonyStark(avengers);

 // tonyStark = 'Tony Stark'

 // Output tonyStark:
 tonyStark;

这种把赋值和返回分成两行代码的做法实在惹人厌烦,代码丑陋,也显得没必要。但很不幸,JavaScript就是这样工作的----你必须先把解构的值赋给一个变量,然后再把它返回,两步必须分开做。

但是,没有说我们只是说分开做,并没有说一定要摆成两行代码,所以像下面这样写成一行,也是能达到预期效果的:

 function getTonyStark(avengers){
  return ({ ironMan: { realName } } = avengers) && realName;
 }

 const avengers = {
  ironMan: {
   realName: 'Tony Stark'
  }
 };

 const tonyStark = getTonyStark(avengers);
 // tonyStark = 'Tony Stark'

 // Output tonyStark:
 tonyStark;

由于JavaScript的 _short-circuit_ 逻辑操作符 (&& and ||) 会基于第一个操作数的值来返回第二个操作数的值,所以这种写法能够达到预期效果。这里,第一个操作数是解构赋值表达式,把值赋给 realName。而 realName 也就是第二个操作数,所以它的值最终被返回。

这样做不是最佳的,但是能实现。在追求代码简短的同时,一定要注意代码的可读性。

总结
本文深入讲解了 解构赋值 的主要原则。解构不仅能减少你的代码量,还能从根本上改变你的编码方式。用的越多,你就会发现越多塑造数据和函数的方式,这些实现方式在过去几乎是不可能的。希望本文对大家学习ES6有所帮助。