系統理解javascript中的數據類型、堆內存棧內存、js的垃圾回收機制、深拷貝淺拷貝原理

目錄

前言

本身在面試博客裏只是想整理一下js的類型,越聯想越感覺這塊的知識體量大,而且都是相關聯的只是,但網上的現有的很多博客繁雜還不太清晰,故此專門記錄一下這幾個點。

正文

一、js中的數據類型

基本類型:number ,string,null,Boolen,undefined,symbol
引用類型:object (Array,Function,Date,Regxp在es6中規定都是object類型)

  • 兩者的區別:
    基本類型:可以直接操作的實際存在的數據段。存在在內存的棧中,比較的是值的比較!
    引用類型:複製操作是複製的是對象的引用,增加操作時是操作的對象本身。存在在堆內存和棧內存中,比較的是引用的比較!

這裏只特別關注一下es6新增的類型:symbol

1.1 什麼是symbol?

symbol是es6新增的一個基本數據類型,保存在棧內存中,通常使用symbol來指代獨一無二的屬性。

1.1.1 symbol的特徵

特徵1 :獨一無二
直接使用Symbol()創建新的symbol變量,可選用一個字符串用於描述。當參數爲對象時,將調用對象的toString()方法。

var sym1 = Symbol();  // Symbol() 
var sym2 = Symbol('111');  // Symbol(ConardLi)
var sym3 = Symbol('111');  // Symbol(ConardLi)
var sym4 = Symbol({name:'222'}); // Symbol([object Object])
console.log(sym2 === sym3);  // false

我們用兩個相同的字符串創建兩個Symbol變量,它們是不相等的,可見每個Symbol變量都是獨一無二的。

特徵2 :是基本數據類型,不能使用new Symbol()創造

let sym = new Symbol() //Symbol is not a constructor

let sym = Symbol('121')
console.log(typeof sym); //Symbol

特徵3:不可枚舉
當使用Symbol作爲對象屬性時,可以保證對象不會出現重名屬性,調用for...in不能將其枚舉出來,另外調用Object.getOwnPropertyNames、Object.keys()也不能獲取Symbol屬性。

可以調用Object.getOwnPropertySymbols()用於專門獲取Symbol屬性。

var obj = {
  name:'ConardLi',
  [Symbol('name2')]:'code祕密花園'
}
Object.getOwnPropertyNames(obj); // ["name"]
Object.keys(obj); // ["name"]
for (var i in obj) {
   console.log(i); // name
}
Object.getOwnPropertySymbols(obj) // [Symbol(name)]

1.2 symbol的作用?

做爲對象屬性名
由於每一個Symbol對象的值都是不相等的,利用這一特性,符號做爲標識符使用。將其用於對象屬性名時,可以保證對象每一個屬性名都是唯一的,不會發生對象屬性被覆蓋的情況。

用符號做爲對象屬性名時,不能用**.**的形式添加對象屬性:

var sym = Symbol();
var a = {};

a.sym = 'itbilu.com';//以點的形式添加屬性名其本質上還是一個字符串
a[sym] // undefined
a['sym'] // "itbilu.com"  

可以使用以下三種方式添加符號的對象屬性:

  1. 用方括號添加
var sym = Symbol();
var a = {};
a[sym] = 'itbilu.com';
  1. 在對象內部定義
var a = {
  [sym]: 'itbilu.com'
};
  1. 用defineProperty添加
var a = {};
Object.defineProperty(a, sym, { value: 'itbilu.com' });

其他類型的具體細節也特別多,不一一列舉了,可以直接參考:
【JS 進階】你真的掌握變量和類型了嗎

可以分辨數據類型之後,我們再看一下在javascript中存儲數據的地方:堆內存和棧內存

二、堆內存和棧內存

2.1 概念理解

在v8引擎中對js變量的存儲主要有兩種位置:堆內存和棧內存,以下簡稱堆、棧。
下面通過兩個例子來理解堆和棧使用

