2023-08-03

#javascript#data filter#sort

JavaScript - 資料排序用法 Sort()

在常見的 UI component 裡面,經常需要先經過資料轉化排序後,才會真正呈現給使用者操作。

那麼今天就來談談 Sort() 在 JavaScript 的用途吧!

數字排序

const numbers = [1,2,3,4,5,6,7,8,9,10,11,12]
numbers.sort()
console.log(numbers)
// Output: [1, 10, 11, 12, 2, 3, 4, 5, 6, 7, 8, 9]

在一開始學習 sort() 的時候,如果沒有代入任何函式的時候,在 JavaScript 中,sort() 方法預設將元素視為字串進行排序,因此在對數字陣列進行排序時,會導致出現不正確的排序結果,也就是會變成以1的字串方式,找到相同的文字,進行排序。

為了解決這個問題,你可以使用自訂的排序函式來進行數字排序,這樣才能得到正確的順序。在自訂的排序函式中,你可以使用數字的差異(或其他數值比較方法)來決定元素的排序順序。

以下是一個將數字陣列按照數字由小到大進行排序的範例:

const numbers = [1.5, 0, 0.88, -3.5, 9, 111, 0.92, -1.23];
numbers.sort((a, b) => a - b);
console.log(numbers);
// Output: [-3.5, -1.23, 0, 0.88, 0.92, 1.5, 9, 111]

在這個例子中,我們使用了自訂的排序函式 (a, b) => a - b,這將返回 a 減去 b 的差異。這樣 sort() 方法就會根據這個差異來決定元素的排序順序,進而得到正確的數值排序結果。

這邊就可以發現 -3.5 是裡面數字最小的, 111 是最大的。

如果希望是反向排序,由數字最大至最小的方式,則是直接調整函式中的 b - a,變成 b 減去 a 的方式。

numbers.sort((a, b) => b - a);
console.log(numbers);
// Output: [111, 9, 1.5, 0.92, 0.88, 0, -1.23, -3.5]

在這邊,我們可以看到最大的數字 111 ,排序在陣列中的第 0 個位子,而 -3.5 則是在陣列的最後一位位子。

字串排序

接下來是進行文字排序的方法,首先是英文的排序方式,一般英文是採用 A-Z 或是 Z-A 的方法來排序字。 我們先來看看,當我們套用 a-b 的時候會發生什麼事呢?

const names = ["John", "Jane", "Sophia", "Michael", "Emily", "Ava", "Bobby", "Chloe", "Gavin", "Ethan", "Olivia", "Mia"]
names.sort( (a, b) => a - b)
// Output: ['John', 'Jane', 'Sophia', 'Michael', 'Emily', 'Ava', 'Bobby', 'Chloe', 'Gavin', 'Ethan', 'Olivia', 'Mia']

基本上,(a, b) => a - b 的函式不適合套用在這個上面,在 a - b 的比較方式下,JavaScript 會嘗試將兩個字串轉換為數字,然後再進行比較。因此,在這個例子中,names 陣列的元素都是字串(名字),它們不能被直接轉換為數字,所以會得到 NaN。 這就是為什麼在使用 a - b 的比較方式後,names 陣列的順序不會改變,結果仍然是:

['John', 'Jane', 'Sophia', 'Michael', 'Emily', 'Ava', 'Bobby', 'Chloe', 'Gavin', 'Ethan', 'Olivia', 'Mia']

只是需要簡單的使用 sort() ,就會得到按照字母順序 (A-Z) 排序的結果。

names.sort()
// Output: ['Ava', 'Bobby', 'Chloe', 'Emily', 'Ethan', 'Gavin', 'Jane', 'John', 'Mia', 'Michael', 'Olivia', 'Sophia']

那麼問題來了,如果希望的結果是按照字母順序 (Z-A) 排序的結果,該如何呢?

names.sort((a, b) => b.localeCompare(a) )
// Output: ['Sophia', 'Olivia', 'Michael', 'Mia', 'John', 'Jane', 'Gavin', 'Ethan', 'Emily', 'Chloe', 'Bobby', 'Ava']

可以看到這邊,一樣是使用 (a,b) 的函式,只是換成使用 localeCompare() 的方法去比對字串,就能夠按照字母順序 (Z-A) 排序了。

localeCompare() 是 JavaScript 字串的一個方法,它用於比較兩個字串的排序順序。這個方法將根據地區設定(locale)來確定排序的方式,因此可以用於不同地區的語言排序。

localeCompare() 方法是:

