저번에는
this
에 대해서 정리를 했다. 이번에는this
를 원하는 대상에 바인딩하는 방법에 대해서 정리해보려고 한다.this
를 이해하지 못하면 앞날이 무척이나 피곤할 예정이니 어서 뿌셔보자.
상황에 따라서 this
는 다른 값을 바인딩하지만 규칙을 깨고 별도의 대상을 바인딩하도록 하는 방법이 있다.
Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])
call
메서드는 호출 주체인 함수를 즉시 실행하도록 하는 명령이다. 이 때 call
메서드의 첫 번째 인자를 this
로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 한다.
let func = function(a, b, c){
console.log(this, a, b, c);
};
func(1, 2, 3); // window{...} 1 2 3
func.call({x:1}, 4, 5, 6) // {x:1} 4 5 6
위의 경우는 함수로 호출하는 경우이다. call
을 사용하자 함수를 호출했음에도 불구하고 this
가 전역 객체가 아닌 첫 번째 인자로 제공한 객체를 바인딩했다.
let obj = {
a: 1,
method: function(x, y) {
console.log(this.a, x, y);
}
};
obj.method(2,3); // 1 2 3
obj.method.call({a: 4}, 5, 6); // 4 5 6
메서드로서 호출하는 경우도 비슷하다. 메서드를 그냥 호출하면 this
가 객체를 참조하지만, call
을 사용하면 임의의 객체를 지정할 수 있다.
apply
메서드는 call
메서드와 기능적으로 동일하다. 다만, apply
메서드의 경우 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 점에서 차이가 있다.
Function.prototype.apply(thisArg[, argsArray]);
원래 객체에는 배열 메서드를 직접 적용할 수 없지만, 키가 0 또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티의 값이 0 또는 양의 정수인 객체, 즉 유사배열객체의 경우에는 call
또는 apply
를 통해서 배열 메서드를 차용할 수 있다.
1. 유사배열객체에 배열 메서드를 적용
let obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
Array.prototype.push.call(obj, 'd');
console.log(obj); // {0: 'a', 1: 'b', 2: 'c', 3: 'd', length: 4}
let arr = Array.prototype.slice.call(obj);
console.log(arr); // ['a', 'b', 'c', 'd']
2. arguments
function a () {
let args = Array.prototype.slice.call(arguments);
args.forEach(function(arg){
console.log(arg);
});
}
a(1, 2, 3); // 1 2 3
3. NodeList
NodeList는 querySelectorAll
, getElementsByClassName
등의 Node 선택자로 선택한 결과로 유사배열이다.
document.body.innerHTML = '<div>a</div><div>b</div><div>c</div>';
let nodeList = document.querySelectorAll('div');
let nodeArr = Array.prototype.slice.call(nodeList);
nodeArr.forEach(function (node) {
console.log(node);
// <div>a</div>
// <div>b</div>
// <div>c</div>
});
4. 문자열
let str = 'cheol jin';
Array.prototype.push.call(str, ', pushed string');
// Error: Cannot assign to read only property 'length' of object [object String]
Array.prototype.concat.call(str, 'string'); // [String {"cheol jin"}, "string" ]
// 원본에 변경을 가하지 않는 경우
Array.prototype.every.call(str, function(char){return char !== ' ';}); // false
Array.prototype.some.call(str, function(char){return char === ' ';}); // true
let newArr = Array.prototype.map.call(str, function(char) { return char + '!';});
console.log(newArr); // ["c!", "h!", "e!", "o!", "l!", " !", "j!", "i!", "n!"]
let newStr = Array.prototype.reduce.apply(str, [ function(string, char, i){return string + char + i; }, '']);
console.log(newStr); // c0h1e2o3l4 5j6i7n8
문자열의 경우에 length
가 읽기 전용이기 때문에 원본에 변경을 가하는 메서드를 사용하는 경우(pop, push, shift, unshift, splice 등) 에러를 보낸다. concat의 경우에도 배열이 아니기 때문에 제대로 된 결과값을 얻을 수 없다.
생성자에 공통된 내용이 있을 경우 call
또는 apply
를 이용해 다른 생성자를 호출하면 간단하게 반복을 줄일 수 있다.
function Person(name, gender){
this.name = name;
this.gender = gender;
}
function Student(name, gender, school){
Person.call(this, name, gender);
this.school = school;
}
function Employee(name, gender, company){
Person.apply(this, [name, gender]);
this.company = company;
}
let jy = new Student("주영", "male", "전남대");
let cj = new Employee("철진", "male", "구글");
여러 개의 인수를 받는 메서드에게 하나의 배열로 인수들을 전달하고 싶을 때 apply 메서드를 이용하면 좋다. 최대 / 최솟값을 구해야 하는 경우를 보자.
직접 구현하는 코드
// 직접 구현하는 코드
let numbers = [10, 20, 3, 16, 45];
let max = min = numbers[0];
numbers.forEach(function(number){
if(number > max){
max = number;
}
if(number < min){
min = number;
}
});
console.log(max, min); // 45, 3
apply 활용
// apply 활용하는 코드
let numbers = [10, 20, 3, 16, 45];
let max = Math.max.apply(null, numbers);
let min = Math.min.apply(null, numbers);
ES6 펼리치 연산자 활용
const numbers = [10, 20, 3, 16, 45];
const max = Math.max(...numbers);
const min = Math.min(...numbers);
console.log(mx, min); // 45 3
Function.prototype.bind(thisArg[, arg1[, age2[, ...]]])
bind
메서드는 ES5에서 추가된 기능으로 call
과 비슷하지만 즉시 호출하지는 않고 넘겨 받은 this
및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드이다.
let func = function(name, age){
console.log(this, name, age);
};
func("cheoljin", 20); // window{...} "cheoljin" 20
// this를 지정
let bindFunc1 = func.bind("kind people");
bindFunc1("cheoljin", 20); // {"kind people"} "cheoljin" 20
// this를 지정하고 부분 적용 함수를 구현
let bindFunc2 = func.bind("kind people", "cheoljin");
bindFunc2("20"); // {"kind people"} "cheoljin" 20
bind
메서드를 사용해서 만든 함수는 독특한 성질이 있다. 바로 name 프로퍼티에 동사 bind의 수동태인 bound라는 접두어가 붙는다는 점이다. 이를 통해 call
이나 apply
에 비해 추적하기 좋아졌다.
console.log(func.name); // func
console.log(bindFunc1.name); // bound func
call
, apply
, bind
메서드를 사용하면 우회를 할 필요없이 깔끔하게 처리할 수 있다.
// call 메소드 사용
let obj = {
outer: function() {
console.log(this);
let innerFunc = function () {
console.log(this);
};
innerFunc.call(this);
}
}
// bind 메소드 사용
let obj = {
outer: function() {
console.log(this);
let innerFunc = function () {
console.log(this);
}.bind(this);
innerFunc();
}
}
ES6에 새롭게 도입된 화살표 함수는 this
를 바인딩하는 과정이 제외됐기 때문에 접근하려고 하면 스코프 체인 상 가장 가까운 this
에 접근하게 된다.
let obj = {
outer: function(){
console.log(this);
let innerFunc = () => {
console.log(this);
};
innerFunc();
}
};
obj.outer();
마치며! ✨
this는 무엇일지 예측하는 연습이 필요하다. 명시적으로 바인딩하지 않은 상황에서 어떤 값으로 사용될지 주의하며 사용하자.
this는 알면 알수록 어려운 것 같아요ㅠㅠ 정리가 깔끔해서 계속 읽어보고 이해해보겠습니다!!