코딩쌀롱

[코어 자바스크립트] 3-2. 명시적으로 this를 바인딩하는 방법 본문

[코어 자바스크립트] 3-2. 명시적으로 this를 바인딩하는 방법

이브✱ 2020. 12. 22. 04:06

call 메서드

Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])

call 메서드는 호출 주체인 함수를 즉시 호출하고, this 바인딩한다.

첫 번째 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 한다.

 

apply 메서드

Function.prototype.apply(thisArg[, argsArray])

apply 메서드는 call 메서드와 기능적으로 완전히 동일하다. 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다. call과의 차이는 호출 함수의 매개변수를 전달하는 방식에서만 있다.

 

call / apply 메서드의 활용

call, apply 메서드는 명시적으로 별도의 this를 바인딩하면서 함수 또는 메서드를 실행하는 훌륭한 방법이지만, 다른 의도로 활용하면서 this를 예측하기 어렵게 만들어 코드 해석을 방해한다는 단점이 있다. ES5 이하의 환경에서는 대안이 없기 때문에 광범위하게 활용된다.

 

<유사배열객체에 배열메서드 적용>

 객체에는 배열 메서드를 직접 적용할 수 없다. 그러나 유사배열객체의 경우 call, apply 메서드를 이용해 배열 메서드를 차용할 수 있다. (본래의 메서드 의도와는 동떨어진 활용법이기 때문에 코드만 봐서는 의도를 파악하기 어려울 수 있다. 이에 ES6에서는 유사배열객체, iterable을 배열로 전환하는 Array.from 메서드를 새로 도입했다.)

* 유사배열객체 : 키가 0또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티 값이 0 또는 양의 정수인 객체(arguments, nodeList..)

let obj = {
   0: 'a',
   1: 'b',
   length: 2
};
Array.prototype.push.call(obj, 'd');
console.log(obj); // (1) {0: 'a', 1: 'b', 2: 'c', length: 3}

let arr = Array.prototype.slice.call(obj);
console.log(arr); // (2) ['a', 'b', 'c']

(1) 배열 메서드 push를 객체 obj에 적용해 프로퍼티2에 'c'를 추가했다.

(2) 배열 메서드 slice를 적용해 객체를 배열로 전환했다.

slice 메서드는 매개변수를 아무것도 넘기지 않을 경우에 원본 배열의 얕은 복사본을 반환한다. 그러니까 call 메서드를 이용해 원본인 유사배열객체의 얕은 복사를 수행한 것. slice 메서드가 배열 메서드이기 때문에 복사본은 배열로 반환된 것이다.

 

 배열처럼 인덱스와 length 프로퍼티를 지니는 문자열도 가능한데, 원본 변경 메서드는 에러를 던지고, 대상이 반드시 배열이어야 하는 경우(concat)는 제대로된 결과를 얻을 수 없다.

let str = 'abc def';
Array.prototype.push.call(str, ' ghi');  // Error
Array.prototype.concat.call(str, 'string'); // [String {'abc def'}, "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); // ['a!', 'b!', 'c!', ' !', 'd!', 'e!', 'f!']

let newStr = Array.prototype.reduce.apply(str, [
   function(string, char, i) { return string + char + i; }, ''
]);
console.log(newStr); // "a0b1c2 3d4e5f6"

 

<생성자 내부에서 다른 생성자 호출>

생성자 내부에 다른 생성자와 공통된 내용이 있을 경우 call, apply를 이용해 다른 생성자를 호출하면 반복을 줄일 수 있다.

function Person(name, gender) {
   this.name = name;
   this.gender = gender;
}
function Student(name, gender, school) {
   Person.call(this, name, gender);          // call
   this.school = school; 
}
function Employee(name, gender, company) {
   Person.apply(this, [name, gender]);       // apply
   this.company = company;
}
let by = new Student('보영', 'female', '단국대');
let jn = new Employee('재남', 'male', '구글');

 

<여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 - apply 활용>

여러 개의 인수를 받는 메서드에게 하나의 배열로 인수들을 전달하고 싶을 때 apply 메서드를 사용할 수 있다. 예를 들어, 배열의 최솟값, 최대값을 구하는 코드를 봐보자.

