Heim > Web-Frontend > js-Tutorial > Hauptteil

Lassen Sie uns über JavaScript-Abschlüsse sprechen (Zusammenfassungsfreigabe)

WBOY
Freigeben: 2022-02-17 18:38:18
nach vorne
1852 Leute haben es durchsucht

Dieser Artikel vermittelt Ihnen relevantes Wissen über Abschlüsse in JavaScript, einschließlich der Betrachtung von Abschlüssen aus der Stack-Perspektive, Problemen mit gemeinsam genutzten Abschlussvariablen und anderen verwandten Themen. Ich hoffe, dass er für alle hilfreich ist.

Lassen Sie uns über JavaScript-Abschlüsse sprechen (Zusammenfassungsfreigabe)

1. Abschluss selbstschließend

Abschlusskonzept:

Das nach der Funktionsausführung zurückgegebene Ergebnis ist eine interne Funktion und wird von externen Variablen referenziert, wenn die interne Funktion die Variablen im Gültigkeitsbereich enthält einen Abschluss bilden. Auf den externen Funktionsumfang kann über interne Funktionen zugegriffen werden.

Mithilfe von Abschlüssen können Sie die Variablen in der Funktion lesen und die Variablen in der Funktion im Speicher speichern, um die Variablen vor Verschmutzung zu schützen. Da Schließungen Variablenwerte in Funktionen im Speicher speichern und Speicher verbrauchen, können Schließungen nicht missbraucht werden, da sie sonst die Webleistung beeinträchtigen und Speicherverluste verursachen. Wenn keine Schließungen erforderlich sind, können Sie den Variablen des inneren Funktionsobjekts Null zuweisen, um rechtzeitig Speicher freizugeben.

Eigenschaften von Abschlüssen: Mehrere durch eine externe Funktion generierte Abschlussspeicherplätze sind unabhängig voneinander.

Schließungsanwendungsszenarien:

  1. Variablen im Speicher behalten: bei zwischengespeicherten Daten, Currying
  2. Schützen Sie die Sicherheit von Variablen innerhalb von Funktionen: wie Iteratoren und Generatoren.

Nachteil: Das Schließen führt dazu, dass die ursprüngliche Scope-Kette nicht freigegeben wird, was zu Speicherverlusten führt.

  1. Der Speicherverbrauch wirkt sich negativ aus. Da interne Funktionen Verweise auf externe Variablen speichern, können sie nicht durch Müll gesammelt werden und erhöhen daher die Speichernutzung zu Speicherverlusten
  2. und wirken sich negativ auf die Verarbeitungsgeschwindigkeit aus. Die Abschlussebene bestimmt die Gültigkeitskettenlänge der referenzierten externen Variablen bei der Suche. Es ist möglich, unerwartete Werte zu erhalten Auf den Bereich der externen Funktion kann über die interne Funktion zugegriffen werden, und die Variablen, auf die zugegriffen wird, sind dauerhaft im Speicher gespeichert und können später verwendet werden.
  3. Vermeiden Sie, dass Variablen die globale Welt verschmutzen. Speichern Sie die Variablen im unabhängigen Bereich , als privates Mitglied vorhanden
  4. 2. AbschlusskonzeptEine Funktion wird mit einem Verweis auf ihren umgebenden Zustand (lexikalische Umgebung, lexikalische Umgebung) gebündelt (oder die Funktion ist von Verweisen umgeben), eine solche Kombination ist ein Abschluss . Mit anderen Worten: Abschlüsse ermöglichen den Zugriff auf den Umfang einer äußeren Funktion innerhalb einer inneren Funktion. In JavaScript wird bei jeder Erstellung einer Funktion gleichzeitig mit der Erstellung der Funktion ein Abschluss erstellt.
  5. Lexikalischer Geltungsbereich

