>웹 프론트엔드 >JS 튜토리얼 >자바스크립트 시리즈 심층 이해 (2) 네임드 함수 표현의 비밀을 밝히다_javascript 스킬

자바스크립트 시리즈 심층 이해 (2) 네임드 함수 표현의 비밀을 밝히다_javascript 스킬

WBOY
WBOY원래의
2016-05-16 17:57:11968검색

서문
인터넷에서 명명된 함수 표현에 대해 깊이 있는 논의를 반복한 사람을 본 적이 없습니다. 이로 인해 인터넷에서 다양한 오해가 나타났습니다. 이 기사에서는 JavaScript의 명명 기능을 원리와 실제 측면에서 논의합니다. 측면. 표현의 장점과 단점.
간단히 말하면, 디버그 또는 프로파일러 분석 중에 함수 이름을 설명하기 위한 명명된 함수 표현식의 사용자는 단 한 명뿐입니다. 함수 이름을 사용하여 재귀를 구현할 수도 있지만 곧 알게 될 것입니다. 실제로는 비현실적입니다. 물론 디버깅에 관심이 없다면 걱정할 필요가 없습니다. 그렇지 않고 호환성에 대해 알고 싶다면 계속 읽어야 합니다.
먼저 함수 표현식이 무엇인지 살펴보고 최신 디버거가 이러한 표현식을 처리하는 방법에 대해 이야기해 보겠습니다. 이미 이에 익숙하다면 이 섹션을 건너뛰시기 바랍니다.
함수 표현식 및 함수 선언
ECMAScript에서 함수를 생성하는 가장 일반적인 두 가지 방법은 함수 표현식과 함수 선언입니다. ECMA 사양에서는 함수라는 한 가지 사항만 명확히 하기 때문에 둘 사이의 차이점은 약간 혼란스럽습니다. 선언에는 식별자(식별자) ​​(모든 사람이 종종 함수 이름이라고 부르는 것)가 있어야 하며, 함수 표현식에서는 이 식별자를 생략할 수 있습니다.
함수 선언:
함수 함수 이름(매개변수: 선택 사항) { 함수 body}
함수 표현식:
함수 함수명(선택) (매개변수: 선택) { 함수 본문}
그래서 함수 이름이 선언되지 않은 경우 표현식이어야 함을 알 수 있으며, 하지만 함수 이름이 선언되면 그것이 함수 선언인지 함수 표현식인지 어떻게 판단할 수 있을까요? ECMAScript는 문맥에 따라 구별됩니다. function foo(){}가 할당 표현식의 일부인 경우 이는 함수 표현식입니다. function foo(){}가 함수 본문 내에 포함되거나 프로그램 맨 위에 있는 경우입니다. 함수 선언.

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

function foo(){} // 선언 , 이는 프로그램의 일부이기 때문입니다.
var bar = function foo(){}; // 표현식의 일부이기 때문에
new function bar(){}; is new Expression
(function(){
function bar(){} // 선언, 왜냐하면 함수 본문의 일부이기 때문입니다
})()

있음 또 다른 함수 표현식은 덜 일반적이며 괄호(function foo(){})로 묶입니다. 괄호()는 그룹화 연산자이고 내부에만 표현식을 포함할 수 있기 때문입니다. 몇 가지 예:
코드 복사 코드는 다음과 같습니다.

function foo(){} / / 함수 선언
(function foo(){}); // 함수 표현식: 그룹화 연산자에 포함됨
try {
(var x = 5) // 그룹화 연산자, 표현식만 포함함 not 문: var here is the 문
} catch(err) {
// SyntaxError
}

생각하시면 됩니다. eval을 사용하면 JSON이 실행될 때, JSON 문자열은 일반적으로 괄호(eval('(' json ')'))로 묶입니다. 그 이유는 그룹화 연산자, 즉 이 괄호 쌍이 파서가 JSON의 중괄호를 표현식으로 구문 분석하도록 하기 때문입니다. 코드 블록 대신.
코드 복사 코드는 다음과 같습니다.

try {
{ "x" : 5 } ; // "{" 및 "}"는 코드 블록으로 구문 분석됩니다.
} catch(err) {
// SyntaxError
}
({ "x": 5 }); // 그룹화 연산자는 "{" 및 "}"를 객체 리터럴로 구문 분석하도록 강제합니다.

식과 선언 사이에는 매우 미묘한 차이가 있습니다. 첫째, 함수 선언은 다른 항목보다 먼저 구문 분석되고 평가됩니다. 값이 먼저 구문 분석되고 평가됩니다. 선언이 코드의 마지막 줄에 있더라도 동일한 범위의 첫 번째 표현식보다 먼저 구문 분석/평가됩니다. 다음 예제를 참조하세요. . 그러나 경고가 실행되면 fn이 이미 정의되어 있습니다.
코드 복사 코드는 다음과 같습니다.

