javascript의 function 호출과 this
이 글은 velog imacoolgirlyo님의 https://velog.io/@imacoolgirlyo/JS-JavaScript-Function-Invocation와-this 글의 내용을 실습과 보존차 거의 그대로 옮기고, 약간의 예제 부분과 arrow function 부분을 보강한 것입니다. 또한 원 글은 Understanding JavaScript Function Invocation and "this" 과 MDN web docs : this 를 참고했다고 합니다. 인용이나 퍼가실때는 원본을 이용해주시기 바랍니다.
javascript에서 this란?
The object that is executing the current function.
즉 현재 함수를 부른 객체가 누구인지를 나타낸다. 함수가 어떻게 호출되냐에 따라서 this가 결정된다.
이 부분은 다른 언어랑 상당히 다른 부분이다. 다른 언어에서 this 혹은 self 라고 지칭할 때 this(self)는 보통 object의 instance 그 자체를 가리키는데, javascript는 그렇지 않다.
먼저 함수의 호출 패턴에 대해서 알아보자.
함수는 여러가지의 패턴으로 실행된다.
- 일반 함수 실행 (Simple Function Invocation)
- 함수가 object의 member function 일때, 즉 method 일때
- bind method를 사용하여 호출했을 때
- 함수가 Constructor function으로 사용될 때
Function.prototype.call(this, argList)을 이용한 함수 호출
먼저 기본적인 함수 호출이 내부적으로 어떻게 이루어지는 지에 대해서 알아보자. 일반 함수는 Function constuctor에 의해서 만들어지고 함수 호출(invocation)은 Function의 call method에 의해 이루어진다.
Function의 call method은 아래대로 동작한다.
- 처음 부터 끝까지 parameter들로 argument list,
argList
를 만든다. - 첫번째 parameter는
thisValue
이다. this
를thisValue
로 설정하고argList
를 argument list로 설정하면서 함수를 호출한다.
function hello(thing){
console.log(this + 'says hello' + thing);
}
hello.call('Jake', 'world') // => Jake says hello world
보다 시피 call method의 첫번째 parameter인 Jake를 this
로 설정하고, 'world'라는 argument를 하나를 받으면서 함수를 호출했다. 이건 JavaScript 함수 호출 (funcion invocation)의 원시적인 개념이다. 다른 모든 function 호출들도 원시적으로 표현할 수 있다.
위와 같이 작동할 수 있는 이유 : function is an object
javascript에서 function은 Function을 prototype으로 가지는 object의 한 종류이다.
let myFunction = function() {
console.log("Hello, World!");
}
혹은
function myFunction() {
console.log("Hello, World!");
}
(이 두 선언은 scope 면에서만 약~간 다르다.)
우리가 이렇게 선언하면 자바스크립트는 내부적으로 function 형태의 특별한 object를 만들어 우리에게 돌려준다. 이 function object에는 call, apply, bind가 정의되어 있다. (이 두 선언은 scope 면에서만 약~간 다르다.)
일반 함수 호출 Single Function Invocation
함수를 ()
를 사용하여 호출하는 일반적인 경우에서의 this
는 global 객체인 window 이다.
function hello(thing){
console.log(this + 'says hello' + thing);
}
hello('world')
hello.call(window, 'world')
따라서 call method를 이용하여 표현해보면 call의 첫번째 parameter를 window로 설정하여 호출하는 것과 같다.
❗️ strict mode에서는 다르다. strict mode에서 함수 호출한 경우는 this가 undefined
이다.
// in strict mode
hello('world')
hello.call(undefined, 'world');
메소드 Member Functions
함수가 Object의 method로써 호출될 때의 this
는 호출한 object를 나타낸다.
var person = {
name : 'Jake',
hello : function(thing){
console.log(this + 'says hello' + thing);
}
}
person.hello('world');
person.hello.call(person, 'world')
- person 객체 안의 hello 메소드를 person.hello('world') 로 실행시킬 수 있다.
person.hello('world')
는 call 함수로 표현했을 때 this가 person, argList는 'world'로 하여 나타낼 수 있고 즉 this는 호출한 객체인 걸 알 수 있다.
한번 더 짚고 넘어가자. this
는 고정값이 아니라 호출될 때 결정되며 caller가 누군지에 따라 달라진다.
function hello(thing){
console.log(this + 'says hello' + thing);
}
var person = { name : 'Jake'}
person.hello = hello;
person.hello('world'); // => Jake says hello world
hello('world') // => [object DOMWindow] says hello world
person.hello.call('another this', 'world') // => another this says hello world
bind 메소드를 이용한 경우,Function.prototype.bind
function.bind(obj) 메소드는 function과 같은 body, scope를 가지며 첫번째 파라미터로를 this로 고정해버리는 새로운 함수를 만든다. 즉 함수가 어디서 사용되었는지와 상관없이 this가 고정된다.
bind를 쓰는 경우를 아래와 같이 정의해보았다.
1. 객체 안에 정의된 method를 전역 컨텍스트(다른 문맥)에서 사용하고 싶을 때
2. 전역 컨텍스트(다른 문맥)에서 사용하지만 method를 객체가 부른 것 처럼 만들고 싶을 때
var person = {
name : 'Jake',
hello : function(thing){
console.log(this + 'says hello' + thing);
}
}
var boundhello = function(thing){
const func = person.hello;
return func(thing);
}
boundhello('world')
하지만 이렇게 boundhello 함수를 만들면, 여전히 boundhello.call('window', 'world')이다. 즉 boundhello에서의 this는 window이다. 어떻게 person으로 고정 시킬 수 있을까?
var bind = function(func, thisValue){
return function(){
return func.apply(thisValue, arguments);
}
}
var boundhello = bind(person.hello, person);
boundhello('world') // 'Jake says hello world'
여기서 arguments는 함수에 전달되는 Array-like object이다. apply
method는 call과 거의 동일하게 동작하지만arguments list 대신에 Array-like object를 취급한다. (Function.prototype.call, Function.prototype.apply의 차이)
새로운 bind 함수를 만들어서 method와 객체를 바인딩 해주었다.
이 처럼 bind
메소드를 이용하여서 기존의 함수와 body, scope가 같지만 this가 고정된 새로운 함수를 만들었다.
이런 표현들이 자주 쓰여졌기 때문에 ES5에서는 모든 Function object에 새로운 method bind가 추가되었다.
var boundhello = person.hello.bind(person);
boundhello('world');
bind 메소드는 callback 함수로 raw function을 사용해야할 때 매우 유용하게 쓰인다.
var person= {
name : 'Jake',
hello: function() { console.log(this.name + " says hello world");
}
document.querySelector('div').click(person.hello.bind(person));
callback 함수는 정의상 이를 다른 곳에 넘겨주고, 무슨 일이 일어나면 거기서 실행하라고 주는 함수이기 때문이다. 항상 caller가 바뀔 가능성을 염두해야 한다.
생성자 함수 Constructor function
함수가 new
키워드와 함께 Constructor로써 사용되었을 때의 this는 새로 만들어진 객체이다. 객체와 Constructor에서 살펴보았듯이 생성자 함수를 사용하면 새로운 object가 만들어지고 해당 객체가 this로 리턴된다.
class MyClass {
a = 3;
}
const a = new MyClass();
console.log(a); // MyClass {a: 3}
console.log(typeof a); // object
console.log(a.constructor.name); // MyClass
Arrow functions
arrow functions는 this를 자체적으로 가지고 있지 않다. arrow function 전까지는 this는 고정되어 있지 않고 caller가 누군지에 따라 결정되었다. 하지만 이런 this
문제들은 객체 지향 프로그래밍 스타일과 맞지 않기 때문에
이를 해결하고자 var that = this
같이 this를 고정하는 방식이나, bind function들이 만들어졌다.
Arrow function은 this를 가지고 있지 않으며, enclosing lexical scope에서의 this 를 arrow function 내에서의 this로 정한다.
class Person {
name = 'Jake';
hello(thing) {
console.log(this.name + ' says hello ' + thing);
}
arrowHello = (thing) => {
console.log(this.name + ' says hello ' + thing);
};
}
var person = new Person();
class MyClass {
name = 'MyClass';
callback1 = person.hello;
callback2 = person.arrowHello;
execute() {
this.callback1('world');
this.callback2('world');
}
}
var myClass = new MyClass();
myClass.execute();
myClass instance는 생성중 callback함수 두개를 내부에 저장해두는데 하나는 person에서 받은 regular function, 하나는 arrow function이다. 이를 실행하면
MyClass says hello world
Jake says hello world
이렇게 regular function의 경우 실행된 곳의 this의 name을 이용하고, arrow function의 경우 실행된 곳과 상관없이 Person의 객체의 name(jake)를 사용한 것을 확인할 수 있다.
결론
지금까지 javascript에서 function이 불리는 과정과 이 때 this의 정체에 대해 알아보았다. 이 개념은 실무에서 논리적인 coding과 debugging에 필수적인 부분이므로 꼭 잘 알아두도록 하자. 그리고 특별한 일이 없으면 arrow function을 선택한다면 this가 타 언어와 비슷하게 직관적으로 작동하므로 많은 문제를 미연에 방지할 수 있다.