ホームページ > 記事 > ウェブフロントエンド > クロージャには何か言いたいことがある - 大きなフロントエンド
私は初めてフロントエンドについて学んだとき、面接中にこの質問をされたとき、クロージャという言葉を見ていつも答えが曖昧で、壁があると感じていました。この概念は非常にわかりにくいと感じました。これをマスターできれば、スキルは大幅に向上します。実際、クロージャはそれほど神秘的なものではなく、どこにでも存在します。
まず、質問を見てみましょう。
クロージャとは何かを一文で説明し、それを説明するコードを書いてください。
遠慮なく言えて説明できる方は、以下の文章を読む必要はありません。
この問題について、私が検討した情報と経験を組み合わせて、ここで簡単に説明します。何か間違っている場合は、修正してください。
まず、クロージャとは何かという上記の質問に答えてください。
クロージャは、関数が実行された後もメモリ内に存在する現象を説明する概念です。
コードの説明:
function foo() { var a = 2; function bar(){ console.log(a); } return bar; } var test = foo(); test(); //2
上記のコードは明らかにクロージャを示しています。
関数 bar() の字句スコープは foo() の内部スコープにアクセスできます。次に、bar() 関数自体を値の型として渡します。上記の例では、bar() で参照される関数オブジェクト自体を戻り値として使用しています。
foo() が実行された後、その内部スコープは破棄されません。これは、bar() の位置のおかげで、bar() が内部スコープへの参照を保持しているためです。foo() クロージャーをカバーする内部スコープを持っています。これにより、スコープが存続し、その後いつでも bar() によって参照されるようになります。この参照は実際にはクロージャです。
test が実際に呼び出されるとき、定義されたときに字句スコープにアクセスできるのはこのためです。
関数転送は間接的に行うこともできます:
var fn; function foo(){ var a = 2; function baz() { console.log( a ); } fn = baz; //将baz 分配给全局变量 } function bar(){ fn(); } foo(); bar(); //2
したがって、どのような場合であってもそれを意味します内部関数が、それが配置されている字句スコープの外に渡されると、元の定義スコープへの参照が保持されます。つまり、この関数が実行される場合はどこでも、クロージャが使用されます。コールバック関数の詳細を気にすることなく、非常に便利に使用できるのもこの理由からです。
実際、タイマー、イベント リスナー、Ajax リクエスト、クロスウィンドウ通信、Web ワーカー、その他の同期タスクまたは非同期タスクでコールバック関数を使用している限り、実際にはクロージャを使用していることになります。
この時点で、おそらくクロージャについての一般的な理解はすでにできていると思いますが、クロージャについての理解を深めるために、さらにいくつかの例を紹介します。
まず、いわゆる即時実行関数を見てみましょう
var a = 2; (function IIFE() { console.log(a); })(); //2
この即時実行関数は通常、正常に動作しますが、厳密に言えば、そうではありません。閉鎖。
なぜですか?
この IIFE 関数は、独自の字句範囲外では実行されないためです。定義されたスコープ内で実行されます。さらに、変数 a はクロージャではなく、通常の字句スコープを通じて検索されます。
クロージャを説明するために使用される別の例はループです。
<p class="tabs"> <li class="tab">some text1</li> <li class="tab">some text2</li> <li class="tab">some text3</li> </p>
var handler = function(nodes) { for(var i = 0, l = nodes.length; i < l ; i++) { nodes[i].onclick = function(){ console.log(i); } } } var tabs = document.querySelectorAll('.tabs .tab'); handler(tabs);
期待される結果は log 0、1、2 です。
実行後の結果は 3 3 です
これはなぜですか?
まず、この 3 がどのようにして得られるかを説明します。
ループの終了条件は、最初に条件が成立したときの i の値は 3 です。
つまり、出力には、ループの終わりの i の最終値が表示されます。 スコープの動作原理によれば、ループ内の関数は反復ごとに個別に定義されますが、それらはすべて共有のグローバル スコープに囲まれているため、実際には i.
handler が 1 つだけ存在します。関数の本来の目的は Trying です。一意の i をイベント ハンドラーに渡そうとしましたが、失敗しました。
イベント ハンドラー関数は、関数の構築時に i の値ではなく i 自体をバインドするため、
これを理解した後、対応する調整を行うことができます:
var handler = function(nodes) { var helper = function(i){ return function(e){ console.log(i); // 0 1 2 } } for(var i = 0, l = nodes.length; i < l ; i++) { nodes[i].onclick = helper(i); } }
ループの外に補助関数を作成し、このヘルパー関数は関数は i の現在値にバインドされているため、混乱はありません。
これを理解すると、上記の処理は新しいスコープを作成することであることがわかります。つまり、反復ごとにブロック スコープが必要です。
ブロック スコープに関しては、一言言及する必要があります。 let.
それで、クロージャをあまり使いたくない場合は、jQueryでlet:
var handler = function(nodes) { for(let i = 0, l = nodes.length; i < l ; i++) { //nodes[i].index = i; nodes[i].onclick = function(){ console.log(i); // 0 1 2 } } }
まず例を見てみましょう
var sel = $("#con"); setTimeout( function (){ sel.css({background:"gray"}); }, 2000);
上記のコードはjQuery Selectorを使用し、要素を見つけますid con でタイマーを登録し、2 秒後に背景色をグレーに設定します。
このコード スニペットの魅力は、setTimeout 関数を呼び出した後も、con が関数内に保持されたままで、2 秒後に ID con を持つ p 要素の背景色が実際に変更されることです。 setTimeout は呼び出し後に返されましたが、con はグローバル スコープ内の変数 con を参照しているため、con は解放されていないことに注意してください。
上記の例は、クロージャについてさらに詳しく理解するのに役立ちます。クロージャの世界をさらに深く掘り下げてみましょう。
首先看一个概念-执行上下文(Execution Context)。
执行上下文是一个抽象的概念,ECMAScript 规范使用它来追踪代码的执行。它可能是你的代码第一次执行或执行的流程进入函数主体时所在的全局上下文。
在任意一个时间点,只能有唯一一个执行上下文在运行之中。
这就是为什么 JavaScript 是“单线程”的原因,意思就是一次只能处理一个请求。
一般来说,浏览器会用栈来保存这个执行上下文。
栈是一种“后进先出” (Last In First Out) 的数据结构,即最后插入该栈的元素会最先从栈中被弹出(这是因为我们只能从栈的顶部插入或删除元素)。
当前的执行上下文,或者说正在运行中的执行上下文永远在栈顶。
当运行中的上下文被完全执行以后,它会由栈顶弹出,使得下一个栈顶的项接替它成为正在运行的执行上下文。
除此之外,一个执行上下文正在运行并不代表另一个执行上下文需要等待它完成运行之后才可以开始运行。
有时会出现这样的情况,一个正在运行中的上下文暂停或中止,另外一个上下文开始执行。暂停的上下文可能在稍后某一时间点从它中止的位置继续执行。
一个新的执行上下文被创建并推入栈顶,成为当前的执行上下文,这就是执行上下文替代的机制。
当我们有很多执行上下文一个接一个地运行时——通常情况下会在中间暂停然后再恢复运行——为了能很好地管理这些上下文的顺序和执行情况,我们需要用一些方法来对其状态进行追踪。而实际上也是如此,根据ECMAScript的规范,每个执行上下文都有用于跟踪代码执行进程的各种状态的组件。包括:
代码执行状态:任何需要开始运行,暂停和恢复执行上下文相关代码执行的状态
函数:上下文中正在执行的函数对象(正在执行的上下文是脚本或模块的情况下可能是null)
Realm:一系列内部对象,一个ECMAScript全局环境,所有在全局环境的作用域内加载的ECMAScript代码,和其他相关的状态及资源。
词法环境:用于解决此执行上下文内代码所做的标识符引用。
变量环境:一种词法环境,该词法环境的环境记录保留了变量声明时在执行上下文中创建的绑定关系。
现在的开发都离不开模块化,下面说说模块是如何利用闭包的。
先看一个实际中的例子。
这是一个统计模块,看一下代码:
define("components/webTrends", ["webTrendCore"], function(require,exports, module) { var webTrendCore = require("webTrendCore"); var webTrends = { init:function (obj) { var self = this; self.dcsGetId(); self.dcsCollect(); }, dcsGetId:function(){ if (typeof(_tag) != "undefined") { _tag.dcsid="dcs5w0txb10000wocrvqy1nqm_6n1p"; _tag.dcsGetId(); } }, dcsCollect:function(){ if (typeof(_tag) != "undefined") { _tag.DCSext.platform="weimendian"; if(document.readyState!="complete"){ document.onreadystatechange = function(){ if(document.readyState=="complete") _tag.dcsCollect() } } else _tag.dcsCollect() } } }; module.exports = webTrends; })
在主页面使用的时候,调用一下就可以了:
var webTrends = require("webTrends"); webTrends.init();
在定义的模块中,我们暴露了webTrends对象,在外面调用返回对象中的方法就形成了闭包。
模块的两个必要条件:
必须有外部的封闭函数,该函数必须至少被调用一次
封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
如果一个任务不需要使用闭包,那最好不要在函数内创建函数。
原因很明显,这会 拖慢脚本的处理速度,加大内存消耗 。
举个例子,当需要创建一个对象时,方法通常应该和对象的原型关联,而不是定义到对象的构造函数中。 原因是 每次构造函数被调用, 方法都会被重新赋值 (即 对于每个对象创建),这显然是一种不好的做法。
看一个能说明问题,但是不推荐的做法:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }
上面的代码并没有很好的利用闭包,我们来改进一下:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype = { getName: function() { return this.name; }, getMessage: function() { return this.message; } };
好一些了,但是不推荐重新定义原型,再来改进下:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype.getName = function() { return this.name; }; MyObject.prototype.getMessage = function() { return this.message; };
很显然,在现有的原型上添加方法是一种更好的做法。
上面的代码还可以写的更简练:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } (function() { this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }).call(MyObject.prototype);
在前面的三个示例中,继承的原型可以由所有对象共享,并且在每个对象创建时不需要定义方法定义。如果想看更多细节,可以参考对象模型。
使用闭包可以在JavaScript中模拟块级作用域;
クロージャを使用して、オブジェクト内にプライベート変数を作成できます。
利点:
ロジックの連続性。クロージャが別の関数呼び出しのパラメータとして使用される場合、現在のロジックから離脱して追加のロジックを個別に記述することを防ぎます。
コンテキストのローカル変数を呼び出すのに便利です。
カプセル化を強化し、ポイント 2 を拡張することで変数の保護を実現できます。
短所:
メモリの無駄。このメモリの浪費は、メモリ内に常駐しているためだけでなく、クロージャを不適切に使用すると無効なメモリが生成される原因になります。
これまでクロージャについて簡単に説明しましたが、最後にまとめると、クロージャの特徴は次のとおりです:
関数の入れ子関数
外部変数にアクセスしたり、オブジェクト
ガベージコレクションを避ける
上記のコミュニケーションを歓迎します;-)
一緒にJavaScriptクロージャを学びましょう
JavaScriptパッケージのスコープとクロージャを理解します
クロージャ
私が初めてフロントエンドを学んだとき、インタビュー中にこの質問をされたとき、私はいつもクロージャという言葉を見て戸惑いました、私の答えはいつも分離の層があるように感じました。この概念は非常に魔法だと思います。これをマスターできれば、あなたのスキルは大幅に向上します。実際、クロージャはそれほど神秘的なものではなく、どこにでも存在します。
まず、質問を見てみましょう。
クロージャとは何かを一文で説明し、それを説明するコードを書いてください。
遠慮なく言えて説明できる方は、以下の文章を読む必要はありません。
この問題について、私が検討した情報と経験を組み合わせて、ここで簡単に説明します。何か間違っている場合は、修正してください。
まず、クロージャとは何かという上記の質問に答えてください。
クロージャは、関数が実行された後もメモリ内に存在する現象を説明する概念です。
コードの説明:
function foo() { var a = 2; function bar(){ console.log(a); } return bar; } var test = foo(); test(); //2
上記のコードは明らかにクロージャを示しています。
関数 bar() の字句スコープは foo() の内部スコープにアクセスできます。次に、bar() 関数自体を値の型として渡します。上記の例では、bar() で参照される関数オブジェクト自体を戻り値として使用しています。
foo() が実行された後、その内部スコープは破棄されません。これは、bar() の位置のおかげで、bar() が内部スコープへの参照を保持しているためです。foo() クロージャーをカバーする内部スコープを持っています。これにより、スコープが存続し、その後いつでも bar() によって参照されるようになります。この参照は実際にはクロージャです。
test が実際に呼び出されるとき、定義されたときに字句スコープにアクセスできるのはこのためです。
関数転送は間接的に行うこともできます:
var fn; function foo(){ var a = 2; function baz() { console.log( a ); } fn = baz; //将baz 分配给全局变量 } function bar(){ fn(); } foo(); bar(); //2
したがって、どのような場合であってもそれを意味します内部関数が、それが配置されている字句スコープの外に渡されると、元の定義スコープへの参照が保持されます。つまり、この関数が実行される場合はどこでも、クロージャが使用されます。コールバック関数の詳細を気にすることなく、非常に便利に使用できるのもこの理由からです。
実際、タイマー、イベント リスナー、Ajax リクエスト、クロスウィンドウ通信、Web ワーカー、その他の同期タスクまたは非同期タスクでコールバック関数を使用している限り、実際にはクロージャを使用していることになります。
この時点で、おそらくクロージャについての一般的な理解はすでにできていると思いますが、クロージャについての理解を深めるために、さらにいくつかの例を紹介します。
まず、いわゆる即時実行関数を見てみましょう。
var a = 2; (function IIFE() { console.log(a); })(); //2
この即時実行関数は通常、古典的なクロージャの例と考えられていますが、厳密に言えば、正常に動作します。それは閉鎖ではありません。
なぜですか?
この IIFE 関数は、独自の字句範囲外では実行されないためです。定義されたスコープ内で実行されます。さらに、変数 a はクロージャではなく、通常の字句スコープを通じて検索されます。
クロージャを説明するために使用される別の例はループです。
<p class="tabs"> <li class="tab">some text1</li> <li class="tab">some text2</li> <li class="tab">some text3</li> </p>
var handler = function(nodes) { for(var i = 0, l = nodes.length; i < l ; i++) { nodes[i].onclick = function(){ console.log(i); } } } var tabs = document.querySelectorAll('.tabs .tab'); handler(tabs);
期待される結果は log 0、1、2 です。
実行後の結果は 3 3 です
これはなぜですか?
まず、この 3 がどのようにして得られるかを説明します。
ループの終了条件は、最初に条件が成立したときの i の値は 3 です。
つまり、出力には、ループの終わりの i の最終値が表示されます。 スコープの動作原理によれば、ループ内の関数は反復ごとに個別に定義されますが、それらはすべて共有のグローバル スコープに囲まれているため、実際には i.
handler が 1 つだけ存在します。関数の本来の目的は Trying です。一意の i をイベント ハンドラーに渡そうとしましたが、失敗しました。
イベント ハンドラー関数は、関数の構築時に i の値ではなく、i 自体をバインドするためです。
これを理解した後、対応する調整を行うことができます:
var handler = function(nodes) { var helper = function(i){ return function(e){ console.log(i); // 0 1 2 } } for(var i = 0, l = nodes.length; i < l ; i++) { nodes[i].onclick = helper(i); } }
在循环外创建一个辅助函数,让这个辅助函数在返回一个绑定了当前i的值的函数,这样就不会混淆了。
明白了这点,就会发现,上面的处理就是为了创建一个新的作用域,换句话说,每次迭代我们都需要一个块作用域.
说到块作用域,就不得不提一个词,那就是let.
所以,如果你不想过多的使用闭包,就可以使用let:
var handler = function(nodes) { for(let i = 0, l = nodes.length; i < l ; i++) { //nodes[i].index = i; nodes[i].onclick = function(){ console.log(i); // 0 1 2 } } }
先来看个例子
var sel = $("#con"); setTimeout( function (){ sel.css({background:"gray"}); }, 2000);
上边的代码使用了 jQuery 的选择器,找到 id 为 con 的元素,注册计时器,两秒之后,将背景色设置为灰色。
这个代码片段的神奇之处在于,在调用了 setTimeout 函数之后,con 依旧被保持在函数内部,当两秒钟之后,id 为 con 的 p 元素的背景色确实得到了改变。应该注意的是,setTimeout 在调用之后已经返回了,但是 con 没有被释放,这是因为 con 引用了全局作用域里的变量 con。
以上的例子帮助我们了解了更多关于闭包的细节,下面我们就深入闭包世界探寻一番。
首先看一个概念-执行上下文(Execution Context)。
执行上下文是一个抽象的概念,ECMAScript 规范使用它来追踪代码的执行。它可能是你的代码第一次执行或执行的流程进入函数主体时所在的全局上下文。
在任意一个时间点,只能有唯一一个执行上下文在运行之中。
这就是为什么 JavaScript 是“单线程”的原因,意思就是一次只能处理一个请求。
一般来说,浏览器会用栈来保存这个执行上下文。
栈是一种“后进先出” (Last In First Out) 的数据结构,即最后插入该栈的元素会最先从栈中被弹出(这是因为我们只能从栈的顶部插入或删除元素)。
当前的执行上下文,或者说正在运行中的执行上下文永远在栈顶。
当运行中的上下文被完全执行以后,它会由栈顶弹出,使得下一个栈顶的项接替它成为正在运行的执行上下文。
除此之外,一个执行上下文正在运行并不代表另一个执行上下文需要等待它完成运行之后才可以开始运行。
有时会出现这样的情况,一个正在运行中的上下文暂停或中止,另外一个上下文开始执行。暂停的上下文可能在稍后某一时间点从它中止的位置继续执行。
一个新的执行上下文被创建并推入栈顶,成为当前的执行上下文,这就是执行上下文替代的机制。
当我们有很多执行上下文一个接一个地运行时——通常情况下会在中间暂停然后再恢复运行——为了能很好地管理这些上下文的顺序和执行情况,我们需要用一些方法来对其状态进行追踪。而实际上也是如此,根据ECMAScript的规范,每个执行上下文都有用于跟踪代码执行进程的各种状态的组件。包括:
代码执行状态:任何需要开始运行,暂停和恢复执行上下文相关代码执行的状态
函数:上下文中正在执行的函数对象(正在执行的上下文是脚本或模块的情况下可能是null)
Realm:一系列内部对象,一个ECMAScript全局环境,所有在全局环境的作用域内加载的ECMAScript代码,和其他相关的状态及资源。
词法环境:用于解决此执行上下文内代码所做的标识符引用。
变量环境:一种词法环境,该词法环境的环境记录保留了变量声明时在执行上下文中创建的绑定关系。
现在的开发都离不开模块化,下面说说模块是如何利用闭包的。
先看一个实际中的例子。
这是一个统计模块,看一下代码:
define("components/webTrends", ["webTrendCore"], function(require,exports, module) { var webTrendCore = require("webTrendCore"); var webTrends = { init:function (obj) { var self = this; self.dcsGetId(); self.dcsCollect(); }, dcsGetId:function(){ if (typeof(_tag) != "undefined") { _tag.dcsid="dcs5w0txb10000wocrvqy1nqm_6n1p"; _tag.dcsGetId(); } }, dcsCollect:function(){ if (typeof(_tag) != "undefined") { _tag.DCSext.platform="weimendian"; if(document.readyState!="complete"){ document.onreadystatechange = function(){ if(document.readyState=="complete") _tag.dcsCollect() } } else _tag.dcsCollect() } } }; module.exports = webTrends; })
在主页面使用的时候,调用一下就可以了:
var webTrends = require("webTrends"); webTrends.init();
在定义的模块中,我们暴露了webTrends对象,在外面调用返回对象中的方法就形成了闭包。
模块的两个必要条件:
必须有外部的封闭函数,该函数必须至少被调用一次
封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
如果一个任务不需要使用闭包,那最好不要在函数内创建函数。
原因很明显,这会 拖慢脚本的处理速度,加大内存消耗 。
举个例子,当需要创建一个对象时,方法通常应该和对象的原型关联,而不是定义到对象的构造函数中。 原因是 每次构造函数被调用, 方法都会被重新赋值 (即 对于每个对象创建),这显然是一种不好的做法。
看一个能说明问题,但是不推荐的做法:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }
上面的代码并没有很好的利用闭包,我们来改进一下:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype = { getName: function() { return this.name; }, getMessage: function() { return this.message; } };
好一些了,但是不推荐重新定义原型,再来改进下:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype.getName = function() { return this.name; }; MyObject.prototype.getMessage = function() { return this.message; };
很显然,在现有的原型上添加方法是一种更好的做法。
上面的代码还可以写的更简练:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } (function() { this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }).call(MyObject.prototype);
在前面的三个示例中,继承的原型可以由所有对象共享,并且在每个对象创建时不需要定义方法定义。如果想看更多细节,可以参考对象模型。
使用闭包可以在JavaScript中模拟块级作用域;
闭包可以用于在对象中创建私有变量。
优点:
逻辑连续,当闭包作为另一个函数调用的参数时,避免你脱离当前逻辑而单独编写额外逻辑。
方便调用上下文的局部变量。
加强封装性,第2点的延伸,可以达到对变量的保护作用。
缺点:
内存浪费。这个内存浪费不仅仅因为它常驻内存,对闭包的使用不当会造成无效内存的产生。
前面对闭包做了一些简单的解释,最后再总结下,其实闭包没什么特别的,其特点是:
函数嵌套函数
函数内部可以访问到外部的变量或者对象
避免了垃圾回收
更多闭包有话说 - 大前端相关文章请关注PHP中文网!