본문 바로가기

Programming

[JavaScript] this, call, apply, bind

 

 

Reference - 자바스크립트 심화개념 in 코드스테이츠 프리코스

 

 

 

this는 실행 컨텍스트 (Execution Context)에 속한다. 

어떤 함수가 호출되면, 실행 컨텍스트가 생성되고 call stack에 메모리를 push하고 함수를 벗어나면 call stack에서 pop한다. 

실행 컨텍스트는  Scope 별로 생성되고 그 안에는 Scope 내 변수 및 함수 (Local, Global), 전달 인자 (arguments), 호출된 근원 (caller) 그리고 this가 있다. 

 

 

'this' keyword

this는 모든 함수 scope 내에서 자동으로 설정되는 특수한 식별자로 위에서 말한 실행 컨텍스트의 구성 요소 중 하나이다. this는 함수가 실행되는 동안 이용할 수 있다. 

 

 

5 Patterns of Binding 'this' 

대부분의 경우 this의 값은 호출한 방법에 따라 결정된다. 

 

 

1. Global: window 

var name = "Global Variable";
console.log(this.name); // "Global Variable"

function foo() {
	console.log(this.name);
}

foo(); // "Global Variable"

위 코드를 보면 알 수 있듯이 전역문맥에서의 this는 window이다. 

 

 

2. Function 호출: window

var name = "Global Variable";

function outer() {
	function inner() {
		console.log(this.name);
    }
    inner();
}

outer();  // "Global Variable"

함수문맥에서 단순 호출되는 경우 역시 this는 window가 된다. 함수가 여러겹 겹쳐있는 클로저에서도 this는 window다.

But, strict mode에선 this 값은 실행 문맥에 진입하며 설정되는 값을 유지하므로 this undefined가 된다.

펑션을 선언할 때 부모객체가 window이므로 this가 window가 된다는 것은 3번의 개념과 일맥상통한다.

 

 

3. Method 호출: 부모 object

메소드로의 호출은 함수가 특정 객체의 property로 존재하며 , o.f()처럼 어떤 객체의 property인 상태로 호출하는 것을 얘기한다.

var counter = {
	val: 0,
    increment: function() {
    	this.val += 1; // 여기서 this는 부모객체 counter가 된다.
    }
};

counter.increment();
console.log(counter.val); // 1
counter['increment']();
console.log(counter.val); // 2

위의 예제에서 this는 부모객체인  counter가 된다. 

 

var obj = {
	fn: function(a,b) {
    	return this;
    }
};
var obj2 = {
	method: obj.fn
};

console.log(obj2.method() === obj2); // true
console.log(obj.fn() === obj); // true

위에 예제에서 this는 원본인 obj가 아닌 직계부모인 객체가 this가 된다. 그래서 .앞에 있는 객체가 this의 값이라고 생각하면 간단하게 this를 파악할 수 있다. 

 

 

4. Construnction mode (new 연산자로 생성된 function 영역의 this): 새로 생성된 객체

// constructor(생성자) 함수, F가 클래스
function F(v) {    
	this.val = v;
}

// f가 F의 새로운 instance
var f = new F('WooHoo!');

console.log(f.val); // WooHoo!
console.log(val); // ReferenceError

 비유하자면 class는 빵틀이고 그 안에 구워지는 빵들이 instance인 Construction mode에선 this === instance이다. 

 

 

5. .call or .apply 호출: call, apply의 첫번째 인자로 명시 된 객체 

call과 apply는 this를 명시적으로 넘겨줄 때 쓴다. 

 

function identify() {
	return this.name.toUpperCase();
}
function speak() {
	var greeting = "Hello, i'm " + identify.call(this);
    console.log(greeting);
}
var me = { name: "Kyle" };
var you = { name: "Reader" };

identify.call(me); // KYLE -> this값이 me가 됨.
identify.call(you); // READER -> this값이 you.
speak.call(me); // Hello, I'm KYLE 
speak.call(you); // Hello, I'm READER

call과 apply의 차이점 -> apply는 인자를 배열로 넘겨주고 call은 쉼표로 넣어준다. 

this가 없을 경우엔 this자리에 null로 인자를 넘겨준다. 

 

var add = function(x,y) {
	this.val = x + y;
}
var obj = {
	val: 0
}

