ES6 变量声明与赋值:值传递、浅拷贝与深拷贝详解

ES6 变量声明与赋值:值传递、浅拷贝与深拷贝详解概括于笔者的现代 JavaScript 开发:语法基础与实践技巧系列文章。本文首先介绍 ES6 中经常使用的三种变量声明方式,而后讨论了 JavaScript 按值传递的特性,最后介绍了复合类型拷贝的技巧;有兴趣的能够阅读下一章节 ES6 变量做用域与提高:变量的生命周期详解javascript

变量声明与赋值

ES6 为咱们引入了 let 与 const 两种新的变量声明关键字,同时也引入了块做用域;本文首先介绍 ES6 中经常使用的三种变量声明方式,而后讨论了 JavaScript 按值传递的特性以及多种的赋值方式,最后介绍了复合类型拷贝的技巧。java

变量声明

在 JavaScript 中,基本的变量声明能够用 var 方式;JavaScript 容许省略 var,直接对未声明的变量赋值。也就是说,var a = 1a = 1,这两条语句的效果相同。可是因为这样的作法很容易不知不觉地建立全局变量(尤为是在函数内部),因此建议老是使用 var 命令声明变量。在 ES6 中,对于变量声明的方式进行了扩展,引入了 let 与 const。var 与 let 两个关键字建立变量的区别在于, var 声明的变量做用域是最近的函数块;而 let 声明的变量做用域是最近的闭合块,每每会小于函数块。另外一方面,以 let 关键字建立的变量虽然一样被提高到做用域头部,可是并不能在实际声明前使用;若是强行使用则会抛出 ReferenceError 异常。node

var

var 是 JavaScript 中基础的变量声明方式之一,其基本语法为:es6

var x; // Declaration and initialization
x = "Hello World"; // Assignment

// Or all in one
var y = "Hello World";复制代码

ECMAScript 6 之前咱们在 JavaScript 中并无其余的变量声明方式,以 var 声明的变量做用于函数做用域中,若是没有相应的闭合函数做用域,那么该变量会被当作默认的全局变量进行处理。ajax

function sayHello(){
  var hello = "Hello World";
  return hello;
}
console.log(hello);复制代码

像如上这种调用方式会抛出异常: ReferenceError: hello is not defined,由于 hello 变量只能做用于 sayHello 函数中,不过若是按照以下先声明全局变量方式再使用时,其就可以正常调用:spring

var hello = "Hello World";
function sayHello(){
  return hello;
}
console.log(hello);复制代码

let

在 ECMAScript 6 中咱们可使用 let 关键字进行变量声明:express

let x; // Declaration and initialization
x = "Hello World"; // Assignment

// Or all in one
let y = "Hello World";复制代码

let 关键字声明的变量是属于块做用域,也就是包含在 {} 以内的做用于。使用 let 关键字的优点在于可以下降偶然的错误的几率,由于其保证了每一个变量只能在最小的做用域内进行访问。json

var name = "Peter";
if(name === "Peter"){
  let hello = "Hello Peter";
} else {
  let hello = "Hi";
}
console.log(hello);复制代码

上述代码一样会抛出 ReferenceError: hello is not defined 异常,由于 hello 只可以在闭合的块做用域中进行访问,咱们能够进行以下修改:数组

var name = "Peter";
if(name === "Peter"){
  let hello = "Hello Peter";
  console.log(hello);
} else {
  let hello = "Hi";
  console.log(hello);
}复制代码

咱们能够利用这种块级做用域的特性来避免闭包中由于变量保留而致使的问题,譬如以下两种异步代码,使用 var 时每次循环中使用的都是相同变量;而使用 let 声明的 i 则会在每次循环时进行不一样的绑定,即每次循环中闭包捕获的都是不一样的 i 实例:bash

for(let i = 0;i < 2; i++){
        setTimeout(()=>{console.log(`i:${i}`)},0);
}

for(var j = 0;j < 2; j++){
        setTimeout(()=>{console.log(`j:${j}`)},0);
}

let k = 0;
for(k = 0;k < 2; k++){
        setTimeout(()=>{console.log(`k:${k}`)},0);
}

// output
i:0
i:1
j:2
j:2
k:2
k:2复制代码

const

