웹 컴포넌트 공부를 하다가 this
를 바인딩하여 메소드를 사용하고 호출하는 경우가 많았다.
그런김에 this
바인딩과 관련된 API
인 call , apply , bind
메소드에 대해 공부해봤다.
call
과 apply
const greet = function () {
console.log(`hi~ i am ${this.firstName}`);
};
다음처럼 this
의 firstName
을 이용하여 인사를 건내는 함수가 있다고 해보자
이 때 사용되는 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
메소드가 바인딩 한 this
는 tom , 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
메소드는 기본적으로 호출시킨 객체인 jerry
를 this
로 참조해야 하나 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()
에서 this
는 DiceGame
인스턴스를 가리키는게 아니라 전역 객체인 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
에 바인딩 시켜버리는것이 훨씬 낫긴 하다.