ブラウザがクロージャをどのように認識するかについて詳しく説明します

ブラウザがクロージャをどのように認識するかについて詳しく説明します

序文

クロージャは、JavaScript を理解する上で大きな難題です。インターネット上にはクロージャに関する記事がたくさんありますが、読んだ後に完全に理解できる記事はほとんどありません。その理由は、閉鎖には一連の知識ポイントが関係しているからだと思います。この一連の知識ポイントを徹底的に理解し、概念の閉ループを達成することによってのみ、それを真に理解することができます。今日は、メモリ割り当てとリサイクルという別の観点からクロージャを理解し、これまで見てきたクロージャの知識を本当に理解する手助けをしたいと考えています。同時に、この記事がクロージャについて読む最後の記事になることも願っています。

この記事の写真を見るときは、矢印の方向に注意してください。ルート オブジェクト ウィンドウがメモリ ガベージを走査するために依存している原則であるため、ウィンドウから始まる矢印に沿って見つかるものはすべてメモリ ガベージではなく、リサイクルされません。見つからないオブジェクトのみがメモリ ガベージとなり、適切なタイミングで GC によってリサイクルされます。

クロージャの紹介

関数がネストされている場合、内部関数は外部関数スコープ内の変数を参照し、内部関数はグローバル環境内の変数によって参照されるため、クロージャが形成されます。

クロージャは本質的に関数スコープの副産物です。

クロージャに関して特に注意する必要があることの 1 つは、関数内で定義されたすべての関数が同じクロージャ オブジェクトを共有することです。それはどういう意味ですか?次のコードを見てください。

var a
関数b() {
  var c = 新しい文字列('1')
  var d = 新しい文字列('2')
  関数e() {
    コンソール.log(c)
  }
  関数f(){
    コンソールログ(d)
  }
  戻り値 f
}
a = b()

上記のコードでは、f は変数 d を参照し、f は外部変数 a によって参照されるため、クロージャが形成され、変数 d がメモリ内に残ります。考えてみましょう。変数 c はどうでしょうか? c は使用していないようですので、メモリ内には残らないはずです。さらに、c もメモリ内に残るという事実もあります。上記のコードによって形成されるクロージャには、c と d の 2 つのメンバーが含まれます。この現象は関数内クロージャ共有と呼ばれます。

なぜこの機能に特別な注意を払う必要があるのでしょうか?この機能のため、注意しないとメモリリークを引き起こすコードを簡単に記述できてしまいます。
クロージャの概念についてはこれまでたくさん説明してきましたが、クロージャを本当に理解したいのであれば、まだいくつかの知識ポイントを理解する必要があります。

  • 関数スコープチェーン
  • 実行コンテキスト
  • 変数オブジェクト、アクティビティオブジェクト

大まかな理解を得るために、これらのコンテンツを Google または Baidu で検索することができます。次回はブラウザの観点からクロージャを理解する方法についてお話ししますので、ここでは詳しく説明しません。

メモリのゴミを識別する方法

最近のブラウザのガベージコレクションのプロセスは比較的複雑です。詳細なプロセスは自分で Google で検索できます。ここでは、メモリガベージを判別する方法についてのみ説明します。一般的に言えば、ルート オブジェクトから開始して参照をたどって見つかるものは、リサイクルできないことがわかります。参照をたどっても見つからないオブジェクトはガベージとみなされ、次のガベージ コレクション ノードでリサイクルされます。ゴミを探すことは、手がかりを追うプロセスとして理解することができます。

クロージャのメモリ表現

最も単純なコードから始めて、グローバル変数の定義を見てみましょう。

var a = new String('小歌')

このようなコードはメモリ内で次のように表現されます。

グローバル環境では、変数 a が定義され、文字列が割り当てられます。矢印は参照を示します。

別の関数を定義しましょう:

var a = new String('小歌')
関数を教える(){
  var b = new String('小谷')
}

メモリ構造は次のとおりです。

どれも簡単に理解できます。注意して見ると、関数オブジェクトteachに[[scopes]]という属性があることに気がつくでしょう。これは何でしょうか?この属性は関数の作成後に表示されるのはなぜですか?これを質問していただいて嬉しいです。これはクロージャを理解するための重要なポイントです。

覚えておいてください: 関数が作成されると、JavaScript エンジンは関数オブジェクトにスコープ チェーンと呼ばれるプロパティを添付します。このプロパティは、関数のスコープと親スコープ、そしてグローバル スコープまでを含む配列オブジェクトを指します。