const 关键字通常用于常量声明,用 const 关键字声明的常量须要在声明时进行初始化而且不能够再进行修改,而且 const 关键字声明的常量被限制于块级做用域中进行访问。

function f() {
  {
    let x;
    {
      // okay, block scoped name
      const x = "sneaky";
      // error, const
      x = "foo";
    }
    // error, already declared in block
    let x = "inner";
  }
}复制代码

JavaScript 中 const 关键字的表现于 C 中存在着必定差别,譬以下述使用方式在 JavaScript 中就是正确的,而在 C 中则抛出异常:

# JavaScript
const numbers = [1, 2, 3, 4, 6]
numbers[4] = 5
console.log(numbers[4]) // print 5 

# C
const int numbers[] = {1, 2, 3, 4, 6};
numbers[4] = 5; // error: read-only variable is not assignable
printf("%d\n", numbers[4]);复制代码

从上述对比咱们也能够看出,JavaScript 中 const 限制的并不是值不可变性;而是建立了不可变的绑定,即对于某个值的只读引用,而且禁止了对于该引用的重赋值,即以下的代码会触发错误:

const numbers = [1, 2, 3, 4, 6]
numbers = [7, 8, 9, 10, 11] // error: assignment to constant variable
console.log(numbers[4])复制代码

咱们能够参考以下图片理解这种机制,每一个变量标识符都会关联某个存放变量实际值的物理地址;所谓只读的变量便是该变量标识符不能够被从新赋值,而该变量指向的值仍是可变的。

JavaScript 中存在着所谓的原始类型与复合类型,使用 const 声明的原始类型是值不可变的:

# Example 1
const a = 10
a = a + 1 // error: assignment to constant variable
# Example 2
const isTrue = true
isTrue = false // error: assignment to constant variable
# Example 3
const sLower = 'hello world'
const sUpper = sLower.toUpperCase() // create a new string
console.log(sLower) // print hello world
console.log(sUpper) // print HELLO WORLD复制代码

而若是咱们但愿将某个对象一样变成不可变类型,则须要使用 Object.freeze();不过该方法仅对于键值对的 Object 起做用,而没法做用于 Date、Map 与 Set 等类型:

# Example 4
const me = Object.freeze({name: “Jacopo”})
me.age = 28
console.log(me.age) // print undefined
# Example 5
const arr = Object.freeze([-1, 1, 2, 3])
arr[0] = 0
console.log(arr[0]) // print -1
# Example 6
const me = Object.freeze({
  name: 'Jacopo', 
  pet: {
    type: 'dog',
    name: 'Spock'
  }
})
me.pet.name = 'Rocky'
me.pet.breed = 'German Shepherd'
console.log(me.pet.name) // print Rocky
console.log(me.pet.breed) // print German Shepherd复制代码

即便是 Object.freeze() 也只能防止顶层属性被修改,而没法限制对于嵌套属性的修改,这一点咱们会在下文的浅拷贝与深拷贝部分继续讨论。

变量赋值

按值传递

JavaScript 中永远是按值传递(pass-by-value),只不过当咱们传递的是某个对象的引用时,这里的值指的是对象的引用。按值传递中函数的形参是被调用时所传实参的副本。修改形参的值并不会影响实参。而按引用传递(pass-by-reference)时,函数的形参接收实参的隐式引用,而再也不是副本。这意味着函数形参的值若是被修改,实参也会被修改。同时二者指向相同的值。咱们首先看下 C 中按值传递与引用传递的区别:

void Modify(int p, int * q)
{
    p = 27; // 按值传递 - p是实参a的副本, 只有p被修改
    *q = 27; // q是b的引用,q和b都被修改
}
int main()
{
    int a = 1;
    int b = 1;
    Modify(a, &b);   // a 按值传递, b 按引用传递,
                     // a 未变化, b 改变了
    return(0);
}复制代码

而在 JavaScript 中,对比例子以下:

function changeStuff(a, b, c)
{
  a = a * 10;
  b.item = "changed";
  c = {item: "changed"};
}

var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};

changeStuff(num, obj1, obj2);

console.log(num);
console.log(obj1.item);    
console.log(obj2.item);

// 输出结果
10
changed
unchanged复制代码

