Front-end/JavaScript

JS 동작원리 9편 - Scope(lexical & dynamic)와 this

파리외 개발자 2022. 12. 19. 00:40
요약 : JS는 선언된 위치에 따라 scope가 결정되는 lexical을 따른다. 하지만 JS의 this는 호출 시점에 따라 값이 결정되는 dynamic scope방식과 '비슷한' 동작을 한다.

JS는 기본적으로 function scope를 가지며

새로 추가된 let,const변수선언을 통해 block scope를 추가로 가질 수 있다.

두 scope의 차이를 모른다면 이전 글에 작성해뒀다.

function scope와 block scope가 scope의 범위에 대해 정의한다면

dynamic과 lexical은 정의된 scope의 범위가 어디에서 파생되는 지를 정의한다.

  • dynamic - 어디에서 호출을 하느냐에 따라 scope가 달라진다. (옛, 일부 언어)
  • lexical - 선언을 한 장소에 따라 scope가 결정된다. (대부분의 언어)

이렇게만 설명하면 이해하기 어려울 테니 천천히 이어서 설명하도록 하겠다.

함수와 객체의 메서드에서 this의 차이

const a = function () {
  console.log('a', this)
  const b = function () {
    console.log('b', this)
    const c = {
      hi: function () {
        console.log('c', this)
      }
    }
    c.hi()
  }
  b()
}
a()

a함수의 this, b함수의 this, 그리고 c객체의 hi속성에 저장된 함수의 this는 어디를 가리킬까?

function으로 선언된 함수 선언식의 this는 global을 가리킨다.

a와 b에 저장된 function을 실행시키는 것은 전역이며

해당 a와 b의 실행컨텍스트의 this value에는 암묵적으로 전역이 저장되게 된다.

 

c객체의 hi속성에 저장된 function의 this는 c객체를 가리키고 있다.

c.hi()는 c라는 객체의 속성으로 선언된 이상 b나 a처럼 그냥 hi()라고 호출할 수 없으며

객체를 적어주고 하위 속성에 접근하는 dot을 사용해줘야 접근할 수 있다.

  • a, b와 같이 선언형 익명 함수를 실행할 때 this는 전역을 가리킨다.
  • c처럼 객체의 속성에 메서드가 실행될 때 this는 객체를 가리킨다.

대체 이 차이가 scope와 무슨 차이가 있으며, 

a가 전역에서 선언, 호출되어서 this가 전역을 가리키는 것은 알겠는데b는 전역에서 접근할 수 없는, a함수 scope안에 속해서 a안에서만 접근할 수 있는데 왜 b의 this 또한 전역을 가리키며,c의 속성에 함수가 선언되면 this가 객체를 가리키는지는JS는 lexical scope를 따르지만 JS의 this는 dynamic scope와 비슷하게 동작하는 것에 그 이유가 있다.

const obj1 = {
  name: 'name',
  say() {
    console.log('a', this);
    var func = function () {
      console.log('b', this)
    }
    func()
  }
}

obj1.say()

한 가지 예시 코드를 또 확인하자.

obj1객체는 say메서드를 가지고 this를 호출한다.

say메서드 안에 func내장 함수가 있으며 이는 객체의 속성이 아니다.

b의 this는 결국 전역을 가리킨다.

this는 scope와 별개로 동작한다.

lexical을 따르는 js방식이 아닌 호출 시점에 따라 정해지며,이는 마치 dynamic scope방식과 비슷하다.

JS는 lexical, 하지만 this는 dynamic, 그렇다면 애로우 func은?

//js==lexical, but this is dynamic

const obj2 = {
  name: 'name',
  say() {
    console.log('a', this);
    var func = () => {
      console.log('b', this)
    }
    func()
  }
}

obj2.say()

일반 함수의 this가 dynamic인 게 마음에 안 든다고요?

그렇다면 arrow func을 사용하도록 하세요!

() => {}를 사용하면 함수 내의 this는 scope를 따르도록 할 수 있다.

//or

const obj3 = {
  name: 'name',
  say() {
    console.log('a', this);
    var func = function () {
      console.log('b', this)
    }
    return func.bind(this)
  }
}

obj3.say()()

혹은 bind나 apply, call을 사용하면 

일반 함수에서 암묵적으로 global에 bind 되어있는 this를

해당 함수에 bind 하도록 명시할 수 있다.

마지막으로 하나의 코드를 더 보고 잘 이해했는지 체크하고 넘어가도록 하겠다.

var b = {
  name: "jay",
  say() {
    console.log(this);
  },
};

var c = {
  name: "jay",
  say() {
    return function () {
      console.log(this); //window
    };
  },
};

var d = {
  name: "jay",
  say() {
    return () => console.log(this); //but arrow is lexical
  },
};

b.say();
c.say()();
d.say()();

다음 글에선 bind류 함수에 대해 다루도록 하겠다.