したがって、上の図は、teach関数がグローバル環境で作成されるため、teachのスコープチェーンには1つのレイヤー、つまりグローバルスコープグローバルのみが存在すると簡単に理解できます。

ブラウザでは、global は window オブジェクトを参照し、nodejs 環境では、global は global オブジェクトを参照することがわかります。

もう一度覚えておいてください: 関数が実行されると、実行コンテキストを作成するためのスペースが要求されます。実行コンテキストには、関数が定義されたときのスコープ チェーンが含まれ、次に関数内で定義された変数とパラメーターが含まれます。関数が現在のスコープで実行されると、まず現在のスコープの下で変数が検索されます。見つからない場合は、関数が定義されたときのスコープ チェーンを検索し、グローバル スコープに到達します。変数がグローバル スコープで見つからない場合は、エラーがスローされます。

関数が実行されると、実行コンテキストが作成され、それが実際にスタック構造のメモリ空間に適用されることは誰もが知っています。関数内のローカル変数はこの空間に割り当てられます。関数が実行された後、ローカル変数は次のガベージ コレクション ノードでリサイクルされます。さて、コードをもう一度更新して、関数が実行されているときのメモリ構造を見てみましょう。

var a = new String('小歌')
関数を教える(){
  var b = new String('小谷')
}
教える()

メモリは次のように表されます。

明らかに、この関数は実行時にローカル変数を割り当てるだけで、グローバル環境の変数とは関係がないことがわかります。したがって、window オブジェクトから参照に沿って検索すると (図の矢印)、実行コンテキストで変数 b を見つけることはできません。したがって、関数が実行された後、変数 b はリサイクルされます。

コードをもう一度更新してみましょう:

var a = new String('小歌')
関数を教える(){
  var b = new String('小谷')
  var say = 関数() {
    コンソールログ(b)
  }
  a = 言う
}
教える()

メモリは次のように表されます。

注: 灰色はルート オブジェクトから追跡できないオブジェクトを示します。

関数の実行順序:

  1. ティーチ関数の実行が開始される前に、上図の青い四角で示すように、スタック領域が要求されます。
  2. コンテキスト スコープ (スタックのような構造) を作成し、teach 関数で定義された [[scopes]] をスコープにプッシュします。
  3. 変数 b を初期化し (変数の昇格)、関数 say を作成し、say の scopes プロパティを初期化し、最初に関数 teach のスコープを関数 say の [[scopes]] にプッシュします。 say は変数 b を参照するため、閉包が形成されます。したがって、クロージャ オブジェクトを関数の [[scopes]] にプッシュする必要もあります。
  4. ローカル変数 b と say を指す変数オブジェクト local を作成し、local をステップ 2 のスコープ内にプッシュします。
  5. 関数の実行開始
    1. 文字列オブジェクト「Xiaogu」を変数 b に割り当てます。
    2. グローバル変数 a を関数 say を指すように設定します。

関数が実行された後、通常は変数 b が解放されるはずです。しかし、ウィンドウに沿って検索すると b が見つかることがわかります。前述のメモリ ガベージを判別する原則によれば、b はメモリ ガベージではないため、b を解放することはできません。これが、クロージャが関数内の変数をメモリ内に保持する理由です。

コードを再度アップグレードして、クロージャによって共有されるメモリ表現を見てみましょう。

var a = 新しい文字列('0')
関数b() {
  var c = 新しい文字列('1')
  var d = 新しい文字列('2')
  関数e() {
    コンソール.log(c)
  }
  関数f(){
    コンソールログ(d)
  }
  戻り値 f
}
a = b()

灰色のグラフィックはメモリ ガベージであり、ガベージ コレクターによってリサイクルされます。

上の図から、関数 f は変数 c を使用していないものの、関数 e によって c が参照されているため、変数 c がクロージャ closure 内に存在することが簡単にわかります。変数 c は window オブジェクトから開始して見つけることができるため、変数 c を解放することはできません。

この機能がどのようにしてメモリリークを引き起こすのか疑問に思うかもしれません。さて、典型的な Meteor メモリ リーク問題である次のコードを考えてみましょう。

        var t = null;
        var replaceThing = 関数() {
            var o = t
            var 未使用 = 関数() {
                もし(o)
                    console.log("こんにちは")
            }
            t = {
                    longStr: 新しい配列(1000000).join('*'),
                    いくつかのメソッド: 関数() {
                      コンソール.log(1)
                    }
                }
        }
        間隔を設定します(replaceThing, 1000)

