阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!

寫了 20 年 JavaScript 之後,我見證了許多變化——從回調地獄到 async/await。但即將推出的 JavaScript 特性將徹底改變我們寫程式的方式。

我們已經使用轉譯器和 polyfill 測試了這些提案,結果令人印象深刻:原來需要 30 行的程式碼現在只需 10 行,複雜的邏輯變得一目了然,甚至初級開發人員(和 Devin😅)也可以理解我們程式碼庫的複雜部分。

如果您讀過我之前關於「你不知道自己需要的 HTML5 元素」「CSS 模態視窗」的文章,您就會知道我們Lingo.dev喜歡不同尋常的技術。這些即將推出的 JavaScript 功能解決了困擾開發人員多年的實際問題。

管道運算符提高程式碼可讀性

JavaScript 中複雜的資料轉換通常會導致函數呼叫深度嵌套,難以閱讀和維護。開發人員必須從內到外追蹤巢狀函數,並在括號之間跳轉才能理解資料流。

管道運算符( |> )透過允許資料以清晰的自上而下的方式流經一系列操作來解決此問題:

// Instead of this nested mess
const result = saveToDatabase(
  validateUser(
    normalizeData(
      enrichUserProfile(user)
    )
  )
);

// You'll write this
const result = user
  |> enrichUserProfile
  |> normalizeData
  |> validateUser
  |> saveToDatabase;

讓我們來看一個更複雜的實際範例。考慮一個隨時間增長的影像處理服務:

// Before: Nested function hell
function processImage(image) {
  return compressImage(
    addWatermark(
      optimizeForWeb(
        applyFilter(
          resizeImage(image, { width: 800, height: 600 }),
          'sepia'
        )
      ),
      'Copyright 2025'
    ),
    0.8
  );
}

// After: Clean, readable pipeline
function processImage(image) {
  return image
    |> (img) => resizeImage(img, { width: 800, height: 600 })
    |> (img) => applyFilter(img, 'sepia')
    |> optimizeForWeb
    |> (img) => addWatermark(img, 'Copyright 2025')
    |> (img) => compressImage(img, 0.8);
}

這是另一個實際範例,展示了管道運算子如何簡化分析資料處理:

// Before: Hard to follow the data flow
function analyzeUserData(users) {
  return generateReport(
    groupByMetric(
      filterInactiveUsers(
        normalizeUserData(users)
      ),
      'registrationMonth'
    )
  );
}

// After: Clear data transformation steps
function analyzeUserData(users) {
  return users
    |> normalizeUserData
    |> filterInactiveUsers
    |> (data) => groupByMetric(data, 'registrationMonth')
    |> generateReport;
}

管道操作符還可以更輕鬆地在步驟之間插入偵錯或日誌記錄:

function processPayment(payment) {
  return payment
    |> validatePayment
    |> (result) => {
        console.log(`Validation result: ${JSON.stringify(result)}`);
        return result;
      }
    |> processTransaction
    |> (result) => {
        console.log(`Transaction result: ${JSON.stringify(result)}`);
        return result;
      }
    |> sendReceipt;
}

如何立即使用

截至 2025 年 5 月,管道操作符目前處於 TC39 流程的第 2 階段。雖然它還不是官方 JavaScript 規範的一部分,但您現在就可以開始使用它:

使用 Babel(通用設定):

# Install the pipeline operator plugin
npm install --save-dev @babel/plugin-proposal-pipeline-operator

加到你的.babelrc

{
  "plugins": [
    ["@babel/plugin-proposal-pipeline-operator", { "proposal": "hack", "topicToken": "%"}]
  ]
}

使用 Vite:

# Install dependencies
npm install --save-dev vite-plugin-babel @babel/core @babel/plugin-proposal-pipeline-operator
// vite.config.js
import { defineConfig } from 'vite';
import babel from 'vite-plugin-babel';

export default defineConfig({
  plugins: [
    babel({
      babelConfig: {
        plugins: [
          ['@babel/plugin-proposal-pipeline-operator', { proposal: 'hack', topicToken: '%' }]
        ]
      }
    })
  ]
});

使用 Next.js:

