본문 바로가기

모던 자바스크립트

6.10 함수 바인딩

📢 객체 메서드를 콜백으로 전달할 때 this 정보가 사라지는 문제가 생긴다. 

 

사라진 'this'

◾️ 객체 메서드가 객체 내부가 아닌 다른 곳에 전달되어 호출되면 this가 사라진다. 

let user = {
  firstName: 'suzu',
  sayHi() {
    console.log(`Hello! ${this.firstName}!`);
  }
};

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

◾️ setTimeout에 객체에서 분리된 함수인 user.sayHi가 전달되기 때문에 undefined가 출력된다. 

let f = user.sayHi;
setTimeout(f, 1000); // user context를 잃어버림

◾️ 브라우저 환경에서 setTimeout메서드는 인수로 전달받은 함수를 호출할 때 this에 window를 할당한다. 

◾️ this.firstName은 window.firstName가 되는데, window객체엔 firstName이 없으므로 undefined가 출력된다. 

 

 

💡 메서드를 전달할 때, 컨텍스트도 제대로 유지하기 위한 방법 

방법 1: 래퍼

◾️ 래퍼함수를 사용한다. 

◾️ 외부 렉시컬 환경에서 user를 받아서 보통 때처럼 메서드를 호출했기 때문에 에러가 발생하지 않는다. 

let user = {
  firstName: 'suzu',
  sayHi() {
    console.log(`Hello! ${this.firstName}!`);
  }
};

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

 

◾️ 화살표 함수로 변경

setTimeout(() => user.sayHi(), 1000); // Hello, suzu!

◾️ setTimeout이 트리거 되기 전에(1초가 되기 전에) user가 변경되면, 변경된 객체의 메서드를 호출하게 된다. 

let user = {
  firstName: 'suzu',
  sayHi() {
    console.log(`Hello! ${this.firstName}!`);
  }
};

setTimeout(() => user.sayHi(), 1000); 

// 1초가 지나기 전에 user의 값이 바뀜
user = { sayHi() {console.log('other!')} };

// other!이 출력된다. 

 

방법 2: bind

◾️ 모든 함수는 this를 수정하게 해주는 내장 메서드 bind를 제공한다. 

let boundFunc = func.bind(context);

◾️ func.bind(context)는 함수처럼 호출 가능한 특수 객체(exotic object)를 반환한다. 

◾️ 이 객체를 호출하면 this가 context로 고정된 함수 func가 반환된다. 

◾️ 따라서 boundFunc를 호출하면 this가 고정된 func를 호출하는 것과 동일한 효과를 본다. 

let user = {
  firstName: 'suzu',
};

function func() {
  console.log(this.firstName);
}

let funcUser = func.bind(user);
funcUser(); // suzu

◾️ 인수는 원본 함수 func에 그대로 전달된다. 

let user = {
  firstName: 'suzu',
};

function func(phrase) {
  console.log(phrase + ', ' + this.firstName);
}

// this를 user로 바인딩한다. 
let funcUser = func.bind(user);

funcUser('Hello'); // Hello, suzu (인수 Hello가 넘겨지고, this는 user로 고정된다)

◾️ 객체 메서드에 bind 적용

let user = {
  firstName: "suzu",
  sayHi() {
    console.log(`Hello, ${this.firstName}!`);
  }
};

let sayHi = user.sayHi.bind(user); // (*)

sayHi(); // Hello, suzu

setTimeout(sayHi, 1000); // Hello, suzu

// 1초 이내에 user 값이 변화해도 sayHi는 기존 값을 사용한다. 
user = {
  sayHi() { console.log('other'); }
};

◾️ (*)로 표시한 줄에서 메서드 user.sayHi를 가져오고, 메서드에 user를 바인딩한다. 

◾️ sayHi는 묶인(bound)함수가 되어 단독으로 호출할 수 있고, setTimeout에 전달하여 호출할 수도 있다. 

◾️ 어떤 방식이든 context는 원하는 대로 고정된다. 

let user = {
  firstName: "John",
  say(phrase) {
    console.log(`${phrase}, ${this.firstName}!`);
  }
};

let say = user.say.bind(user);

say("Hello"); // Hello, suzu (인수 "Hello"가 say로 전달됨)
say("Bye"); // Bye, suzu ("Bye"가 say로 전달됨)

◾️ 인수는 그대로 전달되고 bind에 의해 this만 고정된다. 

 

ℹ️ bindAll로 메서드 전체 바인딩하기 

