>  기사  >  웹 프론트엔드  >  JavaScript 클로저(요약 공유)에 대해 이야기해 보겠습니다.

JavaScript 클로저(요약 공유)에 대해 이야기해 보겠습니다.

WBOY
WBOY앞으로
2022-02-17 18:37:581773검색

이 기사는 스택 관점에서 클로저를 살펴보는 것, 클로저 공유 변수 문제 및 기타 관련 문제를 포함하여 JavaScript의 클로저에 대한 관련 지식을 제공합니다. 모든 사람에게 도움이 되기를 바랍니다.

JavaScript 클로저(요약 공유)에 대해 이야기해 보겠습니다.

1. 클로저 자체 폐쇄

클로저의 개념:

함수 실행 후 반환되는 결과는 내부 함수이며, 내부 함수가 실행된 함수를 보유하는 경우 외부 변수에 의해 참조됩니다. 범위는 클로저를 형성합니다. 외부 함수 범위는 내부 함수에서 액세스할 수 있습니다.

클로저를 사용하면 함수의 변수를 읽고 함수의 변수를 메모리에 저장하여 변수가 오염되는 것을 방지할 수 있습니다. 클로저는 함수에 변수 값을 메모리에 저장하고 메모리를 소비하기 때문에 클로저를 남용할 수 없습니다. 그렇지 않으면 웹 성능에 영향을 미치고 메모리 누수를 일으킬 수 있습니다. 클로저를 사용할 필요가 없을 때 메모리를 제때 해제하기 위해 내부 함수 객체의 변수에 null을 할당할 수 있습니다.

클로저의 특성: 외부 함수에 의해 생성된 여러 클로저 메모리 공간은 서로 독립적입니다.

클로저 애플리케이션 시나리오:

  1. 메모리의 변수 유지: 데이터가 캐시된 경우 커링
  2. 반복자 및 생성기와 같은 함수 내 변수의 안전을 보호합니다.

단점: 닫으면 원래 범위 체인이 해제되지 않아 메모리 누수가 발생합니다.

  1. 메모리 소비는 부정적인 영향을 미칩니다. 내부 함수는 외부 변수에 대한 참조를 저장하므로 가비지 수집이 불가능하고 메모리 사용량이 늘어납니다. 따라서 을 잘못 사용하면 메모리 누수
  2. 가 발생하고 처리 속도에 부정적인 영향을 미칩니다. 클로저 수준에 따라 검색 시 참조된 외부 변수의 범위 체인 길이가 결정됩니다. 외부 함수의 범위는 내부 함수에서 액세스할 수 있으며, 액세스된 변수는 메모리에 영구적으로 상주하며 나중에 사용할 수 있습니다.
  3. 변수가 전역 세계를 오염시키지 않도록 변수를 독립 범위에 저장하세요. , 비공개 멤버로 존재
2. 클로저 개념

함수는 주변 상태(어휘 환경, 어휘 환경)에 대한 참조와 함께 번들로 제공됩니다(또는 함수가 참조로 둘러싸여 있음). 이러한 조합은 클로저입니다. . 즉, 클로저를 사용하면 내부 함수 내에서 외부 함수의 범위에 액세스할 수 있습니다. JavaScript에서는 함수가 생성될 때마다 함수 생성과 동시에 클로저가 생성됩니다.
  1. 어휘 범위
  2. 아래 코드를 살펴보세요.
    function init() {
        var name = "Mozilla"; // name 是一个被 init 创建的局部变量
        function displayName() { // displayName() 是内部函数,一个闭包
            alert(name); // 使用了父函数中声明的变量
        }
        displayName();
    }
    init();
    init()는 지역 변수 이름과 displayName()이라는 함수를 생성합니다. displayName()은 init()에 정의된 내부 함수이며 init() 함수 본문 내에서만 사용할 수 있습니다. displayName()에는 자체 지역 변수가 없습니다. 그러나 외부 함수의 변수에 액세스할 수 있으므로 displayName()은 상위 함수 init()에 선언된 변수 이름을 사용할 수 있습니다.
  3. 이 JSFiddle 링크를 사용하여 코드를 실행한 후 우리는 displayName() 함수 내의 Alert() 문이 변수 이름의 값을 성공적으로 표시했음을 발견했습니다(변수는 상위 함수에서 선언되었습니다). 이 어휘 범위 지정 예제에서는 함수가 중첩될 때 파서가 변수 이름을 확인하는 방법을 설명합니다. 어휘라는 용어는 어휘 범위가 변수가 선언된 소스 코드의 위치에 따라 변수가 사용 가능한 위치를 결정한다는 사실을 나타냅니다. 중첩 함수는 외부 범위에 선언된 변수에 액세스할 수 있습니다.
  4. 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()

스택은 선입 후출 데이터 구조입니다.
  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"

当程序执行完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();

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

  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;
}

我们的文本尺寸调整按钮可以修改 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);

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>

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

编程语言中,比如 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 。

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

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


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();
//一、将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();

7.性能考量

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

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

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


8.闭包注意事项

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

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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 csdn.net에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제