JavaScript - 基本型別與引用型別值

Ozzie發表於2019-12-17

本文講述了在JavaScript中,基本型別與引用型別值的概念及區別


引入概念:基本型別和引用型別

  1. 可以感受到,JS的變數及其鬆散,那麼,正是 JS變數鬆散 的本質,決定了:
    JS變數名只是 一個在特定的時間用於儲存特定值的一個名字 而已,也就是說,變數的值及其資料型別可以在指令碼的生命週期內改變 ,儘管這個功能看起來有趣、強大,但是JS變數實際上是比較複雜,
  2. ECMAScirpt 變數有兩種不同的資料型別:基本型別引用型別,另外還有其他的叫法,比如:原始型別和物件型別擁有方法的型別和不能擁有方法的型別 等等
  3. 將一個值賦值給變數時,解析器 必須確定這個值是基本型別值還是引用型別值
    基本型別 指的是簡單的資料段,而 引用型別 指的是可能由多個值構成的物件
    基本型別:undefinednullstringnumberbooleansymbo(ES6)
    引用型別:ObjectArrayRegExpDateFunction

兩種型別的區別

  1. 儲存:
    • 基本型別的值是存放在 棧區 的,即記憶體中的棧記憶體
      • 假如有以下變數:
        var name = 'jozo';
        var city = 'guangzhou';
        var age = 22;
      • 那麼他們的儲存結構如下:(棧區包括了變數的識別符號和值)
        在這裡插入圖片描述
    • 引用型別的值是同時儲存在 棧記憶體和堆記憶體
      • 假如有以下物件:
        var person1 = {name:'jozo'};
        var person2 = {name:'xiaom'};
        var person3 = {name:'xiaoq'};
      • 那麼他們的儲存結構如下:
        在這裡插入圖片描述
  2. 訪問:
    • 基本型別的值是 按值訪問 的,因為可以操作儲存在變數中的實際的值。
    • 引用型別的值是 按引用訪問 的,因為引用型別的值是儲存在記憶體中的物件,而與其他語言不同的是,JavaScript 不允許直接訪問記憶體中的位置,即不可以直接操作物件的記憶體空間,那麼,在操作物件時,實際上是在操作物件的引用而不是實際的物件。
  3. 動態的屬性:
    • 對於引用型別的值,很明顯,我們可以為其 新增、改變、刪除 屬性和方法:
      var person = new Object();
      person.name = "Ozzie";
      console.log(person.name);  //"Ozzie"
      • 上述過程中,我們建立了一個物件併為其新增了一個屬性,如果物件不被銷燬或者這個屬性不被刪除,那麼這個屬性將一直存在
    • 但是,我們不可以給基本型別的值新增方法和屬性
      var name = "Ozzie";
      name.age = 19;
      consoe.log(name.age);  //undefined
      • 儘管這樣操作不會報錯,但是仍然會被默默地掛掉
  4. 比較:
    • 基本型別的比較是 值的比較
      • 例如:
        var a = 1;
        var b = true;
        console.log(a == b);   //true
    • 引用型別的比較是 引用的比較
      • 例如:
        var person1 = {};
        var person2 = {};
        console.log(person1 == person2);   //false
      • 上文提到過,引用型別時按引用訪問的,換句話說就是比較兩個物件的堆記憶體中的地址是否相同,很明顯,並不是同一個記憶體位置:
        在這裡插入圖片描述
  5. 複製變數值:
    • 將一個基本型別的值複製給另一個變數,那麼,會在新變數上建立一個新值,然後再把該值複製到為新變數分配的位置上:
      • 例如:
        var a = 10;
        var b = a;
        a++;
        console.log(a);    // 11
        console.log(b);    // 10
      • a與b是完全獨立的,該值只是a中的值的一個副本。
      • 基本型別在賦值操作後,兩個變數是相互不受影響的:
        在這裡插入圖片描述
    • 那麼,複製引用型別的值時,同樣也會將一份儲存在物件中的值複製到新變數的空間中,不同的是,這個值的副本實際上是一個 指標,指向的是儲存在堆中的物件。也就是說,複製結束後,這兩個變數將引用同一個物件。
      • 例如:
        var a = {}; // a儲存了一個空物件的例項
        var b = a;  // a和b都指向了這個空物件
        a.name = 'jozo';
        console.log(a.name); // 'jozo'
        console.log(b.name); // 'jozo'
      • 改變其中一個變數就會影響到另一個變數
        在這裡插入圖片描述
  6. 傳遞引數:
    • 請記住,儘管在訪問變數時有著按值訪問和按引用訪問這兩種方式,但 ECMAScript 中所有的函式的引數都是按值傳遞的,即引數只能按值傳遞,也就是說,把函式外部的值複製給函式內部的引數,就類似於變數之間的值複製一樣
    • 基本型別的值的傳遞如同基本型別的變數的複製,被傳遞的值會被賦值給一個區域性變數(即命名引數,用 ECMAScript 中的概念說,就是 arguments 物件中的一個元素),此處不再贅述...
    • 但是向引數傳遞引用型別的值時,複製給區域性變數的是 記憶體中的地址,因此這個區域性變數的變化會被反映在函式的外部。
      • 例如:
        function setName(obj){
            obj.name = "Ozzie";
        }
        var person = new Object();
        setName(person);
        console.log(person.name);  //"Ozzie"
      • 我們可以看到,在函式內部,objperson 引用的是同一個物件,換句話說,即使這個變數是按值傳遞的,obj 也會按引用來訪問同一個物件,因為person指向的物件在堆記憶體中只有一個,而且是全域性物件。
      • 很多人會 錯誤地認為:引數是按引用傳遞的,因為在區域性作用域中修改的引數會在全域性作用域中反映出來,OK,那麼我們再看一個例子:
        function setName(obj){
            obj.name = "Ozzie";
            obj = new Object();
            obj.name = "Nicholas"
        }
        var person = new Object();
        setName(person);
        console.log(person.name);  //Ozzie
      • 如果是按引用傳遞引數的,那麼顯然 person物件就會在函式內部自動修改name 屬性為 Nicholas,但結果仍然是 Ozzie,這說明,即使在函式內部修改了引數的值,但原始的引用仍然保持不變,實際上,在函式內部重寫obj時,這個變數的引用就是一個區域性物件了,而這個區域性物件在函式執行完畢後立即被銷燬。

相關文章