JS面試題-<變數和類型>-JavaScript的數據類型
- 2019 年 11 月 13 日
- 筆記
前言
整理以前的面試題,發現問js數據類型的頻率挺高的,回憶當初自己的答案,就是簡簡單單的把幾個類型名稱羅列了出來,便沒有了任何下文。其實這一個知識點下可以牽涉發散出很多的知識點,如果一個面試者只是羅列的那些名詞出來,可能面試官都不願意繼續問下去了,這該算是js基礎的基礎了。如果這個問題沒有很好的回答,其他問題仍舊沒有突出的亮點,很可能就過不了。
在網上看了一個體系,可作為大致的學習檢閱自己的途徑,按照清單上的知識檢測自己還有哪些不足和提升,最後形成自己的知識體系。在工作、學習甚至面試時,可以快速定位到知識點。
1. JavaScript規定了幾種語言類型2. JavaScript對象的底層數據結構是什麼3. Symbol類型在實際開發中的應用、可手動實現一個簡單的 Symbol4. JavaScript中的變數在記憶體中的具體存儲形式5. 基本類型對應的內置對象,以及他們之間的裝箱拆箱操作6. 理解值類型和引用類型7. null和 undefined的區別8. 至少可以說出三種判斷 JavaScript數據類型的方式,以及他們的優缺點,如何準確的判斷數組類型9. 可能發生隱式類型轉換的場景以及轉換原則,應如何避免或巧妙應用10. 出現小數精度丟失的原因, JavaScript可以存儲的最大數字、最大安全數字, JavaScript處理大數字的方法、避免精度丟失的方法
一、JavaScript規定了幾種語言類型
問:講一下js的數據類型?
答:js的數據類型分為簡單數據類型和複雜數據類型;
簡單數據類型有六種,分別是String(字元串)、Number(數字)、Null(空)、undefined(未定義)、boolean(布爾值)、symbol(符號),表示不能再繼續分下去的類型,在記憶體中以固定的大小存儲在棧中,按值訪問;
複雜數據類型是指對象,這裡有常見的array、function、object等,本質上是一組無序的鍵值對組成。它的值大小不固定,所以保存在堆中,但在棧中會存儲有指向其堆記憶體的地址,按引用來訪問。js不允許直接訪問記憶體中的位置,也就是說不能直接操作對象的記憶體空間。也就是說,當我們想要訪問應用類型的值的時候,需要先從棧中獲得對象的地址指針,然後通過地址指針找到其在堆中的數據。
需要注意的是,
1、簡單數據類型中的boolean、number、string不是由內置函數new出來的,儘管他們有對應的引用類型;
2、symbol是ES6引入的一種新的原始數據,表示獨一無二且不可改變的值。通過 Symbol 函數調用生成,由於生成的 symbol 值為原始類型,所以 Symbol 函數不能使用 new 調用;
3、將一個變數賦值給另一個變數時,基礎類型複製的是值,賦值完成兩個變數在沒有任何關係;而對象類型的複製的是地址,修改一個變數另一個變數也會跟著一起變化。(如何解決這個問題?關於深拷貝and淺拷貝)
二、JavaScript對象的底層數據結構是什麼
這個問題目前對我來說,不能夠理解到底是想問什麼,還有問題,看到一篇這個文章,轉載《從chrome源碼看js object的實現》:https://www.rrfed.com/2017/04/04/chrome-object/
三、Symbol類型在實際開發中的應用、手動實現一個簡單的 Symbol
(暫未學習總結)
四、JavaScript中的變數在記憶體中的具體存儲形式
js的數據類型分為簡單數據類型和複雜數據類型;在記憶體中,簡單數據類型以固定的大小存儲在棧中;複雜數據類型存儲在堆中,且大小不固定,同時在棧中會存儲其指向堆地址的指針。
因為這裡問的是記憶體中的存儲形式,所以我一直注意的是記憶體中堆棧,後來忽然看到一篇文章寫了數據結構中的堆和棧就有一點懵,先簡單記錄一下相關知識點。
記憶體的堆棧:
是一種物理結構,用於存放不同數據的記憶體空間,分為棧區和堆區。
1)棧記憶體:
棧(stack)是向低地址擴展的數據結構,是一塊連續的記憶體區域;一般來說其大小是系統預先規定好的,存儲大小已知的變數(函數的參數值、局部變數的值等)。由作業系統自動申請分配並釋放(回收)空間,無需程式設計師控制,這樣的好處是記憶體可以及時得到回收。但棧的大小有限,如果申請的空間超過棧的剩餘空間,就會提示棧溢出(一般無窮次的遞歸調用或大量的記憶體分配會引起棧溢出)。
在分配記憶體的時候類似於數據結構中的棧,先進後出的原則,即從棧低向棧頂,依次存儲。棧是向下增長,即從高地址到低地址分配記憶體,且記憶體區域連續、每個單元的大小相同。如下圖:
2)堆記憶體:

數據結構的堆棧:
是一種抽象的數據存儲結構,
棧:一種連續存儲的數據結構,特點是存儲的數據先進後出,只能在棧頂一端對數據項進行插入和刪除。
堆:是一棵完全二叉樹結構(知識點未掌握)
五、基本類型對應的內置對象,以及他們之間的裝箱拆箱操作
1)基本包裝類型
問:有了基本類型為什麼還要包裝類型?
2)裝箱和拆箱
var s1 = “stringtext”;var s2 = s1.substring(2);
(1)創建String類型的一個實例 => var s1 = new String(“stringtext”);(2)在實例上調用指定的方法 => var s2 = s1.substring(2);(3)摧毀這個實例 => s1 = null;
var s1 = “stringtext”;s1.color = “red”; //在這一句話執行完的瞬間,第二行創建的String就已經被銷毀了。console.log(s1.color);//執行這一行程式碼時又創建了自己的String對象,而該對象沒有color屬性。//undefine
var obj = new Object(“stringtext”);console.log(obj instanceof String);//true
inputTpye
|
result
|
Null
|
不轉換,直接返回
|
Undefined
|
不轉換,直接返回
|
Number
|
不轉換,直接返回
|
Boolean
|
不轉換,直接返回
|
String
|
不轉換,直接返回
|
Symbol
|
不轉換,直接返回
|
Object
|
按照下列步驟進行轉換
|
六、理解值類型和引用類型
js包含兩種數據類型,基本數據類型和複雜數據類型,而其對應的值基本類型的值指的是簡單的數據段,引用類型指的是那些可能有多個值構成的對象。可以從三個方面來理解:動態的屬性、複製變數的值、傳遞參數
1)、動態的屬性
定義基本類型值和引用類型值的方式類似,即創建一個變數並為該變數賦值。兩者的區別在於,對於引用類型的值,我們可以為其添加屬性和方法,也可以改變和刪除其屬性和方法;對於基本類型的值,我們不能為其動態地添加屬性。
var person = new Object(); //創建一個對象並將其保存在變數person中person.name = “Song”; //為該對象添加一個名為name的屬性,並賦值為Songconsole.log(person.name); //訪問name這個屬性//Song
2)、複製變數的值
在從一個變數向另一個變數複製基本類型值和引用類型值時,兩則也是不同的,這主要是由於基本類型和引用類型在記憶體中存儲不同導致的。
var a = 1;var b = a;b = 2;console.log(a);//1console.log(b);//2
記憶體變化大致如下:

Ⅱ複製引用類型的值
var obj1 = {name:”Song”};var obj2 = obj1;obj2.name = “D”; //改變obj2的name屬性的值,則將obj1的也改變了。console.log(obj1.name);// D