# Install the pipeline operator plugin
npm install --save-dev @babel/plugin-proposal-pipeline-operator
// .babelrc
{
  "presets": ["next/babel"],
  "plugins": [
    ["@babel/plugin-proposal-pipeline-operator", { "proposal": "hack", "topicToken": "%" }]
  ]
}

使用 tsup:

# Install dependencies
npm install --save-dev tsup @babel/core @babel/plugin-proposal-pipeline-operator
// tsup.config.ts
import { defineConfig } from 'tsup';
import * as babel from '@babel/core';
import fs from 'fs';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  esbuildPlugins: [
    {
      name: 'babel',
      setup(build) {
        build.onLoad({ filter: /\.(jsx?|tsx?)$/ }, async (args) => {
          const source = await fs.promises.readFile(args.path, 'utf8');
          const result = await babel.transformAsync(source, {
            filename: args.path,
            presets: [
              ['@babel/preset-env', { targets: 'defaults' }],
              '@babel/preset-typescript'
            ],
            plugins: [
              ['@babel/plugin-proposal-pipeline-operator', { proposal: 'hack', topicToken: '%' }]
            ]
          });

          return {
            contents: result?.code || '',
            loader: args.path.endsWith('x') ? 'jsx' : 'js'
          };
        });
      }
    }
  ]
});

使用 Remix(使用 Vite):

# Install dependencies
npm install --save-dev vite-plugin-babel @babel/core @babel/plugin-proposal-pipeline-operator
// vite.config.js
import { defineConfig } from 'vite';
import { vitePlugin as remix } from '@remix-run/dev';
import babel from 'vite-plugin-babel';

export default defineConfig({
  plugins: [
    babel({
      babelConfig: {
        plugins: [
          ['@babel/plugin-proposal-pipeline-operator', { proposal: 'hack', topicToken: '%' }]
        ]
      }
    }),
    remix()
  ]
});

模式匹配簡化了複雜的條件

在大型程式碼庫中,複雜的 if/else 語句和 switch case 很快就會變得難以處理。嵌套 if/else 語句區塊的函數會檢查各種物件屬性,這使得驗證所有邊緣情況幾乎不可能。

模式匹配為這個問題提供了直接的解決方案。

此功能為 JavaScript 帶來了函數式程式設計功能,可讓您透過單一操作匹配和解構複雜資料:

function processMessage(message) {
  return match (message) {
    when String(text) => `Text message: ${text}`,
    when [String(sender), String(content)] => `Message from ${sender}: ${content}`,
    when { type: 'error', content: String(text), code: Number(code) } => 
      `Error: ${text} (Code: ${code})`,
    when _ => 'Unknown message format'
  };
}

如果沒有模式匹配,您將需要多個帶有類型檢查的 if/else 語句:

function processMessage(message) {
  // Check if it's a string
  if (typeof message === 'string') {
    return `Text message: ${message}`;
  }

  // Check if it's an array with specific structure
  if (Array.isArray(message) && 
      message.length === 2 && 
      typeof message[0] === 'string' && 
      typeof message[1] === 'string') {
    return `Message from ${message[0]}: ${message[1]}`;
  }

  // Check if it's an object with specific properties
  if (message && 
      typeof message === 'object' && 
      message.type === 'error' && 
      typeof message.content === 'string' && 
      typeof message.code === 'number') {
    return `Error: ${message.content} (Code: ${message.code})`;
  }

  // Default case
  return 'Unknown message format';
}

模式匹配在處理現代 Web 應用程式中的複雜狀態轉換時表現出色:

function handleUserAction(state, action) {
  return match ([state, action]) {
    when [{ status: 'idle' }, { type: 'FETCH_START' }] => 
      ({ status: 'loading', data: state.data }),

    when [{ status: 'loading' }, { type: 'FETCH_SUCCESS', payload }] => 
      ({ status: 'success', data: payload, error: null }),

    when [{ status: 'loading' }, { type: 'FETCH_ERROR', error }] => 
      ({ status: 'error', data: null, error }),

    when [{ status: 'success' }, { type: 'REFRESH' }] => 
      ({ status: 'loading', data: state.data }),

    when _ => state
  };
}

沒有模式匹配的等效程式碼明顯更冗長:

function handleUserAction(state, action) {
  // Check idle state + fetch start
  if (state.status === 'idle' && action.type === 'FETCH_START') {
    return { status: 'loading', data: state.data };
  }

  // Check loading state + fetch success
  if (state.status === 'loading' && action.type === 'FETCH_SUCCESS') {
    return { status: 'success', data: action.payload, error: null };
  }

  // Check loading state + fetch error
  if (state.status === 'loading' && action.type === 'FETCH_ERROR') {
    return { status: 'error', data: null, error: action.error };
  }

  // Check success state + refresh
  if (state.status === 'success' && action.type === 'REFRESH') {
    return { status: 'loading', data: state.data };
  }

  // Default: return unchanged state
  return state;
}

模式匹配還提供詳盡性檢查——如果您遺漏了某個可能的情況,編譯器會發出警告。這消除了困擾傳統條件邏輯的一整類錯誤。

這是解析配置格式的另一個實際範例:

function parseConfig(config) {
  return match (config) {
    when { version: 1, settings: Object(settings) } => 
      parseV1Settings(settings),

    when { version: 2, config: Object(settings) } => 
      parseV2Settings(settings),

    when String(jsonString) => 
      parseConfig(JSON.parse(jsonString)),

    when [String(env), Object(overrides)] =>
      mergeConfigs(getEnvConfig(env), overrides),

    when _ => 
      throw new Error(`Invalid configuration format: ${JSON.stringify(config)}`)
  };
}

如何立即使用

截至 2025 年 5 月,模式匹配目前處於 TC39 流程的第 1 階段,這意味著它仍處於提案階段,語法和語義方面的討論仍在進行中。不過,您現在可以嘗試:

使用 Babel(通用設定):

# Install the pattern matching plugin
npm install --save-dev babel-plugin-proposal-pattern-matching

加到你的.babelrc

{
  "plugins": ["babel-plugin-proposal-pattern-matching"]
}

使用 Vite:

# Install dependencies
npm install --save-dev vite-plugin-babel @babel/core babel-plugin-proposal-pattern-matching
// vite.config.js
import { defineConfig } from 'vite';
import babel from 'vite-plugin-babel';

export default defineConfig({
  plugins: [
    babel({
      babelConfig: {
        plugins: [
          'babel-plugin-proposal-pattern-matching'
        ]
      }
    })
  ]
});

使用 Next.js:

# Install the pattern matching plugin
npm install --save-dev babel-plugin-proposal-pattern-matching
// .babelrc
{
  "presets": ["next/babel"],
  "plugins": [
    "babel-plugin-proposal-pattern-matching"
  ]
}

使用 tsup:

# Install dependencies
npm install --save-dev tsup @babel/core babel-plugin-proposal-pattern-matching
// tsup.config.ts
import { defineConfig } from 'tsup';
import * as babel from '@babel/core';
import fs from 'fs';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  esbuildPlugins: [
    {
      name: 'babel',
      setup(build) {
        build.onLoad({ filter: /\.(jsx?|tsx?)$/ }, async (args) => {
          const source = await fs.promises.readFile(args.path, 'utf8');
          const result = await babel.transformAsync(source, {
            filename: args.path,
            presets: [
              ['@babel/preset-env', { targets: 'defaults' }],
              '@babel/preset-typescript'
            ],
            plugins: [
              'babel-plugin-proposal-pattern-matching'
            ]
          });

          return {
            contents: result?.code || '',
            loader: args.path.endsWith('x') ? 'jsx' : 'js'
          };
        });
      }
    }
  ]
});

生產替代方案:

對於今天的生產程式碼,請使用 ts-pattern 函式庫:

npm install ts-pattern
import { match, P } from 'ts-pattern';

function processMessage(message: unknown) {
  return match(message)
    .with(P.string, text => `Text message: ${text}`)
    .with([P.string, P.string], ([sender, content]) => 
      `Message from ${sender}: ${content}`)
    .with({ type: 'error', content: P.string, code: P.number }, 
      ({ content, code }) => `Error: ${content} (Code: ${code})`)
    .otherwise(() => 'Unknown message format');
}

Temporal API 解決日期處理問題

JavaScript 內建的Date物件長期以來一直是開發者們苦惱的根源。它易變(日期可能會被意外修改),月份索引混亂(一月是 0!),而且時區處理也存在問題。這些問題導致了 Moment.js、date-fns 和 Luxon 等函式庫的廣泛使用。

