본문 바로가기

front/javascript

[자바스크립트] 실행 컨텍스트, 스코프와 클로저, 호이스팅, this

728x90

항해 플러스 과제를 진행하기 전 알아야하는 필수 지식을 미리 공부하려고 한다.

이번 주제는 Exeution Context(실행 컨텍스트)이다.

취준 시절 공부했던 내용이지만 3년이 지나면서 다 까먹어버려서 다시 공부한다ㅠ

 

1. Exeution Context(실행 컨텍스트) 란?

실행 컨텍스트함수를 호출했을 때 실행될 수 있는 환경과 그 결과를 저장하는 영역을 말한다.

실행컨텍스트는 실행 가능한 코드를 만났을 때 생성되며, 실행 가능한 코드에는 함수코드, 글로벌 코드, eval 코드가 해당된다. 

 

실행 컨텍스트의 구조

 

실행컨텍스트에는 위와 같이 구성되어 있는데 여기서 중요한 것은

"Lexical Environment(렉시컬 환경)",  "Declaration Binding Instantiation(This 바인딩)" 이다.

 

렉시컬 환경 컴포넌트는 함수와 변수의 식별자 해결을 위한 환경 설정을 말한다.

함수와 변수의 이름과 값이 저장되는 곳이라고 생각하면 된다. 

함수 안과 밖의 함수와 변수들이 하나의 렉시컬 환경 컴포넌트가 되어 저장된다.

 

렉시컬 환경 컴포넌트는 두 가지 구성 요소를 가지고 있다.

- 환경 레코드(Environment Record) : 함수 내에 있는 함수와 변수 저장

- 외부 렉시컬 환경 참조(Outer Lexical Environment Reference) : 외부 환경 참조, 상위 스코프

  현재 렉시컬 환경이 상위 렉시컬 환경을 참조할 수 있도록 해주는 링크로 함수가 정의된 시점에서의 환경이 기준이 된다. 

 

실행 컨텍스트의 생성 과정은 다음과 같다.

 

1. 생성 단계 : 렉시컬 환경 생성 > 변수 환경 생성 > this 바인딩 결정 

2. 실행 단계 : 코드 실행 > 변수 할당 > 함수 실행 

 

let x = 10;

function outer() {
  let y = 20;

  function inner() {
    let z = 30;
    console.log(x + y + z);
  }

  inner();
}

outer();

 

위 코드의 실행 과정을 단계별로 설명해보자.

 

1. 전역 실행 컨텍스트

 

맨 처음엔 전역 렉시컬 환경이 생성된다.

그러면서 전역 변수인 x와 outer 함수가 식별자에 등록되고, this를 전역 객체에 바인딩한다. 

이후 전역 코드가 실행되면서 저장된 전역 변수인 x에 10을 할당하고,outer함수를 호출한다. 

 

*전역 객체

전역 객체는 환경에 따라 다르다.

브라우저 환경에서는 전역 객체가 window가 되고, node.js에서 전역 객체가 global이 된다.

 

 

2. outer 함수 실행 컨텍스트

 

outer 함수가 실행되면서, 실행 컨텍스트가 생성된다.

렉시컬 환경이 생성되면서 지역 변수인 point가 식별자에 등록된다.

outer 함수의 경우 전역에서 정의되었기 때문에, 외부 렉시컬 환경 참조는 전역 렉스컬 환경을 참조한다. 

 

함수가 실행되면서 y에 20을 할당하고, innter 함수를 호출한다. 

 

 

3. innter 함수 실행 컨텍스트

 

innter 함수가 실행되면서, 실행 컨텍스트가 생성된다.

마찬가지로 렉시컬 환경이 생성되고, 지역 변수인 z가 식별자에 등록된다.

외부 렉시컬 환경 참조를 outer 함수의 렉시컬 환경으로 설정한다. 

 

이 후 함수가 실행되면서 z에 30을 할당하고, console.log(x + y + z)가 실행된다.

 

 

4. console.log(x + y + z) 실행

 

console.log(x + y + z)가 실행되면서 자바스크립트 엔진은 다음 순서로 변수를 찾는다.

우선 z를 현재 렉시컬 환경에서 찾는다.

 

innter 함수의 렉시컬 환경에 z=30이 있기 때문에 여기서 찾게 된다.

그 후 y를 찾게 된다.

 

y는 현재 렉시컬 환경에 없기 때문에 외부 렉시컬 환경 참조에 있는 outer 함수의 렉시컬 환경에서 찾게 된다.

해당 환경에 y=20이 있기 때문에 여기서 찾게 된다.

 

