Function.prototype.bind() 메서드

Function.prototype.bind() 메서드

bind()Function 인스턴스의 메서드로, 호출 시 지정된 this 값을 사용하고 주어진 인자들이 먼저 전달되는 새로운 함수를 생성합니다. 이렇게 생성된 "바운드 함수(bound function)"는 원본 함수의 this 고정과 부분 적용(partial application)을 한 번에 해결하는 핵심 도구입니다.

출처: Function.prototype.bind() - MDN Web Docs

본문

개요

bind() 메서드는 Function 인스턴스가 호출될 때, 이 함수를 호출하면서 this 키워드를 제공된 값으로 설정하고, 새 함수가 호출될 때 제공되는 인자들 앞에 주어진 인자 시퀀스를 앞세워 호출하는 새로운 함수를 생성합니다. ECMAScript 2015(ES6) 이후 모든 주요 브라우저에서 널리 지원됩니다.

기본적인 사용 예는 다음과 같습니다.

const module = {
  x: 42,
  getX() {
    return this.x;
  },
};

const unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// Expected output: undefined

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// Expected output: 42

구문(Syntax)

bind(thisArg)
bind(thisArg, arg1)
bind(thisArg, arg1, arg2)
bind(thisArg, arg1, arg2, /* …, */ argN)

매개변수

  • thisArg — 바운드 함수가 호출될 때 타깃 함수 functhis 매개변수로 전달할 값입니다. 함수가 strict mode가 아니면 nullundefined는 전역 객체로 치환되고, 원시 값은 객체로 변환됩니다. 바운드 함수가 new 연산자로 생성(구성)될 때는 이 값이 무시됩니다.
  • arg1, …, argN (선택)func를 호출할 때 바운드 함수에 제공되는 인자들 앞에 붙일 인자들입니다.

반환값 — 지정된 this 값과 (제공된 경우) 초기 인자를 가진, 주어진 함수의 복사본입니다.

설명(Description)

bind() 함수는 새로운 바운드 함수를 생성합니다. 바운드 함수를 호출하면 일반적으로 그 함수가 감싸고 있는 함수(이를 *타깃 함수(target function)*라고도 함)의 실행이 일어납니다. 바운드 함수는 전달된 매개변수 — 즉 this의 값과 처음 몇 개의 인자 — 를 내부 상태로 저장합니다. 이 값들은 호출 시점에 전달되는 것이 아니라 미리 저장됩니다. 일반적으로 const boundFn = fn.bind(thisArg, arg1, arg2)는 호출 시점의 효과 측면에서 const boundFn = (...restArgs) => fn.call(thisArg, arg1, arg2, ...restArgs)와 동등하다고 볼 수 있습니다(단, boundFn이 생성자로 쓰일 때는 다릅니다).

바운드 함수는 다시 boundFn.bind(thisArg, /* more args */)를 호출해 더 바인딩할 수 있으며, 이는 또 다른 바운드 함수 boundFn2를 만듭니다. 새로 바인딩된 thisArg 값은 무시됩니다. 그 이유는 boundFn2의 타깃 함수인 boundFn이 이미 바운드된 this를 갖고 있기 때문입니다. boundFn2가 호출되면 boundFn을 호출하고, 다시 fn을 호출하게 됩니다. fn이 최종적으로 받는 인자는 순서대로: boundFn에 의해 바인딩된 인자, boundFn2에 의해 바인딩된 인자, 그리고 boundFn2가 받은 인자입니다.

"use strict"; // prevent `this` from being boxed into the wrapper object

function log(...args) {
  console.log(this, ...args);
}
const boundLog = log.bind("this value", 1, 2);
const boundLog2 = boundLog.bind("new this value", 3, 4);
boundLog2(5, 6); // "this value", 1, 2, 3, 4, 5, 6

타깃 함수가 생성 가능(constructable)하다면 바운드 함수도 new 연산자로 생성할 수 있습니다. 이 경우 타깃 함수가 직접 생성된 것처럼 동작합니다. 앞에 붙은 인자들은 평소처럼 타깃 함수에 전달되지만, 제공된 this 값은 무시됩니다(생성이 자체적인 this를 준비하기 때문이며, 이는 Reflect.construct의 매개변수에서 확인할 수 있습니다). 바운드 함수가 직접 생성되면 new.target은 타깃 함수가 됩니다(즉, 바운드 함수는 new.target에 대해 투명합니다).

class Base {
  constructor(...args) {
    console.log(new.target === Base);
    console.log(args);
  }
}

