この記事の内容は、js クロージャを (詳細に) 理解するのに役立ちます。必要な方は参考にしていただければ幸いです。
翻訳者: クロージャについてはよく議論されているので、クロージャを理解していなくても、JS について知っていると言うのは恥ずかしいですが、この記事を読んだとき、私の目は輝きました。また、クロージャについても理解することができ、クラスとプロトタイプ チェーンについてもある程度の知識が得られました。これは 2012 年の記事で、少し早いものですが、内容は非常に明確です。読者に新たな理解をもたらすことができれば幸いです。
クロージャは、JavaScript 言語のやや複雑で誤解されている機能です。簡単に言えば、クロージャはメソッド (関数) と、メソッドが作成されたときの環境への参照を含むオブジェクトです。クロージャを完全に理解するには、js の 2 つの機能も理解する必要があります。1 つはファーストクラス関数、もう 1 つは内部関数です。
第一級関数
js では、メソッドは他のデータ型に簡単に変換できるため、第一級市民です。たとえば、第 1 レベルのメソッドをオンザフライで構築し、変数に割り当てることができます。他のメソッドに渡したり、他のメソッドを通じて返すこともできます。これらの基準を満たすことに加えて、メソッドには独自のプロパティとメソッドもあります。
次の例を通じて、第 1 レベルのメソッドの機能を見てみましょう。
var foo = function() { alert("Hello World!"); }; var bar = function(arg) { return arg; }; bar(foo)();
内部メソッド/内部関数
内部メソッドまたはネストされたメソッドは、外部メソッドが呼び出されるたびに、内部メソッドのインスタンスが作成されます。次の例は、内部メソッドの使用を反映しています。add メソッドは外部メソッドであり、doAdd は内部メソッドです。
function add(value1, value2) { function doAdd(operand1, operand2) { return operand1 + operand2; } return doAdd(value1, value2); } var foo = add(1, 2); // foo equals 3
この例の重要な特徴は、内部メソッドが外部メソッドのスコープを取得することです。これは、内部メソッドが外部メソッドの変数、パラメーターなどを使用できることを意味します。この例では、add() のパラメーター value1 および value2 が doAdd() の operand1 および operand2 パラメーターに渡されます。ただし、doAdd は value1 と value2 を直接取得できるため、これは必須ではありません。したがって、上の例を次のように書くこともできます。
function add(value1, value2) { function doAdd() { return value1 + value2; } return doAdd(); } var foo = add(1, 2); // foo equals 3
クロージャの作成
内部メソッドは外部メソッドのスコープを取得し、クロージャを形成します。典型的なシナリオは、外部関数が内部メソッドを返し、外部環境への参照を保持し、スコープ内のすべての変数を保存するというものです。
次の例は、クロージャを作成して使用する方法を示しています。
function add(value1) { return function doAdd(value2) { return value1 + value2; }; } var increment = add(1); var foo = increment(2); // foo equals 3
説明:
add は内部メソッド doAdd を返し、doAdd は add のパラメータを呼び出し、クロージャが作成されます。
value1 は add メソッドのローカル変数であり、doAdd の非ローカル変数です (非ローカル変数とは、変数が関数本体にも関数本体にも存在しないことを意味します)グローバル世界)、value2 は doAdd のローカル変数です。
add(1) が呼び出されると、クロージャが作成され、クロージャの参照環境に格納されます。value1 は 1 にバインドされ、バインドされた 1 は と等価です。この機能の「ブロッキング」が「クロージャ」の名前の由来でもあります。
increment(2) が呼び出されると、クロージャ関数に入ります。つまり、value1 が 1 である doAdd が呼び出されるため、クロージャは本質的に次の関数とみなすことができます。
function increment(value2) { return 1 + value2; }
#クロージャを使用するのはどのような場合ですか?
クロージャは多くの機能を実現できます。たとえば、コールバック関数を指定されたパラメータにバインドします。あなたの生活と成長を容易にする 2 つのシナリオについて話しましょう。<!DOCTYPE html> <html lang="en"> <head> <title>Closures</title> <meta charset="UTF-8" /> <script> window.addEventListener("load", function() { window.setInterval(showMessage, 1000, "some message<br />"); }); function showMessage(message) { document.getElementById("message").innerHTML += message; } </script> </head> <body> <span id="message"></span> </body> </html>
window.addEventListener("load", function() { var showMessage = getClosure("some message<br />"); window.setInterval(showMessage, 1000); }); function getClosure(message) { function showMessage() { document.getElementById("message").innerHTML += message; } return showMessage; }
ほとんどのオブジェクト指向プログラミング言語は、オブジェクトのプライベート プロパティをサポートしています。ただし、js は純粋なオブジェクト指向言語ではないため、プライベート プロパティの概念はありません。ただし、クロージャを通じてプライベート プロパティをシミュレートできます。クロージャには、クロージャが作成された環境への参照が含まれていることに注意してください。この参照は現在のスコープには含まれていないため、この参照は本質的にはプライベート プロパティです。
次の例を見てください (翻訳者: コードのテキスト説明は省略します):
function Person(name) { this._name = name; this.getName = function() { return this._name; }; }
var person = new Person("Colin"); person._name = "Tom"; // person.getName() now returns "Tom"
没有人愿意不经同意就被别人改名字,为了阻止这种情况的发生,通过闭包让_name字段变成私有。看如下代码,注意这里的_name是Person构造器的本地变量,而不是对象的属性,闭包形成了,因为外层方法Person对外暴露了一个内部方法getName。
function Person(name) { var _name = name;// 注:区别在这里 this.getName = function() { return _name; }; }
现在,当getName被调用,能够保证返回的是最初传入类构造器的值。我们依然可以为对象添加新的_name属性,但这并不影响闭包getName最初绑定的值,下面的代码证明,_name字段,事实私有。
var person = new Person("Colin"); person._name = "Tom"; // person._name is "Tom" but person.getName() returns "Colin"
什么时候不要用闭包?
正确理解闭包如何工作何时使用非常重要,而理解什么时候不应该用它也同样重要。过度使用闭包会导致脚本执行变慢并消耗额外内存。由于闭包太容易创建了,所以很容易发生你都不知道怎么回事,就已经创建了闭包的情况。本节我们说几种场景要注意避免闭包的产生。
1.循环中
循环中创建出闭包会导致结果异常。下例中,页面上有三个按钮,分别点击弹出不同的话术。然而实际运行,所有的按钮都弹出button4的话术,这是因为,当按钮被点击时,循环已经执行完毕,而循环中的变量i也已经变成了最终值4.
<!DOCTYPE html> <html lang="en"> <head> <title>Closures</title> <meta charset="UTF-8" /> <script> window.addEventListener("load", function() { for (var i = 1; i < 4; i++) { var button = document.getElementById("button" + i); button.addEventListener("click", function() { alert("Clicked button " + i); }); } }); </script> </head> <body> <input type="button" id="button1" value="One" /> <input type="button" id="button2" value="Two" /> <input type="button" id="button3" value="Three" /> </body> </html>
去解决这个问题,必须在循环中去掉闭包(译者:这里的闭包指的是click事件回调函数绑定了外层引用i),我们可以通过调用一个引用新环境的函数来解决。下面的代码中,循环中的变量传递给getHandler函数,getHandler返回一个闭包(译者:这个闭包指的是getHandler返回的内部方法绑定传入的i参数),独立于原来的for循环。
function getHandler(i) { return function handler() { alert("Clicked button " + i); }; } window.addEventListener("load", function() { for (var i = 1; i < 4; i++) { var button = document.getElementById("button" + i); button.addEventListener("click", getHandler(i)); } });
2.构造函数里的非必要使用
类的构造函数里,也是经常会产生闭包的错误使用。我们已经知道如何通过闭包设置类的私有属性,而如果当一个方法不需要调用私有属性,则造成的闭包是浪费的。下面的例子中,Person类增加了sayHello方法,但是它没有使用私有属性。
function Person(name) { var _name = name; this.getName = function() { return _name; }; this.sayHello = function() { alert("Hello!"); }; }
每当Person被实例化,创建sayHello都要消耗时间,想象一下有大量的Person被实例化。更好的实践是将sayHello放入Person的原型链里(prototype),原型链里的方法,会被所有的实例化对象共享,因此节省了为每个实例化对象去创建一个闭包(译者:指sayHello),所以我们有必要做如下修改:
function Person(name) { var _name = name; this.getName = function() { return _name; }; } Person.prototype.sayHello = function() { alert("Hello!"); };
需要记得一些事情
闭包包含了一个方法,以及创建它的代码环境引用
闭包会在外部函数包含内部函数的情况下形成
闭包可以轻松的帮助回调函数传入参数
类的私有属性可以通过闭包模拟
类的构造器中使用闭包不是一个好主意,将它们放到原型链中
以上がjs クロージャをさらに理解することができます (詳細)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。