>웹 프론트엔드 >JS 튜토리얼 >js의 클로저에 대한 자세한 소개

js의 클로저에 대한 자세한 소개

零下一度
零下一度원래의
2017-06-29 11:35:441199검색

1. 폐쇄란 무엇인가요?

클로저에 대한 몇 가지 정의를 살펴보겠습니다.

  1. 클로저는 다른 함수의 범위에 있는 변수에 액세스할 수 있는 권한이 있는 함수를 나타냅니다. - "JS 고급 프로그래밍 제3판" p178

  2. Function Objects 스코프 체인을 통해 연관될 수 있으며, 함수 본문 내부의 변수를 함수 스코프에 저장할 수 있는 기능을 '클로저'라고 합니다. -- "JS 최종 가이드" p183

  3. 내부 함수는 자신이 정의된 외부 함수의 매개변수와 변수에 액세스할 수 있습니다(인수 제외). ). -- "JS Language Essence" p36thisarguments)。 --《JS语言精粹》 p36

来个定义总结

  1. 可以访问外部函数作用域中变量的函数

  2. 被内部函数访问的外部函数的变量可以保存在外部函数作用域内而不被回收---这是核心,后面我们遇到闭包都要想到,我们要重点关注被闭包引用的这个变量。

来创建个简单的闭包

var sayName = function(){var name = 'jozo';return function(){
        alert(name);
    }
};var say = sayName(); 
say();

来解读后面两个语句:

  • var say = sayName() :返回了一个匿名的内部函数保存在变量say中,并且引用了外部函数的变量name,由于垃圾回收机制,sayName函数执行完毕后,变量name并没有被销毁。

  • say() :执行返回的内部函数,依然能访问变量name,输出 'jozo' .

2. 闭包中的作用域链

理解作用域链对理解闭包也很有帮助。

变量在作用域中的查找方式应该都很熟悉了,其实这就是顺着作用域链往上查找的。

当函数被调用时:

  1. 先创建一个执行环境(execution context),及相应的作用域链;

  2. 将arguments和其他命名参数的值添加到函数的活动对象(activation object)

作用域链:当前函数的活动对象优先级最高,外部函数的活动对象次之,外部函数的外部函数的活动对象依次递减,直至作用域链的末端--全局作用域。优先级就是变量查找的先后顺序;

先来看个普通的作用域链:

function sayName(name){return name;
}var say = sayName('jozo');

这段代码包含两个作用域:a.全局作用域;b.

정의를 요약해보자

함수는 외부 함수 범위의 변수에 접근할 수 있다

는 다음과 같이 접근된다. 내부 함수 외부 함수의 변수는 재활용되지 않고 외부 함수의 범위에 저장될 수 있습니다. 이것이 핵심입니다. 나중에 클로저가 참조하는 변수에 집중해야 합니다. 폐쇄.

간단한 클로저 생성

function sayName(name){return function(){return name;
    }
}var say = sayName('jozo');

다음 두 문 해석:

var say = sayName() code>: say 변수에 저장된 익명의 내부 함수를 반환하며, 외부 함수의 변수 이름을 참조합니다. 가비지 수집 메커니즘으로 인해 sayName 함수가 실행된 후에 변수 이름이 삭제되지 않습니다.

say(): 반환된 내부 함수를 실행하면 여전히 변수 이름에 액세스하고 'jozo'를 출력할 수 있습니다.

2. 범위 체인을 이해하는 것은 클로저를 이해하는 데에도 도움이 됩니다.

스코프에서 변수를 검색하는 방식에 익숙해야 합니다. 실제로 이는 스코프 체인을 검색하는 것입니다.

함수 호출 시:

먼저 실행 컨텍스트와 해당 범위 체인을 만듭니다. 🎜🎜🎜🎜인수 값과 기타 명명된 매개 변수를 함수의 활성 객체에 추가합니다(활성화 객체). )🎜🎜🎜스코프 체인: 현재 함수의 활성 개체가 가장 높은 우선순위를 갖고, 그 다음이 외부 함수의 활성 개체이며, 외부 함수의 활성 개체는 다음이 될 때까지 순차적으로 감소합니다. 범위 체인의 끝-- 전역 범위. 우선순위는 변수가 검색되는 순서입니다. 🎜🎜🎜먼저 일반적인 범위 체인을 살펴보겠습니다. 🎜🎜
// 方式1var a = 0;var add = function(){
    a++;
    console.log(a)
}add();add();//方式2 :闭包var add = (function(){
    var  a = 0;
    return function(){
        a++;
        console.log(a);
    }
})();
console.log(a); //undefinedadd();add();

