간단한 질문부터 시작해 보겠습니다:
<script type="text/javascript">
경고(i); //?
var i = 1;
</script>
출력 결과는 정의되지 않습니다. 이 현상을 "사전 구문 분석"이라고 합니다. JavaScript 엔진은 var 변수와 함수 정의를 먼저 구문 분석합니다. 사전 구문 분석이 완료될 때까지 코드가 실행되지 않습니다. 문서 스트림에 여러 스크립트 코드 세그먼트(스크립트 태그로 구분된 js 코드 또는 가져온 js 파일)가 포함된 경우 실행 순서는
step1입니다.
2단계. 구문 분석을 수행합니다. 오류가 있으면 구문 오류(예: 일치하지 않는 대괄호 등)가 보고되고 5단계로 이동합니다.
3단계. var 변수 및 함수 정의를 "사전 구문 분석"합니다(올바른 선언만 구문 분석되므로 오류가 보고되지 않습니다).
step4.코드 세그먼트를 실행하고 오류가 있는 경우(예: 변수가 정의되지 않음) 오류를 보고합니다.
5단계. 다른 코드 세그먼트가 있으면 다음 코드 세그먼트를 읽고 2단계를 반복합니다.
step6. 위의 분석을 통해 많은 문제를 설명할 수 있었지만 항상 뭔가 부족하다는 느낌을 받습니다. 예를 들어 3단계에서 "사전 구문 분석"이란 정확히 무엇입니까? 4단계에서 다음 예를 살펴보세요.
<script type="text/javascript">
경고(i); // 오류: i가 정의되지 않았습니다.
나는 = 1;
</script>
첫 번째 문장에서 오류가 발생하는 이유는 무엇입니까? JavaScript에서는 변수를 정의하지 않아도 되나요?
편찬의 시간은 백마처럼 지나갔고, 마치 저 멀리 있는 것처럼 책장 옆에 『편찬의 원리』를 펼쳐 보았는데, 익숙하면서도 낯선 여백에 다음과 같은 메모가 적혀 있었다.
전통 편찬 언어를 위하여
., 컴파일 단계는 어휘 분석 및 구문 분석, 의미 검사, 코드 최적화 및 바이트 생성으로 구분됩니다.
그러나 해석된 언어의 경우 어휘 분석과 구문 분석을 통해 구문 트리를 얻은 후에 해석과 실행이 시작됩니다.
간단히 말해서 어휘 분석은 c = a - b를 다음과 같이 변환하는 것과 같이 문자 스트림(char 스트림)을 토큰 스트림(토큰 스트림)으로 변환하는 것입니다
.
같음
이름 "아"
마이너스
이름 "b"
세미콜론
위의 내용은 단지 예일 뿐입니다. 자세한 내용은
"The Definitive Guide to JavaScript"의 2장에서 ECMA-262에도 설명되어 있는 어휘 구조에 대해 설명합니다. 어휘 구조는 언어의 기초이며 익히기가 쉽습니다. 어휘 분석의 구현에 관해서는 이는 또 다른 연구 분야이므로 여기서는 다루지 않습니다.
우리는 자연어의 비유를 사용할 수 있습니다. 어휘 분석은 일대일 하드 번역입니다. 예를 들어, 영어 단락이 단어별로 중국어로 번역되면 우리가 얻는 것은 많은 토큰 스트림입니다. 이해하다. 추가 번역에는 문법 분석이 필요합니다. 다음 그림은 조건문의 구문 트리입니다.
구문 트리를 구성할 때 if(a { i = 2; } 와 같이 구성할 수 없는 것으로 확인되면 구문 오류가 보고되고 전체 코드 블록에 대한 구문 분석이 종료됩니다. 이것이 2 단계입니다. 구문
분석을 통해 구문 트리를 생성한 후에도 번역된 문장은 여전히 모호할 수 있으므로 추가 의미 검사가 필요합니다. 전통적인 강력한 형식의 언어에서는 의미 검사의 주요 부분이 다음과 같습니다. 함수의 실제 매개변수 및 형식 매개변수 유형이 일치하는지 여부 JS 엔진의 의미 확인 단계)
JavaScript 엔진의 경우 어휘 분석 및 구문 분석이 있어야 하며 이러한 컴파일 단계가 완료된 후에는 의미 확인 및 코드 최적화와 같은 단계가 있을 수 있습니다. 그러나 해석된 언어는 바이너리 코드로 컴파일되지 않음)
위의 컴파일 프로세스는 여전히 기사 시작 부분의 "사전 구문 분석"을 설명할 수 없습니다.
Zhou Aimin은 "JavaScript 언어의
본질"에서 이에 대해 매우 주의 깊게 분석했습니다
. 구문 트리로 변환된 후
추가 실행이 필요한 구문 트리에 따라 즉시 실행됩니다. JavaScript의 범위 메커니즘을 이해합니다. JavaScript는 어휘 범위를 사용합니다. 즉, 어휘 범위는 소스 코드에 따라 달라지므로 컴파일러는 정적 분석을 통해 이를 결정할 수 있으므로 정적 범위라고도 합니다. eval은 정적 기술을 통해서만 실현될 수 있습니다. 실제로 JS의 범위 메커니즘에 대해서만 이야기할 수 있습니다.
JS 엔진은 각 함수 인스턴스를 실행할 때 실행 컨텍스트를 포함합니다. call 객체는 내부 변수 테이블을 저장하는 데 사용되는 scriptObject 구조입니다. varDecls, 내장 함수 테이블 funDecls 및 상위 참조 목록 upvalue(참고: varDecls 및 funDecls와 같은 정보는 구문 분석 단계에서 구문 트리에 저장됩니다. 함수 인스턴스가 실행되면 이 정보는 구문 트리에서 scriptObject로 복사됩니다. scriptObject는 함수 인스턴스의 수명 주기와 일치하는 함수와 관련된 정적 시스템입니다. .
어휘 범위는 JS의 범위 메커니즘이며 구현 방법도 이해해야 합니다. 이것이 범위 체인입니다. 범위 체인은 이름 조회 메커니즘입니다. 먼저 현재 실행 환경에서 scriptObject를 검색합니다. 찾을 수 없으면 상위 scriptObject에 대한 upvalue를 따라 전역 개체를 찾습니다.
함수 인스턴스가 실행되면 클로저가 생성되거나 연결됩니다. scriptObject는 함수와 관련된 변수 테이블을 정적으로 저장하는 데 사용되며, 클로저는 실행 중에 이러한 변수 테이블과 실행 값을 동적으로 저장합니다. 클로저의 수명주기는 함수 인스턴스의 수명주기보다 길 수 있습니다. 함수 인스턴스는 활성 참조가 비게 되면 자동으로 삭제되고, 데이터 참조가 비면 JS 엔진에 의해 클로저가 재활용됩니다(어떤 경우에는 자동으로 재활용되지 않아 메모리 누수가 발생합니다).
위의 수많은 명사에 겁먹지 마세요. 실행 환경, 객체 호출, 클로저, 어휘 범위, 범위 체인의 개념을 이해하면 JS 언어에서 발생하는 많은 현상을 쉽게 해결할 수 있습니다.
요약 이 시점에서 기사 시작 부분의 질문을 매우 명확하게 설명할 수 있습니다.
3단계의 소위 "사전 구문 분석"은 실제로 2단계의 구문 분석 단계에서 완료되어 구문 트리에 저장됩니다. 함수 인스턴스가 실행되면 varDelcs 및 funcDecls가 구문 트리에서 실행 환경의 scriptObject로 복사됩니다.
4단계에서 정의되지 않은 변수는 scriptObject의 변수 테이블에서 찾을 수 없음을 의미합니다. JS 엔진은 scriptObject의 upvalue를 따라 위쪽으로 검색합니다. 둘 다 발견되지 않으면 쓰기 작업 i = 1이 됩니다. i = 1; 창 개체에 새 속성을 추가합니다. 읽기 작업의 경우 전역 실행 환경으로 추적되는 scriptObject를 찾을 수 없으면 런타임 오류가 발생합니다.
깨닫고 나니 안개가 걷히고 꽃이 피어나고 하늘이 맑아졌습니다.
마지막으로 질문을 남깁니다:
<script type="text/javascript">
var 인수 = 1;
함수 foo(arg) {
경고(인수);
var 인수 = 2;
}
foo(3);
</script>
경고의 출력은 무엇입니까?