alert(fn());
function fn() {
return 'Hello world!''
}

추가로 한 가지가 더 있습니다. 주의할 점, function 선언은 조건문 내에서 사용할 수 있지만 표준화되어 있지 않아 환경에 따라 실행 결과가 다를 수 있으므로 이 경우 함수 표현식을 사용하는 것이 가장 좋습니다.
코드 복사 코드는 다음과 같습니다.

// 이러지 마세요!
// 일부 브라우저는 첫 번째 함수를 반환하고 일부 브라우저는 두 번째 함수를 반환하기 때문에
if (true) {
function foo() {
return 'first';
}
else {
function foo() {
return 'second';
}
}
foo()
// 반대로 이 경우에는 함수 표현식
var foo;
if (true) {
foo = function() {
return 'first'
};
else {
foo = function() {
return '두 번째';
}
foo()


함수 선언의 경우 다음과 같습니다.
함수 선언은 프로그램이나 함수 본문 내에서만 나타날 수 있습니다. 구문상 블록({ ... }) 내부(예: if, while 또는 for 문 내)에 나타날 수 없습니다. 블록은 명령문만 포함할 수 있고 함수 선언과 같은 소스 요소는 포함할 수 없기 때문입니다. 반면, 규칙을 자세히 살펴보면 표현식이 블록에 나타날 수 있는 유일한 방법은 표현식 문의 일부인 경우라는 것을 알 수 있습니다. 그러나 사양에는 표현식 문이 키워드 function으로 시작할 수 없다고 명시되어 있습니다. 이것이 실제로 의미하는 바는 함수 표현식이 명령문이나 블록에 나타날 수 없다는 것입니다(블록은 명령문으로 구성되기 때문입니다).

함수문
ECMAScript의 구문 확장 중 하나가 함수문입니다. 현재는 Gecko 기반의 브라우저에서만 이 확장을 구현하고 있으므로, 다음 예시에서는 학습용으로만 사용하고 있는 것 같습니다. 일반적으로 권장되지 않습니다(Gecko 브라우저용으로 개발하는 경우 제외).
1. 일반문을 사용할 수 있는 경우 블록 블록을 포함하여 함수문도 사용할 수 있습니다.



코드 복사 코드는 다음과 같습니다. if (true) {
function f(){ }
}
else {
function f(){ }
}


2. 조건부 실행을 포함하여 다른 명령문과 마찬가지로 함수 명령문을 구문 분석할 수 있습니다.


코드 복사 코드는 다음과 같습니다: if (true) {
function foo(){ return 1; }
}
else {
function foo(){ return 2 ; }
}
foo(); // 1
// 참고: 다른 클라이언트는 foo를 함수 선언으로 구문 분석합니다.
// 따라서 두 번째 foo는 첫 번째 foo를 덮어씁니다. 결과는 1 대신 2를 반환합니다


3. 함수 명령문은 변수 초기화 중에 선언되지 않지만 런타임 시 함수 표현식과 동일합니다. 그러나 함수 문의 식별자가 선언되면 함수 전체 범위에 적용됩니다. 식별자 유효성은 함수 문을 함수 표현식과 다르게 만드는 것입니다(다음 섹션에서 명명된 함수 표현식의 구체적인 동작을 보여줍니다).


코드를 복사하세요 코드는 다음과 같습니다. // 현재 foo는 no 선언
typeof foo; // "undefine"
if (true) {
// 여기에 입력하면 foo가 전체 범위에 선언됩니다.
function foo(){ return 1; 🎜>}
else {
// 여기에 오지 않으므로 foo는 선언되지 않습니다.
function foo(){ return 2 }
}
typeof foo; "


그러나 다음 표준 호환 코드를 사용하여 위 예의 함수 명령문을 모델링할 수 있습니다.



코드 복사 foo = function foo(){ return 1 }; > }
else {
foo = function foo() { return 2 };


4. 함수 명령문 및 함수 선언용 문자 식별자를 포함하여 문자열 표현은 유사합니다.



코드 복사

코드는 다음과 같습니다. if (true ) { function foo(){ return 1; } } String(foo); // function foo() { return 1; 또 다른 하나는 초기 Gecko 기반 구현(Firefox 3 및 이전 버전)에 버그가 있습니다. 즉, 함수 명령문이 함수 선언을 잘못 재정의합니다. 이러한 초기 구현에서는 함수 명령문이 함수 선언을 재정의할 수 없었습니다.



코드 복사

코드는 다음과 같습니다.

// 함수 선언
function foo(){ return 1; }
if (true) {
//
function foo(){ return 2를 함수 문으로 다시 작성 ; }
}
foo(); // FF3 이하는 1, FF3.5 이상은 2를 반환합니다.
// 단, 함수 표현식이 앞에 오면 쓸모가 없습니다
var foo = function(){ return 1; };
if (true) {
function foo(){ return 2; }
}
foo(); 🎜>
위의 예시는 특정 브라우저에서만 지원된다는 점을 다시 한번 강조합니다. 따라서 특별한 기능이 있는 브라우저에서 개발하는 경우가 아니면 사용하지 않는 것이 좋습니다.
이름이 지정된 함수 표현식
함수 표현식은 실제 응용 프로그램에서 여전히 매우 일반적입니다. 웹 개발의 일반적인 패턴은 성능 최적화를 달성하기 위해 특정 특성 테스트를 기반으로 함수 정의를 위장하는 것입니다. 범위에서는 기본적으로 함수 표현식을 사용해야 합니다.

코드 복사 코드는 다음과 같습니다.
// 이 코드는 Garrett Smith의 APE Javascript 라이브러리(http://dhtmlkitchen.com/ape/)에서 제공됩니다.
var contain = (function() {
var docEl = document.documentElement;
if (typeof docEl. CompareDocumentPosition != '정의되지 않음') {
return function(el, b) {
return (el.compareDocumentPosition(b) & 16) !== 0
}
else if (typeof docEl.contains != 'undefine') {
return function(el, b) {
return el !== b && el .contains(b)
}; }
return function(el, b) {
if (el === b) return false
while (el != b && (b = b.parentNode) != null); >return el === b;
})();


언급된 함수 표현식 물론 이전 예제에는 이름이 있어야 합니다. foo(){};는 유효한 명명된 함수 표현식이지만 기억해야 할 것이 하나 있습니다. 이 이름은 새로 정의된 함수의 범위 내에서만 유효합니다. 왜냐하면 사양에서는 식별자가 외부 범위에서 유효할 수 없다고 규정하고 있기 때문입니다. 🎜>


코드 복사

코드는 다음과 같습니다. var f = function foo(){ return typeof foo; // foo는 내부 범위 내에서 유효합니다. // foo는 외부에서 사용될 때 보이지 않습니다. typeof foo; // "undefine"
f(); "function"


이것이 요구사항인데 명명된 함수 표현식은 어떻게 사용하나요? 이름이 왜요?
처음에 말했듯이, 이름을 지정하면 디버깅 프로세스가 더 편리해질 수 있습니다. 왜냐하면 디버깅할 때 호출 스택의 각 항목에 이를 설명하는 고유한 이름이 있으면 디버깅 프로세스가 훌륭해지기 때문입니다. , 느낌이 다릅니다.
디버거의 함수 이름
함수에 이름이 있으면 디버거는 디버깅 중에 호출 스택에 해당 이름을 표시합니다. 일부 디버거(Firebug)는 함수를 사용하는 것과 동일한 역할을 갖도록 함수에 이름을 지정하고 표시하는 경우가 있지만 일반적으로 이러한 디버거는 이름 지정에 대한 간단한 규칙만 설치하므로 가격이 크지 않다고 말하면 다음을 살펴보겠습니다. 예:



코드 복사


코드는 다음과 같습니다.
function foo(){ return bar(); } function bar(){ return baz();
function baz(){
}
foo();
// 여기서는 이름이 있는 3개의 함수 선언을 사용합니다
// 따라서 디버거가 디버거 문으로 이동하면 Firebug 호출 스택은 매우 명확해 보입니다.
// 이름이 명확하기 때문에 표시되는
baz
bar
foo
expr_test.html()


콜 스택 정보를 보면 foo가 bar를 호출하고 bar가 baz를 호출한다는 것을 확실히 알 수 있습니다. (그리고 foo 자체는 expr_test.html 문서의 전역 범위에서 호출됩니다.) 그러나 또 다른 멋진 곳이 있습니다. 즉, 방금 언급한 Firebug는 익명 표현식 이름 지정 기능입니다:




코드 복사

코드는 다음과 같습니다.

function foo() { return bar()} var bar = function(){ return baz(); } function baz(){ debugger;
}
foo(); stack
baz
bar() //보이죠?
foo
expr_test.html()


그럼 함수 표현식이 조금 더 복잡해지면 디버거가 그다지 똑똑하지 않고 호출 스택에서만 볼 수 있습니다. 표시:




코드 복사


코드는 다음과 같습니다.

function foo(){
return bar();
}
var bar = (function(){
if (window.addEventListener) {
return function( ){
return baz();
};
}
else if (window.attachEvent) {
return function() {
return baz(); ;
}
})();
function baz(){
디버거;
}
foo()// 호출 스택
baz
(?)() // 물음표 입니다
foo
expr_test.html()


또한, 여러 변수에 함수를 할당할 경우 명령어도 나옵니다. 우울한 질문:


function foo(){
return baz();
}
var bar = function(){
debugger;
}
var baz = bar; > Alert('spoofed');
};
foo();
// 호출 스택:
bar()
foo
expr_test.html()
🎜 >
이때 호출 스택에는 foo가 bar를 호출한 것으로 표시되지만 실제로는 그렇지 않습니다. 이 문제의 원인은 baz와 Alert('spoofed')가 포함된 다른 함수가 참조 교환을 했기 때문입니다. 발생했습니다.
최종 분석에서 가장 위임된 방법은 함수 표현식에 이름을 부여하는 것, 즉 명명된 함수 표현식을 사용하는 것입니다. 명명된 표현식을 사용하여 위의 예를 다시 작성해 보겠습니다(즉시 호출되는 표현식 블록에서 반환되는 두 함수의 이름은 막대입니다).



코드 복사
코드는 다음과 같습니다. function foo(){ return bar() }
var bar = (function() {
if (window.addEventListener) {
return function bar(){
return baz();
}
else if (window.attachEvent) {
return 함수 bar() {
return baz();
};
}
})()
function baz(){
디버거; ()
// 명확한 호출 스택 정보를 다시 볼 수 있습니다!
baz
bar
foo
expr_test.html()


좋아요. 또 다른 비결을 배웠나요? 하지만 너무 흥분하기 전에 특이한 JScript를 살펴보겠습니다.
JScript의 버그
더 나쁜 것은 IE의 JScript ECMAScript 구현이 명명된 함수 표현식을 심각하게 혼동하여 많은 사람들이 명명된 함수 표현식에 반대하게 만든다는 것입니다. 심지어 최신 버전(IE8) 버전 5.8을 사용하더라도 여전히 다음과 같은 문제가 있습니다. 문제.
IEE가 구현 과정에서 어떤 실수를 했는지 살펴보겠습니다. 속담처럼 적을 알아야만 무적이 될 수 있습니다. 다음 예를 살펴보겠습니다.
예제 1: 함수 표현식의 식별자가 외부 범위로 유출됩니다



코드 복사


코드는 다음과 같습니다.
var f = function g(){}; typeof g; // "function" 말씀드린 대로 위의 이름 지정 함수 표현식의 식별자는 외부 범위에서 유효하지 않지만 JScript는 분명히 이 사양을 위반합니다. 위 예의 식별자 g는 찾기 어려운 많은 버그입니다. 이런 이유 때문에. 예 2: 명명된 함수 표현식을 함수 선언과 함수 표현식으로 동시에 처리



코드 복사


코드는 다음과 같습니다. 다음:
typeof g; // "function" var f = function g(){}; 기능 환경에서는 함수 선언이 우선합니다. 모든 표현식에 대해 구문 분석되는 경우 위의 예에서는 JScript가 실제 선언 전에 g를 구문 분석하므로 실제로 명명된 함수 표현식을 함수 선언으로 처리한다는 것을 보여줍니다. 이 예시는 다음 예시로 이어집니다.
예 3: 명명된 함수 표현식은 완전히 다른 두 개의 함수 객체를 생성합니다!



코드 복사


코드는 다음과 같습니다.
이것을 보면 누구나 이렇게 생각할 것입니다. 문제는 심각합니다. 개체를 수정해도 다른 개체는 변경되지 않으며 이는 너무 사악하기 때문입니다. 이 예를 통해 우리는 두 개의 서로 다른 객체를 생성한다는 것을 알 수 있습니다. 즉, f의 속성을 수정하여 특정 정보를 저장한 다음 g의 동일한 이름 속성을 참조하여 당연히 사용하려는 경우입니다. 동일한 개체라면 큰 문제가 발생할 것입니다. 왜냐하면 그것은 단순히 불가능하기 때문입니다.
좀 더 복잡한 예를 살펴보겠습니다.
예 4: 함수 선언만 순차적으로 구문 분석하고 조건문 블록을 무시합니다.



코드 복사


코드는 다음과 같습니다.

var f = function g() {
return 1;
}
if (false) {
f = function g(){
return 2; 🎜>};
}
g(); // 2

이 버그는 찾기가 훨씬 어렵지만 버그의 원인은 매우 간단합니다. 먼저 g는 함수 선언으로 해석됩니다. JScript의 함수 선언은 조건부 코드 블록의 적용을 받지 않기 때문에 이 불쾌한 if 분기에서는 g가 다른 함수 function g(){ return 2 }로 처리되고, 또한 다시 선언되었습니다. . 그런 다음 모든 "정규" 표현식이 평가되고 f에는 새로 생성된 다른 객체에 대한 참조가 제공됩니다. 표현식이 평가될 때 끔찍한 if 분기 ""가 입력되지 않으므로 f는 계속해서 첫 번째 함수 function g(){ return 1 }을 참조합니다. 이를 분석한 후에 문제는 매우 명확합니다. 충분히 주의해서 f에서 g를 호출하면 관련 없는 g 함수 개체가 호출됩니다.
arguments.callee와 다른 개체를 비교할 때 차이점이 무엇인지 궁금할 것입니다.

코드 복사 코드는 다음과 같습니다.
var f = function g( ){
return [
args.callee == f,
arguments.callee == g
}
f(); // [참, 거짓]
g(); false, true]


보시다시피,args.callee의 참조는 항상 호출되는 함수입니다. 사실 이것도 좋은 일인데, 이에 대해서는 나중에 설명하겠습니다. 흥미로운 예는 선언이 포함되지 않은 대입문에서 명명된 함수 표현식을 사용하는 것입니다.



코드 복사 코드는 다음과 같습니다. : (function(){
f = function f(){};
})()


다음 코드를 따르세요. 분석에서 우리는 원래 전역 속성 f를 만들고 싶었습니다(이름이 지정된 life를 사용하는 일반 익명 함수와 혼동하지 않도록 주의하세요). 먼저 JScript는 표현식을 함수 선언으로 구문 분석했습니다. 왼쪽의 f는 지역변수로 선언(일반 익명함수에서의 선언과 동일)한 뒤, 함수가 실행되면 이미 f가 정의되어 있고, 오른쪽의 f(){} 함수가 직접적으로 값은 지역 변수 f에 할당되므로 f는 전역 속성이 아닙니다.
JScript가 얼마나 비정상적인지 이해한 후에는 먼저 이러한 문제를 방지해야 합니다. 둘째, 외부 범위에서 식별자가 유출되는 것을 방지해야 합니다. , 함수 이름으로 사용된 식별자를 인용해서는 안 됩니다. 이전 예에서 g가 존재하지 않는 것처럼 보일 수 있다면 얼마나 불필요한 문제를 피할 수 있는지 기억해 보십시오. 따라서 핵심은 항상 참조하는 것입니다. f 또는args.callee를 통해 함수에 명명된 함수 표현식을 사용하는 경우 디버깅 중에만 해당 이름을 사용해야 합니다. 마지막으로, 잘못 생성된 함수를 정리하는 동안 항상 명명된 함수 표현식을 참조해야 합니다. > 위의 마지막 사항에 대해 다시 설명해야 합니다.
JScript의 메모리 관리
이러한 비호환 코드 구문 분석 버그를 알고 나면 실제로 문제가 있음을 알 수 있습니다. 예를 들어 보겠습니다.




코드 복사
코드는 다음과 같습니다. var f = (function(){ if (true) {
return function g(){};
}
return function g(){};
})();


우리는 이 익명 함수가 반환된 함수(식별자가 g인 함수)를 호출한 다음 이를 외부 f에 할당한다는 것을 알고 있습니다. 또한 명명된 함수 표현식이 반환된 함수 개체와 동일하지 않은 중복 함수 개체로 이어진다는 것도 알고 있습니다. 그래서 이 중복된 g 함수는 반환 함수가 닫힐 때 죽어서 메모리 문제가 발생했습니다. 이는 if 문 내부의 함수가 g와 동일한 범위에서 선언되었기 때문입니다. 이 경우 g 함수에 대한 참조를 명시적으로 연결 해제하지 않으면 항상 메모리를 차지하게 됩니다.



코드 복사
코드는 다음과 같습니다. var f = (function(){ var f, g;
if(true) {
f = 함수 g(){}
}
else {
f = 함수 g(){}; >}
// g를 null로 설정하면 더 이상 메모리를 차지하지 않습니다.
g = null
return f;
g를 null로 설정하면 가비지 수집기가 g에서 참조하는 암시적 함수를 회수합니다. 코드를 확인하기 위해 몇 가지 테스트를 수행하여 메모리가 회수되는지 확인합니다.
테스트
테스트는 매우 간단합니다. 명명된 함수 표현식으로 10,000개의 함수를 만든 다음 배열에 저장하면 됩니다. 잠시 기다렸다가 이러한 기능이 차지하는 메모리 양을 확인하십시오. 그런 다음 해당 참조의 연결을 끊고 프로세스를 반복하십시오. 테스트 코드는 다음과 같습니다.
코드 복사 코드는 다음과 같습니다.

function createFn (){
return (function(){
var f;
if (true) {
f = function F(){
return 'standard';
};
}
else if (false) {
f = 함수 F(){
'대체' 반환
}
else {
f = 함수 F(){
'대체' 반환
}
// var F = null
return f; 🎜>var arr = [ ];
for (var i=0; i<10000; i ) {
arr[i] = createFn()
}


Windows XP에서 실행하면 SP2의 작업 관리자에서 다음 결과를 볼 수 있습니다.



코드 복사
코드는 다음과 같습니다. : IE6: `null` 없음: 7.6K -> 20.3K `null` 포함: 7.6K ->
IE7:
없음 `null`: 14K -> 29.7K
`null`: 14K -> 27K


예상대로 디스플레이 연결 끊김으로 인해 메모리가 해제될 수 있지만 해제되는 메모리는 많지 않습니다. , 10,000개의 함수 개체는 3M에서만 출시됩니다. 이는 일부 작은 스크립트에는 별거 아니지만 대규모 프로그램이나 메모리가 부족한 장치에서 오랫동안 실행하는 경우 매우 필요합니다.

Safari 2.x의 JS 파싱에도 버그가 있는데, 버전이 상대적으로 낮기 때문에 여기서는 소개하지 않겠습니다. 보고 싶으신 분들은 영문 정보를 잘 확인해주세요. .
SpiderMonkey의 단점
우리 모두 알고 있듯이 명명된 함수 표현식의 식별자는 함수의 로컬 범위에서만 유효합니다. 하지만 이 식별자를 포함하는 로컬 범위는 어떤 모습일까요? 실제로는 매우 간단합니다. 명명된 함수 표현식이 평가되면 이름이 함수 식별자에 해당하고 값이 해당 함수에 해당하는 속성을 보유하는 것이 유일한 목적인 특수 객체가 생성됩니다. 이 개체는 현재 범위 체인 앞에 삽입됩니다. 그러면 "확장된" 범위 체인이 초기화 함수에 사용됩니다.
여기서 매우 흥미로운 점 중 하나는 ECMA-262가 이 "특수" 객체(함수 식별자를 보유함)를 정의하는 방식입니다. 표준에서는 "새 Object() 표현식을 호출하는 것처럼" 이 객체를 생성한다고 말합니다. 이 문장을 문자 그대로 이해한다면 이 개체는 전역 개체의 인스턴스여야 합니다. 그러나 문자 그대로 이를 수행하는 구현은 단 하나 뿐이며 그 구현은 SpiderMonkey입니다. 따라서 SpiderMonkey에서 Object.prototype을 확장하면 함수의 로컬 범위를 방해할 수 있습니다.




코드 복사
코드는 다음과 같습니다. : Object.prototype.x = 'outer'; (function(){ var x = 'inner';
/*
함수 범위 체인 foo에는 함수의 식별자를 저장하는 데 사용되는 특수 객체가 있습니다. 이 특수 객체는 실제로는 { foo: }입니다. x가 범위 체인을 통해 구문 분석되면 foo의 로컬 환경이 먼저 구문 분석됩니다. Object.prototype에서 상속되므로 여기서 x를 찾을 수 있습니다.
x의 값은 Object.prototype.x(외부)의 값이기도 합니다. 따라서 x의 역할을 포함하는 외부 함수의 범위도 마찬가지입니다. = 'inner'). domain)은 구문 분석되지 않습니다.
*/
(function foo(){
alert(x); // 프롬프트 상자에 다음이 표시됩니다. external
})();
})()


그러나 이후 버전의 SpiderMonkey에서는 보안 취약점으로 간주되어 위의 동작을 변경했습니다. 즉, "특수" 개체는 더 이상 Object.prototype을 상속하지 않습니다. 그러나 Firefox 3 이하를 사용하는 경우 이 동작을 "다시 방문"할 수 있습니다.
내부 개체를 전역 개체 개체로 구현하는 또 다른 브라우저는 Blackberry 브라우저입니다. 현재 해당 활동 개체(활성화 개체)는 여전히 Object.prototype을 상속합니다. 그러나 ECMA-262에서는 "새 Object() 표현식을 호출하는 것과 같이"(또는 NFE 식별자를 보유하는 객체를 생성하는 것과 같이) 활성 객체를 생성해야 한다고 말하지 않습니다. 다른 사람들의 표준에서는 활동 개체가 표준의 메커니즘이라고만 말합니다.
그럼 BlackBerry에서 무슨 일이 일어나고 있는지 살펴보겠습니다.
코드 복사 코드는 다음과 같습니다.

Object.prototype.x = '외부';
(function(){
var x = 'inner';
(function(){
/*
Inside 스코프 체인에서 x를 파싱할 때 로컬 함수의 활성 객체를 먼저 검색합니다. 물론 이 객체에서는 x를 찾을 수 없습니다.
그러나 활성 객체는 Object.prototype을 상속하므로 다음 것입니다. x가 검색됩니다. 대상은 Object.prototype이고
Object.prototype에는 x의 정의가 있습니다. 결과적으로 x의 값은 이전 예와 마찬가지로
에 포함되어 있습니다. x = 'inner'. 외부 함수(활성 객체)의 범위는 확인되지 않습니다.
*/
alert(x); // 표시: 외부
})(); })();

그러나 놀라운 점은 함수의 변수가 Object.prototype의 기존 멤버와 충돌할 수도 있다는 것입니다.

코드 복사 코드는 다음과 같습니다.
(function(){
var constructor = function(){ return 1; }; (function(){
constructor(); // 평가 결과는 1이 아닌 {}입니다(Object.prototype.constructor()를 호출하는 것과 동일).
constructor = == Object.prototype.constructor; // true
toString === Object.prototype.toString; // true
// ……
})();


이 문제를 방지하려면 Object.prototype에서 toString, valueOf, hasOwnProperty 등과 같은 속성 이름을 사용하지 마세요.
JScript 솔루션



코드 복사 코드는 다음과 같습니다. var fn = (function(){
// 함수를 참조하기 위한 변수 선언
var f;
// 조건부로 명명된 함수를 생성합니다
// 해당 참조를 f에 할당합니다
if ( true) {
f = 함수 F(){ }
}
else if (false) {
f = 함수 F(){ }
}
else {
f = function F(){ }
}
// 함수명(식별자)에 해당하는 변수를 선언하고 null로 할당
// 실제로 해당 함수가 참조하는 함수입니다. 식별자 개체가
// 재활용 가능하다는 것을 알 수 있도록 표시됩니다.
var F = null
//
return f; >}) ();


마지막으로 위의 기술을 적용한 응용예를 제시하는데, 바로 크로스 브라우저 addEvent 함수 코드입니다:



코드 복사
코드는 다음과 같습니다. // 1) 독립된 범위를 사용하여 문을 포함합니다. var addEvent = (function( ){
var docEl = document.documentElement;
// 2) 함수를 참조하도록 변수를 선언합니다.
var fn;
if (docEl.addEventListener) {
// 3) 의도적으로 함수에 설명적 식별자 제공
fn = function addEvent(element, eventName, callback) {
element.addEventListener(eventName, callback, false)
}
}
else if (docEl .attachEvent) {
fn = function addEvent(element, eventName, callback) {
element.attachEvent('on' eventName, callback)
}
}
else {
fn = function addEvent(element , eventName, callback) {
element['on' eventName] = callback;
}
}
// 4) JScript에서 생성된 addEvent 함수 삭제
// 할당하기 전에 반드시 var 키워드를 사용하세요.
// 함수 상단에 addEvent가 선언되지 않은 경우
var addEvent = null
// 5) 마지막으로 fn이 참조하는 함수를 반환합니다. 🎜>return fn;
} )();


대체
사실 이 설명적인 이름을 원하지 않으면 가장 간단한 형식으로 지정할 수 있습니다. (함수 표현식 대신) 함수 내부에서 함수를 선언한 다음 함수를 반환하려면:



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

var hasClassName = (function(){
// 개인 변수 정의
var 캐시 = { };
// 함수 선언 사용
function hasClassName(element, className) {
var _className = '(?:^|\s )' className '(?:\s |$)'
var re = 캐시[_className] || (cache[_className] = new RegExp(_className) ));
return re.test(element.className);
}
// 반환 함수
return hasClassName
})(); 분명히 이 솔루션은 여러 분기 기능 정의가 있는 경우 작동하지 않습니다. 그러나 가능해 보이는 패턴이 있습니다. 즉, 함수 선언을 사용하여 모든 함수를 미리 정의하고 이러한 함수에 대해 서로 다른 식별자를 지정합니다.


var docEl = document.documentElement;
function addEventListener(){
/* .. */
}
function attachmentEvent(){
/* ... */
}
function addEventAsProperty(){
/* . .. */
}
if (typeof docEl.addEventListener != '정의되지 않음') {
return addEventListener;
}
elseif (typeof docEl.attachEvent != '정의되지 않음') {
return attachmentEvent;
}
return addEventAsProperty;
})();


이 솔루션은 훌륭하지만 단점이 없는 것은 아닙니다. 첫째, 다른 식별자 사용으로 인해 명명 일관성이 손실됩니다. 이것이 좋은지 나쁜지는 말할 것도 없고, 적어도 충분히 명확하지는 않습니다. 어떤 사람들은 같은 이름을 사용하는 것을 좋아하지만 다른 사람들은 표현의 차이에 전혀 신경 쓰지 않습니다. 그러나 결국 다른 이름은 사람들에게 사용된 다른 구현을 상기시킵니다. 예를 들어, 디버거에서 attachmentEvent를 보면 addEvent가 attachmentEvent 구현을 기반으로 한다는 것을 알 수 있습니다. 물론 구현에 따른 이름 지정이 항상 작동하는 것은 아닙니다. API를 제공하고 이런 방식으로 함수 이름을 내부로 지정한다고 가정해 보겠습니다. 그러면 API 사용자는 해당 구현의 세부 사항으로 인해 쉽게 혼란을 겪을 수 있습니다.
이 문제를 해결하려면 당연히 좀 더 합리적인 명명 방식을 생각해야 합니다. 하지만 더 이상 문제를 일으키지 않는 것이 핵심입니다. 지금 생각나는 해결책은 다음과 같습니다:
'addEvent', 'altAddEvent', 'fallbackAddEvent'
// 또는
'addEvent', 'addEvent2', 'addEvent3'
// 아니면
'addEvent_addEventListener', 'addEvent_attachEvent', 'addEvent_asProperty'
코드 복사
게다가 이 모드에도 작은 문제가 있는데, 바로 메모리 사용량이 늘어난다는 점이다. N개의 함수를 다른 이름으로 미리 만들어 놓는 것은 N-1개의 함수를 사용하지 않는다는 의미입니다. 특히 document.documentElement에 attachmentEvent가 포함되어 있으면 addEventListener 및 addEventAsProperty가 전혀 필요하지 않습니다. 그러나 이 메모리는 모두 메모리를 차지합니다. 게다가 이 메모리는 JScript의 지저분한 명명 표현식과 같은 이유로 해제되지 않습니다. 두 함수 모두 반환된 함수의 클로저에 "갇혀" 있습니다.
그러나 메모리 사용량 증가 문제는 실제로 큰 문제가 아닙니다. Prototype.js와 같은 라이브러리가 이 패턴을 채택하면 1~200개 이상의 함수만 생성됩니다. 이러한 함수가 반복적으로(런타임 시) 생성되지 않고 단 한 번(로드 시) 생성되는 한 걱정할 필요가 없습니다.
WebKit의 displayName
WebKit 팀은 이 문제에 대해 다소 대안적인 전략을 채택했습니다. 익명 및 명명된 함수는 표현력이 매우 낮기 때문에 WebKit에서는 "특수" displayName 속성(기본적으로 문자열)을 도입합니다. 개발자가 함수의 이 속성에 값을 할당하면 디버깅 중에 이 속성의 값이 사용됩니다. 프로그래머나 프로파일러의 "이름" 함수 대신. Francisco Tolmasky는 이 전략의 원칙과 구현을 자세히 설명합니다.

향후 고려 사항
ECMAScript-262(현재 초안)의 향후 버전 5에는 소위 엄격 모드가 도입될 예정입니다. 엄격 모드 구현을 활성화하면 언어를 불안정하고 신뢰할 수 없으며 안전하지 않게 만드는 언어 기능이 비활성화됩니다. 보안상의 이유로args.callee 속성은 엄격 모드에서 "차단"될 것이라고 합니다. 따라서 엄격 모드에서 Argument.callee에 액세스하면 TypeError가 발생합니다(ECMA-262 5판, 섹션 10.6 참조). 여기서 엄격 모드를 언급하는 이유는 5판 표준을 기반으로 한 구현에서 재귀 연산을 수행하기 위해 인수.callee를 사용할 수 없는 경우 명명된 함수 표현식을 사용할 가능성이 크게 높아지기 때문입니다. 이런 의미에서 명명된 함수 표현식의 의미와 버그를 이해하는 것이 훨씬 더 중요합니다.




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

// 이전에는 인수.callee를 사용할 수 있었습니다
(function(x) {
if (x <= 1) return 1;
return x *args.callee( x - 1);
})(10);
// 그러나 엄격 모드에서는 명명된 함수 표현식을 사용할 수 있습니다
(function Factorial(x) {
if (x <= 1) return 1;
return x * Factorial(x - 1);
})(10)
// 한발 물러나 덜 유연한 함수 선언을 사용하세요
x) {
if (x <= 1) return 1;
return x * 계승(x - 1)
factorial(10); >감사의 글
JScript의 명명된 함수 표현식의 버그를 최초로 설명한 Richard Cornford. Richard는 이 게시물에서 내가 언급한 대부분의 버그를 설명하므로 그의 설명을 확인해 보는 것이 좋습니다. 또한 2003년에 comp.lang.javascript 포럼에서 NFE 문제를 언급하고 토론한 Yann-Erwan Perio와 Douglas Crockford에게도 감사의 말씀을 전하고 싶습니다.
John-David Dalton이 "최종 해결 방법"에 대해 훌륭한 제안을 했습니다.
저는 "The Alternative"에서 Toby Langi의 아이디어를 사용했습니다.
Garrett Smith와 Dmitry Soshnikov는 이 기사의 다양한 측면을 추가하고 수정했습니다.

영문 원문: http://kangax.github.com/nfe/
참고 번역: 연결 액세스(SpiderMonkey's Quirks 이후 장에 대해서는 이 기사를 참조하세요)
동기화 및 권장 사항
디렉토리 색인에 동기화된 이 기사: JavaScript 시리즈에 대한 심층적 이해
원본, 번역, 재인쇄 및 기타 유형의 기사를 포함하여 JavaScript 시리즈에 대한 심층적 이해가 도움이 되었다면 추천해 주세요. 삼촌이 글을 쓸 동기를 부여할 수 있도록 지원하세요.
성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.