>웹 프론트엔드 >JS 튜토리얼 >JavaScript 시리즈에 대한 심층적 이해 (16) Closures_javascript 기술

JavaScript 시리즈에 대한 심층적 이해 (16) Closures_javascript 기술

WBOY
WBOY원래의
2016-05-16 17:54:241176검색

소개
이 장에서는 JavaScript에서 자주 논의되는 주제인 클로저에 대해 소개하겠습니다. 사실, 모두가 이미 폐쇄에 관해 이야기했습니다. 그럼에도 불구하고 여기서는 이론적 관점에서 클로저에 대해 논의하고 ECMAScript의 클로저가 실제로 내부적으로 어떻게 작동하는지 살펴보겠습니다.

이전 기사에서도 언급했듯이 이 기사들은 일련의 기사이며 서로 연관되어 있습니다. 따라서 본 글에서 소개할 내용을 보다 잘 이해하기 위해서는 먼저 14장의 스코프 체인과 12장의 변수 객체를 읽어보는 것이 좋다.

영문 원문: http://dmitrysoshnikov.com/ecmascript/chapter-6-closures/
소개
ECMAScript 클로저에 대해 직접 논의하기 전에 함수형 프로그래밍을 먼저 살펴볼 필요가 있습니다. 몇 가지 기본 정의.

우리 모두 알고 있듯이 함수형 언어(ECMAScript도 이 스타일을 지원함)에서 함수는 데이터입니다. 예를 들어, 함수를 변수에 할당하거나, 다른 함수에 매개변수로 전달하거나, 함수에서 반환할 수 있습니다. 이러한 함수에는 특별한 이름과 구조가 있습니다.

정의
함수 인수("Funarg") — 값이 함수인 인수입니다.
함수 인수(“Funarg”) — 값이 함수인 인수입니다.
예:

코드 복사 코드는 다음과 같습니다.

function exampleFunc(funArg ) {
funArg();
}

exampleFunc(function () {
alert('funArg');
})

위의 예에서 funarg의 실제 매개변수는 실제로 exampleFunc에 전달된 익명 함수입니다.

역으로 기능적 매개변수를 받는 함수를 고차 함수(줄여서 HOF)라고 합니다. 함수형 함수 또는 부분적인 수학 이론 또는 연산자라고도 합니다. 위의 예에서 exampleFunc는 그러한 함수입니다.

앞서 언급했듯이 함수는 매개변수뿐만 아니라 반환값으로도 사용될 수 있습니다. 함수를 반환하는 이러한 함수를 함수 값이 있는 함수 또는 함수 값 함수라고 합니다.
코드 복사 코드는 다음과 같습니다.

