Javscript API - call, apply, bind

ChoiYongHyeun·2024년 1월 28일
0

프로그래밍 공부

목록 보기
4/15

웹 컴포넌트 공부를 하다가 this 를 바인딩하여 메소드를 사용하고 호출하는 경우가 많았다.

그런김에 this 바인딩과 관련된 APIcall , apply , bind 메소드에 대해 공부해봤다.


callapply

어디에 쓸까 ?

const greet = function () {
  console.log(`hi~ i am ${this.firstName}`);
};

다음처럼 thisfirstName 을 이용하여 인사를 건내는 함수가 있다고 해보자

이 때 사용되는 this 는 동적으로 바인딩 되는 객체로서 호출 시점에 결정된다.

이런 경우를 봐보자

const greet = function () {
  console.log(`hi~ i am ${this.firstName}`);
};

const tom = {
  firstName: 'tom',
  greet,
};
const jerry = {
  firstName: 'jerry',
  greet,
};
tom.greet(); // hi~ i am tom
jerry.greet(); // hi~ i am jerry

tom , jerry 객체 내에서 호출된 greet 메소드가 바인딩 한 thistom , jerry 객체 인스턴스 자체이다.

const dongdong = {
  greet,
};

dongdong.greet(); // hi~ i am undefined

firstName 이란 프로퍼티가 없는 경우엔 this.firstName 을 찾지 못해 undefined가 나온다.

이처럼 this 를 사용하는 함수들의 경우 어디에서 호출되느냐에 따라서 결과값이 다르게 나온다.

이번에는 함수자체가 참조만 하는것이 아니라 값을 변경하는 것이라고 해보자

const addProperty = function (property, value) {
  this[property] = value;
};

const tom = {
  firstName: 'tom',
  addProperty,
};

tom.addProperty('lastName', 'dongdong');
console.log(Object.getOwnPropertyNames(tom));
// [ 'firstName', 'addProperty', 'lastName' ]

이렇게 하면 새로운 값이 추가가 되는 모습을 볼 수 있다.

우리는 this 의 값을 참조하거나 변경하기 위해서 함수들을 객체 내부에 넣어 메소드로 사용했어야 했다.

메소드로 넣어 사용해야지만 함수에서 호출되길 기대하는 this 에 객체를 바인딩 시킬 수 있었기 때문이다.

하지만 ~! call , apply 를 사용하면 그럴 필요가 없다.

const addProperty = function (property, value) {
  this[property] = value;
};

const tom = {
  firstName: 'tom',
};

addProperty.call(tom, 'lastName', 'dongdong');
console.log(tom); // { firstName: 'tom', lastName: 'dongdong' }

짜잔 ~

살펴보자

call 의 사용 명세서

thisArg 에 전달된 객체는 함수에서 참조하는 객체로 사용된다.
객체의 값을 참조하거나 교체하기 위해 사용하세용

apply의 사용 명세서

call 과 동일하며 차이점은 인수들을 배열로 받는다는 것 외에는 없다.

addProperty.apply(tom, ['lastName', 'dongdong']);
/* call 과 동일한 결과값 */
console.log(tom); // { firstName: 'tom', lastName: 'dongdong' }

argArray 같은 경우는 call , apply 를 호출하는 함수에서 사용하는 인수들을 넣어주면 된다.

그렇게 되면 위에서 예시로 들었던

const greet = function () {
  /* greet.call(tom)로 호출될 경우 this 에 tom 객체가 바인딩 */
  console.log(`hi~ i am ${this.firstName}`);
};

const tom = { firstName: 'tom' };
greet.call(tom); // hi~ i am tom
greet.call(jerry); // hi~ i am jerry

해당 함수에서도 call 을 이용해 호출할 경우 바인딩 되어 원활하게 사용 할 수 있다.

활용 예시

좀 더 도움이 되는 활용 예시를 살펴보자

this 바인딩을 통해 객체 생성

function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price, category) {
  /* 생성자 함수를 Food 의 this와 바인딩 하여 호출*/
  Product.call(this, name, price);
  this.category = category;
}

console.log(new Food('pepsi', 1500, 'juice'));
/* Food { name: 'pepsi', price: 1500, category: 'juice' } */

다음처럼 복잡한 상속관계 등을 구현해주지 않더라도 call 을 통해 바인딩 하여 호출시켜줄 수 있다.

😶‍🌫️ 그냥 class 사용하자

위에서 사용한 call 방법은 생성자 함수를 호출했을 뿐 실제로 상속이 이뤄진 것은 아니다.

function Product(name, price) {
  this.name = name;
  this.price = price;

  this.introduce = function () {
    `please buy me , i'm just ${this.price}won`;
  };
}

function Food(name, price, category) {
  Product.call(this, name, price);
  this.category = category;
}

const pepsi = new Food('pepsi', 1500, 'juice');
pepsi.introduce(); /* 메소드가 호출되지 않음 */

그러니 상속관계를 표현 할 수 있도록 class 를 사용해주도록 하자

class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }

  introduce() {
    console.log(`please buy me , i'm just ${this.price}won`);
  }
}

class Food extends Product {
  constructor(name, price, category) {
    super(name, price);
    this.category = category;
  }
}

const pepsi = new Food('pepsi', 1500, 'juice');
pepsi.introduce(); /* please buy me , i'm just 1500won */

함수 인수에 여러 인수를 넣어주고 싶을 때

const arr = [1, 2, 3, 4, 5];
const targets = [6, 7, 8, 9, 10];

