JavaScript Array sliceの実践ガイド|実務パターンとサンプルコード解説

JavaScript

JavaScript Array sliceの実践ガイド|実務パターンとサンプルコード解説

JavaScriptで配列を扱うプログラミングは日々の開発で避けられません。その中でもsliceメソッドは一見シンプルですが、適切に使いこなすことでコードの品質と保守性が大きく向上します。本記事では、教科書的な解説ではなく、実務プロジェクトで実際に活用できるsliceの使い方をパターン別に解説します。

1. slice メソッドの基本解説

Array.prototype.slice()は、既存の配列の浅いコピーを返すメソッドです。元の配列を変更せず、指定された範囲の要素を新しい配列として取得できます。

基本構文:

const newArray = array.slice(start, end);
  • start:開始インデックス(含む)。負の値を指定すると末尾からのカウント
  • end:終了インデックス(含まない)。省略すると配列の最後まで
  • 重要:元の配列は変更されない(非破壊的)

この特性が実務では非常に重要です。元の配列を保持したまま、必要な部分だけを取り出せるため、副作用のないコードを書きやすくなります。

2. 実務でのユースケース

2-1. ページネーション実装

APIレスポンスやデータベースから取得した全件データをクライアント側でページング表示する場合、sliceは最も効率的です。

// ユースケース:大量データをページで分割表示する必要がある場合
class PaginationManager {
  constructor(items, itemsPerPage = 10) {
    this.items = items;
    this.itemsPerPage = itemsPerPage;
    this.currentPage = 1;
  }

  getPage(pageNumber) {
    // ページ番号の妥当性チェック
    const totalPages = this.getTotalPages();
    if (pageNumber < 1 || pageNumber > totalPages) {
      throw new Error(`Invalid page number. Must be between 1 and ${totalPages}`);
    }

    const startIndex = (pageNumber - 1) * this.itemsPerPage;
    const endIndex = startIndex + this.itemsPerPage;
    return this.items.slice(startIndex, endIndex);
  }

  getTotalPages() {
    return Math.ceil(this.items.length / this.itemsPerPage);
  }

  nextPage() {
    if (this.currentPage < this.getTotalPages()) {
      this.currentPage++;
      return this.getPage(this.currentPage);
    }
    return null;
  }

  previousPage() {
    if (this.currentPage > 1) {
      this.currentPage--;
      return this.getPage(this.currentPage);
    }
    return null;
  }
}

// 実装例
const products = Array.from({ length: 150 }, (_, i) => ({
  id: i + 1,
  name: `Product ${i + 1}`,
  price: Math.floor(Math.random() * 10000)
}));

const paginator = new PaginationManager(products, 15);
console.log(paginator.getPage(1)); // 最初の15件
console.log(paginator.nextPage()); // 次の15件

このパターンは、e-commerceサイトの商品一覧表示やテーブルデータの表示などで日常的に使われます。

2-2. API レスポンスの選択的抽出

外部APIから取得したレスポンスデータから、必要な件数だけ取り出す場合があります。特に検索結果やフィード機能で活躍します。

// ユースケース:検索APIから上位N件のみ取得して表示
async function fetchAndLimitSearchResults(query, limit = 20) {
  try {
    const response = await fetch(`https://api.example.com/search?q=${query}`);
    const data = await response.json();
    
    // APIが大量の結果を返す場合、必要な分だけsliceで取得
    const limitedResults = data.results.slice(0, limit);
    
    return limitedResults.map(item => ({
      id: item.id,
      title: item.title,
      relevanceScore: item.score
    }));
  } catch (error) {
    console.error('Search failed:', error);
    return [];
  }
}

// 使用例
fetchAndLimitSearchResults('JavaScript', 5).then(results => {
  console.log(`Found ${results.length} results`);
});

2-3. 配列の浅いコピー作成

特にReactなどのフレームワークで、元の配列を保持したまま新しい配列参照が必要な場合に使用します。

// ユースケース:Stateの不変性を保つ(React)
function handleRemoveItem(itemId) {
  // 元のitemsを変更せず、新しい配列を作成
  const updatedItems = items.slice();
  const index = updatedItems.findIndex(item => item.id === itemId);
  
  if (index !== -1) {
    updatedItems.splice(index, 1);
  }
  
  // 新しい配列参照でStateを更新
  setItems(updatedItems);
}

3. 実装コード:実務パターン集

3-1. スライディングウィンドウアルゴリズム

連続したN個の要素を順序を保ったまま処理する必要がある場合(例:7日間の移動平均、ローリング計算)で活躍します。

// ユースケース:過去7日間の売上合計を日次で計算する
class RollingCalculator {
  constructor(windowSize) {
    this.windowSize = windowSize;
  }

  calculateRollingSum(data) {
    const results = [];
    
    for (let i = 0; i <= data.length - this.windowSize; i++) {
      const window = data.slice(i, i + this.windowSize);
      const sum = window.reduce((acc, val) => acc + val, 0);
      results.push({
        endIndex: i + this.windowSize - 1,
        sum: sum,
        average: sum / this.windowSize
      });
    }
    
    return results;
  }
}

