JavaScriptでの関数クロージャの詳細な説明

1.可変スコープ

クロージャを理解するには、最初に変数のスコープを理解する必要があります。ECMAScript5標準には、グローバルスコープと関数スコープの2つのスコープがあります
2つの間の呼び出し関係は次のとおりです。

  • グローバル変数は関数内で直接読み取ることができます。
  • 通常の状況では、関数内で宣言された変数を関数外で読み取ることはできません。
let num = 1;

function test() {
    
    
  let n = 2;
  console.log(num); // 1
}

test();  
console.log(n); // ReferenceError: n is not defined

実際の開発では、さまざまな理由から、関数内でローカル変数を取得する必要があります。

JavaScript言語では、親オブジェクトのすべての変数が子オブジェクトに表示され、その逆も同様であると規定されています。つまり、「チェーンスコープ」構造(チェーンスコープ)です。
これに基づいて、目的関数で別の関数を定義でき、この子関数はその親関数の内部変数に正常にアクセスできます。

function parent() {
    
    
  let n = 1;
  function child() {
    
    
  console.log(n); // 1
  }
}

子関数は親関数のローカル変数を取得できるため、親関数は直接子関数に戻り、グローバルスコープ内の関数の内部変数にアクセスする目的は達成されません。

function parent() {
    
    
  let n = 1;
  function child() {
    
    
  console.log(n);  // 1
  };
  return child;
}

let f1 = parent();
f1();

2.クロージャーの概念と特徴

上記の例は、クロージャを作成する最も簡単な方法です。関数の子はクロージャであるため、クロージャは「関数内で定義された関数」です。本質的に、クロージャは関数の内側と外側を接続するブリッジです。

クロージャー自体にも、次の重要な特性があります。

  • 関数内にネストされた関数。
  • クロージャー内では、外部関数の内部パラメーター、変数、またはメソッドにアクセスできます。
  • クロージャで使用されるパラメータと変数は常にメモリに格納され、関数呼び出しの終了後にガベージコレクションメカニズムによってリサイクルされることはありません。
  • 同じクロージャメカニズムにより、互いに独立していて互いに影響を与えない複数のクロージャ関数インスタンスを作成できます。

3.クロージャを書く古典的な方法

3.1戻り値として機能する

上記の例は無名関数としてさらに簡略化できます。
外部関数の内部変数numは無名関数を介してアクセスされ、外部関数は無名関数を返し、匿名関数は引き続きnum変数を返します。

function closure1(){
    
    
  let num = 1;
  return function(){
    
    
    return num
  }
}

let fn1 = closure1();
console.log(fn1()); // 1

このようにして、変数fn1をグローバルスコープで宣言してnum変数を継承できるため、グローバルスコープ内の関数内のローカル変数にアクセスする目的が達成されます。

3.1.1変数の保存
クロージャーは関数内のローカル変数を読み取ることができますが、これらの変数を常にメモリに保持することもでき、関数呼び出しの終了後にガベージコレクションメカニズムによってリサイクルされることはありません。
たとえば、次の例:

function closure2(){
    
    
  let num = 2;
  return function(){
    
    
    let n = 0;
    console.log(n++,num++);
  }
}

let fn2 = closure2();
fn2();  // 0 2
fn2();  // 0 3

関数インスタンスfn2()を2回実行すると、結果がわずかに異なることがわかります。

  • n ++の出力は2回同じです。
    変数nは無名関数の内部変数です。無名関数が呼び出された後、そのメモリスペースは通常どおり解放されます。つまり、ガベージコレクションメカニズムによってリサイクルされます。

  • num ++の2つの出力には一貫性がありません。
    無名関数は、その外部関数のローカル変数numを参照します。無名関数の呼び出しが終了しても、この依存関係は存在するため、変数numを破棄することはできません。メモリに保存されている無名関数は、次回無名関数が呼び出されたときに、最後の呼び出しの結果を引き続き使用します。

クロージャのこの機能を使用すると、単純なデータキャッシュを実行することが実際に可能です。
ただし、クロージャを悪用しないでください。これにより、メモリ消費量が簡単に増加し、メモリリークやWebページのパフォーマンスの問題が発生する可能性があります。

3.1.2複数のクロージャ関数は互いに独立しています

同じクロージャメカニズムにより、互いに独立していて互いに影響を与えない複数のクロージャ関数インスタンスを作成できます。
たとえば、次の簡単な例:

function fn(num){
    
    
  return function(){
    
    
    return num++
  }
}

クロージャ関数の3つのインスタンスを別々に宣言し、異なるパラメーターを渡します。次に、1、2、3回実行します。

function fn(num){
    
    
  return function(){
    
    
    return num++
  }
}

let f1 = fn(10);
let f2 = fn(20);
let f3 = fn(30);