(function functionValued() {
return function ( ) {
alert('반환된 함수가 호출됨');
})()()

형식으로 존재할 수 있는 함수 일반 데이터(예: 매개변수가 전달될 때, 기능적 매개변수를 허용하거나 함수 값을 반환할 때)를 일급 함수(일반적으로 일급 개체)라고 합니다. ECMAScript에서 모든 함수는 일급 객체입니다.

일반 데이터로 존재할 수 있는 함수(예: 매개변수가 전달될 때, 기능적 매개변수를 받아들이거나 함수 값을 반환할 때)를 일급 함수(일반적으로 일급 객체)라고 합니다.

ECMAScript에서는 모든 함수가 일급 객체입니다.

자신을 매개변수로 받아들이는 함수를 자동 적용 함수 또는 자체 적용 함수라고 합니다.

코드 복사 코드는 다음과 같습니다.
(function selfApplicative(funArg) {

if (funArg && funArg === selfApplicative) {
alert('self- applicative');
return
}

selfApplicative(selfApplicative);


self를 반환 값으로 사용 이 기능을 자기 복제 기능 또는 자기 복제 기능이라고 합니다. 일반적으로 문헌에서는 "자체 복제"라는 단어가 사용됩니다.


코드 복사 코드는 다음과 같습니다. (function selfReplicative() {
return selfReplicative;
})();


자기 복제 함수의 더 흥미로운 패턴 중 하나는 하나만 허용하는 것입니다. 컬렉션 자체를 승인하는 대신 매개변수로 컬렉션의 항목을 승인합니다.


코드 복사 코드는 다음과 같습니다. // 컬렉션을 받아들이는 함수
functionregisterModes(modes) {
modes.forEach(registerMode,modes);
}

// 사용법
registerModes(['roster', 'accounts', 'groups'] );

//자체 복사 기능 선언
function mode(mode) {
registerMode(mode); // 모드 등록
return 모드;
}

// 사용법, 모드 체인 호출
modes('roster')('accounts')('groups')

//약간 유사함: jQueryObject.addClass ("a") .toggle().removClass("b")


하지만 컬렉션을 직접 전달하는 것은 상대적으로 효과적이고 직관적입니다.

"funarg"가 활성화되면 기능적 매개변수에 정의된 변수에 접근할 수 있습니다. (컨텍스트가 입력될 때마다 컨텍스트 데이터를 저장하는 변수 객체가 생성되기 때문입니다.)
코드 복사 코드는 다음과 같습니다.

function testFn(funArg) {
//funarg가 활성화되면 local 변수 localVar는 액세스 가능
funArg(10); // 20
funArg(20); // 30

}

testFn(function (arg) {
var localVar = 10;
alert(arg localVar);
});

그러나 우리는 14장에서 ECMAScript에서 함수가 상위 함수에 캡슐화될 수 있다는 것을 알고 있습니다. 상위 함수 컨텍스트. 이 기능은 funarg 문제를 일으킬 수 있습니다.

Funarg 문제
스택 지향 프로그래밍 언어에서는 함수가 활성화될 때마다 이러한 변수와 함수 매개변수가 스택에 푸시됩니다.

함수가 반환되면 이러한 매개변수는 스택에서 제거됩니다. 이 모델은 함수를 기능적 값(예: 상위 함수의 반환 값)으로 사용하는 데 상당한 제한을 둡니다. 대부분의 경우 함수에 자유 변수가 있을 때 문제가 발생합니다.

자유 변수는 함수에서 사용되지만 함수 매개변수도 함수의 지역 변수도 아닌 변수를 말합니다.

예:
코드 복사 코드는 다음과 같습니다.

function testFn() {

var localVar = 10; innerFn( innerParam) {
alert(innerParam localVar)
}

return innerFn
}

var someFn = testFn()
someFn(20) ); // 30


위의 예에서 innerFn 함수의 경우 localVar는 자유 변수입니다.

스택 지향 모델을 사용하여 로컬 변수를 저장하는 시스템의 경우 이는 testFn 함수 호출이 끝나면 해당 로컬 변수가 스택에서 제거된다는 의미입니다. 이렇게 외부에서 innerFn에 대한 함수 호출이 이루어지면 (localVar 변수가 더 이상 존재하지 않기 때문에) 오류가 발생하게 됩니다.

게다가 위의 예에서 스택 지향 구현 모델에서는 innerFn을 반환 값으로 반환하는 것이 단순히 불가능합니다. testFn 함수의 지역 변수이기도 하므로 testFn이 반환될 때 제거됩니다.

또 다른 문제는 시스템이 동적 범위를 사용하고 함수를 함수 매개변수로 사용하는 경우입니다.

다음 예시(의사코드)를 보세요:


코드를 복사하세요 코드는 다음과 같습니다.
var z = 10;

function foo() {
alert(z)
}

foo(); 정적 및 동적 범위 When

(function () {

var z = 20;
foo(); // 10 – 정적 범위 사용, 20 – 동적 범위 사용

})();

// foo를 매개변수로 사용하는 경우에도 마찬가지입니다.
(function (funArg) {

var z = 30;
funArg () ; // 10 – 정적 범위, 30 – 동적 범위

})(foo)


동적 범위, 변수(식별자)를 사용하여 시스템이 관리되는 것을 볼 수 있습니다. 동적 변수 스택을 통해. 따라서 자유 변수는 함수가 생성될 때 저장된 정적 범위 체인이 아닌 현재 활성화된 동적 체인에서 쿼리됩니다.

이로 인해 갈등이 발생합니다. 예를 들어, Z가 여전히 존재하더라도(스택에서 변수를 제거하는 이전 예와는 대조적으로) 여전히 질문이 있습니다. Z의 어떤 값이 다른 함수 호출(어떤 컨텍스트, 어떤 범위에서) 쿼리에 사용됩니까?

위에서는 함수가 반환 값으로 반환되는지 여부(첫 번째 유형의 문제)와 함수를 함수 매개 변수로 사용하는지 여부(두 번째 유형의 문제)에 따라 두 가지 유형의 funarg 문제에 대해 설명합니다. .

위의 문제를 해결하기 위해 폐쇄(closure) 개념이 도입되었습니다.

클로저
클로저는 코드 블록과 코드 블록이 생성된 컨텍스트의 데이터를 결합한 것입니다.
다음 예시(의사코드)를 살펴보겠습니다.


var x = 20;

function foo() {
alert(x); // 자유 변수 "x" == 20
}

/ / is foo closure
fooClosure = {
call: foo // 함수 참조
lexicalEnvironment: {x: 20} // 컨텍스트의 컨텍스트 검색
};
위 예에서 "fooClosure" 부분은 의사코드입니다. 이에 따라 ECMAScript에서 "foo" 함수에는 이미 내부 속성, 즉 함수의 컨텍스트를 생성하는 범위 체인이 있습니다.

"lexical"은 일반적으로 생략됩니다. 위의 예는 클로저가 생성되면 컨텍스트 데이터가 저장된다는 점을 강조하기 위한 것입니다. 다음에 함수가 호출되면 저장된(클로저) 컨텍스트에서 자유 변수를 찾을 수 있으며, 위 코드에서 볼 수 있듯이 변수 "z"의 값은 항상 10이 됩니다.

정의에서는 "코드 블록"이라는 더 넓은 용어를 사용하지만 일반적으로 (ECMAScript에서는) 자주 사용하는 함수를 사용합니다. 물론 클로저의 모든 구현이 클로저와 함수를 함께 묶는 것은 아닙니다. 예를 들어 Ruby 언어에서 클로저는 프로시저 객체, 람다 표현식 또는 코드 블록일 수 있습니다.

컨텍스트가 소멸된 후 지역 변수를 저장하려는 목적으로 스택 기반 구현은 당연히 적용되지 않습니다(스택 기반 구조와 충돌하므로). 따라서 이 경우 상위 범위의 클로저 데이터는 가비지 컬렉터(GC라고 하는 가비지 컬렉터) 및 참조 카운팅(참조 카운팅)의 사용과 결합하여 동적으로 메모리를 할당("힙" 구현 기반)하여 구현됩니다. .). 이 구현은 스택 기반 구현보다 성능이 떨어지지만 모든 구현은 항상 최적화될 수 있습니다. 함수가 자유 변수, 기능 매개변수 또는 기능 값을 사용하는지 분석한 다음 상황에 따라 결정할 수 있습니다. 스택 또는 힙에 있습니다.

ECMAScript 클로저 구현
이론적인 부분을 논의한 후 ECMAScript에서 클로저를 구현하는 방법을 소개하겠습니다. 여기서 다시 강조할 가치가 있습니다. ECMAScript는 정적(어휘적) 범위만 사용합니다(Perl과 같은 언어는 변수 선언에 정적 범위와 동적 범위를 모두 사용할 수 있습니다).
코드 복사 코드는 다음과 같습니다.

var x = 10; 🎜>function foo() {
alert(x);
}

(function (funArg) {

var x = 20;

// 변수 "x "(어휘적) 컨텍스트에 저장된 정적, 함수 생성 시 저장됨
funArg() // 20 대신 10

})(foo); >
기술적으로 함수를 생성한 상위 컨텍스트의 데이터는 함수의 내부 속성 [[Scope]]에 저장됩니다. [[범위]]가 무엇인지 모른다면 [[범위]]에 대해 매우 자세하게 소개되어 있는 14장을 먼저 읽어 보시기 바랍니다. [[Scope]] 및 범위 체인 지식을 완전히 이해했다면 클로저도 완전히 이해할 것입니다.

함수 생성 알고리즘에 따르면 ECMAScript에서 모든 함수는 생성 시 상위 컨텍스트의 범위 체인을 저장하기 때문에 클로저임을 알 수 있습니다(예외 제외). 나중에 활성화됨 - [[Scope]]는 함수가 생성될 때 거기에 있을 것입니다.):