相比之下方式2更加优雅,也减少全局变量,将变量私有化
🎜이 코드에는 두 가지 범위가 포함되어 있습니다: a.전역 범위; /code>sayName 함수의 범위, 즉 변수 개체가 2개만 있습니다. 해당 실행 환경이 실행되면 변수 개체가 활성 개체가 되어 실행 환경 범위 체인의 프런트 엔드로 푸시됩니다. 즉, 우선순위가 가장 높은 것입니다. 그림을 보면서 이야기해볼까요? 🎜🎜🎜🎜🎜🎜🎜이 그림은 JS 고급 프로그래밍 책에도 나오는데, 제가 다시 그렸습니다. 🎜🎜sayName() 함수를 생성할 때 변수 객체를 미리 포함하는 스코프 체인이 생성되는데, 이는 그림의 인덱스 1인 스코프 체인이며, 호출되면 내부 [[Scope]] 속성에 저장됩니다. sayName() 함수를 호출하면 실행 환경이 생성되고, 그 후 함수의 [[Scope]] 속성에 객체를 복사하여 활성 도메인 체인이 구성됩니다. )이 생성되어 실행 환경 범위 체인 앞으로 푸시됩니다. 🎜🎜일반적으로 함수 실행이 완료되면 로컬 활성 개체가 소멸되고 전역 범위만 메모리에 저장됩니다. 그러나 클로저의 상황은 다릅니다: 🎜🎜🎜 클로저의 범위 체인을 살펴보겠습니다: 🎜🎜
 var oli = document.getElementsByTagName(&#39;li&#39;); var i; for(i = 0;i < 5;i++){
     oli[i].onclick = function(){
         alert(i);
     }
 } console.log(i); // 5 //执行匿名函数
 (function(){
    alert(i);  //5
 }());
🎜이 클로저 인스턴스에는 이전 예제보다 익명 함수 범위가 하나 더 있습니다: 🎜🎜🎜🎜 🎜🎜🎜After the 익명 함수는 sayName() 함수에서 반환되며 해당 범위 체인은 sayName() 함수를 포함하는 활성 개체 및 전역 변수 개체로 초기화됩니다. 이러한 방식으로 익명 함수는 sayName()에 정의된 모든 변수와 매개변수에 액세스할 수 있습니다. 더 중요한 것은 sayName() 함수가 실행된 후 이 활성 개체의 범위 체인으로 인해 해당 활성 개체가 삭제되지 않는다는 것입니다. 즉, sayName() 함수가 실행된 후 해당 실행 환경의 범위 체인은 삭제되지만 해당 활성 개체는 익명 함수가 삭제될 때까지 메모리에 남아 있습니다. 이는 나중에 다루게 될 메모리 누수 문제이기도 합니다. 🎜🎜스코프 체인 문제에 대해서는 별로 쓰지 않겠습니다. o(╯□╰)o🎜🎜3. 클로저의 예🎜🎜🎜예 1: 누적 구현🎜🎜
     var oli = document.getElementsByTagName(&#39;li&#39;);     var i;     for(i = 0;i < 5;i++){
         oli[i].onclick = (function(num){             var a = num; // 为了说明问题             return function(){
                 alert(a);
             }
         })(i)
     }     console.log(i); // 5
🎜🎜 예제 2 : 각 li🎜🎜
//将全部li字体变为红色
(function(){    var els = document.getElementsByTagName(&#39;li&#39;);for(var i = 0,lng = els.length;i < lng;i++){
        els[i].style.color = &#39;red&#39;;
    }    
})();
🎜에 클릭 이벤트를 추가합니다. 위의 내용은 실행 결과가 5라는 것을 모두 알고 있으며 클로저를 사용하여 이 문제를 해결할 수 있다는 것도 알고 있습니다. 왜 매번 나타나는지 이해할 수 없었습니다. 모두 5개인데 왜 이 문제를 해결할 수 있습니까? 약간의 검색 끝에 마침내 알아냈습니다. 🎜

a. 先来分析没用闭包前的情况:for循环中,我们给每个li点击事件绑定了一个匿名函数,匿名函数中返回了变量i的值,当循环结束后,变量i的值变为5,此时我们再去点击每个li,也就是执行相应的匿名函数(看上面的代码),这是变量i已经是5了,所以每个点击弹出5. 因为这里返回的每个匿名函数都是引用了同一个变量i,如果我们新建一个变量保存循环执行时当前的i的值,然后再让匿名函数应用这个变量,最后再返回这个匿名函数,这样就可以达到我们的目的了,这就是运用闭包来实现的!

b. 再来分析下运用闭包时的情况:

     var oli = document.getElementsByTagName(&#39;li&#39;);     var i;     for(i = 0;i < 5;i++){
         oli[i].onclick = (function(num){             var a = num; // 为了说明问题             return function(){
                 alert(a);
             }
         })(i)
     }     console.log(i); // 5