console.log(f1())  // 10
console.log(f2())  // 20
console.log(f2())  // 21
console.log(f3())  // 30
console.log(f3())  // 31
console.log(f3())  // 32

f1()、f2()、f3()の最初の実行は10 20 30を順次出力し、最後の実行の結果にも複数の実行が累積され、相互に影響がないことがわかります。 。

3.2即時実行関数(IIFE)

前のメソッドでは、関数は戻り値としてのみ返され、特定の関数呼び出しは別の場所に書き込まれます。では、外部関数にクロージャーの呼び出し結果を直接返すようにできますか?
もちろん、その答えは「はい」です即時実行関数(IIFE)の表現を使用してください
次に、最初に即時実行関数(IIFE)とは何かを理解しましょう。JavaScriptで関数
を呼び出す最も一般的な方法は、関数名の後に括弧()を付けることです。関数を定義した直後に関数を呼び出す必要がある場合があります。ただし、関数定義の直後に括弧を追加することはできません。これにより、構文エラーが発生するためです。

// 提示语法错误
function funcName(){
    
    }();

エラーの理由は、functionキーワードをステートメントまたは式として使用できるためです。

// 语句
function f() {
    
    }

// 表达式
var f = function f() {
    
    }

式として使用する場合、関数は定義の後に括弧を付けて直接呼び出すことができます。

var f = function f(){
    
     return 1}();
console.log(f) // 1

構文解析のあいまいさを回避するために、JavaScriptは、functionキーワードが行の先頭にある場合、それを文として解釈することを規定しています。したがって、functionキーワードを使用して関数を宣言し、すぐに呼び出せるようにする場合は、関数を行の先頭に直接表示しないようにし、エンジンに式として認識させる必要があります。
これに対処する最も簡単な方法は、括弧内に配置することです。

(function(){
    
     /* code */ }());
// 或者
(function(){
    
     /* code */ })();

これは、「即時呼び出し関数式」(即時呼び出し関数式)、つまり、IIFEと呼ばれる即時実行関数と呼ばれます。

3.2.1タイマーsetTimeoutの古典的なループ出力の問題関数
の即時実行を理解した後、例を簡単に見てみましょう。forループを使用して1〜5を順番に出力します。それで、それが次のコードである場合、その操作の結果は何ですか?

for (var i = 1; i <= 5; i++) {
    
    
  setTimeout( function timer() {
    
    
    console.log(i); // 6 6 6 6 6
  }, 1000 );
} 

結果は56秒でなければなりません。その理由は、forループが同期タスクに属し、setTimeoutタイマーが非同期タスクのマクロタスクカテゴリに属しているためです。JavaScriptエンジンは、最初に同期されたメインスレッドコードを実行し、次にマクロタスクを実行します。

したがって、setTimeoutタイマーを実行する前に、forループは終了しており、この時点でループ変数i = 6です。次に、setTimeoutタイマーがループ内で5回作成され、すべての実行が完了した後に56が出力されます。

しかし、私たちの目的は1〜5を出力することであり、これは明らかに要件を満たしていません。即時実行関数(IIFE)の書き込みメソッドを正式に紹介する前に、別のメソッドについて説明します。ループ変数iはletキーワードで宣言されます。

for (let i = 1; i <= 5; i++) {
    
    
  setTimeout( function timer() {
    
    
    console.log(i); // 1 2 3 4 5
  }, 1000 );
}

なぜletステートメントに置き換えることができるのですか?1〜5ループ出力を実現するための必須要件は、各ループ中にループ変数の値記憶することであるためです。
そして、ステートメントで十分です。ポータル:var、let、constの機能とJavaScriptの違いの詳細な説明

即時実行関数(IIFE)の記述を見てみましょう。

for (var i = 1; i <= 5; i++) {
    
    
  (function(i){
    
    
    setTimeout( function timer() {
    
    
      console.log(i); // 1 2 3 4 5
    }, 1000 );
  })(i);
}

setTimeoutタイマー関数は、外部の無名関数でラップされてクロージャを形成し、次に即時実行関数(IIFE)が使用されます。外部の無名関数を括弧でラップし続け、括弧に従って呼び出し、すべての2番目のループ変数はパラメーターとして渡されます。
このように、各ループの結果はクロージャの呼び出し結果です:iの値を出力します;クロージャ自体の特性の1つに従って:変数またはパラメータを保存でき、すべての条件が満たされ、1〜5正しく出力されます。

もっと言えば、現在の出力フォーマットは、1秒後に1〜5を同時に出力することです。それでは、これらの5つの数値の1つを毎秒出力したいですか?

for (var i = 1; i <= 5; i++) {
    
    
  (function(i){
    
    
    setTimeout( function timer() {
    
    
      console.log(i);
    }, i*1000 );
  })(i);
}