// 実装例:過去7日間の売上データ
const dailySales = [15000, 18000, 12000, 22000, 16000, 19000, 21000, 23000, 17000, 20000];
const calculator = new RollingCalculator(7);
const rollingResults = calculator.calculateRollingSum(dailySales);
console.log(rollingResults);
// [
//   { endIndex: 6, sum: 123000, average: 17571.43 },
//   { endIndex: 7, sum: 130000, average: 18571.43 },
//   ...
// ]

3-2. TypeScript での型安全な実装

実務プロジェクトではTypeScriptの採用も増えており、型付けされたslice処理が重要です。

// TypeScript実装例:ジェネリック型を活用
interface PaginationResult<T> {
  items: T[];
  total: number;
  hasMore: boolean;
}

class TypeSafePaginator<T> {
  constructor(
    private data: T[],
    private pageSize: number = 10
  ) {}

  getPage(pageNumber: number): PaginationResult<T> {
    const startIdx = (pageNumber - 1) * this.pageSize;
    const endIdx = startIdx + this.pageSize;
    
    // sliceで型を保ったまま新しい配列を取得
    const items = this.data.slice(startIdx, endIdx);
    const hasMore = endIdx < this.data.length;
    
    return {
      items,
      total: this.data.length,
      hasMore
    };
  }

  getAllPages(): T[][] {
    const pages: T[][] = [];
    const totalPages = Math.ceil(this.data.length / this.pageSize);
    
    for (let i = 1; i <= totalPages; i++) {
      pages.push(this.data.slice(
        (i - 1) * this.pageSize,
        i * this.pageSize
      ));
    }
    
    return pages;
  }
}

// 使用例
interface User {
  id: number;
  name: string;
  email: string;
}

const users: User[] = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' },
  // ... more users
];

const paginator = new TypeSafePaginator<User>(users, 5);
const page1 = paginator.getPage(1);

3-3. Python での実装(参考)

JavaScriptと同様にPythonでもslice概念は重要です。実務ではJavaScript以外の言語も使うため、参考として記載します。

from typing import List, TypeVar, Generic
from dataclasses import dataclass

T = TypeVar('T')

@dataclass
class PaginationResult(Generic[T]):
    items: List[T]
    total: int
    has_more: bool

class PythonPaginator(Generic[T]):
    def __init__(self, data: List[T], page_size: int = 10):
        self.data = data
        self.page_size = page_size
    
    def get_page(self, page_number: int) -> PaginationResult[T]:
        start_idx = (page_number - 1) * self.page_size
        end_idx = start_idx + self.page_size
        
        # Pythonのスライス記法は直感的
        items = self.data[start_idx:end_idx]
        has_more = end_idx < len(self.data)
        
        return PaginationResult(
            items=items,
            total=len(self.data),
            has_more=has_more
        )
    
    def get_all_pages(self) -> List[List[T]]:
        pages = []
        total_pages = (len(self.data) + self.page_size - 1) // self.page_size
        
        for i in range(total_pages):
            start = i * self.page_size
            end = start + self.page_size
            pages.append(self.data[start:end])
        
        return pages

# 使用例
products = [f'Product {i}' for i in range(1, 51)]
paginator = PythonPaginator(products, 10)
page1 = paginator.get_page(1)
print(f"Total items: {page1.total}, Has more: {page1.has_more}")

4. よくある応用パターン

4-1. 最後のN件を取得

負のインデックスを活用して、配列の末尾からの取得が可能です。これはログファイルの最新N件表示などで活躍します。

// ユースケース:ログの最新100件を表示
function getLatestLogs(allLogs, count = 100) {
  // 最後の100件を取得
  return allLogs.slice(-count);
}

const logs = Array.from({ length: 5000 }, (_, i) => `Log entry ${i + 1}`);
const recentLogs = getLatestLogs(logs, 20);
console.log(recentLogs[0]); // "Log entry 4981"

4-2. 配列の一部を別の配列で置き換え

sliceとspreadオペレータを組み合わせることで、特定範囲のみ置き換え可能です。

// ユースケース:キャッシュデータの部分更新
function updateArrayRange(array, startIndex, newItems) {
  return [
    ...array.slice(0, startIndex),
    ...newItems,
    ...array.slice(startIndex + newItems.length)
  ];
}

const originalData = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const updated = updateArrayRange(originalData, 3, [100, 200]);
console.log(updated); // [1, 2, 3, 100, 200, 6, 7, 8, 9, 10]

4-3. 複数条件でのデータフィルタリング後のページネーション

実務では複数の処理を組み合わせることが大多数です。

// ユースケース:ユーザー検索結果をフィルタリングしてページング
class AdvancedUserSearch {
  constructor(allUsers) {
    this.allUsers = allUsers;
  }

