책/코어 자바스크립트

3-2. this - 명시적으로 this를 바인딩하는 방법

LAZY SIA 2022. 1. 8. 23:12
728x90
반응형

1-2. 명시적으로 this를 바인딩하는 방법

- this에 별도의 대상을 바인딩하는 방법

call 메서드

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

- 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령

- call 메서드의 첫 번째 인자를 this로 바인딩하고 이후 인자들을 호출할 함수의 매개변수로 함

- 임의의 객체를 this로 지정할 수 있음

var 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
var 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




apply 메서드

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

- 기능적으로 call 메서드와 동일

- call 메서드는 첫 번째 인자를 제외한 나머지 모든 인자들을 호출할 함수의 매개변수로 지정하고,

   apply 메서드는 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정함

var func = function(a, b, c) {
    console.log(this, a, b, c);
};
func.apply({ x: 1 }, [4, 5, 6]);	// { x: 1 } 4 5 6

var obj = {
    a: 1,
    method: function(X, y) {
        console.log(this.a, x, y);
    }
};
obj.method.apply({ a: 4 }, [5, 6]);	// 4 5 6




call / apply 메서드의 활용

유사배열객체(array-like object)에 배열 메서드를 적용

 

1) 유사배열객체에 배열 메서드를 적용

- 객체에는 배열 메서드를 직접 적용할 수 없으므로, 키가 0 또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티의 값이 0 또는 양의 정수인 객체(유사배열객체)의 경우 call / apply 메서드를 이용해 배열 메서드를 차용할 수 있음

var obj = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3
};
Array.prototype.push.call(obj, 'd');	// 배열 메서드인 push를 적용
console.log(obj);	// { 0: 'a', 1: 'b', 2: 'c', 3: 'd', length: 4 }

var arr = Array.prototype.slice.call(obj);	// slice - 객체를 배열로 전환
console.log(arr);	// ['a', 'b', 'c', 'd']

- slice 메서드 : 시작 인덱스값과 마지막 인덱스값을 받아 시작값부터 마지막값의 앞 부분까지의 배열 요소를 추출하는 메서드

   ( 매개변수를 아무것도 넘기지 않으면 그냥 원본 배열의 얕은 복사본을 반환 )

-> call 메서드를 이용해 원본 유사배열객체의 얕은 복사를 수행한 것인데 slice 메서드가 배열 메서드라 복사본은 배열로 반환

 

 

2) arguments, NodeList에 배열 메서드를 적용

- argument 객체도 유사배열객체이므로 배열로 전환해서 활용 가능

- NodeList(querySelectorAll, getElementByClassName 등)도 마찬가지

function a () {
    var argv = Array.prototype.slice.call(arguments);
    argv.forEach(function (arg) {
        console.log(arg);
    });
}
a(1, 2, 3);

document.body.innerHTML = '<div>a</dov><div>b</div><div>c</div>';
var nodeList = document.querySelectAll('div');
var nodeArr = Array.prototype.slice.call(nodeList);
nodeArr.forEach(function (node) {
    console.log(node);
});

 


3) 문자열에 배열 메서드 적용 예시

- 배열처럼 인덱스와 length 프로퍼티를 지니는 문자열도 배열 메서드 적용 가능

- 단, 문자열의 경우 length 프로퍼티가 읽기 전용이기 때문에

   원본 문자열에 변경을 가하는 메서드는 에러를 던지며,

   concat처럼 대상이 반드시 배열이어야 하는 경우에는 에러는 나지 않지만 제대로 된 결과를 얻을 수 없음

var str = 'abc def';

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 {"abc def"}, "string"]

Array.prototype.every.call(str, function(char) { return char !== ' '; });	// false
Array.prototype.some.call(str, function(char) { return char === ' '; });	// true

var newArr = Array.prototype.map.call(str, function(char) { return char + '!'; });
console.log(newArr);	// ['a!', 'b!', 'c!', ' !', 'd!', 'e!', 'f!']

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

 

 

4) Array.from 메서드 (ES6)

- call / apply를 이용해 형변환하는 것은 this를 원하는 값으로 지정해서 호출한다는 본래의 메서드의 의도와는 다소 동떨어짐

- 코드만 봐서는 어떤 의도인지 파악하기 쉽지 않음

-> ES6에서는 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 Array.from 메서드를 새로 도입

var obj = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3
};
var arr = Array.from(obj);
console.log(arr);	// ['a', 'b', 'c']

 

 

생성자 내부에서 다른 생성자를 호출

- 생성자 내부에 다른 생성자와 공통된 내용이 있을 경우 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;
}
var sa = new Student('시아', 'female', '단국대');
var hj = new Employee('형준', 'male', '카카오');

 

 

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

예) 배열에서 최대/최솟값을 구하기

 

1) apply 활용하기

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

- 만약 apply를 사용하지 않는다면 코드가 불필요하게 길고 가독성도 떨어질 것임 (더보기)

