0%

201211_TIL(this, 실행 컨텍스트, 클로저)

오늘 배운 것

this

외부 컨텍스트와 내부 컨텍스트가 일치하지 않을 때 각각의 다른 this를 가리키는 문제가 있다.

Function.prototype.apply/call/bind 메서드에 의한 간접 호출

Function.prototype.apply(thisArg[, argsArray]) 와 같은 함수 시그니쳐에서 괄호 안에 있는 내용은 필수 입력, 괄호 안에서 [ ]내부 인수는 옵션이란 뜻이다. 항상 필수는 앞쪽 옵션은 뒷쪽에 위치한다.(인수는 순서가 의미있기 때문이다.)

apply과 call는 . 앞에 있는 함수를 호출하며 첫번째 인수로는 this로 사용한 객체를 전달한다. 다음 인수로는 apply는 배열 목록으로 전달, call은 ,로 구분된 인수들의 목록을 전달하는 차이가 있다.

1
2
3
4
5
6
7
8
9
10
11
function foo(a, b, c) {
console.log(this);
return a + b + c;
}

const thisArg = {
x: 1,
};

console.log(foo.apply(thisArg, [1, 2, 3])); //6
console.log(foo.call(thisArg, 1, 2, 3)); //6

bind는 호출하지 않고 this만 교체한다.

1
2
3
4
5
6
7
8
9
10
11
const person = {
name: "Lee",
foo(callback) {
// bind 메서드로 callback 함수 내부의 this 바인딩을 전달
setTimeout(callback.bind(this), 100); //여기서 this는 외부에 있던 this(foo의 this)를 가져오는 것이다.
},
};

person.foo(function () {
console.log(`Hi! my name is ${this.name}.`); // Hi! my name is Lee.
});

화살표 함수의 경우 내부에 this가 없어서 상위 스코프의 this를 상속 받기 때문에 bind 등을 쓸 필요가 없다.

가변인자 함수에서 call을 사용할 때가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function sum() {
// return Array.prototype.slice.call(arguments).reduce((p, c) => p + c, 0); //10
// return [...arguments].reduce((p, c) => p + c, 0); // 10

let res = 0;

for (let i = 0; i < arguments.length; i++) {
res += arguments[i];
}
return res;
}

function sum(...args) {
return args.reduce((p, c) => p + c, 0);
}

console.log(sum(1, 2, 3, 4));

위는 다 같은 결과이다.

실행 컨텍스트

실행 컨텍스트는 식별자(변수, 함수, 클래스 등의 이름)를 등록하고 관리하는 스코프와 코드 실행 순서 관리를 구현한 내부 매커니즘으로, 모든 코드는 실행 컨텍스트를 통해 실행되고 관리된다.

실행 컨텍스트는 소스코드 실행에 팔요한 정보, 실행 결과를 등록하는 핵심이다.

call stack

실행 컨텍스트 스택은 코드의 실행 순서를 관리한다. 소스코드가 평가되면 실행 컨텍스트가 생성되고 실행 컨텍스트 스택의 최상위에 쌓인다. 실행 컨텍스트 스택의 최상위에 존재하는 실행 컨텍스트는 언제나 현재 실행 중인 코드의 실행 컨텍스트다. 따라서 실행 컨텍스트 스택의 최상위에 존재하는 실행 컨텍스트를 실행 중인 실행 컨텍스트(running execution context)라 부른다.

렉시컬 환경

렉시컬 스코프는 함수가 정의되는 위치가 상위 스코프를 결정하는 것이다.

렉시컬 환경은 렉시컬 스코프의 실체이다.

1
2
3
4
5
var x = 1;
const y = 2;

function foo (a) {
...
  • BindingObject를 통해 전역 객체에 변수 식별자를 키로 등록한 다음, 암묵적으로 undefined를 바인딩한다.
  • let, const 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 되지 않고 전역 개체와 무관한 개념적인 블록인 전역 환경 레코드의 선언적 환경 레코드내 에 존재한다.(ES6에서 추가)
  • 함수 선언문으로 정의한 함수가 평가되면 함수 이름과 동일한 이름의 식별자를 객체 환경 레코드에 바인딩된 BindingObject를 통해 전역 객체에 키로 등록하고 생성된 함수 객체를 즉시 할당한다.
  • JS에서 호이스팅을 의도적으로 하는 것이 아닌 위와 같이 전역 객체에 변수와 함수를 등록하기 때문에 발생하는 부작용이다.
  • 위와 같이 var로 선언한 전역변수와 let, const로 선언한 전역 변수는 다른 위치에 있기 때문에 식별자를 찾을 때는 둘 다 찾아본다.

실행 컨텍스트와 블록 레벨 스코프

1
2
3
4
5
6
7
8
let x = 1;

if (true) {
let x = 10;
console.log(x); // 10
}

console.log(x); // 1

if 문의 코드 블록 내에서 let 키워드로 변수가 선언되었다.(var로 선언된다면 따로 렉시컬 환경을 만들지는 않는다. 또한 전역 코드 평가 단계에서 블록문 안까지 들여다봐야 아래와 같은 대처가 가능하다.)

따라서 if 문의 코드 블록이 실행되면 if 문의 코드 블록을 위한 블록 레벨 스코프를 생성해야 한다. 이를 위해 선언적 환경 레코드를 갖는 렉시컬 환경을 새롭게 생성하여 기존의 전역 렉시컬 환경을 교체한다.

이때 새롭게 생성된 if 문의 코드 블록을 위한 렉시컬 환경의 외부 렉시컬 환경에 대한 참조는 if 문이 실행되기 이전의 전역 렉시컬 환경을 가리킨다.

if 문 코드 블록의 실행이 종료되면 if 문의 코드 블록이 실행되기 이전의 렉시컬 환경으로 되돌린다.

이는 if 문뿐 아니라 블록 레밸 스코프를 생성하는 모든 블록문에 적용된다.

코드 블록은 실행컨텍스트를 만들지는 않지만 렉시컬 환경을 따로 만든다.

클로저

클로저는 함수와 그 함수가 선언된 렉시컬 환경(상위 스코프)과의 조합이다.

함수 객체의 내부 슬롯 [[Environment]]

함수 객체의 내부 슬롯 [[Environment]]에 저장된 현재 실행 중인 실행 컨텍스트의 렉시컬 환경의 참조가 바로 상위 스코프다. 또한 자신이 호출되었을 때 생성될 함수 렉시컬 환경의 “외부 렉시컬 환경에 대한 참조”에 저장될 참조값이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const x = 1;

function foo() {
const x = 10;

// 상위 스코프는 함수 정의 환경(위치)에 따라 결정된다.
// 함수 호출 위치와 상위 스코프는 아무런 관계가 없다.
bar();
}

// 함수 bar는 자신의 상위 스코프, 즉 전역 렉시컬 환경을 [[Environment]]에 저장하여 기억한다.
function bar() {
console.log(x);
}

foo(); // ?
bar(); // ?

클로저와 렉시컬 환경

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const x = 1;

// ①
function outer() {
const x = 10;
const inner = function () {
console.log(x);
}; // ②
return inner;
}

// outer 함수를 호출하면 중첩 함수 inner를 반환한다.
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거된다.
const innerFunc = outer(); // ③
innerFunc(); // ④ 10

outer 함수는 종료되어 실행 컨텍스트 스택에서 pop 됐지만 inner 함수가 참조하기 때문에 생존하고 있다. 여기서 outer 함수는 클로저이다.(순환 참조의 경우에는 가비지 컬렉터에 의해 소멸한다.)

Nyong’s GitHub