string.localeCompare(compareString[, locales[, options]])

其中,string 是要比較的字串,compareString 是要進行比較的另一個字串。locales 可選,用於指定語言地區,如果未指定則使用執行環境的默認地區設定。options 也是可選的,用於指定額外的選項,例如是否區分大小寫、是否忽略標點符號等。

當使用 localeCompare() 時,它會返回一個數字:
如果 string 排在 compareString 前面,則返回負數(通常是 -1)。
如果 string 和 compareString 相等,則返回 0。
如果 string 排在 compareString 後面,則返回正數(通常是 1)。

關於更多 localeCompare() 用法

我們來看看其它國家文字使用 localeCompare() 的排序結果吧!

中文

const users = ["王小明", "陳美玲", "張志偉", "李欣怡", "林俊傑", "吳佳蓉", "黃宗翰", "趙雅文", "劉建宏", "郭婷婷"];
users.sort()
// Output: ['劉建宏', '吳佳蓉', '張志偉', '李欣怡', '林俊傑', '王小明', '趙雅文', '郭婷婷', '陳美玲', '黃宗翰']
users.sort( (a, b) => a.localeCompare(b))
// Output: ['王小明', '吳佳蓉', '李欣怡', '林俊傑', '張志偉', '郭婷婷', '陳美玲', '黃宗翰', '趙雅文', '劉建宏']
users.sort( (a, b) => b.localeCompare(a))
// Output: ['劉建宏', '趙雅文', '黃宗翰', '陳美玲', '郭婷婷', '張志偉', '林俊傑', '李欣怡', '吳佳蓉', '王小明']

這邊排序的結果, sort() 依舊沒辦法依中文字的筆劃去排列出正確的順序,那麼用 localeCompare() 的方法一樣代入,就可以正確取得依中文姓氏排序的方法。

如果是拿來排序中文的數字,來比較大小,["零", "壹", "貳", "參", "肆", "伍", "陸", "柒", "捌", "玖", "拾", "佰", "仟", "萬", "億", "兆"],就不適用 localeCompare() 的方法囉!

泰文

這邊請 ChatGPT 幫忙生成 10 組不同的泰國餐廳名稱,然後再以下進行排序動作,

const restaurants = [
    "ร้านอาหารต้นตำรับ",
    "ร้านอาหารพุธแลนด์",
    "ร้านอาหารส้มตำ",
    "ร้านอาหารไทยซีฟู้ด",
    "ร้านอาหารข้าวกะเพรา",
    "ร้านอาหารต้นไทย",
    "ร้านอาหารสวนไทย",
    "ร้านอาหารก๋วยเตี๋ยว",
    "ร้านอาหารต้นทานตะวัน",
    "ร้านอาหารข้าวมันไก่"
];
restaurants.sort((a, b) => a.localeCompare(b, 'th'));
/* *Output: [
    'ร้านอาหารก๋วยเตี๋ยว',
    'ร้านอาหารข้าวกะเพรา',
    'ร้านอาหารข้าวมันไก่',
    'ร้านอาหารต้นตำรับ',
    'ร้านอาหารต้นทานตะวัน',
    'ร้านอาหารต้นไทย',
    'ร้านอาหารไทยซีฟู้ด',
    'ร้านอาหารพุธแลนด์',
    'ร้านอาหารส้มตำ',
    'ร้านอาหารสวนไทย'
]
*/

restaurants.sort((a, b) => b.localeCompare(a, 'th'));
/* Output: [
    'ร้านอาหารสวนไทย',
    'ร้านอาหารส้มตำ',
    'ร้านอาหารพุธแลนด์',
    'ร้านอาหารไทยซีฟู้ด',
    'ร้านอาหารต้นไทย',
    'ร้านอาหารต้นทานตะวัน',
    'ร้านอาหารต้นตำรับ',
    'ร้านอาหารข้าวมันไก่',
    'ร้านอาหารข้าวกะเพรา',
    'ร้านอาหารก๋วยเตี๋ยว'
]
*/

在我們的範例中,我們指定地區為 th,這代表泰國(Thailand)地區的語言規則。當我們使用 a.localeCompare(b, 'th') 時,它會根據泰國的語言規則來比較兩個泰文字串 a 和 b,並返回比較的結果。

雖然看不懂泰文,但是可以透過文字的排列組合和長度,去了解排序的變動,這裡的 th,只是一個選項,不需要特別比較不同區域的話,就不用特別寫出來。

日文

