iT邦幫忙

2025 iThome 鐵人賽

DAY 8
0
Modern Web

JavaScript 進階修煉與一些 React ——離開初階工程師新手村的頭30天系列 第 8

離開 JS 初階工程師新手村的 Day 08|從新手短劍到魔法武器:ES6+ 重構實戰

  • 分享至 

  • xImage
  •  

實戰:建立一個字串處理的模組化工具庫

看了昨天的 es6+ 工具的介紹,可以幫助我們實現工具庫的模組化實踐,因此我們來練習寫一個字串處理的工具,並在寫好後根據昨天的 es6+ 特性來重構這個小工具。

前提

變數命名始終是一個大哉問,例如命名流派(?)就有分 camel casepascal casekebab casesnake case。可能 conventionally 前端使用一種、後端使用一種、資料庫又使用另一種,這時我們就可能需要轉換變數名稱。

  • camel case: 第一個字母小寫,後續單字首字母大寫 (userName)
  • pascal case: 每個單字首字母都大寫 (UserName)
  • kebab case: 單字間用連字號分隔,全小寫 (user-name)
  • snake case: 單字間用底線分隔,全小寫 (user_name)

我們最終想要一個 functional programming 的介面,可以輸入一個字串,然後不需要重複輸入,就可以轉變成不同的 case,如下

const converter = letterCase("userName");
const camel = converter.toCamelCase();
const snake = converter.toSnakeCase();

因此,這個模組化工具的主要內容應該看起來長這樣:

function letterCase(str = ''){
  const chars = parse(str)

  return {
    toCamelCase: () => ...,
    toSnakeCase: () => ...,
    toKebabCase: () => ...,
    toPascalCase: () => ...,
    toConstantCase: () => ...,
    toDotCase: () => ...
  }
}

此處的 chars 為一個個小寫字母的 array,其中用 null 當作一個 element 為分界,那在每一個轉換的 function 就變得很容易,因為只要每次遇到 null 就代表後面應該要加 -. 、或改成大寫。如 user-name 會被爬成 ['u', 's', 'e', 'r', null, 'n', 'a', 'm', 'e']

雖然用正規表達可以快很多,但因為正規表達式很難懂,所以沒關係!我們就走邏輯派!

https://ithelp.ithome.com.tw/upload/images/20250919/20168365cqNR9MC8pC.png

接著就可以繼續處理拿到的 array,根據觀察,字串分成兩種:一種是用某個東西把字接起來的(也就是用符號把目前的 null 取代),另一種是直接改變大小寫。按照這兩個邏輯,我們可以把上面 letterCase 的內容填起來:
https://ithelp.ithome.com.tw/upload/images/20250919/201683652ycd03RkFk.png

可以開始了

寫完了很開心 XD 忘了這只是為了提供一個範例好讓我們可以改寫成比較 es6+ 特性的結構。

  1. Symbol 取代 null - 更安全的分隔標記
  2. Generator + Iterator - 可以逐一遍歷不同格式
  3. Destructuring - 優雅的參數解構和返回值處理
  4. Spread Operator - 字串轉字符陣列、批量處理
  5. Private Fields - 真正的封裝
  6. Getter - 更直觀的屬性訪問

首先,可以把一些 const 變數提取出來,以更有語意的 Symbol 創建標記以避免打錯或忘記。

const SEPARATOR_SYMBOL = Symbol('separator');

/**
 * 使用 Map 來映射字符碼,提升可讀性
 */
const SEPARATOR_CHARS = new Map([
  [45, '-'],   // hyphen
  [46, '.'],   // dot  
  [95, '_']    // underscore
]);

const CASE_TYPES = {
  CAMEL: Symbol('camel'),
  PASCAL: Symbol('pascal'),
  SNAKE: Symbol('snake'),
  KEBAB: Symbol('kebab'),
  CONSTANT: Symbol('constant'),
  DOT: Symbol('dot')
};

接著使用 desctruingspread operator 改寫 parse()

class LetterCaseConverter {
  #parsedChars = [];
  
  constructor(str = "") {
    this.#parsedChars = this.#parse(str);
  }

  #parse(str) {
    const output = [];
    
    // 使用 Spread Operator 將字串轉為字符陣列
    const chars = [...str];
    
    for (const [index, char] of chars.entries()) {
      const charCode = char.charCodeAt(0);
      const lowerChar = char.toLowerCase();
      const upperChar = char.toUpperCase();

      // 使用 Map 檢查是否為分隔符
      if (SEPARATOR_CHARS.has(charCode)) {
        output.push(SEPARATOR_SYMBOL);
        continue;
      } 
      
      // 檢查大寫字母(除了第一個字符)
      if (char === upperChar && index !== 0) {
        output.push(SEPARATOR_SYMBOL, lowerChar);
      } else {
        output.push(lowerChar);
      }
    }

    return output;
  }