//例1
var num1 = 1 
var num2 = "222"
num2 = num1
num2 = '666'
console.log(num1)// 1
console.log(num2)// '666'

這裏可以看出上述的基本數據類型,是真實值在比較。那我們在看一下引用數據類型:

//例2
var obj1 = {name:'aa',age:18}
var obj2 = obj1
obj2.name = 'bb'
console.log(obj1) // { name: "bb", age: 18 }
console.log(obj2) // { name: "bb", age: 18 }

這裏我們可以看出當obj2改變的時候,obj1也同時一起改變了!爲什麼會被影響而不像基本數據類型呢?我們來分析一下例2的過程:

var obj2 = obj1時,數據存儲的位置如下
在這裏插入圖片描述
obj2.name = 'bb'時,改變的是堆中實際的數據!
在這裏插入圖片描述
所以打印出來的obj1和obj2是相同的。

2.2 通過上述例子我們可以得出堆和棧的區別

  • 棧內存主要用於存儲各種基本類型的變量,包括Boolean、Number、String、Undefined、Nul 和引用數據類型的地址指針,它們都是直接按值存儲在棧中的。
  • 堆內存主要用於存儲引用類型如對象(Object)、數組(Array)、函數(Function) …,當我們想要訪問引用類型的值的時候,需要先從棧中獲得對象的地址指針,然後,在通過地址指針找到堆中的所需要的數據。

2.3 思考

2.3.1 思考一:爲什麼基本數據類型保存在棧中,而引用數據類型保存在堆中?

  • 堆比棧大,棧比堆速度快;
  • 基本數據類型比較穩定,而且相對來說佔用的內存小;
  • 引用數據類型大小是動態的,而且是無限的,引用值的大小會改變,不能把它放在棧中,否則會降低變量查找的速度,因此放在變量棧空間的值是該對象存儲在堆中的地址,地址的大小是固定的,所以把它存儲在棧中對變量性能無任何負面影響;
  • 堆內存是無序存儲,可以根據引用直接獲取;

2.3.2 思考二:es6中的const定義的數據能改嗎?

學習過es6知識後都知道新增了新建兩個局部作用域變量的letconst,const通常用來命名不可更改的常量,但是const命名的值,也不是完全不能改!這裏就涉及到命名基本數據類型還是命名引用數據類型。

const a = 1
a = 2
console.log(a) // error: Assignment to constant variable.
const obj = {b:1}
obj.b = 2
console.log(obj) // { b:2} 

當我們定義一個const引用數據類型的時候,我們說的常量其實是指針,就是const對象對應的堆內存指向是不變的,但是堆內存中的數據本身的大小或者屬性是可變的。而對於const定義的基礎數據類型而言,這個值就相當於const對象的指針,是不可變。

初步理解了堆和棧,那我們來思考一個問題,計算機的內存就那麼大,假設我們一直往裏面存東西而不取的話,內存會滿嗎?如果滿了怎麼辦?

答案肯定是會滿啊!爲了不讓它滿,這裏就要提一下js的垃圾回收機制

三、js的垃圾回收機制

javaScript的內存管理不同於其他語言,程序員本身不可以操作而是由系統全自動回收~當然我們平時碼字的時候不用管它,但是如果代碼有問題的話,還是會造成內存泄漏,長時間積攢下來最終導致內存溢出(就是滿了 ~沒地兒存了)

3.1 js怎麼全自動回收的?——JavaScript垃圾回收機制

JavaScript垃圾回收機制很簡單:找出不再使用的變量(垃圾),然後釋放掉其佔用的內存(回收),但是這個過程不是實時的,因爲其開銷比較大,所以垃圾回收系統(GC)會按照固定的時間間隔,週期性的執行。

var a = 'test11'
var b =  'test222'
var a = b 

這就是簡單的回收,“test11”這個字符串失去了引用(之前是被a引用),系統檢測到這個事實之後,就會釋放該字符串的存儲空間以便這些空間可以被再利用。針對這個解釋,爲方便理解,提出兩個問題:

3.1.1 問題一: 什麼纔算是不再使用的變量?

  • 不再需要使用的變量也就是生命週期結束的變量,是局部變量,局部變量只在函數的執行過程中存在, 當函數運行結束,沒有其他引用(閉包),那麼該變量會被標記回收。

  • 全局變量的生命週期直至瀏覽器卸載頁面纔會結束,也就是說全局變量不會被當成垃圾回收。

如果對什麼是引用還不太清除可以參考這篇:js垃圾回收中的引用概念

3.1.2 問題二:怎麼回收這些垃圾?

js對這類變量有兩種定義方式:標記清除法和引用計數法;

引用計數是指跟蹤記錄每個值被引用的次數,語言引擎有一張"引用表",保存了內存裏面所有的資源(通常是各種值)的引用次數。如果一個值的引用次數是0,就表示這個值不再用到了,因此可以將這塊內存釋放。

var obj = {  a :  {test : 1} }
//兩個對象被創建,一個a{}作爲另一個的屬性被引用,另一個{}被分配給變量o,沒有可以被回收的
var obj2 = obj  
//obj 引用次數爲1
obj  = 1 
// 現在,“這個對象”的原始引用obj被obj2替換了,
var obj3 = obj2.a; 
// 引用 obj2 的a屬性,現在,a{} 引用次數爲2了,一個是obj2,一個是obj3
obj2 = 'test' //原始obj 引用次數爲0 ,可以被回收了
obj3 = null //a{}的引用次數也爲0 ,可以被回收了
  1. 當聲明瞭一個變量並將一個引用類型值(function object array)賦給該變量時,則這個值的引用次數就是1。
  2. 如果同一個值又被賦給另一個變量,則該值的引用次數加1。
  3. 相反,如果包含對這個值引用的變量又取得了另外一個值,則這個值的引用次數減1。
  4. 當這個值的引用次數變成0時,則說明沒有辦法再訪問這個值了,因而就可以將其佔用的內存空間回收回來。
  5. 當垃圾回收器下次再運行時,它就會釋放那些引用次數爲0的值所佔用的內存。

我們再看一個例子:

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o  這裏
  return "azerty";
}
f();

這就是一個簡單的循環引用例子,但是如果一個值不再需要了,引用數卻不爲0,垃圾回收機制無法釋放這塊內存,從而導致內存泄漏。爲了解決循環引用造成的問題,我們通過使用標記清除算法來實現垃圾回收。

標記清除(常用),它的過程可以分爲幾步:

  1. 垃圾收集器會在運行的時候會給存儲在內存中的所有變量都加上標記。
  2. 從根部出發將能觸及到的對象(環境中使用的變量,被環境中的變量引用的變量)的標記清除。
  3. 那些還存在標記的變量被視爲準備刪除的變量。
  4. 最後垃圾收集器會執行最後一步內存清除的工作,銷燬那些帶標記的值並回收它們所佔用的內存空間。

在這裏插入圖片描述

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o  這裏
  return "azerty";
}
f();

console.log(test()) //{ d: "我將要被使用" }

再看之前循環引用的例子,函數調用返回之後,兩個循環引用的對象在垃圾收集時從全局對象出發無法再獲取他們的引用。 因此,他們將會被垃圾回收器回收。