我們都知道日本的姓名會以中文加日文的方式排列組合,所以排序結果,也會以中文字的筆劃會順序排列。

const arr = [
  "宮崎駿",
  "北野武",
  "三島由紀夫",
  "綾波レイ",
  "坂本龍馬",
  "松井秀喜"
];
names.sort( (a,b) => a.localeCompare(b))
// Output: ['三島由紀夫', '北野武', '坂本龍馬', '松井秀喜', '宮崎駿', '綾波レイ']
names.sort( (a,b) => b.localeCompare(a))
// Output: ['綾波レイ', '宮崎駿', '松井秀喜', '坂本龍馬', '北野武', '三島由紀夫']

這邊我們就來看看日本的平假名排序結果吧!

const phrases = [
  "こんにちは", // 你好
  "ありがとう", // 謝謝
  "おはよう", // 早安
  "おやすみなさい", // 晚安
  "すみません", // 對不起
  "いいえ" // 不,沒有
];
phrases.sort( (a,b) => a.localeCompare(b))
// Output: ['ありがとう', 'いいえ', 'おはよう', 'おやすみなさい', 'こんにちは', 'すみません']
phrases.sort( (a,b) => b.localeCompare(a))
// Output: ['すみません', 'こんにちは', 'おやすみなさい', 'おはよう', 'いいえ', 'ありがとう']

法文

const places = [
  "Tour Eiffel", // 艾菲爾鐵塔
  "Louvre", // 羅浮宮
  "Notre-Dame de Paris", // 巴黎聖母院
  "Mont Saint-Michel", // 聖米歇爾山
  "Château de Versailles", // 凡爾賽宮
  "Côte d'Azur" // 蔚藍海岸
];
places.sort( (a,b) => a.localeCompare(b))
// Output: ['Château de Versailles', "Côte d'Azur", 'Louvre', 'Mont Saint-Michel', 'Notre-Dame de Paris', 'Tour Eiffel']
places.sort( (a,b) => b.localeCompare(a))
// Output: ['Tour Eiffel', 'Notre-Dame de Paris', 'Mont Saint-Michel', 'Louvre', "Côte d'Azur", 'Château de Versailles']

以上就是 sort() 的基本排序用法。

實作排序資料

接下來,我們來實作取得資料的時候,怎麼進行排序動作,這個排序的方法,實常會用在下拉式選項(dropdown select options)。
下面由 ChatGPT 產生的17筆使用者資料。

const data = [
  { id: 1, name: 'John Doe', age: 30, job: 'Engineer', company: 'ABC Corp', yearsOfExperience: 5 },
  { id: 2, name: 'Jane Smith', age: 25, job: 'Teacher', company: 'XYZ School', yearsOfExperience: 3 },
  { id: 3, name: 'Michael Johnson', age: 40, job: 'Doctor', company: 'City Hospital', yearsOfExperience: 15 },
  { id: 4, name: 'Emily Brown', age: 28, job: 'Designer', company: 'Design Studio', yearsOfExperience: 6 },
  { id: 5, name: 'William Wilson', age: 35, job: 'Developer', company: 'Tech Solutions', yearsOfExperience: 8 },
  { id: 6, name: 'Olivia Davis', age: 27, job: 'Writer', company: 'Publishing House', yearsOfExperience: 4 },
  { id: 7, name: 'James Lee', age: 32, job: 'Artist', company: 'Art Gallery', yearsOfExperience: 10 },
  { id: 8, name: 'Sophia Martin', age: 22, job: 'Student', company: 'University', yearsOfExperience: 0 },
  { id: 9, name: 'Ava Taylor', age: 29, job: 'Accountant', company: 'Accounting Firm', yearsOfExperience: 7 },
  { id: 10, name: 'Emma Jones', age: 33, job: 'Manager', company: 'Management Company', yearsOfExperience: 12 },
  { id: 11, name: 'Liam Brown', age: 10, job: 'Student', company: 'Elementary School', yearsOfExperience: 0 },
  { id: 12, name: 'Noah Wilson', age: 15, job: 'Student', company: 'High School', yearsOfExperience: 0 },
  { id: 13, name: 'Ethan Davis', age: 20, job: 'Intern', company: 'Tech Startup', yearsOfExperience: 1 },
  { id: 14, name: 'Oliver Lee', age: 45, job: 'Engineer', company: 'Tech Corp', yearsOfExperience: 18 },
  { id: 15, name: 'James Taylor', age: 50, job: 'Manager', company: 'Retail Company', yearsOfExperience: 20 },
  { id: 16, name: 'Grace Martin', age: 55, job: 'Doctor', company: 'Private Clinic', yearsOfExperience: 25 },
  { id: 17, name: 'Sophia Wilson', age: 60, job: 'Retired', company: 'N/A', yearsOfExperience: 40 }
];

