在剛開始寫 JavaScript 可能大多數的人不會特別意識到 JavaScript 的型別系統有什麼特別之處,我是在看完 Youtube 上 CS50 的課程,才理解到在不同的程式語言中,會因為語言的特性而有不同的系統,JavaScript 就是偏向比較特別的那一種。
JavaScript 型別特別之處,源自於「弱型別」與「高階語言」的特性,前者會在遇到需要條件判斷時針對型別轉為真值(Truthy Value)與假值(Falsy Value),而後者意味著:JavaScript 無法直接針對硬體進行操作,會直接使用 JavaScript 所提供的介面操作對應環境(瀏覽器、伺服器)中的項目,這兩件事對於 JavaScript 的型別系統有兩大影響:
初學者可能會看到上方的敘述感到頭很痛,會想說:「這些記憶體、轉型到底跟 JavaScript 的型別有什麼直接關係?」
如果你也有這樣的疑惑,就跟我先從最基礎的型別開始了解吧!
在 JavaScript 中主要會分為兩大型別,分別是:基礎型別與物件型別,兩者的差異有:
這邊我會以自己覺得好理解的資料格式順序介紹:
字串是一個在 JavaScript 非常重要的型別,在一些 API 資料格式,與 HTML Input 元素取出的值大部分都是以字串為主,字串型別通常會以單引號「’」或是「”」雙引號將文字包裹起來,例如:
const name = "Vivian";
const year = "2023";
只要是被引號包裹起來的,不論其中的內容物是數字、符號、字元,在 JavaScript 中都被視為字串型別。
針對字串 JavaScript 提供了一些可以用來處理字串的方法,這邊列舉一些常用的方法:
const name = "Vivian";
const year = "2023";
// string.length 可計算字元長度
const nameLength = name.length;
console.log(nameLength);
// 6
// string.toLowerCase() 可以將字串中的大寫字母轉為小寫
const nameLowerCase = name.toLowerCase();
console.log(nameLowerCase);
// vivian
// string.toUpperCase() 可以將字串中的小寫字母轉為大寫
const nameUpperCase = name.toUpperCase();
console.log(nameUpperCase);
// VIVIAN
//string.trim() 過濾字串前後的連續空白字元,在進行表單欄位驗證時非常方便
const trimName = `${name} `.trim();
console.log(trimName);
// Vivian
在實務上,字串的資料格式常常是在取得來自後台的文案、資料庫的使用者資料:信箱、電話號碼、姓名等,因為數字與符號都可以被使用者不小心輸入進去,因此字串方法五花八門,也會為了符合公司、業主需求,延伸出一些驗證手段,使用 trim 方法過濾掉連續空白字元,可以說是最基本一種表單驗證手段。
JavaScript 數字可以用來顯示整數與小數點位數,但要注意一點是,針對小數點的處理是使用浮點數計算法(非十進位),所以針對小數點的處理會有誤差,如果要針對小數點做運算,建議可以轉為整數做處理,或是可以使用一些函式庫做處理:
// 在 JavaScript 直接計算小數點會有誤差:
0.1 + 0.2 = 0.30000000000000004
// 轉為整數處理
let result = (0.1 * 10 + 0.2 * 10) / 10
// result = 0.3
接著我們來看一些常用的數字方法:
// 將字串轉為數字,常用在轉換表單資訊的數字
const num = Number('1234');
console.log(num);
// 1234
// 針對小數四捨五入
const num = Math.round(3.6);
console.log(num);
// 4
// 針對小數無條件捨去
const num = Math.floor(3.6);
console.log(num);
// 3
// 取得介於 0 與 1 之間的隨機小數,通常可以用來搭配一些運算方式讓 UI 依照這些亂數順序顯示
const randomNumber = Math.random()
console.log(randomNumber);
// 0.03365605210247358
在實務上,數字型別會用來儲存跟數量、時間有關的單位,要注意的是只有數字可以被拿來做數學上的運算,所以需要用到計算時,要特別注意自己拿到的資料,是否真的是數字型別。
布林是一種用來顯示 true
或是 false
的資料格式,通常會用來進行條件判斷,我個人認為布林是基本型別中最容易令人誤解的資料型別,它看似很簡單,但卻會涉及複雜的真值、假值,與自動轉型的議題,由於這部分會需要完整理解 JavaScript 中的型別系統,才能有更清楚的理解,這部分我會獨立出一篇內容來做介紹。
NaN 是英文 not a number 的縮寫,這個型別可能會出現在字串數字混合運算時出現預期外錯誤,導致最後運算出來的結果因型別錯誤而產生的非數字的結果,型別有可能是 NaN。
在 JavaScript 中,只要不是「基本型別」,可以儲存多種複合資料格式的型別,都可以稱之為廣義的物件。
而狹義的物件會用大括號 { } 包裹鍵值(key value pairs),如果有多組鍵值的話會使用逗號來區隔,主要的用途為呈現多種複合的資料,舉一個常見使用者資料作為範例:
const user = {
name: 'Vivian',
age: 26,
gender: 'female',
email: '[email protected]',
isEmailVerified: true,
};
假設我們寫一串假資料來模擬使用者行為,上方這個範例就是一個很常見的使用者資訊,譬如說:使用者的名稱、年齡、電子信箱以及電子信箱是否經過驗證等,可以看出來這樣的資料結構會是較為複合,甚至可以說能夠組成相對應更加複雜的資料結構。
與基本型別最大的不同的是,這些鍵值可以被視作物件的屬性,這些屬性可以被視作這個物件整體的小小器官、組織,各自象徵這個物件的一些特性,例如:一個網站的使用者身份是由許多不同的資料組成。
我們還可以針對物件屬性進行複寫、取值的的動作:
// 覆寫
user.name = 'vvn';
// 讀取物件
console.log(user);
/* {name: 'vvn', age: 26,
gender: 'female',
email: '[email protected]',
isEmailVerified: true} */
// 讀取物件屬性
console.log(user.name);
// 'vvn'
不過這樣覆寫物件屬性的方式,並不是一個好的做法,原因後續我們會提到。
由於物件自己本身的方法並沒有很頻繁被使用到,會需要在特定的場景下才會派的上用場,原因我們後續會提到,所以這裡我們先不談廣義物件的方法。
或是可以參考我撰寫的系列文章:致 JavaScript 開發者的 Functional Programming 新手指南 。
陣列是一種用來管例重複性資料的資料結構,也是物件型別的一種,與物件最大的不同是,陣列是有順序性的,通常會使用中括號 [ ] 並以逗號分隔資料單位,舉例來說:
let numbers = [1, 2, 3, 4, 5];
let fruits = ['apple', 'banana', 'orange'];
// 取用陣列資料
console.log(fruits[0])
// 修改陣列資料
fruits[0] = 'berry';
console.log(fruits);
// ['berry', 'banana', 'orange'];
在實務上,我們會用陣列來儲存更加複雜的資料格式,例如:商品列表、使用者列表、數據等,由於我們會需要針對這些複雜的資料格式進行重複的處理,有些甚至是跟使用者行為有關的 UI 行為,因此比起物件來說,前端工程師可能花更多的時間處理陣列這個資料格式,接著就讓我們來看看有哪一些常用的陣列方法吧!
forEach
:// forEach 遍歷陣列,使用情境:直接針對陣列做操作(對新手來說較好理解,實務上非必要不太會使用)
function processElement(element) {
// 執行特定的邏輯
console.log(element);
}
let fruits = ['apple', 'banana', 'orange'];
fruits.forEach(processElement);
forEach
是一個藉由帶入 callback 函式來遍歷(從第一個到最後一個)陣列中元素的陣列方法,通常陣列方法的第一個參數會是遍歷當前元素,第二個參數會是元素的索引(index)。
由於 forEach
本身不會回傳值,所以通常要碼是直接更改陣列,或是要在新增一個空的陣列,搭配其他的陣列方法來進行處理。
由於直接修改陣列內容會因為物件本身的記憶體位置特性,導致污染原始資料,所以我們比較少用到,如果從 JavaScript 記憶體角度不好理解這件事的話,也可以從另外一個實務開發角度來看這件事。
假設我們撈取了一筆使用者資料的列表,我想要在 A 頁面進行資料的批次處理,B 頁面保持原始資料,為了不要頻繁的呼叫 API ,我們還是要將 A 頁面經處理的資料,與 B 頁面要使用的原始資料分隔開來,不然就會出現頁面資料彼此污染的狀況。
filter
:在許多情境下我們會需要針對既有的資料,來進行內容的篩選來避免網路請求所需要的額外時間與流量,我們會在 filter 陣列方法中帶入一個 callback 函式,並針對回傳條件的布林值,回傳出一個全新的陣列,我們來看一個篩選一到十之間偶數的篩選方式:
// 假設有一個陣列包含數字資料
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 使用 filter 過濾偶數數字
let evenNumbers = numbers.filter(function(number) {
return number % 2 === 0;
});
console.log(evenNumbers); // 輸出: [2, 4, 6, 8, 10]
在實務上的應用可能會更加複雜一點,舉例來說:透過指定的類別、分類篩選出指定的商品列表。
map
:map
的概念為「映射」,我們可以帶入一個 callback 函式針對陣列進行運算後回傳結構類似,但樣貌、規律、結果可能不同的列表,這樣說可能會有點抽象,讓我們來看簡單的範例:
// 假設有一個陣列包含數字資料
let numbers = [1, 2, 3, 4, 5];
// 使用 map 將每個數字平方
let squaredNumbers = numbers.map(function(number) {
return number ** 2;
});
console.log(squaredNumbers); // 輸出: [1, 4, 9, 16, 25]
上述的範例中,我們把一串數字陣列,套用了計算平方的邏輯,最終產出一串與原始資料結構有類似邏輯,但計算規律不同的陣列。
在實務上,前端工程師可能不太會處理到複雜數學的計算邏輯,但有可能拿一串原始資料整理出 UI 元件,例如:商品列表、使用者列表、選單之類的 UI 元件,此時 UI 元件如果有一些重複性的文案或是邏輯,就可以透過 map
一次性套用原始資料的內容。
map
與 filter
的優勢很顯而易見,就是能做到遍歷陣列的目的,同時整理、處理資料,但又不會污染原始資料。
由於陣列時常用於處理重複性的資料,也常常會遇到一些來自使用者的客製化需求,掌握好陣列方法可以說是非常有效益的一件事,這裡簡單條列一些,會用到且容易了解的陣列方法。
push(element)
:向陣列末尾新增一個元素。pop()
:移除並返回陣列末尾的元素。shift()
:移除並返回陣列開頭的元素。unshift(element)
:向陣列開頭新增一個或多個元素。length
:取得陣列的長度。concat(array)
:合併多個陣列。slice(startIndex, endIndex)
:回傳從起始索引到結束索引(不包括結束索引)的子陣列。splice(startIndex, deleteCount, items)
:從指定索引位置開始刪除指定數量的元素,並可插入新的元素。indexOf(element)
:返回指定元素在陣列中首次出現的索引,若不存在則返回 -1。includes(element)
:判斷陣列是否包含指定元素,返回布林值。如果你會對如何更好的使用陣列方法撰寫 JavaScript,我會推薦你來閱讀這一系列的文章:致 JavaScript 開發者的 Functional Programming 新手指南,其中會有一些更進階的陣列方法應用與一些前端的延伸套件介紹。
我是 Vivian,我們下次見。