Temporal API 透過重新構想 JavaScript 中的日期和時間處理為這些問題提供了完整的解決方案。

以下是跨時區安排會議的實際範例:

// Current approach with Date (likely to have bugs)
function scheduleMeeting(startDate, durationInMinutes, timeZone) {
  const start = new Date(startDate);
  const end = new Date(start.getTime() + durationInMinutes * 60000);

  return {
    start: start.toISOString(),
    end: end.toISOString(),
    timeZone: timeZone // Not actually used in calculations
  };
}

// With Temporal API
function scheduleMeeting(startDateTime, durationInMinutes, timeZone) {
  const start = Temporal.ZonedDateTime.from(startDateTime);
  const end = start.add({ minutes: durationInMinutes });

  return {
    start: start.toString(),
    end: end.toString(),
    timeZone: start.timeZoneId // Properly tracked
  };
}

對於國際航班預訂系統,計算跨時區的航班時間變得非常簡單:

// Current approach with Date (error-prone)
function calculateFlightDuration(departure, arrival, departureTimeZone, arrivalTimeZone) {
  // Convert to milliseconds and calculate difference
  const departureTime = new Date(departure);
  const arrivalTime = new Date(arrival);

  // This doesn't account for timezone differences correctly
  const durationMs = arrivalTime - departureTime;

  return {
    hours: Math.floor(durationMs / (1000 * 60 * 60)),
    minutes: Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60)),
    // No easy way to get arrival in departure's timezone
  };
}

// With Temporal API
function calculateFlightDuration(departure, arrival) {
  const departureTime = Temporal.ZonedDateTime.from(departure);
  const arrivalTime = Temporal.ZonedDateTime.from(arrival);

  // Accurate duration calculation across time zones
  const duration = departureTime.until(arrivalTime);

  return {
    hours: duration.hours,
    minutes: duration.minutes,
    inLocalTime: arrivalTime.toLocaleString(),
    inDepartureTime: arrivalTime.withTimeZone(departureTime.timeZoneId).toLocaleString()
  };
}

以下是另一個範例,展示了 Temporal API 如何處理重複事件,這對於目前的 Date 物件來說非常困難:

// Current approach with Date (complex and error-prone)
function getNextMeetingDates(startDate, count) {
  const dates = [];
  const current = new Date(startDate);

  for (let i = 0; i < count; i++) {
    dates.push(new Date(current));

    // Add 2 weeks - error-prone due to month boundaries, DST changes, etc.
    current.setDate(current.getDate() + 14);
  }

  return dates;
}

// With Temporal API
function getNextMeetingDates(startDate, count) {
  const start = Temporal.PlainDate.from(startDate);
  const dates = [];

  for (let i = 0; i < count; i++) {
    const nextDate = start.add({ days: i * 14 });
    dates.push(nextDate);
  }

  return dates;
}

Temporal API 提供了幾個主要優點:

  1. 不變性:所有 Temporal 物件都是不可變的,防止意外修改

  2. 針對不同用例使用不同的類型:PlainDate、PlainTime、ZonedDateTime 等。

  3. 直覺的方法:清晰、可連結的日期運算方法

  4. 適當的時區處理:內建時區和夏令時支持

  5. 一致的行為:在所有瀏覽器中以相同的方式運作

如何立即使用

截至 2025 年 5 月,Temporal API 已處於 TC39 流程的第三階段,這意味著它即將完成,並且正在開發相關實作。以下是目前的使用方法:

使用官方 Polyfill:

npm install @js-temporal/polyfill
// Import in your code
import { Temporal } from '@js-temporal/polyfill';

// Get the current date and time
const now = Temporal.Now.plainDateTimeISO();
console.log(`Current time: ${now.toString()}`);

使用 Vite:

# Install the Temporal API polyfill
npm install @js-temporal/polyfill
// main.js or any entry file
import { Temporal } from '@js-temporal/polyfill';

// Now you can use Temporal API in your code
const now = Temporal.Now.plainDateTimeISO();
console.log(`Current time: ${now.toString()}`);

使用 Next.js:

# Install the Temporal API polyfill
npm install @js-temporal/polyfill
// pages/_app.js
import { Temporal } from '@js-temporal/polyfill';