// array를 가지고 뭔가를 구해야 할 때, apply를 쓰기 좋음
add.apply(obj,[2, 8]); // 여기서 this는 첫번째 인자인 obj
console.log(obj.val) // 10
add.call(obj, 2, 8);
console.log(obj.val) // 10

//this를 안쓰는 경우는 null로 넘겨주면 됨.
function add(x,y) {
	return x + y
}

add.call(null, 2, 8) // 10
add.apply(null, [2, 8]) // 10

 

 

call, apply로 prototype 기능 빌려쓰기

function moreThanFive(str){
	return str.length > 5;
}

let arr = ['code', 'states'];
arr.filter(moreThanFive) // ['states']
Array.prototype.filter.call(arr.moreThanFive) // ['states']
Array.prototype.filter.apply(arr,[moreThanFive]) // ['states']

위에 예제처럼 내장함수 filter의 기능을 call과 apply로 this 값을 넘겨주어 동일하게 재현할 수 있다. 

 

let list = document.querySelectorAll('a');
function getElementID(element) { return element.id}

list.map(getElementID) // -> NodeList라 배열메소드 작동되지 않음.
Array.prototype.map.call(list,getElementID) // -> call을 이용해 배열메소드 사용 가능함.

이를 이용해 유사배열의 형태인 NodeList를 가진 DOM 환경에서 배열메소드를 사용할 수 있다. 

 

.bind란?

call/apply와는 다르게, 함수를 바로 실행시키지 않고,  this 값이 바인딩된 함수를 리턴함. 클로저와 비슷한 개념이다

fn.bind(thisArg[, arg1[, arg2[, ...]]])로 표현하며 call과 인자 순서는 같다. 

 

function add (x,y) {
	this.val = x + y;
    console.log(this.val);
}

let obj = { val: 0 };

let boundFn = add.bind(obj, 2, 8); //boundFn은 함수
boundFn() // 10, add는 여기서 실행됨.

위에 예제를 보면 bind를 쓸 때 바로 함수를 실행시키는 것이 아니라 밑 줄 boundFn()을 통해 비로소 함수가 실행된다는 점을 알 수 있다. 

boundFn()은 일반적인 function(method) 호출처럼 보이지만, 여기에 this 값이 이미 바인딩 되어 있다. 

 

 

bind case: 특정 함수가 this 값을 바꿔버리는 경우

// bind case: 특정 함수가 this 값을 바꿔버리는 경우
function Box(w,h) {
	this.width = w;
    this.height = h;
    
    this.getArea = function() {
    	return this.width * this.height;
    }
    
    this.printArea = function(0 {
    	console.log(this.getArea());
    }
}

let b = new Box(100, 50);
b.printArea(); // 5000

setTimeout(b.printArea, 2000); // this.getArea이 없다는 에러가 뜸. 여기서 this는 window 객체.
setTimeout(b.printArea.bind(b), 2000) // 2초 후 b.printArea를 실행함.

위에 예제를 보면 기본적으로 setTimeout의 경우, 인자로 넘기는 함수의 this값은 window 객체로 바인딩 되어있다. 

그래서 setTimeout에는 함수 실행이 아닌 함수를 인자로 넘기고 this 값이 window가 되지 않도록 명시적으로 this 값을 인스턴스 b로 지정해야 한다. 

 

 

bind case: currying

// bind case: currying
// currying: '인자 여러개를 받는 함수'를, '인자 하나를 받는 함수'로 바꾸는 방법

function template(name, money) {
	return '<h1>' + name + '</h1><span>' + money + '</span>';
}

let tmplSteve = template.bind(null, 'Steve'); // 이미 'Steve'라는 값이 바인딩되어 있음.
tmplSteve(100); // <h1>Steve</h1><span>100</span>

let tmplJohnny = template.bind(null, 'Johnny'); // template 함수는 this값을 사용하지 않으므로 null을 넣음.
tmplJohnny(500); // <h1>Johnny</h1><span>500</span>
tmplJohnny(1000); // <h1>Johnny</h1><span>1000</span>

인자 여러개를 받는 함수를 하나의 인자를 받는 함수로 바꿀 때도 bind를 이용할 수 있다. 

위에 예제에서 name을 bind를 통해 이미 'Steve'로 넘겼기에 나머지 money 인자만 넣으면 함수가 실행되는 것을 볼 수 있다.