我們先來以姓氏和名字排序的方法來重新顯示資料。
首先姓氏和名字各別分開,轉為一個新的物件。

const getFirstLast = (name) => {
    const [ firstName, lastName ] = name.split(" ")
    return { lastName, firstName }
}

依照英文名字排序

data.sort((a,b) => {
    const { firstName: firstA, lastName: lastA } = getFirstLast(a.name)
    const { firstName: firstB, lastName: lastB } = getFirstLast(b.name)
    return firstA.localeCompare( firstB, "en", { sensitivity: 'base' } )
})

輸出結果

[
    { id: 9, name: 'Ava Taylor', age: 29, job: 'Accountant', company: 'Accounting Firm', yearsOfExperience: 7 },
    { id: 4, name: 'Emily Brown', age: 28, job: 'Designer', company: 'Design Studio', yearsOfExperience: 6 },
    { id: 10, name: 'Emma Jones', age: 33, job: 'Manager', company: 'Management Company', yearsOfExperience: 12 },
    { id: 13, name: 'Ethan Davis', age: 20, job: 'Intern', company: 'Tech Startup', yearsOfExperience: 1 },
    { id: 16, name: 'Grace Martin', age: 55, job: 'Doctor', company: 'Private Clinic', yearsOfExperience: 25 },
    { id: 7, name: 'James Lee', age: 32, job: 'Artist', company: 'Art Gallery', yearsOfExperience: 10 },
    { id: 15, name: 'James Taylor', age: 50, job: 'Manager', company: 'Retail Company', yearsOfExperience: 20 },
    { id: 2, name: 'Jane Smith', age: 25, job: 'Teacher', company: 'XYZ School', yearsOfExperience: 3 },
    { id: 1, name: 'John Doe', age: 30, job: 'Engineer', company: 'ABC Corp', yearsOfExperience: 5 },
    { id: 11, name: 'Liam Brown', age: 10, job: 'Student', company: 'Elementary School', yearsOfExperience: 0},
    { id: 3, name: 'Michael Johnson', age: 40, job: 'Doctor', company: 'City Hospital', yearsOfExperience: 15 },
    { id: 12, name: 'Noah Wilson', age: 15, job: 'Student', company: 'High School', yearsOfExperience: 0 },
    { id: 14, name: 'Oliver Lee', age: 45, job: 'Engineer', company: 'Tech Corp', yearsOfExperience: 18 },
    { id: 6, name: 'Olivia Davis', age: 27, job: 'Writer', company: 'Publishing House', yearsOfExperience: 4 },
    { id: 8, name: 'Sophia Martin', age: 22, job: 'Student', company: 'University', yearsOfExperience: 0 },
    { id: 17, name: 'Sophia Wilson', age: 60, job: 'Retired', company: 'N/A', yearsOfExperience: 40 }
    { id: 5, name: 'William Wilson', age: 35, job: 'Developer', company: 'Tech Solutions', yearsOfExperience: 8 },
]

依照英文姓氏排序

data.sort((a,b) => {
    const { firstName: firstA, lastName: lastA } = getFirstLast(a.name)
    const { firstName: firstB, lastName: lastB } = getFirstLast(b.name)
    return lastA.localeCompare( lastB, "en", { sensitivity: 'base' } )
})

輸出結果