3.1.3 問題三:這個回收的週期性怎麼定義?

  • IE7之前的垃圾收集器是根據內存分配量運行的,即 256 個變量、4096 個對象(數組)字面量或 64 KB 的字符串。達到這些臨界值的任何一個,垃圾收集器就會運行。
  • IE7 重寫了垃圾收集例程。新的工作方式爲:觸發垃圾收集的變量分配、字面量和數組元素的臨界值被調整爲 動態修正。初始值與之前版本相同,但如果垃圾收集例程回收的內存低於 15%,則臨界值加倍。若回收內存分配量超過 85%,則臨界值重置回默認值。
  • V8 中的垃圾回收主要使用的是 分代回收 (Generational collection)機制,將保存對象的 堆 (heap) 進行了分代:
    對象最初會被分在 新生區(New Space) (1~8M),新生區的內存分配只需要保有一個指向內存區的指針,不斷根據內存大小進行遞增,當指針達到新生區的末尾,會有一次垃圾回收清理(小週期),清理掉新生區中不再活躍的死對象。
    1. 對於超過 2 個小週期的對象,則需要將其移動至 老生區(Old Space)。老生區在 標記-清除 或 標記-緊縮 的過程(大週期) 中進行回收。一次大週期通常是在移動足夠多的對象至老生區後纔會發生。

    2. GC執行時,中斷代碼,停止其他操作。執行階段遍歷所有對象,對於不可訪問的對象進行回收。該機制執行操作耗時100ms左右。

    3. 關於詳細的回收算法,可以參考:https://segmentfault.com/a/1190000015265100

瞭解了什麼是垃圾回收,我們再瞭解一下沒有被回收的變量造成的後果是什麼?

3.2 通俗理解什麼是內存溢出和內存泄漏?

內存泄漏是指你向系統申請分配內存進行使用,可是使用完了以後卻不歸還(刪除),結果你申請到的那塊內存你自己也不能再訪問(也許你把它的地址給弄丟了),而系統也不能再次將它分配給需要的程序。

內存溢出就是你要求分配的內存超出了系統能給你的,系統不能滿足需求,於是產生溢出。

3.2.1 問題一:什麼情況會導致內存泄漏?

1. 意外的全局變量

function (){
		test = "我沒有被聲明,是一個指向window的全局變量 "
		this.data = "我是掛載在this上的變量,this指向window,所以我也是全局變量"
}

原因:全局變量在頁面關閉之前都不會被回收,從而導致內存泄漏。

優化:在 JavaScript 文件頭部加上 ‘use strict’,可以避免此類錯誤發生。啓用嚴格模式解析 JavaScript ,避免意外的全局變量。

2.沒有被清除的定時器和回調函數

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 處理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

原因:上述代碼,每1秒調一次,如果id爲Node的元素從DOM中移除,該定時器仍會存在,同時,因爲回調函數中包含對someResource的引用,定時器外面的someResource也不會被釋放。

優化:使用完成後就clearInterval()清除定時器

3.閉包

  • 什麼是閉包?
function name() {
    var name = '小明'
    function printName() {
        var age = 13
        console.log(name + '今年' +age+'歲')
    }
    printName()
}
name()

概念:定義在函數內部的函數,printName就是一個閉包,其作用:1.可以訪問到父集函數的局部變量;2.讓這些變量的值始終保持在內存中,不會在name()調用後被自動清除。

原因:上述代碼中nameprintName的父函數,而printName被賦給了一個全局變量,這導致printName始終在內存中,而printName的存在依賴於name,因此name也始終在內存中,不會在調用結束後,被垃圾回收機制回收。

function name() {
    var name = '小明'
    printName(name)
}
function printName(name) {
    var age = 13
    console.log(name + '今年' + age + '歲')
}
name()

優化:將閉包函數定義在外層

4. 沒有清理的DOM元素的引用
很多時候, 我們對 Dom 的操作, 會把 Dom 的引用保存在一個數組或者 Map 中。
雖然我們removeChild移除了button,但是還在elements對象裏保存着#button的引用,換言之,DOM元素還在內存裏面。

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};
function doStuff() {
    image.src = 'http://some.url/image';
    button.click();
    console.log(text.innerHTML);
}
function removeButton() {
    document.body.removeChild(document.getElementById('button'));
    // 此時,仍舊存在一個全局的 #button 的引用
    // elements 字典。button 元素仍舊在內存中,不能被 GC 回收。
}

3.2.2 問題二:內存泄漏如何優化?

關於內存分配通常有4中方法,分類進行優化:

1. new 關鍵詞,new 關鍵詞就意味着一次內存分配,例如 new Foo()。最好的處理方法是:在初始化的時候新建對象,然後在後續過程中儘量多的重用這些創建好的對象。

