js引用類型引發的問題

前言

背景javascript

  • 被問道的引用問題
    • 1 一次會議問道的引用類型問題,同窗想使用惰性加載,但願在網頁中fetch獲得的數據保存下了,若是某些不改變參數,就不在發起請求,前提是會對fetch返回的數據進行修改,但還有使用以前的fetchdata
    if(window.fetchData&&window.fetchDataSomeKeyLength=== window.fetchData.length) return
    const data = fetch(`${url}`
    window.fetchData = data
    window.fetchDataSomeKeyLength=data.someKey.length
    processData(data)//data.someKey的length作了處理
    複製代碼

  • 要get的點
    • 1 js基本類型和引用類型的區別
    • 2 棧存儲和堆存的區別
    • 3 js垃圾回收機制
    • 4 活動對象、執行上下文、this
    • 5 閉包的造成
    • 6 深copy的實現

基本類型和引用類型的區別

6種基本類型

  • string
  • number
  • bool
  • null
  • undefined
  • symbol

通俗易懂的話來說,js的基本類型使用用來存儲值得,它們分配大小是有限度 在定義基本類型變量的時候它們的內存都被分配完成,前端

  • 數字有最大值和最小值
  • null undefined的是固定的值
  • bool 值爲 true和false

stringnumberbooleansymbol 這四種類型統稱爲原始類型(Primitive) ,表示不能再細分下去的基本類型;symbol 表示獨一無二的值,經過 Symbol 函數調用生成,因爲生成的 symbol 值爲原始類型,因此 Symbol 函數不能使用 new 調用;nullundefined 一般被認爲是特殊值,這兩種類型的值惟一,就是其自己。java

引用類型

  • 對象es6

  • 數組web

  • 函數面試

和基本類型區分開來。對象在邏輯上是屬性的無序集合或者有序集合,是存放各類值的容器。對象值存儲的是引用地址,因此和基本類型值不可變的特性不一樣,對象值是可變的。算法

包裝對象

咱們知道對象擁有屬性和方法。但好比字符串這種基本類型值不屬於對象爲何還擁有屬性和方法呢?

實際上在引用字符串的屬性或方法時,會經過調用 new String() 的方式轉換成對象,該對象繼承了字符串的方法來處理屬性的引用,一旦引用結束,便會銷燬這個臨時對象,這就是包裝對象的概念。編程

不只僅只是字符串有包裝對象的概念,數字和布爾值也有相對應的 new Number()new Boolean() 包裝對象。nullundefined 沒有包裝對象,訪問它們的屬性會報類型錯誤。數組

字符串、數字和布爾值經過構造函數顯式生成的包裝對象,既然屬於對象,和基本類型的值必然是有區別的,這點能夠經過 typeof 檢測出來。安全

typeof 'seymoe'                 // 'string'
typeof new String('seymoe')     // 'object'

複製代碼

數據類型的判斷

  • typeof

  • instanceof

  • Object.prototype.toString()

    typeof
    `typeof` 操做符來判斷一個值屬於哪一種基本類型,返回值是一個string,對null判斷有誤,認爲null是個空指針
    typeof 'seymoe' // 'string' 
    typeof true // 'boolean' 
    typeof 10 // 'number' 
    typeof Symbol() // 'symbol' 
    typeof null // 'object' 
    沒法斷定是否爲 null 
    typeof undefined // 'undefined'
    複製代碼

    若是使用 typeof 操做符對對象類型及其子類型,譬如函數(可調用對象)、數組(有序索引對象)等進行斷定,則除了函數都會獲得 object 的結果。

    typeof {} // 'object'
    typeof [] // 'object'
    typeof(() => {})// 'function'
    複製代碼

    因爲沒法得知一個值究竟是數組仍是普通對象,顯然經過 typeof 判斷具體的對象子類型遠遠不夠。

    instanceof
    經過 `instanceof` 操做符也能夠對對象類型鏈上的構造函數進行斷定,其原理就是測試構造函數的 `prototype` 是否出如今被檢測對象的原型鏈上。 ``` [] instanceof Array // true ({}) instanceof Object // true (()=>{}) instanceof Function // true ```

    注意:instanceof 也不是萬能的。其原理就是測試構造函數

    var a={}
     a.__proto__=[]
     a instanceof Array //true
     a instanceof Object //true
    
    複製代碼
    Object.prototype.toString()
    `Object.prototype.toString()` 能夠說是斷定 JavaScript 中數據類型的終極解決方法了,具體用法請看如下代碼:
    Object.prototype.toString.call({})            // '[object Object]'
     Object.prototype.toString.call([])              // '[object Array]'
     Object.prototype.toString.call(() => {})        // '[object Function]'
     Object.prototype.toString.call('seymoe')        // '[object String]'
     Object.prototype.toString.call(1)               // '[object Number]'
     Object.prototype.toString.call(true)            // '[object Boolean]'
     Object.prototype.toString.call(Symbol())        // '[object Symbol]'
     Object.prototype.toString.call(null)            // '[object Null]'
     Object.prototype.toString.call(undefined)       // '[object Undefined]'
    
     Object.prototype.toString.call(new Date())      // '[object Date]'
     Object.prototype.toString.call(Math)            // '[object Math]'
     Object.prototype.toString.call(new Set())       // '[object Set]'
     Object.prototype.toString.call(new WeakSet())   // '[object WeakSet]'
     Object.prototype.toString.call(new Map())       // '[object Map]'
     Object.prototype.toString.call(new WeakMap())   // '[object WeakMap]'
    複製代碼

數據類型轉換

ToPrimitive
JavaScript 對象轉換到基本類型值時,會使用 ToPrimitive 算法,這是一個內部算法,是編程語言在內部執行時遵循的一套規則。

ToPrimitive 算法在執行時,會被傳遞一個參數 hint,表示這是一個什麼類型的運算(也能夠叫運算的指望值),根據這個 hint 參數,ToPrimitive 算法來決定內部的執行邏輯。

hint 參數的取值只能是下列 3 者之一:

  • string
  • number
  • default
轉換算法
當對象與到基本類型值發生轉換時,會按照下面的邏輯調用對象上的方法: **爲了進行轉換,JavaScript 會嘗試查找並調用三個對象方法:**
  1. 調用obj[Symbol.toPrimitive](hint)- 帶有符號鍵Symbol.toPrimitive(系統符號)的方法,若是存在這樣的方法,

  2. 不然若是提示是 "string"

    • 嘗試obj.toString()obj.valueOf(),不管存在什麼。
  3. 不然,若是提示是"number""default"

    • 嘗試obj.valueOf()obj.toString(),不管存在什麼。
肯定 hint

咱們提到了 ToPrimitive 算法中用到的 hint 參數,那怎樣肯定一次運算場景下的 hint 取值是什麼呢?很簡單----新建一個對象,打印各個運算場景下的 hint 值:

let obj = {
  name: "John",
  money: 1000,

  [Symbol.toPrimitive](hint) {
    console.log(`hint: ${hint}`);
  }
};

alert(obj) // hint: string 
+obj // hint: number
obj + 500 // hint: default



// 一個沒有提供 Symbol.toPrimitive 屬性的對象,參與運算時的輸出結果
var obj1 = {};
console.log(+obj1);     // NaN
console.log(`${obj1}`); // "[object Object]"
console.log(obj1 + ""); // "[object Object]"

// 接下面聲明一個對象,手動賦予了 Symbol.toPrimitive 屬性,再來查看輸出結果
var obj2 = {
  [Symbol.toPrimitive](hint) {
    if (hint == "number") {
      return 10;
    }
    if (hint == "string") {
      return "hello";
    }
    return true;
  }
};
console.log(+obj2);     // 10      -- hint 參數值是 "number"
console.log(`${obj2}`); // "hello" -- hint 參數值是 "string"
console.log(obj2 + ""); // "true"  -- hint 參數值是 "default"
複製代碼
## Symbol.toPrimitive 和 toString/valueOf 方法
並不要求 `Symbol.toPrimitive` 和 `toString/valueOf` 方法必須返回 `hint` 參數值所暗示的類型值。

但要注意下面兩點:

  1. Symbol.toPrimitivetoString 方法的返回值必須是基本類型值。
  2. valueOf 方法除了能夠返回基本類型值,也能夠返回其餘類型值。

當咱們建立一個普通對象時({}new Object() 的方式等),對象上是不具有 [Symbol.toPrimitive] (方法)屬性的。因此,對於普通對象的到基本類型值的運算,通常按照具體場景:

  1. hint 值爲 "string" 時,先調用 toStringtoString 若是返回一個基本類型值了,則返回、終止運算;不然接着調用 valueOf 方法。
  2. 不然,先調用 valueOfvalueOf 若是返回一個基本類型值了,則返回、終止運算;不然接着調用 toString 方法。

2 棧存儲和堆存的區別

棧數據結構

棧是一種特殊的列表,棧內的元素只能經過列表的一端訪問,這一端稱爲棧頂。 棧被稱爲是一種後入先出(LIFO,last-in-first-out)的數據結構。 因爲棧具備後入先出的特色,因此任何不在棧頂的元素都沒法訪問。 爲了獲得棧底的元素,必須先拿掉上面的元素。

在這裏,爲方便理解,經過類比乒乓球盒子來分析棧的存取方式。

16b8c0af7dd2aa15.png

這種乒乓球的存放方式與棧中存取數據的方式一模一樣。 處於盒子中最頂層的乒乓球 5,它必定是最後被放進去,但能夠最早被使用。 而咱們想要使用底層的乒乓球 1,就必須將上面的 4 個乒乓球取出來,讓乒乓球1處於盒子頂層。 這就是棧空間先進後出,後進先出的特色。

堆數據結構
堆是一種通過排序的樹形數據結構,每一個結點都有一個值。 一般咱們所說的堆的數據結構,是指二叉堆。 堆的特色是根結點的值最小(或最大),且根結點的兩個子樹也是一個堆。 因爲堆的這個特性,經常使用來實現優先隊列,堆的存取是隨意,這就如同咱們在圖書館的書架上取書, 雖然書的擺放是有順序的,可是咱們想取任意一本時沒必要像棧同樣,先取出前面全部的書, 咱們只須要關心書的名字。
變量類型與內存的關係

基本數據類型保存在棧內存中,由於基本數據類型佔用空間小、大小固定,經過按值來訪問,屬於被頻繁使用的數據。 爲了更好的搞懂基本數據類型變量與棧內存,咱們結合如下例子與圖解進行理解:

let num1 = 1; 
let num2 = 1;
複製代碼

16b8c0b2fba2bdef.png

引用數據類型存儲在堆內存中,由於引用數據類型佔據空間大、大小不固定。 若是存儲在棧中,將會影響程序運行的性能; 引用數據類型在棧中存儲了指針,該指針指向堆中該實體的起始地址。 當解釋器尋找引用值時,會首先檢索其在棧中的地址,取得地址後從堆中得到實體

// 基本數據類型-棧內存
let a1 = 0;
// 基本數據類型-棧內存
let a2 = 'this is string';
// 基本數據類型-棧內存
let a3 = null;

// 對象的指針存放在棧內存中,指針指向的對象存放在堆內存中
let b = { m: 20 };
// 數組的指針存放在棧內存中,指針指向的數組存放在堆內存中
let c = [1, 2, 3];

複製代碼

16b8c0b5752823f6.png 所以當咱們要訪問堆內存中的引用數據類型時,實際上咱們首先是從變量中獲取了該對象的地址指針, 而後再從堆內存中取得咱們須要的數據。

從內存角度來看變量複製
let a = 20;
let b = a;
b = 30;
console.log(a); // 此時a的值是50

複製代碼

在這個例子中,a、b 都是基本類型,它們的值是存儲在棧內存中的,a、b 分別有各自獨立的棧空間, 因此修改了 b 的值之後,a 的值並不會發生變化。

16b8c0b73d4ebd08.png

引用數據類型的複製
let m = { a: 10, b: 20 };
let n = m;
n.a = 15;
console.log(m.a) //此時m.a的值是多少,是10?仍是15?

複製代碼

在這個例子中,m、n都是引用類型,棧內存中存放地址指向堆內存中的對象, 引用類型的複製會爲新的變量自動分配一個新的值保存在變量中, 但只是引用類型的一個地址指針而已,實際指向的是同一個對象, 因此修改 n.a 的值後,相應的 m.a 也就發生了改變。

16b8c0b9df03d885.png

棧內存和堆內存的優缺點

在JS中,基本數據類型變量大小固定,而且操做簡單容易,因此把它們放入棧中存儲。 引用類型變量大小不固定,因此把它們分配給堆中,讓他們申請空間的時候本身肯定大小,這樣把它們分開存儲可以使得程序運行起來佔用的內存最小。

棧內存因爲它的特色,因此它的系統效率較高。 堆內存須要分配空間和地址,還要把地址存到棧中,因此效率低於棧。

3 js垃圾回收機制

爲何要有垃圾回收

在C語言和C++語言中,咱們若是想要開闢一塊堆內存的話,須要先計算須要內存的大小,而後本身經過malloc函數去手動分配,在用完以後,還要時刻記得用free函數去清理釋放,不然這塊內存就會被永久佔用,形成內存泄露。

可是咱們在寫JavaScript的時候,卻沒有這個過程,由於人家已經替咱們封裝好了,V8引擎會根據你當前定義對象的大小去自動申請分配內存。

不須要咱們去手動管理內存了,因此天然要有垃圾回收,不然的話只分配不回收,豈不是沒多長時間內存就被佔滿了嗎,致使應用崩潰。

垃圾回收的好處是不須要咱們去管理內存,把更多的精力放在實現複雜應用上,但壞處也來自於此,不用管理了,就有可能在寫代碼的時候不注意,形成循環引用等狀況,致使內存泄露。

垃圾回收機制
標記清除

當變量進入環境(例如,在函數中聲明一個變量)時,就將這個變量標記爲「進入環境」。從邏輯上講,永遠不能釋放進入環境的變量所佔用的內存,由於只要執行流進入相應的環境,就可能會用到它們。而當變量離開環境時,則將其標記爲「離開環境」。

可使用任何方式來標記變量。好比,能夠經過翻轉某個特殊的位來記錄一個變量什麼時候進入環境,或者使用一個「進入環境的」變量列表及一個「離開環境的」變量列表來跟蹤哪一個變量發生了變化。如何標記變量並不重要,關鍵在於採起什麼策略。

  • (1)垃圾收集器在運行的時候會給存儲在內存中的全部變量都加上標記(固然,可使用任何標記方式)。
  • (2)而後,它會去掉運行環境中的變量以及被環境中變量所引用的變量的標記
  • (3)此後,依然有標記的變量就被視爲準備刪除的變量,緣由是在運行環境中已經沒法訪問到這些變量了。
  • (4)最後,垃圾收集器完成內存清除工做,銷燬那些帶標記的值並回收它們所佔用的內存空間。

目前,IE、Firefox、Opera、Chrome和Safari的JavaScript實現使用的都是標記清除式的垃圾回收策略(或相似的策略),只不過垃圾收集的時間間隔互有不一樣。

引用計數
引用計數的垃圾收集策略不太常見。含義是跟蹤記錄每一個值被引用的次數。當聲明瞭一個變量並將一個引用類型值賦給該變量時,則這個值的引用次數就是1。

若是同一個值又被賦給另外一個變量,則該值的引用次數加1。相反,若是包含對這個值引用的變量改變了引用對象,則該值引用次數減1。

當這個值的引用次數變成0時,則說明沒有辦法再訪問這個值了,於是就能夠將其佔用的內存空間回收回來。

這樣,當垃圾收集器下次再運行時,它就會釋放那些引用次數爲0的值所佔用的內存。

循環引用是指對象A中包含一個指向對象B的指針,而對象B中也包含一個指向對象A的引用,看個例子:
複製代碼
function foo () {
    var objA = new Object();
    var objB = new Object();
    
    objA.otherObj = objB;
    objB.anotherObj = objA;
}

複製代碼

這個例子中,objA和objB經過各自的屬性相互引用,也就是說,這兩個對象的引用次數都是2。

在採用標記清除策略的實現中,因爲函數執行後,這兩個對象都離開了做用域,所以這種相互引用不是問題。

但在採用引用次數策略的實現中,當函數執行完畢後,objA和objB還將繼續存在,由於它們的引用次數永遠不會是0。加入這個函數被重複屢次調用,就會致使大量內存沒法回收

還要注意的是,咱們大部分人時刻都在寫着循環引用的代碼,看下面這個例子,相信你們都這樣寫過:

var el = document.getElementById('#el');
el.onclick = function (event) {
    console.log('element was clicked');
}

複製代碼

咱們爲一個元素的點擊事件綁定了一個匿名函數,咱們經過event參數是能夠拿到相應元素el的信息的。

你們想一想,這是否是就是一個循環引用呢? el有一個屬性onclick引用了一個函數(其實也是個對象),函數裏面的參數又引用了el,這樣el的引用次數一直是2,即便當前這個頁面關閉了,也沒法進行垃圾回收。

若是這樣的寫法不少不少,就會形成內存泄露。咱們能夠經過在頁面卸載時清除事件引用,這樣就能夠被回收了

var el = document.getElementById('#el');
el.onclick = function (event) {
    console.log('element was clicked');
}

// ...
// ...

// 頁面卸載時將綁定的事件清空
window.onbeforeunload = function(){
    el.onclick = null;
}

複製代碼
V8垃圾回收策略
自動垃圾回收有不少算法,因爲不一樣對象的生存週期不一樣,因此沒法只用一種回收策略來解決問題,這樣效率會很低。

因此,V8採用了一種代回收的策略,將內存分爲兩個生代:新生代(new generation)老生代(old generation)

新生代中的對象爲存活時間較短的對象,老生代中的對象爲存活時間較長或常駐內存的對象,分別對新老生代採用不一樣的垃圾回收算法來提升效率,對象最開始都會先被分配到新生代(若是新生代內存空間不夠,直接分配到老生代),新生代中的對象會在知足某些條件後,被移動到老生代,這個過程也叫晉升,後面我會詳細說明。

分代內存

默認狀況下,32位系統新生代內存大小爲16MB,老生代內存大小爲700MB,64位系統下,新生代內存大小爲32MB,老生代內存大小爲1.4GB。

新生代平均分紅兩塊相等的內存空間,叫作semispace,每塊內存大小8MB(32位)或16MB(64位)。

分配方式

新生代存的都是生存週期短的對象,分配內存也很容易,只保存一個指向內存空間的指針,根據分配對象的大小遞增指針就能夠了,當存儲空間快要滿時,就進行一次垃圾回收。

算法

新生代採用Scavenge垃圾回收算法,在算法實現時主要採用Cheney算法。 Cheney算法將內存一分爲二,叫作semispace,一塊處於使用狀態,一塊處於閒置狀態。

162c3526b85b16a7.png

處於使用狀態的semispace稱爲From空間,處於閒置狀態的semispace稱爲To空間

接下來我會結合流程圖來詳細說明Cheney算法是怎麼工做的。 垃圾回收在下面我統稱爲 GC(Garbage Collection)step1. 在From空間中分配了3個對象A、B、C

162c3526d601da9e.png

step2. GC進來判斷對象B沒有其餘引用,能夠回收,對象A和C依然爲活躍對象

162c3526ee73c6b5.png

step3. 將活躍對象A、C從From空間複製到To空間

162c3526f003cd95.png

step4. 清空From空間的所有內存

162c3527027f9a35.png

step5. 交換From空間和To空間

162c352706984982.png

step6. 在From空間中又新增了2個對象D、E

162c3527047d8e26.png

step7. 下一輪GC進來發現對象D沒有引用了,作標記

162c3527073d80a9.png

step8. 將活躍對象A、C、E從From空間複製到To空間

162c3527099bae4b.png

step9. 清空From空間所有內存

162c35270c3a80b6.webp

step10. 繼續交換From空間和To空間,開始下一輪

162c35271dd2cfd7.png

經過上面的流程圖,咱們能夠很清楚的看到,進行From和To交換,就是爲了讓活躍對象始終保持在一塊semispace中,另外一塊semispace始終保持空閒的狀態。

Scavenge因爲只複製存活的對象,而且對於生命週期短的場景存活對象只佔少部分,因此它在時間效率上有優異的體現。Scavenge的缺點是隻能使用堆內存的一半,這是由劃分空間和複製機制所決定的。

因爲Scavenge是典型的犧牲空間換取時間的算法,因此沒法大規模的應用到全部的垃圾回收中。但咱們能夠看到,Scavenge很是適合應用在新生代中,由於新生代中對象的生命週期較短,偏偏適合這個算法。

晉升
當一個對象通過屢次複製仍然存活時,它就會被認爲是生命週期較長的對象。這種較長生命週期的對象隨後會被移動到老生代中,採用新的算法進行管理。

對象重新生代移動到老生代的過程叫做晉升

對象晉升的條件主要有兩個:

  1. 對象從From空間複製到To空間時,會檢查它的內存地址來判斷這個對象是否已經經歷過一次Scavenge回收。若是已經經歷過了,會將該對象從From空間移動到老生代空間中,若是沒有,則複製到To空間。總結來講,若是一個對象是第二次經歷從From空間複製到To空間,那麼這個對象會被移動到老生代中
  2. 當要從From空間複製一個對象到To空間時,若是To空間已經使用了超過25%,則這個對象直接晉升到老生代中。設置25%這個閾值的緣由是當此次Scavenge回收完成後,這個To空間會變爲From空間,接下來的內存分配將在這個空間中進行。若是佔比太高,會影響後續的內存分配
老生代
在老生代中,存活對象佔較大比重,若是繼續採用Scavenge算法進行管理,就會存在兩個問題:
  1. 因爲存活對象較多,複製存活對象的效率會很低。
  2. 採用Scavenge算法會浪費一半內存,因爲老生代所佔堆內存遠大於新生代,因此浪費會很嚴重。

因此,V8在老生代中主要採用了Mark-SweepMark-Sweep相結合的方式進行垃圾回收。

Mark-Sweep
Mark-Sweep是標記清除的意思,它分爲標記和清除兩個階段。

與Scavenge不一樣,Mark-Sweep並不會將內存分爲兩份,因此不存在浪費一半空間的行爲。Mark-Sweep在標記階段遍歷堆內存中的全部對象,並標記活着的對象,在隨後的清除階段,只清除沒有被標記的對象。

也就是說,Scavenge只複製活着的對象,而Mark-Sweep只清除死了的對象。活對象在新生代中只佔較少部分,死對象在老生代中只佔較少部分,這就是兩種回收方式都能高效處理的緣由。

step1. 老生代中有對象A、B、C、D、E、F 162c35271e20f9ab.png

step2. GC進入標記階段,將A、C、E標記爲存活對象

162c3527204267ca.png step3. GC進入清除階段,回收掉死亡的B、D、F對象所佔用的內存空間

162c3527267e7eae.png

能夠看到,Mark-Sweep最大的問題就是,在進行一次清除回收之後,內存空間會出現不連續的狀態。這種內存碎片會對後續的內存分配形成問題。

若是出現須要分配一個大內存的狀況,因爲剩餘的碎片空間不足以完成這次分配,就會提早觸發垃圾回收,而此次回收是沒必要要的。

Mark-Compact
爲了解決Mark-Sweep的內存碎片問題,Mark-Compact就被提出來了。

**Mark-Compact是標記整理的意思,**是在Mark-Sweep的基礎上演變而來的。Mark-Compact在標記完存活對象之後,會將活着的對象向內存空間的一端移動,移動完成後,直接清理掉邊界外的全部內存。以下圖所示: step1. 老生代中有對象A、B、C、D、E、F(和Mark—Sweep同樣)

162c3527267a55b2.png step2. GC進入標記階段,將A、C、E標記爲存活對象(和Mark—Sweep同樣)

162c3527204267ca.png step3. GC進入整理階段,將全部存活對象向內存空間的一側移動,灰色部分爲移動後空出來的空間

162c35272976bf46.png step4. GC進入清除階段,將邊界另外一側的內存一次性所有回收

162c352731840c87.png

二者結合

在V8的回收策略中,Mark-Sweep和Mark-Conpact二者是結合使用的。

因爲Mark-Conpact須要移動對象,因此它的執行速度不可能很快,在取捨上,V8主要使用Mark-Sweep,在空間不足以對重新生代中晉升過來的對象進行分配時,才使用Mark-Compact。

總結

V8的垃圾回收機制分爲新生代和老生代。

新生代主要使用Scavenge進行管理,主要實現是Cheney算法,將內存平均分爲兩塊,使用空間叫From,閒置空間叫To,新對象都先分配到From空間中,在空間快要佔滿時將存活對象複製到To空間中,而後清空From的內存空間,此時,調換From空間和To空間,繼續進行內存分配,當知足那兩個條件時對象會重新生代晉升到老生代。

老生代主要採用Mark-Sweep和Mark-Compact算法,一個是標記清除,一個是標記整理。二者不一樣的地方是,Mark-Sweep在垃圾回收後會產生碎片內存,而Mark-Compact在清除前會進行一步整理,將存活對象向一側移動,隨後清空邊界的另外一側內存,這樣空閒的內存都是連續的,可是帶來的問題就是速度會慢一些。在V8中,老生代是Mark-Sweep和Mark-Compact二者共同進行管理的。

以上就是本文的所有內容,書寫過程當中參考了不少中外文章,參考書籍包括樸大大的《深刻淺出NodeJS》以及《JavaScript高級程序設計》等。咱們這裏並無對具體的算法實現進行探討,感興趣的朋友能夠繼續深刻研究一下。

最後,謝謝你們可以讀到這裏,若是文中有任何不明確或錯誤的地方,歡迎給我留言~~

4 執行環境、執行上下文、活動對象、this

執行環境

執行環境是js中重要的一個概念。執行環境定義了變量和函數有權訪問其餘變量,決定了他們的各自行爲,每一個函數執行都有本身的執行環境,當執行流入一個函數,函數的執行環境就會給推到當前執行棧中,函數執行完畢,函數的執行環境就會被彈出,執行權交給當前棧,這就是js的執行流

變量對象

每一個執行環境都有一個與之關聯變量對象,環境中定義的全部的變量和函數都保存在這個變量中,雖然咱們編寫代碼沒法訪問這個對象,可是解析器可以在處理數據的時會在後臺使用。

全局執行環境

全局執行環境,是最外圍的一個執行環境。根據ecmascript實現所在的宿主不一樣,表示執行環境也不同,web全局執行環境被認爲是window,所以全局全部的變量和函數都被認爲是window的屬性和函數被建立,某個執行環境的中的代碼執行完畢後,該環境就會給銷燬,該環境變量對象也會被銷燬

做用域鏈

當代碼在執行環境中執行時,會建立變量對象的一個做用域鏈,做用域鏈的用途,是保證執行環境有權訪問全部的變量和函數有序訪問,做用域的最前端是當前執行環境的的變量對象,若是這個環境是函數,就將其 **活動對象** ,做爲變量對象,活動對象剛開始就只包含一個變量就是arguments對象(這個對象在全局是不存在的),做用域的下一個變量對象來之與當前函數所在的執行棧的變量對象(能夠理解爲當前函數的執行棧),下一個的下一個就是當前函數執行棧的執行棧,這樣一直延續到全局執行環境中的變量對象,爲做用域的末端。

標識符解析(變量查找),是按照做用域鏈一級一級的操做,查找順序是從當前變量對象開始,知道找到爲止,若是找不到就會一般會有異常

var color = "blue";
function changeColor() {
var otherColor = "red";
function swapColor() {
 var tempColor = otherColor;
 otherColor = color;
 color = tempColor;
 // 這裏能夠訪問 tempColor otherColor color
}
swapColor();
// 這裏能夠訪問  otherColor color swapColor
}
changeColor();
// 這裏能夠訪問 changeColor  color
複製代碼

看圖

image.png

this是什麼

通常對this的誤解分爲兩個方面
  • 1 this是指向當前函數的自己
  • 2 this 指向的是當前函數的 做用域

this是指向當前函數的自己

下面代碼中你們要理解函數的多面性,多個身份

  • 普通的函數
  • 普通的對象
  • 構造函數

接下來說用到函數的是兩個身份普通函數、普通對象, 看代碼()

function foo(){
    this.count++
}
var count=0;
foo.count=0;
for(var i=0;i<5;i++){
    
    foo()
}
console.log(foo.count)//0
console.log(count)//5

複製代碼

從打印的結果上來看顯然,this指向的不是自己函數,固然我們通常看到這類的問題我們就會繞道而行,看代碼

function foo(){
    this.count++
}
var bar={
    count:0
}
foo.count=0;
for(var i=0;i<5;i++){
    
    foo.call(bar)
}
console.log(bar.count)//5
console.log(count)//0

複製代碼

雖然這種解決方案很好,也會有其餘的解決方案,可是咱們仍是不理解this的問題,內心仍是有種不安之感

this 指向的是當前函數的 做用域

接下來說用到函數的是兩個身份普通函數、普通對象, 看代碼()

function foo(){
     var num=2;
     console.log(this.num)
 }
 var num=0;
 foo()//0

複製代碼

我們看到代碼的執行結果後,發現this指向的並非該函數的做用域。

this究竟是什麼

this是在函數調用的時候綁定,不是在函數定義的時候綁定。它的上下文取決於函數調用時的各類條件,函數執行的時候會建立一個活動記錄,這個記錄裏面包含了該函數中定義的參數和參數,包含函數在哪裏被調用(調用棧)...,this就是其中的一個屬性。 來看圖

圖中我們看到this是在函數執行的時候建立的。


全面解析this


前面幾步我們已經肯定的this的建立和this的指向的誤區,接下啦我們要看看this的綁定的規則,分爲4個規則。

  • 默認綁定
  • 隱式綁定(上下文綁定)
  • 顯式綁定
  • new 綁定


默認綁定


默認綁定的字面意思就是,不知足其餘的綁定方式,而執行的綁定規則。默認綁定會把this綁定到全局對象(是一個危險的操做,文章後面會說爲何) 看代碼

function foo(){
     var num=2;
     this.num++
     console.log(this.num)
 }
 var num=0;
 foo()//1
複製代碼

上面代碼中就實現了默認綁定,在foo方法的代碼塊中操做的是window.num++。


隱式綁定(上下文綁定)


定義:
函數被調用的位置有上下文,或者是該函數的引用地址是否是被某個對象的屬性引用,並經過對象的屬性直接運行該函數。若是出現上述的狀況,就會觸發this的隱式綁定,this就會被綁定成當前對象 看代碼

function foo(){
    console.log(this.name)
}
var bar={
    name:'shiny',
    foo:foo
}
bar.foo()//shiny
複製代碼

要須要補充一點,無論你的對象嵌套多深,this只會綁定爲直接引用該函數的地址屬性的對象,看代碼

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny',
    foo:foo
}
var red={
    name:'red',
    obj:shiny
    
}
red.obj.foo()//shiny
複製代碼


