摘要:變量聲明與賦值值傳遞淺拷貝與深拷貝詳解歸納于筆者的現(xiàn)代開(kāi)發(fā)語(yǔ)法基礎(chǔ)與實(shí)踐技巧系列文章。變量聲明在中,基本的變量聲明可以用方式允許省略,直接對(duì)未聲明的變量賦值。按值傳遞中函數(shù)的形參是被調(diào)用時(shí)所傳實(shí)參的副本。
變量聲明與賦值ES6 變量聲明與賦值:值傳遞、淺拷貝與深拷貝詳解歸納于筆者的現(xiàn)代 JavaScript 開(kāi)發(fā):語(yǔ)法基礎(chǔ)與實(shí)踐技巧系列文章。本文首先介紹 ES6 中常用的三種變量聲明方式,然后討論了 JavaScript 按值傳遞的特性,最后介紹了復(fù)合類(lèi)型拷貝的技巧;有興趣的可以閱讀下一章節(jié) ES6 變量作用域與提升:變量的生命周期詳解。
ES6 為我們引入了 let 與 const 兩種新的變量聲明關(guān)鍵字,同時(shí)也引入了塊作用域;本文首先介紹 ES6 中常用的三種變量聲明方式,然后討論了 JavaScript 按值傳遞的特性以及多種的賦值方式,最后介紹了復(fù)合類(lèi)型拷貝的技巧。
變量聲明在 JavaScript 中,基本的變量聲明可以用 var 方式;JavaScript 允許省略 var,直接對(duì)未聲明的變量賦值。也就是說(shuō),var a = 1 與 a = 1,這兩條語(yǔ)句的效果相同。但是由于這樣的做法很容易不知不覺(jué)地創(chuàng)建全局變量(尤其是在函數(shù)內(nèi)部),所以建議總是使用 var 命令聲明變量。在 ES6 中,對(duì)于變量聲明的方式進(jìn)行了擴(kuò)展,引入了 let 與 const。var 與 let 兩個(gè)關(guān)鍵字創(chuàng)建變量的區(qū)別在于, var 聲明的變量作用域是最近的函數(shù)塊;而 let 聲明的變量作用域是最近的閉合塊,往往會(huì)小于函數(shù)塊。另一方面,以 let 關(guān)鍵字創(chuàng)建的變量雖然同樣被提升到作用域頭部,但是并不能在實(shí)際聲明前使用;如果強(qiáng)行使用則會(huì)拋出 ReferenceError 異常。
varvar 是 JavaScript 中基礎(chǔ)的變量聲明方式之一,其基本語(yǔ)法為:
var x; // Declaration and initialization x = "Hello World"; // Assignment // Or all in one var y = "Hello World";
ECMAScript 6 以前我們?cè)?JavaScript 中并沒(méi)有其他的變量聲明方式,以 var 聲明的變量作用于函數(shù)作用域中,如果沒(méi)有相應(yīng)的閉合函數(shù)作用域,那么該變量會(huì)被當(dāng)做默認(rèn)的全局變量進(jìn)行處理。
function sayHello(){ var hello = "Hello World"; return hello; } console.log(hello);
像如上這種調(diào)用方式會(huì)拋出異常: ReferenceError: hello is not defined,因?yàn)?hello 變量只能作用于 sayHello 函數(shù)中,不過(guò)如果按照如下先聲明全局變量方式再使用時(shí),其就能夠正常調(diào)用:
var hello = "Hello World"; function sayHello(){ return hello; } console.log(hello);let
在 ECMAScript 6 中我們可以使用 let 關(guān)鍵字進(jìn)行變量聲明:
let x; // Declaration and initialization x = "Hello World"; // Assignment // Or all in one let y = "Hello World";
let 關(guān)鍵字聲明的變量是屬于塊作用域,也就是包含在 {} 之內(nèi)的作用于。使用 let 關(guān)鍵字的優(yōu)勢(shì)在于能夠降低偶然的錯(cuò)誤的概率,因?yàn)槠浔WC了每個(gè)變量只能在最小的作用域內(nèi)進(jìn)行訪問(wèn)。
var name = "Peter"; if(name === "Peter"){ let hello = "Hello Peter"; } else { let hello = "Hi"; } console.log(hello);
上述代碼同樣會(huì)拋出 ReferenceError: hello is not defined 異常,因?yàn)?hello 只能夠在閉合的塊作用域中進(jìn)行訪問(wèn),我們可以進(jìn)行如下修改:
var name = "Peter"; if(name === "Peter"){ let hello = "Hello Peter"; console.log(hello); } else { let hello = "Hi"; console.log(hello); }
我們可以利用這種塊級(jí)作用域的特性來(lái)避免閉包中因?yàn)樽兞勘A舳鴮?dǎo)致的問(wèn)題,譬如如下兩種異步代碼,使用 var 時(shí)每次循環(huán)中使用的都是相同變量;而使用 let 聲明的 i 則會(huì)在每次循環(huán)時(shí)進(jìn)行不同的綁定,即每次循環(huán)中閉包捕獲的都是不同的 i 實(shí)例:
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 關(guān)鍵字一般用于常量聲明,用 const 關(guān)鍵字聲明的常量需要在聲明時(shí)進(jìn)行初始化并且不可以再進(jìn)行修改,并且 const 關(guān)鍵字聲明的常量被限制于塊級(jí)作用域中進(jìn)行訪問(wèn)。
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 關(guān)鍵字的表現(xiàn)于 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]);
從上述對(duì)比我們也可以看出,JavaScript 中 const 限制的并非值不可變性;而是創(chuàng)建了不可變的綁定,即對(duì)于某個(gè)值的只讀引用,并且禁止了對(duì)于該引用的重賦值,即如下的代碼會(huì)觸發(fā)錯(cuò)誤:
const numbers = [1, 2, 3, 4, 6] numbers = [7, 8, 9, 10, 11] // error: assignment to constant variable console.log(numbers[4])
我們可以參考如下圖片理解這種機(jī)制,每個(gè)變量標(biāo)識(shí)符都會(huì)關(guān)聯(lián)某個(gè)存放變量實(shí)際值的物理地址;所謂只讀的變量即是該變量標(biāo)識(shí)符不可以被重新賦值,而該變量指向的值還是可變的。
JavaScript 中存在著所謂的原始類(lèi)型與復(fù)合類(lèi)型,使用 const 聲明的原始類(lèi)型是值不可變的:
# 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
而如果我們希望將某個(gè)對(duì)象同樣變成不可變類(lèi)型,則需要使用 Object.freeze();不過(guò)該方法僅對(duì)于鍵值對(duì)的 Object 起作用,而無(wú)法作用于 Date、Map 與 Set 等類(lèi)型:
# 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() 也只能防止頂層屬性被修改,而無(wú)法限制對(duì)于嵌套屬性的修改,這一點(diǎn)我們會(huì)在下文的淺拷貝與深拷貝部分繼續(xù)討論。
變量賦值 按值傳遞JavaScript 中永遠(yuǎn)是按值傳遞(pass-by-value),只不過(guò)當(dāng)我們傳遞的是某個(gè)對(duì)象的引用時(shí),這里的值指的是對(duì)象的引用。按值傳遞中函數(shù)的形參是被調(diào)用時(shí)所傳實(shí)參的副本。修改形參的值并不會(huì)影響實(shí)參。而按引用傳遞(pass-by-reference)時(shí),函數(shù)的形參接收實(shí)參的隱式引用,而不再是副本。這意味著函數(shù)形參的值如果被修改,實(shí)參也會(huì)被修改。同時(shí)兩者指向相同的值。我們首先看下 C 中按值傳遞與引用傳遞的區(qū)別:
void Modify(int p, int * q) { p = 27; // 按值傳遞 - p是實(shí)參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 中,對(duì)比例子如下:
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); // 輸出結(jié)果 10 changed unchanged
JavaScript 按值傳遞就表現(xiàn)于在內(nèi)部修改了 c 的值但是并不會(huì)影響到外部的 obj2 變量。如果我們更深入地來(lái)理解這個(gè)問(wèn)題,JavaScript 對(duì)于對(duì)象的傳遞則是按共享傳遞的(pass-by-sharing,也叫按對(duì)象傳遞、按對(duì)象共享傳遞)。最早由Barbara Liskov. 在1974年的GLU語(yǔ)言中提出;該求值策略被用于Python、Java、Ruby、JS等多種語(yǔ)言。該策略的重點(diǎn)是:調(diào)用函數(shù)傳參時(shí),函數(shù)接受對(duì)象實(shí)參引用的副本(既不是按值傳遞的對(duì)象副本,也不是按引用傳遞的隱式引用)。 它和按引用傳遞的不同在于:在共享傳遞中對(duì)函數(shù)形參的賦值,不會(huì)影響實(shí)參的值。按共享傳遞的直接表現(xiàn)就是上述代碼中的 obj1,當(dāng)我們?cè)诤瘮?shù)內(nèi)修改了 b 指向的對(duì)象的屬性值時(shí),我們使用 obj1 來(lái)訪問(wèn)相同的變量時(shí)同樣會(huì)得到變化后的值。
連續(xù)賦值JavaScript 中是支持變量的連續(xù)賦值,即譬如:
var a=b=1;
但是在連續(xù)賦值中,會(huì)發(fā)生引用保留,可以考慮如下情景:
var a = {n:1}; a.x = a = {n:2}; alert(a.x); // --> undefined
為了解釋上述問(wèn)題,我們引入一個(gè)新的變量:
var a = {n:1}; var b = a; // 持有a,以回查 a.x = a = {n:2}; alert(a.x);// --> undefined alert(b.x);// --> [object Object]
實(shí)際上在連續(xù)賦值中,值是直接賦予給變量指向的內(nèi)存地址:
a.x = a = {n:2} │ │ {n:1}<──┘ └─>{n:2}Deconstruction: 解構(gòu)賦值
解構(gòu)賦值允許你使用類(lèi)似數(shù)組或?qū)ο笞置媪康恼Z(yǔ)法將數(shù)組和對(duì)象的屬性賦給各種變量。這種賦值語(yǔ)法極度簡(jiǎn)潔,同時(shí)還比傳統(tǒng)的屬性訪問(wèn)方法更為清晰。傳統(tǒng)的訪問(wèn)數(shù)組前三個(gè)元素的方式為:
var first = someArray[0]; var second = someArray[1]; var third = someArray[2];
而通過(guò)解構(gòu)賦值的特性,可以變?yōu)椋?/p>
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數(shù)組與迭代器
以上是數(shù)組解構(gòu)賦值的一個(gè)簡(jiǎn)單示例,其語(yǔ)法的一般形式為:
[ variable1, variable2, ..., variableN ] = array;
這將為variable1到variableN的變量賦予數(shù)組中相應(yīng)元素項(xiàng)的值。如果你想在賦值的同時(shí)聲明變量,可在賦值語(yǔ)句前加入var、let或const關(guān)鍵字,例如:
var [ variable1, variable2, ..., variableN ] = array; let [ variable1, variable2, ..., variableN ] = array; const [ variable1, variable2, ..., variableN ] = array;
事實(shí)上,用變量來(lái)描述并不恰當(dāng),因?yàn)槟憧梢詫?duì)任意深度的嵌套數(shù)組進(jìn)行解構(gòu):
var [foo, [[bar], baz]] = [1, [[2], 3]]; console.log(foo); // 1 console.log(bar); // 2 console.log(baz); // 3
此外,你可以在對(duì)應(yīng)位留空來(lái)跳過(guò)被解構(gòu)數(shù)組中的某些元素:
var [,,third] = ["foo", "bar", "baz"]; console.log(third); // "baz"
而且你還可以通過(guò)“不定參數(shù)”模式捕獲數(shù)組中的所有尾隨元素:
var [head, ...tail] = [1, 2, 3, 4]; console.log(tail); // [2, 3, 4]
當(dāng)訪問(wèn)空數(shù)組或越界訪問(wèn)數(shù)組時(shí),對(duì)其解構(gòu)與對(duì)其索引的行為一致,最終得到的結(jié)果都是:undefined。
console.log([][0]); // undefined var [missing] = []; console.log(missing); // undefined
請(qǐng)注意,數(shù)組解構(gòu)賦值的模式同樣適用于任意迭代器:
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對(duì)象
通過(guò)解構(gòu)對(duì)象,你可以把它的每個(gè)屬性與不同的變量綁定,首先指定被綁定的屬性,然后緊跟一個(gè)要解構(gòu)的變量。
var robotA = { name: "Bender" }; var robotB = { name: "Flexo" }; var { name: nameA } = robotA; var { name: nameB } = robotB; console.log(nameA); // "Bender" console.log(nameB); // "Flexo"
當(dāng)屬性名與變量名一致時(shí),可以通過(guò)一種實(shí)用的句法簡(jiǎn)寫(xiě):
var { foo, bar } = { foo: "lorem", bar: "ipsum" }; console.log(foo); // "lorem" console.log(bar); // "ipsum"
與數(shù)組解構(gòu)一樣,你可以隨意嵌套并進(jìn)一步組合對(duì)象解構(gòu):
var complicatedObj = { arrayProp: [ "Zapp", { second: "Brannigan" } ] }; var { arrayProp: [first, { second }] } = complicatedObj; console.log(first); // "Zapp" console.log(second); // "Brannigan"
當(dāng)你解構(gòu)一個(gè)未定義的屬性時(shí),得到的值為undefined:
var { missing } = {}; console.log(missing); // undefined
請(qǐng)注意,當(dāng)你解構(gòu)對(duì)象并賦值給變量時(shí),如果你已經(jīng)聲明或不打算聲明這些變量(亦即賦值語(yǔ)句前沒(méi)有let、const或var關(guān)鍵字),你應(yīng)該注意這樣一個(gè)潛在的語(yǔ)法錯(cuò)誤:
{ blowUp } = { blowUp: 10 }; // Syntax error 語(yǔ)法錯(cuò)誤
為什么會(huì)出錯(cuò)?這是因?yàn)镴avaScript語(yǔ)法通知解析引擎將任何以{開(kāi)始的語(yǔ)句解析為一個(gè)塊語(yǔ)句(例如,{console}是一個(gè)合法塊語(yǔ)句)。解決方案是將整個(gè)表達(dá)式用一對(duì)小括號(hào)包裹:
({ safe } = {}); // No errors 沒(méi)有語(yǔ)法錯(cuò)誤默認(rèn)值
當(dāng)你要解構(gòu)的屬性未定義時(shí)你可以提供一個(gè)默認(rèn)值:
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
由于解構(gòu)中允許對(duì)對(duì)象進(jìn)行解構(gòu),并且還支持默認(rèn)值,那么完全可以將解構(gòu)應(yīng)用在函數(shù)參數(shù)以及參數(shù)的默認(rèn)值中。
function removeBreakpoint({ url, line, column }) { // ... }
當(dāng)我們構(gòu)造一個(gè)提供配置的對(duì)象,并且需要這個(gè)對(duì)象的屬性攜帶默認(rèn)值時(shí),解構(gòu)特性就派上用場(chǎng)了。舉個(gè)例子,jQuery的ajax函數(shù)使用一個(gè)配置對(duì)象作為它的第二參數(shù),我們可以這樣重寫(xiě)函數(shù)定義:
jQuery.ajax = function (url, { async = true, beforeSend = noop, cache = true, complete = noop, crossDomain = false, global = true, // ... 更多配置 }) { // ... do stuff };
同樣,解構(gòu)也可以應(yīng)用在函數(shù)的多重返回值中,可以類(lèi)似于其他語(yǔ)言中的元組的特性:
function returnMultipleValues() { return [1, 2]; } var [foo, bar] = returnMultipleValues();Three Dots Rest Operator
在 JavaScript 函數(shù)調(diào)用時(shí)我們往往會(huì)使用內(nèi)置的 arguments 對(duì)象來(lái)獲取函數(shù)的調(diào)用參數(shù),不過(guò)這種方式卻存在著很多的不方便性。譬如 arguments 對(duì)象是 Array-Like 對(duì)象,無(wú)法直接運(yùn)用數(shù)組的 .map() 或者 .forEach() 函數(shù);并且因?yàn)?arguments 是綁定于當(dāng)前函數(shù)作用域,如果我們希望在嵌套函數(shù)里使用外層函數(shù)的 arguments 對(duì)象,我們還需要?jiǎng)?chuàng)建中間變量。
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 來(lái)以數(shù)組形式獲取函數(shù)的調(diào)用參數(shù),Rest Operator 也可以用于在解構(gòu)賦值中以數(shù)組方式獲取剩余的變量:
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 的應(yīng)用場(chǎng)景譬如進(jìn)行不定數(shù)組的指定類(lèi)型過(guò)濾:
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 中并沒(méi)有定義 arguments 對(duì)象,但是我們?nèi)匀豢梢允褂?Rest Operator 來(lái)獲取 Arrow Function 的調(diào)用參數(shù):
(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 的功能正好相反,其常用于進(jìn)行數(shù)組構(gòu)建與解構(gòu)賦值,也可以用于將某個(gè)數(shù)組轉(zhuǎn)化為函數(shù)的參數(shù)列表,其基本使用方式如下:
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 來(lái)簡(jiǎn)化函數(shù)調(diào)用:
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"
還有另外一個(gè)好處就是可以用來(lái)替換 Object.assign 來(lái)方便地從舊有的對(duì)象中創(chuàng)建新的對(duì)象,并且能夠修改部分值;譬如:
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,實(shí)際上 Spread Operator 也是使用的 Iteration Protocols 來(lái)進(jìn)行元素遍歷與結(jié)果搜集;因此我們也可以通過(guò)自定義 Iterator 的方式來(lái)控制 Spread Operator 的表現(xiàn)。Iterable 協(xié)議規(guī)定了對(duì)象必須包含 Symbol.iterator 方法,該方法返回某個(gè) Iterator 對(duì)象:
interface Iterable { [Symbol.iterator]() { //... return Iterator; } }
該 Iterator 對(duì)象從屬于 Iterator Protocol,其需要提供 next 成員方法,該方法會(huì)返回某個(gè)包含 done 與 value 屬性的對(duì)象:
interface Iterator { next() { //... return { value:, done: }; }; }
典型的 Iterable 對(duì)象就是字符串:
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"]
我們可以通過(guò)自定義 array-like 對(duì)象的 Symbol.iterator 屬性來(lái)控制其在迭代器上的效果:
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] 為該對(duì)象創(chuàng)建了值為某個(gè)迭代器的屬性,從而使該對(duì)象符合了 Iterable 協(xié)議;而 iterator() 又返回了包含 next 成員方法的對(duì)象,使得該對(duì)象最終具有和數(shù)組相似的行為表現(xiàn)。
Copy Composite Data Types: 復(fù)合類(lèi)型的拷貝 Shallow Copy: 淺拷貝 頂層屬性遍歷淺拷貝是指復(fù)制對(duì)象的時(shí)候,指對(duì)第一層鍵值對(duì)進(jìn)行獨(dú)立的復(fù)制。一個(gè)簡(jiǎn)單的實(shí)現(xiàn)如下:
// 淺拷貝實(shí)現(xiàn) function shadowCopy(target, source){ if( !source || typeof source !== "object"){ return; } // 這個(gè)方法有點(diǎn)小trick,target一定得事先定義好,不然就不能改變實(shí)參了。 // 具體原因解釋可以看參考資料中 JS是值傳遞還是引用傳遞 if( !target || typeof target !== "object"){ return; } // 這邊最好區(qū)別一下對(duì)象和數(shù)組的復(fù)制 for(var key in source){ if(source.hasOwnProperty(key)){ target[key] = source[key]; } } } //測(cè)試?yán)?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() 方法可以把任意多個(gè)的源對(duì)象所擁有的自身可枚舉屬性拷貝給目標(biāo)對(duì)象,然后返回目標(biāo)對(duì)象。Object.assign 方法只會(huì)拷貝源對(duì)象自身的并且可枚舉的屬性到目標(biāo)對(duì)象身上。注意,對(duì)于訪問(wèn)器屬性,該方法會(huì)執(zhí)行那個(gè)訪問(wèn)器屬性的 getter 函數(shù),然后把得到的值拷貝給目標(biāo)對(duì)象,如果你想拷貝訪問(wèn)器屬性本身,請(qǐng)使用 Object.getOwnPropertyDescriptor() 和Object.defineProperties() 方法。
注意,字符串類(lèi)型和 symbol 類(lèi)型的屬性都會(huì)被拷貝。
注意,在屬性拷貝過(guò)程中可能會(huì)產(chǎn)生異常,比如目標(biāo)對(duì)象的某個(gè)只讀屬性和源對(duì)象的某個(gè)屬性同名,這時(shí)該方法會(huì)拋出一個(gè) TypeError 異常,拷貝過(guò)程中斷,已經(jīng)拷貝成功的屬性不會(huì)受到影響,還未拷貝的屬性將不會(huì)再被拷貝。
注意, Object.assign 會(huì)跳過(guò)那些值為 null 或 undefined 的源對(duì)象。
Object.assign(target, ...sources)
例子:淺拷貝一個(gè)對(duì)象
var obj = { a: 1 }; var copy = Object.assign({}, obj); console.log(copy); // { a: 1 }
例子:合并若干個(gè)對(duì)象
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 }, 注意目標(biāo)對(duì)象自身也會(huì)改變。
例子:拷貝 symbol 類(lèi)型的屬性
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 是個(gè)繼承屬性。 bar: { value: 2 // bar 是個(gè)不可枚舉屬性。 }, baz: { value: 3, enumerable: true // baz 是個(gè)自身可枚舉屬性。 } }); var copy = Object.assign({}, obj); console.log(copy); // { baz: 3 }
例子:原始值會(huì)被隱式轉(zhuǎn)換成其包裝對(duì)象
var v1 = "123"; var v2 = true; var v3 = 10; var v4 = Symbol("foo") var obj = Object.assign({}, v1, null, v2, undefined, v3, v4); // 源對(duì)象如果是原始值,會(huì)被自動(dòng)轉(zhuǎn)換成它們的包裝對(duì)象, // 而 null 和 undefined 這兩種原始值會(huì)被完全忽略。 // 注意,只有字符串的包裝對(duì)象才有可能有自身可枚舉屬性。 console.log(obj); // { "0": "1", "1": "2", "2": "3" }
例子:拷貝屬性過(guò)程中發(fā)生異常
var target = Object.defineProperty({}, "foo", { value: 1, writeable: false }); // target 的 foo 屬性是個(gè)只讀屬性。 Object.assign(target, {bar: 2}, {foo2: 3, foo: 3, foo3: 3}, {baz: 4}); // TypeError: "foo" is read-only // 注意這個(gè)異常是在拷貝第二個(gè)源對(duì)象的第二個(gè)屬性時(shí)發(fā)生的。 console.log(target.bar); // 2,說(shuō)明第一個(gè)源對(duì)象拷貝成功了。 console.log(target.foo2); // 3,說(shuō)明第二個(gè)源對(duì)象的第一個(gè)屬性也拷貝成功了。 console.log(target.foo); // 1,只讀屬性不能被覆蓋,所以第二個(gè)源對(duì)象的第二個(gè)屬性拷貝失敗了。 console.log(target.foo3); // undefined,異常之后 assign 方法就退出了,第三個(gè)屬性是不會(huì)被拷貝到的。 console.log(target.baz); // undefined,第三個(gè)源對(duì)象更是不會(huì)被拷貝到的。使用 [].concat 來(lái)復(fù)制數(shù)組
同樣類(lèi)似于對(duì)于對(duì)象的復(fù)制,我們建議使用[].concat來(lái)進(jìn)行數(shù)組的深復(fù)制:
var list = [1, 2, 3]; var changedList = [].concat(list); changedList[1] = 2; list === changedList; // false
同樣的,concat方法也只能保證一層深復(fù)制:
> list = [[1,2,3]] [ [ 1, 2, 3 ] ] > new_list = [].concat(list) [ [ 1, 2, 3 ] ] > new_list[0][0] = 4 4 > list [ [ 4, 2, 3 ] ]淺拷貝的缺陷
不過(guò)需要注意的是,assign是淺拷貝,或者說(shuō),它是一級(jí)深拷貝,舉兩個(gè)例子說(shuō)明:
const defaultOpt = { title: { text: "hello world", subtext: "It"s my world." } }; const opt = Object.assign({}, defaultOpt, { title: { subtext: "Yes, your world." } }); console.log(opt); // 預(yù)期結(jié)果 { title: { text: "hello world", subtext: "Yes, your world." } } // 實(shí)際結(jié)果 { title: { subtext: "Yes, your world." } }
上面這個(gè)例子中,對(duì)于對(duì)象的一級(jí)子元素而言,只會(huì)替換引用,而不會(huì)動(dòng)態(tài)的添加內(nèi)容。那么,其實(shí)assign并沒(méi)有解決對(duì)象的引用混亂問(wèn)題,參考下下面這個(gè)例子:
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); // 結(jié)果 opt1: { title: { text: "hello world", subtext: "Yes, your world." } } opt2: { title: { text: "hello world", subtext: "Yes, your world." } }DeepCopy: 深拷貝 遞歸屬性遍歷
一般來(lái)說(shuō),在JavaScript中考慮復(fù)合類(lèi)型的深層復(fù)制的時(shí)候,往往就是指對(duì)于Date、Object與Array這三個(gè)復(fù)合類(lèi)型的處理。我們能想到的最常用的方法就是先創(chuàng)建一個(gè)空的新對(duì)象,然后遞歸遍歷舊對(duì)象,直到發(fā)現(xiàn)基礎(chǔ)類(lèi)型的子節(jié)點(diǎn)才賦予到新對(duì)象對(duì)應(yīng)的位置。不過(guò)這種方法會(huì)存在一個(gè)問(wèn)題,就是JavaScript中存在著神奇的原型機(jī)制,并且這個(gè)原型會(huì)在遍歷的時(shí)候出現(xiàn),然后原型不應(yīng)該被賦予給新對(duì)象。那么在遍歷的過(guò)程中,我們應(yīng)該考慮使用hasOenProperty方法來(lái)過(guò)濾掉那些繼承自原型鏈上的屬性:
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."); }
調(diào)用如下:
// 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));
對(duì)于一般的需求是可以滿足的,但是它有缺點(diǎn)。下例中,可以看到JSON復(fù)制會(huì)忽略掉值為undefined以及函數(shù)表達(dá)式。
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 的動(dòng)態(tài)數(shù)據(jù)綁定
ECMAScript 2017(ES8)特性概述
WebAssembly 初體驗(yàn):從零開(kāi)始重構(gòu)計(jì)算模塊
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/87285.html
摘要:淺拷貝實(shí)現(xiàn)方式賦值。但是進(jìn)行的是淺拷貝,拷貝的是對(duì)象的屬性的引用,而不是對(duì)象本身。沒(méi)復(fù)制遞歸拷貝避免相互引用對(duì)象導(dǎo)致死循環(huán),如的情況使用方法直接使用,可以達(dá)到深拷貝的效果。 js有五種基本數(shù)據(jù)類(lèi)型,string,number,boolean,null,undefind。這五種類(lèi)型的賦值,就是值傳遞。特殊類(lèi)型對(duì)象的賦值是將對(duì)象地址的引用賦值。這時(shí)候修改對(duì)象中的屬性或者值,會(huì)導(dǎo)致所有引用這...
摘要:前端日?qǐng)?bào)精選變量聲明與賦值值傳遞淺拷貝與深拷貝詳解淺談自適應(yīng)學(xué)習(xí)比你想象的要簡(jiǎn)單常見(jiàn)排序算法之實(shí)現(xiàn)世界萬(wàn)物誕生記中文深入理解筆記與異步編程譯不可變和中的知乎專(zhuān)欄譯怎樣避免開(kāi)發(fā)時(shí)的深坑瘋狂的技術(shù)宅在翻譯網(wǎng)格布局掘金詳解改變模糊度亮 2017-08-15 前端日?qǐng)?bào) 精選 ES6 變量聲明與賦值:值傳遞、淺拷貝與深拷貝詳解淺談web自適應(yīng)學(xué)習(xí) React.js 比你想象的要簡(jiǎn)單常見(jiàn)排序算法之...
摘要:深拷貝與淺拷貝的出現(xiàn),就與這兩個(gè)數(shù)據(jù)類(lèi)型有關(guān)。這時(shí),就需要用淺拷貝來(lái)實(shí)現(xiàn)了。數(shù)據(jù)一但過(guò)多,就會(huì)有遞歸爆棧的風(fēng)險(xiǎn)。這個(gè)方法是在解決遞歸爆棧問(wèn)題的基礎(chǔ)上,加以改進(jìn)解決循環(huán)引用的問(wèn)題。但如果你并不想保持引用,那就改用用于解決遞歸爆棧即可。 前言 這是前端面試題系列的第 9 篇,你可能錯(cuò)過(guò)了前面的篇章,可以在這里找到: 數(shù)組去重(10 種濃縮版) JavaScript 中的事件機(jī)制(從原生到...
摘要:的翻譯文檔由的維護(hù)很多人說(shuō),阮老師已經(jīng)有一本關(guān)于的書(shū)了入門(mén),覺(jué)得看看這本書(shū)就足夠了。前端的異步解決方案之和異步編程模式在前端開(kāi)發(fā)過(guò)程中,顯得越來(lái)越重要。為了讓編程更美好,我們就需要引入來(lái)降低異步編程的復(fù)雜性。 JavaScript Promise 迷你書(shū)(中文版) 超詳細(xì)介紹promise的gitbook,看完再不會(huì)promise...... 本書(shū)的目的是以目前還在制定中的ECMASc...
摘要:淺拷貝與深拷貝一數(shù)據(jù)類(lèi)型數(shù)據(jù)分為基本數(shù)據(jù)類(lèi)型,和對(duì)象數(shù)據(jù)類(lèi)型。淺拷貝是按位拷貝對(duì)象,它會(huì)創(chuàng)建一個(gè)新對(duì)象,這個(gè)對(duì)象有著原始對(duì)象屬性值的一份精確拷貝。對(duì)于字符串?dāng)?shù)字及布爾值來(lái)說(shuō)不是或者對(duì)象,會(huì)拷貝這些值到新的數(shù)組里。 淺拷貝與深拷貝 一、數(shù)據(jù)類(lèi)型數(shù)據(jù)分為基本數(shù)據(jù)類(lèi)型(String, Number, Boolean, Null, Undefined,Symbol)和對(duì)象數(shù)據(jù)類(lèi)型。 基本數(shù)據(jù)類(lèi)...
閱讀 2821·2023-04-25 18:46
閱讀 712·2021-11-19 09:40
閱讀 2078·2021-09-28 09:36
閱讀 3384·2021-09-10 11:11
閱讀 3465·2019-08-30 15:55
閱讀 1804·2019-08-30 15:54
閱讀 2599·2019-08-29 16:16
閱讀 3543·2019-08-29 15:08