JavaScript資料型別AND深拷貝和淺拷貝的不歸路

Link-X發表於2018-11-20

本文將向讀者介紹在js中資料的拷貝,旨在讓讀者能在工作中或者面試中遇到相似的問題能夠應用起來

首先宣告一下,這批文章僅代表我自己對js的一些理解。由於筆者水平有限,如果有出錯之出,還望多多諒解,指正。謝謝。當然如果你看的不爽...那麼---------你順著網線過來打我呀O(∩_∩)O哈哈~

正文開始------------------

資料型別

說到js的資料拷貝就不得不提js的資料型別。我們知道的7中資料型別:
Number、String、Boolean、Undefined、Null、Object 和es6新增的Symbol

(null undefined number boolean )都是基本型別,存棧裡的資料

Null: 不存在的,沒有的資料物件

Undefined: 變數宣告瞭卻沒有初始化的資料。表示"缺少值",就是此處應該有一個值,但是還沒有定義。

number: 顧名思義就是一個數字型別,number是存放在棧裡的資料

string:string比較特殊,它其實是存在堆裡的,我們拿到的只是一個地址引用。如果對js比較瞭解的話,那麼會知道

js中string是不可變的,我們沒有任何一個方法可以改變一個字串。可以認為string是 行為與基本型別相似的不可變引用型別

Object 是引用型別,存堆裡的地址

Objecty下面又有三員大將,它們都是 Object.prototype 下的熟悉

Array: 一個存放資料的集合,js中 陣列可以存放任何資料。

Function: 函式,其實也是資料。我們可以把一個函式賦值給另一個變數

object: 物件,沒啥好說的。

Symbol的話是一個es6新增物件,用Symbol可以建立唯一的變數名。

這個array有點意思,在有的語言陣列就是陣列,但是js裡它確實物件,陣列的key就是它的下標

 // instanceof 是判斷xxx 是否xxx的例項
    Object instanceof Object // true
    Array instanceof Object // true
    Function instanceof Object // true
複製程式碼

接下來重點來了,請大家看一段程式碼:

    Number.constructor // ƒ Function() { [native code] }
    String.constructor // ƒ Function() { [native code] }
    Boolean.constructor // ƒ Function() { [native code] }
    Object.constructor // ƒ Function() { [native code] }
    Symbol.constructor //ƒ Function() { [native code] }
複製程式碼

大家看到這個有陷入深深的沉思嗎?為啥他們都有 constructor,為啥他們都有方法可以去呼叫。而且如果用 剛剛這個 instanceof 去判斷會發現,string、boolean、number...都是object 的例項。

那麼你知道是為什麼嗎?

你沒猜錯。在js中所以資料都是物件。all in of object。這個英文怕不怕。
接下來再看下面的程式碼:

    Number.prototype.six = () => { console.log(666) }
    const num = 123
    num.six() // 666
複製程式碼

這裡能給number原型新增一個方法,並且定義的一個number 可以去掉用這個方法。已經說明了js中number 其實是基於物件。
string、Boolean 等都是如此
順便解釋一下js堆和棧的一點概念
棧(stack)為自動分配的記憶體空間,它由系統自動釋放;而堆(heap)則是動態分配的記憶體,大小不定也不會自動釋放。

拷貝

js的拷貝是有點複雜的,涉及到引用型別的話

    let a = 1
    let b = '2'
    a = b
    a = 3
    console.log(a, b) // 3 2
複製程式碼

這是js中基本型別的複製,沒有任何問題,我們就不多扯了。就是覆蓋就好。

我們來看看js中引用型別的拷貝

    let obj = {
        a: 1,
        b: {
            b: 1
        }
    }
    let arr = [1, 2, 3]
    let arr2 = arr
    let obj2 = obj
    
    arr[1] = '666'
    obj.a = '777'
    console.log(obj.a) // 666
    console.log(obj2.a) // 666
    console.log(arr[1]) // 777
    console.log(arr2[1]) // 777
複製程式碼

這就是引用型別的尿性,有的時候很煩,有的時候卻很有用。因為js中引用型別給你訪問的是一個地址。這個地址對應著資料的位置,我們像複製普通型別一樣複製引用型別,就等於直接把地址給了別人。改動的時候大家改的其實是同一個資料。
那麼問題來了,我就是複製一個引用型別的所以資料,但是我不想被我複製的物件收到影響怎麼辦。

