JS Function과 Arrow Function

JS Function과 Arrow Function

앞의 글에서는 JS가 lexical scope을 사용하는 언어인데도 불구하고 왜 this가 함수 실행 때 binding되는지 문제제기 했다. 또한 lexical scope와 dynamic scope에 대해서, lexical scope임에도 동적으로 함수가 생성되는 특징 때문에 생겨나는 closure에 대해 다루었다. 이번 글에서는 function과 arrow function의 실행을 다루면서 왜 this가 실행 때 binding되는지와 closure를 사용함으로써 function과 arrow function의 this가 어떻게 달라지는 지 알아보도록 하겠다.

그래서 this는 실행 시에 binding되는거야?

사실 이번에 다루는 주제 또한 JS에서는 함수 또한 Object 객체로부터 만들어진 객체라는 지식과, 객체 간 prototype chain을 통해 이루어 지는 유사 상속에 대한 사전 지식이 필요하다. 하지만 해당 주제들은 단일 주제로 다루어야 할만큼 꽤 많은 내용이기 때문에 해당 내용을 안다는 전제 하에 설명을 하겠다.(사실 쉽고 간결하게 설명할 역량이 부족합니다…)

※ 함수 또한 Object 객체로부터 만들어진 객체라는 사실은 함수를 생성한 후에 생성된 함수의 prototype 값을 확인해 보면 proto값이 Object라는 사실로 확인할 수 있다.

  • this example
1
2
3
4
5
6
7
8
9
10
11
function foo() {
console.log(this.a); // this points for what?
}

var bar = {
a: 10,
foo: foo
}

foo(); //undefined
bar.foo(); //10

위의 예제는 지난 글에서 다루었던 예제이다. 위의 예제를 보면 마치 함수 foo에는 this 값이 없어서 this 값을 외부에서 찾아야 할 것 같아 보인다. 그런데 사실은 그렇지 않다. 우리는 JS에서 함수가 실행될 때 주어진 변수만 받아서 쭉 실행되는 것처럼 생각하지만 사실 JS의 함수는 그렇게 동작하지 않는다.

JS에서 함수가 동작하는 방식

JS에서 함수가 동작하는 방식을 이해하려면 앞서 언급했던 JS에서는 함수도 객체라는 사실을 기억해야 한다. JS에서 함수가 생성될 때는 JS 내부적으로 Function constructor가 함수를 생성한다. contructor가 함수를 생성할 때는 함수 내부에 즉시 prototype이라는 key를 만들고 오브젝트를 할당한다. 그 다음 prototype 오브젝트에 constructor라는 키를 잡고, 함수 자신의 참조 값을 저장한다. 따라서 JS에서 생성된 함수는 다른 언어에서의 함수와는 다르게 그 자체가 객체인 것이다. 아래의 예제는 함수 생성 과정을 간단히 설명한 예제이다. JS내부에서는 아래의 예제의 과정이 자동적으로 이루어진다.

  • JS 함수 생성 과정
1
2
3
4
5
6
7
8
9
10
//함수가 생성되면
function test() {}

//1. 즉시 prototype이라는 키에 오브젝트가 할당되고
test.prototype = {};

//2. contructor에 함수 자신의 참조 값을 저장한다
test.prototype.constructor = test
//출처: 클래스 기반 언어 vs 자바스크립트 1/3
//(https://www.bsidesoft.com/318)

그러면 위의 과정으로 만들어진 함수는 JS Function 객체 prototype의 모든 메소드들에 접근할 수 있고, 사용할 수도 있다. 그리고 JS 함수는 함수를 실행할 때 Function.prototype.call함수를 사용해서 실행한다

Function.prototype.call

func.call(thisArg[, arg1[, arg2[, …]]])

Function.prototype 메소드들이 여러가지 있지만, JS에서 함수를 실행 할 때는 내부적으로 Function.prototype.call에 의해 이루어 진다. Function.prototype.call은 두 종류의 인자를 받는다. 첫번째로 받는 인자는 thisArg로 객체의 참조 값을 받는다. 두번째로 받는 인자는 함수의 인자들을 배열로 묶은 argList이다. 실제로 console에서 arguments를 확인해보면 각각의 인자들을 확인해볼 수 있다.

  • 함수 인자 확인
1
2
3
4
5
function test(a, b, c){
console.log(arguments[0], arguments[1], arguments[2]);
}

test(1, 2, 3); // 1, 2, 3

이를 통해 알 수 있는 사실은 함수에서 this값은 binding되는 게 아니라 인자로 주어진다는 것이다. 위의 예제에서 test함수는 a, b, c 세가지의 인자만 받는 것이라고 생각하지만, 실제 실행 시에는 Function.prototype.call이 사용되며 자동으로 현재 객체의 참조 값이 thisArg로 전달된다. 따라서 this는 lexical scope의 예외가 아니다. this 값은 함수 내부적으로 주어지는 값이다.
  • this example
1
2
3
4
5
6
7
8
9
10
11
function foo() {
console.log(this.a); // this points for what?
}

var bar = {
a: 10,
foo: foo
}

foo(); //undefined
bar.foo(); //10

위의 예제를 다시 보면 foo 함수가 실행될 때, Function.prototype.call에 this 값으로 전역 객체 window가 주어지는 반면 bar.foo 함수가 실행될 때는 foo함수의 this값으로 bar객체의 참조 값이 주어진다. 이제 this에 대한 의문이 풀렸다. 함수의 this가 실행 시의 객체인 이유는 dynamic binding이기 때문이 아니라 함수 내부적으로 this 값이 인자로 전달 되기 때문이다.