// Make Temporal available globally if needed
if (typeof window !== 'undefined') {
  window.Temporal = Temporal;
}

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

export default MyApp;

使用 tsup:

# Install the Temporal API polyfill
npm install @js-temporal/polyfill
// src/index.ts
import { Temporal } from '@js-temporal/polyfill';

// Export it if you want to make it available to consumers of your package
export { Temporal };

// Your other code here

瀏覽器支援:

  • Chrome/Edge:在「實驗性 JavaScript」標誌後可用

  • Firefox:Firefox 139 及更高版本中可用

  • Safari:尚未實現

若要檢查 Temporal 是否受本機支援:

if (typeof Temporal !== 'undefined') {
  console.log('Temporal API is natively supported');
} else {
  console.log('Using polyfill');
  // Import polyfill
}

資源管理消除記憶體洩漏

JavaScript 長期以來缺乏確定性的資源清理機制。在處理文件、資料庫連接或硬體存取時,開發人員必須手動確保資源正確釋放——即使在發生錯誤的情況下也是如此。

這種限制會導致記憶體洩漏和資源耗盡錯誤。典型的模式涉及 try/finally 區塊,這些區塊很快就會變得難以處理:

async function processFile(path) {
  let file = null;
  try {
    file = await fs.promises.open(path, 'r');
    const content = await file.readFile({ encoding: 'utf8' });
    return processContent(content);
  } finally {
    if (file) {
      await file.close();
    }
  }
}

新的usingawait using語句在 JavaScript 中提供了確定性的資源管理:

async function processFile(path) {
  await using file = await fs.promises.open(path, 'r');
  const content = await file.readFile({ encoding: 'utf8' });
  return processContent(content);
  // File is automatically closed when the block exits
}

讓我們來看一個更複雜的例子,其中有多個資源必須按照特定的順序來管理:

// Current approach: Nested try/finally blocks
async function processData(dbConfig, filePath) {
  let db = null;
  let file = null;

  try {
    db = await Database.connect(dbConfig);

    try {
      file = await fs.promises.open(filePath, 'r');
      const data = await file.readFile({ encoding: 'utf8' });
      const processed = processRawData(data);

      return db.store(processed);
    } finally {
      if (file) {
        await file.close();
      }
    }
  } finally {
    if (db) {
      await db.disconnect();
    }
  }
}

// With resource management: Clean and safe
async function processData(dbConfig, filePath) {
  await using db = await Database.connect(dbConfig);
  await using file = await fs.promises.open(filePath, 'r');

  const data = await file.readFile({ encoding: 'utf8' });
  const processed = processRawData(data);

  return db.store(processed);
  // Resources are automatically cleaned up in reverse order:
  // 1. file is closed
  // 2. db is disconnected
}

對於資料庫密集型應用程式,此功能可以改變連接池管理:

class DatabaseConnection {
  constructor(config) {
    this.config = config;
    this.connection = null;
  }

  async connect() {
    this.connection = await createConnection(this.config);
    return this;
  }

  async query(sql, params) {
    return this.connection.query(sql, params);
  }

  async [Symbol.asyncDispose]() {
    if (this.connection) {
      await this.connection.close();
      this.connection = null;
    }
  }
}

// Using the connection with automatic cleanup
async function getUserData(userId) {
  await using db = await new DatabaseConnection(config).connect();
  return db.query('SELECT * FROM users WHERE id = ?', [userId]);
  // Connection is automatically closed when the function exits
}

以下是另一個範例,展示如何管理 WebUSB 設備等硬體資源:

// Current approach: Manual cleanup required
async function readFromUSBDevice(deviceFilter) {
  let device = null;
  try {
    const devices = await navigator.usb.getDevices();
    device = devices.find(d => d.productId === deviceFilter.productId);

    if (!device) {
      device = await navigator.usb.requestDevice({ filters: [deviceFilter] });
    }

    await device.open();
    await device.selectConfiguration(1);
    await device.claimInterface(0);

    const result = await device.transferIn(1, 64);
    return new TextDecoder().decode(result.data);
  } finally {
    if (device) {
      try {
        await device.close();
      } catch (e) {
        console.error("Error closing device:", e);
      }
    }
  }
}

// With resource management: Automatic cleanup
class USBDeviceResource {
  constructor(device) {
    this.device = device;
  }