const BoundBase = Base.bind(null, 1, 2);

new BoundBase(3, 4); // true, [1, 2, 3, 4]

다만 바운드 함수에는 prototype 프로퍼티가 없으므로 extends의 기반 클래스로는 쓸 수 없습니다.

class Derived extends class {}.bind(null) {}
// TypeError: Class extends value does not have valid prototype property undefined

바운드 함수를 instanceof의 우변으로 사용하면, instanceof는 타깃 함수(바운드 함수 내부에 저장됨)에 접근해 그 prototype을 읽습니다.

class Base {}
const BoundBase = Base.bind(null, 1, 2);
console.log(new Base() instanceof BoundBase); // true

바운드 함수는 다음과 같은 프로퍼티를 가집니다.

  • length — 타깃 함수의 length에서 바인딩된 인자 수(thisArg 매개변수는 제외)를 뺀 값이며, 최솟값은 0입니다.
  • name — 타깃 함수의 name"bound " 접두사가 붙은 값입니다.

바운드 함수는 또한 타깃 함수의 prototype chain을 상속합니다. 하지만 타깃 함수의 다른 자체 프로퍼티(예: 타깃 함수가 클래스일 때의 static 프로퍼티)는 갖지 않습니다.

예제

바운드 함수 만들기bind()의 가장 흔한 용도는 어떻게 호출되든 특정 this 값으로 호출되는 함수를 만드는 것입니다. 초보자들이 흔히 저지르는 실수는 객체에서 메서드를 추출한 뒤, 그 함수를 나중에 호출하면서 원래 객체를 this로 사용하리라 기대하는 것입니다(예: 콜백 기반 코드에서 메서드를 사용하는 경우). 특별히 주의하지 않으면 원래 객체는 대개 잃게 됩니다. 원래 객체를 사용해 함수에서 바운드 함수를 만들면 이 문제가 깔끔하게 해결됩니다.

// Top-level 'this' is bound to 'globalThis' in scripts.
this.x = 9;
const module = {
  x: 81,
  getX() {
    return this.x;
  },
};

// The 'this' parameter of 'getX' is bound to 'module'.
console.log(module.getX()); // 81

const retrieveX = module.getX;
// The 'this' parameter of 'retrieveX' is bound to 'globalThis' in non-strict mode.
console.log(retrieveX()); // 9

// Create a new function 'boundGetX' with the 'this' parameter bound to 'module'.
const boundGetX = retrieveX.bind(module);
console.log(boundGetX()); // 81

참고: 이 예제를 strict mode에서 실행하면 retrieveXthis 매개변수가 globalThis 대신 undefined로 바인딩되어 retrieveX() 호출이 실패합니다. ECMAScript 모듈에서 실행하면 최상위 thisundefined로 바인딩되어 this.x = 9 할당이 실패합니다. Node CommonJS 모듈에서 실행하면 최상위 thismodule.exports에 바인딩됩니다.

실제로 내장 "메서드" 중에는 게터(getter)로 바운드 함수를 반환하는 것도 있습니다 — 대표적인 예가 Intl.NumberFormat.prototype.format()인데, 접근 시 바로 콜백으로 전달할 수 있는 바운드 함수를 반환합니다.

부분 적용(Partially applied functions)bind()의 또 다른 용도는 사전 지정된 초기 인자를 가진 함수를 만드는 것입니다. 이 인자들은 제공된 this 값 뒤에 따라오며, 바운드 함수가 호출될 때 전달되는 인자들 앞부분에 삽입됩니다.

function list(...args) {
  return args;
}

function addArguments(arg1, arg2) {
  return arg1 + arg2;
}

console.log(list(1, 2, 3)); // [1, 2, 3]

console.log(addArguments(1, 2)); // 3

// Create a function with a preset leading argument
const leadingThirtySevenList = list.bind(null, 37);

// Create a function with a preset first argument.
const addThirtySeven = addArguments.bind(null, 37);

console.log(leadingThirtySevenList()); // [37]
console.log(leadingThirtySevenList(1, 2, 3)); // [37, 1, 2, 3]
console.log(addThirtySeven(5)); // 42
console.log(addThirtySeven(5, 10)); // 42
// (the last argument 10 is ignored)

setTimeout()와 함께 사용 — 기본적으로 setTimeout() 안에서 this 키워드는 브라우저에서는 windowglobalThis로 설정됩니다. this가 클래스 인스턴스를 가리켜야 하는 클래스 메서드를 다룰 때는 this를 콜백 함수에 명시적으로 바인딩해 인스턴스를 유지할 수 있습니다.

