인터넷에서 JavaScript 클로저에 대한 많은 정보를 확인해 보았는데, 대부분 매우 학술적이고 전문적인 내용이었습니다. 초보자의 경우 클로저에 대한 이해는커녕 텍스트 설명조차 이해하기 어렵습니다. 이 글을 쓰는 목적은 가장 널리 사용되는 단어를 사용하여 JavaScript 클로저의 진정한 모습을 드러내는 것입니다.
1. 폐쇄란 무엇인가요?
"공식적인" 설명은 다음과 같습니다. 클로저는 많은 변수와 이러한 변수에 바인딩된 환경을 포함하는 표현식(일반적으로 함수)이므로 이러한 변수도 표현식의 일부입니다. 나는 그의 설명이 너무 학문적이기 때문에 이 문장을 직접적으로 이해할 수 있는 사람은 거의 없다고 생각합니다. 사실, 일반인의 관점에서 이 문장은 다음을 의미합니다. JavaScript의 모든 함수는 클로저입니다. 그러나 일반적으로 말하면 중첩된 함수에 의해 생성된 클로저가 더 강력합니다. 이를 우리는 대부분 "클로저"라고 부릅니다. 다음 코드를 살펴보세요.
function a() { var i = 0; function b() { alert(++i); } return b; } var c = a(); c();
이 코드에는 두 가지 특징이 있습니다.
함수 b는 함수 a 내에 중첩되어 있습니다.
함수 a는 함수 b를 반환합니다.
이런 식으로 var c=a()를 실행하면 변수 c는 실제로 b 함수를 가리킵니다. c()를 실행하면 변수 i가 b에 사용됩니다. i.(처음은 1입니다.) 이 코드는 실제로 클로저를 생성합니다. 이유는 무엇입니까? 함수 a 외부의 변수 c는 함수 a 내의 함수 b를 참조하기 때문에, 즉 함수 a의 내부 함수 b가 함수 a 외부의 변수에 의해 참조될 때 소위 "클로저"가 생성됩니다.
더 철저하게 합시다. 소위 "클로저"는 생성자 본문에 있는 다른 함수를 대상 개체의 메서드 함수로 정의하고, 이 개체의 메서드 함수는 차례로 외부 함수 본문에 있는 임시 변수를 참조하는 것입니다. 이를 통해 대상 개체가 수명 동안 항상 메서드를 유지할 수 있는 한 원래 생성자 본문에서 사용된 임시 변수 값을 간접적으로 유지할 수 있습니다. 초기 생성자 호출이 종료되고 임시변수의 이름은 사라졌지만, 변수의 값은 항상 대상 객체의 메소드 내에서 참조할 수 있으며, 이 메소드를 통해서만 값에 접근할 수 있다. 동일한 생성자를 다시 호출하더라도 새 개체와 메서드만 생성되며 새 임시 변수는 마지막 호출과 독립적인 새 값에만 해당합니다.
클로저에 대해 더 깊이 이해하기 위해 클로저의 기능과 효과를 계속해서 살펴보겠습니다.
2. 폐쇄의 기능과 효과는 무엇인가요?
요컨대 클로저의 기능은 a가 실행되고 반환된 후 a의 내부 함수 b의 실행으로 인해 a가 점유한 리소스를 회수하는 Javascript의 가비지 수집 메커니즘 GC를 방지하는 것입니다. a의 변수에 의존합니다. 이것은 클로저의 역할에 대한 매우 간단한 설명입니다. 전문적이거나 엄격하지는 않지만 확실히 이해할 수 있습니다. 클로저를 이해하려면 단계별 프로세스가 필요합니다.
위의 예에서 클로저의 존재로 인해 a의 i는 함수 a가 반환된 후에 항상 존재하게 됩니다. 이런 식으로 c()가 실행될 때마다 i는 다음과 같은 i 값이 됩니다. 1을 추가한 후 경고를 받았습니다. .
그럼 a가 b가 아닌 다른 것을 반환한다면 상황은 완전히 달라집니다. 왜냐하면 a가 실행된 후 b는 a의 외부 세계로 반환되지 않고 a에 의해서만 참조되기 때문입니다. 이때 a는 b에 의해서만 참조됩니다. 따라서 함수 a와 b는 서로를 참조하지만 방해받지 않습니다. 외부 세계에서 참조됨), 함수 a 및 b는 GC에 의해 재활용됩니다.
3. 클로저의 미시적 세계
클로저와 함수 a와 중첩 함수 b의 관계를 더 깊이 이해하려면 몇 가지 다른 개념을 소개해야 합니다. 함수 환경(실행 컨텍스트), 활성 개체(호출 개체), 범위(scope), 범위 체인(scope chain). 이러한 개념을 설명하기 위해 정의부터 실행까지 함수 a의 프로세스를 예로 들어 보겠습니다.
함수 a를 정의할 때 js 인터프리터는 함수 a의 범위 체인을 a가 정의된 "환경"으로 설정합니다. a가 전역 함수인 경우 범위 체인에는 window 개체만 있습니다. 그것.
함수 a를 실행할 때 a는 해당 실행 컨텍스트로 들어갑니다.
실행 환경을 생성하는 과정에서 먼저 a, 즉 a의 범위에 범위 속성이 추가되며 해당 값은 1단계의 범위 체인입니다. 즉, a.scope=a의 범위 체인입니다.
그런 다음 실행 환경은 호출 개체를 생성합니다. 활성 객체도 속성은 있지만 프로토타입이 없고 JavaScript 코드에서 직접 액세스할 수 없는 객체입니다. 활성 개체를 만든 후 범위 체인의 맨 위에 활성 개체를 추가합니다. 이때 a의 범위 체인에는 a의 활성 개체와 창 개체라는 두 개체가 포함됩니다.
다음 단계는 함수 a를 호출할 때 전달된 매개변수를 보유하는 활성 객체에 인수 속성을 추가하는 것입니다.
最后把所有函数a的形参和内部的函数b的引用也添加到a的活动对象上。在这一步中,完成了函数b的的定义,因此如同第3步,函数b的作用域链被设置为b所被定义的环境,即a的作用域。
到此,整个函数a从定义到执行的步骤就完成了。此时a返回函数b的引用给c,又函数b的作用域链包含了对函数a的活动对象的引用,也就是说b可以访问到a中定义的所有变量和函数。函数b被c引用,函数b又依赖函数a,因此函数a在返回后不会被GC回收。
当函数b执行的时候亦会像以上步骤一样。因此,执行时b的作用域链包含了3个对象:b的活动对象、a的活动对象和window对象
当在函数b中访问一个变量的时候,搜索顺序是:
先搜索自身的活动对象,如果存在则返回,如果不存在将继续搜索函数a的活动对象,依次查找,直到找到为止。
如果函数b存在prototype原型对象,则在查找完自身的活动对象后先查找自身的原型对象,再继续查找。这就是Javascript中的变量查找机制。
如果整个作用域链上都无法找到,则返回undefined。
小结,本段中提到了两个重要的词语:函数的定义与执行。文中提到函数的作用域是在定义函数时候就已经确定,而不是在执行的时候确定(参看步骤1和3)。用一段代码来说明这个问题:
function f(x) { var g = function () { return x; } return g; } var h = f(1); alert(h());
这段代码中变量h指向了f中的那个匿名函数(由g返回)。
假设函数h的作用域是在执行alert(h())确定的,那么此时h的作用域链是:h的活动对象->alert的活动对象->window对象。
假设函数h的作用域是在定义时确定的,就是说h指向的那个匿名函数在定义的时候就已经确定了作用域。那么在执行的时候,h的作用域链为:h的活动对象->f的活动对象->window对象。
如果第一种假设成立,那输出值就是undefined;如果第二种假设成立,输出值则为1。运行结果证明了第2个假设是正确的,说明函数的作用域确实是在定义这个函数的时候就已经确定了。
4. 闭包的应用场景
保护函数内的变量安全。以最开始的例子为例,函数a中i只有函数b才能访问,而无法通过其他途径访问到,因此保护了i的安全性。
在内存中维持一个变量。依然如前例,由于闭包,函数a中i的一直存在于内存中,因此每次执行c(),都会给i自加1。
function Constructor(...) { var that = this; var membername = value; function membername(...) {...} }
以上3点是闭包最基本的应用场景,很多经典案例都源于此。
5. JavaScript的垃圾回收机制
在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数a被b引用,b又被a外的c引用,这就是为什么函数a执行后不会被回收的原因。
6. 结语
理解JavaScript的闭包是迈向高级JS程序员的必经之路,理解了其解释和运行机制才能写出更为安全和优雅的代码。