[자바스크립트] arrow function과 this
ecmascript6가 나온지는 한참됐지만 아직 브라우저(특히 ie)들이 이를 완벽히 지원하지 않고 있다. 따라서 현재도 여전히 es5에서 es6+로 넘어가는 과도기인데, 다행히 babel의 도움으로 지금 당장 es6로 개발을 해도 크게 문제될 것은 없다.
es6의 가장 큰 문법적 변화사항을 꼽자면 역시나 arrow function이다. 그동안 주로 es5를 활용해서 자바스크립트 개발을 해왔다면 이 arrow function이 익숙지 않을것인데, 특히나 this와 관련해서 짚어볼 점이 있어 정리를 한번 해봤다.
1. Arrow function 문법
Arrow function의 기본적인 문법은 다음과 같다. 함수 작성시에 많은 부분을 생략할 수 있고, 화살표(=>)를 사용하여 조금 더 간결하게 코드를 작성할 수 있다.
//es5
var add = function(a,b){
return a+b;
}
//es6 - arrow function
const add = (a,b) => {
return a + b;
}
//매개변수가 1개라면 소괄호도 생략할수있다.
const square = x => { return x*x };
//한줄로 작성 가능한 경우 중괄호와 return 키워드도 생략 가능하다.
const square = x => x*x;
const add = (a,b) => a+b;
//매개 변수가 없는 경우
() => { return { value : 1 }; }
//객체를 함수의 몸체와 구분해주기 위해 소괄호를 사용한다
() => ( { value : 1 } );
es5에서는 다음과 같이 함수를 다음과 같이 작성할 수 있었다.
var square = function(x){
return x*x;
}
console.log(square(10)); // 100
function add(a, b){
return a + b;
}
console.log(add(1,2)); //3
눈치챘는지 모르겠지만 es5에서 함수는 두 가지 방식으로 쓸 수 있다. 짧게 언급하고 넘어가도록 하겠다. 1라인과 6라인에서 함수를 선언한 방식이 다른데, 각각을 함수 표현식(Function expression)과 함수 선언식(Function Declaration)과 이라고 한다.
둘의 결정적 차이는 호이스팅(스코프와 클로저편 참조)에 있다. 위 코드의 순서를 조금 변경해보자. 콘솔 로그를 코드의 상단에서 찍어보도록 하겠다. 다음과 같이 말이다.
console.log(add(1,2))); //3
console.log(square(10)); //100??
var square = function(x){
return x*x;
}
function add(a, b){
return a + b;
}
결과는? 1라인은 문제없이 3이 찍힌다. 하지만 2라인은? Uncaught TypeError가 뜬다. 못믿을것 같아서 캡쳐를 준비해봤다.
왜 제대로 작동하지 않은걸까. 아니, 생각을 바꾸어보자. 1라인은 왜 정상적으로 실행됐을까. 분명 함수가 선언되기도 전에 실행을 했는데 말이다. 이유는 함수 선언식이 호이스팅의 영향을 받기 때문이다. 즉, 선언식으로 쓰여진 함수는 브라우저에 의해 이렇게 해석된다.
var square;
function add(a, b){
return a + b;
}
console.log(add(1,2))); //3
console.log(square(10)); //100??
square = function(x){
return x*x;
}
둘다 호이스팅이 되어 위로 끌어올려지지만 8라인 실행 시점에서의 square는 할당되지 않은 변수에 불과하므로 에러가 발생하는 것이다. 따라서 es5 코딩을 할때는 둘의 차이를 잘 인지하고 있어야 한다. 자, 이정도까지만 언급하고 넘어가기로 한다.
❙ Arrow function의 호출
반면 es6에서는 고민할 필요가 없다. Arrow function은 오직 익명함수이다. 다시말해 함수 표현식만 사용할 수 있다.
var square = x => x*x;
console.log(square(10)); //100
이쯤되니 궁금증이 생긴다. es6에서도 호이스팅은 똑같이 적용될것이다. 그렇다면 선언전에 호출을 한다면 똑같은 에러가 발생할까?
console.log(square(10)); //??
var square = x => x*x;
정답은 발생한다. 호이스팅은 여전히 똑같이 적용된다.
3. This
사실 자바스크립트의 This에 관해서는 한 차례 다룬적이 있었는데, 여기서는 es6에 조금 더 집중해보도록 하자. 들어가기전에 지난글(this의 개념과 활용법)을 먼저 읽어보기를 권고한다.
자바스크립트에서 this는 그 용법이 특이하다. 요약하자면 es5에서의 this는 이렇다.
1. 함수 실행시에는 전역(window) 객체를 가리킨다.
2. 메소드 실행시에는 메소드를 소유하고 있는 객체를 가리킨다.
3. 생성자 실행시에는 새롭게 만들어진 객체를 가리킨다.
이 정도가 될 것 같다. 하지만 es6는 다르다. e6에서의 this는 Lexical this이다. 즉, 자신을 둘러싼 환경의 this를 그대로 계승 받는다. 다음과 같이 말이다.
function objFunction() {
console.log('Inside `objFunction`:', this.foo); // 13
return {
foo: 25,
bar: () => console.log('Inside `bar`:', this.foo) // 13
};
}
objFunction.call({foo: 13}).bar();
es5에서는 메소드 호출시, this는 메소드를 소유하고 있는 객체를 가리킨다고 하였다. 따라서 es5라면 5라인의 this.foo는 25가 되어야 했겠지만 arrow function은 Lexical this를 가진다. 그렇기 때문에 call 메소드를 통한 간접실행이 일어날때의 this 문맥을 그대로 계승하는 것이다. 더 자세한 것은 (this의 개념과 활용법)을 참조하자.
어쨌든, arrow function은 이런 특징이 있어서 편리할 때가 있다. 이는 대표적으로 콜백 함수 작성시에 유용하다. 콜백도 함수이기 때문이다. es5에서는 함수 호출시 this가 전역 객체를 가리키는데 반해 arrow function은 상위 환경의 this를 계승받는다. 다음 코드도 이상없이 동작한다.
function Prefixer(prefix) {
this.prefix = prefix;
}
Prefixer.prototype.prefixArray = function (arr) {
return arr.map(x => `${this.prefix} ${x}`);
};
const pre = new Prefixer('Hi');
console.log(pre.prefixArray(['Lee', 'Kim']));
6라인의 콜백함수에서, this는 상위 컨텍스트의 this를 그대로 계승받는다. 따라서 생성자 함수의 this를 따르게 되는 것이다. 즉, this가 가리키는 대상은 새롭게 생성된 Prefixer 객체가 될 것이다. 하지만 es5 였다면? 콜백 함수의 this는 전역 객체를 가리켰을 것이다.
es5에서도 미리 상위 컨텍스트의 this를 변수에 할당에 놓는 등 여러가지 파훼법이 있었지만 arrow function으로 이 과정이 필요없어졌다. 하지만, 그렇다고 언제나 arrow function을 활용할 수 있는 것은 아니다. arrow function을 써서는 안될 때가 있기 때문이다.
4. Arrow function을 쓰면 안되는 경우
4.1 메소드
다시 강조하지만 화살표 함수는 자신을 둘러싸고 있는 상위 환경의 this를 그대로 계승하는 Lexical this를 따른다고 했었다. 따라서 메소드를 arrow function으로 작성하면 문제가 생긴다. 코드를 보자.
const person = {
name: 'Lee',
sayHi: () => console.log(`Hi ${this.name}`)
};
person.sayHi(); // Hi undefined
위와 같이 메소드를 arrow function으로 작성한다면, this는 상위 환경의 this를 계승하므로 전역 객체를 가리키게 되는 것이다. 하지만 본래 의도는 person 객체를 가리키는 것이었으므로 결과적으로 부자연스러운 동작이 된다. 아래와 같이 전통을 따르자.
const person = {
name: 'Lee',
sayHi: function(){
console.log(`Hi ${this.name}`)
};
person.sayHi(); // Hi Lee
4.2 prototype
메소드를 prototype에 할당할 때도 arrow function을 사용하면 같은 문제가 발생한다.
const person = {
name: 'Lee',
};
Object.prototype.sayHi = () => console.log(`Hi ${this.name}`);
person.sayHi(); // Hi undefined
마찬가지로 일반 함수를 사용하자.
const person = {
name: 'Lee',
};
Object.prototype.sayHi = function() {
console.log(`Hi ${this.name}`);
};
person.sayHi(); // Hi Lee
4.3 생성자
생성자 함수에서 this는 새롭게 만들어진 객체를 가리킨다고 했다. 하지만 arrow function은 생성자 함수로 쓰는 것 자체가 불가능하다. 예제를 보자.
const Foo = () => {
console.log(this);
}
const foo = new Foo(); // TypeError: Foo is not a constructor
Foo는 생성자 함수가 아니라는 에러가 발생한다. arrow function에는 prototype 프로퍼티가 없기 때문이다. 이를 이해하기 위해서는 생성자 함수와 prototype에 관해 조금은 설명이 필요할 것 같으니 그림을 보자.
자바스크립트에서 함수를 정의하게 되면 함수뿐 아니라 프로토타입 객체도 함께 생성된다. 그리고 생성된 함수(그림의 왼쪽)의 prototype 속성은 이 프로토타입 객체를 가리키게 된다. 즉, 생성자 함수는 new 키워드를 통해 객체를 생성할때 이 프로토타입 객체의 constructor를 사용하는 것이다.
하지만 arrow function은 이 prototype 속성 자체를 가지고 있지 않다. 따라서 생성자 함수는 반드시 일반 함수로 정의해주어야 한다.
4.4 addEventListener의 콜백 함수
arrow function을 addEventListener 함수의 콜백 함수로 정의하면 this는 전역 객체를 가리킨다.
const box = document.getElementById('box');
box.addEventListener('click', () => {
console.log(this); //window
});
arrow function은 부모 scope의 this를 계승하므로 이 경우 this는 window를 가리키게 되는 것이다. 일반 함수로 고쳐주자.
const box = document.getElementById('box');
box.addEventListener('click', function() {
console.log(this); //box
});
이제 this는 자식(box) scope로 rebound되고 의도했던대로 this는 box를 가리키게 된다.
- 끝 -
참고
https://poiemaweb.com/es6-arrow-function
https://wesbos.com/arrow-functions-this-javascript/
https://medium.com/@bluesh55/javascript-prototype-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-f8e67c286b67