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}複製代碼

延伸閱讀

相關文章
相關標籤/搜索