let numbers = [10, 20, 3, 16, 45];
let max = Math.max.apply(null, numbers);
let min = Math.min.apply(null, numbers);
console.log(max, min);  // 45 3

ES6의 spread 연산자를 이용하면 더 간단해진다.

const numbers = [10, 20, 3, 16, 45];
const max = Math.max(...numbers);
const min = Math.min(...numbers);
console.log(max, min);  // 45 3

 

bind 메서드

Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])

bind 메서드는 call과 비슷하지만 즉시 호출하지 않고 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 한다. 

다시 새로운 함수를 호출할 때 인수를 넘기면 그 인수들은 기존 bind 메서드를 호출할 때 전달했던 인수들의 뒤에 이어서 등록된다. ①this 바인딩, ②부분 적용 함수 두 가지 목적을 모두 지닌다.

 

let func = function (a, b, c, d) {
   console.log(this, a, b, c, d);
};
func(1, 2, 3, 4); // Window{...} 1 2 3 4 

let bindFunc1 = func.bind({x: 1}); // ①this 지정
bindFunc1(5, 6, 7, 8);  // ②부분 적용 함수
                        // {x: 1} 5, 6, 7, 8
                        
let bindFunc2 = func.bind({x: 1}, 4, 5); // ①this 지정, 고정 인자 전달
bindFunc2(6, 7); // ②부분 적용 함수(추가 인자 전달)
bindFunc2(8, 9); // ②부분 적용 함수(추가 인자 전달)

 

<name 프로퍼티>

bind 메서드를 적용해 새로 만든 함수는 독특한 성질이 있다. 함수의 name 프로퍼티에 'bound' 접두어가 붙는다는 것.

기존의 call, apply보다 코드를 추적하기에 더 수월하다.

let func = function (a, b, c, d) {
   console.log(this, a, b, c, d);
};

let bindFunc = func.bind({x: 1}, 4, 5);
console.log(func.name);      // func
console.log(bindFunc.name);  // bound func

 

<상위 컨텍스트 this를 내부함수나 콜백함수에 전달하기>

메서드의 내부 함수에서 메서드의 this를 그대로 바라보게 하기 위해 self 변수를 활용했었다. call, apply, bind를 이용하면 훨씬 깔끔하게 처리할 수 있다. 또한 콜백 함수를 인자로 받는 함수나 메서드 중 기본적으로 콜백 함수 내의 this에 관여하는 함수 또는 메서드에 대해 bind 메서드를 이용하면 this를 사용자가 원하는대로 바꿀 수 있다. 

// (1) 루빅스큐브 문제 중 view 클래스에 있는 함수
setEvent() {
   this.step3Btn.addEventListener("click", this.handleData.bind(this));
}

// (2) 클래스 내 메서드라 가정
logThisLater() {
   setTimeout(this.logThis.bind(this), 1000);
}

(1) bind를 안 해주면 handleData 함수 내의 this가 step3Btn 객체를 가리킨다. bind()를 사용해서 this가 인스턴스가 되도록 했다.

(2) bind를 안 해주면 this는 setTimeout 호출 주체인 Window가 된다. bind()를 사용해서 this가 인스턴스가 되도록 했다.

 

화살표 함수의 예외사항

실행 컨텍스트 생성 시 this를 바인딩하는 과정이 없다! 즉 이 함수 내부에는 this가 아예 없으며, 접근하고자 하면 스코프 체인상 가장 가까운 this에 접근한다.

 

별도의 인자로 this를 받는 경우(콜백)

콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체(thisArg)를 인자로 지정할 수 있는 경우가 있다. 

Array.prototype.forEach(callback[, thisArg])

Array.prototype.map(callback[, thisArg])

Array.prototype.filter(callback[, thisArg])

Array.prototype.some(callback[, thisArg])

Array.prototype.every(callback[, thisArg])

Array.prototype.find(callback[, thisArg])

Array.prototype.findIndex(callback[, thisArg])

Array.prototype.flatMap(callback[, thisArg])

Array.prototype.from(arrayLike[, callback[, thisArg]])

Set.prototype.forEach(callback[, thisArg])

Map.prototype.forEach(callback[, thisArg])

 

 

 


정재남 작가님의 〔 코어 자바스크립트 〕 책을 공부하며 정리했습니다.

Comments