어휘 범위: 변수의 범위는 변수가 실행될 때가 아니라 정의할 때 결정된다. 즉, 어휘 범위는 변수에 따라 결정된다. 소스 코드는 정적 분석을 통해 이를 확인할 수 있으므로 어휘 범위를 정적 범위라고도 합니다. with와 eval을 제외하면 JS의 범위 메커니즘은 어휘 범위(Lexical Scope)에 매우 가깝다고 할 수 있습니다.
몇 가지 작은 사례를 통해 JS 실행 중 어휘 범위와 종결을 이해하는 데 필수적인 몇 가지 기본 개념과 이론적 지식에 대한 심층적인 이해를 얻기 시작할 것입니다.
1. 클래식 케이스 1
답변: 네, 10개가 나옵니다. 구체적인 실행 과정은 다음과 같아야 합니다
a 함수에는 형식 매개변수 i가 있습니다. a 함수를 호출하면 실제 매개변수 10이 전달되고 형식 매개변수 i=10이 됩니다.
그런 다음 로컬 동일한 이름의 변수 i가 정의되어 있는데 값이 할당되지 않았습니다.
2. 클래식 케이스 2
a 함수에는 형식 매개변수 i가 있습니다. a 함수 호출 시 실제 매개변수 10이 전달되고, 형식 매개변수 i=10
첫 번째 경고에서는 값 10이 출력됩니다. 형식 매개변수 i의
두 번째 경고는 i여야 하는 인수[0]을 출력합니다.
그런 다음 지역 변수 i를 정의하고 값 2를 할당합니다. 이때 지역 변수 i=2
세 번째 경고는 지역 변수 i의 값을 2로 설정합니다. 출력
네 번째 경고는 다시 인수[0]을 출력합니다.
3. 클래식 케이스 3
첫 번째 문장은 형식 매개 변수 i와 동일한 이름을 가진 지역 변수 i를 선언합니다. 결과에 따르면 우리는 후자의 i가
에 할당하는 것과 동일합니다. 출력 10
4. 클래식 케이스 4
alert(i)
var i; = 2;
경고(i)
a();
疑问:上面的代码又会输出什么呢?(小子,看这回整不死你!哇哈哈,就不给你选项)
答案:在FireBug中的运行结果是 undefined, 2,下面简单说一下具体执行过程
第一个alert输出undefined
第二个alert输出 2
思考:到底怎么回事儿?
5、经典案例五…………..N 看到上面的几个例子,你可能会想,怎么可能,我写了几年的 js 了,怎么这么简单例子也会犹豫,结果可能还答错了。其实可能原因是:我们能很快的写出一个方法,但到底方法内部是怎么执行的呢?执行的细节又是怎么样的呢?你可能没有进行过深入的学习和了解。要了解这些细节,那就需要了解 JS 引擎的工作方式,所以下面我们就把 JS 引擎对一个方法的解析过程进行一个稍微深入一些的介绍
解析过程
1、执行顺序
- 编译型语言,编译步骤分为:词法分析、语法分析、语义检查、代码优化和字节生成。
- 解释型语言,通过词法分析和语法分析得到语法分析树后,就可以开始解释执行了。这里是一个简单原始的关于解析过程的原理,仅作为参考,详细的解析过程(各种JS引擎还有不同)还需要更深一步的研究
JavaScript执行过程,如果一个文档流中包含多个script代码段(用script标签分隔的js代码或引入的js文件),它们的运行顺序是:
- 步骤1. 读入第一个代码段(js执行引擎并非一行一行地执行程序,而是一段一段地分析执行的)
- 步骤2. 做词法分析和语法分析,有错则报语法错误(比如括号不匹配等),并跳转到步骤5
- 步骤3. 对【var】变量和【function】定义做“预解析“(永远不会报错的,因为只解析正确的声明)
- 步骤4. 执行代码段,有错则报错(比如变量未定义)
- 步骤5. 如果还有下一个代码段,则读入下一个代码段,重复步骤2
- 步骤6. 结束
2、特殊说明
全局域(window)域下所有JS代码可以被看成是一个“匿名方法“,它会被自动执行,而此“匿名方法“内的其它方法则是在被显示调用的时候才被执行
3、关键步骤
上面的过程,我们主要是分成两个阶段
- 解析:就是通过语法分析和预解析构造合法的语法分析树。
- 执行:执行具体的某个function,JS引擎在执行每个函数实例时,都会创建一个执行环境(ExecutionContext)和活动对象(activeObject)(它们属于宿主对象,与函数实例的生命周期保持一致)
3、关键概念
到这里,我们再更强调以下一些概念,这些概念都会在下面用一个一个的实体来表示,便于大家理解
- 语法分析树(SyntaxTree)可以直观地表示出这段代码的相关信息,具体的实现就是JS引擎创建了一些表,用来记录每个方法内的变量集(variables),方法集(functions)和作用域(scope)等
- 执行环境(ExecutionContext)可理解为一个记录当前执行的方法【外部描述信息】的对象,记录所执行方法的类型,名称,参数和活动对象(activeObject)
- 活动对象(activeObject)可理解为一个记录当前执行的方法【内部执行信息】的对象,记录内部变量集(variables)、内嵌函数集(functions)、实参(arguments)、作用域链(scopeChain)等执行所需信息,其中内部变量集(variables)、内嵌函数集(functions)是直接从第一步建立的语法分析树复制过来的
- 词法作用域:变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。 with和eval除外,所以只能说JS的作用域机制非常接近词法作用域(Lexical scope)
- 作用域链:词法作用域的实现机制就是作用域链(scopeChain)。作用域链是一套按名称查找(Name Lookup)的机制,首先在当前执行环境的 ActiveObject 中寻找,没找到,则顺着作用域链到父 ActiveObject 中寻找,一直找到全局调用对象(Global Object)
4、实体表示
분석 시뮬레이션
이것을 본 후에도 여전히 다들 혼란스러워하는 것 같습니다. 구문 분석 트리가 무엇인지, 구문 분석 트리는 어떻게 생겼는지, 범위 체인이 어떻게 구현되는지, 활성 객체의 내용은 무엇인지 등이 여전히 그렇습니다. 너무 명확하지는 않지만 아래에서는 실제 코드 조각을 통해 전체 구문 분석 프로세스를 시뮬레이션하고 실제로 구문 분석 트리와 활성 개체를 생성하고 범위와 범위 체인 구현 방법을 이해합니다
1 . 시뮬레이션 코드
/*전역( window) 도메인 코드 조각*/
var i = 1,j = 2,k = 3
function a(o,p,x,q){
var x = 4; >alert(i );
function b(r,s) {
var i = 11,y = 5
alert(i)
function c(t){
var z = 6;
alert(i);
//함수 표현식
var d = function(){
alert(y)
c(60);
d()
};
b(40,50)
a(10,20,30)
2 , Parse tree
위의 코드는 매우 간단합니다. 먼저 일부 전역 변수와 전역 메서드를 정의한 다음 메서드 내에서 로컬 변수와 로컬 메서드를 정의합니다. 이제 JS 인터프리터가 이 코드를 읽고 구문 분석을 시작합니다. 앞에서 언급했듯이 JS 엔진은 먼저 구문 분석 및 사전 구문 분석을 통해 구문 분석 트리를 얻습니다. 구문 구문 분석 트리의 모양과 여기에 포함된 정보에 대해서는 아래에서 간단한 구조인 JS 객체를 사용합니다. 구문 분석 트리를 설명하기 위해 다양한 개체(여기서는 단지 의사 개체 표현일 뿐이며 실행되지 않을 수 있음)를 나타냅니다(이것은 우리에게 더 친숙한 것입니다. 실제 구조는 자세히 다루지 않을 것입니다. 훨씬 더 복잡할 것입니다) .파싱 과정의 이해를 돕기 위해 특별히 작성되었습니다.)
코드 복사
variables:{
i: { 값:1},
j:{ 값: 2},
k:{ 값:3}
},
함수:{
a: this.a
}
},
a:{
변수:{
x:"정의되지 않음"
},
함수:{
b: this.b
},
범위: this.window
} ,
b:{
변수:{
y:"정의되지 않음"
},
함수:{
c: this.c,
d: this .d
},
범위: this.a
},
c:{
변수:{
z:"정의되지 않음"
},
함수:{},
범위: this.b
},
d:{
변수:{},
함수:{},
범위: {
myname:d,
범위: this.b
}
}
}
위는 파스 트리를 간단히 표현한 것인데 앞서 분석한 것처럼 파스 트리는 주로 각 함수에 변수 세트(variables), 메소드 세트(functions), 스코프(scope)를 기록합니다.
파싱 트리의 핵심 사항 파싱 트리
1개의 변수 세트(변수), 변수 정의만 있고 변수 값은 없습니다. 이때 변수 값은 모두 "정의되지 않음"
2개의 범위(scope)입니다. 어휘 범위의 특성에 따라 각 변수의 범위는 현재 명확하며 실행 환경에 따라 변경되지 않습니다. 【무슨 뜻인가요? 즉, 메소드를 반환한 다음 다른 메소드에서 실행하는 경우 메소드가 정의된 범위를 기준으로 메소드의 변수 범위가 결정됩니다. 사실 여기서 표현하고 싶은 것은 메소드를 아무리 복잡하고 원격으로 실행하더라도 메소드의 변수에 접근할 수 있는지 여부에 대한 최종 판단은 메소드가 정의된 곳으로 돌아가서 검증해야 한다는 것입니다. ]
3 범위(scope) 규칙 설정
a 함수 선언 및 익명 함수 표현식의 경우 [범위]는 생성 당시의 범위입니다.
b 명명된 함수 표현식의 경우 [범위]의 상단은 새 JS 객체(즉, Object.prototype을 상속함) 이 객체에는 두 가지 속성이 있습니다. 첫 번째는 자체 이름이고, 두 번째는 정의된 범위입니다. 오류 없이 자체적으로 함수 이름을 지정합니다.
3. 실행 환경 및 활성 개체
구문 분석이 완료되고 코드 실행이 시작됩니다. 각 메소드를 호출하면 JS 엔진이 자동으로 실행 환경과 이에 대한 활성 객체를 설정합니다. 이는 메소드 인스턴스의 수명 주기와 일치하며 위 메소드에 대해 필요한 실행 지원을 제공합니다. 이를 위해 활성 객체가 일률적으로 설정됩니다(이론적으로는 메소드가 실행될 때 활성 객체가 생성됩니다. 설명의 편의를 위해 여기에서 모든 메소드의 활성 객체를 한 번에 정의합니다).
실행 환경
코드 복사
코드는 다음과 같습니다.
/**
* 실행 환경 : 함수가 실행될 때 생성되는 실행 환경
*/
var ExecutionContext = {
window: {
type: "global",
name: "global",
body: ActiveObject.window
},
a:{
유형: "function",
이름: "a",
body: ActiveObject.a,
scopeChain: this .window.body
},
b:{
유형: "함수",
이름: "b",
body: ActiveObject.b,
scopeChain: this.a.body
},
c:{
유형: "함수",
이름: "c",
body: ActiveObject.c,
scopeChain : this.b.body
},
d:{
유형: "함수",
이름: "d",
body: ActiveObject.d,
scopeChain: this.b.body
}
}
위 각 메소드의 실행 환경에는 해당 메소드 유형(함수), 메소드 이름(funcName) 및 활성 상태가 저장됩니다. 객체(ActiveObject), 범위 체인(scopeChain) 및 기타 정보의 핵심은 다음과 같습니다.
body 속성, 현재 메소드의 활성 객체를 직접 가리킵니다.
scopeChain 속성, 범위 체인. 은 연결된 목록 구조이며, 구문 분석 트리에서 현재 메소드에 해당하는 범위 속성에 따라 해당 범위에 해당하는 메소드의 활성 객체(ActivceObject)를 가리킵니다. 변수 검색은 이 체인을 따라 활성 개체
/**
* 활성 개체: 해당 기능이 실행될 때 생성되는 활성 개체 목록
*/
var ActiveObject = {
창: {
변수:{
i: { 값:1},
j: { 값:2},
k: { 값:3}
},
함수:{
a: this.a
}
},
a:{
변수:{
x: {값:4}
},
함수:{
b: SyntaxTree.b
},
매개변수:{
o: {값: 10},
p: {값: 20},
x: this.variables.x,
q: "정의되지 않음"
},
인수:[this.parameters.o,this.parameters.p ,this.parameters.x]
},
b:{
변수:{
y:{ 값:5}
},
함수:{
c: SyntaxTree.c,
d: SyntaxTree.d
},
매개변수:{
r:{값:40},
s:{값:50}
},
인수:[this.parameters.r,this.parameters.s]
},
c:{
변수:{
z:{ 값:6}
},
함수:{},
매개변수: {
u:{값:70}
},
인수:[this.parameters.u]
} ,
d:{
변수:{},
함수:{},
매개변수:{},
인수:[]
}
}
위의 각 활성 개체에는 해당 메소드가 저장되어 있습니다. 내부 변수 집합(변수), 내장 함수 집합(함수), 형식 매개변수(매개변수), 실제 매개변수(인수) 및 기타 실행에 필요한 정보, 활성 객체의 핵심
구문 분석을 통해 활성 객체를 생성하고 트리 복사 방식
메소드의 내부 변수 집합(변수)과 내장 함수 집합(함수)이 실행되기 시작하며, 활성 객체에 설정된 내부 변수는 모두 정의되지 않음으로 재설정
형식 매개변수(매개변수)와 실제 매개변수(인수) 개체를 생성하고, 동일한 이름의 실제 매개변수, 형식 매개변수와 변수는 [참조] 관계
실행 메소드의 할당문을 사용하면 변수 세트의 변수에 값이 할당됩니다.
변수 검색 규칙은 먼저 현재 실행 환경의 ActiveObject에서 검색하고, 찾지 못한 경우 가리키는 ActiveObject에서 검색합니다. Global Object(window)
메소드가 실행될 때까지 실행 환경에서 ScopeChain 속성에 의해 내부 변수 값이 재설정되지 않습니다. 변수가 소멸되는 시점은 다음
을 참조하세요. 메소드 내 변수의 주기는 메소드 인스턴스에 대한 활성 참조가 있는지 여부에 따라 달라집니다. 그렇지 않으면 활성 객체가 파괴됩니다.
6 및 7은 클로저가 외부 변수에 액세스할 수 있도록 합니다. 처리의 근본 원인은 다음과 같습니다. 같은 이름의 변수와 형식 매개변수는 같은 메모리 주소를 참조]하므로 두 번째 경우의 수정된 인수가 지역 변수에 영향을 미치는 상황이 발생합니다
사례 4
[JS Engine 변수 검색 규칙에 따라 현재 실행 환경의 ActiveObject에서 먼저 검색하고, 찾을 수 없으면 실행 환경에서 ScopeChain 속성이 가리키는 ActiveObject를 따라 Global Object(창)까지 검색합니다. ], 즉 네 번째는 현재 변수 i의 정의를 ActiveObject에서 찾았으나 값이 "undefine"이어서 "undefine"이 직접 출력되었기 때문입니다
요약
위 내용은 JS를 좀 더 깊게 이해하고 적용하기 위해 일정 기간 동안 JS를 배우고 사용했는데, 클로저를 학습하는 과정에서 어휘 범위에 대한 이해와 요약이 있을 수 있습니다. 나는 이 문제를 시스템 디자이너가 아닌 새로운 프런트엔드 개발자의 관점에서만 분석하고 있기 때문에 JS 개발자가 이 문제의 범위를 이해하는 데 도움이 되기를 바랍니다. 방법!