[
    { id: 4, name: 'Emily Brown', age: 28, job: 'Designer', company: 'Design Studio', yearsOfExperience: 6 },
    { id: 11, name: 'Liam Brown', age: 10, job: 'Student', company: 'Elementary School', yearsOfExperience: 0},
    { id: 6, name: 'Olivia Davis', age: 27, job: 'Writer', company: 'Publishing House', yearsOfExperience: 4 },
    { id: 13, name: 'Ethan Davis', age: 20, job: 'Intern', company: 'Tech Startup', yearsOfExperience: 1 },
    { id: 1, name: 'John Doe', age: 30, job: 'Engineer', company: 'ABC Corp', yearsOfExperience: 5 },
    { id: 3, name: 'Michael Johnson', age: 40, job: 'Doctor', company: 'City Hospital', yearsOfExperience: 15 },
    { id: 10, name: 'Emma Jones', age: 33, job: 'Manager', company: 'Management Company', yearsOfExperience: 12 },
    { id: 7, name: 'James Lee', age: 32, job: 'Artist', company: 'Art Gallery', yearsOfExperience: 10 },
    { id: 14, name: 'Oliver Lee', age: 45, job: 'Engineer', company: 'Tech Corp', yearsOfExperience: 18 },
    { id: 8, name: 'Sophia Martin', age: 22, job: 'Student', company: 'University', yearsOfExperience: 0 },
    { id: 16, name: 'Grace Martin', age: 55, job: 'Doctor', company: 'Private Clinic', yearsOfExperience: 25 },
    { id: 2, name: 'Jane Smith', age: 25, job: 'Teacher', company: 'XYZ School', yearsOfExperience: 3 },
    { id: 9, name: 'Ava Taylor', age: 29, job: 'Accountant', company: 'Accounting Firm', yearsOfExperience: 7 },
    { id: 15, name: 'James Taylor', age: 50, job: 'Manager', company: 'Retail Company', yearsOfExperience: 20 },
    { id: 5, name: 'William Wilson', age: 35, job: 'Developer', company: 'Tech Solutions', yearsOfExperience: 8 },
    { id: 12, name: 'Noah Wilson', age: 15, job: 'Student', company: 'High School', yearsOfExperience: 0 },
    { id: 17, name: 'Sophia Wilson', age: 60, job: 'Retired', company: 'N/A', yearsOfExperience: 40 }
]

這樣就可以輕鬆依姓氏或是名字個別重新排序資料了。

接下來我們以年齡來重新排序資料。

data.sort((a,b) => a.age - b.age)
[
    { id: 11, name: 'Liam Brown', age: 10, job: 'Student', company: 'Elementary School', yearsOfExperience: 0},
    { id: 12, name: 'Noah Wilson', age: 15, job: 'Student', company: 'High School', yearsOfExperience: 0 },
    { id: 13, name: 'Ethan Davis', age: 20, job: 'Intern', company: 'Tech Startup', yearsOfExperience: 1 },
    { id: 8, name: 'Sophia Martin', age: 22, job: 'Student', company: 'University', yearsOfExperience: 0 },
    { id: 2, name: 'Jane Smith', age: 25, job: 'Teacher', company: 'XYZ School', yearsOfExperience: 3 },
    { id: 6, name: 'Olivia Davis', age: 27, job: 'Writer', company: 'Publishing House', yearsOfExperience: 4 },
    { id: 4, name: 'Emily Brown', age: 28, job: 'Designer', company: 'Design Studio', yearsOfExperience: 6 },
    { id: 9, name: 'Ava Taylor', age: 29, job: 'Accountant', company: 'Accounting Firm', yearsOfExperience: 7 },
    { id: 1, name: 'John Doe', age: 30, job: 'Engineer', company: 'ABC Corp', yearsOfExperience: 5 },
    { id: 7, name: 'James Lee', age: 32, job: 'Artist', company: 'Art Gallery', yearsOfExperience: 10 },
    { id: 10, name: 'Emma Jones', age: 33, job: 'Manager', company: 'Management Company', yearsOfExperience: 12 },
    { id: 5, name: 'William Wilson', age: 35, job: 'Developer', company: 'Tech Solutions', yearsOfExperience: 8 },
    { id: 3, name: 'Michael Johnson', age: 40, job: 'Doctor', company: 'City Hospital', yearsOfExperience: 15 },
    { id: 14, name: 'Oliver Lee', age: 45, job: 'Engineer', company: 'Tech Corp', yearsOfExperience: 18 },
    { id: 15, name: 'James Taylor', age: 50, job: 'Manager', company: 'Retail Company', yearsOfExperience: 20 },
    { id: 16, name: 'Grace Martin', age: 55, job: 'Doctor', company: 'Private Clinic', yearsOfExperience: 25 },
    { id: 17, name: 'Sophia Wilson', age: 60, job: 'Retired', company: 'N/A', yearsOfExperience: 40 }
]

這樣就輕鬆以年齡由小至大重新排序資料了。


結語

通常資料從後端取回來後,都是經由從前端重新送出請求需求,後端會依需求過濾出資料,並且重新排序過,如果資料偏靜態且不會再變動,就有可能會由前端直接調整排序,但資料的處理模式,還是要看公司和前後端團隊的合作模式。

今天的筆記就到這裡!如果你正好卡到排序問題,希望這個能幫助你了解 sort() 的使用方法。