序文
さまざまな関数定義と呼び出し場所によって引き起こされる違いを要約し、再帰とマスターの基本的な使用法を理解し、クロージャー生成の原則を探り、メモリリークの概念を理解します。その他の関連する注意事項:
- JavaScriptの基本(超詳細)
- jsの参照型(組み込みオブジェクト)
- jsのオブジェクト属性-構成可能、書き込み可能など(データ属性とアクセサー属性)
- jsのプロトタイプ、プロトタイプチェーン、継承の概念(詳細で包括的な)
- 高度な機能の使用-再帰-閉鎖
1.関数定義と呼び出しの場所の要約
再帰とクロージャを要約する前に、関数が定義されている場所と呼び出されている場所のさまざまな状況の要約を最初に作成しました。それぞれの状況の主な特徴は次のとおりです。
- 通常の機能の使い方。
- コールバック関数の使用法は、多くの場合、非同期APIの処理結果を操作するために無名関数と組み合わされます。
- この状況の主な特徴は、クロージャーが生成されることです。これについては、以下で詳しく説明します。
- もちろん、返されたクロージャ関数をグローバル変数として使用することは、他の関数内から呼び出すこともできます。
- この種の状況でもクロージャが生成されますが、クロージャの範囲は包含関数の内部に限定されます。包含関数が実行された後、クロージャ関数参照は破棄されます。
- この種の関数は再帰関数とも呼ばれます。無限ループを防ぐために使用するときは、終了条件に注意してください。
// 1、函数在全局作用域定义并调用
function outer1(x) {
console.log(x);
};
outer1(1);
// 2、在函数内部调用另一个外部定义的函数
function outer2(callback) {
let a = 2;
callback && callback(a);
}
// 这种方式常用于操作异步API处理结果
outer2(outer1);
function outer3() {
let a = 3;
// 直接调用跟上面的调用没有本质区别,不易维护
outer1(a);
}
outer3();
// 3、函数内部定义的函数在函数外部调用(产生闭包)
function outer4() {
let a = 4;
// 这里内部函数有没有传参均一样,因为只是定义没有调用
return function insider() {
console.log(a);
};
};
// 如果只是定义没有返回则只是私有方法,外部无法访问
const insider1 = outer4();
insider1();
// 4、内部定义的函数返回后也可以在其他函数内部使用,形同 2
// 5、内部定义的函数在函数内部自己调用(也会产生闭包)
function outer5() {
let a = 5;
// 这里内部函数有没有传参均一样,因为只是定义没有调用
function insider2() {
console.log(a);
};
insider2();
};
// 这里产生的闭包会在outer5执行结束后销毁
outer5();
// 6、外部的函数在自己内部调用自己
function outer6(num) {
return num == 1 ? num : num * outer6(num - 1);
}
console.log(outer6(4));
2.再帰
再帰:関数定義で関数自体を呼び出す方法は再帰です。例として、nの階乗を計算する関数の定義を取り上げます。n!の定義は、nが0または1に等しい場合、n!の値は1であり、nの値が他の値である場合、n!= nx(n-1)!
function fn(num) {
if (num == 1 || num == 0) {
return 1;
} else {
return num * fn(num - 1)
};
};
console.log(fn(4));
実行プロセスを次の図に示します。
再帰の2つの重要な特性:
- チェーン:計算プロセスには規則的で整然とした再帰チェーンがあります。たとえば、nの階乗はnとn-1の階乗の積に等しく、nとn-1の階乗は再帰チェーンを構成します。 。
- 基本ケース:基本的な例。再度再帰する必要のない基本ケースが1つ以上あり、再帰を終了するための基礎となります。たとえば、nが0またはnが1の場合、階乗値は正確な値1であり、他の値との再帰的な関係はありません。
上記の2つの再帰特性は必須ですが、再帰関数を実装する場合、以下の詳細な問題が発生します。
// 1、递归求阶乘
function fn(num) {
return num == 1 ? num : num * fn(num - 1);
};
console.log(fn(4));
var factorial = fn;
console.log(factorial(4));
// 删除fn指向递归函数的指针
fn = null;
// 由于在内层调用时找不到fn函数而报错
console.log(factorial(4)) // ->error
// 2、在非严格模式下可以使用arguments.callee代替递归函数本身
// 'use strict'
function fn(num) {
// arguments.callee指向当前执行的函数
return num == 1 ? num : num * arguments.callee(num - 1);
};
console.log(fn(4));
var factorial = fn;
console.log(factorial(4));
fn = null;
console.log(factorial(4))
// 3、严格模式下不能通过脚本访问callee,采用函数表达式的形式
var factorial = (function fn(num) {
return num == 1 ? num : num * fn(num - 1);
});
console.log(factorial(4));
fn = null;
console.log(factorial(4));
// 即使再次赋值给另一个变量,函数名fn依旧有效
var fn2 = factorial;
factorial = null;
console.log(fn2(4));
例3は理解しにくいようです。私の個人的な理解では、式を使用して関数を宣言すると、fn関数式の値が階乗変数に割り当てられます。これは式であるため、最終値を返す必要があります。したがって、関数fnへの参照を返す必要があります。そして、これは最終値ではなく、内側のfnも参照値に置き換えられます。したがって、外部変数名がどのように変更されても、内部参照値は変更されません。例2は異なり、グローバルスコープでfnという名前の関数オブジェクトを定義するだけです(式ステートメントではなく、宣言ステートメントのみ)。内部のfnは依然として単なる識別子であり、fnへの参照を返しません。
上記は再帰の理解です。再帰には階乗の計算以外にも多くの用途があります。たとえば、フィボナッチ数列を見つけて、ハノイの塔の問題を解決します。
フィボナッチ数列:シーケンスの第1項と第2項の値は1であり、他の項の値は最初の2つの項目の合計です。n番目の項の値を見つけます。
var fact = function fn(n) {
return n == 1 || n == 2 ? 1 : fn(n - 1) + fn(n - 2);
};
console.log(fact(8)); // ->21 (1, 1, 2, 3, 5, 8, 13, 21)
ハノイの塔の問題:3本の柱ABCがあり、柱Aは下から上に順番にn個のディスクで積み重ねられています。要件は、ディスクを1つずつ移動することであり、小さなディスクでディスクを拡大することはできません。最後に、すべてのディスクが同じ順序で別の柱に配置されます。
var hanoi = function fn(disc, src, aux, dst) {
if (disc > 0) {
fn(disc - 1, src, dst, aux);
console.log(disc + ':' + src + '-->' + dst);
fn(disc - 1, aux, src, dst)
}
}
hanoi(3, 'A', 'B', 'C')
3.閉鎖
3.1閉鎖の概要
クロージャは、別の関数のスコープ内の変数にアクセスできる関数です。たとえば、上記の関数定義と呼び出し場所の概要の簡単な例:
function outer4() {
let a = 4;
return function insider() {
let b = 'insider';
console.log(a);
};
};
// 如果只是定义没有返回则只是私有方法,外部无法访问
const insider1 = outer4();
insider1();
上記のコードのinsider()関数は、外部関数outer4()の変数aにアクセスします。この内部関数が返され、outer()内で呼び出されない場合でも、変数aにアクセスできます。クロージャを完全に理解する前に、関数が作成されて呼び出されたときに何が起こるかを理解する必要があります。
3.2クロージャー生成の原則
関数オブジェクトを作成すると、そのオブジェクトの内部[[Scopes]]プロパティが作成されます。この属性は、関数の定義時に実行環境で直接アクセスできるスコープチェーンを保持します。この時点では関数定義のみが実行されないため、この属性が指すスコープチェーンのフロントエンドは、関数の変数オブジェクトポインタではないことに注意してください。ここで概念を明確にする必要があります。スコープチェーンは基本的に変数オブジェクトへのポインタのリストであり、変数オブジェクトを参照するだけで、実際には含まれていません。以下に示すように:
ときに関数が呼び出され、実行環境と対応するスコープチェーンが作成されます。次に、引数の値と他の名前付きパラメーターを使用して、関数のアクティブオブジェクトを初期化します。スコープチェーンを構築するとき、関数の[[Scopes]]属性のオブジェクトが(浅く)コピーされ、関数用に作成されたアクティブオブジェクト(変数オブジェクト)が実行環境のスコープのフロントエンドにプッシュされます。
この図は、関数outer4()およびinsider()の定義および呼び出し中の、スコープチェーンとそれぞれの[[Scopes]]プロパティの関係とステータスを示しています。
グローバルスコープで定義されているすべての関数の[[スコープ]]ポインタリストには、Windowオブジェクトへのポインタが1つだけ格納されます。ただし、outer4()で定義されている関数insider()の[[Scopes]]ポインターリストには、Windowオブジェクトポインターに加えて、その外部関数outer4()のアクティブオブジェクトへのポインターもあります。実行後、関数実行環境を終了した後、insider()はグローバルスコープに戻されます(関数のメモリスペースは再利用されません。つまり、返された関数は破棄できません。すでにグローバル変数として存在します。)。次に、insider()の[[Scopes]]リストプロパティの最初のポインタ(これが最初で、Windowが2番目)はouter4()アクティブオブジェクト参照を保持しているため、このアクティブオブジェクトにもinsider()が付随します。常にメモリに保存されます。この関数を呼び出すと、[[Scopes]]プロパティを使用して、上記のメソッドに従ってスコープチェーンが構築されるため、outer()内の変数にもアクセスできます。これがクロージャーの原則です。
外側の()関数によって返されるインサイダー()関数は、外側の()関数が実行されるたびに同じ機能オブジェクトではない別のオブジェクト(への[スコープ]]プロパティの最初のポインタも指すよう原理安全なコンストラクター)。したがって、形成されたスコープチェーン内の変数の値は互いに影響しません。また、クロージャーは、含まれている関数内の変数の最終値(外部関数の終了後の値)のみを取得できることに注意してください。
function outer4() {
let a = 'insider' + arguments[0];
console.dir(outer4);
function insider() {
console.log(a);
};
return insider;
};
// 返回两个不同的 insider 函数
var insider1 = outer4(1);
var insider2 = outer4(2);
insider1(); // ->'insider1'
insider2(); // ->'insider2'
3.3簡単なアプリケーション
クロージャには主にスコープを拡張する機能があります。2つの簡単なアプリケーションを次に示します。他のアプリケーションシナリオは将来満たされ、その後追加されます。
<body>
<ul class="nav">
<li>第 0 个小li</li>
<li>第 1 个小li</li>
<li>第 2 个小li</li>
<li>第 3 个小li</li>
</ul>
<script>
var lis = document.querySelectorAll('li')
// 错误示范:i此时是全局变量,点击事件外异步任务。点击时i的值已经为4
for (var i = 0; i < lis.length; i++) {
// 绑定点击事件输出i
lis[i].onclick = function() {
console.log('点击元素li的索引:' + i);
}
};
// 解决方案: 利用闭包的方式得到当前小li 的索引号
for (var i = 0; i < lis.length; i++) {
// 利用for循环创建了4个立即执行函数
// 立即执行函数也成为小闭包因为立即执行函数里面的任何一个函数都可以使用它的i这变量
(function(i) {
// console.log(i);
lis[i].onclick = function() {
console.log(i);
}
})(i);
}
// 另一个应用:3秒钟之后,打印所有li元素的内容
for (var i = 0; i < lis.length; i++) {
(function(i) {
setTimeout(function() {
console.log(lis[i].innerHTML);
}, 3000)
})(i);
}
</script>
</body>
もちろん、上記の方法だけが問題の解決策ではありません。たとえば、クリックイベントをバインドする前にカスタム属性インデックスをliに追加したり、ES6でletキーワードを直接使用してブロックレベルのスコープ変数を宣言したりできます(上記の即時実行関数には、実際には模倣ブロック(レベルスコープの役割)などがあります。
3.4メモリリークの問題
メモリリークとは、何らかの理由でプログラムが特定のメモリを占有(使用)する必要がなくなったときに、このメモリがオペレーティングシステムまたは空きメモリプールに戻されない現象を指します。メモリリークを引き起こす多くの状況があり、クロージャによって引き起こされたメモリリークのみがここに記録されます。たとえば、イベント処理コールバックは、DOMオブジェクトとスクリプト内のオブジェクト間の双方向参照につながります。これはリークの一般的な原因です。
function fn1() {
var submitBtn = document.getElementById('submitBtn');
submitBtn.onclick = function() {
console.log(submitBtn.id);
};
}
fn1();
// 改进
function fn2() {
var submitBtn = document.getElementById('submitBtn');
var id = submitBtn.id;
submitBtn.onclick = function() {
console.log(id);
};
submitBtn = null;
}
fn2();
上記の例では、クロージャが形成されます。最初のクロージャにはDOMオブジェクトsubmitBtnが含まれていますが、クリックイベントのコールバック関数はオブジェクトのidプロパティにのみアクセスします。したがって、メモリリークを防ぐために、fn2()はオブジェクトのid属性を保持するid変数を宣言します。もちろん、submitBtn変数を最終的にnullに設定して、DOMオブジェクトへの参照を手動で削除し、参照の数をスムーズに減らし、それによって占有されているメモリが正常に回復されるようにする必要があります。