摘要:變量聲明與賦值值傳遞淺拷貝與深拷貝詳解歸納于筆者的現代開發語法基礎與實踐技巧系列文章。變量聲明在中,基本的變量聲明可以用方式允許省略,直接對未聲明的變量賦值。按值傳遞中函數的形參是被調用時所傳實參的副本。
變量聲明與賦值ES6 變量聲明與賦值:值傳遞、淺拷貝與深拷貝詳解歸納于筆者的現代 JavaScript 開發:語法基礎與實踐技巧系列文章。本文首先介紹 ES6 中常用的三種變量聲明方式,然后討論了 JavaScript 按值傳遞的特性,最后介紹了復合類型拷貝的技巧;有興趣的可以閱讀下一章節 ES6 變量作用域與提升:變量的生命周期詳解。
ES6 為我們引入了 let 與 const 兩種新的變量聲明關鍵字,同時也引入了塊作用域;本文首先介紹 ES6 中常用的三種變量聲明方式,然后討論了 JavaScript 按值傳遞的特性以及多種的賦值方式,最后介紹了復合類型拷貝的技巧。
變量聲明在 JavaScript 中,基本的變量聲明可以用 var 方式;JavaScript 允許省略 var,直接對未聲明的變量賦值。也就是說,var a = 1 與 a = 1,這兩條語句的效果相同。但是由于這樣的做法很容易不知不覺地創建全局變量(尤其是在函數內部),所以建議總是使用 var 命令聲明變量。在 ES6 中,對于變量聲明的方式進行了擴展,引入了 let 與 const。var 與 let 兩個關鍵字創建變量的區別在于, var 聲明的變量作用域是最近的函數塊;而 let 聲明的變量作用域是最近的閉合塊,往往會小于函數塊。另一方面,以 let 關鍵字創建的變量雖然同樣被提升到作用域頭部,但是并不能在實際聲明前使用;如果強行使用則會拋出 ReferenceError 異常。
varvar 是 JavaScript 中基礎的變量聲明方式之一,其基本語法為:
var x; // Declaration and initialization x = "Hello World"; // Assignment // Or all in one var y = "Hello World";
ECMAScript 6 以前我們在 JavaScript 中并沒有其他的變量聲明方式,以 var 聲明的變量作用于函數作用域中,如果沒有相應的閉合函數作用域,那么該變量會被當做默認的全局變量進行處理。
function sayHello(){ var hello = "Hello World"; return hello; } console.log(hello);
像如上這種調用方式會拋出異常: ReferenceError: hello is not defined,因為 hello 變量只能作用于 sayHello 函數中,不過如果按照如下先聲明全局變量方式再使用時,其就能夠正常調用:
var hello = "Hello World"; function sayHello(){ return hello; } console.log(hello);let
在 ECMAScript 6 中我們可以使用 let 關鍵字進行變量聲明:
let x; // Declaration and initialization x = "Hello World"; // Assignment // Or all in one let y = "Hello World";
let 關鍵字聲明的變量是屬于塊作用域,也就是包含在 {} 之內的作用于。使用 let 關鍵字的優勢在于能夠降低偶然的錯誤的概率,因為其保證了每個變量只能在最小的作用域內進行訪問。
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 實例:
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:2const
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 ", 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的變量賦予數組中相應元素項的值。如果你想在賦值的同時聲明變量,可在賦值語句前加入var、let或const關鍵字,例如:
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
請注意,當你解構對象并賦值給變量時,如果你已經聲明或不打算聲明這些變量(亦即賦值語句前沒有let、const或var關鍵字),你應該注意這樣一個潛在的語法錯誤:
{ 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:, done: }; }; }
典型的 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 會跳過那些值為 null 或 undefined 的源對象。
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}延伸閱讀
基于 JSX 的動態數據綁定
ECMAScript 2017(ES8)特性概述
WebAssembly 初體驗:從零開始重構計算模塊
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/87285.html
摘要:淺拷貝實現方式賦值。但是進行的是淺拷貝,拷貝的是對象的屬性的引用,而不是對象本身。沒復制遞歸拷貝避免相互引用對象導致死循環,如的情況使用方法直接使用,可以達到深拷貝的效果。 js有五種基本數據類型,string,number,boolean,null,undefind。這五種類型的賦值,就是值傳遞。特殊類型對象的賦值是將對象地址的引用賦值。這時候修改對象中的屬性或者值,會導致所有引用這...
摘要:前端日報精選變量聲明與賦值值傳遞淺拷貝與深拷貝詳解淺談自適應學習比你想象的要簡單常見排序算法之實現世界萬物誕生記中文深入理解筆記與異步編程譯不可變和中的知乎專欄譯怎樣避免開發時的深坑瘋狂的技術宅在翻譯網格布局掘金詳解改變模糊度亮 2017-08-15 前端日報 精選 ES6 變量聲明與賦值:值傳遞、淺拷貝與深拷貝詳解淺談web自適應學習 React.js 比你想象的要簡單常見排序算法之...
摘要:深拷貝與淺拷貝的出現,就與這兩個數據類型有關。這時,就需要用淺拷貝來實現了。數據一但過多,就會有遞歸爆棧的風險。這個方法是在解決遞歸爆棧問題的基礎上,加以改進解決循環引用的問題。但如果你并不想保持引用,那就改用用于解決遞歸爆棧即可。 前言 這是前端面試題系列的第 9 篇,你可能錯過了前面的篇章,可以在這里找到: 數組去重(10 種濃縮版) JavaScript 中的事件機制(從原生到...
摘要:的翻譯文檔由的維護很多人說,阮老師已經有一本關于的書了入門,覺得看看這本書就足夠了。前端的異步解決方案之和異步編程模式在前端開發過程中,顯得越來越重要。為了讓編程更美好,我們就需要引入來降低異步編程的復雜性。 JavaScript Promise 迷你書(中文版) 超詳細介紹promise的gitbook,看完再不會promise...... 本書的目的是以目前還在制定中的ECMASc...
摘要:淺拷貝與深拷貝一數據類型數據分為基本數據類型,和對象數據類型。淺拷貝是按位拷貝對象,它會創建一個新對象,這個對象有著原始對象屬性值的一份精確拷貝。對于字符串數字及布爾值來說不是或者對象,會拷貝這些值到新的數組里。 淺拷貝與深拷貝 一、數據類型數據分為基本數據類型(String, Number, Boolean, Null, Undefined,Symbol)和對象數據類型。 基本數據類...
閱讀 2804·2023-04-25 18:46
閱讀 697·2021-11-19 09:40
閱讀 2063·2021-09-28 09:36
閱讀 3374·2021-09-10 11:11
閱讀 3453·2019-08-30 15:55
閱讀 1791·2019-08-30 15:54
閱讀 2589·2019-08-29 16:16
閱讀 3536·2019-08-29 15:08