隱式綁定的丟失


先看代碼

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny',
    foo:foo
}
function doFoo(fn){
    fn()
}
doFoo(shiny.foo)//undefind
複製代碼

你們知道函數參數在函數執行的時候,其實有一個賦值的操做,我來解釋一下上面的,當函數doFoo執行的時候會開闢一個新的棧並被推入到全局棧中執行,在執行的過程當中會建立一個活動對象,這個活動對象會被賦值傳入的參數以及在函數中定義的變量函數,在函數執行時用到的變量和函數直接從該活動對象上面取值使用。 看圖 doFoo的執行棧

fn的執行棧

看下面原理和上面同樣經過賦值,致使隱式綁定的丟失,看代碼

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny',
    foo:foo
}
var bar = shiny.foo
bar()//undefined
複製代碼

你們是否是已經明白了爲何是undefined,來解釋一波,其實shiny的foo屬性是引用了foo函數的引用內存地址,那麼有把foo的引用地址賦值給了 bar 那麼如今的bar的引用地址個shiny.foo的引用地址是一個,那麼執行bar的時候也會觸發默認綁定規則由於沒有其餘規則能夠匹配,bar函數執行時,函數內部的this綁定的是全局變量。

看下滿的引用地址賦值是出現的,奇葩 隱式綁定丟失,看代碼

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny',
    foo:foo
}
var red={
    name:'red'
}
(red.foo=shiny.foo)()//undefined
複製代碼

