‘코어 자바스크립트’를 읽던 중 궁금한 점이 생겼다.
배열 내장 메서드(ForEach, map, filter 등)를 쓸 때 콜백함수에 Regular function, Arrow function 둘 다 사용가능하다.
const array1 = ['a', 'b', 'c'];
array1.forEach(function (element) {
console.log(element);
});
array1.forEach((element) => console.log(element));
그리고.. 한번도 신경쓰지 않았던 thisArg
Array.prototype.forEach() - JavaScript | MDN
그렇다면, 어떤 방법이 더 좋을까?
Arrow function에 대하여 조금 더 알아보고 두 가지 방법에 차이점을 알아보자.
ES6는 익명 함수 식을 작성하는 새로운 방법으로 파이썬과 같은 다른 프로그래밍 언어의 람다 함수와 유사한 ‘Arrow Function’을 도입하였다.
const sum = function(x, y) {
return x + y;
}
// Arrow Function
const sum = (x, y) => x + y;
도입된 2가지 이유가 존재한다.
오늘은 2가지 이유중에서 ‘바인딩하지 않는 this’를 중점적으로 살펴보고자 한다.
코드를 읽다보면 ‘this’ 키워드가 나왔을 때 무엇을 지칭하는지 헷갈릴 때가 많다.
왜냐하면 전통적인 함수내부에서 직관적이지 않은 this 문법 때문이다.
아래 예시를 각 this가 무엇을 출력할지 예측해보자.
const obj = {
outerFunc: function () {
console.log(this); // (1)
const innerFunc = function () {
console.log(this); // (2)
};
innerFunc();
},
};
obj.outerFunc();
내가 처음 코드를 보았을 때는 (1)의 경우에는 outerFunc을 Dot notation으로 호출한 obj 객체, (2)의 경우에도 obj 객체를 예상했다.
하지만 결과는 (1): obj, (2): 전역객체 (window) (strict mode라면 undefined) 가 나왔다.
이유는 obj.outerFunc 실행컨텍스트 생성될 때 Dot notation으로 호출한 obj 객체를 ‘this’로 바인딩 시키고, 내부에서 innerFunc가 호출되면서 해당 실행컨텍스트가 생설될 때 어떤 ‘객체의 메서드’가 아닌 함수로 호출하였으므로 전역객체(window)가 바인딩 된다.
위의 예제를 ‘Arrow function'으로 작성해보자.
const obj = {
outerFunc: function () {
console.log(this); // (1)
const innerFunc = () => {
console.log(this); // (2)
};
innerFunc();
},
};
obj.outerFunc();
결과는 (1): obj, (2): 상위 스코프 outerFunc의 this가 가르키는 ‘obj’가 나왔다.
직관대로 나와서 기쁘긴한데, 왜 이런 결과가 나왔을까?
“Arrow function를 실행했을 때 실행 컨텍스트가 정의되지 않기 때문이다.”
보통의 함수라면 함수를 실행했을 때 아래와 같이 ‘this’를 포함하는 속성을 가지는 실행 컨텍스트 객체가 정의되어야 한다.
하지만, Arrow function의 경우에는 다르다.
ECMASCript 2015 Spce에 명시되어 있는 ‘Arrow function’에 관한 설명을 보자.
14.2.16 Runtime Semantics: Evaluation
ArrowFunction : ArrowParameters
=>
ConciseBody
- If the function code for this ArrowFunction is strict mode code (10.2.1), let strict be true. Otherwise let strict be false.
- Let scope be the LexicalEnvironment of the running execution context.
- Let parameters be CoveredFormalsList of ArrowParameters.
- Let closure be FunctionCreate(Arrow, parameters, ConciseBody, scope, strict).
- Return closure.
NOTEAn ArrowFunction does not define local bindings for
arguments
,super
,this
, ornew.target
. Any reference toarguments
,super
,this
, ornew.target
within an ArrowFunction must resolve to a binding in a lexically enclosing environment. Typically this will be the Function Environment of an immediately enclosing function. Even though an ArrowFunction may contain references tosuper
, the function object created in step 4 is not made into a method by performing MakeMethod. An ArrowFunction that referencessuper
is always contained within a non-ArrowFunction and the necessary state to implementsuper
is accessible via the scope that is captured by the function object of the ArrowFunction.
문장중에 우리가 주목해야 할 부분은
“Any reference to arguments, super, this, or new.target within an ArrowFunction must resolve to a binding in a lexically enclosing environment. Typically this will be the Function Environment of an immediately enclosing function.”
Arrow function안에 this 값을 둘러싸인 ‘lexically environment’에서 해결한다고 나와있다.
다시 코드를 살펴보자.
const obj = {
outerFunc: function () {
console.log(this); // (1)
const innerFunc = () => {
console.log(this); // (2)
};
innerFunc();
},
};
obj.outerFunc();
즉, innerFunc (Arrow Function)의 둘러싸인 lexically environment (outerFunc)에 this 값 = obj을 가져온다.
Arrow Function 덕분에 더이상 내부함수에 this를 사용하기 위해 const self = this, innerFunc.bind(this)
를 해주지 않았도 된다.
그럼 이제 처음에 궁금했던 “ForEach 콜백함수에 Regular function을 쓸까? Arrow function을 쓸까?”에 대한 질문에 답을 해보자.
아래와 같이 forEach 콜백함수를 Regular function을 이용하여 정의하였다.
콘솔에 ‘report.sum’이 어떻게 나올까?
const report = {
sum: 0,
add: function (...args) {
args.forEach(function (value) {
this.sum += value;
++this.count;
});
},
};
report.add(50, 100, 150);
console.log(report.sum);
300을 기대했지만 답은 ‘0’이 나온다.
위에서 글을 읽었다면, 이유에 대해서 유추할 수 있을 것이다.
그럼 의도대로 동작하기 위해 코드를 수정해보자.
const report = {
sum: 0,
add: function (...args) {
args.forEach(function (value) {
this.sum += value;
++this.count;
}, this);
},
};
report.add(50, 100, 150);
console.log(report.sum);
forEach 메서드에 ‘thisArg’를 정의해주었다. 이제 내부 콜백함수에 ‘this’는 ‘report’로 바인딩 되어 결과가 의도한대로 ‘300’이 나온다.
그렇다면 콜백함수로 ‘Arrow function’을 사용한다면?
const report = {
sum: 0,
add: function (...args) {
args.forEach((value) => {
this.sum += value;
++this.count;
});
},
};
report.add(50, 100, 150);
console.log(report.sum);
결과는 바로 ‘300’이 나온다.
이유를 순서대로 살펴보자.
이렇게 ‘Arrow function’을 사용하게 되면 this를 명시하지 않아도 의도한대로 동작하는 것을 볼 수 있다.
마찬가지로 콜백함수와 ‘thisArg’를 인자로 받는 다른 ‘배열 메서드’에도 적용되는 이야기이다.
Array.prototype.forEach()
Array.prototype.map()
Array.prototype.filter()
Array.prototype.some()
Array.prototype.every()
Array.prototype.find()
Array.prototype.findIndex()
Array.prototype.flatMap()
Array.prototype.from()
나의 선택은 ‘this’를 직관적으로 사용하여 코드를 이해하기 쉽게 하고, 가독성을 높여주는 ‘Arrow function’을 콜백함수로 쓸 것이다!
5 Differences Between Arrow and Regular Functions
How To Use Javascript Arrow Functions & This Keyword
ECMAScript 2015 Language Specification - ECMA-262 6th Edition
ES6 In Depth: Arrow functions - Mozilla Hacks - the Web developer blog