목차

공부/Javascript

this와 JS의 함수 호출 방식 (call, apply, bind)

천만일 2022. 2. 24. 01:09

this는 무엇일까!

보통 일반적인 객체지향 언어에서 this는 객체 자신을 의미합니다. 마찬가지로 JS에서도 자신을 의미하긴 하지만 JS는 객체와 함수의 구분이 모호하고, 어디서든 사용할 수 있기 때문에 상황에 따라 의미하는 바가 달라지기도 합니다.

this는 실행 컨텍스트가 생성될 때 함께 결정되기 때문에 함수를 호출할 때 결정된다고 할 수 있습니다. (실행 컨텍스트는 함수를 호출할 때 생성되기 때문에!)

전역 공간

어쩌면 당연하게도 전역 공간에서 this는 전역 객체를 가리킵니다.

브라우저 환경에서 전역 객체의 이름은 window이고, Node.js 환경에서는 global입니다.

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

전역 공간에서 변수 a를 선언하면 자바스크립트 엔진에서는 전역 컨텍스트가 a를 LexicalEnvironment의 프로퍼티에 저장하도록 합니다.

이후에 a를 호출 할 때에는 스코프 체인에서 a를 찾아 올라가다가 전역 컨텍스트의 LexicalEnvironment에 있는 a를 발견하여 반환하게 됩니다.

메서드 내부

JS에서의 메서드는 객체의 메서드로 호출할 경우에만 메서드로 동작합니다.

즉, 메서드에서의 this는 메서드를 호출한 객체입니다.

함수 내부

함수는 메서드와 다르게 호출한 주체가 없습니다. 함수를 호출하면 실행 컨택스트가 생성되는데 이 때 this가 지정되지 않으면 this는 전역 객체로 설정됩니다.

내부 함수에서의 this

const obj1 = {
    outer: function() {
        console.log(this);
        const innerFunc = function() {
            console.log(this);
        }
        innerFunc();                   // (2)

        const obj2 = {
            innerMethod: innerFunc
        };
        obj2.innerMethod();            // (3)
    }
}

obj1.outer();                      // (1)
  • (1)
  • obj1.outer()를 호출하면 가장 먼저 console.log(this)를 호출하는데 이 때의 this는 outer가 obj1의 메서드로 실행되었기 때문에 obj1이 됩니다.
  • (2)
  • innerFunc()은 메서드가 아닌 함수로 호출되었습니다. 따라서 this가 지정되지 않았으므로 자동으로 this는 전역 객체로 바인딩됩니다.
  • (3)
  • obj2.innerMethod()는 obj2라는 객체의 메서드로 호출되었기 때문에 this는 obj2가 됩니다.

위와 같은 형태는 결국 사용자가 this라는 변수에 대해 직관적으로 활용하기 어렵게 합니다. 이에 대한 대안으로 ES6에서는 화살표 함수(Arrow Function)이 등장합니다.

화살표 함수(Arrow Function)

화살표 함수는 this를 바인딩하지 않습니다. 위에서 보았던 함수들의 문제는 함수를 호출하면 실행 컨텍스트를 생성하는데 이 때 this를 바인딩하는 과정을 거치는 것이었습니다.

하지만 화살표 함수로 선언된 함수를 호출하여 실행 컨텍스트가 생성될 때에는 this를 바인딩하지 않습니다.

const obj = {
    outer: function() {
        console.log(this);               // (1)
        const innerFunc = () => {
            console.log(this);             // (2)
        };
        innerFunc();
    }
};

obj.outer();

(1)에서의 thisobj.outer()가 호출될 때, 메서드를 호출한 객체가 obj이므로 obj가 바인딩 되었습니다.

(2)의 innerFunc는 화살표 함수로 선언되었기 때문에 innerFunc()를 호출할 때, this가 바인딩되지 않습니다. 따라서 console.log(this);에서 this를 찾기 위해 스코프 체인을 따라 올라가서 outer 함수의 실행 콘텍스트에 있는 this를 반환합니다.

콜백 함수 내부의 this

함수의 제어권을 다른 함수에게 넘긴 경우 제어권이 넘어간 함수를 콜백 함수라고 합니다.

콜백 함수도 함수이기 때문에 전역 객체를 this에 바인딩하는 것을 기본으로 합니다.

단, 콜백 함수의 제어권을 가진 함수가 임의로 콜백 함수의 this를 지정했다면 그것이 바인딩됩니다.

setTimeout(function () { console.log(this); }, 300);

[1, 2, 3, 4, 5].forEach(function (x) {
    console.log(this);
})

document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a').addEventListener('click', function (e) {
    console.log(this);
})

위 예제의 setTimeout함수와 forEach 메서드는 임의로 콜백 함수의 this를 지정하지 않기 때문에 this는 전역 객체를 참조합니다.

하지만, addEventListener 메서드는 콜백 함수가 호출될 때, 자신의 this를 상속하도록 정의되어 있기 때문에 this는 document.body.querySelector('#a')를 참조합니다.

생성자 함수 내부의 this

JS에서는 new 명령어와 함수를 호출하면 호출된 함수가 인스턴스를 생성하는 생성자 함수로 동작합니다. 이 때 생성자 함수 내부의 this는 생성자 함수가 만들 인스턴스 자신을 의미합니다.

const Cat = function (name, age) {
    this.name = name;
    this.age = age;
}

this 바인딩하기

call

Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])