賦值表達式 p.foo = o.foo 的返回值是目標函數的引用,所以調用位置是 foo() 而不是 p.foo() 或者 o.foo()。根據咱們以前說過的,這裏會應用默認綁定。


顯式綁定


call、apply綁定


javascript,在Function的porpertype上提供了3個方法來強行修改this,分別是 call、apply、bind,你們常常用的莫過於call和apply了,這兩個函數的第一個參數,都是須要執行函數綁定的this,對於apply只有連個參數,第二個參數是一個數組,這個數組是要傳入執行函數的參數,而call能夠跟不少參數,從第二個參數起都會被傳入到要執行函數的參數中

看代碼

function foo(){
   console.log(this.age)
}
var shiny={
   age:20
}
foo.call(shiny)//20

function bar(){
console.log(this.age)
}
var red={
age:18
}
bar.apply(red)//18
複製代碼

這兩個方法都是顯式的綁定了tihs

硬綁定:


相似與 bind方法行爲,是顯式綁定的一種方式

function foo(b){
  return this.a+b
}
var obj={
  a:2
}
function bind(fn,obj){
  return function(){
     return fn.apply(obj,arguments)
  }
}
bind(foo,obj)(3)//5
複製代碼

語言解釋: 經過apply + 閉包機制 實現bind方法,實現強行綁定規則

