前言 請講下 JavaScript 中的數據類型? 前端面試中,估計大家都被這麼問過。 答:Javascript 中的數據類型包括原始類型和引用類型。其中原始類型包括 null、undefined、boolean、string、symbol、bigInt、number。引用類型指的是 Object。 ...
前言
請講下
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
的數據類型掌握,是一個前端最基本的知識點
null 還是 undefinded
定義
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 和 Undefined 的區別和應用
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——JS 犯的錯
typeof null // "object"
JavaScript
中的值是由一個表示類型的標簽和實際數據值表示的。第一版的 JavaScript
是用 32 位比特來存儲值的,且是通過值的低 1 位或 3 位來識別類型的,對象的類型標簽是 000。如下
- 1:整型(int)
- 000:引用類型(object)
- 010:雙精度浮點型(double)
- 100:字元串(string)
- 110:布爾型(boolean)
但有兩個特殊值:
- undefined,用整數−2^30(負2的30次方,不在整型的範圍內)
- null,機器碼空指針(C/C++ 巨集定義),低三位也是000
由於 null
代表的是空指針(低三位也是 000
),因此,null
的類型標簽是 000
,typeof null
也因此返回 "object"。
這個算是 JavaScript
設計的一個錯誤,但是也沒法修改,畢竟修改的話,會影響目前現有的代碼
Number——0.1+0.2 !== 0.3
現象
在 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)
未來的解決方案——TC39 Decimal proposal
目前處於 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。
可藉助 這個可視化工具 查看浮點數在記憶體中的二進位表示)
BigInt——突破最大的限制
JavaScript
的 Number
類型為 雙精度IEEE 754 64位浮點類型。
在 JavaScript 中最大的值為 2^53
。
BigInt
任意精度數字類型,已經進入stage3規範。BigInt
可以表示任意大的整數。要創建一個 BigInt
,我們只需要在任意整型的字面量上加上一個 n 尾碼即可。例如,把123 寫成 123n。這個全局的 BigInt(number) 可以用來將一個 Number 轉換為一個 BigInt,言外之意就是說,BigInt(123) === 123n。現在讓我來利用這兩點來解決前面我們提到問題:
Symbol——我是獨一無二最靚的仔
定義
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]"
- Array.isArray()
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
中部分常見的數據類型問題進行了討論和分析。希望對大家面試或者平時的工作都能有所幫助。另外可能沒有提及的比如類型轉換等有機會再討論一下
最後,歡迎大家關註我的公眾號——前端雜貨鋪,技術問題多討論~
參考
- undefined與null的區別
- The history of “typeof null”
- 0.1 + 0.2不等於0.3?為什麼JavaScript有這種“騷”操作?
- 深入理解JavaScript中的精度丟失
- JavaScript著名面試題: 0.1 + 0.2 !== 0.3,即將成為過去
- [譯]BigInt:JavaScript 中的任意精度整型
- ECMAScript 6 入門
- 在JavaScript中,如何判斷數組是數組?