面試官問你基本型別時他想知道什麼

Alan發表於2019-04-02
前言: 面試的時候我們經常會被問答js的資料型別。大部分情況我們會這樣回答包括
1、基本型別(值型別或者原始型別): Number、Boolean、String、NULL、Undefined以及ES6的Symbol
2、引用型別:Object、Array、Function、Date等
我曾經也是這樣回答的,並且一直覺得沒有什麼問題。

1 、在記憶體中的位置不同

  • 基本型別: 佔用空間固定,儲存在棧中
  • 引用型別:佔用空間不固定,儲存在堆中
棧(stack)為自動分配的記憶體空間,它由系統自動釋放;使用一級快取,被呼叫時通常處於儲存空間中,呼叫後被立即釋放。
堆(heap)則是動態分配的記憶體,大小不定也不會自動釋放。使用二級快取,生命週期與虛擬機器的GC演算法有關

當一個方法執行時,每個方法都會建立自己的記憶體棧,在這個方法內定義的變數將會逐個放入這塊棧記憶體裡,隨著方法的執行結束,這個方法的記憶體棧也將自然銷燬了。因此,所有在方法中定義的變數都是放在棧記憶體中的;棧中儲存的是基礎變數以及一些物件的引用變數,基礎變數的值是儲存在棧中,而引用變數儲存在棧中的是指向堆中的陣列或者物件的地址,這就是為何修改引用型別總會影響到其他指向這個地址的引用變數。

當我們在程式中建立一個物件時,這個物件將被儲存到執行時資料區中,以便反覆利用(因為物件的建立成本通常較大),這個執行時資料區就是堆記憶體。堆記憶體中的物件不會隨方法的結束而銷燬,即使方法結束後,這個物件還可能被另一個引用變數所引用(方法的引數傳遞時很常見),則這個物件依然不會被銷燬,只有當一個物件沒有任何引用變數引用它時,系統的垃圾回收機制才會在核實的時候回收它。

2、賦值、淺拷貝、深拷貝

  • 對於基本型別值,賦值、淺拷貝、深拷貝時都是複製基本型別的值給新的變數,之後二個變數之間操作不在相互影響。
  • 對於引用型別值,

    • 賦值後二個變數指向同一個地址,一個變數改變時,另一個也同樣改變;
    • 淺拷貝後得到一個新的變數,這個與之前的已經不是指向同一個變數,改變時不會使原資料中的基本型別一同改變,但會改變會原資料中的引用型別資料
    • 深拷貝後得到的是一個新的變數,她的改變不會影響後設資料
- 和原資料是否指向同一物件 第一層資料為基本資料型別 原資料中包含子物件
賦值 改變會使原資料一同改變 改變會使原資料一同改變
淺拷貝 改變不會使原資料一同改變 改變會使原資料一同改變
深拷貝 改變不會使原資料一同改變 改變不會使原資料一同改變
    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,["二","三"],["四","五"]],
    //};

    console.log(obj2);
    //obj2 = {
    //    'name' : 'lisi',
    //    'age' :  '18',
    //    'language' : [1,["二","三"],["四","五"]],
    //};

    console.log(obj3);
    //obj3 = {
    //    'name' : 'zhangsan',
    //    'age' :  '20',
    //    'language' : [1,["二","三"],["四","五"]],
    //};

2.1、淺拷貝

陣列常用的淺拷貝方法slice,concat,Array.from() ,以及es6的析構

var arr1 = [1, 2,{a:1,b:2,c:3,d:4}];
var arr2 = arr1.slice();
var arr3 = arr1.concat();
var arr4 = Array.from(arr1);
var arr5 = [...arr1];
arr2[0]=2;
arr2[2].a=2;
arr3[0]=3;
arr3[2].b=3;
arr4[0]=4;
arr4[2].c=4;
arr5[0]=5;
arr5[2].d=5;
// arr1[1,2,{a:2,b:3,c:4,d:5}]
// arr2[2,2,{a:2,b:3,c:4,d:5}]
// arr3[3,2,{a:2,b:3,c:4,d:5}]
// arr4[4,2,{a:2,b:3,c:4,d:5}]
// arr5[5,2,{a:2,b:3,c:4,d:5}]

物件常用的淺拷貝方法Object.assign(),es6析構

var obj1 = {
    x: 1, 
    y: {
        m: 1
    }
};
var obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.x=2;
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 2}}
console.log(obj2) //{x: 2, y: {m: 2}}

我們自己實現一個淺拷貝

var obj = { a:1, arr: [2,3] };
var shallowObj = shallowCopy(obj);

var shallowCopy = function(obj) {
    // 只拷貝物件
    if (typeof obj !== 'object') return;
    // 根據obj的型別判斷是新建一個陣列還是物件
    var newObj = obj instanceof Array ? [] : {};
    // 遍歷obj,並且判斷是obj的屬性才拷貝
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = obj[key];
        }
    }
    return newObj;
}

2.2、深拷貝

比較簡單粗暴的的做法是使用JSON.parse(JSON.stringify(obj))

var arr = ['old', 1, true, ['old1', 'old2'], {old: 1}]
var new_arr = JSON.parse( JSON.stringify(arr) );
new_arr[4].old=4;
console.log(arr); //['old', 1, true, ['old1', 'old2'], {old: 1}]
console.log(new_arr); //['old', 1, true, ['old1', 'old2'], {old: 4}]

JSON.parse(JSON.stringify(obj)) 看起來很不錯,不過MDN文件 的描述有句話寫的很清楚:

undefined、任意的函式以及 symbol 值,在序列化過程中會被忽略(出現在非陣列物件的屬性值中時)或者被轉換成 null(出現在陣列中時)。

但是在平時的開發中JSON.parse(JSON.stringify(obj))已經滿足90%的使用場景了。
下面我們自己來實現一個

var deepCopy = function(obj) {
    if (typeof obj !== 'object') return;
    var newObj = obj instanceof Array ? [] : {};
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
        }
    }
    return newObj;
}

3、引數的傳遞

所有的函式引數都是按值傳遞。也就是說把函式外面的值賦值給函式內部的引數,就和把一個值從一個變數賦值給另一個一樣;

  • 基本型別
var a = 2;
function add(x) {
 return x = x + 2;
}
var result = add(a);
console.log(a, result); // 2 4

引用型別

function setName(obj) {
  obj.name = 'laowang';
  obj = new Object();
  obj.name = 'Tom';
}
var person = new Object();
setName(person);
console.log(person.name); //laowang

很多人錯誤地以為在區域性作用域中修改的物件在全域性作用域中反映出來就是說明引數是按引用傳遞的。
但是通過上面的例子可以看出如果person是按引用傳遞的最終的person.name應該是Tom。
實際上當函式內部重寫obj時,這個變數引用的就是一個區域性變數了。而這個變數會在函式執行結束後銷燬。(這是是在js高階程式設計看到的,還不是很清楚)

4、判斷方法

基本型別用typeof,引用型別用instanceof

特別注意typeof null"object", null instanceof Objecttrue;
console.log(typeof "Nicholas"); // "string"
console.log(typeof 10);         // "number"
console.log(typeof true);       // "boolean"
console.log(typeof undefined);  // "undefined"
console.log(typeof null);      // "object"


var items = [];
var obj = {};
function reflect(value){
  return value;
}

console.log(items instanceof Array); // true;
console.log(obj instanceof Object); // true;
console.log(reflect instanceof Function); // true;

Object.prototype.toString.call([]).slice(8, -1)有興趣的同學可以看一下這個是幹什麼的

5、總結

圖片描述

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎star對作者也是一種鼓勵。

相關文章