Sehen Sie sich den Code unten an:
    function init() {
        var name = "Mozilla"; // name 是一个被 init 创建的局部变量
        function displayName() { // displayName() 是内部函数,一个闭包
            alert(name); // 使用了父函数中声明的变量
        }
        displayName();
    }
    init();
    Nach dem Login kopieren
  1. init() erstellt einen lokalen Variablennamen und eine Funktion namens displayName() . displayName() ist eine interne Funktion, die in init() definiert ist und nur innerhalb des init()-Funktionskörpers verfügbar ist. Beachten Sie, dass displayName() keine eigenen lokalen Variablen hat. Da displayName() jedoch Zugriff auf die Variablen der externen Funktion hat, kann es den in der übergeordneten Funktion init() deklarierten Variablennamen verwenden. Nachdem wir den Code über diesen JSFiddle-Link ausgeführt hatten, stellten wir fest, dass die Anweisung „alert()“ innerhalb der Funktion „displayName()“ den Wert des Variablennamens erfolgreich anzeigte (die Variable wurde in ihrer übergeordneten Funktion deklariert). Dieses Beispiel für die lexikalische Festlegung des Gültigkeitsbereichs beschreibt, wie der Parser Variablennamen auflöst, wenn Funktionen verschachtelt sind. Der Begriff „lexikalisch“ bezieht sich auf die Tatsache, dass der lexikalische Geltungsbereich bestimmt, wo eine Variable verfügbar ist, basierend auf der Stelle im Quellcode, an der sie deklariert ist. Verschachtelte Funktionen können auf Variablen zugreifen, die in ihrem äußeren Bereich deklariert sind.
  2. 3. Abschlüsse aus der Perspektive des Stapels anzeigen
  3. Die Werte von Variablen grundlegender Datentypen sind im Allgemeinen im Stapelspeicher vorhanden: Zahl, Boolesch, Undefiniert, Zeichenfolge, Null. ;Der Wert der Objekttypvariablen wird im Heapspeicher gespeichert, und der Stapelspeicher speichert die entsprechende Raumadresse.
  4. var a = 1 
    //a是一个基本数据类型 
    var b = {m: 20 } 
    //b是一个对象
    Nach dem Login kopieren
    Entsprechender Speicher:

Wenn wir b = {m:30} ausführen, befindet sich ein neues Objekt {m:30} im Heap-Speicher und b im Stapelspeicher zeigt auf die neue Raumadresse (zeigt auf {m: 30}), und der ursprüngliche {m: 20} im Heap-Speicher wird von der Programm-Engine als Müll gesammelt, wodurch Speicherplatz gespart wird. Wir wissen, dass js-Funktionen auch Objekte sind und auch im Heap- und Stack-Speicher gespeichert werden. Schauen wir uns die Transformation an:

  1. 在执行fn前,此时我们在全局执行环境(浏览器就是window作用域),全局作用域里有个变量a;
  2. 进入fn,此时栈内存就会push一个fn的执行环境,这个环境里有变量b和函数对象fn1,这里可以访问自身执行环境和全局执行环境所定义的变量
  3. 进入fn1,此时栈内存就会push 一个fn1的执行环境,这里面没有定义其他变量,但是我们可以访问到fn和全局执行环境里面的变量,因为程序在访问变量时,是向底层栈一个个找(这就是Javascript语言特有的"链式作用域"结构(chain scope),如果找到全局执行环境里都没有对应变量,则程序抛出underfined的错误。
  4. 随着fn1()执行完毕,fn1的执行环境被杯销毁,接着执行完fn(),fn的执行环境也会被销毁,只剩全局的执行环境下,现在没有b变量,和fn1函数对象了,只有a 和 fn(函数声明作用域是window下)

在函数内访问某个变量是根据函数作用域链来判断变量是否存在的,而函数作用域链是程序根据函数所在的执行环境栈来初始化的,所以上面的例子,我们在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"
Nach dem Login kopieren

当程序执行完var inner = outer(),其实outer的执行环境并没有被销毁,因为他里面的变量a仍然被被inner的函数作用域链所引用,当程序执行完inner(), 这时候,inner和outer的执行环境才会被销毁调;《JavaScript高级编程》书中建议:由于闭包会携带包含它的函数的作用域,因为会比其他函数占用更多内容,过度使用闭包,会导致内存占用过多。

4.闭包的共享变量问题

下面通过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();
Nach dem Login kopieren

同一个外函数生成的多个闭包是独立空间还是共享空间如何判断?

  1. 第一种情况说明多次调用外函数生成的不同闭包函数没有共享name变量
  2. 第二种情况说明外函数内部循环生成的多个内函数共享 i 局部变量
  3. 第三种情况说明a 、b为两个 不同闭包 函数,同一闭包函数 a 多次调用 共享 i 变量,a b之间不共享。

可以总结出记住三个闭包共享变量的原则

  1. 调用外函数,就会生成内函数和外函数的局部变量组成的闭包。每调用一次生成一个闭包函数。不同闭包函数之间内存空间彼此独立。
  2. 调用同一个闭包函数多次,共享内存空间,即外函数的局部变量值。
  3. 第二种情况for循环。没有调用外函数,只是将内函数存到了数组中,故并没有生成3个独立的闭包函数。而是3个内函数共享一个外函数局部变量,即3个内函数和外函数局部变量组成了一个整体的闭包环境。

简记:调用一次外函数,生成一个独立的闭包环境;外函数内部生成多个内函数,那么多个内函数共用一个闭包环境。


5.闭包应用场景

应用场景主要就两个

  • 在内存中维持变量:如果缓存数据、柯里化  
  • 保护函数内的变量安全:如迭代器、生成器。

场景一:保存局部变量在内存中

闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。

因此,通常你使用只有一个方法的对象的地方,都可以使用闭包。

在 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;
}
Nach dem Login kopieren

