티스토리 뷰
함수가 정의될 때 결정되는 것 : scope (클로저, 콜백의 행동을 이해하는 것과 큰 관련 있다)
함수가 호출될 때 결정되는 것 : this
> 다음을 실행 컨텍스트를 통해 이해해 보자.
( ※ 이글은 모던 자바스크립트 Deep Dive 책을 토대로 작성했습니다. 인용 박스 안의 설명들은 책에서 인용! )
실행 컨텍스트
실행 가능한 코드가 실행되기 위한 환경
(실행 가능한 코드를 형상화하고 구분하는 추상적인 개념)
실행가능한 코드 (executable code)의 단위는 전역코드, 함수코드, eval 코드로 분류
[1] 실행 순서 = 실행 컨텍스트가 push, pop되는 스택으로 관리됨. ( 실행 제어권은 스택 탑이 갖고 있음 )
[2] 실행에 필요한 정보 = 실행 컨텍스트가 식별자, this, Scope 등의 정보를 담고 있는 렉시컬 환경을 참조하는 방식으로 관리.
1.
구현된 코드레벨까지 내려가보진 않았지만, "추상적인" 개념인 실행 컨텍스트는 스택과 렉시컬 환경을 엮기 위한 매개체 역할을 하기 위해 필요한 것으로 사료된다.
실행컨텍스트들이 스택에 push, pop되는 방식으로 실행 순서를 관리하고, 그 실행 컨텍스트가 실행을 위해 필요한 정보 (이를 테면 x+3을 수행하려는데 x가 10이라는 배경지식이 필요한 것 처럼) 들인 렉시컬 환경을 참조하는 방식으로
코드를 실행하기 위한 환경들이 관리된다.
🍺 자바스크립트 엔진이 코드를 실행하는 방식은
실행가능한 코드 (전역 코드, 함수 코드 등) 단위로,
1. 호출되었을 때 (추상적인) 실행컨텍스트란게 생성되어 스택에 push 되고 (이제 얘가 스택 탑)
2. 선언문등을 평가해서 실행에 필요한 정보들을 담는 렉시컬 환경을 생성, 참조하고
3. 드디어 한줄 한줄 실행
4. 종료시 이 실행컨텍스트를 스택에서 pop하고 제어권을 다시 아래에 있는 실행컨텍스트에 넘긴다.
이때 선언문은 2에서 일반 실행문은 3에서 평가되기 때문에 밑에서 설명할 호이스팅이라는 현상이 나타난다.
2.
처음에는 실행 컨텍스트 내부에 정보들이 바인딩 되어 있다고 생각했는데, 아니었다.
실행컨텍스트와 렉시컬 환경은 구분해서 생각해야 한다.
왜나면 실행컨텍스트는 pop돼서 사라졌더라도 렉시컬 환경을 참조하는 애가 있으면, 가비지 컬렉터가 지우지 않고 남겨둔다.
- 전역 실행컨텍스트는 실행이 완료되어 스택에서 사라지더라도 (어차피 현재 이 코드 단위가 제어권을 가지고 있다는 추상적인 표현일 뿐?? ) 전역객체 (브라우저로 치면 window)는 모든 작업이 종료되지 않는 한 계속 살아있는 것.
- 생명주기를 다한 외부함수의 실행 컨텍스트는 사라져도, 그 외부함수가 참조하던 렉시컬 환경은 내부함수 (클로저)가 [[Environment]] 프로퍼티로 참조하고 있기 때문에 살아있는 것.
- 콜백함수를 정의했던 실행컨텍스트는 이미 종료되어 스택에서 pop되고 없어졌어서도, 비동기 작업 종료후 스택에 올려진 콜백 함수는 [[Environment]] 프로퍼티가 가리키던 그 렉시컬 환경을(자기가 정의됐던) 외부 scope로 참조하기 때문에 변수등을 사용할 수 있는 것
등이 그 예시이다.
그리고 같은 함수를 여러번 호출하더라도 한번 생성해둔 렉시컬 환경을 재사용 하는 것이 아니라
몇번이고 호출될 때마다 렉시컬 환경을 생성한다.
이 때문에 같은 외부 함수를 각자 호출해서 생성한 클로저들은 서로 독립적이다.
3.
함수가 호출될때 생성되는 렉시컬 환경이 가리키는 정보들은 크게 3가지가 있는데
- 환경 레코드 ( 지역변수, 매개변수, 객체, 함수 등 )
- this
- 상위 scope (연결 리스트 형태로 타고 타고 계속 상위로 참조 가능)
이때 this는 밑에서 더 자세히 설명하겠지만 이 함수가 어떤 방식으로 호출됐는지에 따라 어떤 객체를 바인딩 할지가
동적으로 결정된다.
( 일반함수로 호출? (객체). 붙여서 메소드로 호출? new 키워드 붙여 생성자 함수로 호출? )
반면 렉시컬 환경이 참조하는 상위 scope는
(1) 스택 상에서 아래에 있는 실행컨텍스트가 참조하는 렉시컬 환경이 아니고!!!
(2) 함수 객체의 [[Environment]] 프로퍼티 (정확히는 내부 슬롯) 가 참조하는 렉시컬 환경이다. 그것은 바로 함수 선언문이나 표현식등이 평가 되서 (즉 정의 될 때) 함수 객체가 생성될 때의 스택 탑이 가리키는 렉시컬 환경이다.
(1) 의 방식이었다면 자바스크립트는 동적 스코프 언어가 됐을 것이다. 하지만 자바스크립트는 렉시컬 스코프(정적 스코프) 언어라는 것!
4.
그리고 ES6의 let,const는 함수가 아닌 블록 스코프를 가지는데
실행중에 블럭을 만나면, 실행 컨텍스트가 기존 렉시컬 환경을 참조하다가 블럭의 렉시컬 환경을 참조하는 방식으로
이것이 가능하게 한다.
( 코드 블록이 실행될 때마다 독립적인 렉시컬 환경을 생성하여 식별자의 값을 유지한다 )
호이스팅
선언문이 코드의 선두로 끌어 올려진 것처럼 동작하는 자바스크립트 고유의 특징
var, let, const, function, function*, class 키워드를 사용해 선언하는 모든 식별자는 호이스팅 된다.
위에서 설명한대로 자바스크립트 엔진이 함수단위 (혹은 블럭단위)로 소스코드를
(1) 렉시컬 환경 생성 단계 ( 선언문 여기서 ) (2) 한줄 한줄 실행단계로
나누어 처리하기 때문에 이런 현상이 나타난다.
let, const, class로 선언된 것들은 호이스팅이 발생하지 않는 것처럼 보이지만 그렇지 않다.
let foo = 1;
{
console.log(foo); //ReferenceError: Cannot access 'foo' before initialization [ TDZ ]
let foo=2
}
호이스팅으로 "foo"라는 이름 (식별자)는 뺏었는데 아직 초기화가 안되서 참조 불가.
렉시컬 환경에 식별자 등록은 했는데 uninitialized!.
(undefined도 값은 값인데 여긴 아예 아무 값도 안 들어 있다고 보면 됨)
var은 (1) 단계 에서 식별자 등록하면서 undefined로 초기화까지 해버리는데,
let, const는 (2)의 한줄 한줄 실행 단계에서 선언문 만나야 undefined로 초기화든, 값 할당이든 함
( 참고 🖐 )
var, let, const 비교
var : 함수 레벨스코프
let, const : 블록 레벨 스코프
var : 상관 X
let : 재선언 불가
const : 재선언, 재할당 불가 ( 상수 )
let, const 키워드로 선언한 전역변수는 전역 객체의 프로퍼티가 아니다.
ES6부터는 var은 사용하지 않도록 한다.
변경이 발생하지 않을 경우 const, 재할당이 필요한 경우에 한정해 let 키워드 사용하기
클로저
외부 함수보다 중첩함수가 더 오래 유지되는 경우 중첩함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다.
이러한 중첩함수를 클로저 closure 라고 한다.
클로저도, 콜백도 마찬가지로 모든 함수 객체들은 [[Environment]] 프로퍼티에 자기가 생성됐을 때의 상위 scope에 대한 참조값을 저장해 두고 있기 때문에 가능.
이렇게 아직 참조되고 있는 렉시컬 환경은 가비지 컬렉터가 메모리에서 해제하지 않는다.
(최적화를 통해 후에 쓰일 변수등만 살려두므로 그렇게 메모리를 많이 잡아먹지는 않는다고 함.)
this 바인딩
자바나 c++같은 클래스 기반 언어에서 this는 언제나 클래스가 생성하는 인스턴스를 가리킨다.
하지만 자바스크립트의 this는 함수가 호출되는 방식에 따라 this 바인딩이 동적으로 결정된다.
해당함수가 호출되서 실행컨텍스트가 스택에 올라가고, 렉시컬 환경을 생성할때 this가 참조할 값도 결정됨!
기본적으로 this는 (1) 메서드에서 자신이 속한 객체 또는 (2) 생성자 함수에서 자신이 생성할 인스턴스를 가리키기 위해 사용하는 자기 참조 변수이다
그러므로 일반 함수에서는 기본적으로 사용할 필요가 없다. ( 그래서 'strict mode'에서는 this에 undefined 를 할당한다고 함). 일반 함수에서 this는 전역 객체에 바인딩 된다.
(이건 중첩함수나 콜백 함수의 경우에도 일반 함수로 호출됐다면 마찬가지다)
// 여기 함수가 있다
let f = function(){
console.log("this:",this === global ? "global" : this);
}
console.log("\n(1) 일반 함수로 호출")
f();
console.log("\n(2)new 연산자와 함께 생성자 함수로 호출")
let inst = new f();
console.log("생성한 인스턴스: ", inst);
console.log("\n(3)메서드로 호출")
let obj = {name:"John", f};
obj.f();
console.log("\n(4)call/apply/bind로 this 바인딩")
let obj2 = {name:"sally"}
f.call(obj2);
this는 함수를 정의한 방식이 아니라, 함수를 호출한 방식에 따라 결정된다는 것을 보여주는 예시.
f를 위와 같이 함수 표현식이 아니라 함수선언문이나 심지어 어떤 객체의 메소드로 정의했어도 마찬가지다.
(단, 화살표 함수와 ES6의 메서드 축약 표현으로 생성한 경우는 제외!)
1. 일반 함수 호출 : 아무 것도 안 붙이고 호출한 경우. this = 전역객체
2. 메서드 호출 : (객체). 를 붙여서 호출한 경우. this = 해당 객체
3. 생성자 함수로 호출 : new 키워드를 붙여서 호출한 경우. this = 생성할 인스턴스
4. Function.prototype.call/apply/bind 로 간접 호출 : this = 전달한 thisArg
( 참고 ✋ )
call, apply, bind 차이점
화살표 함수
생성자 함수로 사용할 수 없으며, prototype 프로퍼티가 없음
arguments객체를 생성하지 않는다.
화살표함수는 함수 자체의 this 바인딩을 갖지 않아서 일반 식별자를 찾는 것처럼 상위 스코프 체인을 통해 this를 탐색한다.
따라서 화살표 함수 내부의 this는 가장 가까운 상위 비화살표 함수의 this와 일치하게 된다.
끝내며...
var x = "ROOT";
function generator(){
var x="GENERATOR";
console.log("generator");
return function(){
console.log("callback 1:",x, this===global);
}
}
function repeat(n, f=generator()){
var x = "REPEAT";
console.log("repeat start");
for (var i=0; i<n; i++){
f();
}
}
console.log("call 1st repeat");
repeat(3);
console.log("call 2nd repeat");
repeat(2, function(){
console.log("callback 2:",x, this===global);
});
repeat의 인자로 전달된 callback들은 모두 [1] 일반함수로 호출됐기 때문에 this가 전역객체에 바인딩 됐지만
[2] 함수 정의 위치가 달라서 참조하는 상위 스코프가 다르고 고로 참조하는 x 값이 다르다.
'시리즈 > Javascript' 카테고리의 다른 글
생성자 함수 vs 클래스 (0) | 2021.05.10 |
---|---|
react 뼈대 (0) | 2021.04.20 |
참고하면 좋은 글 모음 (0) | 2021.03.18 |
[node] node로 웹 프로젝트 시작할 때 나오는 용어들 정리 (0) | 2021.02.27 |
[react] create-react-app으로 만든 app에서 테스트용 서버는 어떻게 돌아가나 (0) | 2021.02.27 |