x의 경우 현재에도, 외부에서도 찾을 수 없기 때문에 또 outer에 있는 외부 렉시컬 환경 참조를 따라 

전역 렉시컬 환경에서 찾게 된다. 

그리고 최종 값을 출력하게 된다.

 

함수 실행이 끝났으면, 함수 실행 컨텍스트가 제거된다.

우선 inner 함수 실행이 종료되었으므로, 해당 함수의 실행 컨텍스트가 제거 된다.

그 후 outer 함수 실행이 종료되고, 실행 컨텍스트가 제거 된다.

 

모든 실행 컨텍스트가 제거되면 전역 실행이 종료된다. 

 

 

 

2. Scope(스코프)와 Closure(클로저)

1) 스코프

스코프는 변수와 함수가 접근 가능한 범위를 정의한다.

코드 작성 시 변수가 어느 범위에서 유효한지를 판단할 때 사용하는 용어라고 생각하면 된다. 

 

*렉시컬 환경 = 스포크를 구현하는 자바스크립트 엔진의 내부 메커니즘.

 

 

 

- 전역 스코프(Global Scope)

var globalVar = "I am global";

function test() {
  console.log(globalVar); // "I am global"
}

 

코드 모든 부분에서 접근 가능한 범위로 자바스크립트 프로그램이 처음 시작될 때 생성된다.

전역 변후와 함수 선언이 포함되며, 프로그램이 종료될 때까지 유지된다.

 

전역 스코프에서 선언된 변수와 함수는 전역 객체 속성에 추가된다.

 

 

- 함수 스코프 (Function Scope)

function test() {
  var localVar = "I am local";
  console.log(localVar); // "I am local"
}

console.log(localVar); // ReferenceError: localVar is not defined

 

함수가 선언되면 함수 스코프가 생성된다.

해당 스코프는 함수 내부에서 선언된 변수와 함수 선언을 포함하며, 함수가 실행되는 동안에만 유효하다.

 

외부에서 접근이 불가하며, 함수가 실행을 마치면 해당 스코프는 사라진다.

 

 

- 블록 스코프 (Block Scope, ES6부터)

if (true) {
  let blockVar = "I am block-scoped";
  console.log(blockVar); // "I am block-scoped"
}

console.log(blockVar); // ReferenceError: blockVar is not defined

 

중괄호 {}로 둘러싸인 코드 블록이 실행될 때 블록 스코프가 생성됩니다.

let과 const 키워드로 선언된 변수들은 블록 스코프를 가진다.

 

중괄호 {}로 둘러싸인 코드 블록 내에서만 유효하며, 블록이 끝나면 스코프는 사라진다.

 

 

 

*식별자 해결 = 변수와 함수의 이름을 찾는 것.

 

function sum(){
  const a = 5;
  const b = 6;
  console.log(a + b);
}

 

위 예시에서 sum 함수가 밖에서 따로 찾지 않아도 사용할 수 있는 변수와 함수가 스코프 범위가 된다.

 

 

2) 클로저

클로저는 함수와 그 함수가 선언된 렉시컬 환경의 조합을 말한다.

 

클로저는 함수가 선언된 스코프(렉시컬 환경)를 기억하고,

스코프 밖에서 함수가 호출되더라도 스코프 안에 있는 변수에 접근할 수 있도록 도와준다. 

 

클로저는 내부 함수가 외부 함수의 렉시컬 환경을 참조할 때 생성된다. 

주로 함수가 반환될 때, 함수 내에 또 다른 함수가 정의될 때 생성된다고 보면 된다.

 

함수가 반환될 때 클로저가 생성되면, 반환된 함수는 외부 함수의 변수를 참조할 수 있게 된다. 

내부 함수가 외부 함수의 변수를 계속해서 참조할 수 있도록 도와주는 것이다.

이로 인해 외부 함수가 종료된 후에도 변수의 값이 유지된다. 

 

function createCounter() {
    let count = 0;  // 프라이빗 변수

    return {
        increment: function() {
            count++;
            console.log(count);
        },
        decrement: function() {
            count--;
            console.log(count);
        },
        getCount: function() {
            return count;
        }
    };
}

const counter = createCounter();
counter.increment();  // 출력: 1
counter.increment();  // 출력: 2
counter.decrement();  // 출력: 1
console.log(counter.getCount());  // 출력: 1
console.log(counter.count);  // 출력: undefined

 

createCounter 함수는 count 변수를 포함하는 클로저를 생성한다.

반환된 객체의 메서드들은 클로저를 통해 count 변수에 접근할 수 있게 된다.

 

 

 

3. Hoisting(호이스팅)