call 메서드는 이름 그대로 함수를 실행하는 명령어입니다. 단, 첫번째 인자로 들어오는 객체를 this에 바인딩합니다.

const func = function (a, b, c) {
    console.log(this, a, b, c);
};

func(1, 2, 3);                     // Window{...} 1 2 3     - (1)
func.call({x: 1}, 4, 5, 6);        // {x: 1} 4 5 6          - (2)
  • (1)이는 func.call(window, 1, 2, 3); 과도 동일합니다.
  • func(1, 2, 3);를 호출하면 this는 전역 객체가 됩니다.
  • (2)
  • 첫 번째 매개변수가 this로 바인딩된 것을 볼 수 있습니다.

const obj = {
    a: 1,
    method: function (x, y) {
        console.log(this.a, x, y)
    }
};

obj.method(2, 3);                  // 1 2 3                 - (1)
obj.method.call({a: 4}, 5, 6)      // 4 5 6                 - (2)
  • (1)이는 obj.method.call(obj, 2, 3)과도 동일합니다.
  • methodobj의 메서드로 호출되었으므로 this에는 obj가 바인딩 되었습니다.
  • (2)
  • call 메서드를 사용하면 첫 번째 매개변수가 this에 바인딩됩니다.

apply

Function.prototype.apply(thisArg[, argsArray])

apply 메서드는 call 메서드와 기능이 동일합니다.

다만 두 번째 인자에 배열을 받고, 배열의 원소를 함수를 호출할 때 매개변수로 지정합니다.

  • call / apply 활용 예시
    1. 유사배열객체에 배열 메서드를 적용대표적인 유사배열객체로는 함수 내부에서 접근 가능한 arguments 객체가 있습니다.
       const 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}
      
       const arr = Array.prototype.slice.call(obj);
       console.log(arr)        // ['a', 'b', 'c', 'd']
    2. 유사배열객체는 객체이기 때문에 Array.prototype의 메서드를 사용할 수 없는데, call / apply를 활용하면 가능합니다.
    3. key 값이 0 이상의 정수이고, length 프로퍼티가 존재하는 객체를 유사배열객체라고 합니다.
    4. 생성자 내부에서 다른 생성자를 호출이를 활용하면 생성자를 재사용할 수 있습니다.
    5. 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.call(this, name, gender); this.company = company; }
    6. new 키워드를 사용하면 생성자 함수를 호출할 수 있는데 이 때 this는 생성될 인스턴스를 참조합니다.
  • apply 활용 예시
    1. 여러 인수를 묶어서 하나의 배열로 전달할 때
    2. const numbers = [10, 20, 30, 40, 50]; const max = Math.max.apply(null, numbers); // ES6에 나온 spread 연산자로도 처리 가능 // Math.max(...numbers); const min = Math.min.apply(null, numbers); console.log(max, min);

bind

Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])

bind 메서드는 call과 동일한 형태를 띄지만, 함수를 호출하는 것이 아닌 인자를 바탕으로 새로운 함수를 반환하기만 합니다.

  • bind 메서드의 목적
    1. this를 미리 적용하는 것
    2. 부분적으로 인자를 미리 적용하는 것
      const func = function (a, b, c, d) {
       console.log(this, a, b, c, d);
      }
      
      func(1, 2, 3, 4);                   // Window{ ... } 1 2 3 4
      
      const bindFunc1 = func.bind({x: 1});
      bindFunc1(5, 6, 7, 8);              // {x: 1} 5 6 7 8
      
      const bindFunc2 = func.bind({x: 1}, 4, 5);
      bindFunc2(6, 7);                    // {x: 1} 4 5 6 7
      bindFunc2(8, 9);                    // {x: 1} 4 5 8 9
    3. 입니다.
  • bind로 생성된 함수의 name 프로퍼티bind 메서드로 생성된 함수는 name 프로퍼티bound ${원본 함수명} 을 가진다는 특징이 있습니다.
  • const bindFunc = func.bind({x: 1}, 4, 5); console.log(func.name); // func console.log(bindFunc.name); // bound func
  • 기본적으로 모든 함수에는 name 프로퍼티가 존재하는데 이는 함수의 이름을 담고 있습니다.

화살표 함수

위에서 화살표 함수로 생성된 함수는 실행 컨텍스트를 생성하는 과정 중에 this를 바인딩하는 과정이 생략된다고 언급한 바가 있습니다.

따라서 화살표 함수는 this가 존재하지 않습니다. 화살표 함수 내부에서 this에 접근하면 스코프 체인을 따라 가장 가까운 this에 접근합니다.

const obj = {
    outer: function () {
        console.log(this);
        var innerFunc = () => {
            console.log(this);
        }    
    }
};

obj.outer();

this를 인자로 받는 함수

일부 콜백 함수를 인자로 받는 메서드들은 this로 지정할 객체도 받을 수 있는 경우가 있습니다.

아래는 this를 지정할 수 있는 메서드중 하나인 Array.prototype.forEach() 입니다.

const report = {
  sum: 0,
  count: 0,
  add: function () {
    const args = Array.prototype.slice.call(arguments);
    args.forEach(function (el) {
      this.sum += el;
      ++this.count;
    }, this);
  },
  average: function () {
    return this.sum  / this.count;
  }
}

forEach()this를 넘기지 않았다면, 콜백 내부에서는 this를 전역객체로 인지했겠지만, this를 report 객체로 지정함으로써 콜백 내부에서도 report 객체의 sum과 count 프로퍼티에 접근할 수 있습니다.