API調用的「上下文」 第三方庫或者寄生在環境,以及js內置的一些方法都提供了一下 content 上下文參數,他的做用和 bind同樣,就是確保回調函數的this被綁定

function foo (el){
  console.log(el,this.id)
}
var obj ={
 id:'some one'
};
[1,2,4].forEach(foo,obj)
// 1 some one 2 some one 4 some one
複製代碼


new 綁定


說道new 你們都會想到js的構造函數,我們想不用着急new 綁定this的問題,我們先看看我們對js的構造函數的誤解,傳統面向類的語言中的構函數和js的構造函數時不同

  • 傳統面向類的語言中的構函數,是在使用new操做符實例化類的時候,會調用類中的一些特殊方法(構造函數)

  • 不少人認爲js中的new操做符和傳統面向類語言的構造函數是同樣的,其實有很大的差異

  • 重新認識一下js中的構造函數,js中的構造函數 在被new操做符調用時,這個構造函數不屬於每一個類,也不會創造一個類,它就是一個函數,只是被new操做符調用。

  • 使用new操做符調用 構造函數時會執行4步

    • 建立一個全新的對象
    • 對全新的對象的__proto__屬性地址進行修改爲構造函數的原型(prototype)的引用地址
    • 構造函數的this被綁定爲這個全新的對象
    • 若是構造函數有返回值而且這個返回值是一個對象,則返回該對象,不然返回當前新對象