我们的文本尺寸调整按钮可以修改 body 元素的 font-size 属性,由于我们使用相对单位,页面中的其它元素也会相应地调整。

以下是 JavaScript:

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + &#39;px&#39;;
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
Nach dem Login kopieren

size12,size14 和 size16 三个函数将分别把 body 文本调整为 12,14,16 像素。我们可以将它们分别添加到按钮的点击事件上。如下所示:

document.getElementById(&#39;size-12&#39;).onclick = size12;
document.getElementById(&#39;size-14&#39;).onclick = size14;
document.getElementById(&#39;size-16&#39;).onclick = size16;
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>
Nach dem Login kopieren

场景二:用闭包模拟私有方法,保护局部变量

编程语言中,比如 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 */
Nach dem Login kopieren

可以将上面的代码拆分成两部分:(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 */
Nach dem Login kopieren

请注意两个计数器 Counter1 和 Counter2 是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 privateCounter 。

每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。

以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。


6.循环中创建闭包的一个常见错误

在 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();
Nach dem Login kopieren
//一、将function直接返回,会发生闭包
 //二、将函数赋值给一个变量,此变量函数外部使用,此时也是闭包。比如,数组、多个变量等。 举例下面也是闭包情况。
 var arr = []
 for (var i = 0; i < 10; i++) {
    arr[i] = function(){console.log(i)}
  }
  arr[6]()此时也是闭包,将十个匿名函数+i组成了一个闭包返回。
Nach dem Login kopieren

数组 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();
Nach dem Login kopieren

这段代码可以如我们所期望的那样工作。所有的回调不再共享同一个环境, 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();
Nach dem Login kopieren

如果不想使用过多的闭包,你可以用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();
Nach dem Login kopieren

这个例子使用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();
Nach dem Login kopieren

7.性能考量

如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。

但是如果某个函数需要不停新建,那么使用闭包保存到内存中对性能有好处。

释放闭包只需要将引用闭包的函数置为null即可。


8.闭包注意事项

第一:多个内函数引用同一局部变量

function outer() {
      var result = [];
      for (var i = 0; i<10; i++){
        result.[i] = function () {
            console.info(i)
        }
     }
     return result
}
Nach dem Login kopieren

看样子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
}
Nach dem Login kopieren

第二: this指向问题

var object = {
     name: ''object",
     getName: function() {
        return function() {
             console.info(this.name)
        }
    }
}
object.getName()()    // underfined
// 因为里面的闭包函数是在window作用域下执行的,也就是说,this指向windows
Nach dem Login kopieren

第三:内存泄露问题

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
}
Nach dem Login kopieren

相关推荐:javascript学习教程

Das obige ist der detaillierte Inhalt vonLassen Sie uns über JavaScript-Abschlüsse sprechen (Zusammenfassungsfreigabe). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Verwandte Etiketten:
Quelle:csdn.net
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage