개발공부/자바스크립트

[JavaScript] 스터디 4일차_ this (2) call, apply, bind

햄❤️ 2021. 6. 20. 22:48
반응형

1. call 메서드

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

 - call 의 첫번째 인자를 this로 바인딩

 - 그냥 실행하면 this === 전역객체 but  call 메서드를 사용하면 임의의 객체를 this로 지정 가능

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

 

//원래는 함수호출로 window {...} 출력이나 첫번째 인자로 this에 {x:1} 전달

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 출력

 

객체의 메서드 를 call 메서드로 호출할 때, 

var obj = {
	a:1,
    method: function (x,y) {
    	console.log(this.a , x, y);
    }

};

// 메서드로서의 호출이므로 obj가 this가 된다. obj.a === 1
obj.method(2,3); // 1 2 3 출력
obj.method.call( {a:4}, 5, 6); // 4 5 6 출력

 

2. apply 메서드

call 메서드와 기능적으로 완전히 동일

  - call: 첫번째 인자를 제외한 나머지 인자들을 호출할 함수의 매개변수로 지정

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

Function.prototype.apply(thisArg[, argsArray])
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로 쓴다면, 

var func2 = function(a,b,c){
    console.log(this,a,b,c);
}
func2.call({x:1},4,5,6);

// {x: 1} 4 5 6

var obj2 = {
    a:1,
    method: function(x,y){
        console.log(this.a, x,y);
    }
};
obj2.method.call({a:4}, 5,6);

// 4 5 6 출력

 

3. call/apply 메서드의 활용

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

 - 유사배열(객체)이란?  배열의 형태를 가지고 있지만 배열이 아닌것

배열처럼 생겼으나 type은 객체

 

 - 유사배열(객체)의 조건?  

    1.반드시 length가 필요해야한다. 이 조건은 필수, 없으면 유사배열이라고 인식하지 않는다.
     ( = length 프로퍼티 값이 0 또는 양의 정수) 

    2. index번호가 0번부터 시작해서 1씩 증가해야한다. ( = 키 이름이 0 이상의 양의 정수인 프로퍼티 존재)

    3. 실제 배열의 네이티브 메소드( push, forEach ...)는 존재하지 않는다.

- 유사배열의 예시: string, arguments, HTML Collection, NodeList 등

 - 유사배열객체는 왜 쓰나요?  

객체에 배열 메서드를 사용하고 싶어서(어떤 함수에서 실행 결과로 배열값을 돌려주고 싶을때, 원래의 배열 객체가 가지고 있는 기능(함수)를 제공하고 싶지 않거나 원래의 배열 객체에 없는 기능을 제공하고 싶을때)

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


// slice메서드의 경우 매개변수가 없을때 원본 배열의 얕은 복사본을 반환
// slice 메서드가 배열 메서드이므로 복사본이 배열로 반환
var arr = Array.prototype.slice.call(obj);
console.log(arr);
// ["a","b","c","d"] 출력

 

✔🙄질문: 그렇다면 arr의 타입은? 

정답  object

 3-1-2) arguments, NodeList에 배열 메서드 적용

  - nodeList는 array가 아니다. 순회 가능한 유사배열이다. 

  - slice에 매개변수를 아무것도 넘기지 않았으므로 유사배열을 얄은복사하여 배열로 내뱉는다 = nodeArr

  - forEach로 배열을 순회하며 div를 하나씩 뱉어낸다. 

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

a(1,2,3);
//출력
//1
//2
//3 

document.body.innerHTML = "<div>a</div><div>b</div><div>c</div>";
var nodeList = document.querySelectorAll("div"); //객체
var nodeArr = Array.prototype.slice.call(nodeList); 
// var nodeArr = [div, div, div]; 배열 메서드 사용 가능 (배열로 전환 가능)
nodeArr.forEach(function (node) {
	console.log(node);
    //아래와 같이 출력
    //<div>a</div>
    //<div>b</div>
    //<div>c</div>
});

 

3-1-3) 문자열에 배열 메서드 적용

🙄🥔🥬🧅❗❗❓

 - 원본 문자열에 변경을 가하는 메서드(push, pop, shift, unshift, splice 등)은 에러 발생

var str = "abc def";

Array.prototype.push.call(str, ", pushed string");
//Uncaught TypeError: Cannot assign to read only property 'length' of object '[object String]'

Array.prototype.concat.call(str, "string");
//[String {"abc def}, "string"]

//every는 배열안의 모든 요소가 주어진 판별함수를 통과하는지 테스트한다. 불리언 반환
Array.prototype.every.call(str, function(char) {return char !== " ";});
//false
// 인덱스 3번은 " " 빈값이기 때문에 false임

//some은 배열안의 어떤 요소라도 주어진 판별 함수를 통과하는지 테스트한다. 빈배열은 무조건 false
Array.prototype.some.call(str, function(char) {return char === " ";});
//true
// 인덱스 3번은 " "; 빈값이기 때문에, 하나라도 true 이므로 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 출력

 

3-1-4) ES6의 Array.from 메서드

 - ES6에서 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 복사해서 새로운 배열 객체를 생성

 - Array.from(배열로 전환할 객체) 

var obj = {
	0: "a";
    1: "b";
    2: "c";
    length: 3
};

var arr = Array.from(obj);
console.log(arr);
// ["a","b","c"] 출력
var arr = Array.from("ABC");
console.log(arr);

//어떻게 나올까요?
document.body.innerHTML = "<div>a</div><div>b</div><div>c</div>";
var nodeList = document.querySelectorAll('div');
console.log(nodeList); //객체
Array.from(nodeList).forEach(function(node) { console.log(node) });

    //아래와 같이 출력
    //<div>a</div>
    //<div>b</div>
    //<div>c</div>

 

3-2) 생성자 내부에서 다른 생성자를 호출

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

function Person(name, gender) {
	this.name = name;
    this.gender = gender;
}
function Student(name, gender, school){
	//여기서 this는 Student 객체에 바인딩 된다
    // name, gender을 Person 생성자에 전달해서 사용
	Person.call(this, name, gender);
    this.school = school;
}
function Employee(name, gender, company){
	Person.apply(this, [name, gender]);
    this.company = company;
}

var gamja = new Student("syong2", "male", "서울대");
var yangpa = new Employee("jiyoung", "female", "kakao");

// Student {name: "syong2", gender: "male", school: "서울대"}
// Employee {name: "jiyoung", gender: "female", company: "kakao"}

 

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

  3-3-1) 배열에서 최대/최솟값을 구해야 할 경우 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 출력

   3-3-2) 배열에서 최대/최솟값을 구해야 할 경우 apply를 사용할 때

     - apply(this, 배열) 에서, Math.max를 구하는데에 this는 역할이 없으므로 null이 되며,
       numbers의 배열을 각각의 인자로 받아서 계산 가능하게 해준다. 

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

//null 자리에 아무거나 써도 상관없음. 숫자를 쓰던, undefined를 쓰던 max값 산출

//만약 call을 사용하고 싶다면
var max = Math.max.call(null, ...numbers);
var min = Math.min.call(null, ...numbers);

console.log(max, min) //45, 3 출력

//만약 Math.max(numbers)를 하면
// undefined 출력
// 숫자만 arguments로 받을 수 있기 때문에(배열x)


3-3-3) ES6에서 배열에서 최대/최솟값을 구해야 할 경우 (...) spread operator을 이용할 때

const numbers = [10, 20, 3, 16, 45];

// 객체나 배열의 원소들을 하나씩 꺼내어서 펼쳐서 리턴
// ... numbers = 10, 20, 3, 16, 45

const max = Math.max(...numbers);
const min = Math.min(...numbers);

console.log(max, min); //45, 3 출력

 

4. bind 메서드

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

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

    - 따라서 변수를 할당하여 호출하는 형태로 사용된다.

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

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

var bindfunc2 = func.bind({x:1}, 4, 5);

//새로운 함수를 호출할때 넘기는 인자(6,7)는
// 기존 bind 메서드를 호출할때 전달했던 인수(4,5) 뒤에 이어서 등록된다.
bindFunc2(6,7); // {x:1} 4 5 6 7  
bindFunc2(8,9); // {x:1} 4 5 8 9

 

 4-1) name 프로퍼티

 - func이라는 원본 함수에 bind 메서드를 적용한 새로운 함수(bindFunc)라는 의미 (추적이 용이하다)

 - bound xxx 라는 뜻은 xxx 라는 함수에 bind 메서드를 적용한 함수라는 뜻

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

 

4-2) 상위 컨텍스트 this를 내부 함수 혹은 콜백함수 전달( self 라는 "변수"를 사용하지 않고 처리하기)

 

  4-2-1) call 사용하기

var obj = {
	outer: function(){
    	console.log(this); // obj === {outer: ƒ}
        var innerFunc = function() {
        	console.log(this); // {outer: ƒ}
        };
        
        innerFunc.call(this); 
        //원래 함수호출이라 window {...} 출력이지만 this를 상위 컨텍스트의 this로 바인딩 함
    }
};
obj.outer();

  4-2-2)  bind 사용하기

