JavaScript의 bind, apply, call

imnotmoon·2021년 9월 15일
4

this

JavaScript에서 this 의 값은 함수를 호출한 방법에 의해 결정됩니다. 그래서 함수를 호출할때마다 다를 수 있습니다.

this 는 context 객체라고도 불리는데, 실행된 문맥(context)에 따라 내부적으로 this 를 바꿔주기 때문입니다. (this 의 값은 런타임에 평가됩니다.)

const a = {
	prop: 42,
	func: function() { console.log(this.prop); }
};

console.log( a.func() ); // 42

여기서 func() 메소드의 this 는 객체 a 를 가리킵니다. 객체 a를 통해 실행된 메소드이기 때문입니다.

const func = function() { console.log(this.prop) };

const a = { func, prop: '객체 a입니다.' }
const b = { func, prop: '객체 b입니다.' }

a.func();  // 객체 a입니다.
b.func();  // 객체 b입니다.

객체 ab 는 모두 func 속성으로 func 함수를 받았습니다. 같은 함수이지만 어디서 실행되는지에 따라 this 는 다르게 평가됩니다.

반대로 아래와 같은 경우도 생각해볼 수 있습니다.

const b = {
	prop: 84,
	func: function() { console.log(this.prop); }
};

const c = b.func;
c();  // undefined

여기서는 새롭게 변수 c 를 만들어 객체 b 에 있던 메소드를 꺼내서 할당했습니다. 함수 c 를 호출할 때는 b.func() 같이 호출할 때와는 다르게 b 라는 context가 사라졌습니다. 그래서 this 는 문맥을 잃고 b 를 가리키지 못하게 된겁니다.

이 코드가 동작되게 하려면 메소드 func 의 정의부를 this.prop 에서 b.prop 으로 바꿔주셔야 합니다.

복잡한걸 싫어하시는 분들은 그냥 간단하게 변수.메소드() 관계에서 this 는 변수를 가리킨다고 외우시면 됩니다.

한가지 주의하실 점은, 화살표 함수에는 this 가 없습니다. 그래서 외부 렉시컬 환경에서 this 를 찾게 되고, 만약 외부에도 없고, 외부의 외부에도 없고.. 이런식으로 쭉 없다면 결국 windowglobalthis 가 될겁니다.

bind

let user = {
	firstName: 'John',
	sayHi() { alert(`Hello, ${this.firstName}`); }
};

setTimeout(user.sayHi, 1000);  // Hello, undefined

브라우저 환경에서 setTimeout 은 콜백함수를 호출할 때 thiswindow 를 할당합니다. Node.js 환경이라면 global 을 할당하겠죠.

user.sayHi() 가 Callback Queue로 넘어가는 과정에서 함수의 정의부만 Callback Queue로 넘어갑니다.

따라서 함수가 Call Stack으로 올라와 실행되려 할 때 이미 문맥정보를 잃어버리게 됩니다.

'Hello, John'이 출력될걸 기대했다면 이 코드가 제대로 작동하기 위해 조금의 과정을 거쳐야 합니다.

우선 렉시컬 환경을 이용하는 방법입니다.

let user = {
	firstName: 'John',
	sayHi() { alert(`Hello, ${this.firstName}`); }
};

setTimeout(function() { user.sayHi(); }, 1000);  // Hello, undefined

setTimeout() 에 주어지는 콜백함수 부분에서 user.sayHi() 를 함수로 한번 래핑해줬습니다.

그 결과 Callback Queue로 들어가고 Call Stack으로 나오게 되는 부분은 user.sayHi() 가 되고, user 는 전역 스코프에 존재하기 때문에 접근이 가능해집니다. 따라서 문맥도 생기고 this 를 포함한 코드가 제대로 작동하게 됩니다.

두번째 방법은 bind 를 사용하는 겁니다.

bind() 메소드가 호출되면 문맥이 묶인 새로운 함수가 하나 생성됩니다. bind() 메소드는 첫번째 인자로 문맥으로 사용할 객체를 받고, 그 뒤로 두번째 인자부터는 새롭게 생성된 함수의 인자에 제공됩니다.

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

const 바인드되지않은getX = module.getX;
const 바인드된getX = module.getX.bind(module);

console.log(바인드되지않은getX());   // undefined
console.log(바인드된getX());       // 42

여기서 바인드된getX바인드되지않은getX 와 마찬가지로 메소드를 외부로 꺼냈기 때문에 호출할 때 context를 잃어야 하지만, bind() 를 통해 함수 내부적으로 this 가 무엇인지를 정해줬습니다.

따라서 바인드된getX 를 호출할 때는 this 가 바인드한 module 로 간주되어 42가 제대로 찍힐 수 있게 됩니다.

const module = {
	x: 42,
	getX : function(a, b, c) { return this.x + a + b + c; };
}

const 인자까지바인드한getX = module.getX.bind(module, 1, 2, 3);
console.log(인자까지바인드한getX());    // 48

위에서 언급했듯 bind 는 인자까지 묶어버릴 수 있습니다.

bind 메소드의 인자로 1, 2, 3을 추가로 넘겨주었고, 이는 module.getX() 의 인자 a, b, c에 각각 매핑되어 고정됩니다.

결국 인자까지바인드한getX 를 호출한다는 것은 return module.x + 1 + 2 + 3; 으로 값이 고정된 함수를 호출하는 것과 같습니다.

call과 apply

bind() 가 context를 묶어 새로운 함수를 생성해준다면, call()apply() 는 함수에 문맥을 주입해서 실행을 시켜버립니다.

const _bind  =  func.bind(obj);
const _apply =  func.apply(obj);
const _call  =  func.call(obj);

_bindobj 가 context로 묶인 새로운 함수입니다.

_apply_callfuncobj 를 넣고 실행시킨 결과(평가된 값)입니다.

apply()call() 은 기본적으로 그 기능이 같지만, 차이점은 인자를 고정시키는 방식입니다.

const _call = func.apply(obj, 1, 2, 3, 4, 5);
const _apply = func.apply(obj, [1, 2, 3, 4, 5]);

위와 같이 call() 은 인자를 고정시킬 때 bind() 에서 그랬듯 쭉 나열시켜 고정하고하고, apply() 는 배열에 고정시킬 인자들을 담아 함수에 고정시킵니다.

참고

1개의 댓글

comment-user-thumbnail
2021년 9월 16일

좋아요! 구독!

답글 달기