iT邦幫忙

1

JS 中的陣列 trim 方法

JavaScript 中有字串的 trim 方法,但卻沒有陣列的 trim ,現在就來嘗試實作一個陣列的 trim 吧。

本文的範例不考慮型別錯誤等問題,聚焦於方法本身的說明。

字串的 trim

在實作前,我們先來了解一下 trim 這個方法的功用,在字串的 trim 中,會將前後的空白字元,包括行的結束字符從字串中去除,例如下面這字串:

` hello
world 
`

string

  • 開頭有個空白字元
  • 中間有空白及換行字元
  • 結尾有換行字元

使用了 trim 回傳的結果為:

string-trim

  • 只剩下中間的空白及換行字元

大部分的情況下,由於字串前後的空白及換行字元多半是沒有意義且容易造成 bug 產生,例如比對字串是否相等時:

'hi' === 'hi ' // false

這時候 trim 就很有用處:

'hi'.trim() === 'hi '.trim() // true

除了前後都作用的 trim 之外,也有只作用一邊的方法:

' ya '.trimStart() // 'ya '
' ya '.trimEnd() // ' ya'

特定字元的 trim

前面內建的 trim 很好用,但如果想要刪去的是除了空白和換行字元以外的其他字元呢?例如:底線(_)或是小老鼠(@)。

這是就需要自製的方法來達成,首先設計介面:

function trimChar(string, chars) {...}
  • string: String 型別,原字串
  • chars: String 型別,要刪除的字元
  • 回傳值是 trim 作用過的字串

也可以直接寫在 Stringprototype 上: String.prototype.trimChar = function(chars) {...}

接下來實作方法:

function trimChar(string, chars) {  
  const strArr = string.split('');

  // 找到第一個不排除的字元索引
  const start = strArr.findIndex(ch => !chars.includes(ch));
  if(start === -1) return '';

  // 找到最後一個不排除的字元索引
  let reverseStart = strArr.slice().reverse().findIndex(ch => !chars.includes(ch));
  const end = string.length - reverseStart;

  // 去頭去尾
  return strArr.slice(start, end).join('');
}
trimChar('!hello!@world@', '!@'); // "hello!@world"
  • 因為要仰賴陣列處理,先將字串分割為字元陣列
  • 使用 findIndex 找到第一個不排除的字元索引
  • 如果找不到的話,代表整個字串都是想要排除的字元組成,因此回傳空字串
  • 將原字串反轉找出第一個不排除的字元,此為最後一個不排除的字元的索引
  • 由於取得的索引是反轉後的,所以需要利用字串長度反轉回正確的索引
  • slice 刪除前後想要排除的字元,並合併後回傳

這樣 trim 特定字元的方法就建置完成了。

只 trim 開頭/結尾特定字元

上面的是前後都 trim 的方法,想想如果只 trim 開頭或結尾其一的方法怎麼寫呢?

function trimCharStart(string, chars) {  
  const strArr = string.split('');

  // 找到第一個不排除的字元索引
  const start = strArr.findIndex(ch => !chars.includes(ch));
  if(start === -1) return '';

  // 直接取長度當作結尾索引
  const end = string.length;

  // 去頭去尾
  return strArr.slice(start, end).join('');
}

function trimCharEnd(string, chars) {  
  const strArr = string.split('');

  // 直接取 0 為開頭索引
  const start = 0;

  // 找到最後一個不排除的字元索引
  let reverseStart = strArr.slice().reverse().findIndex(ch => !chars.includes(ch));
  const end = string.length - reverseStart;

  // 去頭去尾
  return strArr.slice(start, end).join('');
}
trimCharStart('!hello!@world@', '!@'); // "hello!@world@"
trimCharEnd('!hello!@world@', '!@'); // "!hello!@world"

只要將開頭/結尾的索引設回原本的值就可以了。

陣列中的 trim

看了字串的 trim 實作後,對於陣列的 trim 有沒有一點概念了呢?接著就來想一下改怎麼處理陣列的部分吧。

陣列 trim 的定義

首先先來定義陣列 trim 介面:

function arrayTrim(array, exclude) {...}
  • array: any[] 型別,原陣列
  • exclude: element => boolean 型別,要排除的元素傳回 true 的回呼函數
  • 回傳值是 trim 作用過的陣列

