요약
재귀(recursion) – 함수 내부에서 자기 자신을 호출하는 것을 나타내는 프로그래밍 용어입니다. 재귀 함수는 우아하게 원하는 문제를 해결할 때 자주 쓰이곤 합니다.
함수가 자신을 호출하는 단계를 재귀 단계(recursion step) 라고 부릅니다. basis라고도 불리는 재귀의 베이스(base) 는 작업을 아주 간단하게 만들어서 함수가 더 이상은 서브 호출을 만들지 않게 해주는 인수입니다.재귀적으로 정의된 자료 구조는 자기 자신을 이용해 자료 구조를 정의합니다.
재귀적으로 정의된 자료구조에 속하는 연결 리스트는 리스트 혹은 null을 참조하는 객체로 이루어진 데이터 구조를 사용해 정의됩니다.
list = {value, next -> list}
HTML 문서의 HTML 요소 트리나 위에서 다룬 부서를 나타내는 트리 역시 재귀적인 자료 구조로 만들었습니다. 이렇게 재귀적인 자료 구조를 사용하면 가지가 여러 개인데 각 가지가 여러 가지로 뻗쳐 나가는 형태로 자료 구조를 만들 수 있습니다.
예시에서 구현한sumSalary
같은 재귀 함수를 사용하면 각 분기(가지)를 순회할 수 있습니다.모든 재귀 함수는 반복문을 사용한 함수로 다시 작성할 수 있습니다. 최적화를 위해 반복문으로 다시 작성해야 할 수도 있죠. 그러나 상당수 작업은 재귀를 사용해도 만족할 만큼 빠르게 동작합니다. 재귀를 사용하면 구현과 유지보수가 쉽다는 장점도 있습니다.
1. 반복적인 사고를 통한 방법: for 루프
function pow(x, n) {
let result = 1;
// 반복문을 돌면서 x를 n번 곱함
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
alert( pow(2, 3) ); // 8
2. 재귀적인 사고를 통한 방법: 작업을 단순화하고 자기 자신을 호출함
function pow(x, n) {
if (n == 1) {
return x;
} else {
return x * pow(x, n - 1);
}
}
alert( pow(2, 3) ); // 8
function pow(x, n) {
return (n == 1) ? x : (x * pow(x, n - 1));
}
let company = { // 동일한 객체(간결성을 위해 약간 압축함)
sales: [{name: 'John', salary: 1000}, {name: 'Alice', salary: 1600 }],
development: {
sites: [{name: 'Peter', salary: 2000}, {name: 'Alex', salary: 1800 }],
internals: [{name: 'Jack', salary: 1300}]
}
};
// 급여 합계를 구해주는 함수
function sumSalaries(department) {
if (Array.isArray(department)) { // 첫 번째 경우
return department.reduce((prev, current) => prev + current.salary, 0); // 배열의 요소를 합함
} else { // 두 번째 경우
let sum = 0;
for (let subdep of Object.values(department)) {
sum += sumSalaries(subdep); // 재귀 호출로 각 하위 부서 임직원의 급여 총합을 구함
}
return sum;
}
}
alert(sumSalaries(company)); // 7700
value
next
: 다음 연결 리스트 요소를 참조하는 프로퍼티. 다음 요소가 없을 땐 null이 됩니다.let list = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: {
value: 4,
next: null
}
}
}
};
let list = { value: 1 };
list.next = { value: 2 };
list.next.next = { value: 3 };
list.next.next.next = { value: 4 };
list.next.next.next.next = null;
let secondList = list.next.next;
list.next.next = null;
합치기
list.next.next = secondList;
맨 앞에 추가하기
let list = { value: 1 };
list.next = { value: 2 };
list.next.next = { value: 3 };
list.next.next.next = { value: 4 };
// list에 새로운 value를 추가합니다.
list = { value: "new item", next: list };
list.next = list.next.next;
요약
"..."은 나머지 매개변수나 전개 문법으로 사용됩니다.
나머지 매개변수와 전개 문법은 아래의 방법으로 구분할 수 있습니다.
...
이 함수 매개변수의 끝에 있으면 인수 목록의 나머지를 배열로 모아주는 '나머지 매개변수’입니다....
이 함수 호출 시 사용되면 배열을 목록으로 확장해주는 '전개 문법’입니다.사용 패턴:
- 인수 개수에 제한이 없는 함수를 만들 때 나머지 매개변수를 사용합니다.
- 다수의 인수를 받는 함수에 배열을 전달할 때 전개 문법을 사용합니다.
둘을 함께 사용하면 매개변수 목록과 배열 간 전환을 쉽게 할 수 있습니다.
조금 오래된 방법이긴 하지만 arguments라는 반복 가능한 유사 배열 객체를 사용해도 인수 모두를 사용할 수 있습니다.
...
...
뒤에 붙여주면 함수 선언부에 포함시킬 수 있습니다. 이때 마침표 세 개 ...
는 "나머지 매개변수들을 한데 모아 배열에 집어넣어라."는 것을 의미합니다.function sumAll(...args) { // args는 배열의 이름입니다.
let sum = 0;
for (let arg of args) sum += arg;
return sum;
}
alert( sumAll(1) ); // 1
alert( sumAll(1, 2) ); // 3
alert( sumAll(1, 2, 3) ); // 6
function showName(firstName, lastName, ...titles) {
alert( firstName + ' ' + lastName ); // Julius Caesar
// 나머지 인수들은 배열 titles의 요소가 됩니다.
// titles = ["Consul", "Imperator"]
alert( titles[0] ); // Consul
alert( titles[1] ); // Imperator
alert( titles.length ); // 2
}
showName("Julius", "Caesar", "Consul", "Imperator");
나머지 매개변수
...rest
는 항상 마지막에 있어야 합니다.
function showName() {
alert( arguments.length );
alert( arguments[0] );
alert( arguments[1] );
// arguments는 이터러블 객체이기 때문에
// for(let arg of arguments) alert(arg); 를 사용해 인수를 나열할 수 있습니다.
}
// 2, Julius, Caesar가 출력됨
showName("Julius", "Caesar");
// 1, Bora, undefined가 출력됨(두 번째 인수는 없음)
showName("Bora");
화살표 함수에는 \'arguments\'가 없습니다.
화살표 함수에서 arguments 객체에 접근하면, 외부에 있는 ‘일반’ 함수의 arguments 객체를 가져옵니다.
화살표 함수는 자체 this를 가지지 않기 때문입니다.
... arr
를 사용하면, 이터러블 객체 arr
이 인수 목록으로 '확장’됩니다.let arr = [3, 5, 1];
alert( Math.max(...arr) ); // 5 (전개 문법이 배열을 인수 목록으로 바꿔주었습니다.)
let arr1 = [1, -2, 3, 4];
let arr2 = [8, 3, -8, 1];
alert( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25
let arr = [3, 5, 1];
let arr2 = [8, 9, 15];
let merged = [0, ...arr, 2, ...arr2];
alert(merged); // 0,3,5,1,2,8,9,15 (0, arr, 2, arr2 순서로 합쳐집니다.)
let str = "Hello";
alert( [...str] ); // H,e,l,l,o
let str = "Hello";
// Array.from은 이터러블을 배열로 바꿔줍니다.
alert( Array.from(str) ); // H,e,l,l,o
spread
연산자로 복사본을 만들 수 있음let arr = [1, 2, 3];
let arrCopy = [...arr];
let obj = { a: 1, b: 2, c: 3 };
let objCopy = { ...obj };
{...}
안에서 선언한 변수는 블록 안에서만 사용할 수 있습니다.function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2
say
내부의 alert
에서 변수 name
에 접근할 땐, 먼저 내부 렉시컬 환경을 살펴봅니다. 내부 렉시컬 환경에서 변수 name
을 찾았습니다.alert
에서 변수 phrase
에 접근하려는데, phrase
에 상응하는 프로퍼티가 내부 렉시컬 환경엔 없습니다. 따라서 검색 범위는 외부 렉시컬 환경으로 확장됩니다. 외부 렉시컬 환경에서 phrase
를 찾았습니다.function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
<empty>
). 이제 counter()의 렉시컬 환경이 참조하는 외부 렉시컬 환경에서 count를 찾아봅시다. count를 찾았습니다!클로저
'클로저(closure)'는 개발자라면 알고 있어야 할 프로그래밍 용어입니다.
클로저는 외부 변수를 기억하고 이 외부 변수에 접근할 수 있는 함수를 의미합니다. 몇몇 언어에선 클로저를 구현하는 게 불가능하거나 특수한 방식으로 함수를 작성해야 클로저를 만들 수 있습니다. 하지만 자바스크립트에선 모든 함수가 자연스럽게 클로저가 됩니다. 예외가 하나 있긴 한데 자세한 내용은 new Function 문법에서 다루도록 하겠습니다.
요점을 정리해 봅시다. 자바스크립트의 함수는 숨김 프로퍼티인 [[Environment]]를 이용해 자신이 어디서 만들어졌는지를 기억합니다. 함수 내부의 코드는 [[Environment]]를 사용해 외부 변수에 접근합니다.
프런트엔드 개발자 채용 인터뷰에서 "클로저가 무엇입니까?"라는 질문을 받으면, 클로저의 정의를 말하고 자바스크립트에서 왜 모든 함수가 클로저인지에 관해 설명하면 될 것 같습니다. 이때 [[Environment]] 프로퍼티와 렉시컬 환경이 어떤 방식으로 동작하는지에 대한 설명을 덧붙이면 좋습니다.
[[Environment]]
프로퍼티에 외부 함수 렉시컬 환경에 대한 정보가 저장됩니다. 도달 가능한 상태가 되는 것이죠.요약
var
로 선언한 변수는let
이나const
로 선언한 변수와 다른 두 가지 주요한 특성을 보입니다.
var
로 선언한 변수는 블록 스코프가 아닌 함수 수준 스코프를 갖습니다.var
선언은 함수가 시작되는 시점(전역 공간에선 스크립트가 시작되는 시점)에서 처리됩니다.
이 외에도 전역 객체와 관련된 특성 하나가 더 있는데, 이에 대해선 다음 챕터에서 다루도록 하겠습니다.
var
만의 특성은 대부분의 상황에서 좋지 않은 부작용을 만들어냅니다.let
이 표준에 도입된 이유가 바로 이런 부작용을 없애기 위해서입니다. 변수는 블록 레벨 스코프를 갖는 게 좋으므로 이제는let
과const
를 이용해 변수를 선언하는 게 대세가 되었습니다.
var
로 선언한 변수의 스코프는 함수 스코프이거나 전역 스코프입니다. 블록 기준으로 스코프가 생기지 않기 때문에 블록 밖에서 접근 가능합니다.var
는 코드 블록을 무시하기 때문에 if
혹은 for
내부의 변수는 전역 변수가 됩니다. 전역 스코프에서 이 변수에 접근할 수 있죠.var
는 함수 레벨 변수가 됩니다.var
는 if
, for
등의 코드 블록을 관통합니다. 아주 오래전의 자바스크립트에선 블록 수준 렉시컬 환경이 만들어 지지 않았기 때문입니다. var는 구식 자바스크립트의 잔재이죠.(function() {
let message = "Hello";
alert(message); // Hello
})();
// IIFE를 만드는 방법
(function() {
alert("함수를 괄호로 둘러싸기");
})();
(function() {
alert("전체를 괄호로 둘러싸기");
}());
!function() {
alert("표현식 앞에 비트 NOT 연산자 붙이기");
}();
+function() {
alert("표현식 앞에 단항 덧셈 연산자 붙이기");
}();