Arrow Function

그런데 내부적으로 this값을 주는 방식으로 함수가 실행되었을 때 불편한 점이 있다.

  • this 값으로 전역객체가 주어질 때
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person() {
// Person() 생성자는 `this`를 자신의 인스턴스로 정의.
this.age = 0;

(function growUp() {
// 비엄격 모드에서, growUp() 함수는 `this`를
// 전역 객체로 정의하고, 이는 Person() 생성자에
// 정의된 `this`와 다름.
this.age++;
})();
}

var p = new Person();
console.log(p.age); //0
//출처: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Functions/%EC%95%A0%EB%A1%9C%EC%9A%B0_%ED%8E%91%EC%85%98 (mdn예제를 일부 수정)

위의 예제를 보자. Person 생성자 함수가 있고 생성자 함수 안에는 즉시 실행함수 growUp이 this의 age값을 1 증가 시킨다. 문제는 growUp함수에 입력되는 this 값은 Person의 this 값이 아니다. JS에서 함수들은 객체에서 실행되지 않는다면 this값으로 window객체가 주어진다. 따라서 growUp함수에서 사용하는 this는 Person의 인스턴스가 아니라 전역객체 window이다. 그래서 p.age를 출력해보면 age값이 변하지 않고 그대로 0이 출력된다.
  • closure 을 이용한 this binding
1
2
3
4
5
6
7
8
9
10
11
12
13
function Person() {
var that = this;
that.age = 0;

(function growUp() {
// 콜백은 `that` 변수를 참조하고 이것은 값이 기대한 객체이다.
that.age++;
})()
}

var p = new Person();
console.log(p.age); //1
//출처: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Functions/%EC%95%A0%EB%A1%9C%EC%9A%B0_%ED%8E%91%EC%85%98 (mdn예제를 일부 수정)

그러면 growUp함수에서 Person 인스턴스를 사용할 수 있는 방법이 없는 것인가? 그렇지 않다. JS의 특징인 closure를 사용하여 우회적으로 사용할 수 있다. 위의 예제를 보면 Person 생성자 함수가 인스턴스 참조 값인 this를 that 변수에 저장한다. growUp 함수는 that 변수가 없기 때문에 자신이 생성되었을 때의 환경에서 자유변수들을 탐색한다. Person 생성자 함수 안에서 Person 인스턴스 참조가 저장된 that 변수를 발견했기 때문에 that.age를 1 증가시킨다. 따라서 p.age를 출력해보면 1이 출력되는 것을 확인할 수 있다. 그런데 위의 방식으로 일일이 인스턴스 참조값을 따로 저장해서 사용하는 방식은 불편하다. 그래서 es6부터는 arrow function이 생겼다.

arrow function

… EC2015는 스스로의 this 바인딩을 제공하지 않는 화살표 함수를 추가했습니다(이는 렉시컬 컨텍스트안의 this값을 유지합니다).

this 문서를 보면 arrow function은 스스로의 this 바인딩을 제공하지 않는다고 설명한다. 이는 기존의 일반 함수들이 내부적으로 Function.prototype.call을 실행하여 this를 인자로 넘기는 방식으로 작동하지 않는다는 의미다. 괄호 설명을 보면 arrow function이 렉시컬 컨텍스트 안의 this 값을 유지한다고 말하는데, arrow function 문서에서는 arrow function이 렉시컬 컨텍스트 안의 this 값을 유지한다는 말을 다른 말로 설명했다.

화살표 함수는 전역 컨텍스트에서 실행될 때 this를 새로 정의하지 않습니다. 대신 코드에서 바로 바깥의 함수(혹은 class)의 this값이 사용됩니다. 이것은 this를 클로저 값으로 처리하는 것과 같습니다. …

즉, arrow function의 경우에는 인자로 this가 주어지지 않기 때문에 앞의 예제에서처럼 closure를 사용해 this를 binding하는 것과 동일하게 작동하는 것이다. 이때문에 arrow function은 call, apply를 사용해서 this 값을 인자로 넘겨줘도 넘겨 받은 this를 사용하지 않고 closure의 this를 사용한다. 아래의 arrow function을 사용한 예제를 보면 앞에서 closure를 사용해 this를 binding했을 때와 동일하게 작동하는 것을 확인할 수 있다.

  • arrow function을 사용한 this binding
1
2
3
4
5
6
7
8
9
10
11
12
function Person() {
this.age = 0;

(() => {
// arrow function은 closure에 있는 this값을 사용한다.
this.age++;
})()
}

var p = new Person();
console.log(p.age); //1
//출처: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Functions/%EC%95%A0%EB%A1%9C%EC%9A%B0_%ED%8E%91%EC%85%98 (mdn예제를 일부 수정)


정리

  • JS에서 일반 함수들은 내부적으로 Function.prototype.call함수를 통해 실행된다.

  • Function.prototype.call함수를 통해 실행될 때 this값과 arguments들이 인자로 주어진다.

  • 따라서 JS에서 this가 실행 시에 결정되는 이유는 dynamic binding처럼 작동하기 때문이 아니라 실행 시에 인자로 주어지기 때문이다.

  • 그런데 위와 같이 작동하는 경우 함수 내의 함수는 this 값으로 자신을 생성한 함수를 받지 못하고 전역객체 window를 받는다.(비엄격모드일 때라는 조건이 있다.)

  • 이럴 경우 내부 함수는 closure를 사용하여 this 값을 우회적으로 받아야 한다.

  • arrow 함수는 this가 내부적으로 binding되지 않고 closure 값을 사용하는 것처럼 this가 binding 된다.

참고자료

댓글