치춘짱베리굿나이스
자바스크립트 일반함수 vs 화살표함수 본문
일반 함수 vs 화살표 함수 (람다식)
참고로 필자는 자바스크립트 입문을 let, const, 화살표함수로 했기에 그것만 주구장창 썼는데 이걸 왜 쓰는지는 명확히 이해하지 못했다 (그냥 일반함수랑 똑같이 작동한다고 생각했음)
언젠가 두 개의 차이를 정리해봐야겠다 막연한 생각은 했었는데 그게 지금이 될 줄은
일반 함수
function foo() {
console.log("foo");
}
평범하게 작성한 함수이다
const bar = function foo() {
console.log("foo");
}
변수에 할당하고 싶다면 이렇게 작성한다
화살표 함수
const foo = () => {
console.log("foo");
}
화살표를 이용해서 조금 더 간결하게 작성되는 함수이다
ES6에서 추가되었으며, 람다 함수라고도 한다
위의 경우는 foo
라는 변수에 함수를 담았을 뿐이지 (엄밀하게는 함수의 형태를 띈 객체를 담은 것) 화살표 함수 자체에는 이름이 없다
() => { console.log("foo"); }
함수의 본체는 const foo =
부터가 아니라 인자를 넣는 괄호부터이기 때문이다
이름이 없기 때문에 일반 함수와 달리 따로 (위의 foo
예시처럼) 변수에 담지 않는 이상 재호출은 어렵다
대신 일반 함수에 비해 상당히 간결하게 작성할 수 있기 때문에, 짧고 재사용 가능성이 없는 콜백 함수를 작성할 때 많이 사용된다
(v) => v + 5
간단한 값을 연산하여 반환만 하고 싶을 경우엔 중괄호조차도 필요가 없다
중괄호를 사용하지 않을 경우, 화살표 오른쪽에 있는 값을 그대로 반환한다
(v) => ({ name: v })
객체를 반환하고 싶다면 객체 중괄호를 소괄호로 감싼다
그렇지 않으면 중괄호를 함수 스코프의 시작 부분으로 이해하기 때문이다
[1, 2, 3, 4].map((v) => v + 5);
useEffect(() => { console.log("foo"); }, []);
콜백 함수를 인자로 받는 함수론 대표적으로 map
, forEach
등의 배열 메서드나 리액트에서의 useEffect
등이 있다
위의 메서드들 특성상 콜백 함수들이 해당 메서드에서만 사용되고 외부에서 재사용이 일어나지 않기 때문에 이러한 경우 화살표 함수를 쓰기 참 좋다
물론 콜백 함수 내부가 변수 선언이나 로직 때문에 과도하게 길어질 경우 가급적 함수를 분리하는 것이 가독성 면에서 훨씬 좋으므로 여러가지 조건을 고려하여 사용하자
차이점?
this 바인딩
class TestClass {
constructor() {
this.value = "hello";
}
func() {
console.log(this);
}
}
const testInstance = new TestClass();
testInstance.func();
this
는 보통 함수 (메서드) 자신을 소유하고 있는 객체, 또는 인스턴스를 가리킨다
위의 예시에서 testInstance
인스턴스는 testInstance.func
메서드를 소유하고 있으므로, testInstance.func()
메서드를 호출하면 func
내부의 this
는 testInstance
를 가리키게 되고, 따라서 console.log
는 TestClass
클래스의 인스턴스인 testInstance
를 출력한다
function foo() {
console.log(this);
}
const bar = () => {
console.log(this);
};
일반함수와 화살표함수를 가지고 간단한 예제를 만들어 보았다
일반함수 (foo
) 와 화살표 함수 (bar
) 는 둘 다 this
를 출력하는 기능을 한다
허나 두 함수 모두 전역 스코프에 정의되어 있는데, 위의 두 함수에서 this
는 언제 결정되는 걸까?
function foo() {
console.log(this);
}
class TestClass {
constructor() {
this.value = "hello";
this.func = foo;
}
}
function test() {
const testInstance = new TestClass();
const obj1 = {
value: "foo",
func: foo,
};
testInstance.func();
obj1.func();
}
foo();
test();
우선 일반 함수의 this
바인딩 테스트를 위해 전역에 foo
를 선언하고,
- 전역 스코프에서
foo
가 호출되는 경우 (foo()
) - 클래스
TestClass
의 생성자에서 인스턴스의func
메서드를 지정해주고, 이를 호출하는 경우 (testInstance.func()
) - 객체
obj1
의func
메서드로 추가하고, 이를 호출하는 경우 (obj1.func()
)
세 가지 경우에서 각각 this가 어떻게 동작하는지 알아보자
첫 번째 경우 (전역 스코프에서 호출) console.log(this)
는 말그대로 전역 스코프 전체… 를 출력했다
놀랍게도 전역 스코프가 객체의 형태로 존재한다
자바스크립트에서 모든 것은 객체인가요? 에 대해서 알아봤었는데 전역 스코프 마저 객체의 형태로 다뤄지고 있는 것이었다
브라우저에서 코드를 실행할 경우 전역 객체는 Window
를 가리키고, 위처럼 node.js
환경에서 실행할 경우 global
이라는 객체가 기본값으로 생성된다고 한다
자바스크립트는 객체를 정말로 사랑하는구나
두 번째와 세 번째 경우, 각각
TestClass
클래스로부터 생성된 인스턴스testInstance
와obj1
을 가리킨다
예상했던 대로 함수 자신을 가지고 있는 상위 스코프의 멤버를 출력하는 것을 볼 수 있다
const bar = () => {
console.log(this);
};
class TestClass {
constructor() {
this.value = "hello";
this.func = bar;
}
}
function test() {
const testInstance = new TestClass();
const obj2 = {
value: "bar",
func: bar,
};
testInstance.func();
obj2.func();
}
bar();
test();
이번에는 화살표 함수의 this
바인딩 테스트를 위해 전역에 bar
를 선언하고,
- 전역 스코프에서
bar
가 호출되는 경우 (bar()
) - 클래스
TestClass
의 생성자에서 인스턴스의func
메서드를 지정해주고, 이를 호출하는 경우 (testInstance.func()
) - 객체
obj2
의func
메서드로 추가하고, 이를 호출하는 경우 (obj2.func()
)
마찬가지로 세 가지 경우를 모두 살펴보자
세 경우 모두 빈 객체가… 출력된다 (당황)
일반 함수와 다르게 화살표 함수는 선언되는 순간 this
가 결정되기 때문이다
화살표 함수 자체는 this가 없어서, 상위 객체에서 this를 찾게 된다고 한다
따라서 화살표 함수가 가리키는 this는 화살표 함수가 존재하는 스코프의 상위 스코프를 가리킨다
bar
가 선언된 스코프 (전역 스코프) 의 상위 스코프에 해당하는 객체가 빈 객체이므로, 비어있는 객체가 출력되는 것이다
사실 이 객체는 빈 객체가 아니라 Module 객체라 해서 Module.exports 된 값들이 들어있는 객체인데, 전역 스코프에서 this를 찍어보면 Module 객체를 가리키게 된다
위의 예제를 브라우저에서 돌릴 경우, 최상위 객체인 Window 객체가 출력되게 된다
function test() {
const testFunc = () => {
return () => console.log(this);
};
testFunc()();
}
test();
이번에는 이런 함수를 작성해 보자
일부러 스코프 단위를 테스트하기 위해 복잡하게 함수 안에 함수 안에 함수… 방식으로 작성하고, 커링으로 호출하였다
이 함수의 출력값은 무엇일까?
또다시 전역 객체가 출력되었다
그 이유는 testFunc
가 반환하는 화살표 함수의 스코프 (test
) 의 상위 스코프가 전역 스코프이기 때문이다
이처럼 화살표 함수는 this
가 자기자신을 소유한 객체를 가리키는 것이 아니라, 상위 스코프를 가리키기 때문에 this
를 사용해야 하는 경우 화살표 함수로 선언하는 것이 바람직하지 않다
굉장히 까다로운 녀석이다
this 바꾸기
const obj1 = {
value: "hello",
func: function () {
console.log(this);
},
};
const obj2 = {
value: "byebye",
};
obj1.func.call(obj2); // this = obj2
obj1.func.apply(obj2); // this = obj2
obj1.func(); // this = obj1
const func2 = obj1.func.bind(obj2);
func2(); // this = obj2
일반 함수는 call
, apply
, bind
메서드를 통해 this
가 가리키는 객체를 바꿔 호출할 수 있다
call
과apply
는this
를 바꿈과 동시에 함수를 호출하며,this
의 변경은 일시적이므로 다음 호출때 원래 가리키던 객체로 돌아온다call
과apply
의 차이는 함수의 인자를 가변인자로 받는지 배열로 받는지 차이밖에 없는데, 위 예시에선 인자가 따로 없으므로 결국 같은 동작을 한다
bind
는this
가 영구적으로 바뀐 함수를 반환하며, 해당 함수를 호출하면this
가 가리키는 객체가 바뀐 채로 유지된다
아무튼… 예시를 보면 obj1.func
의 this
가 가리키는 객체가 바뀌는 것을 볼 수 있다
const obj1 = {
value: "hello",
func: () => console.log(this);
};
const obj2 = {
value: "byebye",
};
obj1.func.call(obj2);
obj1.func.apply(obj2);
obj1.func();
const func2 = obj1.func.bind(obj2);
func2();
화살표 함수는 세 메서드가 전부 먹통이 된다 = 어떠한 방법으로든 this
가 가리키는 객체 (상위 스코프) 를 바꿀 수 없다
화살표 함수의 this는 선언 시에 결정되며, 이를 바꿀 수는 없다
func
가 선언된 스코프 (전역) 의 상위 스코프니까 빈 객체가 또 출력되고 말았다
화살표 함수의 this
는 자기자신을 소유하고 있는 obj1
조차도 가리키지 않으므로, 만약 객체 내부의 메서드가 this
를 사용해야 한다면 화살표 함수는 매우 좋지 못한 선택이라고 할 수 있겠다
const obj1 = {
value: "hello",
func() {
console.log(this);
},
};
화살표함수의 간결함과 일반 함수의 this
두마리 토끼를 다 잡고 싶다면, ES6부터 지원하는 축약 메서드 표현을 이용하자
굳이 key
값을 따로 입력하거나 function
키워드를 명시할 필요 없이, 함수 선언하듯 key
와 value
를 동시에 정의할 수 있다
생성자로 사용하기
function foo(value) {
this.value = value;
}
const newFoo = new foo("hi!");
console.log(newFoo.value);
이 포스팅에서 함수 객체는 프로퍼티와 메서드를 상속할 수 있는 프로토타입 프로퍼티를 가지고 있다는 사실을 알아보았다
이 프로토타입 프로퍼티를 통해 함수 객체를 생성자로 사용하여 새로운 객체를 만들 수 있었고, 실제로 위의 예시에서 foo
함수를 통해 newFoo
객체를 만들었다
따라서 newFoo.value
는 생성자에 넘겨준 문자열인 hi
를 가지고 있게 된다
const bar = (value) => {
this.value = value;
};
const newBar = new bar("bye!");
console.log(newBar.value);
화살표 함수는 사정이 다르다
일반 함수와 다르게 프로토타입 프로퍼티가 없기 때문에 생성자로 사용할 수 없다
newBar
객체를 만들려고 해도 bar
가 생성자가 아니라는 에러만 뜬다
물론 객체가 생성된다 하더라도 화살표 함수의 this
가 가리키는 위치가 이상해지기 때문에 적절한 객체가 반환되지 않았을 것이다
arguments 변수
function foo() {
console.log(arguments);
}
foo("hello", "byebye", "chichoon");
this
와 비슷하게 함수가 기본적으로 가지고 있는 배열 비슷한 객체이며, 함수의 인자를 갖고 있다
foo
의 선언부에 따르면 foo
는 인자를 받지 않지만, 호출 시에 인자를 넣었다고 해서 오류가 발생하진 않는다
이때 모든 인자들이 arguments
에 들어가며, 인덱스를 통해 인자에 접근할 수 있다 (arguments[0]
이런 식)
const bar = () => {
console.log(arguments);
};
bar("hello", "byebye", "chichoon");
반면에 화살표 함수는 arguments
가 존재하지 않기 때문에 오류를 반환한다
(중간의 undefined
는 선언 부분에서 반환하는 값이 아무도 없기 때문에 출력된다)
위의 테스트를 브라우저에서 진행한 이유는… node
에서 테스트하면 arguments
가 존재할 뿐더러 그 안에는 알수 없는 이상한 값들이 출력되기 때문이었다
오류가 발생하지 않아 적잖이 당황했다
console.log(arguments) // 전역 스코프
저 값들이 대체 무엇인고 알아보니 전역 스코프의 arguments
값이었다
온갖 키워드로 (arguments, ancestor, parent, global arguments, function require…) 다 검색을 해봤는데 그나마 현재 상황과 맞는 답안은 “현재 함수 스코프에 arguments
가 존재하지 않을 때, 부모 스코프에서 arguments
를 찾는다" 였다
node와 브라우저가 다르게 동작하는 이유는 node에서의 전역 스코프 (global object
) 와 브라우저의 전역 스코프 (window
) 가 다르므로,
global object
에서arguments
는 파일명, 경로, 모듈 정보 등의 정보를 반환하지만window
는 에러처리를 한다고 보면 되겠다
node 환경에서 arguments
의 0, 1, 2, 3, 4번째 인덱스에 무엇이 담기는지는 이 Node.js 문서를 참고하면 얼추 맞다
아무튼 결론은 node에서 화살표 함수의 arguments
가 얼추 동작한다고 해서 화살표 함수 내에서 arguments
를 사용하면 안된다는 것이다
화살표 함수의 인자를 담은 것이 아니라 전역 스코프의 정보가 담겨 있으므로 사실상 arguments가 존재하지 않다고 봐야 맞고, 따라서 함부로 쓰면 에러가 이놈한다
결론
화살표 함수가 쓰기 편해서 (…) 온갖 컴포넌트나 함수에 남용했었는데, 함부로 쓰면 안된다는 생각을 좀 했고 반성했다
지금까지 진행했던 프로젝트에선 함수 내에서 this
를 건드릴 일이 좀처럼 없어 화살표 함수와 일반 함수의 차이점도 아예 모르고 있었는데, this
를 사용하는 경우가 있었더라면 큰일날 뻔했다
지금이라도 알게 되었으니 간단한 콜백 함수 작성 외에는 가급적 일반 함수를 사용하도록 습관을 들여야겠다
참고자료
'Javascript + Typescript > 이론과 문법' 카테고리의 다른 글
모듈과 모듈 번들러 (0) | 2022.09.12 |
---|---|
콜 스택 (호출 스택) (0) | 2022.08.27 |
자바스크립트에서의 Symbol (0) | 2022.08.20 |
자바스크립트에서의 싱글톤 패턴과 static (0) | 2022.08.08 |
Set, Map (0) | 2022.07.30 |