Function.prototype.bind() 메서드
Function.prototype.bind() 메서드
bind()는 Function 인스턴스의 메서드로, 호출 시 지정된 this 값을 사용하고 주어진 인자들이 먼저 전달되는 새로운 함수를 생성합니다. 이렇게 생성된 "바운드 함수(bound function)"는 원본 함수의 this 고정과 부분 적용(partial application)을 한 번에 해결하는 핵심 도구입니다.
본문
개요
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 — 바운드 함수가 호출될 때 타깃 함수
func의this매개변수로 전달할 값입니다. 함수가 strict mode가 아니면null과undefined는 전역 객체로 치환되고, 원시 값은 객체로 변환됩니다. 바운드 함수가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에서 실행하면
retrieveX의this매개변수가globalThis대신undefined로 바인딩되어retrieveX()호출이 실패합니다. ECMAScript 모듈에서 실행하면 최상위this가undefined로 바인딩되어this.x = 9할당이 실패합니다. Node CommonJS 모듈에서 실행하면 최상위this는module.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 키워드는 브라우저에서는 window인 globalThis로 설정됩니다. 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);