더보기
더보기
var numbers = [10, 20, 3, 16, 45]
var max = min = numbers[0];
numbers.forEach(function(number) {
    if (number > max) {
        max = number;
    }
    if (number < min) {
        min = number;
    }
});
console.log(max, min);	// 45 3

 

 

2) ES6의 펼치기 연산자(spread operator) 활용

- apply를 적용하는 것보다 더욱 간편하게 작성 가능

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

 

 

call / apply 특징

- call / apply 메서드는 명시적으로 별도의 this를 바인딩하면서 함수/메서드를 실행하는 훌륭한 방법

- 오히려 이로 인해 this를 예측하기 어렵게 만들어 코드 해석 방해한다는 단점

- 그럼에도 불구하고 ES5 이하에선 마땅한 대안이 없어 매우 광범위하게 활용

 

 

 

bind 메서드

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

- 다시 새로운 함수를 호출할 때 인수를 넘기면 그 인수들은 기존 bind 메서드를 호출할 때 전달해던 인수들의 뒤에 이어서 등록

 

bind 메서드의 2가지 목적

- 함수에 this를 미리 적용하는 것

- 부분 적용 함수를 구현

 

 

this 지정과 부분 적용 함수 구현

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

var bindFunc1 = func.bind({ x: 1 });
bindFunc1(5, 6, 7, 8);	// {x: 1} 5 6 7 8

var bindFunc2 = func.binc({ x: 1 }, 4, 5);
bindFunc2(6, 7);	// { x: 1 } 4 5 6 7
bindFunc2(8, 9);	// { x: 1 } 4 5 8 9



name 프로퍼티

- bind 메서드를 적용해 새로 만든 함수가 가지는 특징

- name 프로퍼티에 동사 bind의 수동태인 'bound'라는 접두어가 붙음

- 기존의 call / apply 보다 코드 추적이 수월해짐

var func = function (a, b, c, d) {
    console.log(this, a, b, c, d);
};
var bindFunc = func.bind({ x: 1 }, 4, 5);
console.log(func.name);		// func
console.log(bindFunc.name)	// bound func

 

 

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

- self 등의 변수를 활용한 우회법보다 call, apply, bind 메서드를 이용하면 더 깔끔하게 처리 가능

 

1) 내부함수에 this 전달

// call 메서드 이용
var obj = {
    outer: function() {
        console.log(this);
        var innerFunc = function() {
            console.log(this);
        };
        innerFunc.call(this);
    }
};
obj.outer();
// bind 메서드 이용
var obj = {
    outer: function() {
        console.log(this);
        var innerFunc = function() {
            console.log(this);
        }.bind(this);
        innerFunc();
    }
};
obj.outer();

 

 

2) 콜백 함수 내에서의 this에 관여하는 함수/메서드

var obj = {
    logThis: function() {
        console.log(this);
    },
    logThisLater1: function() {
        setTimeout(this.logThis, 500);
    },
    logThisLater2: function() {
        setTimeout(this.logThis.bind(this), 1000);
    }
};
obj.logThisLater1();	// Window { ... }
obj.logThisLater2();	// obj { logThis: f, logThisLater1: f, logThisLater2: f }

 

 

 

화살표 함수의 예외사항

- 별도의 변수로 this를 우회하거나 call, apply, bind 메서드를 적용할 필요가 없음
- 화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외되어 함수 내부에 this가 아예 없으며,

   접근하고자 하면 스코프체인상 가장 가까운 this에 접근함

var obj = {
    outer: function() {
        console.log(this);
        var innerFunc = () => {
            console.log(this)
        };
        innerFunc();	// { outer: f }
    }
};
obj.outer();

 

 

 

별도의 인자로 this를 받는 경우(콜백 함수 내에서의 this)

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

-> 이러한 메서드의 thisArg 값을 지정하면 콜백 함수 내부에서 this 값을 원하는 대로 변경할 수 있음

- 보통 여러 내부 요소에 대해 같은 동작을 반복 수행하는 배열 메서드가 이런 형태 (ES6의 Set, Map 등에서도 일부 존재)

var report = {
    sum: 0,
    coun: 0,
    add: function() {
        var args = Array.prototype.slice.call(arguments);
        args.forEach(function(entry) {
            this.sum += entry;
            ++this.count;
        }, this);
    },
    average: function() {
        return this.sum / this.count;
    }
};
report.add(60, 85, 95);
console.log(report.sum, report.count, report.average());	// 240 3 80

-> 콜백 함수 내부에서의 this는 forEach 함수의 두 번째 인자로 전달해준 this가 바인딩됨

 

 

콜백 함수와 함께 thisArg를 인자로 받는 메서드 - 메서드명(callback[, thisArg])

- forEach, map, filter, some, every, find, findIndex, flatMap, from

- Set.prototype.forEach

- Map.prototype.forEach

 

반응형