ホームページ  >  記事  >  ウェブフロントエンド  >  フロントエンド応用編(4): スコープチェーンとクロージャの詳細図

フロントエンド応用編(4): スコープチェーンとクロージャの詳細図

PHPz
PHPzオリジナル
2017-04-04 17:39:471337ブラウズ

フロントエンド応用編(4): スコープチェーンとクロージャの詳細図

クロージャの問題を克服する


私が JavaScript を初めて使用したとき、クロージャの学習で多くの回り道をしました。今回は改めて基礎知識を整理してみましたが、クロージャを分かりやすく説明するのは非常に大きな課題でもあります。

クロージャはどのくらい重要ですか?フロントエンドを初めて使用する場合、クロージャが実際の開発でどのように普及しているかを直観的に伝えることはできませんが、フロントエンドのインタビューでは、クロージャについて質問する必要がありますということは言えます。面接官は、クロージャについての理解を利用して面接官の基本レベルを判断することがよくありますが、保守的に見積もっても、フロントエンド面接官の 10 人中少なくとも 5 人はクロージャで死亡します。

しかし、なぜクロージャは非常に重要であるにもかかわらず、これほど多くの人がまだそれを理解していないのでしょうか?みんな学ぶ気がないからでしょうか?実際にはそうではありませんが、検索で見つけたクロージャを説明する中国語の記事のほとんどは、クロージャについて明確に説明していませんでした。それは表面的であるか、理解できないか、あるいは単にナンセンスであるかのいずれかです。私自身も含めて、かつてクロージャについてまとめた記事を書いたことがあるのですが、振り返ってみると、それは見るに耐えませんでした。

そのため、この記事の目的は、読者がクロージャを漠然と理解するのではなく、読んだ後に完全に理解できるように、クロージャを明確かつ明確に説明することです。

1. スコープとスコープチェーン

スコープチェーンを詳しく説明する前に、JavaScript における次の重要な概念を大まかに理解していることを前提としています。これらの概念は非常に役立ちます。

まだ理解していない場合は、この記事の最後に目次のリンクがあるので、このシリーズの最初の 3 つの記事を読むことができます。クロージャを説明するために、皆さんのために基礎知識を用意しました。ハハ、なんて大きなショーなんだ。

スコープ

  • JavaScript では、この一連のルールを使用して、エンジンが現在のスコープおよびネストされたサブスコープ内の識別子名に基づいて操作を実行する方法を管理します。

    ここでの識別子は、変数名または関数name

  • JavaScriptにはグローバルスコープと関数スコープのみがあります(日常の開発ではevalがほとんど使用されないため)、ここでは説明しません) 。

  • スコープと実行コンテキストは、2 つのまったく異なる概念です。混同している人も多いとは思いますが、しっかり区別してください。

    JavaScript コードの実行プロセス全体は、コードのコンパイル段階とコードの実行段階の 2 つの段階に分かれています。コンパイル フェーズはコンパイラによって完了し、コードが実行可能コードに変換されます。スコープ ルールはこの段階で決定されます。実行フェーズはエンジンによって完了します。主なタスクは、実行可能コードを実行することです。このフェーズでは実行コンテキストが作成されます。

フロントエンド応用編(4): スコープチェーンとクロージャの詳細図

プロセス

スコープチェーン

前回の記事で分析した実行コンテキストのライフサイクルを以下に示します。

フロントエンド応用編(4): スコープチェーンとクロージャの詳細図

実行コンテキストのライフサイクル

スコープチェーンは実行コンテキストの作成フェーズ中に生成されることがわかりました。これはおかしい。上でスコープがコンパイル段階でのルールを決定すると述べましたが、なぜ実行段階でスコープチェーンが決定されるのでしょうか?

この質問がある理由は、誰もがスコープとスコープチェーンについて誤解しているからです。上で述べたように、スコープは一連のルールです。では、スコープ チェーンとは何でしょうか?これは、この一連のルールの具体的な実装です。これがスコープとスコープチェーンの関係であり、誰もがそれを理解する必要があると思います。

関数が呼び出されてアクティブ化されると、実行コンテキスト生成プロセス中に、対応する実行コンテキストの作成が開始され、変数オブジェクト、スコープ チェーン、および this の値がそれぞれ決定されることがわかっています。前回の記事では変数オブジェクトについて詳しく説明しましたが、今回はスコープチェーンについて詳しく説明します。

スコープ チェーンは、現在の環境と上位の環境の間の一連の変数オブジェクトで構成され、現在の実行環境がアクセス許可に準拠した変数と関数に正常にアクセスできるようにします。

誰もがスコープチェーンを理解できるように、最初に例と対応する図を使って説明しましょう。