我們瞭解了js new 操做符調用構造函數時都作了些什麼,哪麼我們就知道構造函數裏面的this是誰了

代碼實現

function Foo(a){
  this.a=a
}
var F = new Foo(2)
console.log(F.a)//2
複製代碼


綁定規則的順序


我們在上面瞭解this綁定的4大規則,那麼我們就看看這4大綁定規則的優先級。

默認綁定

我們根據字面意思,都能理解只有其他的3個綁定規則沒法觸發的時候就會觸發默認綁定,沒有比較意義


顯式綁定 VS 隱式綁定


看代碼

function foo(){
    console.log(this.name)
}
var  shiny={
    name:'shiny',
    foo:foo
}
var red={
    name:'red'
}

shiny.foo()//shiny
shiny.foo.call(red)// red
shiny.foo.apply(red)// red
shiny.foo.bind(red)()//red
複製代碼

顯然在這場綁定this比賽中,顯式綁定贏了隱式綁定


隱式綁定 VS new 操做符綁定


看代碼

function  foo(name){
    this.name=name
}
var shiny={
    foo:foo
}
shiny.foo('shiny')
console.log(shiny.name)//shiny

var red = new shiny.foo('red')
console.log(red.name)//red
複製代碼

顯然在這場綁定this比賽中new 操做符綁定贏了隱式綁定