2. 數組

將[ ]賦值給一個數組對象,是清空數組的捷徑(例如: arr = []😉,但是需要注意的是,這種方式又創建了一個新的空對象,並且將原來的數組對象變成了一小片內存垃圾!實際上,將數組長度賦值爲0(arr.length = 0)也能達到清空數組的目的,並且同時能實現數組重用,減少內存垃圾的產生。

const arr = [1, 2, 3, 4];
arr.length = 0  // 可以直接讓數字清空,而且數組類型不變。
// arr = []; 雖然讓a變量成一個空數組,但是在堆上重新申請了一個空數組對象。

3. 對象

對象儘量複用,尤其是在循環等地方出現創建新對象,能複用就複用。不用的對象,儘可能設置爲null,儘快被垃圾回收掉。

var t = {} // 每次循環都會創建一個新對象。
for (var i = 0; i < 10; i++) {
  // var t = {};// 每次循環都會創建一個新對象。
  t.age = 19
  t.name = '123'
  t.index = i
  console.log(t)
}
t = null //對象如果已經不用了,那就立即設置爲null;等待垃圾回收。

4.函數

和上面的閉包一樣,儘量把函數放置到外層作爲返回值,然後使用調用的方式的方式

for (var k = 0; k < 10; k++) {
  var t = function(a) {
    // 創建了10次  函數對象。
    console.log(a)
  }
  t(k)
}

// 推薦用法
function t(a) {
  console.log(a)
}
for (var k = 0; k < 10; k++) {
  t(k)
}
t = null

相比於每次都新建一個方法對象,這種方式在每一幀當中重用了相同的方法對象。這種方式的優勢是顯而易見的,而這種思想也可以應用在任何以方法爲返回值或者在運行時創建方法的情況當中。

3.2.3 問題四:遞歸太深會導致棧溢出怎麼解決?

再次理解了堆和棧,以及垃圾回收機制,爲了加深深入理解,下面我們再看一下 深拷貝和淺拷貝中的應用

四、深拷貝和淺拷貝

4.1 解釋拷貝

我瀏覽了很多文檔,對於賦值、深淺拷貝說法不一,很多都是賦值和深淺拷貝分不清楚,在此,特意說明一下賦值和拷貝的區別!專門查了書,發現淺拷貝這裏的東西沒有具體定義,但是我更認可:引用類型的 賦值不等於淺拷貝淺拷貝是一層數據的拷貝,深拷貝是多層的拷貝這個觀點。

上述例2的過程是一個賦值操作,賦值的只是對象的引用,如上述obj2=obj1,實際上傳遞的只是obj1的內存地址,所以obj2和obj1指向的是同一個內存數據,所以這個內存數據中值的改變對obj1和obj2都有影響。這個過程是不同於深淺拷貝的!

4.1.1 到底什麼是淺拷貝?

MDN中數組的slice方法中有這句話 ,slice不會修改原數組,只會返回一個淺複製了原數組中的元素的一個新數組。原數組的元素會按照下述規則淺拷貝…那就用slice來驗證一下什麼是淺拷貝:

var a = [ 1, 3, 5, { x: 1 } ];
var b = Array.prototype.slice.call(a);
b[0] = 2;
b[3].x = 2
console.log(a); // [ 1, 3, 5, { x: 2 } ];
console.log(b); // [ 2, 3, 5, { x: 2 } ];

這裏b[0] = 2;時候a[0]沒有隨着改變,b[3].x = 2時候a[3].x發生了變化。

MDN中splice的規則:
。。
如果該元素是個對象引用 (不是實際的對象),slice會拷貝這個對象引用到新的數組裏。兩個對象引用都引用了同一個對象。如果被引用的對象發生改變,則新的和原來的數組中的這個元素也會發生改變。
。。
對於字符串、數字及布爾值來說(不是 String、Number 或者 Boolean 對象),slice會拷貝這些值到新的數組裏。在別的數組裏修改這些字符串或數字或是布爾值,將不會影響另一個數組。

  • 綜上:淺拷貝:新的數據複製了原數據中 非對象屬性的值對象屬性的引用,也就是說對象屬性並不複製到內存,但非對象屬性的值卻複製到內存中。
  • 而深拷貝會另外拷貝一份一個一模一樣的對象,從堆內存中開闢一個新的區域存放新對象,新對象跟原對象不共享內存,修改新對象不會改到原對象。

4.2 如何實現淺拷貝?

那怎麼才能不引用同一個堆中的數值呢?這就涉及到了其他拷貝方式,我們來實現一下:

4.2.1.使用循環實現只複製一層的淺拷貝

//例3
var obj1 = {name:'aa',age:18}
var obj2 = {}
for(const key in obj1){
	obj2[key] = obj1[key]
}
obj2.name = 'bb'
console.log(obj1) // { name: "aa", age: 18 }
console.log(obj2) // { name: "bb", age: 18 }

4.2.2.使用手動複製實現只複製一層的淺拷貝

//例4
var obj1 = {name:'aa',age:18}
var obj2 = {
	name:obj1.name,
	age:obj1.age
}
obj2.name = 'bb'
console.log(obj1) // { name: "aa", age: 18 }
console.log(obj2) // { name: "bb", age: 18 }

在例4中,我們再棧內存var obj2 = {}新建了一個地址指針,通過賦值,在堆中複製了name:‘aa’,age:18,obj2指向新的堆內存地址;
在這裏插入圖片描述
obj2.name = 'bb'時obj2指向的內存中的name改變,並不影響obj1中的值,在這裏插入圖片描述

4.2.3.通過Object.assign()實現一層的淺拷貝

//例5
let obj1 = { a: { b:'bb1'}, c: 'bb1'}
let obj2 = Object.assign({},obj1)
obj2.a.b = 'bb2';
obj2.c = 'cc2'
console.log(obj1); // { a:{ b: "bb2" }, c: "bb1" }
console.log(obj2); // { a:{ b: "bb2" }, c: "cc2" }

例5中的ES6中的Object.assign方法,如果對象只有一層的話可以使用,其原理和例4相同是:先新建一個空對象,在堆中複製相同的屬性,obj2指向另一個內存地址,但是這個方法不能使用在多層深拷貝!

4.2.4.Array.prototype.slice實現淺拷貝

//例6
var a = [ 1, 3, 5, { x: 1 } ];
var b = Array.prototype.slice.call(a);
b[0] = 2;
b[3].x = 2
console.log(a); // [ 1, 3, 5, { x: 2 } ];
console.log(b); // [ 2, 3, 5, { x: 2 } ];

上面已經解析過了,不再解析。

4.2.5.Array.prototype.concat實現淺拷貝

//例7
let array = [{a: 1}, {b: 2},666];
let array1 = [{c: 3},{d: 4}];
let array2= array.concat(array1);
array1[0].c= 123;
array[0].a = 456
array[2] = 999
console.log(array);// { a: 456 },{ b: 2 },999]
console.log(array1);// [ { c: 123 }, { d: 4 } ]
console.log(array2);// [ { a: 456 }, { b: 2 },666 { c: 123 }, { d: 4 } ]

這裏array2就只實現了一層的拷貝,數值類型被複制,所以array[2] = 999時候array2[2] = 666沒有改變,但是array2和array內層的對象引用地址相同,所有array[0].a = 456的時候array2也跟着變化了。

4.2.6.使用es6的擴展運算符 … 實現淺拷貝

//例8
let obj1 = [{ b:1}, 2]
let obj2 = [...obj1]
obj2[0].b = 'bb2';
obj1[1] = 3
console.log(obj1); // [{ b: "bb2" }, 3]
console.log(obj2); // [{ b: "bb2" }, 2]

擴展運算符只能用在可迭代的對象上,不會改變原數組,只會返回一個淺拷貝了原數組中的元素的一個新數組。

4.2.7.通過第三方庫Lodash來實現淺拷貝

官網:https://www.lodashjs.com/

var objects = [{ 'a': 1 }, { 'b': 2 }];

var shallow = _.clone(objects);
console.log(shallow[0] === objects[0]);

4.3 如何實現深拷貝?

4.3.1.通過JSON.parse(JSON.stringify(obj1))實現深拷貝

//例9
var obj1 = { body: { a: 10 } };
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.body.a = 20;
console.log(obj1);// { body: { a: 10 } }
console.log(obj2);// { body: { a: 20 } }
console.log(obj1 === obj2);// false
console.log(obj1.body === obj2.body);// false

這個方法是開發中最常用也是最簡單的,哈哈,but 你以爲真的這麼簡單嗎?這個深拷貝也是有缺陷的!

JSON.parse(JSON.stringify(obj1))的原理是:通過JSON.stringify(obj1)把obj1轉化爲字符串,再用JSON.parse把字符串轉化爲一個新對象來進行拷貝;

  • 這就只能拷貝數據類型,而拷貝不了對象的原型鏈,構造函數上面的方法或屬性;
  • 而且使用這個方法去拷貝的前提是 數據必須是JSON格式,如果你要拷貝的引用類型爲:RegExp,function是沒有辦法實現的!

4.3.2.通過例1循環遞歸賦值實現對象的深拷貝

//例10
let obj1 = {
    name: 'aa',
    age: 18,
    data: {
        mom: '小紅',
        else: {
            money: 9999
        }
    }
}
function clone(params) {
    if (typeof params === 'object') {
        let obj2 = {}
        for (const key in params) {
            obj2[key] = clone(params[key])
        }
        return obj2
    } else {
        return params
    }
}
let obj3 = clone(obj1)
obj3.data.mom = '小明'
obj3.age = 60
obj3.data.else.money = 666
console.log(obj1);
//{"name":"aa","age":18,"data":{"mom":"小紅","else":{"money":9999}}}
console.log(obj3);
//{"name":"aa","age":60,"data":{"mom":"小明","else":{"money":666}}}

當然,通過遞歸就能實現深拷貝,但是還是會有很多性能問題,在此就不一一例舉了,可以看這篇文章去加深自己的理解:面試特供深拷貝,我日常開發真的不會這樣寫的深拷貝!!

4.3.3.通過第三方庫Lodash來實現深拷貝

官網:https://www.lodashjs.com/

var objects = [{ 'a': 1 }, { 'b': 2 }];
 
var deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]);//false

