[JavaScript] 함수실행방식으로 This 값 알아보기

grinding_hannah·2020년 7월 3일
1

Introduction

모든 'function'이라는 키워드로 생성된 함수는 this를 쓸 수 있다.
화살표 함수의 경우에 this는 항상 상위 scope가 된다.

this 에 관해 알아야 할 가장 중요한 특징은,
thisthis가 사용된 함수가 어떤 방식으로 "실행"되었는지에 따라 달라진다는 점이다.
즉, 선언된 부분만으로는 this가 무엇을 가리키는 지 알 수 없다는 뜻이다.

this를 가진 함수가 실행될 수 있는 방식은 다음과 같이 4가지로 나누어볼 수 있다:

1. Regular Function Call
2. Dot Notation
3. Call, Appply, Bind
4. "new" Keyword

그렇다면 지금부터 this 값을 정확히 구분하는 방법에 대해 차례대로 정리해보자❗️


1. Regular Function Call

첫번째는, '일반함수실행(Regular Function Call)' 방식이다.

일반적으로 함수를 호출할 때 함수명(); 와 같은 방법을 사용한다.
이처럼 함수명만을 이용해 함수를 실행하는 방식을 '일반함수실행' 방식이라고 명명하기로 한다.

일반함수실행 방식에서의 thisStrict Mode를 기준으로, 다시 2가지로 나뉘어진다.

1) Strict Mode가 아닌 상태에서 일반함수실행 (this = global object)

this를 포함하고 있는 함수가 실행된 방식이 일반함수실행 방식이라면,
thisglobal object(= 브라우저에서는 window)이다.

var name = "pepper"; 

var pet = { 
  name: "salt", 
  printName: function() { 
    who(); //`this`값을 판별하는 데에 보아야할 부분은 who()가 실행된 이 부분!!
  }
}; 

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

pet.printName();     //pepper 

위 예제에서 this값을 판별하는 데에 중요한 정보는 주석으로 표시된 부분,this 를 포함한 함수인 who()가 실행되는 부분이다.
비록 pet.printName();이 실행됨으로 인해 who()함수가 실행되고 있지만, 달라지는 것은 없다.

즉, this를 포함한 함수가 또 다른 함수 내에 포함되어 있다 하더라도 확인해야 하는 부분은 실제로 this를 갖고 있는 함수가 실행되는 방식인 것이다.

+PLUS: 만일 var name = "pepper";var가 아닌 let으로 변수를 선언했다면, 전역스코프(global scope)에 선언했다 할지라도 let이나 const로 선언한 변수는 window 객체에 추가되지 않으므로 "pepper"가 아닌 undefined 를 반환한다.
게시글 <var, let, const 의 차이점>2. window 객체 요소 추가 부분 참고

2) Strict Mode에서 일반함수실행 (this = undefined)

"use strict"; 를 입력하면 Strict Mode를 활성화된다.
Strict Mode를 활성화하면 엄격한 코드실행모드가 켜지면서 this의 값도 달라진다.
이 때 thisundefined가 된다.

'use strict'; 

var name = 'Hannah';

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

call();      //Error: Cannot read property 'name' of undefined 

Strict Mode를 키고 this값을 가진 일반함수를 실행한다면 this는 무조건 undefined가 된다!
왜냐하면 undefine.nameerror 이기 때문!

만약 Strict Mode가 아닌 일반함수실행 모드였다면 thiswindow를 가리키게 되고,
그러면 console.log(this.name)name이라는 속성이 없다해도 아래와 같이 error가 아닌 undefined를 반환해서 자연스럽게 넘어가게 된다.

//non-strict mode 
console.log(window.name);    //undefined 

//strict mode 
'use strict'; 
console.log(undefined.name); //Error: Cannot read property 'name' of undefined 

객체에 없는 속성을 불러오려고 하면 undefined를 반환하기 때문이다.

사실 실무에서 window를 가리키는 목적으로 this를 사용하는 경우는 없다고 한다.
this = window의 목적으로 사용되었다면 bug일 수 있고 오히려 혼선만 생길 수 있기 때문에,
이런 this의 특징을 잘 활용하면 debugging 을 보다 쉽게 할 수 있다는 장점이 있다 😃


2. Dot Notation

두번째는, 실행문의 dot앞에 있는 객체가 this가 되는 방식이다.
객체명.함수명(); 으로 실행되는 함수에서 this는 dot(.) 앞에 위치한 객체가 된다.

var color = "yellow"; 

function printColor () { 
    console.log(this.color); 
} 

var salt = { 
    color: "white", 
    printColor: printColor
}; 

var hannah = { 
    color: "skyblue", 
    printColor: salt.printColor
}; 

salt.printColor();       //white
hannah.printColor();     //skyblue
  

위 예제에서 dot(.)앞에 위치한 객체를 기준으로 this값을 판별해보면, salt.printColor();thissalt객체가 되고,
hannah.printColor();thishannah객체가 된다.

this를 가진 함수가 객체명.함수명();의 형식으로 실행이 되었다면, this값을 찾기위해 오롯이 확인해야 하는 것은 실행문에서 dot(.) 앞의 객체가 무엇이냐는 것이다.

여기서 주의할 점은,
hannah 객체의 printColor속성값은 salt.printColor이기 때문에 hannah.printColor();를 실행 시 salt객체가 this가 될 것이라 생각할 수 있으나, 실제 this를 가진 printColor함수를 실행시키는 부분은 hannah.printColor();이므로 여기서의 thishannah객체가 된다.


3. Call, Apply, Bind

세번째는, Call, Apply, Bind 메소드를 이용해서 함수를 실행하는 방식이다.

함수도 객체이기 때문에 메소드를 사용할 수가 있다. 언제, 어디에서든 위 메소드들을 사용해서 함수를 실행시킬 수 있는데, 이 때 this 의 값도 바뀐다.

메소드는 this의 값을 명확하게 지정하기 때문에 "Explicit Binding" 이라고 명명할 수 있다.

1) .call()

함수명.call(this값, 매개변수1, 매개변수2, ...)

기본적으로 call은 앞에 있는 함수를 호출해주는 기능을 한다.
그렇지만 this를 사용한 함수에서는 this값을 정해주는 역할을 한다.

function family() { 
	console.log(this.number); 
} 

const sister = { 
	number: 2 
}; 

family.call(sister);       //2

위 예제의 경우, this값은 sister객체가 된다.
call 메소드를 사용할 때에는 괄호 안의 첫번째 인자가 this값이 된다.

즉, call 메소드는
1. 함수를 실행함과 동시에
2. this값을 지정하고 싶을 때

사용할 수 있다.

만약 매개변수를 가지는 함수를 실행함과 동시에 this값을 설정해주고 싶다면....?

function yeah (a, b, c) { 
	console.log(this.name); 
	console.log(a*b*c); 
}

var she = { 
	name: "Rachael"
}; 

yeah.call(she, 1, 2, 3);    //Rachael
			    // 6

위 예제처럼 call은 첫번째 인자인 shethis값으로 설정을 하고, 나머지 1, 2, 3은 매개변수로 함수 yeah에게 인자로 전달한다.
call메소드는 받을 수 있는 인자의 갯수 제한이 없다.

Advanced Example

  var status = "😎";
  
  setTimeout(() => {
  	const status = "😍";
  
  	const data = {
  		status: "🥕",
  		getStatus: function () {
  			return  this.status; 
  		}
  	};
  
	console.log(data.getStatus.call(this);      //여기서 this는 무엇?
  }, 0);
  

위 예제에서 callthis를 가지고 getStatus()를 실행시킨다.
여기에서의 this를 찾기 위해서는 this를 가지고 있는 function을 찾아야한다.

왜냐하면, call,apply,bindthis라는 키워드를 this로 지정해서 함수를 실행시킬 때는 this를 가지고 있는 함수가 this그 자체가 되기 때문이다.

화살표함수인 setTimeout()this를 가지고 있지 않기 때문에 상위스코프에서 찾는다.
따라서 위 예제에서의 this는 최상위스코프인 전역스코프가 되어 this.status는 "😎 "가 된다.

+PLUS: 만약에 call을 사용한 함수에서 this값을 null을 할당하는 경우(ex.foo.call(null, 1, 2, 3);)도 있는데, strict mode가 아니라면, nullundefined전역객체로 대체되어 실행된다(error가 나지 않는다). 즉, this값을 null을 할당한다고 해서 실제로 this값이 null이 되지는 않는다.

2) .apply()

함수명.apply(this값, [매개변수로 전달되는 배열])

apply도 call 메소드와 유사한 역할을 한다.
함수를 실행함과 동시에 this값을 지정할 수 있다.

function yeah (a, b, c) { 
	console.log(this.name); 
	console.log(a*b*c); 
}

var she = { 
	name: "Rachael"
}; 

yeah.apply(she, [1, 2, 3]);    //Rachael
			       // 6

