js底層數(shù)據(jù)類型

js底層數(shù)據(jù)類型

堆和棧的區(qū)別

其實深拷貝和淺拷貝的主要區(qū)別就是其在內(nèi)存中的存儲類型不同。

堆和棧都是內(nèi)存中劃分出來用來存儲的區(qū)域。

棧(stack)為自動分配的內(nèi)存空間,它由系統(tǒng)自動釋放;而堆(heap)則是動態(tài)分配的內(nèi)存,大小不定也不會自動釋放。


  • 只能在一端(稱為棧頂(top))對數(shù)據(jù)項進(jìn)行插入和刪除。

    先進(jìn)后出(FILO—First-In/Last-Out)。

    由操作系統(tǒng)自動分配釋放</br>
    棧使用的是一級緩存, 他們通常都是被調(diào)用時處于存儲空間中,調(diào)用完畢立即釋放。


  • 堆可以被看成是一棵樹,如:堆排序。

    隊列優(yōu)先,先進(jìn)先出(FIFO—first in first out)

    堆則是存放在二級緩存中,生命周期由虛擬機(jī)的垃圾回收算法來決定(并不是一旦成為孤兒對象就能被回收)。所以調(diào)用這些對象的速度要相對來得低一些。

這也是解釋為什么會造成內(nèi)存泄漏的原因
三種典型的內(nèi)存泄漏:

  1. 意外的全局變量
  2. 被遺忘的計時器或回調(diào)函數(shù)
  3. 脫離 DOM 的引用
  4. 閉包

我們先來重新回顧一下 ECMAScript 中的數(shù)據(jù)類型。主要分為

基本數(shù)據(jù)類型(undefined,boolean,number,string,null)

基本數(shù)據(jù)類型存放在棧中

存放在棧內(nèi)存中的簡單數(shù)據(jù)段,數(shù)據(jù)大小確定,內(nèi)存空間大小可以分配,是直接按值存放的,所以可以直接訪問。

基本數(shù)據(jù)類型值不可變

javascript中的原始值(undefined、null、布爾值、數(shù)字和字符串)與對象(包括數(shù)組和函數(shù))有著根本區(qū)別。原始值是不可更改的:任何方法都無法更改(或“突變”)一個原始值。對數(shù)字和布爾值來說顯然如此 —— 改變數(shù)字的值本身就說不通,而對字符串來說就不那么明顯了,因為字符串看起來像由字符組成的數(shù)組,我們期望可以通過指定索引來假改字符串中的字符。實際上,javascript 是禁止這樣做的。字符串中所有的方法看上去返回了一個修改后的字符串,實際上返回的是一個新的字符串值。

基本數(shù)據(jù)類型的值是不可變的,動態(tài)修改了基本數(shù)據(jù)類型的值,它的原始值也是不會改變的,例如:

var str = "abc";
console.log(str[1]="f");    // f
console.log(str);           // abc

這一點(diǎn)其實開始我是比較迷惑的,總是感覺 js 是一個靈活的語言,任何值應(yīng)該都是可變的,真是圖樣圖森破,我們通常情況下都是對一個變量重新賦值,而不是改變基本數(shù)據(jù)類型的值。就如上述引用所說的那樣,在 js 中沒有方法是可以改變布爾值和數(shù)字的。倒是有很多操作字符串的方法,但是這些方法都是返回一個新的字符串,并沒有改變其原有的數(shù)據(jù)。

所以,記住這一點(diǎn):基本數(shù)據(jù)類型值不可變。

基本類型的比較是值的比較

基本類型的比較是值的比較,只要它們的值相等就認(rèn)為他們是相等的,例如:

var a = 1;
var b = 1;
console.log(a === b);//true

比較的時候最好使用嚴(yán)格等,因為 == 是會進(jìn)行類型轉(zhuǎn)換的,比如:

var a = 1;
var b = true;
console.log(a == b);//true

引用類型

基本數(shù)據(jù)類型存放在堆中

引用類型(object)是存放在堆內(nèi)存中的,變量實際上是一個存放在棧內(nèi)存的指針,這個指針指向堆內(nèi)存中的地址。每個空間大小不一樣,要根據(jù)情況開進(jìn)行特定的分配,例如。

ar person1 = {name:'jozo'};
var person2 = {name:'xiaom'};
var person3 = {name:'xiaoq'};
引用類型值可變

引用類型是可以直接改變其值的,例如:

var a = [1,2,3];
a[1] = 5;
console.log(a[1]); // 5
引用類型的比較是引用的比較

所以每次我們對 js 中的引用類型進(jìn)行操作的時候,都是操作其對象的引用(保存在棧內(nèi)存中的指針),所以比較兩個引用類型,是看其的引用是否指向同一個對象。例如:

var a = [1,2,3];
var b = [1,2,3];
console.log(a === b); // false

雖然變量 a 和變量 b 都是表示一個內(nèi)容為 1,2,3 的數(shù)組,但是其在內(nèi)存中的位置不一樣,也就是說變量 a 和變量 b 指向的不是同一個對象,所以他們是不相等的。

傳值與傳址

函數(shù)的參數(shù)是按值傳遞
不懂看看這篇文字javascript高級教程技術(shù)之第四章 變量 作用域和內(nèi)存問題

了解了基本數(shù)據(jù)類型與引用類型的區(qū)別之后,我們就應(yīng)該能明白傳值與傳址的區(qū)別了。在我們進(jìn)行賦值操作的時候,基本數(shù)據(jù)類型的賦值(=)是在內(nèi)存中新開辟一段棧內(nèi)存,然后再把再將值賦值到新的棧中。例如:

var a = 10;
var b = a;

a ++ ;
console.log(a); // 11
console.log(b); // 10

所以說,基本類型的賦值的兩個變量是兩個獨(dú)立相互不影響的變量。

但是引用類型的賦值是傳址。只是改變指針的指向,例如,也就是說引用類型的賦值是對象保存在棧中的地址的賦值,這樣的話兩個變量就指向同一個對象,因此兩者之間操作互相有影響。例如:

var a = {}; // a保存了一個空對象的實例
var b = a;  // a和b都指向了這個空對象

a.name = 'jozo';
console.log(a.name); // 'jozo'
console.log(b.name); // 'jozo'

b.age = 22;
console.log(b.age);// 22
console.log(a.age);// 22

console.log(a == b);// true

淺拷貝

在深入了解之前,我認(rèn)為上面的賦值就是淺拷貝,哇哈哈,真的是圖樣圖森破。上面那個應(yīng)該只能算是“引用”,并不算是真正的淺拷貝。
一下部分參照知乎中的提問: javascript中的深拷貝和淺拷貝

賦值(=)和淺拷貝的區(qū)別

那么賦值和淺拷貝有什么區(qū)別呢,我們看下面這個例子:

    var obj1 = {
        'name' : 'zhangsan',
        'age' :  '18',
        'language' : [1,[2,3],[4,5]],
    };

    var obj2 = obj1;


    var obj3 = shallowCopy(obj1);
    function shallowCopy(src) {
        var dst = {};
        for (var prop in src) {
            if (src.hasOwnProperty(prop)) {
                dst[prop] = src[prop];
            }
        }
        return dst;
    }

    obj2.name = "lisi";
    obj3.age = "20";

    obj2.language[1] = ["二","三"];
    obj3.language[2] = ["四","五"];

    console.log(obj1);  
    //obj1 = {
    //    'name' : 'lisi',
    //    'age' :  '18',
    //    'language' : [1,[4,5]],
    //};

    console.log(obj2);
    //obj2 = {
    //    'name' : 'lisi',
    //    'age' :  '18',
    //    'language' : [1,[4,5]],
    //};

    console.log(obj3);
    //obj3 = {
    //    'name' : 'zhangsan',
    //    'age' :  '20',
    //    'language' : [1,[4,5]],
    //};

先定義個一個原始的對象 obj1,然后使用賦值得到第二個對象 obj2,然后通過淺拷貝,將 obj1 里面的屬性都賦值到 obj3 中。也就是說:

  • obj1:原始數(shù)據(jù)
  • obj2:賦值操作得到
  • obj3:淺拷貝得到

然后我們改變 obj2 的 name 屬性和 obj3 的 name 屬性,可以看到,改變賦值得到的對象 obj2 同時也會改變原始值 obj1,而改變淺拷貝得到的的 obj3 則不會改變原始對象 obj1。這就可以說明賦值得到的對象 obj2 只是將指針改變,其引用的仍然是同一個對象,而淺拷貝得到的的 obj3 則是重新創(chuàng)建了新對象。

