자바스크립트 클로저란 정확히 무엇인가요?
1년 넘게 JavaScript를 사용해왔는데 클로저가 항상 혼란스럽습니다. 나는 클로저에 대한 지식을 차례로 접했고 클로저에 대한 이해가 부족하여 몇 가지 실수를 저질렀지만 최근에는 여전히 잘 이해되지 않습니다. 우연히 부록 A의 JavaScript 클로저에 대한 소개를 보다가 이해하기 쉽고 간단해서 부처님께 공양하기 위해 꽃을 빌려 정리했습니다.
1. 정의
클로저: 다른 함수의 범위에 있는 변수에 접근할 수 있는 함수를 말합니다. 클로저를 생성하는 일반적인 방법은 다른 함수 내에 함수를 생성하는 것입니다.
예시 바로가기
function a(){ var i=0; function b(){ alert(++i); } return b; } var c = a(); c();
이 코드에는 두 가지 특징이 있습니다.
1) 함수 b는 함수 a 안에 중첩되어 있습니다.
2) 함수 a는 함수 b를 반환합니다.
이런 식으로 var c=a()를 실행한 후 변수 c는 실제로 함수 b를 가리킵니다. c()를 다시 실행하면 i 값을 표시하는 창이 팝업됩니다(처음은 1입니다). 이 코드는 실제로 클로저를 생성합니다. 이유는 무엇입니까? 함수 a 외부의 변수 c는 함수 a 내의 함수 b를 참조하기 때문에 다음과 같습니다.
함수 a 내부의 함수 b가 함수 a 외부의 변수에 의해 참조되면 클로저가 생성됩니다.
클로저의 기능을 모르기 때문에 아직 클로저를 이해하지 못하는 것 같습니다.
2. 클로저의 기능은 무엇인가요?
간단히 말하면, 클로저의 기능은 a가 실행되고 반환된 후 a의 내부 함수 b의 실행이 a에 의존해야 하기 때문에 클로저가 Javascript의 가비지 수집 메커니즘 GC가 a가 점유한 리소스를 회수하는 것을 방지한다는 것입니다. 의 변수 이는 클로저의 역할에 대한 매우 간단한 설명입니다. 전문적이거나 엄격하지는 않지만 일반적인 의미는 클로저를 이해하는 데에는 단계별 프로세스가 필요하다는 것입니다.
위의 예에서 클로저의 존재로 인해 a의 i는 항상 함수 a가 반환된 후에 존재하므로 c()가 실행될 때마다 i는 1을 더한 후 경고되는 i 값이 됩니다.
그럼 a가 b가 아닌 다른 것을 반환한다면 상황은 완전히 달라집니다. 왜냐하면 a가 실행된 후 b는 a의 외부 세계로 반환되지 않고 a에 의해서만 참조되기 때문입니다. 이때 a는 b에 의해서만 참조됩니다. 따라서 함수 a와 b는 서로를 참조하지만 방해받지 않습니다. 외부 세계에서 참조됨), 함수 a 및 b는 GC에 의해 재활용됩니다. (Javascript의 가비지 수집 메커니즘은 나중에 자세히 소개하겠습니다.)
3. 클로저 속 미시세계
클로저와 함수 a와 중첩 함수 b 사이의 관계를 더 깊이 이해하려면 함수 실행 컨텍스트(실행 컨텍스트), 활성 객체(호출 객체), 범위(범위) 등 몇 가지 다른 개념을 도입해야 합니다. ) ), 범위 체인. 이러한 개념을 설명하기 위해 정의부터 실행까지 함수 a의 프로세스를 예로 들어 보겠습니다.
1) 함수 a를 정의할 때 js 인터프리터는 함수 a의 범위 체인을 a가 정의된 "환경"으로 설정합니다. a가 전역 함수인 경우 범위 체인에는 창 개체만 있습니다. .
2) 함수 a가 실행되면 a는 해당 실행 컨텍스트로 들어갑니다.
3) 실행 환경을 생성하는 과정에서 먼저 a, 즉 a의 범위에 범위 속성이 추가되며 해당 값은 1단계의 범위 체인입니다. 즉, a.scope=a의 범위 체인입니다.
4) 그런 다음 실행 환경은 활성 개체(호출 개체)를 생성합니다. 활성 개체도 속성은 있지만 프로토타입이 없고 JavaScript 코드에서 직접 액세스할 수 없는 개체입니다. 활성 개체를 만든 후 범위 체인의 맨 위에 활성 개체를 추가합니다. 이때 a의 범위 체인에는 a의 활성 개체와 창 개체라는 두 개체가 포함됩니다.
5) 다음 단계는 함수 a를 호출할 때 전달된 매개변수를 저장하는 활성 객체에 인수 속성을 추가하는 것입니다.
6) 마지막으로 함수 a의 모든 형식 매개변수와 내부 함수 b에 대한 참조를 a의 활성 개체에 추가합니다. 이번 단계에서는 함수 b의 정의가 완료되므로 3단계와 마찬가지로 함수 b의 스코프 체인은 b가 정의된 환경, 즉 a의 스코프에 설정된다.
이제 전체 함수 a의 정의부터 실행까지의 단계가 완료됩니다. 이때 a는 함수 b의 참조를 c에 반환하고 함수 b의 범위 체인에는 함수 a의 활성 개체에 대한 참조가 포함되어 있습니다. 이는 b가 a에 정의된 모든 변수와 함수에 액세스할 수 있음을 의미합니다. 함수 b는 c에 의해 참조되고 함수 b는 함수 a에 의존하므로 함수 a는 반환 후 GC에 의해 재활용되지 않습니다.
b 함수가 실행되면 위의 단계와 동일합니다. 따라서 실행 중 b의 범위 체인에는 b의 활성 개체, a의 활성 개체 및 창 개체의 세 가지 개체가 포함됩니다. 함수 b의 변수에 액세스할 때 검색 순서는 해당 개체가 존재하는 경우 먼저 검색하는 것입니다. 존재하지 않으면 함수 a의 활성 개체를 찾을 때까지 계속 검색합니다. 전체 범위 체인에서 찾을 수 없으면 정의되지 않은 값이 반환됩니다. 함수 b에 프로토타입 프로토타입 객체가 있는 경우 먼저 자신의 활성 객체를 검색한 후 자신의 프로토타입 객체를 검색한 다음 계속 검색합니다. 이것은 Javascript의 변수 조회 메커니즘입니다.
4. 폐쇄 적용 시나리오
1) 함수 내 변수의 보안을 지켜주세요. 초기 예를 들어, 함수 a의 i는 함수 b를 통해서만 접근할 수 있고 다른 수단으로는 접근할 수 없으므로 i의 보안이 보호됩니다.
2) 메모리에 변수를 유지합니다. 이전 예제와 마찬가지로 클로저로 인해 함수 a의 i는 항상 메모리에 존재하므로 c()가 실행될 때마다 i는 1씩 증가합니다.
위의 두 가지 사항은 클로저의 가장 기본적인 적용 시나리오이며 많은 고전적인 사례가 여기에서 유래합니다.
5. 자바스크립트 가비지 수집 메커니즘
Javascript에서 객체가 더 이상 참조되지 않으면 해당 객체는 GC에 의해 재활용됩니다. 두 객체가 서로를 참조하고 더 이상 제3자가 참조하지 않는 경우, 서로를 참조하는 두 객체도 재활용됩니다. 함수 a는 b에 의해 참조되고 b는 a 외부의 c에 의해 참조되기 때문에 함수 a는 실행 후 재활용되지 않습니다.
在javascript中没有块级作用域,一般为了给某个函数申明一些只有该函数才能使用的局部变量时,我们就会用到闭包,这样我们可以很大程度上减少全局作用域中的变量,净化全局作用域。
使用闭包有如上的好处,当然这样的好处是需要付出代价的,代价就是内存的占用。
如何理解上面的那句话呢?
每个函数的执行,都会创建一个与该函数相关的函数执行环境,或者说是函数执行上下文。这个执行上下文中有一个属性 scope chain(作用域链指针),这个指针指向一个作用域链结构,作用域链中的指针又都指向各个作用域对应的活动对象。正常情况,一个函数在调用开始执行时创建这个函数执行上下文及相应的作用域链,在函数执行结束后释放函数执行上下文及相应作用域链所占的空间。
//声明函数 function test(){ var str = "hello world"; console.log(str); } //调用函数 test();
在调用函数的时候会在内存中生成如下图的结构:
但是闭包的情况就有点特殊了,由于闭包函数可以访问外层函数中的变量,所以外层函数在执行结束后,其作用域活动对象并不会被释放(注意,外层函数执行结束后执行环境和对应的作用域链就会被销毁),而是被闭包函数的作用域链所引用,直到闭包函数被销毁后,外层函数的作用域活动对象才会被销毁。这也正是闭包要占用内存的原因。
所以使用闭包有好处,也有坏处,滥用闭包会造成内存的大量消耗。
使用闭包还有其他的副作用,可以说是bug,也可以说不是,相对不同的业务可能就会有不同的看法。
这个副作用是闭包函数只能取到外层函数变量的最终值。
测试代码如下:(这里使用了jquery对象)
/*闭包缺陷*/ (function($){ var result = new Array(), i = 0; for(;i<10;i++){ result[i] = function(){ return i; }; } $.RES1 = result; })(jQuery); // 执行数组中的函数 $.RES1[0]();
上面的代码先通过匿名函数表达式开辟了一块私有作用域,这个匿名函数就是我们上面所说的外层函数,该外层函数有一个参数$,同时还定义了变量result和 I , 通过for循环给数组result赋值一个匿名函数,这个匿名函数就是闭包,他访问了外层函数的变量I , 理论上数组resulti 会返回相应的数组下标值,实际情况却不如所愿。
如上代码 $.RES10 的执行结果是10.
为什么会这样呢,因为i的最终值就是10.
下面我们通过下图来详细说明下,上面的那段代码执行时在内存中到底发生了什么:
那么这个副作用有没有办法可以修复呢?当然可以!
我们可以通过下面的代码来达到我们的预期。
/*修复闭包缺陷*/ (function($){ var result = new Array(), i = 0; for(;i<10;i++){ result[i] = function(num){ return function(){ return num; } }(i); } $.RES2 = result; })(jQuery); //调用闭包函数 console.log($.RES2[0]());
上面的代码又在内存中发生了什么?我们同样用下面的一幅图来详细解释。看懂了上面的图,我们也就不难理解下面的图。
6.简单的例子
首先从一个经典错误谈起,页面上有若干个div, 我们想给它们绑定一个onclick方法,于是有了下面的代码
<div id="divTest"> <span>0</span> <span>1</span> <span>2</span> <span>3</span> </div> <div id="divTest2"> <span>0</span> <span>1</span> <span>2</span> <span>3</span> </div> $(document).ready(function() { var spans = $("#divTest span"); for (var i = 0; i < spans.length; i++) { spans[i].onclick = function() { alert(i); } } });
很简单的功能可是却偏偏出错了,每次alert出的值都是4,简单的修改就好使了
var spans2 = $("#divTest2 span"); $(document).ready(function() { for (var i = 0; i < spans2.length; i++) { (function(num) { spans2[i].onclick = function() { alert(num); } })(i); } });
7.内部函数
让我们从一些基础的知识谈起,首先了解一下内部函数。内部函数就是定义在另一个函数中的函数。例如:
function outerFn () { functioninnerFn () {} }
innerFn就是一个被包在outerFn作用域中的内部函数。这意味着,在outerFn内部调用innerFn是有效的,而在outerFn外部调用innerFn则是无效的。下面代码会导致一个JavaScript错误:
function outerFn() { document.write("Outer function<br/>"); function innerFn() { document.write("Inner function<br/>"); } } innerFn();//Uncaught ReferenceError: innerFn is not defined
不过在outerFn内部调用innerFn,则可以成功运行:
function outerFn() { document.write("Outer function<br/>"); function innerFn() { document.write("Inner function<br/>"); } innerFn(); } outerFn();
8、伟大的逃脱(内部函数如何逃脱外部函数)
JavaScript允许开发人员像传递任何类型的数据一样传递函数,也就是说,JavaScript中的内部函数能够逃脱定义他们的外部函数。
逃脱的方式有很多种,例如可以将内部函数指定给一个全局变量:
//定义全局变量逃脱 var globalVar; function outerFn() { document.write("Outer function<br/>"); function innerFn() { document.write("Inner function<br/>"); } globalVar = innerFn; } outerFn(); //Outer function Inner function globalVar(); //Outer function Inner function innerFn(); //ReferenceError: innerFn is not defined
调用outerFn时会修改全局变量globalVar,这时候它的引用变为innerFn,此后调用globalVar和调用innerFn一样。这时在outerFn外部直接调用innerFn仍然会导致错误,这是因为内部函数虽然通过把引用保存在全局变量中实现了逃脱,但这个函数的名字依然只存在于outerFn的作用域中。
也可以通过在父函数的返回值来获得内部函数引用
function outerFn() { document.write("Outer function<br/>"); function innerFn() { document.write("Inner function<br/>"); } return innerFn; } var fnRef = outerFn(); fnRef();
这里并没有在outerFn内部修改全局变量,而是从outerFn中返回了一个对innerFn的引用。通过调用outerFn能够获得这个引用,而且这个引用可以可以保存在变量中。
这种即使离开函数作用域的情况下仍然能够通过引用调用内部函数的事实,意味着只要存在调用内部函数的可能,JavaScript就需要保留被引用的函数。而且JavaScript运行时需要跟踪引用这个内部函数的所有变量,直到最后一个变量废弃,JavaScript的垃圾收集器才能释放相应的内存空间(红色部分是理解闭包的关键)。
说了半天总算和闭包有关系了,闭包是指有权限访问另一个函数作用域的变量的函数,创建闭包的常见方式就是在一个函数内部创建另一个函数,就是我们上面说的内部函数,所以刚才说的不是废话,也是闭包相关的 ^_^
9、变量的作用域
内部函数也可以有自己的变量,这些变量都被限制在内部函数的作用域中:
function outerFn() { document.write("Outer function<br/>"); function innerFn() { var innerVar = 0; innerVar++; document.write("Inner function\t"); document.write("innerVar = "+innerVar+"<br/>"); } return innerFn; } var fnRef = outerFn(); fnRef(); fnRef(); var fnRef2 = outerFn(); fnRef2(); fnRef2();
每当通过引用或其它方式调用这个内部函数时,就会创建一个新的innerVar变量,然后加1,最后显示
Outer function Inner function innerVar = 1 Inner function innerVar = 1 Outer function Inner function innerVar = 1 Inner function innerVar = 1
内部函数也可以像其他函数一样引用全局变量:
var globalVar = 0; function outerFn() { document.write("Outer function<br/>"); function innerFn() { globalVar++; document.write("Inner function\t"); document.write("globalVar = " + globalVar + "<br/>"); } return innerFn; } var fnRef = outerFn(); fnRef(); fnRef(); var fnRef2 = outerFn(); fnRef2(); fnRef2();
现在每次调用内部函数都会持续地递增这个全局变量的值:
Outer function Inner function globalVar = 1 Inner function globalVar = 2 Outer function Inner function globalVar = 3 Inner function globalVar = 4
但是如果这个变量是父函数的局部变量又会怎样呢?因为内部函数会引用到父函数的作用域(有兴趣可以了解一下作用域链和活动对象的知识),内部函数也可以引用到这些变量
function outerFn() { var outerVar = 0; document.write("Outer function<br/>"); function innerFn() { outerVar++; document.write("Inner function\t"); document.write("outerVar = " + outerVar + "<br/>"); } return innerFn; } var fnRef = outerFn(); fnRef(); fnRef(); var fnRef2 = outerFn(); fnRef2(); fnRef2();
这一次结果非常有意思,也许或出乎我们的意料
Outer function Inner function outerVar = 1 Inner function outerVar = 2 Outer function Inner function outerVar = 1 Inner function outerVar = 2
我们看到的是前面两种情况合成的效果,通过每个引用调用innerFn都会独立的递增outerVar。也就是说第二次调用outerFn没有继续沿用outerVar的值,而是在第二次函数调用的作用域创建并绑定了一个一个新的outerVar实例,两个计数器完全无关。
当内部函数在定义它的作用域的外部被引用时,就创建了该内部函数的一个闭包。这种情况下我们称既不是内部函数局部变量,也不是其参数的变量为自由变量,称外部函数的调用环境为封闭闭包的环境。从本质上讲,如果内部函数引用了位于外部函数中的变量,相当于授权该变量能够被延迟使用。因此,当外部函数调用完成后,这些变量的内存不会被释放(最后的值会保存),闭包仍然需要使用它们。
10.闭包之间的交互
当存在多个内部函数时,很可能出现意料之外的闭包。我们定义一个递增函数,这个函数的增量为2
function outerFn() { var outerVar = 0; document.write("Outer function<br/>"); function innerFn1() { outerVar++; document.write("Inner function 1\t"); document.write("outerVar = " + outerVar + "<br/>"); } function innerFn2() { outerVar += 2; document.write("Inner function 2\t"); document.write("outerVar = " + outerVar + "<br/>"); } return { "fn1": innerFn1, "fn2": innerFn2 }; } var fnRef = outerFn(); fnRef.fn1(); fnRef.fn2(); fnRef.fn1(); var fnRef2 = outerFn(); fnRef2.fn1(); fnRef2.fn2(); fnRef2.fn1();
我们映射返回两个内部函数的引用,可以通过返回的引用调用任一个内部函数,结果:
Outer function Inner function 1 outerVar = 1 Inner function 2 outerVar = 3 Inner function 1 outerVar = 4 Outer function Inner function 1 outerVar = 1 Inner function 2 outerVar = 3 Inner function 1 outerVar = 4
innerFn1和innerFn2引用了同一个局部变量,因此他们共享一个封闭环境。当innerFn1为outerVar递增一时,久违innerFn2设置了outerVar的新的起点值,反之亦然。我们也看到对outerFn的后续调用还会创建这些闭包的新实例,同时也会创建新的封闭环境,本质上是创建了一个新对象,自由变量就是这个对象的实例变量,而闭包就是这个对象的实例方法,而且这些变量也是私有的,因为不能在封装它们的作用域外部直接引用这些变量,从而确保了了面向对象数据的专有性。
11.解惑
现在我们可以回头看看开头写的例子就很容易明白为什么第一种写法每次都会alert 4了。
for (var i = 0; i < spans.length; i++) { spans[i].onclick = function() { alert(i); } }
위 코드는 페이지가 로드된 후 실행됩니다. i 값이 4이면 판단 조건이 성립되지 않고 for 루프가 실행됩니다. 왜냐하면 각 범위의 onclick 메서드는 에서 내부 함수이기 때문입니다. 이번에는 i가 닫힙니다. 참조(클로저 참조가 참조를 전달함), 메모리는 제거될 수 없으며 i의 값은 4로 유지되며 프로그램이 이를 변경하거나 모든 onclick 기능이 제거될 때까지 재활용되지 않습니다(활성 할당) 함수를 null로 설정하거나 페이지가 언로드됩니다). 이런 식으로, 범위를 클릭할 때마다 onclick 함수는 i 값(범위 체인은 참조 방법임)을 조회하고, 이 값이 4와 같으면 경고합니다.
두 번째 방법은 즉시 실행되는 함수를 사용하여 클로저 레이어를 만드는 것입니다. 함수 선언은 괄호 안에 넣어서 표현식이 됩니다. 이때, 매개변수 i가 호출됩니다. 이 전달되면 함수는 즉시 실행되고 num은 매번 i의 값을 저장합니다.
이 글을 읽고 나면 누구나 저처럼 클로저에 대해 어느 정도 이해하고 있을 것입니다. 물론 클로저를 완전히 이해하려면 함수의 실행 환경과 범위 체인을 이해해야 합니다.