개발을 하다보면 this의 범위가 함수, function, bind, call, apply별로 달라지는 걸볼 수 있는데 왜 이런지에 대해서 다뤄보려고 한다.
1. 실행 컨텍스트란?
2. VariableEnvironment
3. LexicalEnvironment
3-1 environmentRecord와 hoisting
호이스팅 규칙
함수 선언문과 함수 표현식
3-2. scope, scope chain, outerEnvironmentReference
스코프 체인
전역변수와 지역변수
4. this
1. 실행 컨텍스트란?
- 실행컨텍스트(execution context)는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체
- 자바스크립트의 동적 언어로서의 성격을 가장 잘 파악할 수 있는 개념
- 어떤 실행 컨텍스트가 활성화되는 시점에 선언된 변수를 위로 끌어 올리고(=호이스팅), 외부환경정보를 구성, this값을 설정 하는 동작 수행
-> 이로인해 다른언어에서는 발견할 수 없는 특이한 현상들이 발생
-
코드 실행시 실행 컨텍스트 순서
- '동일한 환경'에 있는 코드들을 실행할 때 필요한
환경 정보
들을 모아 컨텍스트를 구성 - 이를
콜 스택
에 쌓아 올림 - 가장 위에 쌓여있는 컨텍스트와 관련 잇는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장(자료구조 스택)
- '동일한 환경'에 있는 코드들을 실행할 때 필요한
-
'동일한 환경'(하나의 실행 컨텍스트)을 구성할 수 있는 방법
- 전역공간, eval()함수, 함수 등이 있음
- eval을 제외하면 우리가 흔히 실행 컨텍스트를 구성하는 방법은 함수를 실행하는 것
-
javascript engine 역할
- 어떤 실행 컨텍스트가 활성화될 때 자바스크립트 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는 데 '필요한 환경정보들'을 수집해서 실행 컨텍스트 객체에 저장
- 이 객체는 javascript engine이 활용할 목적으로 생성(개발자가 코드를 통해 확인할 수 없다.)
- 담기는 정보 세가지
: 목차 2, 3, 4에서 각각 더 자세하게 설명하도록 한다.
* VariableEnvironment : 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보. : 선언 시점의 LexicalEnvironment의 스냅샷 : 변경 사항은 반영되지 않음 * LexicalEnvironment : 처음에는 VariableEnvironment와 같지만 변경사항이 실시간으로 반영 * ThisBinding : this 식별자가 바라봐야 할 객체
-
아래 코드의 활성화된 실행 컨텍스트의 수집정보
var a = 1; function outer(){ function inner(){ console.log(a);// undefined; var a = 3; } inner(); console.log(a); } outer(); console.log(a); // * inner // - VariableEnvironment : environmentRecord(snapshot)/ outerEnvironmentReference(snapshot) // - LexicalEnvironment : environmentRecord/ outerEnvironmentReference // - ThisBindding // * outer // * 전역 컨텍스트
2. VariableEnvironment
- LexicalEnvironment와 같지만 최초 실행 시의 스냅 샷을 유지한다는점이 다름
- 실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 먼저 담은 다음,
이를 그대로 복사해서 LexicalEnvironment를 만들어 LexicalEnvironment를 주로 활용하게 된다. -
VariableEnvironment, LexicalEnvironment의 내부는 environmentRecord와 outerEnvironmentReference로 구성
- 초기화 과정 중에는 사실상 완전히 동일, 이후 코드 진행에 따라 서로 달라짐
- 자세한 내용은 LexicalEnvironment를 통해 설명
3. LexicalEnvironment
- 컨텍스트를 구성하는 환경 정보들을 사전에서 접하는 느낌으로 모아 놓은 것
-
environmentRecord와 outerEnvironmentReference로 구성 되어 있다.
-
environmentRecord
- var, function, arguments 등으로 구성
-
outerEnvironmentReference
- 외부 실행 컨텍스트 객체를 참조
- 이것으로 scope chain, closure 가능
-
3-1 environmentRecord와 hoisting
-
호이스팅
- js engine은 컨텍스트 내부 전체를 처음부터 쭉 훑어나가며 순서대로 수집
- 식별자들을 최상단으로 끌어올려놓은 다음 실제코드를 실행(실제로 끌어 올리는 것이 아님, 편의상 끌어올린것으로 간주)
- 실행 컨텍스트가 관여할 코드들은 실행되기 전의 상태
- 코드가 실행되기 전임에도 불구하고 js engine은 이미 해당 환경에 속한 코드의 변수명들을 모두 알고 있게 된다.
호이스팅 규칙
- environmentRecord에는 매개변수의 이름, 함수선언, 변수명 등이 담긴다
-
예시
함수 선언문
: hoisting 대상함수 표현식
: hosisting 대상 x
function a() { console.log(b) // ƒ b() {} var b console.log(b) // ƒ b() {} function b() {} // #함수선언문(hoisting대상) console.log(b) // ƒ b() {} console.log(c) //undefined var c = function() {} // #함수표현식(hoisting대상X) }
함수 선언문과 함수 표현식
-
함수를 정의하는 세가지 방식
- 함수 선언문: 반드시 함수명이 정의 돼 있어야 함 => hoisiting이 됨
- 함수 표현식: 함수명이 없어도 됨 => hoisiting이 안 됨
- 기명 함수 표현식: 함수명을 정의한 함수 표현식 => 함수명으로 호출 안됨
//상대적으로 긴 코드 속에서 함수 표현식이 안전한다. a(); function a() { console.log('a') } //함수 선언문 b(); // is note a function (hoisting 안됨) var b = function() { console.log('b') } //(익명) 함수 표현식 var c = function d() { console.log('c,d') } // 기명 함수 표현식. 변수명: c, 함수명 d c(); // 실행 d(); // error
3-2. scope, scope chain, outerEnvironmentReference
-
scope
- 식별자에 대한 유표범위
- ES5까지의 자바스크립트는 특이하게도 전역 공간을 제외하면 오직 함수에 의해서만 스코프가 생성
-
scope chain
- 이런 '식별자의 유효범위'를 안에서부터 바깥으로 차례로 검색해나가는 것
outerEnvironmentReference
에 의해서 가능하다.- outerEnvironmentReference: 스코프체인을 가능하게 하는 것이 바로 LexicalEnvironment의 두번째 수집자료
scope chain
-
outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조
- 아래 예시1 scopechain 도식화 참고
-
과거 시점인 '선언될 당시'는 행위가 실제로 일어 날 수 있는 시점이란?
- 콜 스택 상에서 어떤 실행 컨텍스트가 활성화된 상태일 때뿐
- 모든 코드는 실행 컨텍스트가 활성화 상태일 때 실행되기 때문
-
scope chain 예1
- A함수 내부에 B함수 선언, B함수 내에 C함수 선언
- 함수 C의 outerEnvironmentReference는 함수 B의 LexicalEnvironment를 참조
- 함수 B의 LexicalEnvironment에 있는 outerEnvironmentReference는 다시 함수 B가 선언되던 때 A함수의 LexicalEnvironment를 참조
- 이처럼 outerEnvironmentReference는 연결리스트 형태를 띤다.
- '선언 시점의 LexicalEnvironment'를 계속 찾아 올라가면 마지막엔 전역 컨텍스트의 LexicalEnvironment가 있을 것'
-
위 예시로본 특징
- 예시 3번에서 본것 처럼 각 outerEnvironmentReference는 오직 자신이 선언된 시점의 LexicalEnvironment만 참조하고 있으므로 가장 가까운 요소부터 차례대로만 접근할 수 있고 다른 순서로 접근하는 것은 불가능
- 이런 구조적 특성 덕분에 여러 스코프에서 동일한 식별자를 선언한 경우에는 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능
-
scope chain 예1 코드
/*01*/ var a = 1; /*02*/ var outer = function() { /*03*/ var inner = function() { /*04*/ console.log(a); // 1 출력 /*05*/ var a = 3; /*06*/ } /*07*/ inner(); /*08*/ console.log(a); // 1 출력 /*09*/ } /*10*/ outer(); /*11*/ console.log(a);
-
scope chaing 예1 코드 도식화
- 그림을 보면서 라인별로 어떤 동작을 하는지 생각해보자
- LE, e, o 의미
LE: Lexical Envinronment e: environmentRecord o: outerEnvironmentReference
- 전역 컨텍스트 활성화
- 0: LexicalEnvironment, VariableEnvironment, thisBinding
- 전역 컨텍스트 생성/ outer 함수 호출
- 1,2: a에 1, outer에 함수 할당 - 10: outer 함수호출, 전역 컨텍스트 비활성화 - 2: outer실행 컨텍스트 활성화
- outer 컨텍스트 생성/ inner 함수 호출
- 3: inner에 함수 할당 - 7: inner 함수 호출, outer 실행 컨텍스트 비활성화 - 3: inner 실행 컨텍스트 활성화
- inner 함수 수행
- 4: inner의 LE에서 a 탐색 -> undefined 출력 - 5: a에 3할당
- inner 컨텍스트 종료
- 6: inner 함수 종료, inner 실행 컨텍스트 제거 - 7: outer 실행 컨텍스트 재활성화 - 8: outer의 LE에서 a탐색 -> GLOBAL의 LE에서 a 탐색 -> 1출력
- outer 컨텍스트 종료
- 9: outer함수 종료, outer 실행 컨텍스트 제거 - 10: 전역 컨텍스트 재활성화 - 11: GLOBAL의 LE에서 a탐색 -> 1출력
- 전역, outer, inner execute context
"전역 컨텍스트": { environmentRecord: 'a, outer function', outerEnvironmentReference: null, this: window } "outer컨텍스트": { environmentRecord: ['inner function'], outerEnvironmentReference: 'GLOBAL LexicalEnvironment', this: window } "inner컨텍스트": { environmentRecord: ['a'], outerEnvironmentReference: 'outer 컨텍스트', this: window }
전역변수와 지역변수
- 전역변수: 전역 공간에서 선언
- 지역변수: 함수 내부에서 선언
3-3. environmentRecord와 closure
- 생성된 함수 객체는 [[Scopes]] 프로퍼티를 가지게 된다.
- [[Scopes]] 프로퍼티는 함수 객체만이 소유하는 내부 프로퍼티(Internal Property)로서 함수 객체가 실행되는 환경을 가리킨다.
- 따라서 현재 실행 컨텍스트의 스코프 체인이 참조하고 있는 객체를 값으로 설정한다.
- 내부 함수의 [[Scopes]] 프로퍼티는 자신의 실행 환경(Lexical Enviroment)과 자신을 포함하는 외부 함수의 실행 환경과 전역 객체를 가리키는데 이때 자신을 포함하는 외부 함수의 실행 컨텍스트가 소멸하여도 [[Scopes]] 프로퍼티가 가리키는 외부 함수의 실행 환경(Activation object)은 소멸하지 않고 참조할 수 있다.
- 이것이 클로저이다.
-
예
- 내부함수 bar()는 외부함수 foo()의 실행컨텍스트, 전역객체를 가르키는데
- 이때 외부함수 foo()의 실행컨텍스트가 소명해도
- scope 프로퍼티가 외부 함수의 실행환경은 소멸하지 않고 참조할 수 있다.
var x = 'xxx'; function foo () { var y = 'yyy'; function bar () { var z = 'zzz'; console.log(x + y + z); } bar(); } foo();
4. this
- 실행컨텍스트의 thisBinding에 this로 지정된 객체가 저장
- 실행컨텍스 활성 당시 this가 지정되지 않은경우 "전역객체" 저장
- 함수 호출하는 방법에 따라 this가 달라진다.(this에 대해서만 내용을 다룰 예정)
참고
- 코어 자바스크립트 - 위키북스
- 인사이드 자바스크립트
- 자바스크립트 완벽 가이드
- 제로초 블로그
- poiema