iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 5
0
Modern Web

JavaScript 之旅系列 第 5

JavaScript 之旅 (5):String method - padStart & padEnd

在字串的前綴或後綴補字元,是字串處理常見的需求,過去要自行處理,終於在 ES2017 (ES8) 新增了 String.prototype.padStart()String.prototype.padEnd(),解決常見的需求!本篇來介紹它們,以及在 ECMAScript spec 是如何定義的,並附上 polyfill。

本文同步發表於 Titangene Blog:JavaScript 之旅 (5):String method - padStart & padEnd

「JavaScript 之旅」系列文章發文於:

前言

pad strings 是一個很常見的需求,例如:

  • 檔名編號的前綴,例如:final-001.mdfinal-066.md
  • 流水號的前綴,例如:000021000456
  • 日期、時間,例如:2020-09-2009:05
  • 讓 console 的輸出可以對齊:Error 001: xxx
  • 固定位數的二進位數字或十六進位數字,例如:00100x00FF

那過去和現代是如何處理這個需求?讓我們繼續看下去...

過去的補字元

也許你會自己寫個 padStart() 來處理補字元這種字串處理:

function padStart(string, targetLength, padString = ' ') {
  return (Array(targetLength).join(padString) + string)
    .slice(-targetLength)
}

console.log(padStart('18', 4, '0'));  // 0018

function padStart(string, targetLength, padString) {
  return padString.repeat(Math.max(0, targetLength - string.length)) + string;
}

console.log(padStart('18', 4, '0'));  // 0018

或者從 Stack Overflow 找到不錯的解法參考一下直接複製貼上?上面的範例都是我從 Stack Overflow 參考的 XD。

補充一個與 padStart() 有關的 npm 套件:left-pad,當年發生了一些故事,詳情可參閱:

現代的 padStartpadEnd

在 ES2017 (ES8) 新增了 String.prototype.padStart()String.prototype.padEnd(),終於不用自己處理常見的字串處理需求了!

語法:

  • 第一個參數都是 maxLength
  • 第二個參數都是 fillString
string.padStart(maxLength [, fillString])
string.padEnd(maxLength [, fillString])

這些 String 方法都會重複 fillString 這個字串多次,直到字串的長度到 maxLength 為止。

String.prototype.padStart() 是將重複的 fillString 字串加在原字串的前面,而 String.prototype.padEnd() 是加在原字串的後面。看一些簡單的範例:

console.log('18'.padStart(4, '0'));  // "0018"
console.log('18'.padEnd(4, '0'));    // "1800"

console.log('x'.padStart(4, 'ab'));  // "abax"
console.log('x'.padEnd(4, 'ab'));    // "xaba"

若不使用第二個參數 (即 fillString ),預設會是 " " (U+0020),也就是 space:

console.log('18'.padStart(4));  // "  18"
console.log('18'.padEnd(4));    // "18  "

若第二個參數為 '' (空字串),會回傳原字串:

console.log('18'.padStart(4, ''));  // "18"
console.log('18'.padEnd(4, ''));    // "18"

若原字串的 length >= 第一個參數的值 (即 maxLength ),則會回傳原字串:

console.log('1234'.padStart(2, '0'));  // "1234"
console.log('1234'.padEnd(2, '0'));    // "1234"

console.log('1234'.padStart(4, '0'));  // "1234"
console.log('1234'.padEnd(4, '0'));    // "1234"

若想在 Number 型別的值前面補 0,需要先將 Number 型別強制轉型成 String 型別:

let n = 18;

console.log(String(n).padStart(4, '0'));  // "0018"

否則會出現 TypeError 的錯誤 (因為 Number 沒有 padStartpadEnd 這些 method):

let n = 18;

console.log(n.padStart(4, '0'));
// TypeError: n.padStart is not a function

Sepc 定義

以下是 String.prototype.padStart()String.prototype.padEnd() 在 spec 中的定義:可以看到兩者的差異不大,但步驟 2 StringPad() 的最後一個傳入參數不一樣

傳入 startend 能幹嘛?我們接續看步驟 2 StringPad() 的定義:在步驟 11 和步驟 12 就是將 fillString concat 在原字串的前面或後面的關鍵 (看後面的 polyfill 會更好理解)

polyfill

下面是從 TC39 的 String.prototype.padStart()String.prototype.padEnd() 提案提供的 polyfill 稍做修改的 (看過剛剛的 spec 定義後,應該知道兩者只差一個步驟不同,原本的 polyfill 是將 spec 中的 StringPad() 都在兩者上分別實作,因邏輯可重用,我就將它抽成額外的 function 了):

const RequireObjectCoercible = O => {
  if (O === null || typeof O === 'undefined') {
    throw new TypeError('"this" value must not be null or undefined');
  }
  return O;
};
const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || Math.pow(2, 53) - 1;
const ToLength = argument => {
  const len = Number(argument);
  if (Number.isNaN(len) || len <= 0) {
    return 0;
  }
  if (len > MAX_SAFE_INTEGER) {
    return MAX_SAFE_INTEGER;
  }
  return len;
};

const StringPad = (O, maxLength, fillString, placement) => {
  const S = String(O);
  const intMaxLength = ToLength(maxLength);
  const stringLength = ToLength(S.length);
  if (intMaxLength <= stringLength) {
    return S;
  }
  let filler = typeof fillString === 'undefined' ? ' ' : String(fillString);
  if (filler === '') {
    return S;
  }
  const fillLen = intMaxLength - stringLength;
  while (filler.length < fillLen) {
    const fLen = filler.length;
    const remainingCodeUnits = fillLen - fLen;
    if (fLen > remainingCodeUnits) {
      filler += filler.slice(0, remainingCodeUnits);
    } else {
      filler += filler;
    }
  }
  const truncatedStringFiller = filler.slice(0, fillLen);
  if (placement === 'start') {
    return truncatedStringFiller + S;
  } else {
    return S + truncatedStringFiller;
  }
}

if (!String.prototype.padStart) {
  String.prototype.padStart = function padStart(maxLength, fillString = ' ') {
    const O = RequireObjectCoercible(this);
    return StringPad(O, maxLength, fillString, 'start');
  };
}

if (!String.prototype.padEnd) {
  String.prototype.padEnd = function padEnd(maxLength, fillString = ' ') {
    const O = RequireObjectCoercible(this);
    return StringPad(O, maxLength, fillString, 'end');
  };
}

其他 polyfill:

資料來源


上一篇
JavaScript 之旅 (4):Object.keys() & Object.values() & Object.entries()
下一篇
JavaScript 之旅 (6):Async Functions & await (1)
系列文
JavaScript 之旅30

尚未有邦友留言

立即登入留言