클로저란 무엇인가요
클로저(Closure)란 무엇인가요? 클로저(Closure)는 정적 언어에는 없는 새로운 기능입니다. 하지만 클로저는 이해하기에는 너무 복잡한 것이 아닙니다. 간단히 말해서 클로저는
입니다.클로저는 함수의 지역 변수 모음이지만 이러한 지역 변수는 함수가 반환된 후에도 계속 존재합니다.
클로저는 함수가 반환된 후에도 해제되지 않는 함수의 "스택"입니다. 또한 이러한 함수 스택은 스택에 할당되지 않고 힙에 할당된다는 것도 이해할 수 있습니다. will 클로저 생성 위의 두 번째 정의는 첫 번째 정의에 대한 보충으로, 첫 번째 정의의 주어, 술어 및 객체를 추출합니다. 클로저는 함수의 '지역 변수' 집합입니다. 단지 함수가 반환된 후에 이 지역 변수에 액세스할 수 있다는 것뿐입니다. (공식적인 정의는 아니지만 클로저를 이해하는 데 이 정의가 더 도움이 될 것입니다.)
Javascript의 클로저를 이해하는 것이 매우 중요합니다. 이 글에서는 가장 간단한 예를 들어 이 개념을 이해하려고 합니다.
function greet(sth){ return function(name){ console.log(sth + ' ' + name); } } //hi darren greet('hi')('darren');
또는 다음과 같이 쓸 수도 있습니다.
var sayHi = greet('hi'); sayHi('darren');
우리가 묻고 싶은 질문은: Greeting의 내부 함수가 sth 변수를 사용할 수 있는 이유는 무엇입니까?
내부 동작은 대략 다음과 같습니다.
→ 글로벌 컨텍스트 생성
→ var sayHi = Greeting('hi'); 문을 실행하여 Greeting 컨텍스트를 생성하고, Greeting 컨텍스트에 변수 sth가 저장됩니다.
→ Greeting 함수의 문장을 계속 실행하여 익명 함수를 반환합니다. Greeting 컨텍스트가 스택에서 사라지더라도 sth 변수는 메모리의 특정 공간에 여전히 존재합니다.
→ 계속 실행 sayHi('darren'); sayHi 컨텍스트를 생성하고 sth 변수를 검색하려고 시도하지만 sayHi 컨텍스트에 sth 변수가 없습니다. sayHi 컨텍스트는 범위 체인을 따라 sth 변수에 해당하는 메모리를 찾습니다. 외부 함수는 클로저와 같으며 내부 함수는 외부 함수의 변수를 사용할 수 있습니다.
간단한 폐쇄 예시
function buildFunctions(){ var funcArr = []; for(var i = 0; i < 3; i++){ funcArr.push(function(){console.log(i)}); } return funcArr; } var fs = buildFunctions(); fs[0](); //3 fs[1](); //3 fs[2](); //3
위 결과는 왜 0, 1, 2가 아닌가?
--i는 클로저 변수 역할을 하기 때문에 현재 값은 3이고 내부 함수에서 사용됩니다. 원하는 효과를 얻으려면 순회 중에 각 순회에 대한 독립적인 컨텍스트를 생성하여 클로저의 영향을 받지 않도록 할 수 있습니다. 자체 트리거 기능은 독립적인 컨텍스트를 구현할 수 있습니다.
function buildFunctions(){ var funcArr = []; for(var i = 0; i < 3; i++){ funcArr.push((function(j){ return function(){ console.log(j); }; }(i))); } return funcArr; } var fs = buildFunctions(); fs[0](); //0 fs[1](); //1 fs[2](); //2
이 글의 두 가지 예는 클로저의 두 가지 측면을 정확하게 반영합니다. 하나는 내부 함수가 클로저 변수를 사용한다는 것이고, 다른 하나는 내부 함수가 클로저의 영향을 받지 않도록 자체 트리거링 함수로 작성되었다는 것입니다.
로컬 변수이므로 함수 내의 코드로 액세스할 수 있습니다. 이는 정적 언어와 다르지 않습니다. 클로저와의 차이점은 함수 실행이 끝난 후에도 함수 외부의 코드를 통해 지역 변수에 계속 액세스할 수 있다는 것입니다. 이는 함수가 클로저를 가리키는 "참조"를 반환하거나 이 "참조"를 외부 변수에 할당하여 클로저의 지역 변수가 외부 코드에서 액세스될 수 있도록 해야 함을 의미합니다. 물론, 이 참조를 포함하는 엔터티는 객체여야 합니다. 왜냐하면 Javascript에서는 기본 유형을 제외한 모든 것이 객체이기 때문입니다. 안타깝게도 ECMAScript는 클로저의 지역 변수에 액세스하기 위한 관련 멤버와 메서드를 제공하지 않습니다. 하지만 ECMAScript에서는 함수 객체에 정의된 내부 함수()는 외부 함수에 직접 접근할 수 있는 로컬 변수입니다. 이 메커니즘을 통해 다음과 같은 방법으로 클로저에 대한 접근을 완료할 수 있습니다.
function greeting(name) { var text = 'Hello ' + name; // local variable // 每次调用时,产生闭包,并返回内部函数对象给调用者 return function () { alert(text); } } var sayHello=greeting( "Closure" ); sayHello() // 通过闭包访问到了局部变量text
上述代码的执行结果是:Hello Closure,因为sayHello()函数在greeting函数执行完毕后,仍然可以访问到了定义在其之内的局部变量text。
好了,这个就是传说中闭包的效果,闭包在Javascript中有多种应用场景和模式,比如Singleton,Power Constructor等这些Javascript模式都离不开对闭包的使用。
ECMAScript闭包模型
ECMAScript到底是如何实现闭包的呢?想深入了解的亲们可以获取ECMAScript 规范进行研究,我这里也只做一个简单的讲解,内容也是来自于网络。
在ECMAscript的脚本的函数运行时,每个函数关联都有一个执行上下文场景(Execution Context) ,这个执行上下文场景中包含三个部分
文法环境(The LexicalEnvironment)
变量环境(The VariableEnvironment)
this绑定
其中第三点this绑定与闭包无关,不在本文中讨论。文法环境中用于解析函数执行过程使用到的变量标识符。我们可以将文法环境想象成一个对象,该对 象包含了两个重要组件,环境记录(Enviroment Recode),和外部引用(指针)。环境记录包含包含了函数内部声明的局部变量和参数变量,外部引用指向了外部函数对象的上下文执行场景。全局的上下文 场景中此引用值为NULL。这样的数据结构就构成了一个单向的链表,每个引用都指向外层的上下文场景。
例如上面我们例子的闭包模型应该是这样,sayHello函数在最下层,上层是函数greeting,最外层是全局场景。如下图:
因此当sayHello被调用的时候,sayHello会通过上下文场景找到局部变量text的值,因此在屏幕的对话框中显示出”Hello Closure”
变量环境(The VariableEnvironment)和文法环境的作用基本相似,具体的区别请参看ECMAScript的规范文档。
闭包的样列
前面的我大致了解了Javascript闭包是什么,闭包在Javascript是怎么实现的。下面我们通过针对一些例子来帮助大家更加深入的理解闭包,下面共有5个样例,例子来自于JavaScript Closures For Dummies(镜像)。
例子1:闭包中局部变量是引用而非拷贝
function say667() { // Local variable that ends up within closure var num = 666; var sayAlert = function() { alert(num); } num++; return sayAlert; } var sayAlert = say667(); sayAlert()
因此执行结果应该弹出的667而非666。
例子2:多个函数绑定同一个闭包,因为他们定义在同一个函数内。
function setupSomeGlobals() { // Local variable that ends up within closure var num = 666; // Store some references to functions as global variables gAlertNumber = function() { alert(num); } gIncreaseNumber = function() { num++; } gSetNumber = function(x) { num = x; } } setupSomeGolbals(); // 为三个全局变量赋值 gAlertNumber(); //666 gIncreaseNumber(); gAlertNumber(); // 667 gSetNumber(12);// gAlertNumber();//12
例子3:当在一个循环中赋值函数时,这些函数将绑定同样的闭包
function buildList(list) { var result = []; for (var i = 0; i < list.length; i++) { var item = 'item' + list[i]; result.push( function() {alert(item + ' ' + list[i])} ); } return result; } function testList() { var fnlist = buildList([1,2,3]); // using j only to help prevent confusion - could use i for (var j = 0; j < fnlist.length; j++) { fnlist[j](); } }
testList的执行结果是弹出item3 undefined窗口三次,因为这三个函数绑定了同一个闭包,而且item的值为最后计算的结果,但是当i跳出循环时i值为4,所以list[4]的结果为undefined.
例子4:外部函数所有局部变量都在闭包内,即使这个变量声明在内部函数定义之后。
function sayAlice() { var sayAlert = function() { alert(alice); } // Local variable that ends up within closure var alice = 'Hello Alice'; return sayAlert; } var helloAlice=sayAlice(); helloAlice();
执行结果是弹出”Hello Alice”的窗口。即使局部变量声明在函数sayAlert之后,局部变量仍然可以被访问到。
例子5:每次函数调用的时候创建一个新的闭包
function newClosure(someNum, someRef) { // Local variables that end up within closure var num = someNum; var anArray = [1,2,3]; var ref = someRef; return function(x) { num += x; anArray.push(num); alert('num: ' + num + '\nanArray ' + anArray.toString() + '\nref.someVar ' + ref.someVar); } } closure1=newClosure(40,{someVar:'closure 1'}); closure2=newClosure(1000,{someVar:'closure 2'}); closure1(5); // num:45 anArray[1,2,3,45] ref:'someVar closure1' closure2(-10);// num:990 anArray[1,2,3,990] ref:'someVar closure2'
闭包的应用
Singleton 单件:
var singleton = function () { var privateVariable; function privateFunction(x) { ...privateVariable... } return { firstMethod: function (a, b) { ...privateVariable... }, secondMethod: function (c) { ...privateFunction()... } }; }();
这个单件通过闭包来实现。通过闭包完成了私有的成员和方法的封装。匿名主函数返回一个对象。对象包含了两个方法,方法1可以方法私有变量,方法2访 问内部私有函数。需要注意的地方是匿名主函数结束的地方的'()',如果没有这个'()'就不能产生单件。因为匿名函数只能返回了唯一的对象,而且不能被 其他地方调用。这个就是利用闭包产生单件的方法。