請講下
JavaScript
中的資料型別?
前端面試中,估計大家都被這麼問過。
答:Javascript
中的資料型別包括原始型別和參照型別。其中原始型別包括 null
、undefined
、boolean
、string
、symbol
、bigInt
、number
。參照型別指的是 Object
。
沒錯,我也是這麼回答的,只是這通常是第一個問題,由這個問題可以引出很多很多的問題,比如
Null
和 Undefined
有什麼區別?前端的判空有哪些需要注意的?typeof null
為什麼是 object
?ES6
要提出 Symbol
?BigInt
解決了什麼問題?0.1 + 0.2 !== 0.3?
你如何解決這個問題?因為 JavaScript
是弱型別語言或者說是動態語言。這意味著你不需要提前宣告變數的型別,在程式執行的過程中,型別會被自動確定,也就是說你可以使用同一個變數儲存不同型別的值
var foo = 42; // foo is a Number now
foo = "bar"; // foo is a String now
foo = true; // foo is a Boolean now
這一特性給我們帶來便利的同時,也給我們帶來了很多的型別錯誤。試想一下,假如 JS
說是強型別語言,那麼各個型別之間沒法轉換,也就有了一層隔閡或者說一層保護,會不會更加好維護呢?——這或許就是 TypeScript
誕生的原因。
對 JavaScript
的資料型別掌握,是一個前端最基本的知識點
undefined
表示未定義的變數。null
值表示一個空物件指標。
追本溯源: 一開始的時候,
JavaScript
設計者Brendan Eich
其實只是定義了null
,null
像在Java
裡一樣,被當成一個物件。但是因為JavaScript
中有兩種資料型別:原始資料型別和參照資料型別。Brendan Eich
覺得表示"無"的值最好不是物件。
所以 Javascript
的設計是 null是一個表示"無"的物件,轉為數值時為0;undefined是一個表示"無"的原始值,轉為數值時為NaN。
Number(null)
// 0
5 + null
// 5
Number(undefined)
// NaN
5 + undefined
// NaN
null表示"沒有物件",即該處不應該有值。,典型的用法如下
Object.getPrototypeOf(Object.prototype)
// null
undefined表示"缺少值",就是此處應該有一個值,但是還沒有定義。典型用法是:
undefined
。undefined
。undefined
。undefined
。var i;
i // undefined
function f(x){console.log(x)}
f() // undefined
var o = new Object();
o.p // undefined
var x = f();
x // undefined
javaScript
五種空值和假值,分別為 undefined,null,false,"",0,NAN
這有時候很容易導致一些問題,比如
let a = 0;
console.log(a || '/'); // 本意是隻要 a 為 null 或者 Undefined 的時候,輸出 '/',但實際上只要是我們以上的五種之一就輸出 '/'
當然我們可以寫成
let a = 0;
if (a === null || a === undefined) {
console.log('/');
} else {
console.log(a);
}
始終不是很優雅,所以 ES規範 提出了空值合併操作符(??)
空值合併操作符(??)是一個邏輯操作符,當左側的運算元為 null 或者 undefined 時,返回其右側運算元,否則返回左側運算元。
上面的例子可以寫成:
let a = 0;
console.log(a??'/'); // 0
typeof null // "object"
JavaScript
中的值是由一個表示型別的標籤和實際資料值表示的。第一版的 JavaScript
是用 32 位位元來儲存值的,且是通過值的低 1 位或 3 位來識別型別的,物件的型別標籤是 000。如下
但有兩個特殊值:
由於 null
代表的是空指標(低三位也是 000
),因此,null
的型別標籤是 000
,typeof null
也因此返回 "object"。
這個算是 JavaScript
設計的一個錯誤,但是也沒法修改,畢竟修改的話,會影響目前現有的程式碼
在 JavaScript
會存在類似如下的現象
0.1 + 0.2
0.30000000000000004
我們在對浮點數進行運算的過程中,需要將十進位制轉換成二進位制。十進位制小數轉為二進位制的規則如下:
對小數點以後的數乘以2,取結果的整數部分(不是1就是0),然後再用小數部分再乘以2,再取結果的整數部分……以此類推,直到小數部分為0或者位數已經夠了就OK了。然後把取的整數部分按先後次序排列
根據上面的規則,最後 0.1 的表示如下:
0.000110011001100110011(0011無限迴圈)……
所以說,精度丟失並不是語言的問題,而是浮點數儲存本身固有的缺陷。
JavaScript
是以 64
位雙精度浮點數儲存所有 Number
型別值,按照 IEEE754
規範,0.1 的二進位制數只保留 52 位有效數位,即
1.100110011001100110011001100110011001100110011001101 * 2^(-4)
同理,0.2的二進位制數為
1.100110011001100110011001100110011001100110011001101 * 2^(-3)
這樣在進位制之間的轉換中精度已經損失。運算的時候如下
0.00011001100110011001100110011001100110011001100110011010
+0.00110011001100110011001100110011001100110011001100110100
------------------------------------------------------------
=0.01001100110011001100110011001100110011001100110011001110
所以導致了最後的計算結果中 0.1 + 0.2 !== 0.3
function add(num1, num2) {
const num1Digits = (num1.toString().split('.')[1] || '').length;
const num2Digits = (num2.toString().split('.')[1] || '').length;
const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
return (num1 * baseNum + num2 * baseNum) / baseNum;
}
類庫
NPM
上有許多支援 JavaScript
和 Node.js
的數學庫,比如 math.js
,decimal.js
,D.js
等等
ES6
ES6
在 Number
物件上新增了一個極小的常數——Number.EPSILON
Number.EPSILON
// 2.220446049250313e-16
Number.EPSILON.toFixed(20)
// "0.00000000000000022204"
引入一個這麼小的量,目的在於為浮點數計算設定一個誤差範圍,如果誤差能夠小於 Number.EPSILON
,我們就可以認為結果是可靠的。
function withinErrorMargin (left, right) {
return Math.abs(left - right) < Number.EPSILON
}
withinErrorMargin(0.1+0.2, 0.3)
目前處於 Stage 1
的提案。後文提到的 BigInt
擴充套件的是 JS
的正數邊界,超過 2^53 安全整數問題。Decimal
則是解決JS的小數問題-2^53。這個議案在JS中引入新的原生型別:decimal
(字尾m),宣告這個數位是十進位制運算。
let zero_point_three = 0.1m + 0.2m;
assert(zero_point_three === 0.3m);
// 提案中的例子
function calculateBill(items, tax) {
let total = 0m;
for (let {price, count} of items) {
total += price * BigDecimal(count);
}
return BigDecimal.round(total * (1m + tax), {maximumFractionDigits: 2, round: "up"});
}
let items = [{price: 1.25m, count: 5}, {price: 5m, count: 1}];
let tax = .0735m;
console.log(calculateBill(items, tax));
所以最終浮點數在記憶體中的儲存是什麼樣的呢?EEE754
對於浮點數表示方式給出了一種定義
(-1)^S * M * 2^E
各符號的意思如下:S,是符號位,決定正負,0時為正數,1時為負數。M,是指有效位數,大於1小於2。E,是指數位。
Javascript 是 64 位的雙精度浮點數,最高的 1 位是符號位S,接著的 11 位是指數E,剩下的 52 位為有效數位M。
可藉助 這個視覺化工具 檢視浮點數在記憶體中的二進位制表示)
JavaScript
的 Number
型別為 雙精度IEEE 754 64位元浮點型別。
在 JavaScript 中最大的值為 2^53
。
BigInt
任意精度數位型別,已經進入stage3規範。BigInt
可以表示任意大的整數。要建立一個 BigInt
,我們只需要在任意整型的字面量上加上一個 n 字尾即可。例如,把123 寫成 123n。這個全域性的 BigInt(number) 可以用來將一個 Number 轉換為一個 BigInt,言外之意就是說,BigInt(123) === 123n。現在讓我來利用這兩點來解決前面我們提到問題:
ES6 引入了一種新的原始資料型別 Symbol
,表示獨一無二的值
let s = Symbol();
typeof s
// "symbol"
定義一組常數,保證這組常數都是不相等的。消除魔法字串
物件中保證不同的屬性名
let mySymbol = Symbol();
// 第一種寫法
let a = {};
a[mySymbol] = 'Hello!';
// 第二種寫法
let a = {
[mySymbol]: 'Hello!'
};
// 第三種寫法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上寫法都得到同樣結果
a[mySymbol] // "Hello!"
Vue
中的 provide
和 inject
。provide
和 inject
可以允許一個祖先元件向其所有子孫後代注入一個依賴,不論元件層次有多深,並在起上下游關係成立的時間裡始終生效。但這個侵入性也是非常強的,使用 Symbols
作為 key
可以避免對減少對元件程式碼干擾,不會有相同命名等問題請說下判斷 Array 的方法?
因為陣列是一個特殊的存在,是我們平時接觸得最多的資料結構之一,它是一個特殊的物件,它的索引就是「普通物件」的 key
值。但它又擁有一些「普通物件」沒有的方法,比如 map
等
typeof
是 javascript
原生提供的判斷資料型別的運運算元,它會返回一個表示引數的資料型別的字串。但我們不能通過 typeof
判斷是否為陣列。因為 typeof
陣列和普通物件以及 null
,都是返回 "object"
const a = null;
const b = {};
const c= [];
console.log(typeof(a)); //Object
console.log(typeof(b)); //Object
console.log(typeof(c)); //Object
Object.prototype.toString.call()
。Object
的物件都有 toString
方法,如果 toString
方法沒有重寫的話,會返回 [Object type]
,其中 type
為物件的型別const a = ['Hello','Howard'];
const b = {0:'Hello',1:'Howard'};
const c = 'Hello Howard';
Object.prototype.toString.call(a);//"[object Array]"
Object.prototype.toString.call(b);//"[object Object]"
Object.prototype.toString.call(c);//"[object String]"
const a = [];
const b = {};
Array.isArray(a);//true
Array.isArray(b);//false
Array.isArray()
是 ES5
新增的方法,當不存在 Array.isArray()
,可以用 Object.prototype.toString.call()
實現
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
instanceof
。instanceof
運運算元可以用來判斷某個建構函式的 prototype
屬性所指向的物件是否存在於另外一個要檢測物件的原型鏈上。因為陣列的建構函式是 Array
,所以可以通過以下判斷。注意:因為陣列也是物件,所以 a instanceof Object
也為 true
const a = [];
const b = {};
console.log(a instanceof Array);//true
console.log(a instanceof Object);//true,在陣列的原型鏈上也能找到Object建構函式
console.log(b instanceof Array);//false
constructor
。通過建構函式範例化的範例,擁有一個 constructor
屬性。function B() {};
let b = new B();
console.log(b.constructor === B) // true
而陣列是由一個叫 Array
的函數範例化的。所以可以
let c = [];
console.log(c.constructor === Array) // true
注意:constructor 是會被改變的。所以不推薦這樣判斷
let c = [];
c.constructor = Object;
console.log(c.constructor === Array); // false
根據上面的描述,個人推薦的判斷方法有如下的優先順序
isArray
> Object.prototype.toString.call()
> instanceof
> constructor
本文針對於 JavaScript
中部分常見的資料型別問題進行了討論和分析。希望對大家面試或者平時的工作都能有所幫助。另外可能沒有提及的比如型別轉換等有機會再討論一下
最後,歡迎大家關注我的公眾號——前端雜貨鋪,技術問題多討論~