언어/자바스크립트

[Javascript] 스코프, 스코프 체인, outerEnvironmentReference

youngble 2022. 6. 16. 21:11

스코프, Scope

스코프는 식별자에 대한 유효범위를 나타낸다.

어떤 경계 A의 외부에서 선언한 변수는 A의 외부와 내부에서 접근이 가능하지만 A 내부에서 선언한 경우는 오직 내부에서만 접근이 가능하다. 다른 언어 역시 스코프라는 개념을 가지고있지만 자바스크립트의 경우 ES5까지는 오직 함수에 의해서만 스코프가 생성된다.

또한 식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것스코프 체인이라고 한다. 이를 가능하게 하는 것이 바로 LexicalEnvironment의 두번째 수집 자료인 outerEnvironmentReference 이다.

그전에 올렸던 실행컨텍스트의 3가지 정보수집의 그림을 다시보면 알수있다.

스코프 체인, Scope Chain

outerEnvironmentReference현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다.

쉽게 말해 상위 영역의 environmentRecord 정보를 참조저장한다고 생각하면된다.

예를들어 A,B,C 라는 함수가 있고 A함수 내부에 B를 선언하고 B함수 내부에서 C함수를 선언했다고 한다면,

함수 C의 outerEnvironmentReference는 함수B의 LexcialEnvironment를 참조한다.

함수 B의 LexicalEnvironment에 있는 outerEnvironmentReferenceA의LexicalEnvironment를 참조한다.

이처럼 outerEnvironmentReferece연결리스트(linked list)형태를 띈다.

선언 시점의 LexicalEnvironment를 계속 찾아올라가다보면 전역컨텍스트의 LexicalEnvironment가 있을거다.

또한 각 outerEnvironmentReferece는 오직 자신이 선언된 시점의 LexicalEnvironment만 참조하고 있으므로 가장 가까운 요소부터 차례대로만 접근할수있고 다른 순서로 접근하는 것은 불가능하다.

이는 여러 스코프에서 동일한 식별자를 선언한 경우에는 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능하다.

 

코드 예시

var a =1;
var outer = function(){
    var inner = function(){
        console.log(a);
        var a =3;
    };
    inner();
    console.log(a);
};
outer();
console.log(a);

위와 같이 코드가 있다고한다면 inner함수안에서 변수 a가 선언되고 다시 할당되는걸 볼수있다.

이렇게 여러 스코프에서는 전역컨텍스트에서 선언할당된 a값을 불러오지 않고 inner함수 스코프체인에서의 가장먼저 발견된 선언 inner함수에서의 a로 접근하게 된다는 것이다. 

결과

undefined
1

따라서 결과는 undefined 와 1이 나온다.

inner함수에서 호이스팅이 이루워지면 a가 선언되고 그다음 console.log(a)를 실행하면 선언만 될뿐 값이 할당되지 않았기때문에 undefined가 나오는것이다. 만약 inner함수내에서 a가 선언되지않았고 consol.log(a)만 있었다면, 외부로 outerEnvironmentReference를 찾아가면서 해당 a의 값을 찾을것이다.

 

LexicalEnvironment 내부 정보가 담기는 순서

1. LexicalEnvironment 와 그안에있는 environmentRecord, outerEnvironmentReference에 무엇이 저장되고 담기는지 알아보면

먼저 전역컨텍스트의 environmentRecord{a, outer} 식별자를 저장한다(outer 함수도 식별자이다. 식별자라는 정의에 해당하는게 무엇인지 찾아보면안다). 하지만 전역컨텍스트의 outerEnvironmentReference에는 선언 시점이 없으므로 아무것도 담기지 않는다.

 

2.  outer 실행컨텍스트가 활성화되면서 전역컨텍스트는 임시중지되고 outer 실핼컨텍스트의 environmentRecord에는 {inner} 식별자를 저장한다. outerEnvironmentReference에는 outer 함수가 선언될 당시의 LexicalEnvironment가 담긴다. 따라서 outer함수는 전역 공간에서 선언됐으므로 전역 컨텍스트의 LexicalEnvironment를 참조복사한다. 위에서 전역 컨텍스트는 {a, outer} 였기에 이를 담으면 되는데 이때 [ Global, {a , outer}]라고 표기하고 저장한다. 첫번째 Global은 실행 컨텍스트의 이름이고, 두번째가 environmentRecord 객체이다.

 

3. inner 함수가 호출되면 outer 실행컨켄스트가 임시중단되고 inner 실행컨텍스트가 콜스택에 담기고 활성화되고 이때 inner 실행컨텍스트의 environmentRecord에  {a} 식별자가 저장한다. outerEnvironmentReference에는 inner함수가 선언될 당시LexicalEnvironment를 참조복사하는데 inner함수는 outer함수 내부에서 선언되었으므로 outer함수의 LexicalEnvironment를 저장한다. 이때 outer의 environmentRecord에는 {inner} 식별자가 저장되었으므로 [outer, {inner}]가 inner 실행컨텍스트 outerEnvironmentReference로 담긴다.

 

4. inner 내부에서 console.log(a)로 인해서 a라는 식별자에 접근하고자 하는데 이때 활성화된 inner 컨텍스트의 environmentRecord에서 a를 검색하는데 이때 3번째에 나온 inner의 environmentRecord에는 {a}의 식별자가 저장되어있으므로(호이스팅) 이를 발견하는데 아직 할당된 값이 없으므로 undefined가 출력되는 것이다. 그후 a = 3 을 할당한다고해도 inner 실행컨텍스트는 콜스텍에서 완료 제거 되므로 이는 남아있지 않게된다. 다시 outer함수 임시중단된 코드부터 실행되는데 outer함수도 더이상 실행할 코드가 없으므로 콜스택에서 제거되고 전역컨텍스트가 임시중단된 코드에서 다시 실행된다.

 

5. 전역 컨텍스트가 임시중지된 시점이 다시 활성화되고 마지막 코드인 console.log(a)는 전역 컨텍스트의 LexicalEnvironmentenvironmentRecord{a, outer} 식별자가 담겨있으므로 이를 검색하고 할당된 1을 반환한다.

 

6. 마지막으로 전역컨텍스트가 실행할 코드가 끝났으므로 콜스택에서 제거되고 종료된다.

 

이를 더 정확히 확인하기 위해서 크롬, 사파리의 개발자도구에서 console.dir(inner) 를 사용하거나 debugger를 코드에 넣어주면된다.

 

(1). console.dir(inner)

var a =1;
var outer = function(){
    var inner = function(){
        console.log(a);
        var a =3;
        console.dir(inner);
    };
    inner();
    console.log(a);
};
outer();
console.log(a);

 

(2). debugger

var a =1;
var outer = function(){
    var inner = function(){
        console.log(a);
        var a =3;
        debugger;
    };
    inner();
    console.log(a);
};
outer();
console.log(a);

전역변수와 지역변수

결국 위에 나와있는 변수들은 전역변수나 지역변수라고 말한다. 여기서 a와 outer는 전역변수에 해당하고, outer함수내부에 있는 inner나 다른 변수 선언은 지역변수에 해당한다.

 

이렇게 전체적인 스코프, 스코프체인, LexicalEnvironment의 outerEnvironmentReference에 대해서 알아보았는데 이때 포커싱한건 var이다. es6부터 생긴 let, const 등에 대해선 나중에 다뤄보도록 한다.