호이스팅은 변수와 함수 선언이 실제 코드가 실행되기 전에 해당 스코프의 맨 위로 끌어올려지는 것처럼 동작하는 것을 말한다.

 

자바스크립트는 함수를 '함수 선언문 > 변수 초기화 > 나머지 코드 실행' 순서로 해석한다.

호이스팅은 이로 인해 발생하는 것이라고 보면된다.

 

따라서 함수를 선언문 형태로 작성하면 먼저 해석되지만, 표현식 형태로 작성하면 변수 초기화 순서에 해석되기 때문에

호이스팅이 적용되지 않는다. 

 

 

1) 변수 호이스팅

console.log(a); // undefined
var a = 10;        
console.log(a); // 10

 

변수 선언만 끌어올려지며, 변수 초기화는 호이스팅 되지 않는다.

 

var로 선언된 변수는 코드 블록의 맨 위로 끌어올려지지만, 선언만 끌어올려지고 초기화는 나중에 실행된다. 

즉, 'var a' (선언)가 먼저 끌어올려지고, 그 후에 a = 10이라는 값이 들어가게 된다.

따라서, 값이 초기화되기 전의 console.log(a)에선 undefined가 출력된다. 

 

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;

 

let과 const로 선언된 변수도 호이스팅되지만,

일시적 사각지대(Temporal Dead Zone, TDZ)에 들어가기 때문에 값이 들어가기 전까지는 접근할 수 없다. 

 

클래스 선언도 호이스팅되지만, let/const와 유사하게 TDZ의 영향받는다.

 

 

2) 함수 호이스팅

greet(); // "Hello, world!"

function greet() {
  console.log("Hello, world!");
}

 

함수 선언문은 전체가 호이스팅 되기 때문에 코드 전체에서 정의된 위치와 상관없이 사용할 수 있다. 

 

 

sayHi(); // TypeError: sayHi is not a function

var sayHi = function() {
  console.log("Hi!");
};

 

그러나 함수 표현식은 변수와 같이 처리되기 때문에 선언문과 다르게 처리된다.

sayHi라는 변수는 호이스팅 되지만 선언만 끌어올려 진 것이기 때문에 초기화되기 전까지는 undefined의 값을 갖게 된다.

 

 

 

4. This 바인딩 

This는 자바스크립트에서 현재 실행 컨텍스트를 참조하는 키워드를 의미한다. 

따라서 코드의 실행 컨텍스트에 따라 다르게 설정된다. 

 

 

1) 전역 컨텍스트

console.log(this === window); // 브라우저에서 true

 

전역 스코프에서 this는 전역 객체를 참조한다.

브라우저의 경우 전역 객체는 window가 된다. 

 

 

2) 메서드 호출

// 메서드 호출
const obj = {
    name: "MyObject",
    greet: function() {
        console.log(`Hello from ${this.name}`);
    }
};
obj.greet(); // "Hello from MyObject"

 

객체의 메서드로 호출될 때 this는 해당 객체를 참조한다. 

여기서 this는 obj를 가리키며, this.name은 obj.name을 의미한다. 

 

 

3) 함수 호출

function standalone() {
    console.log(this === window); // 브라우저에서 true
}
standalone();

 

일반 함수로 호출될 때 this는 전역 객체를 참조한다.

strict mode에선 this가 undefined로 설정된다. 

 

 

4) 생성자 함수

// 생성자 함수
function Person(name) {
    this.name = name;
}
const john = new Person("John");
console.log(john.name); // "John"

 

new 키워드로 호출될 때는 this는 새로 생성된 인스턴스를 참조한다.

new 키워드가 사용되면, 자바스크립트는 새로운 객체를 생성하고 this를 그 객체로 바인딩한다.

 

 

 

5) 화살표 함수

const arrowObj = {
    name: "ArrowObject",
    greet: () => {
        console.log(`Hello from ${this.name}`);
    }
};
arrowObj.greet(); // "Hello from undefined" (전역 객체의 name 또는 undefined)

 

자신만의 this를 가지지 않고, 상위 스코프에서 상속받는다.

 

 

 

6) call, apply, bind

function introduce(greeting) {
    console.log(`${greeting}, Im ${this.name}`);
}

const mary = { name: "Mary" };
introduce.call(mary, "Hi"); // "Hi, Im Mary"
introduce.apply(mary, ["Hello"]); // "Hello, Im Mary"
const maryIntroduce = introduce.bind(mary);
maryIntroduce("Hey"); // "Hey, Im Mary"

 

this를 명시적으로 지정할 수 있다. 

call과 apply는 첫 번째 인자로 this를 설정하며, bind는 새로운 함수를 반환하면서 this를 영구적으로 바인딩한다.

 

728x90