このコードにはメモリリークがあります。このコードをブラウザで実行すると、メモリが増加し続けていることがわかります。GC によってメモリの一部が解放されますが、解放できないメモリがまだ残っており、勾配をつけて増加しています。下記の通り

この曲線はメモリ リークがあることを示しています。開発者ツールを使用して、どのオブジェクトがリサイクルされていないかを分析できます。実際のところ、解放されないメモリは、毎回作成される大きなオブジェクトであると言えます。これを絵で描いてみましょう:

上の図では、replaceThing 関数が 3 回実行されることを前提としています。変数 t に大きなオブジェクトを割り当てるたびに、クロージャの共有により、以前の大きなオブジェクトは window オブジェクトから追跡できるため、これらの大きなオブジェクトはリサイクルできないことがわかります。実際、私たちにとって本当に役立つのは、前回 t に割り当てられた大きなオブジェクトであり、そのため、前のオブジェクトがメモリ リークを引き起こしました。

ご想像のとおり、これを認識せずにプログラムを実行すると、ブラウザはすぐにクラッシュしてしまいます。

この問題の解決方法も非常に簡単です。コードが実行されるたびに、変数 o を null に設定します。試してみてください。

結論

ブラウザがクロージャをどのように認識するかについての記事はこれで終わりです。ブラウザがクロージャをどのように認識するかについての詳細は、123WORDPRESS.COM の過去の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • JavaScript の js クロージャの詳細な理解
  • 1分でJSクロージャを理解する
  • フロントエンド開発者が知っておくべき JS クロージャとアプリケーション
  • JavaScript でクロージャを記述して使用する方法の詳細な説明
  • JavaScript のクロージャを理解するかどうかはあなた次第です。とにかく私は理解しています。
  • jsクロージャの使用に関する詳細な説明

<<:  CSS3 の display:grid、グリッドレイアウトの紹介

>>:  iframe タグの使用方法の詳細な説明 (属性、透明度、適応高さ)

推薦する

Sparkの紹介とHadoopとの比較

目次1. SparkとHadoopの比較1.1 Haoopの欠点1.2 Hadoop MR に対する...

JavaScriptでマクロを使用する方法

言語では、DSL を実装するためにマクロがよく使用されます。マクロを使用すると、開発者は JSX 構...

HTML/CSS の基礎 - HTML コード記述におけるいくつかの注意事項 (必読)

この記事の警告事項は、ブラウザの互換性とはまったく関係ありません。主に、プロジェクトで遭遇したいくつ...

MySQL 最適化における B ツリー インデックスの知識ポイントのまとめ

SQL を最適化する必要があるのはなぜですか?当然ですが、SQL ステートメントを記述する場合、次の...

光るテキストとちょっとしたJS特殊効果を実現するCSS

実装のアイデア: CSSでtext-shadowを使用してテキストの光る効果を実現します効果画像: ...

CSS コンテナ背景 10 色グラデーション デモ (linear-gradient())

文法 背景: linear-gradient(direction,color-stop1,color...

jsを使ってシンプルなディスククロックを実現する

この記事では、参考までに、シンプルなディスククロックを実装するためのjsの具体的なコードを紹介します...

HTML でのテキストエリアの使用と一般的な問題およびケース分析

textarea タグはよく使われる HTML タグです。主に長いテキストを入力するときに改行するた...

Windows 10 64 ビット版に MySQL 5.6.35 をインストールするためのグラフィック チュートリアル

1. MySQL Community Server 5.6.35をダウンロードするダウンロードアドレ...

Alibaba Cloud Server Tomcatにアクセスできません

目次1. はじめに2. 解決策2.1 ファイアウォールを設定してポートを開く2.3 ポートを確認し、...

Linux での sshd サービスとサービス管理コマンドの詳細な説明

sshd SSH は Secure Shell の略で、アプリケーション層のセキュリティ プロトコル...

MySQL サービスに iptables ファイアウォール ポリシーを追加するためのソリューション

MySQL データベースが Centos7 システムにインストールされており、オペレーティング シス...

MySQL binlog を使用して誤って削除されたデータベースを復元する方法

目次1 現在のデータベースの内容を表示し、データベースをバックアップする2 bin_log関数を有効...

count(1)、count(*)、count(列名)の実行の違いの詳細な説明

実施効果: 1. count(1) と count(*)テーブル内のデータ量が多い場合、テーブルを分...

MySQLオンラインデータベースのデータをクリーンアップする方法

目次01 シナリオ分析02 操作方法03 結果分析01 シナリオ分析今日の午後、開発仲間がオンライン...