  search(options) {
    const { 
      keyword = '', 
      role = null, 
      minAge = 0,
      status = 'active',
      page = 1,
      pageSize = 10
    } = options;

    // 複数条件でフィルタリング
    let filtered = this.allUsers.filter(user => {
      const matchesKeyword = !keyword || 
        user.name.toLowerCase().includes(keyword.toLowerCase());
      const matchesRole = !role || user.role === role;
      const matchesAge = user.age >= minAge;
      const matchesStatus = user.status === status;

      return matchesKeyword && matchesRole && matchesAge && matchesStatus;
    });

    // フィルタリング結果をページング
    const startIdx = (page - 1) * pageSize;
    const endIdx = startIdx + pageSize;
    const paginatedResults = filtered.slice(startIdx, endIdx);

    return {
      items: paginatedResults,
      totalCount: filtered.length,
      pageCount: Math.ceil(filtered.length / pageSize),
      currentPage: page
    };
  }
}

// 実装例
const users = [
  { id: 1, name: 'Alice Johnson', age: 28, role: 'admin', status: 'active' },
  { id: 2, name: 'Bob Smith', age: 35, role: 'user', status: 'active' },
  { id: 3, name: 'Charlie Brown', age: 42, role: 'user', status: 'inactive' },
  // ... more users
];

const searcher = new AdvancedUserSearch(users);
const results = searcher.search({
  keyword: 'john',
  role: 'admin',
  minAge: 25,
  page: 1,
  pageSize: 5
});
console.log(results);

5. 注意点と落とし穴

5-1. 浅いコピーの特性

sliceは浅いコピー(shallow copy)を作成します。つまり、配列の要素がオブジェクトや配列の場合、参照がコピーされるだけで、ネストされたオブジェクトは共有されます。

// 注意例:オブジェクトの参照問題
const originalUsers = [
  { id: 1, name: 'Alice', roles: ['admin', 'user'] },
  { id: 2, name: 'Bob', roles: ['user'] }
];

const copiedUsers = originalUsers.slice();

// 新しい配列には影響しない
copiedUsers.pop();
console.log(originalUsers.length); // 2(変わらない)

// しかし、オブジェクトのプロパティ変更は影響する
copiedUsers[0].name = 'Modified';
console.log(originalUsers[0].name); // "Modified"(変わってしまう!)

// ネストされた配列も共有される
copiedUsers[0].roles.push('superuser');
console.log(originalUsers[0].roles); // ["admin", "user", "superuser"](変わってしまう!)

// 深いコピーが必要な場合はJSON方式またはstructuredCloneを使用
const deepCopy = JSON.parse(JSON.stringify(originalUsers));
// または
const deepCopyModern = structuredClone(originalUsers);

5-2. パフォーマンスの考慮

非常に大きな配列に対してsliceを繰り返し実行する場合、パフォーマンスに影響する可能性があります。

// 注意:大規模データでの性能測定
const largeArray = Array.from({ length: 1000000 }, (_, i) => i);

console.time('slice_operation');
for (let i = 0; i < 1000; i++) {
  const sliced = largeArray.slice(0, 10);
}
console.timeEnd('slice_operation');

// 代替案:必要な場合はイテレータパターンを検討
class LazyPaginator {
  constructor(data, pageSize) {
    this.data = data;
    this.pageSize = pageSize;
  }

  *pages() {
    for (let i = 0; i < this.data.length; i += this.pageSize) {
      yield this.data.slice(i, i + this.pageSize);
    }
  }
}

const paginator = new LazyPaginator(largeArray, 100);
for (const page of paginator.pages()) {
  // 必要な時だけページを処理
  console.log(page.length);
}

5-3. インデックス指定時の誤り

// よくある間違い
const arr = [1, 2, 3, 4, 5];

// 間違い:endIndexが含まれると思っている
const wrong = arr.slice(1, 3);
console.log(wrong); // [2, 3](4は含まれない)

// 正しい理解:startは含まれ、endは含まれない
const correct = arr.slice(1, 3); // インデックス1と2のみを取得

// インデックスが負の値の場合の動作
console.log(arr.slice(-2)); // [4, 5](最後の2個)
console.log(arr.slice(-3, -1)); // [3, 4](最後から3番目から最後から2番目まで)

6. まとめ

JavaScriptのsliceメソッドは単純に見えますが、実務プロジェクトではページネーション、データ抽出、配列の複製など、非常に頻繁に使用される重要なメソッドです。本記事で紹介したパターンを押さえることで、以下のメリットが得られます:

  • 副作用のないコード:元の配列を変更しないため、予期しないバグを防ぎやすい
  • 読みやすさの向上:意図が明確に表現される
  • パフォーマンス:多くの場合、効率的にメモリを利用できる
  • 他言語への応用:同じ概念がPythonなど他言語でも使える

ただし、浅いコピーの特性やパフォーマンス上の考慮点を理解した上で使用することが重要です。TypeScriptを使用する場合は、型安全性を確保しながらsliceを活用することで、より堅牢なアプリケーション開発が可能になります。

実務では単一の技術だけでなく、filtermapなどのメソッドと組み合わせて使うことが多くあります。各メソッドの特性をしっかり理解し、適切に組み合わせることで、保守性の高い実用的なコードを書くことができるようになります。

タイトルとURLをコピーしました