모던자바스크립트 DeepDive : 12장 함수

te-ing·2022년 4월 22일
0
post-thumbnail

화살표 함수와 일반 함수의 차이점

  • 호이스팅이 되지 않음
  • 생성자 함수로 사용할 수 없음
  • 기존 함수와 this 바인딩 방식이 다름 (상위 객체의 this를 바인딩)
  • prototype 프로퍼티가 없으며 arguments 객체를 생성하지 않음

함수정의

함수는 객체지만 호출할 수 있으며 고유한 프로퍼티를 갖는다는 점에서 일반 객체와는 다르다.

함수정의에는 함수 선언문, 함수 표현식, Function 생성자 함수, 화살표 함수(ES6)가 있다.

함수 선언문

function add(x, y) {
	return x + y;
}
console.log(add(2, 5)); // 7

var add = function add(x, y) {
	return x + y;
};
console.log(add(2, 5)); // 7

함수 선언문은 표현식이 아닌 문이다. 따라서 함수 선언문은 변수에 할당할 수 없어야 한다. 하지만 함수 선언문을 위의 var add 처럼 변수에 할당하면 할당되는 것처럼 보이는데, 이는 자바스크립트 엔진이 코드 문맥에 따라 함수 리터럴을 표현식이 아닌 문인 함수 선언문으로 해석하였기 때문이다.

이때 함수 선언문이든 표현식이든 함수가 생성되는 것은 동일하지만, 함수를 생성하는 내부 동작에는 차이가 있다.

function foo() { console.log('foo'); }
foo(); // foo
(function bar() { console.log('bar'); }
bar(); // ReferenceError: bar is not defined

위 예제에서 단독으로 사용된 함수 리터럴(foo)은 함수 선언문으로 해석되는데, 그룹 연산자 () 내에 있는 함수 리터럴은 선언문으로 해석되지 않고 함수 리터럴 표현식으로 해석된다.

이러한 이유는 함수 이름은 함수 몸체 내에서만 참조할 수 있는 식별자 이기 때문에 외부에서는 함수 이름으로 호출할 수 없다는 것이다. 하지만 함수 선언문으로 정의된 함수가 foo로 호출될 수 있었던 이유는 자바스크립트 엔진이 생성된 함수를 호출하기 위해 함수 이름과 동일한 이름의 식별자(foo)를 암묵적으로 생성하고 거기에 함수 객체를 할당한 것이기 때문이다.

💡 요약하자면 함수 선언문으로 함수를 호출하는 것은 함수 이름으로 호출하는 것이 아닌, 함수 객체를 가리키는 식별자로 호출하는 것이다.


함수표현식

값의 성질을 가지기 때문에 값처럼 변수 할당도 가능하며, 프로퍼티 값이 될 수도 있으며, 배열의 요소가 될 수도 있는 객체를 일급 객체라 한다.

자바스크립트의 함수는 객체 타입의 값으로, 일급객체이므로 함수 리터럴로 생성한 함수 객체를 변수에 할당할 수 있다.

함수 리터럴의 함수 이름은 생략할 수 있는데, 함수 리터럴은 함수 이름을 생략하는 것이 일반적이다.

var add = function(x, y) { // 익명 함수 표현식 
var add = function foo (x, y) { // 기명 함수 표현식
	return x + y;
};
console.log(add(2, 5)); // 7
console.log(foo(2, 5)); // ReferenceError 함수 이름은 함수 내부에서만 유효한 식별자로, 함수이름으로 호출할 수 없음

함수 생성 시점과 함수 호이스팅

console.log(add(2, 5)); // 7 함수 선언문
console.log(sub(2, 5)); // TypeError: sub is not a function 함수 표현식
// 함수 선언문
function add(x, y) {
	return  x + y;
}
// 함수 표현식
var sub = function (x, y) {
	return x - y;
};

함수 선언문은 모든 선언문과 마찬가지로, 런타임 이전에 자바스크립트 엔진에 의해 먼저 실행되어 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고 생성된 함수 객체를 할당한다.

이처럼 함수 선언문이 코드의 선두로 끌어 올려진 것 처럼 동작하는 것을 함수 호이스팅 이라 한다.
변수 호이스팅은 선언된 변수가 undefined로 초기화되지만, 함수 호이스팅은 선언문 이전에도 호출 가능하다는 점에서 다르다. 함수 표현식은 변수에 할당되는 값이 함수 리터럴인 문으로, 런타임 동안 평가되어 함수 객체가 된다.
즉 함수 표현식으로 함수를 정의하면 함수 호이스팅이 아닌 변수 호이스팅이 발생한다.

💡 함수 호이스팅은 함수를 호출하기 이전에 반드시 함수를 선언해야 한다는 규칙을 무시하기 때문에 함수 선언문 대신 함수 표현식을 권장한다.


Function 생성자 함수

var add = new Function('x', 'y', 'return x + y');
console.log(add(2, 5)); // 7

Function 생성자 함수로 생성한 함수는 클로저를 생성하지 않는 등 함수 선언문이나 표현식으로 생성한 함수와 다르게 동작하는데, 일반적이지 않으며 바람직하지 않은 함수 정의 방식이다.


화살표 함수

const add = (x, y) => x + y;
console.log(add(2, 5)); // 7

ES6에 도입된 것으로, 기존의 함수보다 표현만 간략한 것이 아니라 내부 동작 또한 간략화 되어있다.

화살표 함수는 생성자 함수로 사용할 수 없으며, 기존 함수와 this 바인딩 방식이 다르며 prototype 프로퍼티가 없으며 arguments 객체를 생성하지 않는다.


함수 호출

매개변수와 인수

매개변수는 함수를 정의할 때 선언하며, 함수 내부에서 변수와 동일하게 취급된다. 인수가 부족해서 인수가 할당되지 않는다면 에러가 발생하지 않으며, 매개변수의 값은 undefined이다.

function add(x, y) {
	return x + y;
}
console.log(add(2)); // NaN

위와 같은 상황에서는 매개변수 y에 해당하는 인수가 없기 때문에 y는 undefined로 초기화된 상태 그대로이며, x + y는 2 + undefined가 되어 NaN이 반환된다.

console.log(add(2, 5, 10)); 과 같이 초과된 인수는 무시된 채 7이 반환되며, 초과된 인수는 arguments의 객체 프로퍼티에 보관된다.

매개변수의 이상적인 개수는 0개이며 적을수록 좋다.

💡 이상적인 함수는 한 가지 일만 해야 하며 가급적 작게 만들어야 하기 때문에 매개변수는 최대 3개 이상을 넘지 않는 것을 권장한다.


참조에 의한 전달(얕은 복사)과 외부 상태의 변경

function changeVal (primitive, obj) {
	primitive += 100;
	obj.name = 'Kim';
}
var num = 100;
var person = { name: 'Lee'};
changeVal(num, person);;
console.log(num); // 100 원시값은 원본이 훼손되지 않음
console.log(person); // {name: "Kim"} 객체는 원본이 훼손된다.

매개변수도 함수 내부에서 변수와 같이 취급되므로 역시 깊은복사, 얕은복사를 그대로 따른다.

값 자체가 복사되어 매개변수로 전달되는 원시 타입 인수와는 달리 객체 타입 인수는 참조 값이 복사되어 전달되기 때문에 참조 값을 통해 객체를 변경할 경우 원본이 훼손된다.

이렇게 함수가 외부 상태를 변경하는 것은 상태 변화를 추적하기 어려워지므로 지양해야 한다. 이러한 문제의 해결 방법 중 하나는 객체의 복사본을 만들어 객체를 불변객체로 만들어 사용하는 것이다. 만약 객체의 상태 변경이 필요한 경우 원본객체를 깊은 복사하여 새로운 객체를 생성하고 재할당하여 교체해야 한다.

즉시 실행 함수

(function () {
	var a = 3;
	var b = 5;
	return a * b;
}());

함수 정의와 동시에 즉시 호출되는 함수인 즉시 실행 함수는 위처럼 익명함수를 사용하는 것이 일반적이며, 즉시 실행 함수는 단 한 번만 호출되며 다시 호출할 수 없다.

즉시 실행 함수는 먼저 함수 리터럴을 평가하여 함수 객체를 생성해야하기 때문에 반드시 그룹연산자()로 감싸야만 한다. 그렇지 않으면 에러가 발생한다.

중첩함수

함수 내부에 정의된 함수를 중첩함수 혹은 내부함수라고 한다. ES6부터 함수 정의는 문이 위치할 수 있는 문맥이라면 어디든지 가능하지만 호이스팅으로 인한 혼란이 발생할 수 있으므로 if문이나 for문 등의 코드 블록에서 함수선언문을 사용하는 것은 바람직하지 않다.


콜백함수

function repeat(n, f) {
	for (var i = 0; i < n; i++) {
		f(i); // i를 전달하면서 f를 호출
	}
}
var logAll = function(i) {
	console.log(i);
};
repeat(5, logAll); // 0 1 2 3 4 5 logAll를 인수로 전달
var logOdds = function(i) {
	if (i % 2) console.log(i);
};
repeat(5, logOdds); // 1 3 logOdds를 인수로 전달

함수의 매개변수를 통해 다른 함수의 내부로 전달되는 함수를 콜백함수 라고 하며, 매개변수를 통해 콜백 함수를 전달받은 함수를 고차함수 라고 한다. 콜백함수는 고차함수에 의해 호출되며 고차함수는 필요에 따라 콜백함수에 인수를 전달할 수 있다.

위 repeat 함수는 경우에 따라 변경되는 일을 함수 f로 추상화한 것이며, repeat를 고차함수, logAll, logOdds를 콜백 함수라고 볼 수 있다.

콜백함수로서 전달된 함수 리터럴은 고차 함수가 호출될 때 마다 평가되어 함수 객체를 생성하므로 콜백 함수를 다른곳에서 호출할 필요가 있거나 해당 함수가 자주 호출된다면 콜백 함수를 정의한 후 함수 참조를 고차 함수에 전달하는 편이 효율적이다.

콜백함수가 활용되는 예시

// 콜백 함수를 사용하는 비동기 처리
document.getElementById('MyButton').addEventListener('click', function() {
	console.log('button clicked!');
});
setTimeout(function () {
	console.log('1초 경과');
}, 1000);
// 콜백 함수를 사용하는 고차 함수
var res = [1, 2, 3].map(function (item) {
	return item * 2;
});
console.log(res); // [2, 4, 6]

순수함수와 비순수 함수

var count = 0;
// 순수함수
function increase(n) {
	return ++n;
}
count = increase(count);
console.log(count); // 1

// 비순수 함수
function increase() {
	return ++count; // 외부 상태에 의존하여 외부 상태를 변경한다.
}
increase(); // 외부 상태를 변경하므로 상태 변화를 추적하기 어려워진다.
console.log(count); // 1

함수형 프로그래밍에서 외부 상태에 의존하지도, 변경되지도 않는 함수를 순수 함수라고 하고, 그렇지 않은 함수를 비순수 함수 라고 한다.

순수함수는 동일한 인수가 전달되면 언제나 동일한 값을 반환(멱등성)하며, 오직 매개변수를 통해 전달된 인수만을 의존해 반환값을 생성한다

profile
병아리 프론트엔드 개발자🐣

0개의 댓글