JavaScript 按值传递就表现于在内部修改了 c 的值可是并不会影响到外部的 obj2 变量。若是咱们更深刻地来理解这个问题,JavaScript 对于对象的传递则是按共享传递的(pass-by-sharing,也叫按对象传递、按对象共享传递)。最先由Barbara Liskov. 在1974年的GLU语言中提出;该求值策略被用于Python、Java、Ruby、JS等多种语言。该策略的重点是:调用函数传参时,函数接受对象实参引用的副本(既不是按值传递的对象副本,也不是按引用传递的隐式引用)。 它和按引用传递的不一样在于:在共享传递中对函数形参的赋值,不会影响实参的值。按共享传递的直接表现就是上述代码中的 obj1,当咱们在函数内修改了 b 指向的对象的属性值时,咱们使用 obj1 来访问相同的变量时一样会获得变化后的值。

连续赋值

JavaScript 中是支持变量的连续赋值,即譬如:

var a=b=1;复制代码

可是在连续赋值中,会发生引用保留,能够考虑以下情景:

var a = {n:1};  
a.x = a = {n:2};  
alert(a.x); // --> undefined复制代码

为了解释上述问题,咱们引入一个新的变量:

var a = {n:1};  
var b = a; // 持有a,以回查  
a.x = a = {n:2};  
alert(a.x);// --> undefined  
alert(b.x);// --> [object Object]复制代码

实际上在连续赋值中,值是直接赋予给变量指向的内存地址:

a.x  =  a  = {n:2}
              │      │
      {n:1}<──┘      └─>{n:2}复制代码

Deconstruction: 解构赋值

解构赋值容许你使用相似数组或对象字面量的语法将数组和对象的属性赋给各类变量。这种赋值语法极度简洁,同时还比传统的属性访问方法更为清晰。传统的访问数组前三个元素的方式为:

var first = someArray[0];
    var second = someArray[1];
    var third = someArray[2];复制代码

而经过解构赋值的特性,能够变为:

var [first, second, third] = someArray;复制代码
// === Arrays

var [a, b] = [1, 2];
console.log(a, b);
//=> 1 2


// Use from functions, only select from pattern
var foo = () => {
  return [1, 2, 3];
};

var [a, b] = foo();
console.log(a, b);
// => 1 2


// Omit certain values
var [a, , b] = [1, 2, 3];
console.log(a, b);
// => 1 3


// Combine with spread/rest operator (accumulates the rest of the values)
var [a, ...b] = [1, 2, 3];
console.log(a, b);
// => 1 [ 2, 3 ]


// Fail-safe.
var [, , , a, b] = [1, 2, 3];
console.log(a, b);
// => undefined undefined


// Swap variables easily without temp
var a = 1, b = 2;
[b, a] = [a, b];
console.log(a, b);
// => 2 1


// Advance deep arrays
var [a, [b, [c, d]]] = [1, [2, [[[3, 4], 5], 6]]];
console.log("a:", a, "b:", b, "c:", c, "d:", d);
// => a: 1 b: 2 c: [ [ 3, 4 ], 5 ] d: 6


// === Objects

var {user: x} = {user: 5};
console.log(x);
// => 5


// Fail-safe
var {user: x} = {user2: 5};
console.log(x);
// => undefined


// More values
var {prop: x, prop2: y} = {prop: 5, prop2: 10};
console.log(x, y);
// => 5 10

// Short-hand syntax
var { prop, prop2} = {prop: 5, prop2: 10};
console.log(prop, prop2);
// => 5 10

// Equal to:
var { prop: prop, prop2: prop2} = {prop: 5, prop2: 10};
console.log(prop, prop2);
// => 5 10