各setTimeoutタイマーの2番目のパラメーターを制御できます。間隔期間はループ変数iで乗算されます。
次のような効果があります
ここに画像の説明を挿入します
3.2.2が
正式なパラメータなどの機能を必要とするさまざまなAPI:APIの正式なパラメータ即時実行機能(生命維持)と組み合わせる閉鎖のメカニズムは別の非常に重要な用途を持っているような機能に渡します。
例として、配列のsort()メソッドを取り上げます。Array.prototype.sort()メソッドは、コンパレータ関数の受け渡しをサポートしており、並べ替えルールをカスタマイズできます。コンパレータ関数には戻り値が必要です。数値型を返すことをお勧めします。

たとえば、次の配列シナリオ:mySort()メソッドを記述できることを願っています。指定された属性値に従って配列要素を降順で並べ替えることができます。
mySort()メソッドには、ソートする配列arrと指定されたプロパティ値プロパティの2つの仮パラメーターが必ず必要です。
さらに、使用するAPIは引き続きsort()メソッドである必要があります。ここでは、コンパレータ関数を直接渡すことはできませんが、閉じたIIFEメソッドを使用して記述します。
プロパティ値プロパティはパラメータとして外部の無名関数に渡されます。 、その後、無名関数は内部的に戻ります。sort()メソッドに必要な最後のコンパレータ関数。

var arr = [
  {
    
    name:"code",age:19,grade:92},
  {
    
    name:"zevi",age:12,grade:94},
  {
    
    name:"jego",age:15,grade:95},
];

function mySort(arr,property){
    
    
  arr.sort((function(prop){
    
    
    return function(a,b){
    
    
       return a[prop] > b[prop] ? -1 : a[prop] < b[prop] ? 1 : 0;
    }
  })(property));
};


mySort(arr,"grade");
console.log(arr); 
/*
[
  {name:"jego",age:15,grade:95},
  {name:"zevi",age:12,grade:94},
  {name:"code",age:19,grade:92},
]
*/

3.3カプセル化されたオブジェクトのプライベートプロパティとプライベートメソッド

クロージャーは、オブジェクトをカプセル化するため、特にオブジェクトのプライベートプロパティとプライベートメソッドをカプセル化するためにも使用でき
ますパブリックプロパティ名、プライベートプロパティ_age、および2つのプライベートメソッドを持つオブジェクトPersonをカプセル化します。
プライベート属性_ageに直接アクセスして変更することはできません。その内部クロージャ、getAgeおよびsetAgeを呼び出す必要があります。

function Person(name) {
    
    
  var _age;
  function setAge(n) {
    
    
    _age = n;
  }
  function getAge() {
    
    
    return _age;
  }

  return {
    
    
    name: name,
    getAge: getAge,
    setAge: setAge
  };
}


var p1 = Person('zevin');
p1.setAge(22);
console.log(p1.getAge()); // 22

4.クロージャーを使用することの長所と短所

4.1利点

4.1.1カプセル化を実現し、関数内の変数の安全性を保護します。
クロージャを使用すると、変数をメモリに保存でき、システムのガベージコレクションメカニズムによって破壊されないため、変数を保護する役割を果たします。

function closure2(){
    
    
  let num = 1;
  return function(){
    
    
    console.log(num++)
  }
}

let fn2 = closure2();
fn2();  // 1
fn2();  // 2

4.1.2グローバル変数の汚染を
回避する不必要な名前の競合や呼び出しの混乱を防ぐために、開発ではグローバル変数の使用を可能な限り回避する必要があります。

// 报错
var num = 1;
function test(num){
    
    
  console.log(num)
}

test();
let num = test(4);
console.log(num);

このとき、関数内で変数を宣言し、クロージャーメカニズムを使用することを選択できます。
これにより、変数の通常の呼び出しが保証されるだけでなく、グローバル変数の汚染も回避されます。

function test(){
    
    
  let num = 1;
  return function(){
    
    
    return num
  }
}

let fn = test();
console.log(fn());

4.2デメリット

4.2.1メモリ消費とメモリリーク
外部関数が実行されるたびに、新しいクロージャが生成され、このクロージャは外部関数の内部変数を保持するため、メモリ消費が高くなります。
解決策:クロージャを乱用しないでください。
同時に、クロージャで参照されている内部変数は保存され、解放できません。これにより、メモリリークの問題も発生します。
解決:

window.onload = function(){
    
    
  var userInfor = document.getElementById('user');
  var id = userInfor.id;
  oDiv.onclick = function(){
    
    
    alert(id);
  }
  userInfor = null;
}

内部クロージャで変数userInforを使用する前に、別の変数IDを使用してそれを継承し、使用後に変数userInforを手動でnullに割り当てます。

おすすめ

転載: blog.csdn.net/ZYS10000/article/details/113834843