하지만 apply 메소드는,
1. 2개의 인자만을 전달해야하고, (call과의 차이점 #1)
2. 첫번째 인자는 this 값이 되고,
3. 두번째 인자는 반드시 배열 (call과의 차이점 #2)

이어야만 한다.

3) .bind()

함수명.bind(this값, 매개변수1, 매개변수2, ...)

bind 메소드는
1. 원본 함수를 복제한 "새로운 함수"를 반환해줄뿐 함수를 실행하지 않으며,
2. 반환된 함수를 실행해야 원본함수가 실행된다.
3. 첫번째 인자는 this값이 되고,
4. 전달할 수 있는 인자의 갯수 제한이 없다.

function hello (a, b, c) { 
	console.log(this.name); 
} 

var her = { name<: "Kemaya" }; 

var him = hello.bind(her, 1, 2);   //this=her, a=1, b=2 가 된다. 

him(3);   //c=3 이 된다. 복사본 함수에게 주어지는 인자도 순차적으로 원본함수에게 전달되기 때문.  
	  //Kemaya

+PLUS: bind가 일반함수호출방식보다 우선순위가 높기 때문에, 만약 일반함수실행방식(ex. foo();)으로 실행이 되었는데 this값이 window나 undefined가 아니라면, 분명 앞전 단계에서 bind 로 this를 지정해 준 내역이 있다고 추측해볼 수 있다.

+PLUS: 이벤트핸들러에서 bind메소드를 다음과 같이 활용할 수 있다.

 function dog(a, b, c) { 
  	a + b + c 
 }
  
 button.addEventListener("click", function(event) { 
  	dog(a, b, c); 
 }); 

위 예제는 bind메소드를 사용해 아래의 코드로 대체할 수 있다.

function dog(a, b, c) { 
	a + b + c 
}

button.addEventListener("click", dog.bind(null, a, b, c)); 

foo.bind(null, a, b, c)로 실행한 값을 이벤트핸들러로 주는 것!
이것은 또 아래와 같은 방법으로도 기재할 수 있다.

function dog(a, b, c) { 
	a + b + c 
}
var pet = <dog.bind(null, a, b, c); 
button.addEventListener("click", pet); 

❗️ 항상 메소드를 이용할 때는 이 메소드에 어떤 매개변수를 넣어줘야 하는지, 그리고 어떤 반환값이 나오는지를 깊이 생각해 보아야 한다. ❗️


4. "new" Keyword

네번째는, new키워드로 함수를 실행하는 방식이다.
new라는 키워드를 이용해 함수를 호출 시, 새롭게 생겨난 빈 객체를 반환한다.

따라서 this = {} 와 같다고 볼 수 있으며, new를 이용해 함수를 실행하면 함수 안에서의 this값은 빈 객체이다.

function Pet() { 
	this.age = 3; 
	this.logBreed = function () { 
		console.log("papillion")
		} 
	console.log(this); 
} 

const family = new Pet();     //{age: 3, logBreed: ƒ}

위 예제는 new키워드로 생성된 빈 객체에 age = 3라는 속성과 logBreed함수를 할당해준다.
여기서 new키워드가 사용된 원본 함수Pet"constructor function(생성자함수)" 이라고 부른다.
생성자함수에 속성을 추가하면, 새로운 객체에도 같은 속성이 추가된다.

일반적으로 생성자함수는
1) 함수명이 명사이며,
2) 첫글자를 대문자로 표기한다.

그리고 생성자 함수가 반환해주는 빈 객체는 "instance(인스턴스)" 라고 부른다.
생성자함수는 만드는 단계에서부터 인스턴스를 생성할 목적으로 만들어지기 때문에 일반함수와 구분되어 사용된다.
함수 실행 시 new 라는 키워드가 사용되면 return값이 명시되지 않아도 특정 객체를 반환해 family라는 변수에 담기게 된다. = 생성자 함수의 기본 반환값이 this이기 때문!

Advanced Example

function People () { 
	this.play = function () { 
		console.log("YAY PLAY!"); 
	};
}

function Person (name) {
	People.call(this); 
	this.name = name; 
}

var hannah = new Person("hannah"); 

hannah.play();    //YAY PLAY!

위 예제에서 Person이라는 함수 안에 this가 무엇인지를 파악하기 위해서는 이 함수가 "어떻게" 실행되었는지를 봐야한다.

new Person();Person을 실행시키고 있으므로, Person함수 중괄호 내 this는 모두 새로 만들어진 빈 객체, 인스턴스 hannah가 된다.

그리고 Person함수 내 People.call(this)this를 새로 생성된 Person의 인스턴스, hannah으로 지정해줌과 동시에 People함수를 실행시킨다.


Reference

본 포스팅은 (바닐라코딩의 수장이신)Ken님의 this 이론을 기반으로 하였습니다. 🙂
학습단계로 잘못된 정보가 있을 수 있습니다. 잘못된 부분에 대해 알려주시면 곧바로 정정하도록 하겠습니다.

profile
개발자를 꿈꾸는 프린이의 코묻은 코드가 쌓이는 공간

0개의 댓글