// Oops: This doesn't work: var a, b; { a, b } = {a: 1, b: 2}; // But this does work var a, b; ({ a, b } = {a: 1, b: 2}); console.log(a, b); // => 1 2 // This due to the grammar in JS. // Starting with { implies a block scope, not an object literal. // () converts to an expression. // From Harmony Wiki: // Note that object literals cannot appear in // statement positions, so a plain object // destructuring assignment statement // { x } = y must be parenthesized either // as ({ x } = y) or ({ x }) = y. // Combine objects and arrays var {prop: x, prop2: [, y]} = {prop: 5, prop2: [10, 100]}; console.log(x, y); // => 5 100 // Deep objects var { prop: x, prop2: { prop2: { nested: [ , , b] } } } = { prop: "Hello", prop2: { prop2: { nested: ["a", "b", "c"]}}}; console.log(x, b); // => Hello c // === Combining all to make fun happen // All well and good, can we do more? Yes! // Using as method parameters var foo = function ({prop: x}) { console.log(x); }; foo({invalid: 1}); foo({prop: 1}); // => undefined // => 1 // Can also use with the advanced example var foo = function ({ prop: x, prop2: { prop2: { nested: b } } }) { console.log(x, ...b); }; foo({ prop: "Hello", prop2: { prop2: { nested: ["a", "b", "c"]}}}); // => Hello a b c // In combination with other ES2015 features. // Computed property names const name = 'fieldName'; const computedObject = { [name]: name }; // (where object is { 'fieldName': 'fieldName' }) const { [name]: nameValue } = computedObject; console.log(nameValue) // => fieldName // Rest and defaults var ajax = function ({ url = "localhost", port: p = 80}, ...data) { console.log("Url:", url, "Port:", p, "Rest:", data); }; ajax({ url: "someHost" }, "additional", "data", "hello"); // => Url: someHost Port: 80 Rest: [ 'additional', 'data', 'hello' ] ajax({ }, "additional", "data", "hello"); // => Url: localhost Port: 80 Rest: [ 'additional', 'data', 'hello' ] // Ooops: Doesn't work (in traceur)
var ajax = ({ url = "localhost", port: p = 80}, ...data) => {
  console.log("Url:", url, "Port:", p, "Rest:", data);
};
ajax({ }, "additional", "data", "hello");
// probably due to traceur compiler

But this does:
var ajax = ({ url: url = "localhost", port: p = 80}, ...data) => {
  console.log("Url:", url, "Port:", p, "Rest:", data);
};
ajax({ }, "additional", "data", "hello");


// Like _.pluck
var users = [
  { user: "Name1" },
  { user: "Name2" },
  { user: "Name2" },
  { user: "Name3" }
];
var names = users.map( ({ user }) => user );
console.log(names);
// => [ 'Name1', 'Name2', 'Name2', 'Name3' ]


// Advanced usage with Array Comprehension and default values
var users = [
  { user: "Name1" },
  { user: "Name2", age: 2 },
  { user: "Name2" },
  { user: "Name3", age: 4 }
];

[for ({ user, age = "DEFAULT AGE" } of users) console.log(user, age)];
// => Name1 DEFAULT AGE
// => Name2 2
// => Name2 DEFAULT AGE
// => Name3 4复制代码

数组与迭代器

以上是数组解构赋值的一个简单示例,其语法的通常形式为:

[ variable1, variable2, ..., variableN ] = array;复制代码

这将为variable1到variableN的变量赋予数组中相应元素项的值。若是你想在赋值的同时声明变量,可在赋值语句前加入varletconst关键字,例如:

var [ variable1, variable2, ..., variableN ] = array;
    let [ variable1, variable2, ..., variableN ] = array;
    const [ variable1, variable2, ..., variableN ] = array;复制代码

事实上,用变量来描述并不恰当,由于你能够对任意深度的嵌套数组进行解构:

var [foo, [[bar], baz]] = [1, [[2], 3]];
    console.log(foo);
    // 1
    console.log(bar);
    // 2
    console.log(baz);
    // 3复制代码

此外,你能够在对应位留空来跳过被解构数组中的某些元素:

var [,,third] = ["foo", "bar", "baz"];
    console.log(third);
    // "baz"复制代码

并且你还能够经过“不定参数”模式捕获数组中的全部尾随元素:

var [head, ...tail] = [1, 2, 3, 4];
    console.log(tail);
    // [2, 3, 4]复制代码

当访问空数组或越界访问数组时,对其解构与对其索引的行为一致,最终获得的结果都是:undefined

console.log([][0]);
    // undefined
    var [missing] = [];
    console.log(missing);
    // undefined复制代码

请注意,数组解构赋值的模式一样适用于任意迭代器:

function* fibs() {
      var a = 0;
      var b = 1;
      while (true) {
        yield a;
        [a, b] = [b, a + b];
      }
    }
    var [first, second, third, fourth, fifth, sixth] = fibs();
    console.log(sixth);
    // 5复制代码

