ホームページ > 記事 > ウェブフロントエンド > JavaScript クロージャーについて話しましょう (概要の共有)
この記事では、スタックの観点からクロージャを表示すること、クロージャの共有変数の問題、その他の関連問題など、JavaScript のクロージャに関する関連知識を提供します。
クロージャの概念:
関数の後に返される結果は、実行される関数は、内部関数が外部変数によって参照されます。内部関数が実行される関数のスコープ内に変数を保持している場合、クロージャが形成されます。外部関数スコープには内部関数からアクセスできます。
クロージャを使用すると、関数内の変数を読み取り、関数内の変数をメモリに保存して、変数が汚染されないように保護できます。クロージャは関数の変数値をメモリに格納しメモリを消費するため、クロージャを悪用することはできません。悪用しないと、Web パフォーマンスに影響を与え、メモリ リークが発生します。クロージャを使用する必要がない場合は、適切なタイミングでメモリを解放するために、内部関数オブジェクトの変数に null を割り当てることができます。
クロージャ機能: 外部関数によって生成された複数のクロージャ メモリ空間は互いに独立しています。
クロージャ アプリケーション シナリオ:
欠点: 閉じると元のスコープ チェーンが解放されなくなり、メモリ リークが発生します。
次のコードを見てください。
function init() { var name = "Mozilla"; // name 是一个被 init 创建的局部变量 function displayName() { // displayName() 是内部函数,一个闭包 alert(name); // 使用了父函数中声明的变量 } displayName(); } init();
init() は、ローカル変数名と、displayName() という名前の変数を作成します。関数。 displayName() は init() で定義された内部関数であり、init() 関数本体内でのみ使用できます。 displayName() には独自のローカル変数がないことに注意してください。ただし、displayName() は外部関数の変数にアクセスできるため、親関数 init() で宣言された変数名を使用できます。
この JSFiddle リンクを使用してコードを実行した後、displayName() 関数内のalert() ステートメントが変数名の値を正常に表示したことがわかりました (変数は親関数で宣言されています)。この字句スコープの例では、関数がネストされている場合にパーサーが変数名を解決する方法を説明します。 「字句」という用語は、ソース コード内の変数が宣言されている場所に基づいて、変数が使用できる場所が字句スコープによって決定されるという事実を指します。ネストされた関数は、外側のスコープで宣言された変数にアクセスできます。
3. スタックの観点からクロージャを表示するvar a = 1 //a是一个基本数据类型 var b = {m: 20 } //b是一个对象
対応するメモリ ストレージ:
b={m:30} を実行すると、ヒープ内に新しいオブジェクト {m:30} が存在します。メモリ 、スタック メモリ内の b は新しい空間アドレス ({m: 30} を指す) を指し、ヒープ メモリ内の元の {m: 20} はプログラム エンジンによってガベージ コレクションされ、メモリ領域が節約されます。 js 関数もオブジェクトであり、ヒープ メモリとスタック メモリに格納されることはわかっています。変換を見てみましょう:
var a = 1; function fn(){ var b = 2 function fn1(){ console.log(b) } fn1() } fn()
スタックは先入れ後出しのデータ構造です:
在函数内访问某个变量是根据函数作用域链来判断变量是否存在的,而函数作用域链是程序根据函数所在的执行环境栈来初始化的,所以上面的例子,我们在fn1里面打印变量b,根据fn1的作用域链的找到对应fn执行环境下的变量b。所以当程序在调用某个函数时,做了一下的工作:准备执行环境,初始函数作用域链和arguments参数对象
我们现在看下闭包例子
function outer() { var a = '变量1' var inner = function () { console.info(a) } return inner // inner 就是一个闭包函数,因为他能够访问到outer函数的作用域 } var inner = outer() // 获得inner闭包函数 inner() //"变量1"
当程序执行完var inner = outer(),其实outer的执行环境并没有被销毁,因为他里面的变量a仍然被被inner的函数作用域链所引用,当程序执行完inner(), 这时候,inner和outer的执行环境才会被销毁调;《JavaScript高级编程》书中建议:由于闭包会携带包含它的函数的作用域,因为会比其他函数占用更多内容,过度使用闭包,会导致内存占用过多。
下面通过outer外函数和inner内函数来讲解闭包的共享变量问题。
同一个外函数生成的多个闭包是独立空间还是共享空间如何判断?请先看实例
//第一种情况 调用时给外函数传入变量值 function outer(name){ return function(){ console.log(name) } } f1 = outer('yang') f2 = outer('fang') console.log(f1.toString()) f1() //yang f2() //fang f1() //yang //第二种情况:外函数局部变量值为变化 function count() { var arr = []; for (var i=1; i<=3; i++) { arr.push(function () { return i * i; }); } return arr; } var results = count(); var f1 = results[0]; //16 var f2 = results[1]; //16 var f3 = results[2]; //16 console.log(f1 ) //第三种情况:外函数的局部变量值变化。 function test(){ var i = 0; return function(){ console.log(i++) } }; var a = test(); var b = test(); //依次执行a,a,b,控制台会输出什么呢?0 1 0 //b为什么不是2 a();a();b();
同一个外函数生成的多个闭包是独立空间还是共享空间如何判断?
可以总结出记住三个闭包共享变量的原则
简记:调用一次外函数,生成一个独立的闭包环境;外函数内部生成多个内函数,那么多个内函数共用一个闭包环境。
应用场景主要就两个
场景一:保存局部变量在内存中
闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。
因此,通常你使用只有一个方法的对象的地方,都可以使用闭包。
在 Web 中,你想要这样做的情况特别常见。大部分我们所写的 JavaScript 代码都是基于事件的 — 定义某种行为,然后将其添加到用户触发的事件之上(比如点击或者按键)。我们的代码通常作为回调:为响应事件而执行的函数。
假如,我们想在页面上添加一些可以调整字号的按钮。一种方法是以像素为单位指定 body 元素的 font-size,然后通过相对的 em 单位设置页面中其它元素(例如header)的字号:
body { font-family: Helvetica, Arial, sans-serif; font-size: 12px; } h1 { font-size: 1.5em; } h2 { font-size: 1.2em; }
我们的文本尺寸调整按钮可以修改 body 元素的 font-size 属性,由于我们使用相对单位,页面中的其它元素也会相应地调整。
以下是 JavaScript:
function makeSizer(size) { return function() { document.body.style.fontSize = size + 'px'; }; } var size12 = makeSizer(12); var size14 = makeSizer(14); var size16 = makeSizer(16);
size12,size14 和 size16 三个函数将分别把 body 文本调整为 12,14,16 像素。我们可以将它们分别添加到按钮的点击事件上。如下所示:
document.getElementById('size-12').onclick = size12; document.getElementById('size-14').onclick = size14; document.getElementById('size-16').onclick = size16; <a href="#" id="size-12">12</a> <a href="#" id="size-14">14</a> <a href="#" id="size-16">16</a>
场景二:用闭包模拟私有方法,保护局部变量
编程语言中,比如 Java,是支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。
而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。
下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为 模块模式(module pattern):
var Counter = (function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } })(); console.log(Counter.value()); /* logs 0 */ Counter.increment(); Counter.increment(); console.log(Counter.value()); /* logs 2 */ Counter.decrement(); console.log(Counter.value()); /* logs 1 */
可以将上面的代码拆分成两部分:(function(){}) 和 () 。第1个() 是一个表达式,而这个表达式本身是一个匿名函数,所以在这个表达式后面加 () 就表示执行这个匿名函数。
在之前的示例中,每个闭包都有它自己的词法环境;而这次我们只创建了一个词法环境,为三个函数所共享:Counter.increment,Counter.decrement 和 Counter.value。
该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数 返回的三个公共函数访问。
这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。
你应该注意到我们定义了一个匿名函数,用于创建一个计数器。我们立即执行了这个匿名函数,并将他的值赋给了变量Counter。我们可以把这个函数储存在另外一个变量makeCounter中,并用他来创建多个计数器。 var makeCounter = function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } }; var Counter1 = makeCounter(); var Counter2 = makeCounter(); console.log(Counter1.value()); /* logs 0 */ Counter1.increment(); Counter1.increment(); console.log(Counter1.value()); /* logs 2 */ Counter1.decrement(); console.log(Counter1.value()); /* logs 1 */ console.log(Counter2.value()); /* logs 0 */
请注意两个计数器 Counter1 和 Counter2 是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 privateCounter 。
每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。
以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。
在 ECMAScript 2015 引入 let 关键字 之前,在循环中有一个常见的闭包创建错误。参考下面的示例:
<p id="help">Helpful notes will appear here</p> <p>E-mail: <input type="text" id="email" name="email"></p> <p>Name: <input type="text" id="name" name="name"></p> <p>Age: <input type="text" id="age" name="age"></p> function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } } setupHelp();
//一、将function直接返回,会发生闭包 //二、将函数赋值给一个变量,此变量函数外部使用,此时也是闭包。比如,数组、多个变量等。 举例下面也是闭包情况。 var arr = [] for (var i = 0; i < 10; i++) { arr[i] = function(){console.log(i)} } arr[6]()此时也是闭包,将十个匿名函数+i组成了一个闭包返回。
数组 helpText 中定义了三个有用的提示信息,每一个都关联于对应的文档中的input 的 ID。通过循环这三项定义,依次为相应input添加了一个 onfocus 事件处理函数,以便显示帮助信息。
运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个input上,显示的都是关于年龄的信息。
原因是赋值给 onfocus 的是闭包。这些闭包是由他们的函数定义和在 setupHelp 作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item。这是因为变量item使用var进行声明,由于变量提升,所以具有函数作用域。当onfocus的回调执行时,item.help的值被决定。由于循环在事件触发之前早已执行完毕,变量对象item(被三个闭包所共享)已经指向了helpText的最后一项。
解决这个问题的一种方案是使用更多的闭包:特别是使用前面所述的函数工厂:
function showHelp(help) { document.getElementById('help').innerHTML = help; } function makeHelpCallback(help) { return function() { showHelp(help); }; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = makeHelpCallback(item.help); } } setupHelp();
这段代码可以如我们所期望的那样工作。所有的回调不再共享同一个环境, makeHelpCallback 函数为每一个回调创建一个新的词法环境。在这些环境中,help 指向 helpText 数组中对应的字符串。
另一种方法使用了匿名闭包:
function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { (function() { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } })(); // 马上把当前循环项的item与事件回调相关联起来 } } setupHelp();
如果不想使用过多的闭包,你可以用ES2015引入的let关键词:
function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { let item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } } setupHelp();
这个例子使用let而不是var,因此每个闭包都绑定了块作用域的变量,这意味着不再需要额外的闭包。
另一个可选方案是使用 forEach()来遍历helpText数组,如下所示:
function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; helpText.forEach(function(text) { document.getElementById(text.id).onfocus = function() { showHelp(text.help); } }); } setupHelp();
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
但是如果某个函数需要不停新建,那么使用闭包保存到内存中对性能有好处。
释放闭包只需要将引用闭包的函数置为null即可。
第一:多个内函数引用同一局部变量
function outer() { var result = []; for (var i = 0; i<10; i++){ result.[i] = function () { console.info(i) } } return result }
看样子result每个闭包函数对打印对应数字,1,2,3,4,...,10, 实际不是,因为每个闭包函数访问变量i是outer执行环境下的变量i,随着循环的结束,i已经变成10了,所以执行每个闭包函数,结果打印10, 10, ..., 10
怎么解决这个问题呢?
function outer() { var result = []; for (var i = 0; i<10; i++){ result.[i] = function (num) { return function() { console.info(num); // 此时访问的num,是上层函数执行环境的num,数组有10个函数对象,每个对象的执行环境下的number都不一样 } }(i) } return result }
第二: this指向问题
var object = { name: ''object", getName: function() { return function() { console.info(this.name) } } } object.getName()() // underfined // 因为里面的闭包函数是在window作用域下执行的,也就是说,this指向windows
第三:内存泄露问题
function showId() { var el = document.getElementById("app") el.onclick = function(){ aler(el.id) // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放 } } // 改成下面 function showId() { var el = document.getElementById("app") var id = el.id el.onclick = function(){ aler(id) // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放 } el = null // 主动释放el }
相关推荐:javascript学习教程
以上がJavaScript クロージャーについて話しましょう (概要の共有)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。