◾️ 객체에 복수의 메서드가 있고 이 메서드 전체를 전달하려 할 땐, 반복문을 사용해 메서드를 바인딩 할 수 있다. 

◾️ bindAll 라이브러리

for(let key in user) {
  if(typeof user[key] === 'function'){
    user[key] = user[key].bind(user);
  }
}

 

 

부분 적용

◾️ 인수도 바인딩이 가능하다. 

◾️ bind의 전체 문법

let bound = func.bind(context, [arg1], [arg2], ...);

◾️ bind는 context를 this로 고정하는 것 뿐만 아니라 함수의 인수도 고정해준다. 

function mul(a, b){
  return a * b;
}

◾️ bind를 사용해 새로운 함수 double을 만든다. 

function mul(a, b){
  return a * b;
}

let double = mul.bind(null, 2);

alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10

◾️ mul.bind(null, 2)를 호출하면 새로운 함수 double이 만들어진다.  double엔 컨텍스트가 null, 첫 번째 인수는 2인 mul의 호출 결과가 전달된다. 추가 인수는 그대로 전달된다.  

◾️ 부분 적용(partial application) : 부분 적용을 사용하면 기존 함수의 매개변수를 고정하여 새로운 함수를 만들 수 있다. 

◾️ 부분 함수 장점

    ◾️ 가독성이 좋은 이름을 가진 독립 함수를 만들 수 있다. 

    ◾️ bind를 사용해 첫 번째 인수를 고정할 수 있기 때문에 매번 인수를 전달할 필요도 없다. 

    ◾️ 매우 포괄적인 함수를 기반으로 덜 포괄적인 변형 함수를 만들 수 있다. 

    ◾️ 함수 send(from, to, text)가 있을 때, 객체 user 안에서 부분 적용을 활용하면, 전송 주체가 현재 사용자인 함수 sendTo(to, text)를 구현할 수 있다. 

 

 

컨텍스트 없는 부분 적용

◾️ 인수 일부는 고정하고 context this는 고정하고 싶지 않을 때 인수만 바인딩해주는 헬퍼 함수 partial 구현

function partial(func, ...argsBound) {
  return function(...args) { // (*)
    return func.call(this, ...argsBound, ...args);
  }
}

// 사용법:
let user = {
  firstName: 'suzu',
  say(time, phrase) {
    alert(`[${time}] ${this.firstName}: ${phrase}!`);
  }
};

// 시간을 고정한 부분 메서드를 추가함
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());

user.sayNow("Hello"); // [22:17] suzu: Hello!

◾️ partial(func[, arg1, arg2...])을 호출하면 래퍼((*))가 반환된다. 

◾️ 래퍼를 호출했을 때 func의 동작 과정

  • 동일한 this를 받는다. (user.sayNow는 user를 대상으로 호출된다.)
  • partial을 호출할 때 받은 인수 ('22:17')는 ...argsBound에 전달된다. 
  • 래퍼에 전달된 인수 ("Hello")는 ...args가 된다. 

 

📝 요약

◽️ func.bind(context, ...args)는 this가 context로 고정되고 인수도 고정된 함수 func을 반환한다. 
◽️ bind는 보통 객체 메서드의 this를 고정해 어딘가에 넘기고자 할 때 사용한다. (ex. setTimeout에 넘길 때)
◽️ 기존 함수의 인수 몇개를 고정한 함수를 부분 적용(partially applied)함수 또는 부분(partial) 함수라고 한다. 
◽️ 부분 적용은 같은 인수를 여러 번 반복하고 싶지 않을 때 유용하다. 

 

 



📝 오늘 새롭게 배운 내용 📝

 

✅ 객체 메서드가 객체 내부가 아닌 다른 곳에 전달되어 호출되면 this가 사라진다...

✅ bind함수는 객체 메서드의 this를 고정해 어딘가에 넘길때 사용한다. 

✅ 그래도 bind는 react에서 사용해 본적이 있다. 이벤트 연결할때...

그래서 this를 bind하는 대신에 this를 자동으로 bind 해주는 화살표 함수를 사용한다. 

부분 적용은 인수 몇개를 고정할 때 사용하면 유용하다고 한다... 

 

 

 

📌 [JAVASCRIPT_INFO 함수 바인딩]

 

함수 바인딩

 

ko.javascript.info

 

📌 [MDN bind()]

 

Function.prototype.bind()

bind() 메소드가 호출되면 새로운 함수를 생성합니다. 받게되는 첫 인자의 value로는 this 키워드를 설정하고, 이어지는 인자들은 바인드된 함수의 인수에 제공됩니다.

developer.mozilla.org