对象

经过解构对象,你能够把它的每一个属性与不一样的变量绑定,首先指定被绑定的属性,而后紧跟一个要解构的变量。

var robotA = { name: "Bender" };
    var robotB = { name: "Flexo" };
    var { name: nameA } = robotA;
    var { name: nameB } = robotB;
    console.log(nameA);
    // "Bender"
    console.log(nameB);
    // "Flexo"复制代码

当属性名与变量名一致时,能够经过一种实用的句法简写:

var { foo, bar } = { foo: "lorem", bar: "ipsum" };
    console.log(foo);
    // "lorem"
    console.log(bar);
    // "ipsum"复制代码

与数组解构同样,你能够随意嵌套并进一步组合对象解构:

var complicatedObj = {
      arrayProp: [
        "Zapp",
        { second: "Brannigan" }
      ]
    };
    var { arrayProp: [first, { second }] } = complicatedObj;
    console.log(first);
    // "Zapp"
    console.log(second);
    // "Brannigan"复制代码

当你解构一个未定义的属性时,获得的值为undefined

var { missing } = {};
    console.log(missing);
    // undefined复制代码

请注意,当你解构对象并赋值给变量时,若是你已经声明或不打算声明这些变量(亦即赋值语句前没有letconstvar关键字),你应该注意这样一个潜在的语法错误:

{ blowUp } = { blowUp: 10 };
    // Syntax error 语法错误复制代码