顯式綁定(硬綁定) VS new 操做符綁定


使用call、apply方法不能結合new操做符會報錯誤

可是我們能夠是bind綁定this來比較 顯式綁定和new操做符的綁定this優先級。 看代碼

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny'
}

var bar = foo.bind(shiny)
var obj = new bar();
console.log(obj.name)// undefind
複製代碼

顯然 new操做符綁定 打敗了 顯式綁定


this的判斷


我們在上面已經瞭解 4個綁定this的優先級。我們能夠列舉出來

  • 1 判斷該函數是否是被new操做符調用,有的話 this就是 構造函數運行時建立的新對象 var f = new foo()
  • 2 判斷 函數是否是使用顯式綁定 call、apply、bind,若是有,那麼該函數的this就是 這個三個方法的第一個參數

foo.call(window)

  • 3 判斷該函數是否是被一個對象的屬性引用了地址,該函數有上下文(隱式綁定),在函數執行的時候是經過該對象屬性的引用觸發,這個函數的this就是當前對象的。

obj.foo();

  • 4 上面的三種都沒有的話,就是默認綁定,該函數的this就是全局對象或undefined(嚴格模式下)


綁定例外


😁 規則老是會有意外的,this綁定也是會有的,某些場面的綁定也是會出乎意料的,有可能觸發了默認綁定 看代碼

function foo(){
    console.log(name)
}
var name ='shiny'
foo.call(null)//shiny
foo.call(undefined)//shiny
var bar = foo.bind(null)
var baz = foo.bind(undefined)
bar()//siny
baz()//siny
複製代碼

把 null、undefined經過 apply、call、bind 顯式綁定,雖然實現可默認綁定,可是建議這麼作由於在非嚴格的模式下會給全局對象添加屬性,有時候會形成不可必要的bug。


更安全的this


我們從上面知道在非嚴格模式下 默認綁定是並操做this的話會該全局對象添加屬性,這樣的操做是有風險性的

function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 咱們的空對象
var ø = Object.create( null );
// 把數組展開成參數
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用 bind(..) 進行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3

複製代碼


es6中的this


在es5及一下版本,咱們被this深深的困惑,可是看完了上面的文章,應該判斷this沒有關係,可是 重點來了 es6的this能夠經過箭頭函數直接綁定在該函數的執行的做用域上。 看代碼

function foo(){
     return ()=>{
          console.log(this.name)
     }
 }
 var obj ={
     name:'obj'
 }
  var shiny ={
     name:'shiny'
 }
 var bar = foo.call(obj);
 bar.call(shiny)// foo
 

複製代碼

咱們看到箭頭函數的this被綁定到該函數執行的做用域上。

我們在看看 js內部提供內置函數使用箭頭函數

