Front-end/JavaScript

JS 동작원리 8편 - 실행 컨텍스트(VE, scope chain, this)

파리외 개발자 2022. 12. 11. 17:57

실행 컨텍스트

실행 컨텍스트(Execution Context)는 JS 동작의 핵심원리로써 

동작의 단위라고 볼 수 있다.

NodeJS의 런타임 동작 방식에서 콜 스택에 쌓이는 것이 실행 컨텍스트 이다.

이 실행 컨텍스트는 객체의 형태이며 세 개의 속성 값을 가진다. 

Variable Object

실행 컨텍스트의 첫 번째 속성 VO는 변수 정보를 가리킨다.

위 코드가 런타임에서 실행된다면

anonymous(Global) => one => two 순서로 콜 스택에 담기게 된다.

각 세 개의 실행컨텍스트의 VO가 가르키는 객체에 담겨있는 것은

  • 전역 컨텍스트  var isValid = false, function one(), function two()
  • 함수 컨텍스트(one)  var isValid = true
  • 함수 컨텍스트(two)  var isValid = undefined

여기서 전역 컨텍스트와 함수 컨텍스트는 각각 가리키는 객체가 다르다.

전역 컨텍스트의 VO가 가리키는 것은 GO(Global Object)로 전역변수와 함수 선언식이 담긴다.(함수 표현식은 제외된다.)
함수 컨텍스트의 VO가 가르키는 것은 AO(Activation Object)로 지역변수와 내부 함수, 인자(arguments)가 담겨있다.

Scope

스코프 체인을 알기 전에 간단하게 scope에 대해 알아보도록 한다.

function scope

//function scope
if (5 > 4) {
  var secret = '123';
}

secret;//'123'

function a() {
  var secret = '123';
}

secret;//err

JS에는 두 종류의 scope가 있다.

그중 function scope는 {}로 표시된 함수 범위는 다른 영역으로 취급받는다. 

여기서 var는 함수 스코프를 가진다.

if는 함수가 아니므로 secret을 외부에서도 접근 가능하다.

하지만 function내부에 정의된 secret은 외부에서 접근이 불가능하다.

block scope

//block scope
if (5 > 4) {
  let secret = '123';
}

secret;//err

let과 const는 var와 다르게 block scope를 가진다.

if나 for 등 {}로 표시되는 모든 블록요소를 분리된 범위로 생각하며

블럭 스코프는 함수 스코프도 포함한다.

var, let, const의 스코프 차이를 이해하기 위해 아래 코드를 보자

function loop() {
  for (var i = 0; i < 5; i++) {//var or let
    console.log(i);
  }
  console.log('final', i);
}

loop();

위 코드의 결과를 예상하자면 0,1,2,3,4 그리고 final 5가 나온다고 생각할 수 있다.

그리고 var 대신 let을 사용한다면 for의 블록 스코프를 벗어나므로 마지막에 에러가 발생할 것이다.

Scope Chain

var x = 'x';

function findName() {
  var b = 'b';
  return printName();
}

function printName() {
  var c = 'c';
  return 'name';
}

function sayName() {
  var a = 'a';
  return findName()
}

sayName();

여기 세 개의 선언식 함수가 지역변수 하나씩을 가지고 있으며 전역 변수 x 또한 있다.

여기서 printName함수에서는 전역 변수인 x에 접근이 가능했다.

다른 함수의 지역변수의 접근에서는 참조 오류가 발생했다.

실행 컨텍스트의 SC(Scope Chain)에는 참조할 수 있는 변수 scope가 리스트 형태로 저장되어 있다.

위 코드를 예시로 보자면 아래 그림과 같다.

세 개의 함수는 모두 lexicall로 보자면 전역 위에 선언되어있다.

따라서 각 함수 컨텍스트가 가르키는 SC리스트에는 자기자신의 VE, 전역의 VE가 순서대로 저장되어 있다.

그렇기 때문에 해당 함수 컨텍스트 들은 자신의 지역변수와 전역 변수에만 접근할 수 있다.

Scope Chain에는 접근할 수 있는 scope가 순서대로 저장되어 있으며, 첫 번째 scope부터 접근하고자 하는 변수를 VE에서 찾고, 없다면 다음 scope로 넘어가서 계속해서 참조할 변수를 찾는다. 여기서 마지막 scope까지 참조하고자 하는 변수가 없다면 reference error를 낸다.
//lexically
function say() {
  var a = 'a';
  return function find() {
    var b = 'b';
    return function print() {
      var c = 'c';
      console.log(a, b, c);
      return 'name'
    }
  }
}