为何会出错?这是由于JavaScript语法通知解析引擎将任何以{开始的语句解析为一个块语句(例如,{console}是一个合法块语句)。解决方案是将整个表达式用一对小括号包裹:

({ safe } = {});
    // No errors 没有语法错误复制代码

默认值

当你要解构的属性未定义时你能够提供一个默认值:

var [missing = true] = [];
    console.log(missing);
    // true
    var { message: msg = "Something went wrong" } = {};
    console.log(msg);
    // "Something went wrong"
    var { x = 3 } = {};
    console.log(x);
    // 3复制代码

因为解构中容许对对象进行解构,而且还支持默认值,那么彻底能够将解构应用在函数参数以及参数的默认值中。

function removeBreakpoint({ url, line, column }) {
      // ...
    }复制代码

当咱们构造一个提供配置的对象,而且须要这个对象的属性携带默认值时,解构特性就派上用场了。举个例子,jQuery的ajax函数使用一个配置对象做为它的第二参数,咱们能够这样重写函数定义:

jQuery.ajax = function (url, { async = true, beforeSend = noop, cache = true, complete = noop, crossDomain = false, global = true, // ... 更多配置 }) {
      // ... do stuff
    };复制代码

一样,解构也能够应用在函数的多重返回值中,能够相似于其余语言中的元组的特性:

function returnMultipleValues() {
      return [1, 2];
    }
var [foo, bar] = returnMultipleValues();复制代码

Three Dots

Rest Operator

在 JavaScript 函数调用时咱们每每会使用内置的 arguments 对象来获取函数的调用参数,不过这种方式却存在着不少的不方便性。譬如 arguments 对象是 Array-Like 对象,没法直接运用数组的 .map() 或者 .forEach() 函数;而且由于 arguments 是绑定于当前函数做用域,若是咱们但愿在嵌套函数里使用外层函数的 arguments 对象,咱们还须要建立中间变量。

function outerFunction() {  
   // store arguments into a separated variable
   var argsOuter = arguments;
   function innerFunction() {
      // args is an array-like object
      var even = Array.prototype.map.call(argsOuter, function(item) {
         // do something with argsOuter               
      });
   }
}复制代码

ES6 中为咱们提供了 Rest Operator 来以数组形式获取函数的调用参数,Rest Operator 也能够用于在解构赋值中以数组方式获取剩余的变量:

function countArguments(...args) {  
   return args.length;
}
// get the number of arguments
countArguments('welcome', 'to', 'Earth'); // => 3  
// destructure an array
let otherSeasons, autumn;  
[autumn, ...otherSeasons] = cold;
otherSeasons      // => ['winter']复制代码

典型的 Rest Operator 的应用场景譬如进行不定数组的指定类型过滤:

function filter(type, ...items) {  
  return items.filter(item => typeof item === type);
}
filter('boolean', true, 0, false);        // => [true, false]  
filter('number', false, 4, 'Welcome', 7); // => [4, 7]复制代码

尽管 Arrow Function 中并无定义 arguments 对象,可是咱们仍然可使用 Rest Operator 来获取 Arrow Function 的调用参数:

(function() {
  let outerArguments = arguments;
  const concat = (...items) => {
    console.log(arguments === outerArguments); // => true
    return items.reduce((result, item) => result + item, '');
  };
  concat(1, 5, 'nine'); // => '15nine'
})();复制代码

Spread Operator

Spread Operator 则与 Rest Opeator 的功能正好相反,其经常使用于进行数组构建与解构赋值,也能够用于将某个数组转化为函数的参数列表,其基本使用方式以下:

let cold = ['autumn', 'winter'];  
let warm = ['spring', 'summer'];  
// construct an array
[...cold, ...warm] // => ['autumn', 'winter', 'spring', 'summer']
// function arguments from an array
cold.push(...warm);  
cold              // => ['autumn', 'winter', 'spring', 'summer']复制代码

咱们也可使用 Spread Operator 来简化函数调用:

class King {  
   constructor(name, country) {
     this.name = name;
     this.country = country;     
   }
   getDescription() {
     return `${this.name} leads ${this.country}`;
   }
}
var details = ['Alexander the Great', 'Greece'];  
var Alexander = new King(...details);  
Alexander.getDescription(); // => 'Alexander the Great leads Greece'复制代码

还有另一个好处就是能够用来替换 Object.assign 来方便地从旧有的对象中建立新的对象,而且可以修改部分值;譬如:

var obj = {a:1,b:2}
var obj_new_1 = Object.assign({},obj,{a:3});
var obj_new_2 = {
  ...obj,
  a:3
}复制代码

最后咱们还须要讨论下 Spread Operator 与 Iteration Protocols,实际上 Spread Operator 也是使用的 Iteration Protocols 来进行元素遍历与结果搜集;所以咱们也能够经过自定义 Iterator 的方式来控制 Spread Operator 的表现。Iterable 协议规定了对象必须包含 Symbol.iterator 方法,该方法返回某个 Iterator 对象:

interface Iterable {  
  [Symbol.iterator]() {
    //...
    return Iterator;
  }
}复制代码

该 Iterator 对象从属于 Iterator Protocol,其须要提供 next 成员方法,该方法会返回某个包含 done 与 value 属性的对象:

interface Iterator {  
  next() {
     //...
     return {
        value: <value>,
        done: <boolean>
     };
  };
}复制代码

典型的 Iterable 对象就是字符串:

var str = 'hi';  
var iterator = str[Symbol.iterator]();  
iterator.toString(); // => '[object String Iterator]'  
iterator.next();     // => { value: 'h', done: false }  
iterator.next();     // => { value: 'i', done: false }  
iterator.next();     // => { value: undefined, done: true }  
[...str];            // => ['h', 'i']复制代码

咱们能够经过自定义 array-like 对象的 Symbol.iterator 属性来控制其在迭代器上的效果:

function iterator() {  
  var index = 0;
  return {
    next: () => ({ // Conform to Iterator protocol
      done : index >= this.length,
      value: this[index++]
    })
  };
}
var arrayLike = {  
  0: 'Cat',
  1: 'Bird',
  length: 2
};
// Conform to Iterable Protocol
arrayLike[Symbol.iterator] = iterator;  
var array = [...arrayLike];  
console.log(array); // => ['Cat', 'Bird']复制代码

arrayLike[Symbol.iterator] 为该对象建立了值为某个迭代器的属性,从而使该对象符合了 Iterable 协议;而 iterator() 又返回了包含 next 成员方法的对象,使得该对象最终具备和数组类似的行为表现。

Copy Composite Data Types: 复合类型的拷贝

Shallow Copy: 浅拷贝

顶层属性遍历

浅拷贝是指复制对象的时候,指对第一层键值对进行独立的复制。一个简单的实现以下:

// 浅拷贝实现
function shadowCopy(target, source){ 
    if( !source || typeof source !== 'object'){
        return;
    }
    // 这个方法有点小trick,target必定得事先定义好,否则就不能改变实参了。
       // 具体缘由解释能够看参考资料中 JS是值传递仍是引用传递
    if( !target || typeof target !== 'object'){
        return;
    }  
    // 这边最好区别一下对象和数组的复制
    for(var key in source){
        if(source.hasOwnProperty(key)){
            target[key] = source[key];
        }
    }
}

//测试例子
var arr = [1,2,3];
var arr2 = [];
shadowCopy(arr2, arr);
console.log(arr2);
//[1,2,3]

var today = {
    weather: 'Sunny',
    date: {
        week: 'Wed'
    } 
}

var tomorrow = {};
shadowCopy(tomorrow, today);
console.log(tomorrow);
// Object {weather: "Sunny", date: Object}复制代码

Object.assign

Object.assign() 方法能够把任意多个的源对象所拥有的自身可枚举属性拷贝给目标对象,而后返回目标对象。Object.assign 方法只会拷贝源对象自身的而且可枚举的属性到目标对象身上。注意,对于访问器属性,该方法会执行那个访问器属性的 getter 函数,而后把获得的值拷贝给目标对象,若是你想拷贝访问器属性自己,请使用 Object.getOwnPropertyDescriptor()Object.defineProperties() 方法。

注意,字符串类型和 symbol 类型的属性都会被拷贝。

注意,在属性拷贝过程当中可能会产生异常,好比目标对象的某个只读属性和源对象的某个属性同名,这时该方法会抛出一个 TypeError 异常,拷贝过程当中断,已经拷贝成功的属性不会受到影响,还未拷贝的属性将不会再被拷贝。

注意, Object.assign 会跳过那些值为 nullundefined 的源对象。

Object.assign(target, ...sources)复制代码
  • 例子:浅拷贝一个对象
var obj = { a: 1 };
var copy = Object.assign({}, obj);
console.log(copy); // { a: 1 }复制代码
  • 例子:合并若干个对象
var o1 = { a: 1 };
var o2 = { b: 2 };
var o3 = { c: 3 };

var obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(o1);  // { a: 1, b: 2, c: 3 }, 注意目标对象自身也会改变。复制代码
  • 例子:拷贝 symbol 类型的属性
var o1 = { a: 1 };
var o2 = { [Symbol("foo")]: 2 };

var obj = Object.assign({}, o1, o2);
console.log(obj); // { a: 1, [Symbol("foo")]: 2 }复制代码
  • 例子:继承属性和不可枚举属性是不能拷贝的
var obj = Object.create({foo: 1}, { // foo 是个继承属性。
    bar: {
        value: 2  // bar 是个不可枚举属性。
    },
    baz: {
        value: 3,
        enumerable: true  // baz 是个自身可枚举属性。
    }
});

var copy = Object.assign({}, obj);
console.log(copy); // { baz: 3 }复制代码
  • 例子:原始值会被隐式转换成其包装对象
var v1 = "123";
var v2 = true;
var v3 = 10;
var v4 = Symbol("foo")

var obj = Object.assign({}, v1, null, v2, undefined, v3, v4); 
// 源对象若是是原始值,会被自动转换成它们的包装对象,
// 而 null 和 undefined 这两种原始值会被彻底忽略。
// 注意,只有字符串的包装对象才有可能有自身可枚举属性。
console.log(obj); // { "0": "1", "1": "2", "2": "3" }复制代码
  • 例子:拷贝属性过程当中发生异常
var target = Object.defineProperty({}, "foo", {
    value: 1,
    writeable: false
}); // target 的 foo 属性是个只读属性。

Object.assign(target, {bar: 2}, {foo2: 3, foo: 3, foo3: 3}, {baz: 4});
// TypeError: "foo" is read-only
// 注意这个异常是在拷贝第二个源对象的第二个属性时发生的。

console.log(target.bar);  // 2,说明第一个源对象拷贝成功了。
console.log(target.foo2); // 3,说明第二个源对象的第一个属性也拷贝成功了。
console.log(target.foo);  // 1,只读属性不能被覆盖,因此第二个源对象的第二个属性拷贝失败了。
console.log(target.foo3); // undefined,异常以后 assign 方法就退出了,第三个属性是不会被拷贝到的。
console.log(target.baz);  // undefined,第三个源对象更是不会被拷贝到的。复制代码

使用 [].concat 来复制数组

一样相似于对于对象的复制,咱们建议使用[].concat来进行数组的深复制:

var list = [1, 2, 3];
var changedList = [].concat(list);
changedList[1] = 2;
list === changedList; // false复制代码

一样的,concat方法也只能保证一层深复制:

> list = [[1,2,3]]
[ [ 1, 2, 3 ] ]
> new_list = [].concat(list)
[ [ 1, 2, 3 ] ]
> new_list[0][0] = 4
4
> list
[ [ 4, 2, 3 ] ]复制代码

浅拷贝的缺陷

不过须要注意的是,assign是浅拷贝,或者说,它是一级深拷贝,举两个例子说明:

const defaultOpt = {
    title: {
        text: 'hello world',
        subtext: 'It\'s my world.'
    }
};

const opt = Object.assign({}, defaultOpt, {
    title: {
        subtext: 'Yes, your world.'
    }
});

console.log(opt);

// 预期结果
{
    title: {
        text: 'hello world',
        subtext: 'Yes, your world.'
    }
}
// 实际结果
{
    title: {
        subtext: 'Yes, your world.'
    }
}复制代码

上面这个例子中,对于对象的一级子元素而言,只会替换引用,而不会动态的添加内容。那么,其实assign并无解决对象的引用混乱问题,参考下下面这个例子:

const defaultOpt = {
    title: {
        text: 'hello world',
        subtext: 'It\'s my world.'
    } 
};

const opt1 = Object.assign({}, defaultOpt);
const opt2 = Object.assign({}, defaultOpt);
opt2.title.subtext = 'Yes, your world.';

console.log('opt1:');
console.log(opt1);
console.log('opt2:');
console.log(opt2);

// 结果
opt1:
{
    title: {
        text: 'hello world',
        subtext: 'Yes, your world.'
    }
}
opt2:
{
    title: {
        text: 'hello world',
        subtext: 'Yes, your world.'
    }
}复制代码

DeepCopy: 深拷贝

递归属性遍历

通常来讲,在JavaScript中考虑复合类型的深层复制的时候,每每就是指对于Date、Object与Array这三个复合类型的处理。咱们能想到的最经常使用的方法就是先建立一个空的新对象,而后递归遍历旧对象,直到发现基础类型的子节点才赋予到新对象对应的位置。不过这种方法会存在一个问题,就是JavaScript中存在着神奇的原型机制,而且这个原型会在遍历的时候出现,而后原型不该该被赋予给新对象。那么在遍历的过程当中,咱们应该考虑使用hasOenProperty方法来过滤掉那些继承自原型链上的属性:

function clone(obj) {
    var copy;

    // Handle the 3 simple types, and null or undefined
    if (null == obj || "object" != typeof obj) return obj;

    // Handle Date
    if (obj instanceof Date) {
        copy = new Date();
        copy.setTime(obj.getTime());
        return copy;
    }

    // Handle Array
    if (obj instanceof Array) {
        copy = [];
        for (var i = 0, len = obj.length; i < len; i++) {
            copy[i] = clone(obj[i]);
        }
        return copy;
    }

    // Handle Object
    if (obj instanceof Object) {
        copy = {};
        for (var attr in obj) {
            if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]);
        }
        return copy;
    }

    throw new Error("Unable to copy obj! Its type isn't supported.");
}复制代码

调用以下:

// This would be cloneable:
var tree = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "right" : null,
    "data"  : 8
};

// This would kind-of work, but you would get 2 copies of the 
// inner node instead of 2 references to the same copy
var directedAcylicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
directedAcyclicGraph["right"] = directedAcyclicGraph["left"];

// Cloning this would cause a stack overflow due to infinite recursion:
var cylicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
cylicGraph["right"] = cylicGraph;复制代码

利用 JSON 深拷贝

JSON.parse(JSON.stringify(obj));复制代码

对于通常的需求是能够知足的,可是它有缺点。下例中,能够看到JSON复制会忽略掉值为undefined以及函数表达式。

var obj = {
    a: 1,
    b: 2,
    c: undefined,
    sum: function() { return a + b; }
};

var obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2);
//Object {a: 1, b: 2}复制代码

延伸阅读

相关文章
相关标签/搜索