ES6 화살표 함수(Arrow function) 방식은 작성에 편리하고 보기에도 간결하기 때문에 한번 익히고 나면 이 방식만 고수하는 경우도 종종 있다. 하지만 사람이 쓰고 읽기에 좋다는 이유만으로 남용하게 되면 의도치 않은 실수를 범할 수 있다.
기존 function 키워드 선언 방식과 달리 화살표 선언 방식은 this, prototype, arguments 정보를 생성하지 않는데, 이 3가지가 없음으로 인해 발생하는 실수들을 정리했다.
일반 함수와 달리, 화살표 함수는 자신이 호출되면서 생성된 실행 콘텍스트에서 thisBinding 정보를 생성하지 않는다. 쉽게 말해서 호출이 되더라도 '누가' 자신을 호출하는지에 대한 정보를 생성하지 않는 것이다. 그래서 만약 화살표 함수 내부에서 this
가 참조된다면, 그 this
는 화살표 함수가 호출될 때 생성되는 실행 콘텍스트의 thisBinding 정보가 아니라, 화살표 함수가 선언되어 있는 실행 콘텍스트가 참조하는 thisBinding 정보를 참조한다. 즉, 스코프 상의 this를 참조하는 것이다. this
가 위치한 스코프에서 this가 무엇인지 찾고, 해당 스코프에 this가 없다면 스코프 체인을 타고 올라가며 가장 가까운 this를 참조하게 된다.
화살표 함수는 prototype
이 존재하지 않는다. 그래서 그래서 만약 화살표 함수로 선언된 함수를 new
와 함께 호출하더라도, prototype 객체가 없기 때문에 자신의 인스턴스 객체가 만들어질 수 없다. 그렇기 때문에 화살표 함수는 생성자 함수의 역할을 할 수 없다.
화살표 함수는 arguments 프로퍼티를 생성하지 않는다. 그래서 화살표 함수 내부에서 arguments
가 참조된다면 ReferenceError가 발생하거나, 상위 실행 콘텍스트가 있다면 스코프상의 arguments 객체를 참조하게 된다. 이는 this와 같은 맥락이다. 단, 파라미터에 ...args
을 넣어 args
배열을 참조함으로써 arguments
객체를 완전히 대체할 수 있다.
참고 : Arrow function and arguments - raram2
const obj = {
fn1: function() { console.log(this); },
fn2: () => { console.log(this); },
}
obj.fn1(); // this === myObj
obj.fn2(); // this === Window
fn2를 호출한 myObj가 fn2의 호출자(this)가 될 것이라 생각하기 쉽지만, fn2는 화살표 함수이므로 아무리 호출이 된다고 한들 this는 존재하지 않는다. 그래서 fn2 내부의 this
는 상위에 있는 실행 콘텍스트의 this
를 찾아 참조한다. 위 예시에서는 상위 실행 콘텍스트가 전역이므로 this
는 전역 객체가 된다.
1 document.body.innerHTML += '<button>버튼1</button><button>버튼2</button>';
2 const btnList = document.querySelectorAll('button');
3 const [btn1, btn2] = [btnList[0], btnList[1]];
4 btn1.addEventListener("click", function() { console.log(this); });
5 // btn1 클릭시 btn1 객체 출력 (this === btn1)
6 btn2.addEventListener("click", () => { console.log(this); });
7 // btn2 클릭시 Window 객체 출력 (this === Window)
addEventListener은 기본적으로 callback을 자신의 this로 바인딩한다. 그래서 콜백을 화살표 함수로 선언한다면, 설계된 로직상 바인딩 자체가 일어날 수 없게 된다. 이 외에도 addEventListener와 같이 내부 설계상 콜백 함수에 this가 강제 바인딩된 고차 함수의 경우도 마찬가지이니 이점 주의가 필요하다.
8 btn1.onclick = function() { console.log(this); }
9 // btn1 클릭시 btn1 객체 출력 (this === btn1)
10 btn2.onclick = () => { console.log(this); }
11 // btn2 클릭시 Window 객체 출력 (this === Window)
이벤트 속성 선언 방식도 이벤트 리스너의 경우와 마찬가지다. 10번째 줄에서 btn2.onclick
의 값이 화살표 함수로 선언되었으므로, btn2를 클릭하더라도btn.onclick
내부의 this
는 스코프상의 this, 즉 이 예시에서는 전역을 참조하게 된다.
function
키워드로 함수를 선언하게 되면, 그 함수을 호출하는 방법에 따라 함수의 this
가 동적으로 변하게 된다. 그래서 ES6 이전 고차(High-Order) 함수에서는 콜백 함수의 this
를 명확하게 설정하기 위해 자신의 this
를 self
또는 that
변수에 할당한 후, 콜백에 그 변수를 강제 바인딩(bind) 하는 방법을 사용했다. forEach
, map
, filter
와 같은 고차 함수에는 이런 방법이 적용되어 있으며, 마지막 인자로 콜백을 바인딩할 대상(thisArg)를 넣을 수 있게 옵션을 제공한다. 이렇게 하면 콜백 함수 내부에 사용되는 this
가 누구인지 분명해지기 때문에 디버깅이 수월해지기 때문이다.
하지만 콜백(또는 내부 함수)에 화살표 함수를 사용하면 이러한 수고를 하지 않아도 된다. 이들 내부에 사용되는 this
는 자연스럽게 상위에 있는 스코프, 즉 고차 함수(또는 내부 함수를 포함하고 있는 함수)의 this
가 되기 때문이다. 이처럼 화살표 함수는 같은 맥락에 있어야하는 함수가 다른 this
를 가리키게 되는 문제점을 원천차단해준다.
화살표 함수가 일반 함수보다 겉보기에는 간결하기에 막 사용했었다. 하지만 그 이면에 달라진 모습은 보지 못했고, 이로 인해 일어난 역효과들을 파악하느라 애를 먹었다. 비교적 최신 기술이라고 일단 사용하기보다, 왜 이 방식이 등장했는지, 원래는 어떤 문제점이 있었고 어떤 원리로 보완하는지에 대해 파악해보는 습관을 길러야겠다.
좋은 정리 감사합니다.