MochiuWiki : SUSE, EC, PCB
検索
個人用ツール
ログイン
Toggle dark mode
名前空間
ページ
議論
表示
閲覧
ソースを閲覧
履歴を表示
JavaScriptの基礎 - クロージャのソースを表示
提供: MochiuWiki : SUSE, EC, PCB
←
JavaScriptの基礎 - クロージャ
あなたには「このページの編集」を行う権限がありません。理由は以下の通りです:
この操作は、次のグループのいずれかに属する利用者のみが実行できます:
管理者
、new-group。
このページのソースの閲覧やコピーができます。
== 概要 == クロージャ (Closure) とは、関数がその定義時のレキシカル環境 (Lexical Environment) を保持する仕組みのことである。<br> JavaScriptでは、内部関数が外部関数の変数を参照する場合、外部関数の実行が終了した後もその変数にアクセスし続けることができる。<br> <br> この動作の根拠となるのがレキシカルスコープ (Lexical Scope) である。<br> JavaScriptはレキシカルスコープを採用しており、関数のスコープは定義された場所によって決定される。<br> 呼び出し場所は関係しない。<br> <br> クロージャは、カウンタの状態管理、プライベート変数のエミュレート、関数ファクトリ、メモ化 (Memoization) 等、多様な実用パターンで活用される。<br> モダンJavaScript開発において不可欠な概念であり、ReactのHooks (useState、useEffect等) もクロージャに基づいて実装されている。<br> <br> 一方で、stale closure (古いクロージャ) と呼ばれる問題も存在する。<br> クロージャが古い状態のスナップショットを参照し続け、期待した値と異なる値を扱ってしまう現象である。<br> Reactのフック内で発生しやすく、特に <code>useEffect</code> 内での <code>setInterval</code> や <code>setTimeout</code> との組み合わせで注意が必要である。<br> <br> <u>クロージャはJavaScriptの関数とレキシカルスコープを深く理解することで初めて使いこなせる機能であり、設計力に直結する重要な概念である。</u><br> <br><br> == レキシカルスコープ == ==== スコープチェーン ==== JavaScriptで変数を参照する時、エンジンは現在のスコープから外側に向かって順番に変数を探索する。<br> この探索経路をスコープチェーン (Scope Chain) と呼ぶ。<br> <br> 探索の順序は以下の通りである。<br> # ローカルスコープ #: 現在の関数内で宣言された変数を最初に探す。 # 外側のスコープ #: ローカルスコープで見つからない場合、1つ外側の関数スコープへと遡る。 # グローバルスコープ #: 最終的にグローバルスコープまで遡り、見つからない場合は <u>ReferenceError</u> となる。 <br> スコープチェーンの動作例を以下に示す。<br> <br> <syntaxhighlight lang="javascript"> const globalVar = "global"; function outer() { const outerVar = "outer"; function inner() { const innerVar = "inner"; console.log(innerVar); // "inner" : ローカルスコープ console.log(outerVar); // "outer" : 外側のスコープ console.log(globalVar); // "global" : グローバルスコープ } inner(); } outer(); </syntaxhighlight> <br> JavaScriptには、3種類のスコープが存在する。<br> <br> <center> {| class="wikitable" |+ JavaScript のスコープ ! スコープ !! 説明 |- | グローバルスコープ || スクリプト全体からアクセスできる。<br><code>var</code> のトップレベル宣言 や <code>let</code> / <code>const</code> のトップレベル宣言が対象 |- | 関数スコープ || 関数の <code>{}</code> 内で定義された変数が対象<br><code>var</code> はこのスコープを使用する。 |- | ブロックスコープ || <code>if</code>、<code>for</code>、<code>while</code> 等の <code>{}</code> 内で定義された変数が対象<br><code>let</code> と <code>const</code> はこのスコープを使用する。 |} </center> <br> ==== 静的スコープ と 動的スコープ ==== スコープの決定方式には、静的スコープと動的スコープの2種類がある。<br> JavaScriptは静的スコープ (レキシカルスコープ) を採用している。<br> <br> <center> {| class="wikitable" |+ スコープの決定方式 ! 方式 !! 説明 |- | 静的スコープ (レキシカルスコープ) || 変数のスコープはソースコードを定義した場所によって決定される。<br>JavaScriptはこの方式を採用している。 |- | 動的スコープ || 変数のスコープは関数が呼び出された場所によって決定される。<br>JavaScriptは採用していない。 |} </center> <br> 静的スコープの動作を実証する例を以下に示す。<br> <br> <syntaxhighlight lang="javascript"> const x = "global"; function showX() { console.log(x); // 定義時のスコープ (グローバル) のxを参照 } function callShowX() { const x = "local"; // この変数は、showX() のスコープに影響しない showX(); // "global"が出力される } callShowX(); // "global" </syntaxhighlight> <br> <u>showX()</u> 関数は <u>callShowX()</u> の内部から呼び出されているが、<u>showX()</u> のスコープは定義場所 (グローバルスコープ) によって決定される。<br> そのため、<u>callShowX()</u> 内の <u>const x = "local"</u> は参照されず、グローバルの <u>x</u> が出力される。<br> <br><br> == クロージャの仕組み == ==== クロージャとは ==== クロージャとは、内部関数が外部関数の変数を参照しており、外部関数の実行が終了した後もその変数にアクセスできる状態のことを指す。<br> <br> 以下に基本的なクロージャの例を示す。<br> <br> <u>makeGreeter("Hello")</u> の実行が終了した後も、返された内部関数は変数 <u>greeting</u> に "Hello" の値でアクセスし続けることができる。<br> これがクロージャである。<br> <br> <syntaxhighlight lang="javascript"> function makeGreeter(greeting) { // greeting は makeGreeter のローカル変数 return function(name) { // 内部関数が外部関数の greeting を参照している console.log(greeting + ", " + name); }; } const helloGreeter = makeGreeter("Hello"); const hiGreeter = makeGreeter("Hi"); helloGreeter("Alice"); // "Hello, Alice" helloGreeter("Bob"); // "Hello, Bob" hiGreeter("Carol"); // "Hi, Carol" </syntaxhighlight> <br> ==== クロージャが変数を保持する理由 ==== <u>関数オブジェクトは内部に <code>[[Environment]]</code> という内部スロットを持つ。</u><br> <u>このスロットは、関数が定義された時点のレキシカル環境への参照を保持している。</u><br> <br> 内部関数が外部変数を参照している限り、JavaScriptのガベージコレクタ (GC) はそのレキシカル環境を解放できない。<br> これにより、外部関数の実行が終了しても変数が保持される。<br> <br> <u>また、クロージャはスナップショットではなくリアルタイムの参照であることに注意する。</u><br> <u>外部変数の値が変更された場合、クロージャはその変更を反映した最新の値を参照する。</u><br> <br> <syntaxhighlight lang="javascript"> function makeCounter() { let count = 0; return { increment: function() { count++; }, getCount: function() { return count; } }; } const counter = makeCounter(); counter.increment(); counter.increment(); console.log(counter.getCount()); // 2 : リアルタイム参照のため最新値が返される </syntaxhighlight> <br> <u>increment</u> と <u>getCount</u> はどちらも同じ <u>count</u> 変数への参照を共有している。<br> <u>increment</u> で変更した値が <u>getCount</u> で取得できるのは、クロージャがリアルタイムの参照だからである。<br> <br> ==== クロージャとループ ==== ループ内でクロージャを作成する際、<code>var</code> を使用すると意図しない動作が発生する。<br> <code>var</code> はブロックスコープを持たないため、全てのクロージャが同一の変数を参照してしまう。<br> <br> * 問題のあるコード例 *: <syntaxhighlight lang="javascript"> // var を使用した場合 : 全て同じ変数 i を参照する for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(i); // 3, 3, 3 が出力される (期待値: 0, 1, 2) }, 100); } </syntaxhighlight> *: <br> * この問題を解決する方法として、以下の2種類がある。 *: <syntaxhighlight lang="javascript"> // 解決策1 : let を使用する (推奨) // let はループの各イテレーションで新しいブロックスコープを作成する for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(i); // 0, 1, 2 が正しく出力される }, 100); } // 解決策2 : IIFE (即時実行関数式) を使用する (ES2015以前) // 各イテレーションの i の値を IIFE の引数に渡して新しいスコープを作成する for (var i = 0; i < 3; i++) { (function(j) { setTimeout(function() { console.log(j); // 0, 1, 2 が正しく出力される }, 100); })(i); } </syntaxhighlight> <br> <u>現代的なJavaScript開発では <code>let</code> を使用する解決策が推奨される。</u><br> IIFEによる解決策は、ES2015以前のコードベースで見られることがある。<br> <br><br> == 実用例 == ==== カウンタ ==== クロージャを使用したカウンタは、状態を安全に保持しながら複数のメソッドで操作できる実用的なパターンである。<br> 変数 <u>count</u> は外部から直接アクセスできず、返されたオブジェクトのメソッドを通じてのみ操作できる。<br> <br> <syntaxhighlight lang="javascript"> function createCounter(initialValue = 0) { let count = initialValue; return { increment: function() { count++; return count; }, decrement: function() { count--; return count; }, reset: function() { count = initialValue; return count; }, getCount: function() { return count; } }; } const counter = createCounter(10); console.log(counter.increment()); // 11 console.log(counter.increment()); // 12 console.log(counter.decrement()); // 11 console.log(counter.getCount()); // 11 console.log(counter.reset()); // 10 // countに直接アクセスはできない console.log(counter.count); // undefined </syntaxhighlight> <br> ==== プライベート変数 ==== クロージャを使用することで、外部からのアクセスを制限したプライベート変数を実現できる。<br> オブジェクトのデータを保護し、getter / setterを通じてバリデーションを行うパターンである。<br> <br> <syntaxhighlight lang="javascript"> function createUser(initialName) { let name = initialName; // プライベート変数 let age = 0; // プライベート変数 return { getName: function() { return name; }, setName: function(newName) { if (typeof newName === "string" && newName.length > 0) { name = newName; } else { console.log("無効な名前です"); } }, getAge: function() { return age; }, setAge: function(newAge) { if (typeof newAge === "number" && newAge >= 0) { age = newAge; } else { console.log("無効な年齢です"); } } }; } const user = createUser("Alice"); console.log(user.getName()); // "Alice" user.setName("Bob"); console.log(user.getName()); // "Bob" user.setAge(-5); // "無効な年齢です" user.setAge(30); console.log(user.getAge()); // 30 // プライベート変数には直接アクセスできない console.log(user.name); // undefined </syntaxhighlight> <br> <u>このパターンはモジュールパターン (Module Pattern) とも呼ばれ、クロージャによってデータのカプセル化を実現している。</u><br> <br> ==== 関数ファクトリ ==== 関数ファクトリ (Function Factory) とは、設定値をクロージャで保持し、同じロジックで異なる動作をする関数を生成するパターンである。<br> <br> <syntaxhighlight lang="javascript"> // 乗算関数ファクトリ function createMultiplier(multiplier) { return function(number) { return number * multiplier; }; } const double = createMultiplier(2); const triple = createMultiplier(3); const times10 = createMultiplier(10); console.log(double(5)); // 10 console.log(triple(5)); // 15 console.log(times10(5)); // 50 // 挨拶関数ファクトリ function createGreeter(greeting) { return function(name) { return greeting + ", " + name + "!"; }; } const sayHello = createGreeter("Hello"); const sayGoodbye = createGreeter("Goodbye"); console.log(sayHello("Alice")); // "Hello, Alice!" console.log(sayGoodbye("Bob")); // "Goodbye, Bob!" </syntaxhighlight> <br> <u>関数ファクトリを使用することにより、ソースコードの重複を避けながら柔軟な設定を持つ関数を簡潔に作成できる。</u><br> <br> ==== メモ化 ==== メモ化 (Memoization) は、関数の計算結果をキャッシュし、同じ入力に対して再計算を行わずにキャッシュから値を返す最適化技法である。<br> クロージャを使用してキャッシュオブジェクトを保持する。<br> <br> 以下の例では、<u>cache</u> オブジェクトはクロージャによって保持され、<u>memoizedCalc</u> が呼び出されるたびに参照される。<br> 同じ引数での再計算を防ぐことで、パフォーマンスを改善できる。<br> <br> <syntaxhighlight lang="javascript"> function memoize(fn) { const cache = {}; // クロージャでキャッシュを保持 return function(...args) { const key = JSON.stringify(args); if (key in cache) { console.log("キャッシュから返却: " + key); return cache[key]; } const result = fn(...args); cache[key] = result; return result; }; } // 重い計算を行う関数 function expensiveCalc(n) { console.log("計算中: " + n); return n * n; } const memoizedCalc = memoize(expensiveCalc); console.log(memoizedCalc(5)); // "計算中: 5" -> 25 console.log(memoizedCalc(5)); // "キャッシュから返却: [5]" -> 25 console.log(memoizedCalc(10)); // "計算中: 10" -> 100 console.log(memoizedCalc(10)); // "キャッシュから返却: [10]" -> 100 </syntaxhighlight> <br> 下表に、クロージャを使用した実用パターンの比較を示す。<br> <br> <center> {| class="wikitable" |+ クロージャ実用パターン比較 ! パターン名 !! 用途 !! 特徴 |- | カウンタ || 状態の保持と管理 || プライベート変数をカウント値として複数メソッドで共有 |- | プライベート変数 || データ保護とバリデーション || getter / setterで直接アクセスを制限 |- | 関数ファクトリ || 設定値を持つ関数の生成 || 同じロジックで異なる設定の関数を作成 |- | メモ化 || 計算結果のキャッシュ || 同じ入力に対して計算を繰り返さない |} </center> <br><br> == クロージャの注意点 == ==== メモリへの影響 ==== クロージャは外部関数のレキシカル環境への参照を保持するため、参照が存在する限りガベージコレクタはその環境を解放できない。<br> 大きなオブジェクトや大量のデータへの参照をクロージャが保持している場合、メモリリークの原因となる可能性がある。<br> <br> <syntaxhighlight lang="javascript"> function createHeavyClosure() { const largeArray = new Array(1000000).fill("data"); // 大きなオブジェクト return function() { // largeArray を参照しているため、GC によって解放されない return largeArray[0]; }; } let heavyClosure = createHeavyClosure(); console.log(heavyClosure()); // "data" // 参照を解放することにより、GCがlargeArrayを回収できるようになる heavyClosure = null; </syntaxhighlight> <br> 参照を解放する方法として、クロージャを保持している変数に <code>null</code> を代入することが有効である。<br> これにより、クロージャからのレキシカル環境への参照が切れ、ガベージコレクタが対象のメモリを回収できるようになる。<br> <br> 大きなオブジェクトを扱う場合は、クロージャで保持する必要があるのはその一部のデータだけであることが多い。<br> 必要な値だけを抽出してクロージャに渡すことにより、不要なメモリ保持を防ぐことができる。<br> <br> <syntaxhighlight lang="javascript"> function createEfficientClosure() { const largeArray = new Array(1000000).fill("data"); const neededValue = largeArray[0]; // 必要な値だけ抽出 // largeArray 自体への参照はクロージャに含まれない return function() { return neededValue; }; } </syntaxhighlight> <br> ==== 意図しないクロージャ ==== クロージャは意図せず作成されることがある。<br> 必要のない変数への参照を保持し続けることでメモリを無駄に消費したり、デバッグを困難にする場合がある。<br> <br> <syntaxhighlight lang="javascript"> // 意図しないクロージャの例 function setup() { const largeData = fetchLargeData(); // 大きなデータを取得 const id = largeData.id; // 必要なのは id だけ // largeData 全体がクロージャに捕捉されてしまう return function handler() { process(largeData); }; } // 推奨 : 必要な値だけをクロージャに渡す function setupBetter() { const largeData = fetchLargeData(); const id = largeData.id; // id だけがクロージャに捕捉される return function handler() { process({ id: id }); }; } </syntaxhighlight> <br> <u>クロージャが保持している変数を確認するには、Chrome DevToolsの[Sources]パネルを使用する。</u><br> <u>ブレークポイントを設定した状態で実行すると、右側の[Scope]パネルに[Closure]のセクションが表示されて、クロージャが保持している変数の一覧を確認できる。</u><br> <br><br> == Reactとクロージャ == ==== Hooksとクロージャ ==== ReactのHooks (<code>useState</code>、<code>useEffect</code> 等) はクロージャに基づいて実装されている。<br> 関数コンポーネントが呼び出されるたびに新しいレキシカル環境が生成され、各レンダリング時の状態がクロージャによって管理される。<br> <br> <syntaxhighlight lang="javascript"> import React, { useState, useEffect } from "react"; function Counter() { const [count, setCount] = useState(0); useEffect(function() { // この関数はクロージャであり、レンダリング時の count を参照している console.log("count の値:", count); }, [count]); // count が変更されるたびに実行される return ( <div> <p>Count: {count}</p> <button onClick={function() { setCount(count + 1); }}> インクリメント </button> </div> ); } </syntaxhighlight> <br> <code>useState</code> の <u>state</u> 値は、各レンダリング時のスナップショットとして機能する。<br> <code>useEffect</code> 内のクロージャは、エフェクトが実行された時点のレンダリングの <u>state</u> を参照する。<br> <br> ==== stale closure問題への伏線 ==== stale closure (古いクロージャ) 問題とは、クロージャが古いレンダリング時の状態を参照し続けることで、期待した値と異なる値を参照してしまう現象である。<br> <br> 特に、<code>useEffect</code> 内の <code>setInterval</code> と <code>useState</code> の組み合わせで発生しやすい。<br> <br> <syntaxhighlight lang="javascript"> import React, { useState, useEffect } from "react"; function StaleClosureExample() { const [count, setCount] = useState(0); useEffect(function() { // count の初期値 0 をキャプチャしたクロージャが作成される const interval = setInterval(function() { // ここでの count は常に 0 (初期値) を参照する console.log("count:", count); // 常に 0 が出力される setCount(count + 1); // 常に 0 + 1 = 1 となる }, 1000); return function() { clearInterval(interval); }; }, []); // 依存配列が空のため、count が変わっても再実行されない return <p>Count: {count}</p>; } </syntaxhighlight> <br> この問題への対処として、主に以下の3種類の解決策がある。<br> <br> <center> {| class="wikitable" |+ Stale Closure 問題の解決策 ! 解決策 !! 説明 |- | 解決策1 : 依存配列に count を含める || <code>useEffect</code> の依存配列に <u>[count]</u> を指定することにより、<br>countが変わるたびにエフェクトが再実行され、最新のcountを参照したクロージャが作成される。<br>ただし、インターバルが毎回リセットされるという副作用がある。 |- | 解決策2 : 関数型更新を使用する || <u>setCount(prev => prev + 1)</u> のように関数型更新を使用することにより、最新のstateを受け取れる。<br>stale closureの影響を受けずに状態を更新できる。 |- | 解決策3 : useRef で最新値を保持する || <code>useRef</code> で最新のcountを保持することにより、クロージャが古い値を参照する問題を回避できる。 |} </center> <br> <u>stale closure問題の詳細な解決策、<code>useRef</code> を使用したパターン、<code>useCallback</code> との関係については、Reactのクロージャ問題を扱う専門のページを参照のこと。</u><br> <br><br> == 関連情報 == * [[JavaScriptの基礎 - 関数宣言と関数式]] *: 関数宣言と関数式、デフォルト引数、残余引数 * [[JavaScriptの基礎 - アロー関数]] *: アロー関数の構文、暗黙のreturn、レキシカルthis * [[JavaScriptの基礎 - コールバック関数]] *: コールバック関数、高階関数、タイマ、イベントリスナー <br><br> {{#seo: |title={{PAGENAME}} : Exploring Electronics and SUSE Linux | MochiuWiki |keywords=MochiuWiki,Mochiu,Wiki,Mochiu Wiki,Electric Circuit,Electric,pcb,Mathematics,AVR,TI,STMicro,AVR,ATmega,MSP430,STM,Arduino,Xilinx,FPGA,Verilog,HDL,PinePhone,Pine Phone,Raspberry,Raspberry Pi,C,C++,C#,Qt,Qml,MFC,Shell,Bash,Zsh,Fish,SUSE,SLE,Suse Enterprise,Suse Linux,openSUSE,open SUSE,Leap,Linux,uCLnux,電気回路,電子回路,基板,プリント基板 |description={{PAGENAME}} - 電子回路とSUSE Linuxに関する情報 | This page is {{PAGENAME}} in our wiki about electronic circuits and SUSE Linux |image=/resources/assets/MochiuLogo_Single_Blue.png }} __FORCETOC__ [[カテゴリ:Web]]
JavaScriptの基礎 - クロージャ
に戻る。
案内
メインページ
最近の更新
おまかせ表示
MediaWiki についてのヘルプ
ツール
リンク元
関連ページの更新状況
特別ページ
ページ情報
We ask for
Donations
Collapse