這個庫還是很好用的,簡單操作。

4.4 總結淺拷貝和深拷貝

淺拷貝:創建一個新對象,這個對象有着原始對象屬性值的一份精確拷貝。如果屬性是基本類型,拷貝的就是基本類型的值,如果屬性是引用類型,拷貝的就是內存地址 ,所以如果其中一個對象改變了這個地址,就會影響到另一個對象。

深拷貝:將一個對象從內存中完整的拷貝一份出來,從堆內存中開闢一個新的區域存放新對象,且修改新對象不會影響原對象。

區別:淺拷貝是一層數據的拷貝,深拷貝是多層的拷貝

類型 和原數據是否指向同一地址 原數據只有一層數據 原數據有多層子數據
賦值 新對象改變 使原數據一同改變 改變 使原數據一同改變
淺拷貝 新對象改變 不會 使原數據改變 改變 使原數據一同改變
深拷貝 新對象改變 不會 使原數據一同改變 改變 不會 使原數據一同改變

結語

好了,差不多梳理結束了,這種內層的知識點,雖然平時不會特別重要,代碼怎麼都能寫,但是真正理解了後,可以避免很多BUG,最重要的是:面試會問 〒▽〒!

參考鏈接:
1.js垃圾回收機制
2.JS深拷貝和淺拷貝的實現
3.https://juejin.im/post/5d0706a6f265da1bc23f77a9#heading-14

如果本文對你有幫助的話,請不要忘記給我點贊打call哦~o( ̄▽ ̄)do
有問題留言 over~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章