여기 위와 비슷하지만 중첩의 형태로 재구성한 세 개의 함수가 있다.

중첩 형태로 함수를 작성한다면 스코프 체인은 연속해서 scope순서를 저장하게 된다.

따라서 print함수의 SC에는 print, find, say, Global이 저장된다.

print함수에서는 a, b, c 모두 접근이 가능한데 내부적으로는

c를 참조할 때 print 함수의 scope에서 찾게 되고,

b를 참조할 때 print 함수의 scope에서 찾는 것을 실패하여

SC의 다음 scope인 find 함수의 scope로 넘어가서 b를 찾게 되는 방식이다.

This value

function a() {
  console.log(this)
}

a()

window.a()

전역에 선언된 a함수는 this를 출력한다.

a와 window.a의 this는 같은 결과인 Window를 가리키는 것으로 보인다.

function b() {
  'use strict'
  console.log(this)
}

b();

window.b();

이번엔 use strict를 사용해 좀 더 엄격한 문법을 적용해서 같은 동작을 해본다.

window.b는 아까와 같이 this가 window를 가리키지만

b함수에서의 this는 사실상 undefined임을 알 수 있다.

a함수에서의 this는 암묵적으로 전역 처리가 되었다는 것이다.

use strict란?

use strict는 JS의 문법을 좀 더 강력하게 적용시키겠다는 선언이다.

(JS문법은 사실상 자유도가 무척이나 높은 편... 좋은 게 아니다)

weird함수의 height변수는 var나 let 등의 변수 선언 없이 달랑 이름만으로 선언했다.

하지만 리턴 값은 50으로 제대로 출력된다.

이유는 JS에서 알아서 변수라고 판단해 오류 없이 이를 허용하기 때문이다.

이번엔 상단에 use strict를 선언 후 같은 코드를 동작시킨다.

변수 height가 정의되지 않았다는 에러를 출력하게 된다.

이처럼 script문의 상단에 'use strict'를 선언하게 되면 JS의 느슨한 문법구조를 

좀 더 타이트하게 적용해 예기치 못한 오류에 대비를 할 수 있게끔 해준다.

다만 요즘은 타입 스크립트나, eslint, prettier 등 정적 코드 분석이 발달했기에 use strict가 자주 사용되진 않는다.

This

 

[JS] this 가 가르키는 것

this는 자기 자신을 가르킨다. 클래스에서 this 함수형 클래스에 this로 속성을 하나 생성하고 프로토타입으로 메소드를 정의하여 this.property1을 띄우도록 한다. 여기서 this는 자기자신, 즉 클래스인

developefeel.tistory.com

위 글을 요약하자면 this가 가리키는 것은

  • 이벤트 리스너에서는 이벤트 대상 객체
  • 클래스 내에서는 클래스
  • 함수 내에서는 전역
  • 클래스가 인스턴스를 생성해 새로운 공간을 할당받을 때에만 그 공간을 가리킨다.

처음의 코드에서는 함수 내의 this였기에 전역인 window를 가리키는 것이었다.

//1: this로 자신의 메서드에 접근할 수 있다.
const obj = {
  a: 'A',
  b: function () {
    return this.a + 'B'
  },
  c() {
    return this.b() + 'C'
  }
}

obj.a
obj.b()
obj.c()

객체 obj의 실행컨텍스트는 공간을 할당받으며 this는 해당 obj의 공간을 가리키게 된다.

따라서 this는 obj자체를 의미하며 obj에 속해있는 a, b, c에 접근할 수 있다.

this를 이용하면 자신의 변수나 함수에 접근할 수 있다.

//2: 같은 코드를 여러개의 객체에서 실행할 수 있다.
function sky() {
  console.log(this.name)
}

const name = 'sky';

const obj1 = {
  name: 'sun',
  sky: sky
}

const obj2 = {
  name: 'moon',
  sky: sky
}

sky();
obj1.sky();
obj2.sky();

sky함수를 그냥 실행하면 this는 global을 가리키므로 name변수의 값 sky를 출력한다.

obj1의 속성에 sky함수를 대입하여 출력하면 sky함수의 this는 obj1을 가르키므로

obj1의 name속성인 sun을 출력한다.

마찬가지로 obj2의 sky함수의 this는 obj2를 가르키므로

moon을 출력하게 된다.

실행 컨텍스트가 생성될 때 this가 가리키는 곳을 this value에 지정하게 된다.