然而,我們接下來來看一下改變引用類型會是什么情況呢,我又改變了賦值得到的對象 obj2 和淺拷貝得到的 obj3 中的 language 屬性的第二個值和第三個值(language 是一個數(shù)組,也就是引用類型)。結(jié)果見輸出,可以看出來,無論是修改賦值得到的對象 obj2 和淺拷貝得到的 obj3 都會改變原始數(shù)據(jù)。

這是因為淺拷貝只復(fù)制一層對象的屬性,并不包括對象里面的為引用類型的數(shù)據(jù)。所以就會出現(xiàn)改變淺拷貝得到的 obj3 中的引用類型時,會使原始數(shù)據(jù)得到改變。

深拷貝:將 B 對象拷貝到 A 對象中,包括 B 里面的子對象,

淺拷貝:將 B 對象拷貝到 A 對象中,但不包括 B 里面的子對象

深拷貝

看了這么半天,你也應(yīng)該清楚什么是深拷貝了吧,如果還不清楚,我就剖腹自盡(?_?)

深拷貝是對對象以及對象的所有子對象進(jìn)行拷貝。

那么問題來了,怎么進(jìn)行深拷貝呢?

思路就是遞歸調(diào)用剛剛的淺拷貝,把所有屬于對象的屬性類型都遍歷賦給另一個對象即可。我們直接來看一下 Zepto 中深拷貝的代碼:

// 內(nèi)部方法:用戶合并一個或多個對象到第一個對象
    // 參數(shù):
    // target 目標(biāo)對象  對象都合并到target里
    // source 合并對象
    // deep 是否執(zhí)行深度合并
    function extend(target, source, deep) {
        for (key in source)
            if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
                // source[key] 是對象,而 target[key] 不是對象, 則 target[key] = {} 初始化一下,否則遞歸會出錯的
                if (isPlainObject(source[key]) && !isPlainObject(target[key]))
                    target[key] = {}

                // source[key] 是數(shù)組,而 target[key] 不是數(shù)組,則 target[key] = [] 初始化一下,否則遞歸會出錯的
                if (isArray(source[key]) && !isArray(target[key]))
                    target[key] = []
                // 執(zhí)行遞歸
                extend(target[key], source[key], deep)
            }
            // 不滿足以上條件,說明 source[key] 是一般的值類型,直接賦值給 target 就是了
            else if (source[key] !== undefined) target[key] = source[key]
    }

    // Copy all but undefined properties from one or more
    // objects to the `target` object.
    $.extend = function(target){
        var deep, args = slice.call(arguments, 1);

        //第一個參數(shù)為boolean值時,表示是否深度合并
        if (typeof target == 'boolean') {
            deep = target;
            //target取第二個參數(shù)
            target = args.shift()
        }
        // 遍歷后面的參數(shù),都合并到target上
        args.forEach(function(arg){ extend(target, arg, deep) })
        return target
    }

在 Zepto 中的 $.extend 方法判斷的第一個參數(shù)傳入的是一個布爾值,判斷是否進(jìn)行深拷貝。

在 $.extend 方法內(nèi)部,只有一個形參 target,這個設(shè)計你真的很巧妙。
因為形參只有一個,所以 target 就是傳入的第一個參數(shù)的值,并在函數(shù)內(nèi)部設(shè)置一個變量 args 來接收去除第一個參數(shù)的其余參數(shù),如果該值是一個布爾類型的值的話,說明要啟用深拷貝,就將 deep 設(shè)置為 true,并將 target 賦值為 args 的第一個值(也就是真正的 target)。如果該值不是一個布爾類型的話,那么傳入的第一個值仍為 target 不需要進(jìn)行處理,只需要遍歷使用 extend 方法就可以。

參考文章:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 內(nèi)存管理 簡述OC中內(nèi)存管理機(jī)制。與retain配對使用的方法是dealloc還是release,為什么?需要與a...
    丶逐漸閱讀 2,081評論 1 16
  • 本文思維導(dǎo)圖如下: 本文首發(fā)于我的個人網(wǎng)站: http://cherryblog.site/本文作者: Cherr...
    sunshine小小倩閱讀 1,099評論 2 14
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結(jié)起來就是把...
    Dove_iOS閱讀 27,607評論 30 472
  • 從三月份找實習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍(lán)閱讀 42,790評論 11 349
  • 一 “從明天起,做一個幸福的人。我有一所房子,面朝大海,春暖花開?!笔畮啄昵埃?dāng)我倚了窗口,望著樓前來來往往的人流...
    一地月光閱讀 411評論 0 1

友情鏈接更多精彩內(nèi)容