  static async create(deviceFilter) {
    const devices = await navigator.usb.getDevices();
    let device = devices.find(d => d.productId === deviceFilter.productId);

    if (!device) {
      device = await navigator.usb.requestDevice({ filters: [deviceFilter] });
    }

    await device.open();
    await device.selectConfiguration(1);
    await device.claimInterface(0);

    return new USBDeviceResource(device);
  }

  async read() {
    const result = await this.device.transferIn(1, 64);
    return new TextDecoder().decode(result.data);
  }

  async [Symbol.asyncDispose]() {
    try {
      await this.device.close();
    } catch (e) {
      console.error("Error closing device:", e);
    }
  }
}

async function readFromUSBDevice(deviceFilter) {
  await using device = await USBDeviceResource.create(deviceFilter);
  return device.read();
  // Device is automatically closed when the function exits
}

如何立即使用

截至 2025 年 5 月,顯式資源管理提案 (using/await using) 已進入 TC39 流程的第 3 階段,這意味著它即將實現標準化。以下是目前的使用方法:

使用 Babel(通用設定):

# Install the resource management plugin
npm install --save-dev @babel/plugin-proposal-explicit-resource-management

加到你的.babelrc

{
  "plugins": ["@babel/plugin-proposal-explicit-resource-management"]
}

使用 Vite:

# Install dependencies
npm install --save-dev vite-plugin-babel @babel/core @babel/plugin-proposal-explicit-resource-management
// vite.config.js
import { defineConfig } from 'vite';
import babel from 'vite-plugin-babel';

export default defineConfig({
  plugins: [
    babel({
      babelConfig: {
        plugins: [
          '@babel/plugin-proposal-explicit-resource-management'
        ]
      }
    })
  ]
});

使用 Next.js:

# Install the resource management plugin
npm install --save-dev @babel/plugin-proposal-explicit-resource-management
// .babelrc
{
  "presets": ["next/babel"],
  "plugins": [
    "@babel/plugin-proposal-explicit-resource-management"
  ]
}

使用 tsup:

# Install dependencies
npm install --save-dev tsup @babel/core @babel/plugin-proposal-explicit-resource-management
// tsup.config.ts
import { defineConfig } from 'tsup';
import * as babel from '@babel/core';
import fs from 'fs';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  esbuildPlugins: [
    {
      name: 'babel',
      setup(build) {
        build.onLoad({ filter: /\.(jsx?|tsx?)$/ }, async (args) => {
          const source = await fs.promises.readFile(args.path, 'utf8');
          const result = await babel.transformAsync(source, {
            filename: args.path,
            presets: [
              ['@babel/preset-env', { targets: 'defaults' }],
              '@babel/preset-typescript'
            ],
            plugins: [
              '@babel/plugin-proposal-explicit-resource-management'
            ]
          });

          return {
            contents: result?.code || '',
            loader: args.path.endsWith('x') ? 'jsx' : 'js'
          };
        });
      }
    }
  ]
});

使物體可丟棄:

為了讓您的物件能夠using ,請實作Symbol.dispose方法:

class MyResource {
  [Symbol.dispose]() {
    // Cleanup code here
  }
}

對於非同步資源,實作Symbol.asyncDispose

class MyAsyncResource {
  async [Symbol.asyncDispose]() {
    // Async cleanup code here
  }
}

符號的 Polyfill:

// polyfill.js
if (!Symbol.dispose) {
  Symbol.dispose = Symbol("Symbol.dispose");
}

if (!Symbol.asyncDispose) {
  Symbol.asyncDispose = Symbol("Symbol.asyncDispose");
}

裝飾器無需改變核心邏輯即可加入功能

JavaScript 裝飾器提供了一種簡潔的方式來修改類別和方法,並賦予它們額外的功能。如果您使用過 TypeScript 或 Angular 等框架,那麼您一定對這個概念很熟悉。現在,裝飾器即將登陸原生 JavaScript。

裝飾器解決了一個基本問題:如何在不擾亂業務邏輯的情況下加入橫切關注點(如日誌記錄、驗證或效能監控)。

這是一個實際的例子:

class UserController {
  @authenticate
  @rateLimit(100)
  @validate(userSchema)
  @logAccess
  async updateUserProfile(userId, profileData) {
    // The method is automatically wrapped with:
    // 1. Authentication check
    // 2. Rate limiting (100 requests per hour)
    // 3. Input validation against userSchema
    // 4. Access logging

    const user = await this.userRepository.findById(userId);
    Object.assign(user, profileData);
    return this.userRepository.save(user);
  }
}

如果沒有裝飾器,您將需要手動包裝每個方法,這導致程式碼更難閱讀和維護:

class UserController {
  async updateUserProfile(userId, profileData) {
    // Authentication check
    if (!isAuthenticated()) {
      throw new Error('Unauthorized');
    }

    // Rate limiting
    if (isRateLimited(this.constructor.name, 'updateUserProfile', 100)) {
      throw new Error('Rate limit exceeded');
    }

    // Input validation
    const validationResult = validateWithSchema(userSchema, profileData);
    if (!validationResult.valid) {
      throw new Error(`Invalid data: ${validationResult.errors.join(', ')}`);
    }

    // Logging
    logAccess(this.constructor.name, 'updateUserProfile', userId);

    // Actual business logic
    const user = await this.userRepository.findById(userId);
    Object.assign(user, profileData);
    return this.userRepository.save(user);
  }
}

以下是使用裝飾器進行效能監控的真實範例:

class DataProcessor {
  @measure
  @cache
  processLargeDataset(data) {
    // Complex and time-consuming operation
    return data.map(item => /* complex transformation */)
               .filter(item => /* complex filtering */)
               .reduce((acc, item) => /* complex aggregation */);
  }
}

// Performance measurement decorator
function measure(target, name, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    const start = performance.now();
    const result = original.apply(this, args);
    const end = performance.now();
    console.log(`${name} took ${end - start}ms to execute`);
    return result;
  };
  return descriptor;
}

// Caching decorator
function cache(target, name, descriptor) {
  const original = descriptor.value;
  const cacheStore = new Map();

  descriptor.value = function(...args) {
    const key = JSON.stringify(args);
    if (cacheStore.has(key)) {
      console.log(`Cache hit for ${name}`);
      return cacheStore.get(key);
    }

    console.log(`Cache miss for ${name}`);
    const result = original.apply(this, args);
    cacheStore.set(key, result);
    return result;
  };

  return descriptor;
}

裝飾器對於 API 開發尤其重要。以下是使用裝飾器實作 RESTful API 並進行適當錯誤處理的範例:

class ProductAPI {
  @route('GET', '/products')
  @paginate
  @handleErrors
  async getAllProducts(req) {
    return this.productRepository.findAll();
  }

  @route('GET', '/products/:id')
  @handleErrors
  async getProductById(req) {
    const product = await this.productRepository.findById(req.params.id);
    if (!product) {
      throw new NotFoundError(`Product with ID ${req.params.id} not found`);
    }
    return product;
  }

  @route('POST', '/products')
  @validate(productSchema)
  @handleErrors
  async createProduct(req) {
    return this.productRepository.create(req.body);
  }
}

// Route decorator
function route(method, path) {
  return function(target, name, descriptor) {
    if (!target.constructor._routes) {
      target.constructor._routes = [];
    }

    target.constructor._routes.push({
      method,
      path,
      handler: descriptor.value,
      name
    });

    return descriptor;
  };
}

// Error handling decorator
function handleErrors(target, name, descriptor) {
  const original = descriptor.value;

  descriptor.value = async function(req, res) {
    try {
      const result = await original.call(this, req);
      return res.json(result);
    } catch (error) {
      if (error instanceof NotFoundError) {
        return res.status(404).json({ error: error.message });
      }
      if (error instanceof ValidationError) {
        return res.status(400).json({ error: error.message });
      }
      console.error(`Error in ${name}:`, error);
      return res.status(500).json({ error: 'Internal server error' });
    }
  };

  return descriptor;
}

如何使用裝飾器

截至 2025 年 5 月,裝飾器已進入 TC39 流程的第 3 階段,主流瀏覽器正在逐步實現。以下是目前的使用方法:

使用 Babel(通用設定):

# Install the decorators plugin
npm install --save-dev @babel/plugin-proposal-decorators

加到你的.babelrc

{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "version": "2023-05" }]
  ]
}

使用 Vite:

# Install dependencies
npm install --save-dev vite-plugin-babel @babel/core @babel/plugin-proposal-decorators
// vite.config.js
import { defineConfig } from 'vite';
import babel from 'vite-plugin-babel';

export default defineConfig({
  plugins: [
    babel({
      babelConfig: {
        plugins: [
          ['@babel/plugin-proposal-decorators', { version: '2023-05' }]
        ]
      }
    })
  ]
});

使用 Next.js:

# Install the decorators plugin
npm install --save-dev @babel/plugin-proposal-decorators
// .babelrc
{
  "presets": ["next/babel"],
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "version": "2023-05" }]
  ]
}

使用 TypeScript:

tsconfig.json中啟用裝飾器:

{
  "compilerOptions": {
    "target": "ES2022",
    "experimentalDecorators": true
  }
}

使用 tsup:

# Install dependencies
npm install --save-dev tsup @babel/core @babel/plugin-proposal-decorators
// tsup.config.ts
import { defineConfig } from 'tsup';
import * as babel from '@babel/core';
import fs from 'fs';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  esbuildPlugins: [
    {
      name: 'babel',
      setup(build) {
        build.onLoad({ filter: /\.(jsx?|tsx?)$/ }, async (args) => {
          const source = await fs.promises.readFile(args.path, 'utf8');
          const result = await babel.transformAsync(source, {
            filename: args.path,
            presets: [
              ['@babel/preset-env', { targets: 'defaults' }],
              '@babel/preset-typescript'
            ],
            plugins: [
              ['@babel/plugin-proposal-decorators', { version: '2023-05' }]
            ]
          });

          return {
            contents: result?.code || '',
            loader: args.path.endsWith('x') ? 'jsx' : 'js'
          };
        });
      }
    }
  ]
});

重要提示:

裝飾器提案已經歷了多次迭代。請確保您使用的是最新語法(2023-05 版),因為舊版本不相容。目前規格為裝飾器定義了三項功能:

  • 他們可以用匹配的值替換裝飾值

  • 它們可以提供對裝飾值的存取

  • 它們可以初始化裝飾值

測試您的配置

設定配置後,建立一個簡單的測試檔案來驗證功能是否正常運作:

// test-features.js

// Test pipeline operator
const double = x => x * 2;
const add = x => x + 1;
const square = x => x * x;

const result = 5
  |> double
  |> add
  |> square;

console.log("Pipeline result:", result); // Should be 121

// Test pattern matching
function processValue(value) {
  return match (value) {
    when String(s) => `String: ${s}`,
    when Number(n) => `Number: ${n}`,
    when { type, data } => `Object with type ${type}`,
    when _ => 'Unknown'
  };
}

console.log("Pattern matching:", processValue("test"), processValue(42), processValue({ type: "user", data: {} }));

// Test Temporal API
import { Temporal } from '@js-temporal/polyfill';
const now = Temporal.Now.plainDateTimeISO();
console.log("Current time:", now.toString());

// Test resource management
class Resource {
  constructor(name) {
    this.name = name;
    console.log(`Resource ${name} created`);
  }

  [Symbol.dispose]() {
    console.log(`Resource ${this.name} disposed`);
  }
}

{
  using resource = new Resource("test");
  console.log("Using resource");
} // Resource should be disposed here

// Test decorators
function log(target, name, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    console.log(`Calling ${name} with ${JSON.stringify(args)}`);
    return original.apply(this, args);
  };
  return descriptor;
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

const calc = new Calculator();
console.log("Decorator result:", calc.add(2, 3));

結論

這些即將推出的 JavaScript 功能不僅僅是語法糖,它們從根本上改變了我們編寫程式碼的方式。在Lingo.dev ,我們已經見證了它們如何將複雜、易錯的程式碼轉化為簡潔、易於維護的解決方案。

最棒的是,您無需等待。有了合適的工具,您現在就可以開始使用這些功能。從資源管理到正確處理日期,每個功能都能解決開發人員日常面臨的實際問題。

你對即將推出的 JavaScript 功能最感興趣嗎?你有沒有找到其他方法來解決程式碼庫中的這些問題?

分享您的想法!


有用的連結:


原文出處:https://dev.to/maxprilutskiy/upcoming-javascript-features-you-should-know-about-178h

按讚的人:

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。

阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!