ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 1급시민, Lexical Scope, 실행컨텍스트, Closure, this, apply, call, bind
    JavaScript 2021. 1. 6. 15:25

    JavaScript에서 중요한 개념들에 포함되는 1급시민, Lexical Scope, 실행컨텍스트, Closure, this에 대해서 

    핵심만 간단하게 정리해보려 한다

     

    1] 1급시민

    JavaScript에서 1급 시민의 조건은

    1. 변수에 담을 수 있으며

    2. 반환할 수 있다

    3. 인자로 전달할 수 있다.

     

    함수도 변수에 담을 수 있고 반환할 수 있으며 함수 자체를 인자로 전달할 수 있으므로 함수도 1급 시민임과 동시에 1급함수라고 부른다

     

    객체 같은 경우 1급 시민의 조건을 만족함과 동시에 객체이므로 1급 객체라고 부르며

    이외에도 모든 자료형은 1급 시민의 조건을 만족한다

     

    // 원시타입
    function AnyFunc(a) {
      console.log(a)
      return a
    }
    
    const anyFunc = AnyFunc(null)
    console.log(anyFunc)
    // 콘솔 첫째줄에는 => null
    // 두번째 줄에는 null
    
    // 객체
    function AnyFunc2(b) {
      console.log(b)
      return b
    }
    
    const anyFunc2 = AnyFunc2({ name: 'Chifuyu' })
    console.log(anyFunc2)
    // console window => { name: 'Chifuyu' }
    
    // 함수
    function AnyFunc3(c) {
      c()
      return c
    }
    
    const anyFunc3 = AnyFunc3(function nice() {
      console.log('Function')
    })
    console.log(anyFunc3)
    // console window => Function, f nice() {}
    
    const anyFunc3 = AnyFunc3(function nice() {
      console.log('Function')
    })

     

    2] Lexical Scope

    lexical scope (렉시컬 스코프): 렉시컬 스코프는 우리말로 직역하면 어휘적 범위라는 뜻이 되겠다.

    본 의미는 함수 단위의 상위 스코프를 결정하는 방식 중에 하나로 함수가 어디서 선언되었는지에 따라 함수의 상위 스코프가 결정된다.

     

    var name = 'name'
    
    function anyFunc() {
      var name = 'name'
      function anyTime() {
        return 'anyTime'
      }
      return name
    }
    
    // anyFunc는 전역에서 선언되었으므로 렉시컬 스코프는 전역이 되고
    // anyTime은 anyFunc안에서 선언되었으므로 렉시컬 스코프는 anyFunc가 된다

     

    3] 실행 컨텍스트

    실행 컨텍스트란 실행가능한 코드를 형상화하고 구분하는 추상적인 개념으로서 실행가능한 코드가 실행되기 위한 환경이라고 한다

     

    조금 어려운 개념인데 집중해서 잘 이해할 수 있도록 노력해보자

     

    실행 컨텍스트는 2가지 종류가 있으며 전역 컨텍스트, 함수 컨텍스트가 있다

    실행 컨텍스트 스택(호출 스택)은 자바스크립트가 코드를 실행할 때 함수를 호출하는 순서를 정하는 스택

     

    하나의 실행 컨텍스트 안에는 다음 3가지가 생성된다

    1) Variable Object (변수 객체, V/O) : 전역 컨텍스트는 전역 객체를 가리키고 함수 컨텍스트는 활성객체를 가리킨다

     

      1} 함수 컨텍스트가 가리키는 활성객체 (Activation Object)

        - 함수 표현식을 제외한 함수 선언

        - 지역변수: 변수값

        - 매개변수와 인수정보 => 유사배열 arguments 객체 (주의! 배열의 메서드를 쓸 수 없다)

     

      2} 전역 컨텍스트가 가리키는 전역객체 (Global Object)

        - 전역 함수 => 전역함수명: 함수식

        - 전역 변수 => 전역변수명: 전역변수 값

        - 특징: 전역 컨텍스트에서의 변수 객체는 유일하고 최상위에 위치한다

     

    2) Scope Chain (SC): 전역 또는 해당 함수가 참조할 수 있는 변수, 함수 선언 등의 정보를 가지고 있는 전역 객체 또는 활성 객체를 가리킨다. => 변수를 검색하는 메커니즘

      - 엔진이 스코프 체인을 통해 렉시컬 스코프를 파악하고 상위 스코프와 전역스코프까지 참조한다.

      - 함수가 중첩되면 부모 함수의 Scope가 자식 함수의 Scope Chain에 포함된다

      - 함수 실행 중에 변수를 만나면 현재 스코프에서 검색을 해보고 없다면 스코프 체인에 담겨진 순서(상위 스코프 방향)대로 검색을 한다.

      - 변수 검색에 실패한다면 참조에러를 발생시킨다

     

    3) this

      1} this에 대한 설명

        - this에 할당되는 값은 this의 호출 패턴에 의해 결정된다.

        - 기본적으로 전역에서 호출하면, JavaScript에서는 window 객체, Nodejs에서는 global 객체가 호출된다.

        - 이렇게 환경에 따라 호출되는 객체가 달라서 globalThis라는 것도 지원한다.

        - 일반함수: 아래에서 설명합니다.

        - 화살표 함수: 렉시컬 스코프(상위 스코프)의 this를 가리킨다

     

    => 다시 정리합니다

    * 실행 환경이 브라우저라 가정합니다.

     

    1] 일반함수는 기본적으로 전역 객체가 바인딩되고 (중첩함수도 마찬가지) 호출 방식에 따라 결정됩니다.

     

    1-1) bind, call, apply의 this 매개변수 자리에 넣은 변수로 바인딩됩니다. (명시적 바인딩이라 하고 우선순위가 높습니다)

    // call, apply, bind -> 명시적 바인딩
    const x = [1, 2, 3, 4, 5]
    const y = Math.max.call(null, ...x);
    console.log(y) // 5, this는 x
    
    const z = Math.max.apply(undefined, x);
    console.log(z) // 5, this는 x

     

     

    1-2) 이게 아니라면 addEventListener 같은 특별한 함수 같은 경우에는 해당 객체, 즉 html 태그(Element) 객체가 바인딩됩니다.

    const header = document.querySelector('header')
    header.addEventListener('click', function (e) {
      console.log(this) // <header></header>
    });

     

    1-3) html 태그에서 인라인으로 넣게 되면 해당 태그(Element) 객체로 바인딩됩니다.

    <html>
        <head></head>
        <body>
            <header onclick="console.log(this);">asdasd</header>
            <script>
            	// 위 결과는 <header onclick="console.log(this);">asdasd</header>
            </script>
        <body>
    </html>

     

    1-4) 반대로 일반함수가 중첩되었을 때는 다음과 같이 모두 window를 가리킵니다.

    // globalThis는 browser에서는 window가 됩니다.
    function x() {
      console.log(globalThis); // window
      function y() {
        console.log(globalThis) // window
        function z() {
          console.log(globalThis); // window
        }
        z();
      }
      y();
    }
    
    x();

     

    1-5) 객체 리터럴에서도 다음과 같이 볼 수 있습니다.

    const a = {
      b: function () {
        console.log(this);
      },
      c: function() {
        function y() {
          console.log(this)
        }
        y()
      }
    };
    
    a.b(); // 익명함수도 여기서는 메서드로 볼 수 있습니다
    
    a.c(); // window 또는 undefined
    
    const e = a.b; // e는 전역 변수이기에 스코프 체인을 따라가면 window 또는 undefined가 나옵니다.
    e();

     

    2] 생성자 함수는 미래에 생성할 인스턴스가 바인딩 (new가 없으면 일반함수처럼 동작합니다. -> window 객체 가능성이 높고 new를 했을 경우에는 무조건 인스턴스에 바인딩됩니다.)

     

    3] 메서드는 기본적으로 객체에 바인딩

     

    4] 화살표 함수는 무조건 렉시컬 스코프에 의해 결정됩니다. (= 상위 스코프로 결정됨)

     

    다시 한 번,

    일반함수는 호출되는 방식에 따라 결정됩니다.

    생성자 함수는 new를 하게 되면 인스턴스에 바인딩되고 new 연산자가 없다면 전역 객체로 바인딩됩니다.

    메서드는 기본적으로 객체에 바인딩

    화살표 함수는 렉시컬 스코프에 의해 결정됨

    // 객체 리터럴의 메소드
    const any = {
      objThis: function () {
        console.log(this) // this: any
        const innerFunc = () => {
          console.log('', this) // this: any
        }
        innerFunc()
        
        function time(x, y) {
          console.log(this) // this: any
          console.log(arguments) // arguments 객체
        }
        time.call(this, 1, 2) // this: any
        time.apply(this, [1, 2]) // this: any
        time.bind(this, [1, 2]) // this: any
      },
      windowThis: () => console.log(this) // window 객체
    }
    
    any.objThis() 
    any.windowThis() // window
    
    // 생성자 함수
    function Car() {
      this.name = 'car'
    }
    
    Car.prototype.say = function() {
      console.log(this.name)
    }
    
    const car = new Car() // 생성자 키워드인 new를 반드시 작성!
    car.say() // 'car'
    
    // callback 함수
    const div = document.getElementById('app')
    
    div.addEventListener('click', function() {
      console.log(this) // <div id='app'>...</div>
    })
    
    div.addEventListener('click', () => {
      console.log(this) // window 객체
    })

    2} 이러한 호출 패턴에 의해서 결정된 this가 실행 컨텍스트의 this에 들어가게 된다

     

    3} apply, call, bind의 차이점

    Function.prototype.call(this, thisArg, ...argArray)
    // => this를 연결하고 2번째 인자로 값들을 받는데 넣어도 되고 넣지 않아도 됨, 최종적으로 함수를 실행
    
    Function.prototype.apply(thisArg, argArray?: any[])
    // => this를 연결하고 2번째 인자로 배열을 받는데 넣어도 되고 넣지 않아도 됨, 최종적으로 함수를 실행
    
    Function.prototype.bind()
    // => this를 연결하고 함수를 실행하진 않음

     

    4) 호출 스택호출 스택이 쌓이는 과정을 다음 코드를 통해 알아보자

    function outFunc() {
      console.log('hello')
      
      function inFunc() {
        console.log('js')
      }
      inFunc()
    }
    
    outFunc()

    1. 전역 컨텍스트 push

    2. outFunc 컨텍스트 push

    3. inFunc 컨텍스트 push

    4. inFunc 컨텍스트 pop (함수의 실행이 끝나면 지체없이 컨텍스트는 파기됨)

    5. outFunc 컨텍스트 pop

    6. 전역 실행 컨텍스트는 애플리케이션이 종료될 때(웹 페이지에서 나가거나 브라우저를 닫을 때)까지 유지된다.

    7. 브라우저를 닫았다면 전역 컨텍스트는 pop

     

    4] Closure

    클로저란 스코프 체인의 메커니즘을 활용한 것이다

    비공개 변수를 사용할 때, 상태를 저장할 때 사용한다

     

    아래의 코드를 보자

    // Closure
    function outFunc() {
      const name = 'js'
      
      return function() {
        console.log(name)
      }
    }
    
    outFunc()() // 'js'
    
    

     

    name이라는 변수는 outFunc함수 내부에서 반환하는 익명 함수 내부에서 선언되지도 매개변수로 name을 받지도 않았다.

    그러나 사용은 가능하다

    왜?

    실행 컨텍스트에서 스코프 체인은 함수 자신의 스코프 내에서 변수를 참조했을 때 값이 없다면 스코프 체인에 담겨진 순서대로 (상위 스코프 방향) 검색을 시도한다.

    위의 경우 스코프체인은 익명함수 => outFunc 함수 => 전역 으로 탐색한다.

    만약 전역 컨텍스트까지 name이 없다면 참조 에러를 발생하였겠지만 outFunc함수에서 검색 성공했기 때문에

    실행결과가 정상적으로 나오는 것이다.

     

     

     

     

     

     

     

     

    @@@@@

    댓글

Designed by Tistory.