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

2024년 6월 2일 by지우진
thumbnail_3-2.png

this에 별도의 대상을 바인딩하는 방법이 있다고 합니다.
call, apply, bind와 같은 함수를 사용하는 방법을 소개하겠습니다.

call 메서드

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

call 메서드는 메서드의 호출 주체인 함수를 즉시 실행하는 명령어입니다.
이때, call 메서드의 첫번째 인자를 this로 바인딩하고, 이후 인자들을 호출할 함수의 매개변수로 합니다.

apply 메서드

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

apply 메서드는 call 메서드와 기능적으로 동일합니다.
call 메서드와 다른 점은 두번째 인자를 배열로 받아 그 배열의 요소들을 호출한 함수의 매개변수로 지정합니다.

call / apply 메서드의 활용

유사배열객체(array-like object)에 배열 메서드를 적용

객체에는 배열 메서드를 직접 적용할 수 없습니다.
유사배열객체도 마찬가지 입니다. 하지만 call이나 apply 메서드를 이용해서 배열 메서드를 차용할 수 있습니다.

💡

유사배열객체란 키가 0이상의 정수인 프로퍼티가 존재하고 length 프로퍼티의 값이 0 또는 양의 정수인 객체입니다.

유사배열객체의 배열 메서드 사용
var obj = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
};
Array.prototype.push.call(obj, 'd'); // Add 'd' to obj
var arr = Array.prototype.slice.call(obj); // copy obj to array

위 코드와 같이 배열 메서드인 push를 객체 obj에 적용해서 프로퍼티 3d를 추가할 수 있습니다.
또한, slice 배열 메서드를 적용해 객체를 배열로 전환할 수 있습니다. 이외에도 arguments, NodeList에 모든 배열 메서드를 적용할 수 있습니다.

더 나아가, 배열과 같이 인덱스와 length 프로퍼티를 지니는 문자열에 대해서도 마찬가지로 call,apply를 활용해 배열 메서드를 적용할 수 있습니다. 그러나 문자열은 length 프로퍼티가 읽기 전용이기 때문에 원본 문자열에 변경을 가하는 메서드는 에러가 나고, 대상이 반드시 배열이어야 하는 경우에는 제대로 작동하지 않습니다.

사실 이러한 call, apply를 통해 형변환하는 것은 this와는 다소 동떨어진 방법입니다. 그래서 ES6에서는 순회가능한 모든 데이터 타입을 배열로 전환하는 Array.from이라는 메서드를 새로 도입했습니다.

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

생성자 내부에서 다른 생성자 호출

생성자 내부에서 다른 생성자 호출
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;
}
var kim = new Student('김김김', 'male', 'High School');
var lee = new Student('이이이', 'female', 'Middle School');

위 코드와 같이 생성자 내부에서 다른 생성자와 공통된 내용이 있을 경우 call 또는 apply 메서드를 이용해 다른 생성자를 호출하면 간단하게 반복을 줄일 수 있습니다.

여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 - apply 활용

여러개의 인수를 하나의 배열로
var numbers = [10, 20, 3, 16, 45];
var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);

여러개의 인수를 받는 메서드에게 하나의 배열로 인수들을 전달하고 싶을 때 apply 메서드를 사용하면 코드를 더 간단하게 작성할 수 있습니다.

ES6의 펼치기 연산자
const numbers = [10, 20, 3, 16, 45];
const max = Math.max(...numbers);
const min = Math.min(...numbers);

참고로 ES6에서는 펼치기 연산자로 간단하게 작성 가능합니다.

💡

call, apply 메서드는 명시적으로 별도의 this를 바인딩하면서 함수 또는 메서드를 실행하는 좋은 방법이지만 오히려 이로 인해 this를 예측하기 어렵게 만들어 코드 해석을 방해한다는 단점이 있습니다. 그럼에도 불구하고 ES5이하의 환경에서는 마땅한 대안이 없어서 실무에서 광범위하게 사용됩니다.

bind 메서드

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

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

bind메서드의 활용
var func = function (a, b, c, d) {
  console.log(this, a, b, c, d);
};
func(1, 2, 3, 4); // Window 1 2 3 4
 
var bindFunc1 = func.bind({ x: 1 });
bindFunc1(5, 6, 7, 8); // { x:1 } 5 6 7 8
 
var 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

다시 새로운 함수를 호출할 때 인수를 넘기면 그 인수들은 기존 bind 메서드를 호출할 때 전달했던 인수들의 뒤에 이어서 등록됩니다.
bind 메서드는 함수에 this를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 가집니다.

name 포로퍼티

bind 메서드를 적용해서 새로 만든 함수는 name프로퍼티에 'bound'라는 접두어가 붙습니다. 이를 통해 기존의 callapply보다 코드를 추적하기 더 수월하게 해줍니다.

상위 컨텍스트의 this를 내부함수나 콜백 함수에 전달하기

메서드의 내부함수에서 메서드의 this를 그대로 바라보게 하는 방법에는 self 변수를 만드는 방법도 있지만, 보다 깔끔하게 call, apply, bind를 쓰는 방법도 있습니다.

this를 내부함수에 전달하기
var obj = {
  outer: function () {
    console.log(this); // outer
    var innerFunc = function () {
      console.log(this); // outer
    };
    innerFunc.call(this);
  },
};
obj.outer();
 
var obj = {
  outer: function () {
    console.log(this); // outer
    var innerFunc = function () {
      console.log(this); // outer
    }.bind(this);
    innerFunc();
  },
};
obj.outer();

콜백 함수를 인자로 받는 함수나 메서드 중에서 기본적으로 콜백함수 내애서의 this에 관여하는 함수 또는 메서드에 대해서도 bind 메서드를 이용하면 this의 값을 조작할 수 있습니다.

this를 콜백함수에 전달하기
var obj = {
  logThis: function () {
    console.log(this);
  },
  logThisLater1: function () {
    setTimeout(this.logThis, 500);
  },
  logThisLater2: function () {
    setTimeout(this.logThis.bind(this), 1000);
  },
};
obj.logThisLater1(); // Window
obj.logThisLater2(); // logThis

화살표 함수의 예외사항

ES6에서 새롭게 도입된 화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 빠졌습니다. 즉 함수 내부에는 this가 없고, 접근하고자 하면 스코프체인상 가장 가까운 this에 접근합니다.

화살표 함수의 this
var obj = {
  outer: function () {
    console.log(this); // outer
    var innerFunc = () => {
      console.log(this); // outer
    };
    innerFunc();
  },
};
obj.outer(); // outer

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

콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체를 인자로 지정할 수 있는 경우가 있습니다. 이러한 메서드를 이용하면 콜백함수 내부에서 this값을 원하는 대로 변경할 수 있습니다.
주로 반복 수행하는 배열 메서드에 많이 포진되어 있으며, 같은 이유로 ES6에 새로 나온 Set, Map 등의 메서드에도 일부 존재합니다. 대표적으로 forEach가 있습니다.

forEach 예시
var report = {
  sum: 0,
  count: 0,
  add: function () {
    var args = Array.prototype.slice.call(arguments);
    args.forEach(function (entry) {
      this.sum += entry;
      ++this.count;
    }, this);
  },
  average: function () {
    return this.sum / this.count;
  },
};
report.add(60, 85, 95);
console.log(report.sum, report.count, report.average()); // 240 3 80

콜백 함수를 실행할 때, 콜백 함수 내부에서의 thisforEach함수의 두번째 인자로 전달해준 this가 바인딩 됩니다.