다음처럼 생겼을 때 arr 값에 targets 내부에 있는 원소들을 모두 추가시켜주고 싶다고 해보자

arr.push(targets) 를 사용하면 [ 1, 2, 3, 4, 5, [ 6, 7, 8, 9, 10 ] ] 다음처럼 배열 자체가 추가된다.

그럼 어떻게 해야 할까 ? 반복문을 사용해야 할까 ?

const arr = [1, 2, 3, 4, 5];
const targets = [6, 7, 8, 9, 10];

arr.push.apply(arr, targets);
console.log(arr); /* [1,2,3, ... 8,9,10 ]*/

다음처럼 apply 를 사용해서 this 값을 바인딩 시켜주고 추가해줄 수 있다.

😶‍🌫️ 그냥 스프레드 문법 사용하자

사용 명세서를 보면 원소들을 받아 사용 할 수 있으니 사실 스프레드 문법을 사용하면 더 간단하다.

const arr = [1, 2, 3, 4, 5];
const targets = [6, 7, 8, 9, 10];
arr.push(...targets);

class , spread 등 다양한 기능이 추가된 요즘에는 잘 사용되지 않는 방법들이긴 하지만 그래도 개념은 알아두면 좋을 것 같다.


bind

call , apply 에서는 함수를 호출 할 때 this 를 바인딩 시킨다고 하였다.

bind 메소드는 함수를 호출 할 때 바인딩 시키는 것이 아니라 this 를 바인딩시킨 함수를 새롭게 생성해낸다.

function.bind(thisArg , ...argArray[]) 를 호출하면 function 내에서 참조할 object (this) 를 너가 전달한 thisArgs 값으로 특정짓겠다.
매개변수들은 ...argArray[] 형태로 전달해줘 ~!!

예시를 통해 보는것이 가장 빠르다.

const tom = {
  firstName: 'tom',
};

function introduce() {
  console.log(`hi i am ${this.firstName}`);
}

const jerry = {
  firstName: 'jerry',
  introduce: introduce.bind(tom), /* this 가 tom 객체를 가리키도록 바인딩 */
};

jerry.introduce(); /* hi i am tom */

jerry 내부에서 호출된 introduce 메소드는 기본적으로 호출시킨 객체인 jerrythis 로 참조해야 하나 bind 값으로 명시적으로 tom 을 바인딩 시켰기 때문에 결과는 hi i am tom 이 나온다.

바인딩한 함수는 다음과 같은 내부 속성들을 가지고 있다.

  <script>
    const tom = { firstName: 'tom' };
    function introduce() {
      console.log(`hi i am ${this.firstName}`);
    }
    const bindedIntroduce = introduce.bind(tom);
    console.dir(bindedIntroduce);
  </script>

  • [[TargetFunction]] : 바인딩 할 함수
  • [[BoundThis]] : this 값에 바인딩 시킬 객체
  • [[BoundArgs]] : 함수에 전달할 매개변수들

바인딩한 함수가 호출 될 때는 function.prototype.call 메소드가 호출되며 함수가 실행되며

내부에서 참조하는 this 의 객체는 [[BoundThis]] 에 존재하는 객체와 바인딩 된다.

이러한 바인딩 관계는 [[Scopes]] 내부 슬롯을 봐도 알 수 있다. 참조하는 스코프 체인이 tom 객체를 가리키고 있다.

활용 예시

다양한 활용 예시가 있겠지만 MDN 에서 제시하는 예시를 조금만 수정해서 써보자

function DiceGame() {
  this.setup = function () {
    this.dice = Math.floor(Math.random(0.6) * 10);
  };

  this.rollDice = function () {
    setTimeout(function () {
      this.setup();
      console.log(this.dice);
    }, 1000);
  }; /* TypeError: this.setup is not a function */
}

const myDice = new DiceGame();
myDice.rollDice();

내가 만약 주사위 게임을 하고싶다고 해보자

그래서 주사위의 값을 설정하고 1초뒤에 주사위 값이 나오는 것을 맞추는 것이다.

코드를 보면 rollDice 를 호출하면 setTimeout 이 되면서 1초뒤에 주사위가 설정되고 설정된 주사위 값이 로그될 것만 같다.

하지만 실행해보면 이런 오류가 뜬다.

그 이유는 setTImeout 내부에서 호출된 this.setup() 에서 thisDiceGame 인스턴스를 가리키는게 아니라 전역 객체인 This 를 가리키고 있다.

그 이유는 setTimeout 과 같은 비동기 함수는 호출자가 인스턴스가 아닌 이벤트 루프이기 때문에 this 는 전역 객체를 가리키게 되기 때문이다.

this 는 호출자인 인스턴스를 가리키게 된다.

이를 해결하기 위해서는 setTImeout 내부에 있는 콜백 함수에 this 를 미리 바인딩 시켜주어야 한다.

function DiceGame() {
  this.setup = function () {
    this.dice = Math.floor(Math.random(0, 0.6) * 10);
  };

  this.rollDice = function () {
    setTimeout(
      function () {
        this.setup();
        console.log(this.dice);
      }.bind(this), /* 내부에 사용되는 콜백함수의 this 는 myDice 를 가리키도록 바인딩*/
      1000,
    );
  };
}

const myDice = new DiceGame();
myDice.rollDice();

사실 이 방법은 ES6 이후에 나온 화살표 함수를 이용하여 this 자체를 미리 바인딩 시킬 필요 없이, 스코프 체인을 통해 DiceGame 에 바인딩 시켜버리는것이 훨씬 낫긴 하다.

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글