同字串 trim 也可以寫在 Array.prototype 上。

可以先試著用上面字串的思維想想要怎麼實作 arrayTrim

實作 arrayTrim

function arrayTrim(array, exclude) {
  // 找到第一個不排除的字元索引
  const start = array.findIndex(ch => !exclude(ch));
  if(start === -1) return '';

  // 找到最後一個不排除的字元索引
  let reverseStart = array.slice().reverse().findIndex(ch => !exclude(ch));
  const end = array.length - reverseStart;

  // 去頭去尾
  return array.slice(start, end);
}

可以發現跟字串的方法大同小異,這是因為在上面講解字串 trim 時刻意使用陣列的思路來說明,要不然字串可以使用正規表達式寫出更簡潔的方法,詳細可以參考 StackOverflow 上的解答

// ex1
arrayTrim(['!', 'hello', '!', '@', 'world', '@'], element => ['!', '@'].includes(element));
// ["hello", "!", "@", "world"]

// ex2
arrayTrim([[0, '@'], [1, 'hello'], [2, '!'], [3, '@'], [4, 'world'], [5, '@']], element => ['!', '@'].includes(element[1]));
// [[1, 'hello'], [2, '!'], [3, '@'], [4, 'world']]

陣列的第二個參數會需要是回呼函數,因為陣列中可能會是複雜結構,像是第二個例子一樣。

trim 陣列的開頭/結尾的方法就交給各位想想嘍~

結論

會需要這個方法是由於最近處理到時間流的資料,他的資料會像下面這樣:

const datapoints = [
  [622,1450754160000],
  [587,1450754220000],
  [622,1450754280000],
  [123,1450754340000],
  [622,1450754400000],
  [851,1450754460000]
];

待在某個時間點的資料有可能是 null


const datapoints = [
  [null,1450754160000],
  [587,1450754220000],
  [null,1450754280000],
  [123,1450754340000],
  [622,1450754400000],
  [null,1450754460000]
];

繪圖時前後的資訊是不需要的,因為那個時間點本來就沒有資料或是已經沒有資料了,但中間的資料如果是 null 的話就需要將圖上的那個時間畫為 0 值,否則前後的資料會相連使人誤以為這是連續的數值。

參考資料

同步發表於 Limitless Ping


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
ch_lute
iT邦新手 5 級 ‧ 2020-03-20 14:09:31

小弟不太懂JS,都寫C和C#,請問
ch => !chars.includes(ch)
是什麼意思? 輸入的字元不是chars嗎? ch在includes()這裡的時候會代表什麼值?

應該是 chars => !string.includes(chars) 嗎?

ch => !chars.includes(ch)arrow function

  • ch : 是傳入參數
  • !chars.includes(ch) : 是回傳值,這裡是一個布林值

這個 arrow function 是 findIndex 的回呼函數,定義可以在 MDN 找到,他是一個循覽陣列每個元素時叫用的函數,有三個參數的函數並要回傳布林值,我們只需要第一個參數 element ,因此一個參數就好( JS 不需要的參數可以省略)。

到這裡我們知道 ch => !chars.includes(ch) 是傳入 findIndex 的回呼函數,而 findIndex 是作用在 strArr 上,而 strArr 是 string 拆開的陣列,可以得到下面的程式:

// 假定傳入的 string = '!hello!@world@'
'!hello!@world@'.split('').findIndex(ch => !chars.includes(ch));

因此 ch 會分別為 '!', 'h', 'e', 'l', 'l', 'o', '!', '@', 'w', 'o', 'r', 'l', 'd', '@'

所以 ch => !chars.includes(ch) 的意思是說 string 分為陣列後的每個元素 ch 做尋覽,如果包含想排除的字元(chars)的話回傳 false 否則回傳 true

ch_lute iT邦新手 5 級 ‧ 2020-03-20 15:44:23 檢舉

了解了,謝謝大神!
錯在我不了解findIndex函數內放arrow function的使用方法,把ch當成類似C#的委派時的用法,才會想不出後面includes()內為什麼放ch。

/images/emoticon/emoticon12.gif

我要留言

立即登入留言