var a = 20;

function test() {
    var b = a + 10;

    function innerTest() {
        var c = 10;
        return b + c;
    }

    return innerTest();
}

test();

上記の例では、global、関数test、関数innerTestの実行コンテキストが連続して作成されます。変数オブジェクトをそれぞれ VO(global)、VO(test)、VO(innerTest) として設定します。 innerTest のスコープ チェーンにはこれら 3 つの変数オブジェクトが同時に含まれるため、innerTest の実行コンテキストは次のように表現できます。

innerTestEC = {
    VO: {...},  // 变量对象
    scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域链
    this: {}
}

はい、正しくお読みいただけます。配列を直接使用してスコープ チェーンを表すことができます。配列の最初の項目、scopeChain[0] はスコープ チェーンのフロント エンドであり、スコープ チェーンの最後の項目です。配列はスコープ チェーンの終端では、すべての終端がグローバル変数オブジェクトです。

現在のスコープと上位スコープが包含関係にあると誤解している人が多いかもしれませんが、そうではありません。先頭から始まり最後尾で終わる一方通行という表現の方が適切だと思います。写真の通り。

フロントエンド応用編(4): スコープチェーンとクロージャの詳細図

スコープチェーン図

なお、実行コンテキストが実行フェーズに入ると変数オブジェクトがアクティブオブジェクトになるため、これについては前の記事で述べたので、図ではAOを使用して表現しています。 。アクティブ オブジェクト

はい、スコープ チェーンは一連の変数オブジェクトで構成されており、この一方向チャネルでは、変数オブジェクト内の識別子を クエリして、上位レベルのスコープ変数にアクセスできます。で 。

2. クロージャ

JavaScript の使用経験はあるものの、クロージャの概念をよく理解したことがない人にとって、クロージャを理解することは、ある意味、クロージャのボトルネックを突破することによって、飛躍的に向上することができます。スキル。

  • クロージャはスコープチェーンと密接に関連しています。

  • クロージャは関数の実行中に確認されます。

まず、クロージャの定義を直接破棄します。 クロージャは、関数が現在のスコープ外で実行された場合でも、関数がそのスコープ (グローバル スコープを除く) を記憶してアクセスできるときに生成されます。

簡単に言えば、関数 A が関数 B 内で定義されており、関数 A が実行されると関数 B 内の変数オブジェクトにアクセスすると仮定すると、B はクロージャです。

基礎応用編(1)では、JavaScriptのガベージコレクションの仕組みについてまとめました。 JavaScript には自動ガベージ コレクション メカニズムがあります。ガベージ コレクション メカニズムに関しては、重要な 動作 があります。つまり、値がメモリ内で参照を失った場合、ガベージ コレクション メカニズムは特別なアルゴリズムに従ってそれを見つけて再利用します。 、メモリを解放します。

そして、関数の実行コンテキストが完了し、ライフサイクルが終了すると、関数の実行コンテキストは参照を失うことがわかっています。占有しているメモリ空間は、ガベージ コレクターによって間もなく解放されます。ただし、クロージャが存在すると、このプロセスが妨げられます。

まず簡単な例を見てみましょう。

var fn = null;
function foo() {
    var a = 2;
    function innnerFoo() { 
        console.log(a);
    }
    fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}

function bar() {
    fn(); // 此处的保留的innerFoo的引用
}

foo();
bar(); // 2

上記の例では、関数 innerFoo への参照が保持され、グローバル変数 fn にコピーされます。この動作により、foo の変数オブジェクトが保持されます。したがって、関数 fn が関数 bar 内で実行された場合でも、保持されている変数オブジェクトにアクセスできます。したがって、変数 a の値には現時点でもアクセスできます。 foo()执行完毕之后,按照常理,其执行环境生命周期会结束,所占内存被垃圾收集器释放。但是通过fn = innerFoo

このようにして、foo をクロージャと呼ぶことができます。

次の図は、クロージャ foo のスコープ チェーンを示しています。

フロントエンド応用編(4): スコープチェーンとクロージャの詳細図

クロージャ foo のスコープ チェーン、画像のタイトルが間違っています、無視してください

このコードが

chrome ブラウザの開発者ツールで実行されているときに生成される関数呼び出しスタックを表示できます。スコープチェーンの生成。以下に示すように。

Chrome でクロージャを観察する方法とクロージャのその他の例の詳細については、基本シリーズ (6 つ) をお読みください

フロントエンド応用編(4): スコープチェーンとクロージャの詳細図

写真からわかるように、Chrome ブラウザはクロージャがfoo、私たちが通常考える innerFoo ではなく