淺拷貝

    let obj = {
        a: 1,
        b: {
            b: 1
        }
    }
    let arr = [1, 2, 3]
    let obj2 = { ...obj }
    let arr2 = [ ...arr ]
    obj2.a = '666'
    arr2[1] = '777'
    console.log(obj.a) // 1
    console.log(obj2.a) // 666
    console.log(arr[1]) // 1
    console.log(arr2[1]) // 777
複製程式碼

好像沒問題了,似乎很簡單的樣子啊。
我們接著上面的程式碼

    ...
    obj2.b.b = '888'
    console.log(obj.b.b) // { b: '888' } 
    console.log(obj2.b.b) // { b: '888' }
複製程式碼

震驚!似乎又出現了剛剛的問題。沒錯這就是因為 ...是es6的擴充套件運算子。他只是拷貝了第一層變數。後面的依然還是直接複製地址。

深拷貝

先來一個簡單的,百度一搜尋一大堆的看看

function clone(params) {
    var obj = {};
    for(var i in params) {
        if (params.hasOwnProperty(i)) {
            if (typeof params[i] === 'object') {
                obj[i] = clone(params[i]); // 通過判斷是否物件而進行遞迴
            } else {
                obj[i] = params[i];
            }
        }
    }
    return obj;
}
let obj = {
    a: 1,
    b: {
        b: 1
    }
}
let obj2 = clone(obj)
obj2.b.b = '888'
console.log(obj.b.b, obj2.b.b) // 1  888
複製程式碼

這就實現了一個簡單的深拷貝,但是它有一些問題。主要就是考慮的不夠嚴謹,比如一些資料沒有做到相容。比如set、 map、weakset、weakmap、array... 是不是感覺很麻煩。當然其實我們有個簡單的方法。

function clone2(params) {
    return JSON.parse(JSON.stringify(params));
}
複製程式碼

平時我一般工作中拷貝一些json型別的資料就用這個....簡單粗暴。
這個方法其實也有一些問題。就是沒法克隆 函式和正則匹配等.當然如果只是簡單的資料還是可以的。

這個方法是我在前端早讀課中的一篇文章看到的一個方法。這裡就厚顏無恥的clone了下來,如果大家有興趣的話可以去關注前端早讀課。看那篇關於js物件拷貝的文章。

function cloneForce(x) {
    // =============
    const uniqueList = []; // 用來去重
    // =============

    let root = {};

    // 迴圈陣列
    const loopList = [
        {
            parent: root,
            key: undefined,
            data: x,
        }
    ];

    while(loopList.length) {
        // 深度優先
        const node = loopList.pop();
        const parent = node.parent;
        const key = node.key;
        const data = node.data;

        // 當第一次迴圈的時候key是undefined,所以靠譜到第一級,後面的都是拷貝到子級
        let res = parent;
        if (typeof key !== 'undefined') {
            res = parent[key] = {};
        }

        // =============
        let uniqueData = find(uniqueList, data);
        if (uniqueData) {
            parent[key] = uniqueData.target;
            continue; // 判斷資料是否存在,如果存在就不繼續這次迴圈了
        }

        // 資料不存在
        // 儲存源資料,在拷貝資料中對應的引用
        uniqueList.push({
            source: data,
            target: res,
        });
        // =============

        for(let k in data) {
            if (data.hasOwnProperty(k)) {
                if (typeof data[k] === 'object') {
                    // 如果有object 就push禁 loopList,進行下一次轉換
                    loopList.push({
                        parent: res,
                        key: k,
                        data: data[k],
                    });
                } else {
                    res[k] = data[k];
                }
            }
        }
    }

    return root;
}
function find(arr, item) {
    for(let i = 0; i < arr.length; i++) {
        if (arr[i].source === item) {
            return arr[i];
        }
    }

    return null;
}
複製程式碼

最後在向大家介紹一種神奇的方法。

let obj = {a:1, b: {b:1}}
var channel = new MessageChannel();
        var port1 = channel.port1;
        var port2 = channel.port2;
        port1.onmessage = function(event) {
            let obj2 =  event.data
			obj2.b.b = '666'
			console.log(obj.b.b,obj2.b.b) // 1  666
        }
        port2.onmessage = function(event) {
            console.log("port2收到來自port1的資料:" + event.data);
        }

        port2.postMessage(obj);
複製程式碼

是不是很神奇。不過這個也無法解決物件迴圈引用的問題。並且它是非同步的。

相關文章