class LateBloomer {
  constructor() {
    this.petalCount = Math.floor(Math.random() * 12) + 1;
  }
  bloom() {
    // Declare bloom after a delay of 1 second
    setTimeout(this.declare.bind(this), 1000);
  }
  declare() {
    console.log(`I am a beautiful flower with ${this.petalCount} petals!`);
  }
}

const flower = new LateBloomer();
flower.bloom();
// After 1 second, calls 'flower.declare()'

이 목적에는 arrow 함수를 사용할 수도 있습니다. setTimeout(() => this.declare(), 1000)처럼 말이죠.

생성자로 사용되는 바운드 함수 — 바운드 함수는 타깃 함수가 만든 새 인스턴스를 구성하기 위해 new 연산자와 함께 사용하기에 자동으로 적합합니다. 바운드 함수로 값을 구성할 때 제공된 this는 무시되지만, 제공된 인자들은 여전히 생성자 호출 앞에 붙습니다.

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return `${this.x},${this.y}`;
};

const p = new Point(1, 2);
p.toString();
// '1,2'

// The thisArg's value doesn't matter because it's ignored
const YAxisPoint = Point.bind(null, 0 /* x */);

const axisPoint = new YAxisPoint(5);
axisPoint.toString(); // '0,5'

axisPoint instanceof Point; // true
axisPoint instanceof YAxisPoint; // true
new YAxisPoint(17, 42) instanceof Point; // true

new와 함께 쓰기 위한 바운드 함수를 만드는 데 특별한 처리가 필요하지 않습니다. new.target, instanceof, this 등이 마치 생성자가 바인딩되지 않은 것처럼 정상 동작합니다. 유일한 차이는 더 이상 extends에 사용할 수 없다는 점입니다. 반대로, 바운드 함수를 new 없이 평범하게 호출하면 바인딩된 this가 갑자기 무시되지 않습니다.

const emptyObj = {};
const YAxisPoint = Point.bind(emptyObj, 0 /* x */);

// Can still be called as a normal function
// (although usually this is undesirable)
YAxisPoint(13);

// The modifications to `this` is now observable from the outside
console.log(emptyObj); // { x: 0, y: 13 }

바운드 함수를 new로만 또는 new 없이만 호출되도록 제한하려면, 타깃 함수가 new.target !== undefined 검사나 class 사용 같은 방식으로 그 제한을 강제해야 합니다.

클래스 바인딩 — 클래스에 bind()를 사용하면 현재 클래스의 모든 static 자체 프로퍼티가 사라지는 것을 제외하고 대부분의 클래스 의미론이 보존됩니다. 다만 prototype chain은 보존되므로 부모 클래스에서 상속된 static 프로퍼티에는 여전히 접근할 수 있습니다.

class Base {
  static baseProp = "base";
}

class Derived extends Base {
  static derivedProp = "derived";
}

const BoundDerived = Derived.bind(null);
console.log(BoundDerived.baseProp); // "base"
console.log(BoundDerived.derivedProp); // undefined
console.log(new BoundDerived() instanceof Derived); // true

메서드를 유틸리티 함수로 변환bind()는 특정 this 값을 요구하는 메서드를, 이전의 this 매개변수를 일반 매개변수로 받는 평범한 유틸리티 함수로 변환하려는 경우에도 유용합니다. 이는 일반적인 범용 유틸리티 함수의 방식과 비슷합니다 — array.map(callback) 대신 map(array, callback)을 사용하는 것처럼 말이죠. 이를 통해 Object.prototype을 변경하지 않으면서도 배열이 아닌 유사 배열 객체(예: arguments)에 map을 사용할 수 있습니다.

Array.prototype.slice()를 예로 들어, 유사 배열 객체를 실제 배열로 변환하고 싶다면 다음과 같은 단축 코드를 만들 수 있습니다.

const slice = Array.prototype.slice;

// …

slice.call(arguments);

slice.call을 저장해 일반 함수로 호출할 수는 없습니다. 왜냐하면 call() 메서드도 자기 자신의 this 값을 읽는데, 그것이 호출해야 할 함수이기 때문입니다. 이 경우 bind()를 사용해 call()this 값을 바인딩할 수 있습니다. 아래 코드에서 slice()this 값이 Array.prototype.slice()에 바인딩된 Function.prototype.call()의 바운드 버전입니다.

// Same as "slice" in the previous example
const unboundSlice = Array.prototype.slice;
const slice = Function.prototype.call.bind(unboundSlice);

// …

slice(arguments);

더 알아보기