應用同樣的技巧,一步步改寫其他的 function,並使用 getter 回傳內部定義好的方法。

  #capitalizedBasedSeparation(initialCapitalization = false) {
    let capitalize = initialCapitalization;
    const output = [];

    // 使用 Destructuring Assignment 處理字符
    for (const char of this.#parsedChars) {
      if (char === SEPARATOR_SYMBOL) {
        capitalize = true;
      } else if (capitalize) {
        output.push(char.toUpperCase());
        capitalize = false;
      } else {
        output.push(char);
      }
    }

    return output.join("");
  }

  #charBasedSeparation(separator = "", transform = char => char) {
    const output = [];

    for (const char of this.#parsedChars) {
      if (char === SEPARATOR_SYMBOL) {
        output.push(separator);
      } else {
        output.push(transform(char));
      }
    }

    return output.join("");
  }

  get camelCase() {
    return this.#capitalizedBasedSeparation(false);
  }

  get pascalCase() {
    return this.#capitalizedBasedSeparation(true);
  }

  get snakeCase() {
    return this.#charBasedSeparation("_");
  }

  get kebabCase() {
    return this.#charBasedSeparation("-");
  }

  get constantCase() {
    return this.#charBasedSeparation("_", char => char.toUpperCase());
  }

  get dotCase() {
    return this.#charBasedSeparation(".");
  }

  /**
   * 使用 Destructuring Assignment 返回所有格式
   */
  get allCases() {
    return {
      camelCase: this.camelCase,
      pascalCase: this.pascalCase,
      snakeCase: this.snakeCase,
      kebabCase: this.kebabCase,
      constantCase: this.constantCase,
      dotCase: this.dotCase
    };
  }

  /**
   * 使用 Generator 創建迭代器,可以逐一產生不同格式
   */
  *caseIterator() {
    const cases = this.allCases;
    
    // 使用 Destructuring 遍歷
    for (const [type, value] of Object.entries(cases)) {
      yield { type, value };
    }
  }

  /**
   * 使用 Symbol.iterator 讓物件可以被 for...of 遍歷
   */
  *[Symbol.iterator]() {
    yield* this.caseIterator();
  }

  /**
   * 使用 Spread Operator 的批量轉換方法
   */
  static convertMultiple(...strings) {
    return strings.map(str => new LetterCaseConverter(str).allCases);
  }

  /**
   * 使用 Rest Parameters 和 Destructuring 的工廠方法
   */
  static fromMultipleSources(...sources) {
    const [primary, ...alternatives] = sources;
    const converter = new LetterCaseConverter(primary);
    
    // 如果主要來源為空,嘗試替代來源
    if (!primary && alternatives.length > 0) {
      const validSource = alternatives.find(src => src && src.trim());
      return new LetterCaseConverter(validSource || '');
    }
    
    return converter;
  }
}

// 使用 ES6 Modules 導出
export default LetterCaseConverter;

// 使用 Destructuring 導出便利函數
export const { 
  convertMultiple,
  fromMultipleSources 
} = LetterCaseConverter;

// 便利的工廠函數
export const letterCase = str => new LetterCaseConverter(str);

最後,我們來看一下如何使用

1. 基本使用

const converter = letterCase("userName");
console.log('Getter API:', converter.camelCase);  // Getter API:
userName

2. Destructuring Assignment

const { camelCase, snakeCase, kebabCase } = converter.allCases;
console.log('Destructuring:', { camelCase, snakeCase, kebabCase });

https://ithelp.ithome.com.tw/upload/images/20250919/20168365xfzZbIDZQS.png

3. Spread Operator - 批量轉換

const results = LetterCaseConverter.convertMultiple(
"userName", 
"user-name", 
"user_name"
);
console.log('Spread Operator:', results);

這邊的輸出結果會得到三個 item 一樣的 array,符合我們轉換全部種類的預期。

https://ithelp.ithome.com.tw/upload/images/20250919/20168365YwIFw7byRc.png

4. Iterator & Generator - 使用 for...of 遍歷

也可以使用 iterator 得到以上的內容

for (const { type, value } of converter) {
console.log(`  ${type}: ${value}`);
// Iterator:
//  camelCase: userName
//  pascalCase: UserName
//  snakeCase: user_name
//  kebabCase: user-name
//  constantCase: USER_NAME
//  dotCase: user.name
}

5. Destructuring with Rest - 工廠方法

const converter2 = LetterCaseConverter.fromMultipleSources(
'', 
'fallback-name', 
'another-option'
);
console.log('Rest Parameters:', converter2.camelCase); // Rest Parameters:
fallbackName

6. Symbol

console.log('SEPARATOR_SYMBOL:', SEPARATOR_SYMBOL.toString());
console.log('CASE_TYPES:', Object.values(CASE_TYPES).map(s => s.toString()));

https://ithelp.ithome.com.tw/upload/images/20250919/20168365S9aFrdDNwl.png


上一篇
離開 JS 初階工程師新手村的 Day 07|新武器庫:ES6+ 語法
下一篇
離開 JS 初階工程師新手村的 Day 09|自訂 Hook:鍛造自己的技能卷軸
系列文
JavaScript 進階修煉與一些 React ——離開初階工程師新手村的頭30天10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言