var obj = {
	outer: function(){
    	console.log(this); // {outer: ƒ}
        
        var innerFunc = function(){
        	console.log(this); // {outer: ƒ}
        }.bind(this);
        //innerFunc 라는 새로운 함수를 반환하며 bind 메서드를 통해 this를 미리 적용
        // window {...} => obj인 {outer : f} 출력된다
        innerFunc();
    }
};

obj.outer();

   4-2-3) 콜백함수에서의 bind 메서드 사용하기

     - 콜백함수도 함수이기 때문에 기본적으로 this는 전역객체를 참조

     -  제어권을 받은 함수 B ( setTimeout) 에서 this를 bind로 별도 지정해줌 
         -> 상위 컨텍스트의 this 인 obj를 가리킨다 

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, ...}

 

5) 화살표 함수의 예외사항

 - ES6에서 도입된 화살표 함수에서는 실행 컨텍스트 생성시(함수 호출시) this를 바인딩하는 과정이 제외됨

 - 함수 내부에는 this가 존재X, 접근하고자하면 스코프 체인상 가장 가까운 this에 접근한다.
    (화살표 함수는 자신을 둘러싸고 있는 상위 환경의 this를 그대로 따른다 === Lexical this )

- 즉 별도의 변수로 this를 우회하거나, call/ apply/ bind를 적용할 필요가 없어 아주 간결하고 편리하다! 

- 화살표 함수는 call, applay, bind 메소드를 사용하여 this를 변경할 수 없다.

 

var obj = {
	outer: function(){
    	console.log(this); // {outer: ƒ}
        
        //화살표 함수를 사용했다. 함수 호출은 window를 출력하나, 상위 this를 계승하여 obj가 출력
        var innerFunc = () => {
        	console.log(this); // {outer: ƒ}
        };
        innerFunc();
    }
}
obj.outer();

 

  ⭐⭐화살표 함수를 사용해서는 안되는 경우를 꼭 공부해보자⭐⭐
        (메소드, prototype, 생성자함수, addEventListener의 콜백함수)

1. 메소드 : 메소드로 정의한 화살표 함수 내부의 this는 메소드를 소유한 객체 = 메소드를 호출한 객체를 가리키지 않고 상위 컨텍스트인 전역객체 window를 가리킨다. 

const person = {
	name: "mushroom",
    myName: () => console.log(`${this.name} no study`)
};
person.myName();

//  no study 출력

2. prototype: 화살표 함수 내부 this는 전역객체 window를 가리키므로, prototype에 메소드를 할당할때는 일반함수로 할당해야함

const person = {
	name: "mushroom",
};

Object.prototype.myName = () => console.log(`${this.name} 는 공부를 안한다`);

person.myName();
// 는 공부를 안한다

3. 생성자함수: 화살표 함수는 생성자 함수로 사용할 수 없음. 생성자 함수는 prototype 프로퍼티를 가지며, prototype 프로퍼티가 가리키는 프로토타입 객체의 constructor을 사용하지만 화살표 함수는 prototype 프로퍼티가 없음

일반함수로 생성자 함수 생성
애로우 펑션의 경우 생성자가 없음

 

4. addEventListener의 콜백함수: addEventListener 함수의 콜백 함수를 화살표 함수로 정의하면 this가 상위 컨택스트인 전역 객체 window를 가리킨다.

document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector("#a").addEventListener("click", () => {
	console.log(this === window);
})

화살표 함수에서 this 는 전역객체가 된다(상위 컨텍스트) 

 

 

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

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

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

 - 이런 형태는 내부 요소에 대해 같은 동작을 반복 수행하는 배열 메서드에 많이 포진,
    ES6에서 추가된 Set, Map에도 일부 존재

 🔎 forEach 메서드 예시

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

 ◾ callback -> 각 요소에 대해 실행할 함수. 다음 세 가지 인수를 받습니다.

     1. currentValue : 처리할 현재 요소

     2. index (Optional) : 처리할 현재 요소의 인덱스

     3. array (Optional) : forEach()를 호출할 배열

 ◾  thisArg (Optional) -> callback을 실행할 때 this로 사용할 값

  1) add라는 메서드는 arguments를 call 메서드를 통해 배열로 반환하여 args라는 변수에 담았다. 

  2) this로 사용할 값을 add 메서드의 this로 바인딩하였다 = report 

var report = {
	sum: 0,
    count: 0,
    add: function(){
    	var args = Array.prototype.slice.call(arguments); // args = [60, 85, 90]
        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

 

반응형