자바스크립트의 일급 객체인 함수 정의하는 방법, 인수, 재귀 함수, 클로저, 고차함수, ES6의 새로운 기능 등에 대해 배웁니다.
1. 함수 정의하기
정의하는 방법
1) 함수 선언문으로 정의
function square(x) { return x*x; }
2) 함수 리터럴로 정의
var square = function(x) { return x*x; }
3) Function 생성자로 정의
var square = new Function("x", "return x*x");
4) 화살표 함수 표현식으로 정의 (ES6)
var square = x => x*x;
JS 엔진은 호출문 뒤에 함수 선언문이 있어도 프로그램이나 함수의 첫번째 줄로 끌어올려 호출할 수 있으나 함수 리터럴, Function 생성자, 화살표 함수 표현식으로 정의한 함수는 변수에 함수의 참조를 할당해야 사용할 수 있습니다. (=호출 코드보다 앞에 있어야 됩니다.)
중첩 함수
특정 함수 내부에 선언된 함수로 Nested Function이라고 합니다. 지역함수 또는 내부함수라고도 합니다. 외부 함수의 최상위 레벨에만 중첩 함수를 작성할 수 있습니다. 즉 함수 내 if문과 while 문 등의 문장 블록 안에는 중첩함수를 작성할 수 없습니다.
// 제곱합에 대한 제곱근 구하기
function norm(x) {
var sum2 = sumSquare();
return Math.sqrt(sum2); //제곱근 반환 함수
function sumSquare(){
sum=0;
for(var i=0; i<x.length; i++) sum+= x[i]*x[i];
// console.log(sum); //88
return sum;
}
}
var a = [2,1,3,5,7];
var n = norm(a);
console.log(n); // 9.380...
중첩 함수의 참조는 중첩함수를 둘러싼 외부 함수의 지역 변수에 저장되므로 외부 함수의 바깥에서는 읽고 쓸 수 없습니다.
또한 자신을 둘러싼 외부 함수의 인수와 지역 변수에 접근할 수 있다는 중요한 성질이 있습니다. 예제의 중첩함수 sumSquare는 변수 x를 사용합니다. 이 변수 x는 외부 함수인 norm 의 인수입니다.
외부 함수의 변수 유효 범위가 그 함수의 중첩함수에까지 미친다는 규칙은 클로저의 핵심 구성 요소가 됩니다.
2. 함수 호출하기
호출/실행하는 방법
1) 함수 호출 : 함수의 참조가 저장된 변수 뒤 그룹 연산자()를 붙여서 호출
var s = square(5);
2) 메서드 호출 : 객체의 프로퍼티에 저장된 값이 함수 타입일 때 그 프로퍼티를 메서드라고 부릅니다. 메서드를 호출할 때 그룹 연산자인 ()를 붙여서 호출합니다. (1)과 본질적으로 같습니다.
obj.m = function(){...};
obj.m();
3) 생성자 호출 : 함수나 메서드를 호출할 때 함수의 참조를 저장한 변수 앞에 new 키워드를 추가하면 함수가 생성자로 동작합니다.
var obj = new Object();
4) 함수의 call, apply 메서드를 사용한 간접 호출
즉시 실행 함수
익명함수를 실행할 때에는 익명 함수의 참조를 변수에 할당한 뒤에 그룹 연산자()를 붙여 실행합니다. 일반 함수를 익명함수를 정의하고 곧바로 실행하는 즉시실행함수로 바꾸겠습니다.
var f = function(){...};
f();
// 일반 함수
// 함수 정의식을 함수 값으로 변환
첫 번째, 두 번째 모두 사용 가능합니다. 즉시실행함수 구문에서 함수 정의식을 그룹 연산자()로 묶습니다. 그러면 괄호 안 함수 정의식이 평가되어 함수 값(함수 객체의 참조 값)으로 바뀝니다.
(function() {...})();
(function() {...}());
+function() {...}()
// 이렇게 사용해도 함수 정의식을 함수 값으로 만들 수 있다.
즉시실행함수도 인수를 넘길 수 있습니다.
(function(a,b) {...})(1,2); //a에 1, b에 2 대입
즉시실행함수에도 이름을 붙일 수 있지만 그 이름은 함수 내부에서만 유효합니다.
(function fact(n){
if(n<=1) return 1;
return n*fact(n-1);
})(5); //120
변수에 즉시실행함수 저장 : 함수 실행결과를 변수에 할당할 수 있으며 표현식 안에서 사용할 수 있습니다.
var x = (function() { ... } )();
즉시실행함수는 전역유효범위를 오염시키지 않는 이름 공간을 생성할 때 사용합니다.
3. 함수의 인수
인수 생략
함수 호출 시 인수 생략이 가능하며, 함수 정의식에 작성된 인자 개수보다 더 많은 개수의 인수를 넘겨서 함수를 실행할 수 있습니다.
함수 정의식에 작성된 인자 개수보다 인수를 적게 전달해서 함수를 실행하면 인수에서 생략한 인자는 undefined가 됩니다.
function f(x,y){
console.log("x: " + x + ", y: " + y);
}
f(2); // x:2, y:undefined
이를 이용하면 함수 호출 시 인수를 생략할 수 있는 함수를 정의할 수 있습니다. 함수 정의식에서 인수를 생략했을 때 사용할 초기값을 설정해줍니다.
function multiply(a, b){
b = b || 1; //초기값 1
return a*b;
}
multiply(2,3); //6
multiply(2); //1
논리합연산자 || 는 왼쪽 피연산자가 true면 왼쪽 피연산자를 반환하고, false면 오른쪽 피연산자를 반환합니다. 인자 b에 값을 넘기면 true가 되어 인자 b에 들어온 값을 사용하고 값이 없으면 undefined므로 false로 평가되어 1이 됩니다.
가변 길이 인수 목록 (Arguments 객체)
모든 함수에서 사용할 수 있는 지역변수로는 arguments 변수가 있습니다. arguments 변수의 값은 Arguments 객체입니다. 함수에 인수를 n개 넘겨서 호출하면 인수 값이 arguments에 저장됩니다.
arguments[0] : 첫 번째 인수 값
arguments[1] : 두 번째 인수 값
...
arguments[n-1] : n번째 인수 값
Arguments 객체는 프로퍼티로 length와 callee를 갖고 있으며 각각 다음과 같은 값이 담겨 있습니다. Arguments 객체는 배열이 아니고 유사배열객체입니다. (추가)
arguments.length : 인수 개수
arguments.callee : 현재 실행되고 있는 함수의 참조
arguments[i] 값을 바꾸면 i+1번째 인자가 있을 때 값이 함께 바뀝니다. arguments[1] 값을 바꾸면 함수의 인자 y 값이 같이 바뀝니다. 일반 배열은 이런 성질이 없습니다. arguments 변수를 활용하면 인수 개수가 일정하지 않은 가변 인수 함수를 정의할 수 있습니다.
function f(x, y) {
arguments[1] = 3;
console.log("x=" + x + ", y = " + y);
}
f(1, 2); // x = 1, y = 3
function myConcat(seperator) {
var s ="";
for(var i=1; i<arguments.length; i++){
s += arguments[i];
if(i<arguments.length-1) s+= seperator;
}
return s;
}
console.log(myConcat("/", "apple", "orange", "peach")); //apple/orange/peach
위 예제는 문자열을 연결하는 함수입니다.
유사배열객체인 arguments[]를 배열 객체로 변환하려면 배열메서드인 slice, call을 이용합니다.
var params = [].slice.call(argumetns);
4. 재귀함수
재귀 호출 (recursive call) : 함수가 자기자신을 호출하는 것
재귀함수는 재귀호출을 수행하는 함수.
// n의 팩토리얼을 구하는 함수
function fact(n){
if(n<=1) return 1;
return n*fact(n-1);
}
fact(5); //120
함수 fact를 리터럴로 정의하려면 함수 리터럴 표현식에 함수 이름을 적으면 되며, 이 함수 이름은 함수 안에서만 유효합니다.
var fact = function(n){
if(n<=1) return 1;
return n*f(n-1);
}
argument.callee를 사용하면 익명 함수도 재귀 호출을 할 수 있습니다. argument.callee 가 지금 실행중인 함수를 가리키기 때문입니다.
var fact = function(n){
if(n<=1) return 1;
return n*arguments.callee(n-1);
}
재귀 함수 정의의 규칙
1. 재귀 호출은 반드시 중간에 멈춰야 한다.
함수가 자신을 호출하면 무한 연쇄 호출로 이어지므로 프로그램이 멈추지 않을 수 있어 중간에 멈출 수 있게 해야 합니다.
2. 재귀 호출로 간단한 문제 해결이 가능할 때만 사용한다.
재귀 함수는 재귀 호출이 멈출 때까지 반복해서 자기자신을 호출합니다. 호출된 각각의 재귀함수는 메모리의 다른 영역을 사용합니다. 따라서 호출횟수만큼 메모리 소비량이 늘어납니다.
반복문을 재귀함수로 바꿔 표현할 수도 있지만, 재귀함수보다 while문이나 for문이 이해도 쉽고 메모리 공간도 적게 차지합니다. 따라서 꼭 필요할 때만 사용합니다.
재귀 함수 예시
퀵 정렬은 찰스 앤터니 리처드 호어가 발표한 정렬로 어떤 배열을 오름차순으로 정렬하는 알고리즘입니다. 퀵 정렬은 배열을 둘로 나눈 후 나눈 부분을 대상으로 퀵 정렬을 재귀적으로 반복합니다. 퀵 정렬 알고리즘은 다음을 따릅니다.
1. p값 이상인 요소의 개수와 p값 이하인 요소의 개수가 거의 같게끔 p값을 설정한다.
2. 배열 앞부분에는 p값 이상인 요소를 옮기고, 뒷부분에는 p값 이하인 요소를 옮긴다.
3. 배열 앞부분의 길이가 2 이상이면 그 부분을 대상으로 퀵 정렬을 한다.
4. 배열 뒷부분의 길이가 2 이상이면 그 부분을 대상으로 퀵 정렬을 한다.
// x : 정렬할 배열
// first : 정렬할 첫 번째 요소의 위치
// last : 정렬할 마지막 요소의 위치
function quicksort(x, first, last) {
var p = x[Math.floor((first + last) / 2)];
for (var i = first, j = last; ; i++, j--) {
while(x[i]<p) i++; // 왼쪽부터 차례로 p 이상의 요소를 검색
while(p<x[j]) j--; // 오른쪽부터 차례로 p 이하의 요소를 검색
if(i>=j) break; // i와 j가 교차하면 다음으로 이동
var w = x[i]; x[i] = x[j]; x[j] = w; // 발견하면 x[i]와 x[j]를 교환한다
}
if(first<i-1) quicksort(x, first, i-1); // 왼쪽에 두 개 이상 남았으면 왼쪽을 다시 정렬
if(j+1<last) quicksort(x, j+1, last); // 오른쪽에 두 개 이상 남았으면 오른쪽을 다시 정렬
}
var a = [7, 2, 5, 1, 8, 9, 3];
quicksort(a, 0, a.legnth-1);
console.log(a); // [1,2,3,5,7,8,9]
** 코드 오류 찾아보기
5. 프로그램의 평가와 실행 과정
실행 가능한 코드
JS 엔진은 실행 가능한 코드 (Executable Code) 를 만나면 그 코드를 평가(Evaluation)해 실행 문맥으로 만듭니다. 실행 가능한 코드의 유형은 전역 코드, 함수 코드, eval 코드 (eval 함수)가 있습니다.
전역 코드는 전역 객체 Window 아래에 정의된 함수, 함수 코드는 함수, eval 코드는 eval 함수입니다.
JS 엔진이 실행 가능한 코드의 유형을 분류하는 이유는 실행 문맥을 초기화하는 환경과 과정이 다르기 때문이며, 특히 eval 코드는 Lexical Environment 이 아니라 별도 동적 환경에서 실행됩니다.
실행 문맥의 구성
실행 문맥 (Execution Context)은 실행가능한 코드가 실제로 실행되고 관리되는 영역으로 실행에 필요한 모든 정보를 컴포넌트 여러 개가 나누어 관리하도록 만들어져 있습니다. 가장 중요한 컴포넌트는 아래 3가지입니다.
-
LexicalEnvironment (렉시컬 환경) 컴포넌트
-
VariableEnvironment(변수 환경) 컴포넌트
-
This Binding 컴포넌트
// 객체 표현을 빌린 의사 코드 (실행 불가)
// 실행 문맥
ExecutionContext = {
// 렉시컬 환경 컴포넌트
LexicalEnvironment: {},
// 변수 환경 컴포넌트
VariableEnvironment: {},
// 디스 바인딩 컴포넌트
ThisBinding: null,
}
렉시컬 환경 컴포넌트와 변수 환경 컴포넌트는 렉시컬 환경 타입의 컴포넌트입니다. 둘은 타입이 같고 with문을 쓸 때 외에는 내부 값이 같아 똑같이 취급하며, 설명도 통일합니다. 디스 바인딩 컴포넌트는 함수를 호출한 객체의 참조가 저장되는 곳입니다. 이것이 가리키는 값이 곧 해당 실행 문맥의 this가 됩니다.
LexicalEnvironment (렉시컬 환경) 컴포넌트의 구성
* lexical : 어휘
실행 문맥의 구성 요소인 렉시컬환경 컴포넌트는 JS 엔진이 JS 코드를 실행하기 위해 자원을 모아둔 곳으로
함수나 블록의 유효 범위 안에 있는 식별자와 그 결과값이 저장되는 곳
JS 엔진은 해당 JS 코드의 유효 범위 안에 있는 식별자와 그 식별자가 가리키는 값을 키/값의 쌍으로 바인드(묶다)해서 렉시컬 환경 컴포넌트에 기록합니다.
// JS의 객체 표현을 빌린 의사 코드
LexicalEnvironment: {
// 환경 레코드
EnvironmentRecord: {},
// 외부 렉시컬 환경 참조
OuterLexicalEnvironment Reference: {}
}
렉시컬 환경 컴포넌트는 환경 레코드와 외부 렉시컬 환경 참조 컴포넌트로 구성되어 있습니다.
환경 레코드 : 유효 범위 안에 포함된 식별자를 기록하고 실행하는 영역
ECMAScript 3의 변수 객체(Variable Object)와 비슷한 역할입니다.
JS 엔진은 유효 범위 안의 식별자와 결과값을 바인드해서 환경 레코드에 기록합니다.
외부 렉시컬 환경 참조 : JS 는 함수 안에 함수를 중첩해서 정의할 수 있는 언어입니다. 따라서 JS 엔진은 유효 범위 너머의 유효 범위도 검색할 수 있어야 합니다. 외부 렉시컬 환경 참조 Outer Lexical Environment Reference에는 함수를 둘러싸고 있는 코드가 속한 렉시컬 환경 컴포넌트의 참조가 저장됩니다.
중첩 함수 내에서 바깥 코드에 정의된 변수를 읽거나 써야할 때, JS 엔진은 외부 렉시컬 환경 참조를 따라 한 단계씩 렉시컬 환경을 거슬러 올라가 그 변수를 검색합니다.
환경 레코드의 구성
렉시컬 환경 컴포넌트의 구성 요소인 환경 레코드(Environment Record)는 렉시컬 환경 안의 식별자와 이 식별자가 가리키는 값의 묶음이 실제 저장되는 영역입니다.
이 환경 레코드는 선언적 환경 레코드와 객체 환경 레코드로 구성되어 있으며 저장하는 값의 유형에 따라 쓰임새가 달라집니다.
// JS의 객체 표현을 빌린 의사 코드로 환경 레코드 표현
EnvironmentRecord: {
// 선언적 환경 레코드
DeclarativeEnvironmentRecord: {},
// 객체 환경 레코드
ObjectEnvironmentRecord: {}
}
선언적 환경 레코드
Declarative Environment Record는 실제로 함수와 변수, catch 문의 식별자와 실행 결과가 저장되는 영역입니다.
객체 환경 레코드
선언적 환경 레코드가 식별자와 실행결과를 키와 값의 쌍으로 관리하는 반면,
객체 환경 레코드 (object environment records)는 실행 문맥 외부에 별도로 저장된 객체의 참조에서 데이터를 읽거나 씁니다.
with 문의 렉시컬 환경이나 전역 객체처럼 별도의 객체에 저장된 데이터는 그 객체가 가진 키와 값의 쌍을 복사해 오는 것이 아니라 그 객체 전체의 참조를 가져와서 객체 환경 레코드의 bindObject라는 프로퍼티에 바인드하도록 만들어져 있습니다.
전역 환경과 전역 객체의 생성
JS 인터프리터는 시작하자마자 렉시컬 환경 타입의 전역 환경을 생성합니다. 웹 브라우저에 내장된 JS 인터프리터는 새 웹 페이지를 읽어들인 후 전역 환경을 생성합니다.
그리고 전역 객체를 생성한 후 전역 환경의 객체 환경 레코드에 전역 객체의 참조를 대입합니다. 전역 객체에는 undefined, NaN, INfinity와 같은 프로퍼티가 있으며 최상위 레벨 (함수 바깥에 있는 코드)의 this는 전역 객체를 가리킵니다.
this === window //true
이 코드의 상태를 의사 코드로 표현하면 다음과 같습니다.
// 전역 환경
GlobalEnvironment = {
ObjectEnvironmentRecord:{
bindObject: window
},
OuterLexicalEnvironmentReference: null
}
// 전역 실행 문맥
ExecutionContext = {
LexicalEnvironment: GlobalEnvironment,
ThisBinding: window,
}
웹브라우저의 JS 실행 환경에서는 Window 객체가 전역 객체이므로 객체 환경 레코드의 bindObject 프로퍼티에는 전역 객체 Window의 참조가 할당됩니다. 이로 인해 전역 환경의 변수와 함수를 Window 안에서 검색하게 됩니다. 또한 전역 환경의 외부에는 다른 렉시컬 환경이 없으므로 외부 렉시컬 환경 참조에는 null을 할당합니다.
전역 실행 문맥의 디스 바인딩 컴포넌트에도 Window의 참조가 할당되어 전역 실행 문맥의 this가 Window를 가리키게 되고, 전역 실행 문맥의 프로퍼티를 디스 바인딩 컴포넌트 안에서 검색하게 됩니다.
Window 객체는 Window 객체의 프로퍼티인 window로 참조할 수 있습니다. Window 객체에는 일반 전역 객체의 프로퍼티와 클라이언트 측 JS에서만 사용할 수 있는 다양한 프로퍼티가 정의되어 있습니다. (Window객체는 13장으로)
프로그램의 평가와 전역 변수
전역환경과 전역 객체를 생성한 후에는 JS 프로그램을 읽어들입니다. 다 읽어들인 후 프로그램을 평가하며, 최상위 레벨에 var 문으로 작성한 전역 변수는 전역 환경의 환경 레코드(객체 환경 레코드)의 프로퍼티로 추가됩니다.
// 전역 환경
GlobalEnvironment = {
// 전역 환경의 환경 레코드인 객체 환경 레코드에 Window의 참조가 설정되어 있음
ObjectEnvironmentRecord:{
bindObject: window
},
OuterLexicalEnvironmentReference: null
}
JS 엔진은 전역 코드를 평가할 때 최상위 레벨에 var 문으로 작성한 전역 변수를 전역 환경의 환경 레코드(객체 환경 레코드)에 프로퍼티로 기록합니다. 그 프로퍼티 이름은 식별자 이름이 되고 프로퍼티 값은 undefined가 됩니다.
함수의 경우에는 최상위 레벨에 작성된 함수 선언문을 함수 객체로 생성해서 전역 환경의 환경 레코드(객체 환경 레코드)에 프로퍼티로 기록합니다.
'Javascript' 카테고리의 다른 글
[JS] replaceAll 대신하여 replace 정규식 사용하기 (0) | 2020.09.22 |
---|---|
자바스크립트로 SAP 3D Visual Enterprise Viewer 제어하기 (0) | 2020.08.26 |
모던 자바스크립트 입문 : 7. 제어 구문 (0) | 2020.07.07 |
모던 자바스크립트 입문 : 6. 웹 브라우저에서의 입출력 (0) | 2020.07.03 |
Local Storage (로컬 스토리지) 를 이용한 데이터 저장 (1) | 2020.06.26 |