12장 함수
12.1 함수란?
- 일련의 과정을 문(statement)으로 구현하고 코드 블록으로 감싸서 하나의 실행 단위로 정의한 것.
- 함수 내부로 입력을 전달받는 변수를 매개변수, 입력을 인수, 출력을 반환값이라 한다.
12.2 함수를 사용하는 이유
- 함수는 몇 번이든 호출할 수 있으므로 코드의 재사용이라는 측면에서 매우 유용
- 잘 정의된 함수는 유지보수의 편의성을 높이고 실수를 줄여 코드의 신뢰성을 높이는 효과
- 함수는 객체 타입의 값으로, 이름을 붙일 수 있으며, 함수 이름은 함수 자신의 역할을 잘 설명해야 한다. → 함수의 내부 코드를 이해하지 않고도 함수의 역할 파악 가능. 코드의 가독성 향상
12.3 함수 리터럴
- 자바스크립트의 함수는 객체 타입의 값
- 함수 리터럴은 function 키워드, 함수 이름, 매개변수 목록, 함수 몸체로 구성
// 변수에 함수 리터럴을 할당 var f = fucntion add(x, y) { return x + y; }
- 함수 리터럴의 자세한 구성 요소는 157p 참고
- 함수 이름은 함수 몸체 내에서만 참조할 수 있는 식별자다.
- 리터럴은 사람이 이해할 수 있는 문자 또는 약속된 기호를 사용해 값을 생성하는 표기 방식(5.2절 “리터럴” 참고)
- 즉, 리터럴은 값을 생성하기 위한 표기법이며, 이 값은 객체로, 함수는 객체다.
- 일반 객체는 호출할 수 없지만 함수는 호출 가능. 함수는 일급 객체이다.
12.4 함수 정의
- 함수를 호출하기 이전에 인수를 전달받을 매개변수와 실행할 문들, 반환할 값을 지정하는 것을 말한다.
- 정의된 함수는 자바스크립트 엔진에 의해 평가되어 함수 객체가 된다.
- 함수 정의에는 4가지 방법이 존재
// 함수 선언문. 함수 이름 생략 불가능 function add(x, y) { return x + y; } // 함수 표현식. var add = fucntion(x, y) { return x + y; } // Function 생성자 함수 var add = new Function('x', 'y', 'return x + y'); // 화살표 함수(ES6) var add = (x, y) => x + y;
- 함수 선언문은 표현식이 아닌 문. 함수 선언문을 콘솔에서 실행하면 완료 값 undefined가 출력
- 함수 선언문과 함수 리터럴 표현식의 차이
// 기명 함수 리터럴을 단독으로 사용하면 함수 선언문으로 해석 // 함수 선언문에서는 함수 이름을 생략할 수 없다. function foo() { console.log('foo'); } foo(); // foo // 함수 리터럴을 피연산자로 사용하면 함수 선언문이 아니라 함수 리터럴 표현식으로 해석 // 함수 리터럴에서는 함수 이름을 생략할 수 있다. (function bar() { console.log('bar'); }); bar(); // ReferenceError: bar is not defined;
- foo는 함수 선언문으로 해석, 그룹 연산자 () 내에 있는 bar는 함수 리터럴 표현식으로 해석된다.
- 그룹 연산자의 피연산자는 값으로 평가될 수 있는 표현식이어야 하므로, 표현식이 아닌 문인 함수 선언문은 피연산자로 사용할 수 없다.
- 함수 이름은 함수 몸체 내에서만 참조할 수 있는 식별자이므로, 함수를 가리키는 식별자가 없는 것과 마찬가지이다. 따라서 위 예제의 bar 함수는 호출할 수 없다.
- 그렇다면 foo는 어떻게 실행되는 것인가? → foo는 자바스크립트 엔진이 암묵적으로 생성한 식별자이다.
- 자바스크립트 엔진은 생성된 함수를 호출하기 위해 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고, 거기에 함수 객체를 할당한다.
함수 선언문을 의사 코드로 표현하면 다음과 같다.
var add = function add(x, y) { return x + y; } console.log(add(2, 5)); // 7
- 함수는 함수 이름으로 호출하는 것이 아니라 함수 객체를 가리키는 식별자로 호출한다.
- 즉, 함수 선언문으로 생성한 함수를 호출한 것은 함수 이름 add가 아니라, 자바스크립트 엔진이 암묵적으로 생성한 식별자 add이다.
- 위 의사 코드는 함수 표현식과 유사하다. 결론적으로 자바스크립트 엔진은 함수 선언문을 함수 표현식으로 변환해 함수 객체를 생성한다고 생각할 수 있다.
- 단, 함수 선언문과 함수 표현식이 정확히 동일하게 동작하는 것은 아니다.
// 기명 함수 표현식 var add = function foo (x, y) { return x + y; } // 함수 객체를 가리키는 식별자로 호출 console.log(add(2, 5)); // 7 // 함수 이름으로 호출하면 ReferenceError 발생 // 함수 이름은 함수 몸체 내부에서만 유효한 식별자 console.log(foo(2, 5)); // ReferenceError: foo is not defined
12.4.3 함수 생성 시점과 호이스팅
// 함수 참조 console.dir(add); // f add(x, y) console.dir(sub); // undefined // 함수 호출 console.log(add(2, 5)); //7 console.log(sub(2, 5)); // TypeError: sub is nor a function // 함수 선언문 function add(x, y) { return x + y; } // 함수 표현식 var sub = function(x, y) { return x - y; };
- 함수 선언문으로 함수를 정의하면 런타임 이전에 함수 객체가 먼저 생성되며, 자바스크립트 엔진이 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고 함수 객체를 할당한다. → 이처럼 함수 선언문이 코드의 선두로 끌어 올려진 것처럼 동작하는 자바스크립트 고유의 특징을 함수 호이스팅이라 한다.
- 함수 표현식은 변수에 함수 리터럴을 할당하는 것. 변수 할당문의 값은 할당문이 실행되는 시점, 즉 런타임에 평가되므로 함수 표현식의 함수 리터럴도 할당문이 실행되는 시점에 평가되어 함수 객체가 된다. → 따라서 함수 표현식으로 함수를 정의하면 함수 호이스팅이 발생하는 것이 아니라 변수 호이스팅이 발생한다.
- 함수 호이스팅은 함수를 호출하기 전에 반드시 함수를 선언해야 한다는 당연한 규칙을 무시하기 때문에 함수 선언문 대신 함수 표현식을 사용할 것을 권장한다.
12.5 함수 호출
12.5.1 매개변수와 인수
- 함수는 매개변수의 개수와 인수의 개수가 일치하는지 체크하지 X
- 인수가 부족해서 할당되지 않은 매개변수의 값은 undefined
- 매개변수보다 인수가 많은 경우 초과된 인수는 무시
- 그냥 버려지는 것은 아니고 모든 인수는 암묵적으로 arguments 객체의 프로퍼티로 보관
12.5.2 인수 확인
- 자바스크립트의 경우 함수를 정의할 때 적절한 인수가 전달되었는지 확인할 필요가 있음. 함수 내부에서 확인하더라도 부적절한 호출을 사전에 방지할 수는 없고 에러는 런타임에 발생하게 됨. → 타입스크립트로 사전에 방지 가능
- 또는 매개변수 기본값을 통해 인수 체크 및 초기화를 간소화 가능
12.5.3 매개변수의 최대 개수
- 엔진마다 조금씩 다르지만 충분히 많은 매개변수를 지정 가능
- 매개변수는 최대 몇 개까지 사용하는 것이 좋을까?
- 가장 이상적인 매개변수의 개수는 0개 → 매개변수의 개수가 많다는 것은 함수가 여러 가지 일을 한다는 증거이므로 바람직하지 않음. 이상적인 함수는 한 가지 일만 해야 하며 가급적 작게 만들어야 한다.
- 매개변수는 최대 3개 이상을 넘지 않는 것을 권장한다. 만약 그 이상의 매개변수가 필요하다면 객체를 인수로 전달하는 것이 유리
- 매개변수의 순서를 신경 쓰지 않아도 되고, 명시적으로 인수의 의미를 설명하는 키를 사용하게 되므로 코드의 가독성도 좋아지고 실수도 줄어든다. 단, 변경 시 발생하는 부수 효과에 주의
12.5.4 반환문
- return 키워드로 함수 실행 결과를 함수 외부로 반환할 수 있다.
- return 키워드 뒤에 표현식을 명시적으로 지정하지 않으면 undefined가 반환된다.
12.6 참조에 의한 전달과 외부 상태의 변경
- 매개변수에 원시 타입 인수는 원본이 훼손되지 않으며, 객체 타입 인수는 원본이 훼손된다. → 원시 값은 값 자체가 복사되어 전달되고, 객체는 참조 값이 복사되어 전달되기 때문
- 함수가 외부 상태를 변경하면 상태 변화를 추적하기 어려워진다. → 이는 코드의 복잡성을 증가시키고 가독성을 해치는 원인
- 객체의 변경을 추적하는 옵저버 패턴이나, 객체를 불변 객체로 만들어 사용하기, 외부 상태를 변경하지 않고 외부 상태에 의존하지도 않는 순수 함수의 사용 등으로 이러한 문제를 해결할 수 있음.
- 순수 함수를 통해 부수 효과를 최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이려는 프로그래밍 패러다임을 함수형 프로그래밍이라 한다.
12.7 다양한 함수의 형태
- 즉시 실행 함수
- 함수 정의와 동시에 즉시 호출, 단 한 번만 호출되며 다시 호출할 수 없다.
// 가장 일반적인 방법 (function () { // ... }());
- 재귀 함수
- 함수가 자기 자신을 호출하는 함수
- 반복되는 처리를 위해 사용 ex) 10부터 0까지 출력, 팩토리얼
- 탈출 조건을 반드시 만들어야 함. 없으면 스택 오버플로 에러 발생
- 중첩 함수
- 함수 내부에 정의된 함수. 내부 함수라고도 한다.
- 호이스팅으로 인해 혼란이 발생할 수 있으므로 if 문이나 for 문 등의 코드 블록에서 함수 선언문을 통해 함수를 정의하는 것은 바람직하지 않다.
- 콜백 함수
- 함수의 매개변수를 통해 다른 함수의 내부로 전달되는 함수를 콜백 함수, 매개변수를 통해 함수의 외부에서 콜백 함수를 전달받은 함수를 고차 함수라고 한다. → 고차 함수는 콜백 함수를 자신의 일부분으로 합성한다.
- 함수형 프로그래밍 패러다임뿐만 아니라 비동기 처리(이벤트 처리, Ajax 통신, 타이머 함수 등)에 활용되는 중요한 패턴이다.
// 콜백 함수를 사용한 이벤트 처리 // myButton 버튼을 클릭하면 콜백 함수를 실행한다. document.getElementById('myButton').addEventListener('click', function() { console.log('button clicked!'); }); // 콜백 함수를 사용한 비동기 처리 // 1초 후에 메시지를 출력한다. setTimeout(function () { console.log('1초 경과'); }, 1000);
- 순수 함수와 비순수 함수
- 어떤 외부 상태에 의존하지도 않고 변경하지도 않는, 즉 부수 효과가 없는 함수를 순수 함수, 외부 상태에 의존하거나 외부 상태를 변경하는, 즉 부수 효과가 있는 함수를 비순수 함수라고 한다.
- 순수 함수를 통해 부수 효과를 최대한 억제해 오류를 피하고 프로그램의 안정성을 높여야 한다.
13장 스코프
13.1 스코프란?
- 모든 식별자(변수 이름, 함수 이름, 클래스 이름 등)는 자신이 선언된 위치에 의해 다른 코드가 자신을 참조할 수 있는 유효 범위가 결정된다. 이를 스코프라 한다. 즉, 스코프는 식별자가 유효한 범위를 말한다.
- 스코프 내에서 식별자는 유일해야 하지만 다른 스코프에는 같은 이름의 식별자를 사용할 수 있다.
- var 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언이 가능
- let, const 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언을 허용하지 않는다.
13.2 스코프의 종류
구분 | 설명 | 스코프 | 변수 |
전역 | 코드의 가장 바깥 영역 | 전역 스코프 | 전역 변수 |
지역 | 함수 몸체 내부 | 지역 스코프 | 지역 변수 |
13.3 스코프 체인
- 스코프가 계층적으로 연결된 것
- 변수를 참조할 때 자바스크립트 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다.
- 상위 스코프에서 유효한 변수는 하위 스코프에서 자유롭게 참조할 수 있지만 하위 스코프에서 유효한 변수를 상위 스코프에서 참조할 수는 없다. → 상속과 유사
13.4 함수 레벨 스코프
- 코드 블록이 아닌 함수에 의해서만 지역 스코프가 생성된다는 의미
- var 키워드로 선언된 변수는 함수의 코드 블록(함수 몸체)만을 지역 스코프로 인정하지만 ES6에서 도입된 let, const는 블록 레벨 스코프를 지원한다.
13.5 렉시컬 스코프
var x = 1; function foo() { var x = 10; bar(); } function bar() { console.log(x); } foo(); // 1 bar(); // 1
- 함수를 어디서 호출했는지에 따라 함수의 상위 스코프를 결정한다. - 동적 스코프
- 함수를 어디서 정의했는지에 따라 함수의 상위 스코프를 결정한다. - 렉시컬 스코프, 정적 스코프
- 자바스크립트는 렉시컬 스코프를 따른다. 따라서 함수가 호출된 위치는 상위 스코프 결정에 어떠한 영향도 주지 않는다. 즉, 함수의 상위 스코프는 언제나 자신이 정의된 스코프다.
- 이처럼 함수의 상위 스코프는 함수 정의가 실행될 때 정적으로 결정된다. 함수 정의(함수 선언문 또는 함수 표현식)가 실행되어 생성된 함수 객체는 이렇게 결정된 상위 스코프를 기억한다.
퀴즈
- 빈 칸에 들어갈 내용을 순서대로 작성해 주세요. 가장 이상적인 매개변수의 개수는 [ ]개이며, 최대 [ ]개 이상을 넘지 않는 것을 권장한다. 그 이상의 매개변수가 필요하다면 [ ]를 인수로 전달해야한다.
- 다음 코드의 실행 결과는?
var x = 1; function foo() { var x = 10; bar(); } function bar() { console.log(x); } foo(); // ? bar(); // ?