과거의 내가 미래의 나에게
딥다이브 :: 함수 본문
「딥다이브 자바스크립트」 정리 - 함수
※ 아래 내용은 책을 통해 학습한 것을 개인적으로 정리한 것으로 내용이 다소 부정확 할 수 있습니다.
함수란
함수는 일련의 과정을 문으로 구현하고 코드 블록으로 감싸서 하나의 실행 단위로 정의한 것이다. 함수 내부로 입력을 전달 받는 변수를 매개변수, 호출 시 입력하는 부분은 인수, 출력을 반환값이라 부른다. 함수는 함수 정의를 통해 생성하고 호출을 통해 실행한다.
함수는 객체 타입의 값이므로 식별자 이름을 붙일 수 있는데 함수 이름은 자신을 잘 설명할 수 있도록 해야한다.
함수 리터럴
함수는 객체 타입의 값이므로 함수도 함수 리터럴로 생성할 수 있다.
var test = function add(x){
return x + 1
}
함수 이름(add)은 식별자이므로 식별자 네이밍 규칙을 준수해야한다. 또한 함수 이름은 함수 몸체 내에서만 참조할 수 있는 식별자이고 함수 이름은 생략 가능하다.
리터럴은 값을 생성하는 표기이므로 함수 리터럴도 평가되어 값을 생성하며 이 값은 객체이다. 즉, 함수는 객체라 할 수 있다. 하지만 함수는 호출할 수 있고 함수 객체만의 고유한 프로퍼티를 갖고있기에 일반 객체와는 다르다.
함수 정의
변수는 선언하지만 함수는 정의한다고 한다. 함수 선언문이 평가되면 식별자가 암묵적으로 생성되고 함수 객체가 할당된다. 따라서 함수는 정의한다라고 표현한다.
함수는 다양한 방법으로 정의할 수 있는데, 함수 선언문, 함수 표현식, function 생성자 함수, 화살표 함수가 그 종류이다.
1. 함수 선언문
함수를 생성 시 js 엔진은 함수 선언문을 해석해 함수 객체를 메모리에 생성해놓는다.
함수 선언문은 함수 이름을 생략할 수 없다. 이 때 함수의 이름은 함수 내부에서만 활성화되는 식별자이므로 이 함수를 부르려면 별도의 이름이 필요하다. 따라서 js 엔진은 이 생성된 함수를 호출하기 위해 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고 거기에 함수 객체를 할당한다. 즉, 함수는 함수 이름으로 호출하는 것이 아닌, 함수 객체를 가리키는 식별자로 호출되는 것이다.
let test = function test(x){
return x
}
test(2) // 2
함수 선언문은 문이기에 변수에 할당 할 수 없다. 하지만 위와 같은 경우 분명 함수 선언문인데 test라는 변수에 할당되어 값이 출력되었다. 그 이유는 무엇일까?
함수 선언문은 함수 리터럴과 형태가 동일하다. 이는 함수 이름이 있는 기명 함수는 함수 선언문으로도 함수 리터럴로도 해석될 가능성이 있다는 것이다. 그렇기에 js 엔진은 코드의 문맥에 따라 해석을 달리 하는 것이다. 그렇기에 위의 함수 선언문은 함수 리터럴로 해석되어 정상적으로 출력된 것이다.
참고로 함수 선언문은 표현식이 아니라 문이기에 함수 선언문을 콘솔로 실행하면 평가되지 않아 undefined가 나온다.
2. 함수 표현식
함수는 객체 타입의 값이다. 함수는 값처럼 변수에 할당도 하고 프로퍼티 값이 될 수도있고 배열의 요소도 될 수 있다. 이처럼 값의 성질을 갖는 객체를 일급 함수라고 한다.
함수는 일급 객체이므로 함수 리터럴로 생성한 함수 객체를 변수에 할당할 수 있다. 이러한 함수 정의 방식을 함수 표현식이라 한다.
함수 리터럴에서 함수 이름은 생략할 수 있는데 이렇게 이름을 생략하는 것이 일반적이다. 만약 함수 이름이 있는 함수 표현식이라면 호출 시 함수 이름이 아닌 식별자 이름으로 호출해야한다.
*** 함수 선언문과 함수 표현식의 차이
함수 선언문에서는 함수 이름으로 암묵적으로 식별자를 생성하고 함수 객체가 할당되므로 함수 표현식과 유사하게 동작하는 것처럼 보이지만 정확히 동일하게 동작하지는 않는다. 함수선언문은 표현식이 아닌 문이고 함수 표현식은 표현식인 문이기에 그 차이가 발생한다.
console.log(test1(1)) // 2
console.log(test2(1)) // TypeError: test2 is not a function
// 함수 선언문
function test1(x){
return x + 1
}
//함수 표현식
let test2 = function (x){
return x + 1
}
함수 선언문으로 정의한 함수는 함수 선언문 이전에 호출가능하고, 함수 표현식으로 정의한 함수는 함수 표현식 이전에 호출 불가능하다. 이는 두 함수의 생성시점이 다르기 때문이다.
모든 선언이 런타임 이전에 실행되는데 이는 함수 선언문도 마찬가지이다. 함수 선언문은 런타임 이전에 함수 객체가 생성되고 함수 이름과 동일한 식별자를 암묵적으로 생성하고 생성된 함수 객체를 할당한다. 따라서 런타임이 진행될 때는 이미 함수 객체가 생성되어 있고 할당까지 완료 된 상태이기에 함수 선언문 이전에 함수를 참조할 수 있고 호출도 할 수 있는 것이다.
함수 표현식은 var 키워드로 선언된 변수에 함수 객체를 담는 것인데, var 키워드 역시 호이스팅 되지만 함수 객체를 담는 것이 아닌 undefined로 초기화한다. 그리고 할당문이 실행되는 시점에서야 함수 객체가 되므로 함수 표현식 이전에는 호출할 수 없는 것이다. 따라서 함수 표현식으로 정의한 함수는 반드시 함수 표현식 이후에 참조 또는 호출해야한다.
함수는 호출하기 전에 반드시 선언해야한다는 당연한 규칙이 있다. 그러나 함수 선언문으로는 이러한 규칙이 무시되기에 함수 표현식으로 사용할 것을 권장한다.
3. Function 생성자 함수
빌트인 함수인 Function 생성자 함수에 매개변수 목록과 함수 몸체를 문자열로 전달하면서 new 연산자와 함께 호출하면 함수 객체를 생성해서 반환한다. 그러나 이 방식은 일반적이지도 않고 바람직하지도 않는다고 한다. 생성자 함수로 생성한 함수는 함수 선언문이나 함수 표현식으로 생성한 함수와 다르게 동작한다는 정도만 알아두랜다.
var test = new Function('x','return x + 1')
console.log(test(1)) //2
4. 화살표 함수
화살표 함수는 화살표를 사용해 간략한 방법으로 표현할 수 있게 했는데 표현만 간략한 것이 아니라 내부 동작 또한 간략화되어 있다.
const test = (x) => x + 1
console.log(test(x)) //2
화살표 함수는 생성자 함수로 사용할 수 없고 this 바인딩 방식도 다르며 프로토타입 프로퍼티가 없으며 argument 객체를 생성하지도 않는다. 이는 추후에 보강하도록 하겠다.
함수 호출
1. 매개변수와 인수
함수 외부에서 함수 내부로 값을 전달할 필요가 있는 경우 매개변수를 통해 인수를 전달하게 된다.
함수는 매개변수와 인수의 개수가 일치하는지 체크하지 않는다. 매개변수가 인수보다 많으면 나머지 매개변수는 초기화때 넣어진 undefined이다. 반대로 인자가 매개변수보다 많으면 초과된 인수는 무시되는 것처럼 보이지만 사실 모든 인수는 암묵적으로 argument 객체의 프로퍼티로 보관된다. argument 객체는 함수 정의 시 매개변수 개수를 확정할 수 없는 가변 인자 함수를 구현할 때 유용하게 사용된다. 이는 후에 더 자세히 알아보겠다.
2. 인수 확인의 중요성
js는 동적 타입 언어이기에 매개변수나 인수의 타입을 명확하게 하지 않고 또한 매개변수 개수와 인수 개수가 일치하는지 확인하지 않는다. 이와 같은 불확실성은 원하는 코드를 얻지 못할 가능성도 높다는 뜻이다. 이를 방지하지 위해 다양한 방법이 있다.
- 인수의 타입 체크
아래와 같이 에러를 체크하는 방법도 있지만 부적절한 호출을 사전에 방지할 수는 없고 런타임 중에 에러가 발생하게 된다. 따라서 이를 방지하기 위해 타입스크립트와 같은 정적 언어를 쓰는 것도 하나의 방법일 것이다.
function test(x){
if(typeof x !== 'number'){
throw new TypeError('인수는 숫자여야합니다')
}
return x + 1
}
- argument 객체를 통한 인수 갯수 확인
- 단축 평가를 사용한 매개변수에 기본값 할당
- 매개변수 기본값 사용
3. 반환문
반환문은 함수의 실행을 중단하고 함수 몸체를 빠져나가는 역할과 return 뒤에 오는 표현식을 평가해 반환한다.
return 뒤의 표현식을 명시적으로 지정하지 않으면 undefined가 반환된다. 반환문 자체를 생략해도 undefined가 반환된다.
return과 표현식 사이에는 줄바꿈이 있어서는 안된다.
참조에 의한 전달 주의하기
매개변수는 함수 몸체 내부에서 변수와 동일하게 취급된다. 그러므로 매개변수 또한 원시값과 객체 모두 받을 수 있다. 원시값을 받는다면 원시값은 변경 불가능한 값이기고 또 함수 몸체 내에서 재할당이 되기 때문에 원본 원시값에 영향이 가지 않아 문제의 소지가 없지만, 객체를 받게 된다면 이는 객체 자체를 받는 것이 아닌 참조값을 받는 것이기에 함수 내부에서 객체를 변경하면 원본 객체 자체가 변경되는 위험성이 있다.
의도치 않는 객체의 변경을 피하기 위해 옵저버 패턴 등을 통해 객체 변경 시 이를 참조하고 있는 모든 이들에게 변경 사실을 통지하고 이에 대처하는 추가 대응을 마련하거나, 깊은 복사를 통해 새로운 객체를 생성하고 재할당을 통해 교체하는 방법 등이 잇다. 그러나 객체를 새로 생성하는 것은 비용이 어느정도 든다는 것도 기억해두자.
다양한 함수 형태
1. 즉시 실행 함수
함수 정의와 동시에 즉시 호출되고 이는 단 한 번만 호출되며 다시는 호출할 수 없다. 즉시 실행 함수는 익명 함수를 사용하는 것이 일반적이다. 기명 함수도 사용할 수 있지만 그룹 연산자 내의 기명 함수는 함수 리터럴로 평가되고 함수 이름은 함수 몸체 내부에서만 참조할 수 있는 것이므로 외부에서 다시 호출할 수는 없다.
(function () {
//...
}())
즉시 실행 함수는 반드시 그룹 연산자로 감싸야하는데 그렇지 않고 바로 함수 뒤에 ()를 붙여 호출한다면, 기명 함수일 시, 함수 선언문이 끝나는 위치에 세미콜론이 암묵적으로 자동 삽입되기에 ()는 함수 호출 연산자가 아니라 그냥 그룹 연산자로 해석되어 에러가 난다.
그룹 연산자로 함수를 묶는 이유는 연산자의 내부는 값이 들어간다. 그리고 함수 선언문을 연산자 내부에 넣으면 js 엔진이 문맥을 읽어 함수 리터럴로 평가하여 함수 객체를 생성하게 된다. 따라서 함수 객체를 생성할 수 있다면 그룹 연산자 이외의 연산자(+,!…)를 사용해도 좋지만 가장 일반적인 방법은 그룹 연산자이다.
즉시 실행 함수도 일반 함수처럼 값을 반환할 수도 있고 인수를 전달할 수도 있다.
2. 재귀 함수
함수 이름은 함수 몸체 내부에서만 유효하다 했는데, 이를 이용하여 함수 내부에서 함수 이름을 통해 자기 자신을 호출하는 것이 재귀함수이다. 물론 함수를 가리키는 식별자로도 자신을 호출할 수 있다.
재귀함수는 무한으로 자기 자신을 호출하기에 반드시 탈출 조건을 만들어야한다.
3. 중첩 함수
4. 콜백 함수
js의 함수는 일급 객체이기에 삼수의 매개변수를 통해 함수를 전달할 수 있다. 함수의 매개변수를 통해 다른 함수의 내부로 전달되는 함수를 콜백함수라고 한다. 그리고 매개 변수를 통해 함수의 외부에서 콜백 함수를 전달 받은 함수를 고차 함수라고 한다.
고차 함수 내에서 콜백 함수를 원하는 때에 호출하게 된다. 이 때 고차 함수는 콜백함수에 인수를 전달할 수도 있다.
function test(n, func) {
for(var i = 0; i<n; i++){
func(i)
}
}
test(5, function(i) {
if(i === 1) {
console.log('1입니다')
}
})
// 익명 함수로 콜백 함수
// test 함수를 실행 할 때 마다 콜백 함수가 생성된다
var call = function(i) {
if(i === 1) {
console.log('1입니다')
}
}
test(5, call)
// 외부에서 함수 정의 후 콜백 함수
// 콜백 함수는 한번만 생성되고 test에 함수 참조만을 전달한다
순수 함수와 비순수 함수
순수함수는 외부 상태에 의존하지 않고 또 외부 상태에 영향을 주지도 않는 함수이다. 그저 외부에서 전달된 인수를 받아 동일한 로직으로 처리하여 반환해내는 것이다.
비순수함수는 외부 상태에 의존하기도 하고 또 외부 상태를 변경 시키는 부수효과가 있는 함수이다.
함수가 외부 상태를 변경하면 상태 변화를 추적하기 어려워진다. 그러므로 함수 외부 상태 변경을 지양하는 순수함수를 추구하는 것이 바람직하다.
함수형 프로그래밍은 순수 함수와 보조 함수의 조합을 통해 외부 상태를 변경하는 부수 효과를 최소화하여 불변성을 지향하는 프로그래밍 패러다임이다. js는 멀티 패러다임 언어이므로 객체지향 프로그래밍뿐만 아니라 함수형 프로그래밍을 적극적으로 활용하고 있다.
'js 이론' 카테고리의 다른 글
딥다이브 :: let, const (0) | 2023.01.24 |
---|---|
딥다이브 :: 스코프 (0) | 2023.01.24 |
딥다이브 :: 객체 리터럴 & 원시 값과 객체의 비교 (1) | 2023.01.13 |
script 태그의 삽입 위치에 대하여 (0) | 2023.01.06 |
딥다이브 :: 데이터 타입 & 연산자 & 제어문 & 단축평가 (0) | 2023.01.01 |