JavaScript 修練 (4) | 型別判斷與原始型別包裹物件
強型別與弱型別
開始介紹 JavaScript 中型別有哪些和判斷的方式之前,我想先介紹一些強型別與弱型別的基礎知識,程式語言中依語言型別系統 ( Type system ) 分成「強型別語言」與「弱型別語言」兩種
- 強型別:程式所定義的變數型別等於變數在執行時的型別,在宣告變數時必須指定一種資料型別給它
- 弱型別:不需要在宣告變數時指定資料型別,雖語法較簡潔,但要注意型別轉換所產生的非預期結果
JavaScript 是弱型別語言
那麼 JavaScript 經過前幾篇文章的程式碼,可以很明顯發現宣告變數時只是直接賦予值,就開始使用這個變數了,並不需要指定資料型別給變數,所以我們可以知道 JavaScript 屬於「弱型別」語言
可是不需要指定型別,JavaScript 的型別是怎麼來的?JavaScript 的型別只在值本身,而非透過變數帶來資料型別的資訊,也就是說「變數沒有型別,值才有」
不用指定型別不是少一件事要做,也挺好的不是嗎?但是 JavaScript 沒有這麼單純,它的型別不是固定不變的,你可以透過一些方式讓變數的值轉換型別,這對於習慣撰寫強型別語言的開發者來說,簡直就是災難 調皮的 JavaScript
這表示如果不熟悉 JavaScript 型別轉換背後的規則,將可能在執行程式碼時產生不如預期的結果,至於哪些情況會讓型別發生轉換,就是這篇文章將提到的主要內容
JavaScript 的型別與判斷
在開始說明型別轉換之前,首先要先認識 JavaScript 有哪些型別,JavaScript 的型別主要區分為兩大類:「原始型別」( Primitives,也稱為基本型別 ) 與「物件型別」( Object )
原始型別
以下七種型別屬於原始型別,這邊就不細說每個型別的特徵,只專注在介紹型別判斷的內容,若想深入了解每個型別可以參考 MDN 的文件
- String ( 字串 )
- Number ( 數值 )
- Boolean ( 布林 )
- Undefined
- Null
- BigInt
- Symbol
物件型別
Object:不屬於原始型別的,都屬於物件型別
🔮只要不屬於原始型別,就會被歸類為「物件型別」,所以沒有什麼陣列型別或函式型別,不是原始型別就是物件型別
判斷型別的方式
想知道一個值屬於原始型別還是物件型別,只需要記得一個重點「物件型別可以自由新增屬性,但是原始型別不行」,接下來看看幾個範例
物件型別可以自由新增屬性
物件、陣列、函式屬於物件型別,試著新增屬性
物件:物件本身就有新增屬性的功能,印出 person 物件可以看見新增的 name 屬性,也能夠存取到物件內的 name 屬性值為
'小夏'
1
2
3
4const person = {};
person.name = "小夏";
console.log(person); // {name: '小夏'}
console.log(person.name); // 小夏陣列:雖然根據印出結果可以看見陣列內確實有新增的 name 屬性,可以運作,但實戰中請不要這樣做,陣列有它自己的運作方法,這樣奇怪的結構可能會造成一些操作問題
1
2
3
4
5const arr = [];
arr.name = "可愛的陣列列";
arr.push(1);
console.log(arr); // [1, name: '可愛的陣列列']
console.log(arr.name); // 可愛的陣列列函式:因為函式本身已經有 name 屬性了,為了不覆蓋它,改為新增 myName 屬性,可以用
console.log
取得屬性值,但是看不見 fn 函式裡面的內容,所以要改用console.dir
,強制它使用物件結構展示,就可以看到 fn 函式變成一個可展開的物件,展開後可以看見裡面有剛新增的 myName 屬性1
2
3
4
5const fn = function() {};
fn.myName = "小春";
console.log(fn.myName); // 小春
console.log(fn); // ƒ () {}
console.dir(fn); // 下拉展開可以看到 myName 屬性
原始型別無法新增屬性
相反的,原始型別無法新增屬性,以下方原始型別中的 number 數值型別來說,新增屬性不會跳錯,但是無法取得 myName 的值,而且無論用 console.log
、console.dir
都只會印出數值 1
1 | let num = 1; |
🔮如果試圖在 null、undefined 內新增屬性,會跳出 TypeError 的錯誤
typeof 的例外
相信講到判斷型別,一定會有人第一時間想到前篇提到的一元運算子 typeof
,沒錯!它會回傳運算元的型別,但是有一些例外狀況
1 | // 原始型別 |
觀察上方 typeof
程式碼,你就會發現 null
和 function
怪怪的,怎麼不是顯示我們預期的 null 跟 object
null
typeof null
為什麼是object
? 其實它是一個過去就存在的 bug,曾經也有被討論要修改,但考慮到這會影響太多舊有的程式,所以就不修改了產生這個 bug 的原因,簡單來說就是在 JavaScript 初期的實作中,JavaScript 的值是由一個表示「型別」的標籤,與實際內容的「值」所組合成的
但是由於物件 ( Object ) 這個型別的標籤是「0」,而且 null 代表的是空值 ( NULL pointer,慣例上會以 0x00 來表示 ),於是代表 null 的標籤與物件的標籤搞混了,就產生了這樣的錯誤結果
如果想了解更多 typeof null 的內容可以參考這篇文章:Null and typeof
function
再來說說函式,嚴格來說「函式是一個函式物件」,
typeof function(){ }
回傳的是function
,但實際上仍然是屬於「物件型別」,前面也驗證過物件型別能新增屬性,而函式也可以,只是與一般物件不同的是,函式多了可以被呼叫 ( be invoked ) 的功能而 ECMAScript 對於 function 的定義也可以作為參考
原始型別包裹物件
之後的文章會介紹一些型別轉換的內容,而其中會需要一些「原始型別包裹物件」( Primitive Wrapper Objects ) 的相關知識,所以加入在本篇文章介紹,原始型別中除了 undefined、null 之外,每個型別都有各自可以使用的方法 ( undefined、null 型別沒有包裹物件 )
如下範例,字串型別可以使用 toUpperCase
方法,將字串字母都轉為大寫,數值型別卻無法使用,可是字串是純值,不是物件、也不是函式,toUpperCase
方法又是從哪裡來的呢?
1 | let str = 'i am amy'; |
1 | // 數值型別沒有 toUpperCase 這個方法,試圖使用會跳出錯誤 |
原始型別的方法來自於「原始型別包裹物件」,建立一個純值時,就會套用與該值型別對應的包裹物件,因此純值就能使用包裹物件中的方法了
以剛剛的 toUpperCase
方法為例,就可以在 String 包裹物件中找到這個方法,使用 console.dir
印出 String 這個包裹物件,展開一個叫 prototype
的物件
往下尋找就可以找到 String 包裹物件中的 toUpperCase
方法
同理,數值和布林型別也有各自的包裹物件:Number、Boolean,同樣也可以試著用 console.dir(Number);
查看數值型別的包裹物件中有哪些方法,這邊就交給大家嘗試做做看囉 ~
原始型別包裹物件可用來轉換型別
而包裹物件其實也是一個可以被直接呼叫的函式,實戰中也會使用它來做型別的轉換,如以下範例,String 作為函式可以將傳入的值轉換成字串型別的值,所以轉型後的值能使用 String 中的 length 方法來查看字串的長度
1 | const str = String(123); |
不要把原始型別包裹物件作為函式建構子使用
以 String 來說,雖然可以使用 new 運算子來建構「字串」,但是這會造成所謂的「字串」變成物件,而這個物件會同時包含包裹物件的所有方法
直接看看用 new 運算子結合 String 包裹物件建立字串,會發生什麼事:印出結果都會明示或暗示你,你建立的字串變成物件了,所以要避免用這種方式來建立原始型別的值
1 | let str = new String('I am Amy'); // str 會是一個物件 |
📎補充:BigInt、Symbol 僅能作為函式,無法作為函式建構子使用,若試圖這樣做,會跳出 TypeError 的錯誤如下圖
結語
本篇文章簡單介紹了強型別與弱型別的基本概念,以及 JavaScript 中型別有哪些、型別判斷的方式和 typeof 的例外,最後介紹一些與原始型別包裹物件有關的內容,原始型別包裹物件在後面的文章也會出現,本篇文章算是建立一些基本知識,為接下來會往下介紹型別轉換時做鋪墊
那我們就下篇文章見囉 ~