在上面的图中,红色箭头所指的正是闭包。其中Call Stack为当前的函数调用栈,Scope为当前正在被执行的函数的作用域链,Local为当前的局部变量。

所以,通过闭包,我们可以在其他的执行上下文中,访问到函数的内部变量。比如在上面的例子中,我们在函数bar的执行环境中访问到了函数foo的a变量。个人认为,从应用层面,这是闭包最重要的特性。利用这个特性,我们可以实现很多有意思的东西。

不过读者老爷们需要注意的是,虽然例子中的闭包被保存在了全局变量中,但是闭包的作用域链并不会发生任何改变。在闭包中,能访问到的变量,仍然是作用域链上能够查询到的变量。

对上面的例子稍作修改,如果我们在函数bar中声明一个变量c,并在闭包fn中试图访问该变量,运行结果会抛出错误。

var fn = null;
function foo() {
    var a = 2;
    function innnerFoo() { 
        console.log(c); // 在这里,试图访问函数bar中的c变量,会抛出错误
        console.log(a);
    }
    fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}

function bar() {
    var c = 100;
    fn(); // 此处的保留的innerFoo的引用
}

foo();
bar();

关于这一点,很多同学把函数调用栈与作用域链没有分清楚,所以有的大神看了我关于介绍执行上下文的文章时就义正言辞的说我的例子有问题,而这些评论有很大的误导作用,为了帮助大家自己拥有能够辨别的能力,所以我写了基础(六),教大家如何在chrome中观察闭包,作用域链,this等。当然我也不敢100%保证我文中的例子就一定正确,所以教大家如何去辨认我认为才是最重要的。

闭包的应用场景

接下来,我们来总结下,闭包的常用场景。

我们知道setTimeout的第一个参数是一个函数,第二个参数则是延迟的时间。在下面例子中,

function fn() {
    console.log('this is test.')
}
var timer =  setTimeout(fn, 1000);
console.log(timer);

执行上面的代码,变量timer的值,会立即输出出来,表示setTimeout这个函数本身已经执行完毕了。但是一秒钟之后,fn才会被执行。这是为什么?

按道理来说,既然fn被作为参数传入了setTimeout中,那么fn将会被保存在setTimeout变量对象中,setTimeout执行完毕之后,它的变量对象也就不存在了。可是事实上并不是这样。至少在这一秒钟的事件里,它仍然是存在的。这正是因为闭包。

很显然,这是在函数的内部实现中,setTimeout通过特殊的方式,保留了fn的引用,让setTimeout的变量对象,并没有在其执行完毕后被垃圾收集器回收。因此setTimeout执行结束后一秒,我们任然能够执行fn函数。

  • 柯里化

在函数式编程中,利用闭包能够实现很多炫酷的功能,柯里化算是其中一种。关于柯里化,我会在以后详解函数式编程的时候仔细总结。

  • 模块

在我看来,模块是闭包最强大的一个应用场景。如果你是初学者,对于模块的了解可以暂时不用放在心上,因为理解模块需要更多的基础知识。但是如果你已经有了很多JavaScript的使用经验,在彻底了解了闭包之后,不妨借助本文介绍的作用域链与闭包的思路,重新理一理关于模块的知识。这对于我们理解各种各样的设计模式具有莫大的帮助。

(function () {
    var a = 10;
    var b = 20;

    function add(num1, num2) {
        var num1 = !!num1 ? num1 : a;
        var num2 = !!num2 ? num2 : b;

        return num1 + num2;
    }

    window.add = add;
})();

add(10, 20);

在上面的例子中,我使用函数自执行的方式,创建了一个模块。add是模块对外暴露的一个公共方法。而变量a,b被作为私有变量。在面向对象的开发中,我们常常需要考虑是将变量作为私有变量,还是放在构造函数中的this中,因此理解闭包,以及原型链是一个非常重要的事情。模块十分重要,因此我会在以后的文章专门介绍,这里就暂时不多说啦。

フロントエンド応用編(4): スコープチェーンとクロージャの詳細図

此图中可以观看到当代码执行到add方法时的调用栈与作用域链,此刻的闭包为外层的自执行函数

为了验证自己有没有搞懂作用域链与闭包,这里留下一个经典的思考题,常常也会在面试中被问到。

利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5

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

点此查看关于此题的详细解读

关于作用域链的与闭包我就总结完了,虽然我自认为我是说得非常清晰了,但是我知道理解闭包并不是一件简单的事情,所以如果你有什么问题,可以在评论中问我。你也可以带着从别的地方没有看懂的例子在评论中留言。大家一起学习进步。

以上がフロントエンド応用編(4): スコープチェーンとクロージャの詳細図の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。