function foo() {
    setTimeout(() => {
    // 這裏的 this 在此法上繼承自 foo()
    console.log( this.a );
    },100);
}
var obj = {
    a:2
};
foo.call( obj ); // 2
複製代碼

箭頭函數能夠像 bind(..) 同樣確保函數的 this 被綁定到指定對象,此外,其重要性還體 如今它用更常見的詞法做用域取代了傳統的 this 機制。實際上,在 ES6 以前咱們就已經 在使用一種幾乎和箭頭函數徹底同樣的模式。

function foo() {
var self = this; // lexical capture of this
setTimeout( function(){
    console.log( self.a );
    }, 100 );
}
var obj = {
    a: 2
};
foo.call( obj ); // 2
複製代碼

雖然 self = this 和箭頭函數看起來均可以取代 bind(..),可是從本質上來講,它們想替 代的是 this 機制。 若是你常常編寫 this 風格的代碼,可是絕大部分時候都會使用 self = this 或者箭頭函數。 若是徹底採用 this 風格,在必要時使用 bind(..),儘可能避免使用 self = this 和箭頭函數。

5 閉包的造成

閉包

有關如何建立做用域鏈以及做用域鏈有什麼做用的細節,對完全 理解閉包相當重要。當某個函數被調用時,會建立一個執行環境(execution context)及相應的做用域鏈。 而後,使用 arguments 和其餘命名參數的值來初始化函數的活動對象(activation object)。但在做用域 鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,……直至做爲做用域鏈終點的全局執行環境

在函數執行過程當中,爲讀取和寫入變量的值,就須要在做用域鏈中查找變量。來看下面的例子。

function compare(value1, value2) {
if (value1 < value2) {
 return -1;
} else if (value1 > value2) {
 return 1;
} else {
 return 0;
}
}
var result = compare(5, 10);
複製代碼

以上代碼先定義了 compare()函數,而後又在全局做用域中調用了它。當調用 compare()時,會 建立一個包含 arguments、value1 和 value2 的活動對象。全局執行環境的變量對象(包含 result 和 compare)在 compare()執行環境的做用域鏈中則處於第二位。圖片 展現了包含上述關係的 compare()函數執行時的做用域鏈。

image.png 後臺的每一個執行環境都有一個表示變量的對象——變量對象。全局環境的變量對象始終存在,而像 compare()函數這樣的局部環境的變量對象,則只在函數執行的過程當中存在。在建立 compare()函數 時,會建立一個預先包含全局變量對象的做用域鏈,這個做用域鏈被保存在內部的[[Scope]]屬性中。 當調用 compare()函數時,會爲函數建立一個執行環境,而後經過複製函數的[[Scope]]屬性中的對 象構建起執行環境的做用域鏈。此後,又有一個活動對象(在此做爲變量對象使用)被建立並被推入執 行環境做用域鏈的前端。對於這個例子中 compare()函數的執行環境而言,其做用域鏈中包含兩個變 量對象:本地活動對象和全局變量對象。顯然,做用域鏈本質上是一個指向變量對象的指針列表,它只 引用但不實際包含變量對象。 不管何時在函數中訪問一個變量時,就會從做用域鏈中搜索具備相應名字的變量。通常來說, 當函數執行完畢後,局部活動對象就會被銷燬,內存中僅保存全局做用域(全局執行環境的變量對象)。 可是,閉包的狀況又有所不一樣。

在看一個案例

function createComparisonFunction(propertyName) {
  return function (object1, object2) {
    var value1 = object1[propertyName];
    var value2 = object2[propertyName];

    if (value1 < value2) {
      return -1;
    } else if (value1 > value2) {
      return 1;
    } else {
      return 0;
    }
  };
}
複製代碼

在這個例子中,object1[propertyName] object2[propertyName] 兩行代碼是內部函數(一個匿名函數)中的代碼,這兩行代碼訪問了外部 函數中的變量 propertyName。即便這個內部函數被返回了,並且是在其餘地方被調用了,但它仍然可 以訪問變量 propertyName。之因此還可以訪問這個變量,是由於內部函數的做用域鏈中包含 createComparisonFunction()的做用域。要完全搞清楚其中的細節,必須從理解函數被調用的時候 都會發生什麼入手。

當某個函數被調用時,會建立一個執行環境(execution context)及相應的做用域鏈。 而後,使用 arguments 和其餘命名參數的值來初始化函數的活動對象(activation object)。但在做用域 鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,……直至做爲做用域鏈終點的全局執行環境

看圖

image.png

閉包與變量

做用域鏈的這種配置機制引出了一個值得注意的反作用,即閉包只能取得包含函數中任何變量的最 後一個值。別忘了閉包所保存的是整個變量對象,而不是某個特殊的變量。下面這個例子能夠清晰地說 明這個問題。
function createFunctions() {
  var result = new Array();
  for (var i = 0; i < 10; i++) {
    result[i] = function () {
      return i;
    };
  }
  return result;
}

複製代碼

image.png

這個函數會返回一個函數數組。表面上看,彷佛每一個函數都應該返本身的索引值,即位置 0 的函數 返回 0,位置 1 的函數返回 1,以此類推。但實際上,每一個函數都返回 10。由於每一個函數的做用域鏈中 都保存着 createFunctions() 函數的活動對象,因此它們引用的都是同一個變量 i 。 當 createFunctions()函數返回後,變量 i 的值是 10,此時每一個函數都引用着保存變量 i 的同一個變量 對象,因此在每一個函數內部 i 的值都是 10。可是,咱們能夠經過建立另外一個匿名函數強制讓閉包的行爲 符合預期,以下所示。

function createFunctions() {
  var result = new Array();
  for (var i = 0; i < 10; i++) {
    result[i] = (function (num) {
      return function () {
        return num;
      };
    })(i);
  }
  return result;
}
複製代碼

下載 (1).png 在重寫了前面的 createFunctions()函數後,每一個函數就會返回各自不一樣的索引值了。在這個版 本中,咱們沒有直接把閉包賦值給數組,而是定義了一個匿名函數,並將當即執行該匿名函數的結果賦 給數組。這裏的匿名函數有一個參數 num,也就是最終的函數要返回的值。在調用每一個匿名函數時,我 們傳入了變量 i。因爲函數參數是按值傳遞的,因此就會將變量 i 的當前值複製給參數 num。而在這個 匿名函數內部,又建立並返回了一個訪問 num 的閉包。這樣一來,result 數組中的每一個函數都有本身 num 變量的一個副本,所以就能夠返回各自不一樣的數值了。

關於this

