分析 JavaScript 的資料型別與變數

OLeo發表於2018-11-14

這篇文章,來聊聊 JS 中的資料型別與變數。這是學習 JS 時最基礎的一類問題,但卻很重要。

比如:

如何理解引數的按值傳遞?

什麼是暫時性死區?

什麼是變數提升?

全域性變數和 window 的屬性有什麼區別?為什麼?

... ...

以上的問題均來自面試。如果你並不清楚,我覺得你有必要接著讀下去。

基本資料型別

在 JS 中,基本資料型別有 6 種,即數值、字串、布林值、null、undefined、Symbol。

對於基本資料型別,我們需要明白的是:基本型別在記憶體中的儲存方式是棧。每一個值都是單獨存放,互不影響。

基本型別都是按值訪問的。在比較時,按值進行比較:

1 === 1 // => true
複製程式碼

引用資料型別

引用型別的值儲存在堆中,而引用是儲存在棧中。

引用型別按引用訪問。在比較時,也比較的引用:

{} === {} // => false
複製程式碼

理解引數的傳遞方式

在 JS 中,引數可以是任何型別的值,甚至可以是函式。

這裡要分析的是引數是以哪種型別傳遞的?引用型別還是基本型別?

先看一個基礎的例子:

var out_num = 1;

function addOne(in_num) {
    in_num += 1;
    return in_num;
}

addOne(out_num); // => 2
out_num // => 1
複製程式碼

這個例子中,我們給 addOne() 函式傳遞一個實參 out_num,這個時 out_num 會傳遞給 in_num,即內部存在著 in_num = out_num 的過程。最後我們看到的結果是 out_num 並沒有被函式改變,說明 in_num 和 out_num 是兩個在記憶體中獨立存放的值,即按值傳遞。

再來看一個變形:

var out_obj = { value: 1 };

function addOne(in_obj) {
    in_obj.value += 1;
    return in_obj;
}

addOne(out_obj); // => { value: 2 }
out_obj // => { value: 2 }
複製程式碼

問題來了?函式引數不是按值傳遞嗎?為什麼這裡函式內部的處理反映到外部了?這是一個超級超級超級的理解誤區。

首先,我們還是得擺正觀點,即函式引數是按值傳遞的。 那這裡怎麼理解呢?對於引用型別而言,前面說引用型別分為引用和實際的記憶體空間。在這裡 out_obj 依舊傳遞給 in_obj,即 in_obj = out_objout_obj 和 in_obj 是兩個引用,它們在記憶體中的儲存方式是獨立的,但是它們卻指向同一塊記憶體。

in_obj.value = 1 則是直接操作的實際物件。實際物件的改變,會同步到所有引用這個實際物件的引用。

在這裡插入圖片描述
在這裡插入圖片描述
再來看這個例子,或許就會更清晰一些。

var out_obj = { value: 1 };

function addOne(in_obj) {
    in_obj = { value: 2 };
    return in_obj;
}

addOne(out_obj); // => { value: 2 }
out_obj // => { value: 1 }
複製程式碼

你只要抓住一點:物件的賦值就會造成引用指向的實際物件發生改變。

如何判斷資料型別

判斷資料型別,通常有三種具體的方法:

1、typeof 操作符

typeof 操作符返回一個表示資料型別的字串。它存在以下明顯的缺陷:

typeof null // => 'object'

typeof [] // => 'object'
複製程式碼

這是因為在 JS 語言設計之初遺留的 bug。可以閱讀這篇文章 2ality.com/2013/10/typ… 瞭解更多關於 typeof 處理 null 的問題。

所以 typeof 最好用於判斷一些基本型別,比如數值、字串、布林值、undefined、Symbol。

2、instanceof 操作符

typeof 的背後是通過判斷 type tags 來判斷資料型別,而 instanceof 則是通過判斷建構函式的 prototype 是否出現在物件原型鏈上的任何位置。

舉個例子:

{} instanceof Object // => true

[] instanceof Array // => true
[] instanceof Object // => true
複製程式碼

也判斷自定義型別:

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
var auto = new Car('Honda', 'Accord', 1998);

console.log(auto instanceof Car);
// => true

console.log(auto instanceof Object);
// => true
複製程式碼