코드 복사function foo() {
alert(x)
}

// foo는 클로저
foo: = {
[[Call]]: ,
[[Scope]]: [
global: {
x: 10
}
],
... // 기타 속성
}


앞서 말했듯이 최적화 목적으로 함수를 사용하지 않는 경우 자유 변수의 경우 구현이 부작용 도메인 체인에 저장되지 않을 수 있습니다. 그러나 ECMA-262-3 사양에는 아무 것도 언급되어 있지 않습니다. 따라서 일반적으로 모든 매개변수는 생성 단계에서 [[Scope]] 속성에 저장됩니다.

일부 구현에서는 클로저 범위에 직접 액세스할 수 있습니다. 예를 들어, Rhino에는 12장에서 소개된 함수의 [[Scope]] 속성에 대한 비표준 __parent__ 속성이 있습니다.




코드 복사
코드는 다음과 같습니다. var global = this; var x = 10
var foo = (function () {

var y = 20;

반환 함수() {
alert(y)
};

foo(); // 20
alert(foo.__parent__.y); // 20

foo.__parent__.y = 30
foo(); 🎜>// 범위 체인을 통해 최상위로 이동할 수 있습니다.
alert(foo.__parent__.__parent__ === global); // true
alert(foo.__parent__.__parent__.x) // 10


모든 객체는 [[Scope]]를 참조합니다.
또한 여기에 참고하세요: ECMAScript에서는 동일한 상위 컨텍스트에서 생성된 클로저가 [[Scope]] 속성을 공유합니다. 즉, [[Scope]] 변수에 대한 특정 클로저에 의한 수정은 다른 클로저에 의한 변수 읽기에 영향을 미칩니다:

즉, 모든 내부 함수는 동일한 상위 범위를 공유합니다. 🎜>
코드 복사 코드는 다음과 같습니다.
var firstClosure
var secondClosure; 🎜>
function foo() {

var x = 1;

firstClosure = function() { return x }; x; };

x = 2; // 2개의 클로저가 공유하는 [[Scope]]에 영향을 미칩니다.

alert(firstClosure()) // 3 , 첫 번째 클로저의 [[Scope]]
}

foo()

alert(firstClosure()) // 4
Alert(secondClosure()); // 3


이 함수에 대해 매우 흔한 오해가 있습니다. 개발자는 루프 문에서 함수를 생성할 때(내부적으로 계산) 결과를 얻지 못하는 경우가 많습니다. 그 자체의 가치.


코드 복사 코드는 다음과 같습니다. var data = [];
for (var k = 0; k data[k] = function () {
alert(k)
}

data[0](); // 0이 아닌 3
data[1](); // 1이 아닌 3
data[2]() // 2가 아닌 3


위의 예는 동일한 컨텍스트에서 생성된 클로저가 [[Scope]] 속성을 공유한다는 것을 증명합니다. 따라서 상위 컨텍스트의 변수 "k"는 쉽게 변경될 수 있습니다.



코드 복사
코드는 다음과 같습니다. activeContext.Scope = [ . .. / / 기타 변수 객체{data: [...], k: 3} // 활성 객체
]

data[0].[[Scope]] == = 범위;
data[1].[[범위]] === 범위;
data[2].[[범위]] === 범위


방식으로, 기능이 활성화되면 최종적으로 사용되는 k는 3이 됩니다. 아래와 같이 클로저를 생성하면 이 문제를 해결할 수 있습니다.



코드 복사
코드는 다음과 같습니다. var data = []; for (var k = 0; k < 3; k ) {
data[k] = (function _helper(x) {
return 함수 () {
alert(x);
};
})(k) // "k" 값을 전달합니다.
}

// 이제 결과는 다음과 같습니다. 올바른
data[0](); // 0
data[1](); // 1
data[2]() // 2


위 코드에서 어떤 일이 일어나는지 살펴볼까요? "_helper" 함수가 생성된 후 매개변수 "k"를 전달하여 활성화됩니다. 반환 값도 함수이며 해당 배열 요소에 저장됩니다. 이 기술은 다음과 같은 효과를 생성합니다. 함수가 활성화되면 "_helper"가 매개변수 "x"를 포함하는 새 변수 개체를 생성할 때마다 "x" 값은 전달된 "k" 값이 됩니다. 이렇게 하면 반환되는 함수의 [[Scope]]는 다음과 같습니다.



코드 복사
코드는 다음과 같습니다. data[0].[[Scope]] === [ ... // 기타 변수 객체 상위 컨텍스트 AO: {data: [. ..], k: 3},
_helper 컨텍스트의 활성 객체 AO: {x: 0}
]

data[1].[[Scope]] === [
... // 기타 변수 객체
상위 컨텍스트의 활성 객체 AO: {data: [...], k: 3},
_helper 컨텍스트의 활성 객체 AO: {x : 1 }
];

data[2].[[Scope]] === [
... // 기타 변수 객체
상위 컨텍스트의 활성 객체 AO : {data : [...], k: 3},
_도우미 컨텍스트의 활성 개체 AO: {x: 2}
]


이때 함수의 [[Scope]] 속성이 정말 원하는 값을 가지고 있음을 알 수 있습니다. 이 목적을 달성하려면 [[Scope]]에 추가 변수 개체를 생성해야 합니다. 반환된 함수에서 "k" 값을 얻으려는 경우 값은 여전히 ​​3이라는 점에 유의해야 합니다.

그런데 JavaScript를 소개하는 글에서는 추가로 생성된 함수만 클로저라고 생각하는 경우가 많은데 이는 잘못된 것입니다. 실제로는 이 방법이 가장 효과적입니다. 그러나 이론적 관점에서 보면 ECMAScript의 모든 함수는 클로저입니다.

그러나 위에서 언급한 방법만이 전부는 아닙니다. "k"의 올바른 값은 다음과 같은 다른 방법으로도 얻을 수 있습니다.
코드 복사 코드는 다음과 같습니다. :

var data = [];

for (var k = 0; k (data[k] = function () {
alert (arguments.callee.x);
}).x = k; // k를 함수의 속성으로 사용
}

// 결과도 정확합니다
data[0] (); // 0
data[1](); // 1
data[2](); // 2

Funarg 및 반환
클로저에서 또 다른 기능이 반환됩니다. ECMAScript에서 클로저의 return 문은 호출 컨텍스트(호출자)로 제어 흐름을 반환합니다. Ruby와 같은 다른 언어에는 다양한 형태의 클로저가 있으며 해당 클로저 반환도 다릅니다. 다음 방법이 가능합니다. 호출자에게 직접 반환되거나 경우에 따라 컨텍스트에서 직접 종료됩니다. .

ECMAScript 표준 종료 동작은 다음과 같습니다.
코드 복사 코드는 다음과 같습니다.

function getElement() {

[1, 2, 3].forEach(함수(요소) {

if(요소 % 2 == 0) {
// "forEach" 함수로 복귀
// getElement 함수로 복귀하는 대신
alert('found: ' element); // 발견: 2
return element; >
} );

return null;
}


그러나 ECMAScript의 try catch를 통해 다음과 같은 효과를 얻을 수 있습니다.


var $break = {}; function getElement( ) {

try {

[1, 2, 3].forEach(함수 (요소) {

if (요소 % 2 == 0) {
// // getElement에서 반환 "
alert('found: ' 요소); // 발견: 2
$break.data = element;
throw $break;
}

});

} catch (e) {
if (e == $break) {
return $break.data;
}

return null;
}

alert(getElement()); // 2


이론적인 버전
여기서는 개발자가 클로저를 다음과 같이 잘못 이해하는 경우가 많다는 점을 설명하겠습니다. 상위 컨텍스트에서 단순화됨 내부 함수를 반환하면 익명 함수만 클로저가 될 수 있다는 것도 이해합니다.

다시 말하지만, 범위 체인으로 인해 모든 함수는 클로저입니다(함수 유형에 관계없이 익명 함수, FE, NFE 및 FD는 모두 클로저입니다).
Function 생성자를 통해 생성된 함수를 제외하고 함수 유형은 한 가지뿐입니다. 해당 [[Scope]]에는 전역 개체만 포함되기 때문입니다.

이 문제를 더 명확하게 하기 위해 ECMAScript의 클로저 정의에 대한 두 가지 올바른 버전을 제공합니다.

ECMAScript에서 클로저는 다음을 의미합니다.

이론적인 관점에서 : 모든 기능. 왜냐하면 모두 생성 시 상위 컨텍스트의 데이터를 저장하기 때문입니다. 이는 간단한 전역 변수의 경우에도 마찬가지입니다. 함수에서 전역 변수에 액세스하는 것은 자유 변수에 액세스하는 것과 동일하기 때문입니다.
실용적인 관점에서 볼 때: 다음 함수는 클로저로 간주됩니다.
생성된 컨텍스트가 파괴되더라도 여전히 존재합니다(예: 내부 함수가 상위 함수에서 반환됨)
무료 변수는 코드에서 참조됩니다
클로저의 실제 사용
실제로 사용하면 클로저는 funarg에 정의된 다양한 계산 방법을 사용자 정의할 수 있어 매우 우아한 디자인을 만들 수 있습니다. 다음은 정렬 조건 함수를 매개변수로 받는 배열 정렬의 예입니다.




코드 복사
코드는 다음과 같습니다. : [1, 2, 3].sort(function (a, b) { ... // 정렬 조건 })

같은 예도 그렇습니다. 배열의 맵 메소드는 함수에 정의된 조건에 따라 원래 배열을 새 배열에 매핑합니다.



코드 복사
코드는 다음과 같습니다: [1, 2, 3].map(function (element) { return element * 2; } ); // [2, 4, 6]

기능적 매개변수를 사용하면 검색 방법을 쉽게 구현하고 무제한 검색 조건을 지원할 수 있습니다.
코드 복사 코드는 다음과 같습니다. 다음과 같습니다:

someCollection.find(function (element) {
return element.someProperty == 'searchCondition';
})

있습니다. 각 배열 요소에 함수를 적용하는 일반적인 forEach 메서드와 같은 응용 프로그램 함수도 있습니다.
코드 복사 코드는 다음과 같습니다. 다음:

[1, 2, 3].forEach(함수(요소) {
if(요소 % 2 != 0) {
alert(요소);
}
}); // 1, 3

그런데 함수 객체의 적용 및 호출 메소드는 함수형 프로그래밍에서 응용 함수로도 사용할 수 있습니다. 여기서 "this"에 대해 논의할 때 apply와 call이 이미 소개되었습니다. 우리는 이를 응용 프로그램 함수, 즉 인수(적용의 인수 목록, 호출의 개별 인수)에 적용되는 함수로 생각합니다.
코드 복사 코드는 다음과 같습니다.

(function () {
alert([] .join.call (인수, ';')); // 1;2;3
}).apply(this, [1, 2, 3])

클로저 또 다른 매우 중요한 것이 있습니다. 애플리케이션 - 지연된 통화:
코드 복사 코드는 다음과 같습니다.

var a = 10 ;
setTimeout(function () {
alert(a); // 10, 1초 후
}, 1000)

콜백 함수도 있습니다
코드 복사 코드는 다음과 같습니다.

//...
var x = 10 ;
// 예를 들어
xmlHttpRequestObject.onreadystatechange = function () {
// 데이터가 준비되면 호출됩니다.
// 여기서는 어떤 컨텍스트에서 생성되든 상관없습니다.
// 현재 변수 "x"의 값이 이미 존재합니다.
alert(x) // 10
}//...

또한 캡슐화된 범위를 생성하여 도우미 개체를 숨길 수 있습니다.

코드 복사 코드는 다음과 같습니다.
var foo = {};

// 초기화
(함수(객체) {

var x = 10;

object.getX = function _getX () {
return x;

})(foo)

alert(foo.getX());


요약
이 글에서는 ECMAScript-262-3에 대한 좀 더 이론적 지식을 소개하는데, 이러한 기본 이론들이 ECMAScript의 클로저 개념을 이해하는데 도움이 된다고 생각합니다. 궁금하신 점은 댓글로 답변해드리겠습니다.

기타 참고자료

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