在閉包中使用 this 對象也可能會致使一些問題。咱們知道,this 對象是在運行時基於函數的執 行環境綁定的:在全局函數中,this 等於 window,而當函數被做爲某個對象的方法調用時,this 等 於那個對象。不過,匿名函數的執行環境具備全局性,所以其 this 對象一般指向 window。但有時候 因爲編寫閉包的方式不一樣,這一點可能不會那麼明顯。下面來看一個例子。
var name = "The Window";
var object = {
  name: "My Object",
  getNameFunc: function () {
    return function () {
      return this.name;
    };
  },
};
alert(object.getNameFunc()()); //"The Window"
複製代碼

每一個函數在被調用時都會自動取得兩個特殊變量:this 和 arguments。內部函 數在搜索這兩個變量時,只會搜索到其活動對象爲止,所以永遠不可能直接訪問外部函數中的這兩個變 量。不過,把外部做用域中的 this 對象保存在一個閉包可以訪問 到的變量裏,就可讓閉包訪問該對象了。

var name = "The Window";
var object = {
  name: "My Object",
  getNameFunc: function () {
    var that = this;
    return function () {
      return that.name;
    };
  },
};
alert(object.getNameFunc()()); //"My Object"
複製代碼

在幾種特殊狀況下,this 的值可能會意外地改變。好比,下面的代碼是修改前面例子的結果。

var name = "The Window";
var object = {
  name: "My Object",
  getName: function () {
    return this.name;
  },
};

複製代碼

第一行代碼跟日常同樣調用了 object.getName(),返回的是"My Object",由於 this.name 就是 object.name。第二行代碼在調用這個方法前先給它加上了括號。雖然加上括號以後,就好像只 是在引用一個函數,但 this 的值獲得了維持,由於 object.getName 和(object.getName)的定義 是相同的。第三行代碼先執行了一條賦值語句,而後再調用賦值後的結果。由於這個賦值表達式的值是 函數自己,因此 this 的值不能獲得維持,結果就返回了"The Window"。 固然,你不大可能會像第二行和第三行代碼同樣調用這個方法。不過,這個例子有助於說明即便是 語法的細微變化,都有可能意外改變 this 的值。

6 深copy的實現

深拷貝和淺拷貝的定義

深拷貝已是一個老生常談的話題了,也是如今前端面試的高頻題目,可是令我吃驚的是有不少同窗尚未搞懂深拷貝和淺拷貝的區別和定義

淺拷貝:

16ce894a1f1b5c32.png

建立一個新對象,這個對象有着原始對象屬性值的一份精確拷貝。若是屬性是基本類型,拷貝的就是基本類型的值,若是屬性是引用類型,拷貝的就是內存地址 ,因此若是其中一個對象改變了這個地址,就會影響到另外一個對象。

深拷貝:

16ce893a54f6c13d.png

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

乞丐版

在不使用第三方庫的狀況下,咱們想要深拷貝一個對象,用的最多的就是下面這個方法。
JSON.parse(JSON.stringify());
複製代碼

這種寫法很是簡單,並且能夠應對大部分的應用場景,可是它仍是有很大缺陷的,好比拷貝其餘引用類型、拷貝函數、循環引用等狀況。

基礎版本

function clone(target) {
   if (typeof target === 'object') {
       let cloneTarget = {};
       for (const key in target) {
           cloneTarget[key] = clone(target[key]);
       }
       return cloneTarget;
   } else {
       return target;
   }
};

複製代碼

image.png

這是一個最基礎版本的深拷貝,這段代碼可讓你向面試官展現你能夠用遞歸解決問題,可是顯然,他還有很是多的缺陷,好比,尚未考慮數組。

考慮數組

在上面的版本中,咱們的初始化結果只考慮了普通的object,下面咱們只須要把初始化代碼稍微一變,就能夠兼容數組了:

module.exports = function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8]
};



複製代碼

image.png OK,沒有問題,你的代碼又向合格邁進了一小步。

循環引用

const target = {
   field1: 1,
   field2: undefined,
   field3: {
       child: 'child'
   },
   field4: [2, 4, 8]
};
target.target = target;
複製代碼

image.png 很明顯,由於遞歸進入死循環致使棧內存溢出了。

緣由就是上面的對象存在循環引用的狀況,即對象的屬性間接或直接的引用了自身的狀況:

解決循環引用問題,咱們能夠額外開闢一個存儲空間,來存儲當前對象和拷貝對象的對應關係,當須要拷貝當前對象時,先去存儲空間中找,有沒有拷貝過這個對象,若是有的話直接返回,若是沒有的話繼續拷貝,這樣就巧妙化解的循環引用的問題。

這個存儲空間,須要能夠存儲key-value形式的數據,且key能夠是一個引用類型,咱們能夠選擇Map這種數據結構:

  • 檢查map中有無克隆過的對象
  • 有 - 直接返回
  • 沒有 - 將當前對象做爲key,克隆對象做爲value進行存儲
  • 繼續克隆
function clone(target, map = new Map()) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);
        for (const key in target) {
            cloneTarget[key] = clone(target[key], map);
        }
        return cloneTarget;
    } else {
        return target;
    }
};


複製代碼

image.png

接下來,咱們可使用,WeakMap提代Map來使代碼達到畫龍點睛的做用。

function clone(target, map = new WeakMap()) {
    // ...
};

複製代碼

爲何要這樣作呢?,先來看看WeakMap的做用:

WeakMap 對象是一組鍵/值對的集合,其中的鍵是弱引用的。其鍵必須是對象,而值能夠是任意的。

什麼是弱引用呢?

在計算機程序設計中,弱引用與強引用相對,是指不能確保其引用的對象不會被垃圾回收器回收的引用。 一個對象若只被弱引用所引用,則被認爲是不可訪問(或弱可訪問)的,並所以可能在任什麼時候刻被回收。

舉個例子:

let obj = { name : 'ConardLi'}
const target = new Map();
target.set(obj,'code祕密花園');
obj = null;

複製代碼

雖然咱們手動將obj,進行釋放,然是target依然對obj存在強引用關係,因此這部份內存依然沒法被釋放。

再來看WeakMap

let obj = { name : 'ConardLi'}
const target = new WeakMap();
target.set(obj,'code祕密花園');
obj = null;

複製代碼

若是是WeakMap的話,targetobj存在的就是弱引用關係,當下一次垃圾回收機制執行時,這塊內存就會被釋放掉。

設想一下,若是咱們要拷貝的對象很是龐大時,使用Map會對內存形成很是大的額外消耗,並且咱們須要手動清除Map的屬性才能釋放這塊內存,而WeakMap會幫咱們巧妙化解這個問題。

相關文章
相關標籤/搜索