所以,對於字面量形式的基本資料型別,不能通過 instanceof 判斷:

1 instanceof Number // => false

Number(1) instanceof Number // => false

new Number(1) instanceof Number // => true
複製程式碼

3、Object.prototype.toString()

這是目前最為推薦的一種方法,可以更加精細且準確的判斷任何資料型別,甚至是 JSON、正則、日期、錯誤等等。在 Lodash 中,其判斷資料型別的核心也是 Object.prototype.toString() 方法。

Object.prototype.toString.call(JSON) // => "[object JSON]"
複製程式碼

關於這背後的原理,你可以閱讀這篇文章 www.cnblogs.com/ziyunfei/ar…

4、其他

上面三種是通用的判斷資料型別的方法。面試中還會出現如何判斷一個陣列、如何判斷 NaN、如何判斷類陣列物件、如何判斷一個空物件等問題。這一類問題比較開放,解決思路通常是抓住判斷資料的核心特點。

舉個例子:判斷類陣列物件。

你先要知道 JS 中類陣列物件是什麼樣子的,並尋求一個實際的參照物,比如 arguments 就是類陣列物件。那麼類陣列物件具有的特點是:真值 & 物件 & 具有 length 屬性 & length 為整數 & length 的範圍大於等於 0,小於等於最大安全正整數(Number.MAX_SAFE_INTEGER)。

在你分析特點的時候,答案就呼之欲出了。【注意全面性】

資料型別如何轉換

JS 資料型別的動態性將貫穿整個 JS 的學習,這是 JS 非常重要的特性,很多現象就是因為動態性的存在而成為 JS 獨有。

正是由於動態性,JS 的資料型別可能在你毫無察覺的情況下,就發生了改變,直到執行時報錯。

這裡主要分析下面 8 種轉換規則。

1、if 語句

if 語句中的型別轉換是最常見的。

if (isTrue) {
    // ...
} else {}
複製程式碼

在 if 語句中,會自動呼叫 Boolean() 轉型函式對變數 isTrue 進行轉換。

當 isTrue 的值是 null, undefined, 0, NaN, '' 時,都會轉為 false。其餘值除 false 本身外都會轉為 true。

2、Number() 轉型函式

我們重點關注 null undefined 以及字串在 Number() 下的轉換:

Number(null) // => 0
Number(undefined) // => NaN
Number('') // => 0
Number('123') // => 123
Number('123abc') // => NaN
複製程式碼

注意和 parseInt() 對比。

3、parseInt()

parseInt(null) // => NaN
parseInt(undefined) // => NaN
parseInt('') // => NaN
parseInt('123') // => 123
parseInt('123abc') // => 123
複製程式碼

4、==

這裡需要注意的是:

null == undefined // => true

null == 0 // => false
undefined == false // => false
複製程式碼

null 與 undefined 的相等性是由 ECMA-262 規定的,並且 null 與 undefined 在比較相等性時不能轉換為其他任何值。

5、關係操作符

對於兩個字串的比較,是比較的字元編碼值:

'B' < 'a' // => true
複製程式碼

一個數值,另一個其他型別,都將轉為數字進行比較。

兩個布林值轉為數值進行比較。

物件,先呼叫 valueOf(),若不存在該方法,則呼叫 toString()。

6、加法

加法中特別注意的是,數字和字串相加,將數字轉為字串。

'1' + 2 => // '12'
1 + 2 => // 3
複製程式碼

對於物件和布林值,呼叫它們的 toString() 方法得到對應的字串值,然後進行字串相加。對於 undefined 和 null 呼叫 String() 取得字串 'undeifned' 和 'null'。

{ value: 1 } + true // => "[object Object]true"
複製程式碼

7、減法

對於字串、布林值、null 或者 undefined,自動呼叫 Number(),轉換結果若為 NaN,那麼最終結果為 NaN。

對於物件,先呼叫 valueOf(),如果得到 NaN,結果為 NaN。如果沒有 valueOf(),則呼叫 toString()。

8、乘法、除法

對於非數值,都會呼叫 Number() 轉型函式。

變數提升與暫時性死區

JS 中有三種宣告變數的方式:var, let, const。

var 宣告變數最大的一個特點是存在變數提升。

console.log(a); // undefined
var a = 1;
console.log(a); // 1
複製程式碼

第一個列印結果表示,在宣告變數 a 之前,a 就已經可以訪問了,只不過並未賦值。這就是變數提升現象。(具體原因,我放在後面分析作用域的時候來寫)

let 和 const 就不存在這個問題,但是又引入了暫時性死區這樣的概念。

/**
* 這上面都屬於變數 a 的暫時性死區
* console.log(a) // => Reference Error
*/
let a = 1;
console.log(a); // => 1
複製程式碼

即宣告 a 之前,不能夠訪問 a,而直接報錯。

**而暫時性死區的出現又引出另外一個問題,即 typeof 不再安全。**你可以參考這篇文章 es-discourse.com/t/why-typeo…

補充:一個經典面試題

for (var i = 0; i < 4; i++) {
    setTimeout(function(){
        console.log(i);
    }, i * 1000);
}
複製程式碼

我先不再這裡展開分析,我打算放到非同步與事件迴圈機制中去分析。不過這裡將 var 替換成 let 可以作為一種解決方案。如果你有興趣,也可以先去分析。

對於 const,這裡再補充一點,用於加深對基本型別和引用型別的理解。

const a = 1;
const b = { value: 1 };

a = 2; // => Error
b.value = 2; // => 2
b = { value: 2 }; // => Error
複製程式碼

本質上,const 並不是保證變數的值不得改動,而是變數指向的記憶體地址不得改動。

宣告全域性變數

直接通過 var 宣告全域性變數,這個全域性變數會作為 window 物件的一個屬性。

var a = 1;
window.a // => 1
複製程式碼

在這裡提出兩個問題,一是 let 宣告的全域性變數會成為 window 的屬性嗎?二是 var 宣告的全域性變數和直接在 window 建立屬性有沒有區別?

先來回答第一問題。**let 宣告的全域性變數不會成為 window 的屬性。**用什麼來支撐這樣的結論呢?**在 ES6 中,對於 let 和 const 宣告的變數從一開始就形成封閉作用域。**想想之前的暫時性死區。

第二個問題,**var 宣告的全域性變數和直接在 window 建立屬性存在著本質的區別。**先看下面的程式碼:

var a = 1;
window.a // => 1

window.b = 2;

delete window.a
delete window.b

window.a // => 1
window.b // => undefined
複製程式碼

我們可以看到,**直接建立在 window 上的屬性可以被 delete 刪除,而 var 建立的全域性屬性則不會。**這是現象,通過現象看本質,二者本質上的區別在於:

使用 var 宣告的全域性變數的 [[configurable]] 資料屬性的值為 false,不能通過 delete 刪除。而直接在物件上建立的屬性預設 [[configurable]] 的值為 true,即可以被 delete 刪除。(關於 [[configurable]] 屬性,在後面的文章中分析物件的時候還會提到)

小結

在這篇 「資料型別與變數」 文章中,分析了 7 個大類。再來回顧一下:

基本型別、引用型別、引數傳遞方式、如何判斷資料型別、資料型別如何轉換、變數提升與暫時性死區、宣告全域性變數。

這些不僅是校招面試中的高頻考點,也是學習 JS 必不可少的知識點。

Tip1:《JavaScript 高階程式設計》這本書被稱作“前端的聖經”是有原因的。對於正在準備校園招聘的你,非常有必要!書讀百遍,其義自見。 你會發現你在面試中遇到的絕大部分 JS 相關的知識點都能在這本書中找到“答案”!

Tip2:在準備複習的過程中,注意知識的模組性與相關性。你得有自己劃分知識模組的能力,比如今天的「資料型別與變數」模組。相關性是指,任何的知識都是由聯絡的,比如這裡牽涉到作用域、記憶體等模組。

這篇文章會不斷更新,如有出入,也希望你在後臺留言,或者 Email 給我:

swpu.leo@gmail.com

文章首發在 cameraee 微信公眾號

在這裡插入圖片描述

並同步更新至以下平臺:

CSDN blog.csdn.net/swpu_leo

Segment fault segmentfault.com/u/swpuleo

掘金 juejin.im/user/58bd20…

如果這篇文章幫助到了你,希望你能把它分享給更多人。

相關文章