註:關於深拷貝和淺拷貝
3)、傳遞參數
ECMAScript中所有函數的參數都是按值傳遞的,無論在向參數傳遞的是基本類型還是引用類型。(我的理解:正因為是按值傳遞的,所以我們才可以利用此來完成深拷貝)
有一道關於證明引用類型是按值傳遞還是按引用傳遞的題目如下:
function test(person){ person.age = 26; person = { name:'yyy', age:30 } return person } const p1 = { name:'yck', age:25 }; const p2 = test(p1); console.log(p1); console.log(p2);
首先當我們從一個變數向另一個變數複製引用類型的值時,這個值是存儲在棧中的指針地址,複製操作結束後,兩個變數引用的是同一個對象,改變其中一個變數,就會影響另一個變數。
而在向參數傳遞引用類型的值時,同樣是把記憶體中的地址複製給一個局部變數,所以在上述程式碼中,將p1的記憶體地址指針複製給了局部變數person,兩者引用的是同一個對象,這個時候在函數中改變變數,就會影響到外部。
接下來相當於從新開闢了一個記憶體空間,然後將此記憶體空間的地址賦給person,可以理解為將剛才指向p1的指針地址給覆蓋了,所以改變了person的指向,當該函數結束後便釋放此記憶體。
(此圖作為自己的理解,不代表實際,很有可能實際並不是這樣操作的。)
所以在person.age = 26;這句話執行後把p1記憶體里的值改變了,列印出來p1是{name: “yck”, age: 26} p2是{name: “yyy”, age: 30}
而我理解的如果按引用傳遞,則相當於person的指向是和p1也一樣,所以後續只要是對person進行了操作,都會直接影響p1。
因此在這種情況下,列印出來p1和p2都是{name: “yyy”, age: 30}
七、null和 undefined的區別
1)、null類型
var car = null;console.log(typeof car);//object
var s;console.log(s == undefined);//true
八、至少可以說出三種判斷 JavaScript數據類型的方式,以及他們的優缺點,如何準確的判斷數組類型
問:判斷js數據類型有哪幾種方式,分別有什麼優缺點?怎麼樣判斷一個值是數組類型還是對象?(或者typeof能不能正確判斷類型)
答:一般來說有5種常用的方法,分別是typeof、instanceof、Object.prototype.toString()、constructor、jquery的type();
1)對於typeof來說,在檢測基本數據類型時十分得力,對於基本類型,除了null都可以返回正確類型,對於對象來說,除了function都返回object。
基本類型
typeof “somestring” // ‘string’
typeof true // ‘boolean’
typeof 10 // ‘number’
typeof Symbol() // ‘symbol’
typeof null // ‘object’ 無法判定是否為 null
typeof undefined // ‘undefined’複雜類型
typeof {} // ‘object’
typeof [] // ‘object’ 如果需要判斷數組類型,則不能使用這樣方式
typeof(() => {}) // ‘function’
註:怎麼使用複合條件來檢測null值的類型?
var a = null;
(!a && typeof a === “object”); // true
2)對於instanceof來說,可以來判斷已知對象的類型,如果使用instanceof來判斷基本類型,則始終返回false。
其原理是測試構造函數的prototype是否出現在被檢測對象的原型鏈上;所有的複雜類型的值都是object的實例,在檢測一個引用類型值和Object構造函數時,instanceof操作符始終返回true。
[] instanceof Array //true -》 無法優雅的判斷一個值到底屬於數組還是普通對象({}) instanceof Object //true(()=>{}) instanceof Function //true
而且在《高程》上還看到說一個問題,如果不是單一的全局執行環境,比如網頁中包含多個框架,那麼實際上存在兩個以上不同的全局執行環境,從而存在兩個以上不同版本的Array構造函數,如果從一個框架向另外一個框架傳入數組,那麼傳入的數據與在第二個框架中原生創建的數組分別具有各自不同的構造函數。eg:例如index頁面傳入一個arr變數給iframe去處理,則即使arr instanceof Array還是返回false,因為兩個引用的Array類型不是同一個。並且constructor可以重寫所以不能確保萬無一失。
對於數組來說,相當於new Array()出的一個實例,所以arr.proto === Array.prototype;又因為Array是Object的子對象,所以Array.prototype.proto === Object.prototype。因此Object構造函數在arr的原型鏈上,便無法判斷一個值到底屬於數組還是普通對象。
註:判斷變數是否為數組的方法
3)通用但比較繁瑣的方法Object.prototype.toString()
該方法本質是利用Object.prototype.toString()方法得到對象內部屬性[[Class]],傳入基本類型也能夠判斷出結果是因為對其值做了包裝。
Object.prototype.toString.call({}) === ‘[object Object]’ ——-> true;
Object.prototype.toString.call([]) === ‘[object Array]’ ——-> true;
Object.prototype.toString.call(() => {}) === ‘[object Function]’ ——-> true;
Object.prototype.toString.call(‘somestring’) === ‘[object String]’ ——-> true;
Object.prototype.toString.call(1) === ‘[object Number]’ ——-> true;
Object.prototype.toString.call(true) === ‘[object Boolean]’ ——-> true;
Object.prototype.toString.call(Symbol()) === ‘[object Symbol]’ ——-> true;
Object.prototype.toString.call(null) === ‘[object Null]’ ——-> true;
Object.prototype.toString.call(undefined) === ‘[object Undefined]’ ——-> true;Object.prototype.toString.call(new Date()) === ‘[object Date]’ ——-> true;
Object.prototype.toString.call(Math) === ‘[object Math]’ ——-> true;
Object.prototype.toString.call(new Set()) === ‘[object Set]’ ——-> true;
Object.prototype.toString.call(new WeakSet()) === ‘[object WeakSet]’ ——-> true;
Object.prototype.toString.call(new Map()) === ‘[object Map]’ ——-> true;
Object.prototype.toString.call(new WeakMap()) === ‘[object WeakMap]’ ——-> true;
4)根據對象的constructor判斷
[].constructor === Array ——–> truevar d = new Date();d.constructor === Date ———> true(()=>{}).constructor === Function ——-> true注意: constructor 在類繼承時會出錯eg:function A(){};function B(){};A.prototype = new B(); //A繼承自Bvar aobj = new A();aobj.constructor === B ——–> true;aobj.constructor === A ——–> false;而instanceof方法不會出現該問題,對象直接繼承和間接繼承的都會報true:
5)jquery的type()
如果對象是undefined或null,則返回相應的“undefined”或“null”,jQuery.type( undefined ) === “undefined”jQuery.type() === “undefined”jQuery.type( null ) === “null”如果對象有一個內部的[[Class]]和一個瀏覽器的內置對象的 [[Class]] 相同,我們返回相應的 [[Class]] 名字。jQuery.type( true ) === “boolean”jQuery.type( 3 ) === “number”jQuery.type( “test” ) === “string”jQuery.type( function(){} ) === “function”jQuery.type( [] ) === “array”jQuery.type( new Date() ) === “date”jQuery.type( new Error() ) === “error” // as of jQuery 1.9jQuery.type( /test/ ) === “regexp”
6)如何判斷一個數組?
var a = [];
a.instanceof Array; ——–> true
a.constructor === Array ——–> true
Object.prototype.toString.call(a) === ‘[object Array]’ ——–> true
Array.isArray([]); ——–> true
九、可能發生隱式類型轉換的場景以及轉換原則,應如何避免或巧妙應用
(暫未整理)
十、出現小數精度丟失的原因、 JavaScript可以存儲的最大數字以及最大安全數字、JavaScript處理大數字的方法、避免精度丟失的方法
問:0.1+0.2 === 0.3 為什麼是false?
答:在ECMAScript數據類型中的Number類型是使用IEEE754格式來表示的整數和浮點數值,所謂浮點數值就是該數值必須包含一個小數點,並且小數點後面必須至少有一位數字。而在使用基於IEEE754數值的浮點運算時出現參數舍入的誤差問題,即出現小數精度丟失,無法測試特定的浮點數值。
①在進行0.1+0.2的時候首先要將其轉換成二進位。
0.1 => 0.0001 1001 1001 1001…(無限循環)0.2 => 0.0011 0011 0011 0011…(無限循環)②由於 JavaScript 採用 IEEE 754 標準,數值存儲為64位雙精度格式,數值精度最多可以達到 53 個二進位位(1 個隱藏位與 52 個有效位)。如果數值的精度超過這個限度,第54位及後面的位就會被丟棄,所以在相加的時候會因為小數位的限制而將二進位數字截斷。0.0001 1001 1001 1001…+0.0011 0011 0011 0011… = 0.0100110011001100110011001100110011001100110011001100③再轉換成十進位就成了0.30000000000000004,而非我們期望的0.3
在《js權威指南》中有指出:
Javascript採用了IEEE-745浮點數表示法(幾乎所有的程式語言都採用),這是一種二進位表示法,可以精確地表示分數,比如1/2,1/8,1/1024。遺憾的是,我們常用的分數(特別是在金融的計算方面)都是十進位分數1/10,1/100等。二進位浮點數表示法並不能精確的表示類似0.1這樣 的簡單的數字,上訴程式碼的中的x和y的值非常接近最終的正確值,這種計算結果可以勝任大多數的計算任務:這個問題也只有在比較兩個值是否相等時才會出現。這個問題並不是只在javascript中才會出現,在任何使用二進位浮點數的程式語言中都會出現這個問題。 所以說,精度丟失並不是語言的問題,而是浮點數存儲本身固有的缺陷。只不過在 C++/C#/Java 這些語言中已經封裝好了方法來避免精度的問題,而 JavaScript 是一門弱類型的語言,從設計思想上就沒有對浮點數有個嚴格的數據類型,所以精度誤差的問題就顯得格外突出。javascript的未來版本或許會支援十進位數字類型以避免這些舍入問題,在這之前,你更願意使用大整數進行重要的金融計算,例如,要使用整數‘分’而不是使用小數‘元’進行貨比單位的運算。
問:怎麼避免精度丟失?
Number.EPSILON === Math.pow(2, -52)// true 說明這個值Number.EPSILON是等於 2 的 -52 次方
Number.EPSILON * Math.pow(2, 2)
),即如果兩個浮點數的差小於這個值,我們就認為這兩個浮點數相等。function withinErrorMargin (left, right) { return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2); } withinErrorMargin(0.1 + 0.2, 0.3) //true
②math.js是一個廣泛應用於JavaScript 和 Node.js的數學庫,它的特點是靈活表達式解析器,支援符號計算,內置大量函數與常量,並提供集成解決方案來處理不同的數據類型,如數字,大數字,複數,分數,單位和矩陣。
2.65.toFixed(1) //2.6 結果正確2.45.toFixed(1) //2.5 希望得到的結果是2.42.35.toFixed(1) //2.4 結果正確
function RoundNum(n, m){ //n表示需要四捨五入的數,m表示需要保留的小數位數 var newNum = Math.round(n * Math.pow(10, m)) / Math.pow(10, m) ; //首先將要保留的小數位數的小數部分轉成整數部分,利用冪函數將n乘以10的m次方 //然後利用Math.round()方法進行四捨五入處理 //最後再除以10的m次方還原小數部分 //註:此時還未能將所有數字正確轉換。例如將1.0001保留3位小數我們想要的結果是1.000,而此時newNum裡面的值是1 //所以還需要處理此種特殊情況,即保留的小數位上全0 var newSNum = newNum.toString(); //這一步將剛才進行處理過的數轉換成字元串 var rs = newSNum.indexOf('.'); //利用indexOf查找字元串中是否有.,它返回某個指定的字元串值在字元串中首次出現的位置,不存在則返回-1 if (rs < 0) { rs = newSNum.length; newSNum += '.'; } while (newSNum.length <= rs + m) { //在末尾加0 newSNum += '0'; } return newSNum; } console.log(RoundNum(1.0005, 3)); //得到1.001
④封裝一個計算類(加、減、乘、除)
(暫未實際寫過)
問:JavaScript可以存儲的最大數字以及最大安全數字
答:最大數字是Number.MAX_VALUE、最大安全數字是Number.MAX_SAFE_INTEGER。Number.MAX_VALUE大於Number.MAX_SAFE_INTEGER,我的理解是js可以精確表示最大安全數字以內的數,超過了最大安全數字但沒超過最大數字可以表示,但不精確,如果超過了最大數字,則這個數值會自動轉換成特殊的Infinity值。
由於記憶體的限制,ECMAScript並不能保存世界上所有的數值,ECMAScript能夠表示的最小數值是Number.MIN_VALUE,能夠表示的最大數值是Number.MAX_VALUE。超過數值是正值,則被轉成Infinity(正無窮),如果是負值則被轉成-Infinity(負無窮)。如果在某次返回了正或負的Infinity值,那麼該值將無法繼續參與下一次的計算,所以我們需要確定一個數值是不是有窮的,即是不是位於最小和最大的數值之間,可以使用isFinite()函數,如果該函數參數在最小和最大數值之間時會返回true。注意,如果參數類型不是數值,Number.isFinite
一律返回false
。
JavaScript 能夠準確表示的整數範圍在-2^53
到2^53
之間(不含兩個端點),超過這個範圍,無法精確表示這個值。ES6 引入了Number.MAX_SAFE_INTEGER
和Number.MIN_SAFE_INTEGER
這兩個常量,用來表示這個範圍的上下限。Number.isSafeInteger()
則是用來判斷一個整數是否落在這個範圍之內。