这里for循环执行时,给点击事件绑定的匿名函数传递i后立即执行返回一个内部的匿名函数,因为参数是按值传递的,所以此时形参num保存的就是当前i的值,然后赋值给局部变量 a,然后这个内部的匿名函数一直保存着a的引用,也就是一直保存着当前i的值。 所以循环执行完毕后点击每个li,返回的匿名函数执行弹出各自保存的 a 的引用的值。

4. 闭包的运用

我们来看看闭包的用途。事实上,通过使用闭包,我们可以做很多事情。比如模拟面向对象的代码风格;更优雅,更简洁的表达出代码;在某些方面提升代码的执行效率。

1. 匿名自执行函数

我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,比如UI的初始化,那么我们可以使用闭包:

//将全部li字体变为红色
(function(){    var els = document.getElementsByTagName(&#39;li&#39;);for(var i = 0,lng = els.length;i < lng;i++){
        els[i].style.color = &#39;red&#39;;
    }    
})();

我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,
因此els,i,lng这些局部变量在执行完后很快就会被释放,节省内存!
关键是这种机制不会污染全局对象。

2. 实现封装/模块化代码

var person= function(){    //变量作用域为函数内部,外部无法访问    var name = "default";       return {    
       getName : function(){    
           return name;    
       },    
       setName : function(newName){    
           name = newName;    
       }    
    }    
}();console.log(person.name);//直接访问,结果为undefined    
console.log(person.getName());  //default 
person.setName("jozo");    
console.log(person.getName());  //jozo

3. 实现面向对象中的对象
这样不同的对象(类的实例)拥有独立的成员及状态,互不干涉。虽然JavaScript中没有类这样的机制,但是通过使用闭包,
我们可以模拟出这样的机制。还是以上边的例子来讲:

function Person(){    var name = "default";       return {    
       getName : function(){    
           return name;    
       },    
       setName : function(newName){    
           name = newName;    
       }    
    }    
};    


var person1= Person();    
print(person1.getName());    
john.setName("person1");    
print(person1.getName());  // person1  

var person2= Person();    
print(person2.getName());    
jack.setName("erson2");    
print(erson2.getName());  //person2

Person的两个实例person1 和 person2 互不干扰!因为这两个实例对name这个成员的访问是独立的 。

5. 内存泄露及解决方案

垃圾回收机制

说到内存管理,自然离不开JS中的垃圾回收机制,有两种策略来实现垃圾回收:标记清除 和 引用计数;

标记清除:垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记,然后,它会去掉环境中的变量的标记和被环境中的变量引用的变量的标记,此后,如果变量再被标记则表示此变量准备被删除。 2008年为止,IE,Firefox,opera,chrome,Safari的javascript都用使用了该方式;

引用计数:跟踪记录每个值被引用的次数,当声明一个变量并将一个引用类型的值赋给该变量时,这个值的引用次数就是1,如果这个值再被赋值给另一个变量,则引用次数加1。相反,如果一个变量脱离了该值的引用,则该值引用次数减1,当次数为0时,就会等待垃圾收集器的回收。

这个方式存在一个比较大的问题就是循环引用,就是说A对象包含一个指向B的指针,对象B也包含一个指向A的引用。 这就可能造成大量内存得不到回收(内存泄露),因为它们的引用次数永远不可能是 0 。早期的IE版本里(ie4-ie6)采用是计数的垃圾回收机制,闭包导致内存泄露的一个原因就是这个算法的一个缺陷。

我们知道,IE中有一部分对象并不是原生额javascript对象,例如,BOM和DOM中的对象就是以COM对象的形式实现的,而COM对象的垃圾回收机制采用的就是引用计数。因此,虽然IE的javascript引擎采用的是标记清除策略,但是访问COM对象依然是基于引用计数的,因此只要在IE中设计COM对象就会存在循环引用的问题!

举个栗子:

window.onload = function(){var el = document.getElementById("id");
    el.onclick = function(){
        alert(el.id);
    }
}

这段代码为什么会造成内存泄露?

el.onclick= function () {
    alert(el.id);
};

执行这段代码的时候,将匿名函数对象赋值给el的onclick属性;然后匿名函数内部又引用了el对象,存在循环引用,所以不能被回收;

解决方法:

window.onload = function(){var el = document.getElementById("id");var id = el.id; //解除循环引用
    el.onclick = function(){
        alert(id); 
    }
    el = null; // 将闭包引用的外部函数中活动对象清除
}

6. 总结闭包的优缺点

优点:

  • 可以让一个变量常驻内存 (如果用的多了就成了缺点

  • 避免全局变量的污染

  • 私有化变量

缺点

  • 클로저는 자신을 포함하는 함수의 범위를 전달하므로 다른 함수보다 더 많은 메모리를 차지합니다

  • 메모리 누수를 유발합니다

위 내용은 js의 클로저에 대한 자세한 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.