ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JavaScript] 3. this
    개발자 이야기/JavaScript 2021. 7. 13. 23:36

    this는 '자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수'다. 

    this에는 호출한 주체에 대한 정보가 담긴다.

     

    *바인딩(binding)이란 식별자와 값을 연결하는 과정을 의미한다. 예를 들어, 변수 선언은 변수명(식별자)과 확보된 메모리 공간의 주소를 바인딩하는 것이다. this 바인딩은 this와 this가 가리킬 객체를 바인딩하는 것이다.

     

    자바스크립트에서 this는 실행 컨텍스트가 생성(호이스팅, 스코프 체인 정보 수집, this 바인딩한다)될 때 결정된다. 일반적으로 함수를 호출할 때 실행 컨텍스트가 생성되기 때문에, this는 함수를 호출할 때 결정된다고 할 수 있다. 함수를 어떤 방식으로 호출하느냐에 따라 this의 값이 달라지는 것이다. 즉, this는 '함수가 호출되는 방식'에 따라 '동적'으로 결정된다.

     

    그러므로 '호출되는 방식'에 따라 this가 어떤 값을 바라보게 되는지 살펴보려고 한다.

    • 전역 공간에서의 this
    • 메서드로서 호출할 때 그 메서드 내부에서의 this
    • 함수로서 호출할 때 그 함수 내부에서의 this
    • 콜백 함수 호출 시 그 함수 내부에서의 this
    • 생성자 함수 내부에서의 this

     

    전역 공간에서의 this

    전역 공간에서의 this는 전역 객체를 가리킨다. 전역 객체는 자바스크립트 런타임 환경에 따라 다른 이름과 정보를 가진다. 브라우저에서의 전역 객체는 window이고, Node.js 환경에서는 global이다.

     

    let a = 1;
    console.log(a);         // 1
    console.log(window.a);  // 1
    console.log(this.a);    // 1

    위 코드에서 window.a와 this.a의 값이 1인 이유는

    전역변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당하기 때문이다.

     

    메서드로서 호출할 때 그 메서드 내부에서의 this

    함수와 메서드

    함수를 실행하는 가장 일반적인 방법 두 가지는 함수로서 호출하는 경우와 메서드로서 호출하는 경우다.

    이 둘을 구분하는 유일한 차이는 독립성이다. 함수는 그 자체로 독립적인 기능을 수행한다. 반면, 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행한다.

    let func = function (x) {
      console.log(this, x);
    };
    func(1);           // Window {...} 1
    
    let obj = {
      method: func
    };
    obj.method(2);     // { method: f } 2
    obj['method'](2);  // { method: f } 2

    위 예제는 변수에 담아 호출한 경우와 obj 객체의 프로퍼티에 할당해서 호출한 경우에 this가 달라지는 것을 보여준다.

     

    '함수로서 호출'과 '메서드로서 호출'을 구분하는 방법은 함수 앞에 점(.)이 있는지(또는 대괄호 표기법인지) 여부만으로 간단히 구분할 수 있다. 즉, 함수 호출 시 그 함수 이름 앞에 객체가 명시되어 있는 경우에는 메서드로 호출한 것이다. 그렇지 않은 모든 경우엔 함수로 호출한 것이다.

     

    함수로서 호출할 때 그 함수 내부에서의 this

    어떤 함수를 함수로서 호출할 경우엔 this가 지정되지 않는다. 그러므로 함수에서의 this는 전역 객체를 가리킨다.

     

    그렇다면 메서드 내부에서 호출한 함수의 경우 this는 무엇을 가리킬까?

    let obj1 = {
      outer: function () {
        console.log(this);             // obj1
        let innerFunc = function () {
          console.log(this);           
        }
        innerFunc();                   // Window
        
        let obj2 = {
          innerMethod: innerFunc
        };
        obj2.innerMethod();            // obj2
        
        let a = obj2.innerMethod;
        a();                           // Window
      }
    };
    obj1.outer();

    위 예제에서는 같은 함수임에도 다른 결과를 가져오는 것을 알 수 있다.

    다시 말해, this 바인딩에 관하여 함수 실행 당시의 주변 환경(메서드 내부인지 함수 내부인지 등)은 전혀 중요하지 않다.

    오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기의 여부가 this 바인딩을 결정하는 유일한 조건인 것이다.

     

    아래 예제는 ES5까지에서 메서드 내부 함수에서의 this를 우회하는 방법이다.

    self라는 변수에 this를 저장한 상태에서 함수를 호출하면 메서드 호출과 같은 this의 값을 가리키는 것을 알 수 있다.

    let obj = {
      outer: function () {
        console.log(this);              // {outer: f}
        
        let self = this;
        let innerFunc1 = function () {
          console.log(self);
        };
        innerFunc1();                   // {outer: f}
      }
    };
    obj.outer();

     

    ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제를 보완하기 위해 this를 바인딩하지 않는 화살표 함수(arrow function)를 도입했다. 화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩를 하지 않고, 상위 스코프의 this를 그대로 활용할 수 있다.

    let obj = {
      outer: function () {
        console.log(this);        // {outer: f}
    
        let innerFunc = () => {
          console.log(this);
        };
        innerFunc();              // {outer: f}
      }
    };
    obj.outer();

     

    콜백 함수 호출 시 그 함수 내부에서의 this

    콜백 함수는 다른 코드의 인자로 넘겨주는 함수다. 콜백 함수를 위임받은 코드는 자체적인 내부 로직에 의해 이 콜백 함수를 적절한 시점에 실행하게 된다.

     

    콜백 함수도 함수이기 때문에 기본적으로 this가 전역 객체를 참조한다. 하지만, 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 된다.

    setTimeout(function () {console.log(this);}, 300);
    
    let arr = [1, 2, 3];
    arr.forEach(function(x) {
      console.log(this, x);     // Window 1, Window 2, Window 3
    });
    
    document.body.innerHTML += '<button id="a">버튼</button>';
    document.body.querySelector('#a')
      .addEventListener('click', function(e) {
        console.log(this, e);
      });

    setTimeout 함수와 forEach 메서드 내부에서의 this는 전역 객체를 참조한다. 반면에 addEventListener 메서드는 콜백 함수를 호출할 때 자신의 this를 상속하도록 정의되어 있기 때문에, 메서드명의 점(.) 앞부분이 this가 된다.

     

    이처럼 콜백 함수의 제어권을 가진 함수(메서드)가 콜백 함수에서의 this를 무엇으로 할지 결정하는데, 특별히 정의하지 않는다면 일반적인 함수와 같이 전역 객체를 참조한다.

     

    생성자 함수 내부에서의 this

    생성자 함수는 어떤 공통된 성질을 지닌 객체들을 생성하는 데 사용하는 함수다. 객체지향 언어에서는 생성자를 클래스(class), 클래스를 통해 만든 객체를 인스턴스(instance)라고 한다.

     

    자바스크립트에서 함수는 생성자로서의 역할을 할 수 있다. new 명령어로 함수를 호출하면 해당 함수가 생성자로서 동작하게 된다. 중요한 것은 어떤 함수가 생성자 함수로서 호출되었을 때, 내부의 this가 새로 만들어질 인스턴스의 자신을 바라본다.

    let Person = function (name, age) {
      this.name = name;
      this.age = age;
    };
    
    let mok = new Person('Mok', 10);
    let tom = new Person('Tom', 14);
    console.log(mok, tom);
    
    // Person { name: 'Mok', age: 10 }
    // Person { name: 'Tom', age: 14 }

     

    명시적으로 this 바인딩하는 방법

    call 메서드 & apply 메서드

    call 메서드는 메서드의 호출 주체가 되는 함수를 즉시 실행하도록 한다.

    call 메서드의 첫 번째 인자는 this로 바인딩되고, 이후의 인자들은 호출할 함수의 매개변수가 된다.

     

    apply 메서드도 첫 번째 인자는 call 메서드와 같이 this로 바인딩된다. 유일한 차이점은 call 메서드의 경우 첫 번째 인자를 제외한 나머지 모든 인자들을 호출할 함수의 파라미터로 지정하지만, apply 메서드의 경우엔 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 파라미터로 지정한다는 점이다.

    즉, 첫 번째 인자에 this로 세팅하고 싶은 객체를 넘겨주면 this를 바꾼 후 함수를 실행한다.

    let obj = {name: 'Mok'};
    let say = function(city) {
      console.log(`My name is ${this.name}, I live in ${city}`);
    };
    
    say("New York City");               // My name is , I live in New York City
    say.call(obj, "New York City");     // My name is Mok, I live in New York City
    say.apply(obj, ["New York City"]);  // My name is Mok, I live in New York City

     

    call / apply 메서드의 활용

    1. 유사배열객체에 배열 메서드 적용

    유사배열객체(array-like object) : 키가 0 또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티의 값이 0 또는 양의 정수인 객체

    객체에는 배열 메서드를 직접 적용할 수 없지만, 배열의 구조와 유사한 객체의 경우(유사배열객체) call 또는 apply 메서드를 이용해 배열 메서드를 차용할 수 있다. 

    arguments 객체도 유사배열객체이기 때문에 아래 방법을 통해 배열로 전환해서 활용이 가능하다.

    let obj = {
      0: 'a',
      1: 'b',
      2: 'c',
      length: 3
    };
    
    Array.prototype.push.call(obj, 'd');
    console.log(obj);   // {0: "a", 1: "b", 2: "c", 3: "d", length: 4}
    
    let arr = Array.prototype.slice.call(obj);
    console.log(arr);   // ["a", "b", "c", "d"]

    ES6에서는 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 Array.from 메서드를 새롭게 도입했다.

    let obj = {
      0: 'a',
      1: 'b',
      2: 'c',
      length: 3
    };
    
    let arr = Array.from(obj);
    console.log(arr);  // ["a", "b", "c"]

     

    2. 생성자 내부에서 다른 생성자를 호출

    function Person(name, gender) {
      this.name = name;
      this.gender = gender;
    }
    
    function Student(name, gender, school) {
      Person.call(this, name, gender);
      this.school = school;
    }
    
    function Employee(name, gender, company) {
      Person.apply(this, [name, gender]);
      this.company = company;
    }
    
    let mok = new Student('Mok', 'male', '버클리');
    let tom = new Employee('Tok', 'male', '구글');
    console.log(mok);  // Student {name: "Mok", gender: "male", school: "버클리"}
    console.log(tom);  // Employee {name: "Tok", gender: "male", company: "구글"}

     

    3. 여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 - apply 메서드

     

    call / apply 메서드는 명시적으로 별도의 this를 바인딩하는 ES5 이하의 환경에서의 훌륭한 대안이다. 하지만 this를 예측하기 어렵게 만들어 코드 해석을 방해한다는 단점이 존재한다.

     

    bind 메서드

    bind 메서드는 ES5에서 새롭게 추가된 메서드로, 보통 객체 메서드의 this를 지정해 어딘가에 넘기고자 할 때 사용한다.

    call 메서드와 비슷하지만 즉시 호출하지는 않고, 넘겨 받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드다.

    다시 말해, bind 메서드는 this를 지정하는 것 뿐만 아니라 함수의 인수도 지정해줄 수 있다.

    let func = function (a, b, c, d) {
      console.log(this, a, b, c, d);
    };
    func(1, 2, 3, 4);                           // Window{...} 1 2 3 4
    
    let bindFunc1 = func.bind({ x: 1 });        // this만을 지정한 경우
    bindFunc1(5, 6, 7, 8);                      // { x: 1 } 5 6 7 8
    
    let bindFunc2 = func.bind({ x: 1 }, 4, 5);  // this 지정 + 앞에서부터 두 개의 인수 4, 5로 지정
    bindFunc2(6, 7);                            // { x: 1 } 4 5 6 7
    bindFunc2(8, 9);                            // { x: 1 } 4 5 8 9
    
    console.log(func.name);                     // func
    console.log(bindFunc2.name);                // bound func

    bind 메서드를 적용해서 새로 만든 함수는 name 프로퍼티에 bound라는 접두어가 붙는다. 이를 통해 원본 함수에 bind 메서드를 적용했단 사실을 알 수 있으므로 call이나 apply보다 코드 추적을 수월하게 할 수 있다.

     

    화살표 함수의 예외사항

    화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외됐다. 그러므로 이 함수 내부에는 this 자체가 없으며, 접근하려고 한다면 스코프체인상 가장 가까운 this에 접근하게 된다.

    let obj = {
      outer: function() {
        console.log(this);        // {outer: ƒ}
        let innerFunc = () => {
          console.log(this);
        };
        innerFunc();              // {outer: ƒ}
      }
    };
    obj.outer();

     

    별도의 인자로 this를 받는 경우(콜백 함수 내에서의 this)

    콜백 함수를 인자로 받는 메서드 중 this로 지정할 객체(thisArg)를 인자로 지정이 가능한 경우가 있다.

    thisArg 값을 지정하면 콜백 함수 내부에서 this 값을 원하는 대로 변경가능하다.

     

    아래는 콜백 함수와 함께 thisArg를 인자로 받는 메서드들이다.

    • forEach, map, filter, some, every, find, findIndex, flatMap, from, Set, Map etc.
    let report = {
      sum: 0,
      count: 0,
      add: function() {
        let args = Array.prototype.slice.call(arguments);
        args.forEach(function(entry) {
          this.sum += entry;
          ++this.count;
        }, this);
      }
    };
    
    report.add(60, 85, 95);
    console.log(report.sum, report.count);  // 240 3

    댓글

Designed by Tistory.