0%

let, const와 블록 레벨 스코프

var 키워드로 선언한 변수의 문제점

  1. 변수 중복 선언 허용
1
2
3
4
5
6
7
8
9
10
11
var x = 1;
var y = 1;

// var 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언을 허용한다.
// 초기화문이 있는 변수 선언문은 자바스크립트 엔진에 의해 var 키워드가 없는 것처럼 동작한다.
var x = 100;
// 초기화문이 없는 변수 선언문은 무시된다.
var y;

console.log(x); // 100
console.log(y); // 1
  1. 함수 레벨 스코프
    var키워드로 선언한 변수는 함수 코드 블럭만을 지역 스코프로 인정하기 때문에 함수 코드블럭을 제외한 곳에서는 전역 변수가 된다.
1
2
3
4
5
6
7
8
9
var i = 10;

// for문에서 선언한 i는 전역 변수이다. 이미 선언된 전역 변수 i가 있으므로 중복 선언된다.
for (var i = 0; i < 5; i++) {
console.log(i); // 0 1 2 3 4
}

// 의도치 않게 i 변수의 값이 변경되었다.
console.log(i); // 5
  1. 변수 호이스팅
    변수 호이스팅에 의해 변수 선언문 이전에 변수를 참조할 수 있다. 하지만 선언문 이전에는 언제나 undefined를 반환한다.
1
2
3
4
5
6
7
8
9
10
11
// 이 시점에는 변수 호이스팅에 의해 이미 foo 변수가 선언되었다(1. 선언 단계)
// 변수 foo는 undefined로 초기화된다. (2. 초기화 단계)
console.log(foo); // undefined

// 변수에 값을 할당(3. 할당 단계)
foo = 123;

console.log(foo); // 123

// 변수 선언은 런타임 이전에 자바스크립트 엔진에 의해 암묵적으로 실행된다.
var foo;

let 키워드

let키워드는 같은 변수명으로 선언하면 문법 에러가 발생한다.

1
2
3
let bar = 123;
// let이나 const 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언을 허용하지 않는다.
let bar = 456; // SyntaxError: Identifier 'bar' has already been declared

let키워드로 선언한 변수는 모든 코드 블록을 지역 스코프로 인정하는 블록 레벨 스코프를 따른다.

1
2
3
4
5
6
7
let foo = 1; // 전역 변수
{
let foo = 2; // 지역 변수
let bar = 3; // 지역 변수

console.log(foo); // 1
console.log(bar); // ReferenceError: bar is not defined

변수 호이스팅

let키워드로 선언한 변수는 변수 호이스팅이 발생하지 않는 것처럼 동작한다.
let 키워드로 선언한 변수는 “선언 단계”와 “초기화 단계”가 분리되어 진행된다. 스코프의 시작 지점부터 초기화 시작 지점(변수 선언문)까지 변수를참조할 수 없으며 이러한 구간을 **일시적 사각지대(Temporal Dead Zone; TDZ)**라고 부른다.

1
2
3
4
5
6
7
8
9
10
11
// var 키워드로 선언한 변수는 런타임 이전에 선언 단계와 초기화 단계가 실행된다.
// 따라서 변수 선언문 이전에 변수를 참조할 수 있다.
console.log(foo); // undefined
var foo;
// let 키워드로 선언한 변수는 런타임 이전에 선언 단계가 실행된다. 아직 변수가 초기화되지 않았다.
// 초기화 이전의 일시적 사각 지대에서는 변수를 참조할 수 없다.
console.log(foo); // ReferenceError: foo is not defined
let foo; // 변수 선언문에서 초기화 단계가 실행된다.
console.log(foo); // undefined
foo = 1; // 할당문에서 할당 단계가 실행된다.
console.log(foo); // 1

하지만 사실 let키워드로 선언한 변수도 변수 호이스팅이 발생한다.

1
2
3
4
5
let foo = 1; // 전역 변수
{
console.log(foo); // ReferenceError: Cannot access 'foo' before initialization
let foo = 2; // 지역 변수
}

변수 호이스팅이 발생하지 않는다면 전역 변수 값을 출력해야하지만 호이스팅으로 참조 에러가 발생한다.
자바스크립트는 ES6에서 도입된 let, const를 포함해서 모든 선언(var, let, const, function, function*, class 등)을 호이스팅한다. 단, ES6에서도입된 let, const, class를 사용한 선언문은 호이스팅이 발생하지 않는 것처럼 동작한다.

전역 객체와 let

var 키워드로 선언한 전역 변수와 전역 함수, 암묵적 전역(선언하지 않은 변수에 값을 할당)은 전역 객체 window의 프로퍼티가 된다. 전역 객체의 프로퍼티를 참조할 때 window를 생략할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 이 예제는 브라우저 환경에서 실행해야 한다.

// 전역 변수
var x = 1;
// 암묵적 전역
y = 2;
// 전역 함수
function foo() {}

// var 키워드로 선언한 전역 변수는 전역 객체 window의 프로퍼티다.
console.log(window.x); // 1
// 전역 객체 window의 프로퍼티는 전역 변수처럼 사용할 수 있다.
console.log(x); // 1

// 암묵적 전역은 전역 객체 window의 프로퍼티다.
console.log(window.y); // 2
console.log(y); // 2

// 함수 선언문으로 정의한 전역 함수는 전역 객체 window의 프로퍼티다.
console.log(window.foo); // ƒ foo() {}
// 전역 객체 window의 프로퍼티는 전역 변수처럼 사용할 수 있다.
console.log(foo); // ƒ foo() {}

let 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 아니다. let 전역 변수는 보이지 않는 개념적인 블록(전역 렉시컬 환경의 선언적 환경 레코드) 내에 존재한다.

1
2
3
4
5
6
// 이 예제는 브라우저 환경에서 실행해야 한다.
let x = 1;

// let, const 키워드로 선언한 전역 변수는 전역 객체 window의 프로퍼티가 아니다.
console.log(window.x); // undefined
console.log(x); // 1

const 키워드

const 키워드는 상수(constant)를 선언하기 위해 사용한다. const 키워드의 특징은 let 키워드와 대부분 동일하다.

선언과 초기화

const 키워드로 선언한 변수는 반드시 선언과 동시에 초기화해야 한다.

1
2
3
4
const foo = 1;

// 선언과 동시에 초기화하지 않으면 문법 에러가 발생
const bar; // SyntaxError: Missing initializer in const declaration

재할당 금지

const 키워드로 선언한 변수는 재할당이 금지된다.

1
2
const foo = 1;
foo = 2; // TypeError: Assignment to constant variable.

상수

  • const 키워드에 의해 재할당이 금지되므로 원시값으로 할당된 값을 변경할 수 있는 방법은 없다.
  • 일반적으로 상수의 이름은 대문자로 선언해 상수임을 명확히 나타낸다. 여러 단어로 이뤄진 경우에는 언더스코어(_)로 구분해서 스네이크 케이스로 표현하는 것이 일반적이다.

const 키워드와 객체

const 키워드로 선언된 변수에 객체를 할당한 경우, 값을 변경할 수 있다.

1
2
3
4
5
6
7
8
const person = {
name: "Lee",
};

// 객체는 변경 가능한 값이다. 따라서 재할당없이 변경이 가능하다.
person.name = "Kim";

console.log(person); // {name: "Kim"}

const 키워드는 재할당을 금지할 뿐 “불변(immutable)”을 의미하지는 않는다.

변수 선언 관련 권고

  • ES6를 사용한다면 var 키워드는 사용하지 않는다.
  • 재할당이 필요한 경우에 한정해 let 키워드를 사용한다. 이때 변수의 스코프는 최대한 좁게 만든다.
  • 변경이 발생하지 않고 읽기 전용으로 사용하는(재할당이 필요 없는 상수) 원시값과 객체에는 const 키워드를 사용한다. const 키워드는 재할당을 금지하므로 var, let 키워드보다 안전하다.

변수를 선언하는 시점에는 재할당이 필요할지 잘 모르는 경우가 많다. 따라서 변수를 선언할 때는 일단 const 키워드를 사용하자. 반드시 재할당이 필요하다면 그때 const 키워드를 let 키워드로 변경해도 결코 늦지 않는다.

참고 도서: 모던 자바스크립트 Deep Dive

Nyong’s GitHub

오늘 한 것

이진 검색 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function binarySearch(array, target) {
var start = 0;
var end = array.length - 1;
var mid = Math.floor((start + end) / 2);

while (start <= end) {
mid = Math.floor((start + end) / 2);
if (target > array[mid]) {
start = mid + 1;
} else if (target < array[mid]) {
end = mid - 1;
} else if (target === array[mid]) return mid;
}
return -1;
}

console.log(binarySearch([1, 2, 3, 4, 5, 6], 1)); // 0
console.log(binarySearch([1, 2, 3, 4, 5, 6], 3)); // 2
console.log(binarySearch([1, 2, 3, 4, 5, 6], 5)); // 4
console.log(binarySearch([1, 2, 3, 4, 5, 6], 6)); // 5
console.log(binarySearch([1, 2, 3, 4, 5, 6], -1)); // -1
console.log(binarySearch([1, 2, 3, 4, 5, 6], 0)); // -1
console.log(binarySearch([1, 2, 3, 4, 5, 6], 7)); // -1
  • 시간 복잡도: O(log n)
  • 좀 더 간결하게 코드를 정리해보자. 이왕이면 재귀함수로도 구현해보자

함수

매개변수의 최대 개수

매개변수의 개수가 많아진다면 매개변수에 객체로 전달하는 것이 좋다. → 객체는 프로퍼티 키를 통해 접근하니 순서가 상관없다. → 단점으로는 객체는 변경 가능한 값이라 불안정할 수 있다.

반환문

1
2
3
4
5
function add(x, y) {
return x + y; // 함수에 return이 없다면 암묵적으로 undefined를 반환한다.
}

var res = add(1, 2); // 함수를 호출할 때는 반환값을 할당할 변수가 필요하다

참조에 의한 전달과 외부 상태의 변경

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 매개변수 primitive는 원시값을 전달받고, 매개변수 obj는 객체를 전달받는다.
function changeVal(primitive, obj) {
primitive += 100;
obj.name = "Kim";
}

// 외부 상태
// 변수로 할당했다는 것은 재사용 의도가 있다는 것이다.
var num = 100;
var person = { name: "Lee" };

console.log(num); // 100
console.log(person); // {name: "Lee"}

// 원시값은 값 자체가 복사되어 전달되고 객체는 참조값이 복사되어 전달된다.
changeVal(num, person);

// 원시값은 원본이 훼손되지 않는다.
console.log(num); // 100

// 객체는 원본이 훼손된다.
console.log(person); // {name: "Kim"}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 매개변수 primitive는 원시값을 전달받고, 매개변수 obj는 객체를 전달받는다.
function changeVal(primitive, obj) {
primitive += 100;
obj.name = "Kim";
}

// 재사용 의도가 없다면 변수에 할당하지 말자.
// var num = 100;
// var person = { name: 'Lee' };

console.log(num); // 100
console.log(person); // {name: "Lee"}

// 값을 직접 넣어주면 외부 상태의 영향을 받을 일이 없다.
changeVal(100, { name: "Lee" });

즉시 실행 함수

즉시 실행 함수(IIFE)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 익명 즉시 실행 함수
(function () {
var a = 3;
var b = 5;
return a * b;
})();

// ( 그룹 연산자: 값으로 평가될 수 있는 표현식을 감싼다.
// 함수 리터럴
function () {
var a = 3;
var b = 5;
return a * b;
}
// )
// () 호출
  1. let, const 등장 전 전역변수 방지를 위한 스코프 역할로 사용했다.
  2. 클로저(closure)와 관련되어 사용한다.

즉시 실행 함수를 여러번 사용할 경우 → 즉시 실행 함수를 쓸 이유가 없다.

재귀 함수

재귀함수는 StackOverFlow 에러에 취약하다. for문과의 가독성 등을 잘 비교해서 더 나은 경우에만 쓰는 것이 좋다.

많이 쓰이지는 않지만 트리 탐색 및 깊은 복사할 때 사용된다.

중첩 함수

JS에서 중요한 개념이다.

전역 스코프에서 함수가 실행될 때 runtime 이전에 함수 스코프를 평가한다. 이때 변수, 내부 함수 등이 호이스팅된다.

외부 함수 내부에 중첩된 함수는 외부 함수라는 스코프에 한정된 함수라는 것이다.

콜백 함수

1
2
3
4
5
6
7
8
9
10
11
// 고차함수
function repeat(n, f) {
for (var i = 0; i < n; i++) f();
}

// 콜백함수
function foo() {
console.log("hello");
}

repeat(5, foo);

고차함수: 인수로 함수를 받거나 리턴값으로 함수를 반환하는 함수

콜백함수: 매개변수를 통해 다른 함수 내부로 전달되는 함수

순수 함수와 비순수 함수

객체의 메서드는 프로퍼티를 잠조하고 조작하기 때문에 기본적으로 비순수 함수이다.

함수형 프로그램의 이상향은 순수 함수만을 사용하며 코딩하는 것이다.

Nyong’s GitHub

오늘 한 것

공부한 javaScript 코딩 문제

1.가운데 글자 가져오기

문제 설명
단어 s의 가운데 글자를 반환하는 함수, solution을 만들어 보세요. 단어의 길이가 짝수라면 가운데 두글자를 반환하면 됩니다.

재한사항
s는 길이가 1 이상, 100이하인 스트링입니다.

입출력 예
s return
abcde c
qwer we

나의 답

1
2
3
4
5
6
7
8
function solution(s) {
var x = s.split("");
var answer = "";
return (answer =
s.length % 2 == 0
? x[x.length / 2 - 1] + x[x.length / 2]
: x[x.length / 2 - 0.5]);
}

감탄한 타인의 답안

1
2
3
4
function solution(s) {
const mid = Math.floor(s.length / 2);
return s.length % 2 === 1 ? s[mid] : s[mid - 1] + s[mid];
}
  • Math.floor()함수는 주어진 숫자와 같거나 작은 정수 중에서 가장 큰 수를 반환한다. 즉 홀 수일 경우 0.5를 자동으로 버릴 수 있다.
  • 이 함수를 사용하면 내 코드에서 -0.5 같이 지저분하게 만들 필요가 없었다.

2. 콜라츠 추측

문제 설명
1937년 Collatz란 사람에 의해 제기된 이 추측은, 주어진 수가 1이 될때까지 다음 작업을 반복하면, 모든 수를 1로 만들 수 있다는 추측입니다. 작업은 다음과 같습니다.

  1. 입력된 수가 짝수라면 2로 나눕니다. 입력된 수가 홀수라면 3을 곱하고 1을 더합니다.
  2. 결과로 나온 수에 같은 작업을 1이 될 때까지 반복합니다.

예를 들어, 입력된 수가 6이라면 6→3→10→5→16→8→4→2→1 이 되어 총 8번 만에 1이 됩니다. 위 작업을 몇 번이나 반복해야하는지 반환하는 함수, solution을 완성해 주세요. 단, 작업을 500번을 반복해도 1이 되지 않는다면 –1을 반환해 주세요.

제한 사항
입력된 수, num은 1 이상 8000000 미만인 정수입니다.

입출력 예
n result
6 8
16 4
626331 -1

입출력 예 설명
입출력 예 #2
16 -> 8 -> 4 -> 2 -> 1 이되어 총 4번만에 1이 됩니다.

입출력 예 #3
626331은 500번을 시도해도 1이 되지 못하므로 -1을 리턴해야합니다.

나의 답

1
2
3
4
5
6
7
8
function solution(num) {
var answer = 0;

for (var i = 0; num !== 1 && i <= 500; i++) {
num = num % 2 == 0 ? num / 2 : num * 3 + 1;
}
return (answer = i <= 500 ? i : -1);
}

3.서울에서 김서방 찾기

문제 설명
String형 배열 seoul의 element중 Kim의 위치 x를 찾아, 김서방은 x에 있다는 String을 반환하는 함수, solution을 완성하세요. seoul에 Kim은 오직 한 번만 나타나며 잘못된 값이 입력되는 경우는 없습니다.

제한 사항
seoul은 길이 1 이상, 1000 이하인 배열입니다.
seoul의 원소는 길이 1 이상, 20 이하인 문자열입니다.
Kim은 반드시 seoul 안에 포함되어 있습니다.

입출력 예
seoul return
[“Jane”, “Kim”] “김서방은 1에 있다”

나의 답

1
2
3
function solution(seoul) {
return "김서방은 " + seoul.indexOf("Kim") + "에 있다";
}
  • for문을 이용할 수도 있지만 내장함수를 사용하면 너무 간편하다.

출처: programmers

Nyong’s GitHub

오늘 한 것

전역 변수의 문제점

변수의 생명 주기

변수는 자신이 선언된 위치에서 생성되고 소멸한다. 전역 변수의 생명 주기는 애플리케이션의 생명 주기와 같다.(var 키워드로 선언한 전역 변수의 생명 주기는 전역 객체의 생명 주기와 일치한다.) 하지만 함수 내부에서 선언된 지역 변수는 함수가 호출되면 생성되고 함수가 종료하면 소멸한다.

전역 변수는 선언문이 어디에 있뜬 호이스팅에 의해 runtime 이전에 가장 먼저 실행된다. 하지만 함수 내부 지역 변수는 함수가 호출된 직후 함수 몸체 코드가 실행되기 직전 JS 엔진에 의해 먼저 실행고 초기화된다. 그 후 함수가 종료되면 지역 변수도 소명되어 생명 주기가 종료된다. 즉, 지역 변수의 생명 주기는 함수의 생명 주기와 일치한다.

지역 변수의 생명 주기는 함수의 생명 주기와 대부분 일치하지만 지역 변수가 함수보다 오래 생존하는 경우도 있다. 함수 내부에서 선언된 지역 변수는 자신이 등록된 스코프가 소멸(스코프가 메모리에서 해제)될 때까지 유효하다. 할당된 메모리 공간은 더 이상 아무도 참조하지 않을 때 가비지 콜렉터에 의해 해제되어 가용 메모리 풀에 반환된다. 즉, 누군가가 메모리 공간을 참조하고 있으면 해제되지 않고 확보된 생태로 남아 있게 된다. 이는 스코프도 마찬가지다. 누군가 스코프를 참조하고 있으면 스코프는 소멸하지 않고 생존하게 된다.

1
2
3
4
5
6
7
8
9
var x = "global";

function foo() {
console.log(x); // undefined
var x = "local";
}

foo();
console.log(x); // global

이처럼 호이스팅은 스코프를 단위로 동작한다.

전역 변수의 문제점

암묵적 결합
어디서든 참조 가능한 전역변수는 모든 코드가 전역 변수를 참조하고 변경할 수 있는 **암묵적 결합(implicit coupling)**을 허용한ㄷ다. 변수의 유효 범위가 크면 클수록 코드의 가독성은 나빠지고 의도치 않게 상태가 변경될 수 있는 위험성도 높아진다.

긴 생명 주기
전역 변수는 생명 주기가 길다. 따라서 메모리 리소스도 오랜 기간 소비하며 var키워드는 변수의 중복 선언을 허용하여 변수 이름이 중복되어 의도치 않은 재할당이 이뤄질 수 있다.
지역 변수는 전역 변수보다 생명 주기가 휠씬 짧아 상태 변경에 따른 오류 발생 확률이 낮고 메모리 리소스도 비교적 짧은 기간 소비한다.

스코프 체인 상에서 종점에 존재
전역 변수는 스코프 체인 상에서 종점에 존재한다. 즉, 변수 검색 시 최상위 스코프에 존재한 전역 변수의 검색 속도가 가장 느리다.

네임스페이스 오염
JS는의 가장 큰 문제점 중 하나는 파일이 분리되어 있다 해도 하나의 전역 스코프를 공유한다는 것이다. 따라서 다른 파일 내에서 동일한 이름으로 명명된 전역 변수나 전역 함수가 같은 스코프 내에 존재할 경우 예상치 못한 결과를 가져올 수 있다.

전역 변수의 사용을 억제하는 방법

전역 변수를 반드시 사용해야 할 이유를 찾지 못한다면 지역 변수를 사용해야 한다. 변수의 스코프는 좁을수록 좋다.

  1. 즉시 실행 함수
1
2
3
4
5
6
(function () {
var foo = 10; // 즉시 실행 함수의 지역 변수
// ...
})();

console.log(foo); // ReferenceError: foo is not defined

모든 코드를 즉시 실행 함수로 감싸면 모든 변수는 즉시 실행 함수의 지역 변수가 된다. 이러한 특성으로 전역 변수의 사용을 제한한다.

  1. 네임스페이스 객체
1
2
3
4
5
var MYAPP = {}; // 전역 네임스페이스 객체

MYAPP.name = "Lee";

console.log(MYAPP.name); // Lee

전역에 네임스페이스(Namespace) 역할을 담당할 객체를 생성하고 전역 변수처럼 사용하고 싶은 변수를 프로퍼티로 추가한다.

1
2
3
4
5
6
7
8
var MYAPP = {}; // 전역 네임스페이스 객체

MYAPP.person = {
name: "Lee",
address: "Seoul",
};

console.log(MYAPP.person.name); // Lee

네임스페이스 객체에 또 다른 네임스페이스 객체를 프로퍼티로 추가해서 네임 스페이스를 계층적으로 구성할 수도 있다. 네임스페이스를 분리해서 식별자 충돌을 방지하는 효과는 있으나 네임스페이스 객체 자체가 전역 변수에 할당되므로 그다지 유용해 보이지는 않는다.

  1. 모듈 패턴

모듈 패턴은 클래스를 모방해서 관련이 있는 변수와 함수를 모아 즉시 실행 함수로 감싸 하나의 모듈을 만든다. 모듈 패턴은 클로저를 기반으로 동작한다. 모듈 패턴의 특징은 전역 변수의 억제는 물론 캡슐화까지 구현할 수 있다는 것이다.

캡슐화(encapsulation)는 객체의 프로퍼티와 메서드를 하나로 묶는 것을 말한다. 캡슐화는 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 정보 은닉(information hiding)이라 한다.

모듈 패턴은 전역 네임스페이스의 오염을 막는 기능은 물론 한정적이기는 하지만 정보 은닉을 구현하기 위해 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var Counter = (function () {
// private 변수
var num = 0;

// 외부로 공개할 데이터나 메서드를 프로퍼티로 추가한 객체를 반환한다.
return {
increase() {
return ++num;
},
decrease() {
return --num;
},
};
})();

// private 변수는 외부로 노출되지 않는다.
console.log(Counter.num); // undefined

console.log(Counter.increase()); // 1
console.log(Counter.increase()); // 2
console.log(Counter.decrease()); // 1
console.log(Counter.decrease()); // 0

위 예제의 즉시 실행 함수는 객체를 반환한다. 이 객체에는 외부에 노출하고 싶은 변수나 함수를 담아 반환한다. 이때 반환되는 객체의 프로퍼티는 외부에 노출되는 퍼블릭 멤버(public menber)이다. 외부로 노출하고 싶지 않은 변수나 함수는 반환하는 객체에 추가하지 않으면 외부에서 접근할 수 없는 프라이빗 멤버(private member)가 된다.

  1. ES6 모듈
    ES6 모듈은 파일 자체의 독자적인 모듈 스코프를 제공한다. 즉, 모듈 내에서 var 키워드로 선언한 변수는 더는 전역 변수가 아니며 window 객체의 프로퍼티도 아니다.
    모던 브라우저(Chrome 61, FF 60, SF 10.1, Edge 16 이상)에서 ES6 모듈을 사용할 수 있다. script 태그에 type="module" 어트리뷰트를 추가하면 로드된 자바스크립트 파일은 모듈로서 동작한다. 모듈의 파일 확장자는 mjs를 권장한다.
1
2
<script type="module" src="lib.mjs"></script>
<script type="module" src="app.mjs"></script>

ES6 모듈은 구형 브라우저에서는 동작하지 않으며, 브라우저의 ES6 모듈 기능을 사용하더라도 트랜스파일링이나 번들링이 필요하기 때문에 브라우저가 지원하는 ES6 모듈 기능보다는 Webpack 등의 모듈 번들러를 사용하는 것이 일반적이다.

참고 도서: 모던 자바스크립트 Deep Dive

Nyong’s GitHub

오늘 한 것

스코프

모든 식별자(변수 이름, 함수 이름, 클래스 이름 등)는 자신이 선언된 위치에 의해 자신을 참조할 수 있는 유효 범위가 결정된다. 식별자의 유효 범위를 스코프라한다.

JS 엔진은 이름이 같은 두 개의 변수 중에서 어떤 변수를 참조해야 할 것인지를 결정하는 것을 식별자 결정(identifier resolution)이라 한다. 식별자 결정은 스코프를 통해 어떤 변수를 참조해야 할 것인지 결정한다.

1
2
3
4
5
6
7
8
9
10
var x = "global";

function foo() {
var x = "local";
console.log(x); // local
}

foo();

console.log(x); // global

위 예제의 두 개의 x 변수는 식별자 이름이 동일하지만 자신이 유효한 범위, 즉 스코프가 다른 별개의 변수다.
스코프라는 개념이 없다면 같은 이름을 갖는 변수는 충돌을 일으키므로 프로그램 전체에서 하나밖에 사용할 수 없다.

프로그래밍 언어에서는 스코프(유효 범위)를 통해 식별자인 변수 이름의 충돌을 방지하여 같은 이름의 변수를 사용할 수 있게 한다. 스코프 내에서 식별자는 유일해야 하지만 다른 스코프에는 같은 이름의 식별자를 사용할 수 있다.

스코프의 종류

코드는 전역(global)과 지역(local)으로 구분할 수 있다.

전역과 전역 스코프

전역이란 코드의 가장 바깥 영역을 말한다. 전역은 전역 스코프(global scope)를 만든다. 전역에 변수를 선언하면 전역 스코프를 갖는 어디서든 참조 가능한 전역 변수(global variable)가 된다.

지역과 지역 스코프

지역이란 함수 몸체 내부를 말한다. 지역은 지역 스코프(local scope)를 만든다. 지역에 변수를 선언하면 지역 스코프를 갖는 자신과 하위 지역에서 참조 가능한 지역 변수(local variable)가 된다.

스코프 체인

함수는 전역, 함수 몸체 내부 모두 정의할 수 있다. 함수 몸체 내부에서 함수가 정의되면 ‘함수의 중첩’이라 한다. 그리고 중첩 함수를 포함하는 함수를 ‘외부 함수’라고 한다.

함수의 중첩은 함수의 지역 스코프도 중첩될 수 있다는 것을 말한다. 이는 스코프가 함수의 중첩으로 계층적 구조를 갖음을 의미한다. 이때 외부 함수의 지역 스코프를 중첩 함수의 상위 스코프라 한다.
모든 스코프는 하나의 계층적 구조로 연결되며 전역 스코프가 최상위 스코프이다. 이와 같이 스코프가 계층적으로 연결된 것을 스코프 체인이라 한다.

식별자를 참조할 때 JS 엔진은 스코프 체인을 통해 식별자를 참조하는 코드의 스코프부터 상위 스코프 방향으로 이동하며 식별자를 검색한다. 이러한 방향성으로 하위 스코프의 유효한 식별자를 상위 스코프에서는 참조할 수 없다. 변수와 함수 객체가 할당된 식별자 모두 스코프 체인의 규칙에 따라 검색된다.

함수 레벨 스코프

C, JAVA 등에서는 모든 코드 블록이 지역 스코프를 만들지만(블록 레벨 스코프) JS에서는 코드 블록이 아닌 함수에 의해서만 지역 스코프가 생성(함수 레벨 스코프)된다.

1
2
3
4
5
6
7
8
9
var i = 10;

// for 문에서 선언한 i는 전역 변수다. 이미 선언된 전역 변수 i가 있으므로 중복 선언된다.
for (var i = 0; i < 5; i++) {
console.log(i); // 0 1 2 3 4
}

// 의도치 않게 변수의 값이 변경되었다.
console.log(i); // 5

var로 선언된 변수는 함수의 코드 블록만을 지역 스코프로 인정한다. 하지만 ES6에서 도입된 let, const 키워드는 블록 레벨 스코프를 지원한다.

렉시컬 스코프

1
2
3
4
5
6
7
8
9
10
11
12
13
var x = 1;

function foo() {
var x = 10;
bar();
}

function bar() {
console.log(x);
}

foo(); // 1
bar(); // 1

위 예제의 실행 결과는 bar 함수의 상위 스코프가 무엇인지에 따라 결정된다.

  1. 함수를 어디서 호출했는지에 따라 함수의 상위 스코프를 결정하는 동적 스코프#
  2. 함수를 어디서 정의했는지에 따라 함수의 상위 스코프를 결정하는 렉시컬 스코프(정적 스코프)

JS는 렉시컬 스코프를 따르므로 함수를 정의한 위치에 따라 상위 스코프가 결정된다. 즉, 함수의 상위 스코프는 언제나 자신이 정의된 스코프다. 이처럼 함수의 상위 스코프는 함수 정의가 실행될 때 정적으로 결정된다. 함수 정의(함수 선언문 또는 함수 표현식)가 실행되어 생성된 함수 객체는 이렇게 결정된 상위 스코프를 기억한다. 함수가 호출될 때마다 함수의 상위 스코프를 참조할 필요가 있기 때문이다.

위 예제의 bar() 함수는 자신이 정의된 전역 스코프를 기억하고 어디서 호출되든 자신이 정의됐던 전역 스코프를 상위 스코프로 사용한다.

참고 도서: 모던 자바스크립트 Deep Dive

Nyong’s GitHub

오늘 한 것

타입 변환과 단축 평가

자열 타입으로 변환

1.toString() → 중의적 표현이라서 문맥에 따라 숫자 뒤의 .은 소수점으로 인식
(1).toString()으로 해야한다.

원시값과 객체의 비교

원시값

1
2
3
// y에는 x를 평가하여 x의 값을 참조하여 값을 전달한다. (값에 의한 전달)
var x = 10;
var y = x;

원시값은 변경 불가이다.
변수값 변경은 ‘재할당’이다. 변수값 변경과 원시값 변경은 다른 의미이다.

상수는 단 한 번만 할당이 허용되는 변수이므로 상수와 변경 불가능한 값을 동일시하는 것은 곤란하다. 상수는 재할당이 금지된 변수일 뿐이다.

값에 의한 전달

1
2
var score = 80;
var copy = score;

score의 80이 평가되어 score의 값 80을 복사한 후 메모리에 마련한 뒤 copy에 할당해 준다.

객체

객체는 프로퍼티의 개수가 정해져 있지 않으며, 동적으로 추가되고 삭제할 수 있다. 또한 프로퍼티의 값에도 제약이 없다. 따라서 객체는 원시값과 같이 확보해야 할 메모리 공간의 크기를 사전에 정해 둘 수 없다.

클래스 기반 객체 지향 언어는 객체에 할당된 메모리량을 정의한 클래스를 이용해 알 수 있다. 하지만 프로토타입 기반 객체 지향 언어는 객체의 할당 메모리량을 알 수 없다.
만약 객체가 원시값 처럼 동작한다면 메모리 소비가 매우 많다. → 변경 불가능한 값을 변경을 위해서는 복사한 값에 대한 새로운 메모리 공간이 필요하기 때문이다.

1
2
3
4
5
var person = {
name: "Kim",
};
// 프로퍼티 값 갱신
person.name = "Lee";
1
2
3
4
5
6
7
var person = {
name: "Kim",
};

person = {
name: "Lee",
};

위 두 상황은 같은 과정일까? → 물론 아니다. 전자는 프로퍼티 값만을 갱신해주는 행위지만, 후자는 person 식별자에 새로운 객체 생성해서 참조해주는 것이다.

1
2
3
4
5
6
7
8
9
10
var person1 = {
name: "Lee",
};

var person2 = {
name: "Lee",
};

console.log(person1 === person2); // false
console.log(person1.name === person2.name); // true

person1과 person2의 변수 값은 객체의 참조 주소이다. 물론 person1과 person2는 내용이 같을 뿐 다른 객체이기에 참조 주소도 다르다.
반면 person1.name과 person2.name은 모두 원시값 문자열 ‘Lee’의 표현식이기 때문에 같은 값으로 평가된다.

얕은 복사와 깊은 복사

1
2
3
4
5
6
7
8
9
10
11
12
13
// 얕은 복사 예
var person = {
name: "Lee",
};

// 참조에 의한 전달.
// 두 식별자가 하나의 객체를 공유한다.
var copy = person;

copy.name = "Kim";

console.log(person); // Kim
console.log(copy === person); // true

얕은 복사는 항상 조심해야한다.
깊은 복사를 해야 객체의 불변성을 얻을 수 있다. 하지만 깊은 복사는 퍼포먼스 측면에서 좋지 않기 때문에 대부분 얕은 복사를 기본으로 한다.
깊은 복사를 사용하고 싶다면 라이브러리를 사용해는 것이 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const o = {
a: {
b: 2,
},
f() {},
};

// lodash의 cloneDeep을 사용한 깊은 복사
// "npm install lodash"로 lodash를 설치한 후, Node.js 환경에서 실행
const _ = require("lodash");

let c = _.cloneDeep(o);
console.log(o === c); // false
console.log(o.a === c.a); // false
console.log(c.f); // f

함수

1
2
3
4
// 변수에 함수 리터럴을 할당 (함수 표현식)
var f = function add(x, y) {
return x + y;
};
  • ;이 붙는 것은 표현식이라는 것이다. 값으로 평가되는 함수 리터럴이다. 함수 리터럴은 변수에 참조하기 직전에 생성한다.(함수 선언문은 runtime 전에 정의됨)
  • 함수 이름 add는 함수 몸체 내에서만 참조할 수 있는 식별자다.(함수 이름 생략 가능)
  • 함수가 객체라는 사실은 함수형 프로그래밍이 가능하다는 말이다. 함수가 객체가 아니라면 함수형 프로그래밍이 불가능하다.
1
2
3
4
5
6
7
8
9
10
11
// 함수 선언문(function declaration/function statement)
function add(x, y) {
return x + y;
}
// 함수 표현식(function expression)
var add = function (x, y) {
return x + y;
};

// 화살표 함수
var add = (x, y) => x + y;

선언문은 익명함수가 불가능하고 ; 가 붙지 않으며 표현식처럼 값으로 평가되지 않는다. 값으로 평가되는 문맥에서는 함수는 표현식이다.

함수 호이스팅

1
2
3
4
5
6
7
8
9
10
11
// 함수 호이스팅
foo();

// 함수 선언문은 runtime 이전에 정의되고 함수 이름으로 동일한 식별자를 생성함.
function foo() {
console.log("foo");
}

// 변수 호이스팅
console.log(x);
var x = 1;

가변인자 함수

1
2
3
4
5
6
7
8
function sum() {
var total = 0;
for (var i = 0; i < arguments.length; i++) total += arguments[i];
return total;
}

var result = sum(1, 2, 3, 4, 5);
console.log(result); // 15

argument는 유사배열로서 인자가 내부에 모두 저장되기 때문에 가능하다.

오늘 느낀 것

  • 슬슬 어렵다. 그래도 본격적으로 공부하니까 재밌다.

  • 배우는 것이 많다. 배움에서 끝내지 말고 학습하자.

Nyong’s GitHub

오늘 한 것

함수

1
2
3
4
5
6
7
8
// (x,y) = 매개변수
function add(x, y) {
// x + y = 반환값
return x + y;
}

// 함수 호출 과정 / (2,5) = 인수
var result = add(2, 5);

함수는 코드의 재사용에 있어 유용하다.

함수 리터럴

JS에서 함수는 객체 타입의 값이다. 함수 리터럴로 생성할 수 있다. 함수 리터럴은 function 키워드, 함수 이름, 매개변수 목록, 함수 몸체로 구성된다.

1
2
3
4
// 변수에 함수 리터럴을 할당
var f = function add(x, y) {
return x + y;
};

함수 리터럴의 구성 요소는 다음과 같다.

  • 함수 이름
    • 함수 이름은 식별자로서 식별자 네이밍 규칙을 준수해야 한다.
    • 함수 이름은 함수 몸체 내에서만 참조할 수 있는 식별자다.
    • 이름이 있는 함수를 기명 함수(named function), 이름이 없는 함수를 무명/익명 함수(anonymous function)라 한다.
  • 매개변수 목록
    • 0개 이상의 매개변수를 소괄호로 감싸고 쉼표로 구분한다.
    • 각 매개변수에는 함수를 호출할 때 지정한 인수가 순서대로 할당된다. 즉, 매개변수 목록은 순서에 의미가 있다.
    • 매개변수는 함수 몸체 내에서 변수와 동일하게 취급된다. 따라서 매개변수도 식별자 네이밍 규칙을 준수해야 한다.
  • 함수 몸체
    • 함수가 호출되었을 때 일괄적으로 실행될 문들을 하나의 실행 단위로 정의한 코드 블록이다.
    • 함수 몸체는 함수 호출에 의해 실행된다.

함수는 객체지만 일반 객체와는 다르다. 일반 객체는 호출할 수 없지만 함수는 호출할 수 있다. 그리고 일반 객체에는 없는 함수 객체만의 고유한 프로퍼티를 갖는다.

함수 정의

함수 정의 방식에는 4가지가 있다.

1. 함수 선언문

1
2
3
4
5
6
7
8
9
10
11
12
// 함수 선언문
function add(x, y) {
return x + y;
}

// 함수 참조
// console.dir은 console.log와는 달리 함수 객체의 프로퍼티까지 출력한다.
// 단, Node.js 환경에서는 console.log와 같은 결과가 출력된다.
console.dir(add); // ƒ add(x, y)

// 함수 호출
console.log(add(2, 5)); // 7

함수 리터럴은 함수 이름을 생략할 수 있으나 함수 선언문은 함수 이름을 생략할 수 없다.

1
2
3
4
5
6
function add(x,y){
return x + y;
}

//크롬 개발자 도구 콘솔
<- undefined

함수 선언문은 표현식이 아닌 문이다. 크롬 콘솔에서 함수 선언문을 실행하면 완료 값 undefined가 출력된다.

1
2
3
4
5
6
7
8
// 함수 선언문은 표현식이 아닌 문이므로 변수에 할당할 수 없다.
// 하지만 함수 선언문이 변수에 할당되는 것처럼 보인다.
var add = function add(x, y) {
return x + y;
};

// 함수 호출
console.log(add(2, 5)); // 7

JS 엔진은 동일한 코드도 코드의 문맥에 따라 해석이 달라질 수 있다. 예를 들어 단독으로 존재하는 { }은 블록문으로 해석하지만 { }이 피연산자로써 값으로 평가되는 문맥에서는 객체 리터럴로 해석한다. 이와 같이 문맥에 따라 코드 해석이 달라진다.

함수의 경우에도 동일한 코드를 표현식이 아닌 함수 선언문, 표현식인 기명 함수 함수 리터럴에 대한 판단을 문맥에 따라 해석한다. 기명 함수 리터럴을 단독으로 사용하면 함수 선언문으로 해석하고, 표현식인 문맥(변수에 할당, 피연산자로 사용 등)에 사용되는 기명 함수 리터럴은 함수 리터럴 표현식으로 해석한다. 하지만 함수를 생성하는 내부 동작에 차이가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 기명 함수 리터럴을 단독으로 사용하면 함수 선언문으로 해석된다.
// 함수 선언문에서는 함수 이름을 생략할 수 없다.
function foo() {
console.log("foo");
}
foo(); // foo

// 함수 리터럴을 피연산자로 사용하면 함수 선언문이 아니라 함수 리터럴 표현식으로 해석된다.
// 함수 리터럴에서는 함수 이름을 생략할 수 있다.
(function bar() {
console.log("bar");
});
bar(); // ReferenceError: bar is not defined

함수는 함수 이름으로 호출하는 것이 아니라 함수 객체를 가리키는 식별자로 호출한다.
함수 선언문으로 생성한 함수를 호출한 것은 함수 이름이 아니라 자바스크립트 엔진이 암묵적으로 생성한 함수 이름과 동일한 이름의 식별자인 것이다. 함수 이름과 변수 이름이 일치하므로 함수 이름으로 호출되는 듯하지만 사실은 식별자로 호출된 것이다.

foo()는 함수 선언문으로 해석되어 호출이 가능하다. JS 엔진은 생성된 함수를 호출하기 위해 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고 해당 함수 객체를 할당하기 때문이다.
bar()는 함수 리터럴로 해석되었다. 때문에 함수 리터럴인 bar()는 변수에 참조되어야만 호출할 수 있기 때문에 참조되지 않은 함수 리터럴 bar()의 결과로 ReferenceError가 출력된다. 이는 “함수 리터럴의 함수 이름은 함수 몸체 내에서만 참조할 수 있는 식별자다” 규칙 때문이다.

2. 함수 표현식

자바스크립트의 함수는 객체 타입의 값이다. 자바스크립트의 함수는 값처럼 변수에 할당할 수도 있고 프로퍼티 값이 될 수도 있으며 배열의 요소가 될 수도 있다. 이처럼 값의 성질을 갖는 객체를 일급 객체(first-class object) (다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체) 라 한다. 함수가 일급 객체라는 것은 함수를 값처럼 자유롭게 사용할 수 있다는 의미다.

함수는 일급 객체이므로 함수 리터럴로 생성한 함수 객체를 변수에 할당할 수 있다. 이러한 함수 정의 방식을 함수 표현식이라 한다.

1
2
3
4
5
6
7
// 함수 표현식
// 함수 표현식의 함수 리터럴은 함수 이름을 생략하는 것(익명 함수)이 일반적이다.
var add = function (x, y) {
return x + y;
};

console.log(add(2, 5)); // 7

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 함수 참조
console.dir(add); // ƒ add(x, y)
console.dir(sub); // undefined

// 함수 호출
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;
};

함수 선언문으로 정의한 함수는 함수 선언문 이전에 호출할 수 있다. 그러나 함수 표현식으로 정의한 함수는 함수 표현식 이전에 호출할 수 없다. 이는 함수 선언문으로 정의한 함수와 함수 표현식으로 정의한 함수의 생성 시점이 다르기 때문이다.

모든 선언문과 마찬가지로 함수 선언문도 runtime 이전에 JS 엔진에 의해 먼저 실행되어 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고 함수 객체를 할당한다. 때문에 runtime에는 이미 함수 객체가 생성되어 식별자에 할당이 완료된 상태임으로 함수 선언문 이전에 함수를 참조하고 호출할 수 있다. 이처럼 함수 선언문이 코드의 선두로 끌어 올려진 것처럼 동작하는 특징을 함수 호이스팅이라 한다.

변수 호이스팅과 다른 점은 var 키워드로 선언된 변수는 undefined로 초기화되고, 함수 선언문을 통해 암묵적으로 생성된 식별자는 함수 객체로 초기화된다. 따라서 var 키워드를 사용한 변수 선언문 이전에 변수를 참조하면 변수 호이스팅에 의해 undefined로 평가되지만 함수 선언문으로 정의한 함수를 함수 선언문 이전에 호출하면 함수 호이스팅에 의해 호출이 가능하다.

함수 표현식의 경우 var 변수에 할당되는 함수 리터럴이기 때문에 runtime 이전에 변수가 undefined로 초기화되며 변수 할당문의 값은 할당문이 실행되는 시점, 즉 runtime에 평가되기 때문에 함수 표현식의 함수 리터럴도 runtime 때 평가되어 함수 객체가 된다. 따라서 함수 표현식으로 함수를 정의하면 함수 호이스팅이 아닌 변수 호이스팅이 발생한다.

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

3. Function 생성자 함수

기본 제공하는 빌트인 함수인 Function 생성자 함수에 매개변수 목록과 함수 몸체를 문자열로 전달하면서 new 연산자와 함께 호출하면 함수 객체를 생성해서 반환한다. 사실 new 연산자 없이 호출해도 결과는 동일하다.

1
2
3
var add = new Function("x", "y", "return x + y");

console.log(add(2, 5)); // 7

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var add1 = (function () {
var a = 10;
return function (x, y) {
return x + y + a;
};
})();

console.log(add1(1, 2)); // 13

// Function 생성자 함수로 생성한 함수는 클로저(closure)를 생성하지 않는다.
var add2 = (function () {
var a = 10;
return new Function("x", "y", "return x + y + a;");
})();

console.log(add2(1, 2)); // ReferenceError: a is not defined

4. 화살표 함수

function 키워드 대신 => 를 사용해 간략하게 익명 함수를 선언한다.

1
2
3
// 화살표 함수
const add = (x, y) => x + y;
console.log(add(2, 5)); // 7

화살표 함수는 기존 함수보다 표현과 내부 동작이 간략화되었다.
(화살표 함수는 생성자 함수로 사용할 수 없으며 기존의 함수와 this 바인딩 방식이 다르고, prototype 프로퍼티가 없으며 arguments 객체를 생성하지 않는다.)

함수 호출

매개변수와 인수

1
2
3
4
5
6
7
8
// 함수 선언문
function add(x, y) {
return x + y;
}

// 함수 호출
// 인수 1과 2는 매개변수 x와 y에 순서대로 할당되고 함수 몸체의 문들이 실행된다.
var result = add(1, 2);

함수가 호출되면 함수 몸체 내에서 암묵적으로 매개변수가 생성되고 일반 변수와 마찬가지로 undefined로 초기화된 이후 인수가 순서대로 할당된다.
매개변수는 함수 몸체 내부에서만 참조할 수 있고 함수 몸체 외부에서는 참조할 수 없다. 매개변수의 스코프는 함수 내부이다.

함수는 매개변수의 개수와 인수의 개수가 일치하는지 체크하지 않는다. 함수를 호출할 때 매개변수의 개수만큼 인수를 전달하는 것이 일반적이지만 그렇지 않은 경우에도 에러가 발생하지는 않는다. 인수가 부족해서 인수가 할당되지 않은 매개변수의 값은 undefined이다.

1
2
3
4
5
function add(x, y) {
return x + y;
}
// 2 + undefined = NaN
console.log(add(2)); // NaN
1
2
3
4
5
6
7
8
9
10
// 인수가 더 많을 경우에 초과된 인수는 무시된다. 초과된 인수는 버려지는 것이 아니다.
// 모든 인수는 암묵적으로 arguments 객체의 프로퍼티로 보관된다.
function add(x, y) {
console.log(arguments);
// Arguments(3) [2, 5, 10, callee: ƒ, Symbol(Symbol.iterator): ƒ]

return x + y;
}

add(2, 5, 10); // 7

인수 확인

1
2
3
4
5
6
function add(x, y) {
return x + y;
}

console.log(add(2)); // NaN
console.log(add("a", "b")); // 'ab'

본래 목적은 숫자 타입 인수를 매개변수로 받으려했지만 문자열이 입력되었으며 이에 대한 연산이 진행됐다.
그 이유는 아래와 같다.

  • 자바스크립트 함수는 매개변수와 인수의 개수가 일치하는지 확인하지 않는다.
  • 자바스크립트는 동적 타입 언어다. 따라서 자바스크립트 함수는 매개변수의 타입을 사전에 지정할 수 없다.

해결 방법은 아래와 같은 코드이다.

1
2
3
4
5
6
7
8
9
10
11
function add(x, y) {
if (typeof x !== "number" || typeof y !== "number") {
// 매개변수를 통해 전달된 인수의 타입이 부적절한 경우 에러를 발생시킨다.
throw new TypeError("인수는 모두 숫자 값이어야 합니다.");
}

return x + y;
}

console.log(add(2)); // TypeError: 인수는 모두 숫자 값이어야 합니다.
console.log(add("a", "b")); // TypeError: 인수는 모두 숫자 값이어야 합니다.

위 예제의 경우, 인수의 개수는 확인하고 있지 않다. 인수가 전달되지 않은 경우 단축 평가를 사용해 매개변수에 기본값을 할당하는 방법도 있다.

1
2
3
4
5
6
7
8
9
10
11
function add(a, b, c) {
a = a || 0;
b = b || 0;
c = c || 0;
return a + b + c;
}

console.log(add(1, 2, 3)); // 6
console.log(add(1, 2)); // 3
console.log(add(1)); // 1
console.log(add()); // 0

ES6부터 매개변수 기본값이 도입됐다. 매개변수 기본값은 매개변수에 인수를 전달하지 않았을 경우와 undefined를 전달한 경우에만 유효하다.

1
2
3
4
5
6
7
8
function add(a = 0, b = 0, c = 0) {
return a + b + c;
}

console.log(add(1, 2, 3)); // 6
console.log(add(1, 2)); // 3
console.log(add(1)); // 1
console.log(add()); // 0

반환문

실행 결과를 함수 외부로 반환(return)할 수 있다.

1
2
3
4
5
6
7
function multiply(x, y) {
return x * y; // 반환문
}

// 함수 호출은 반환값으로 평가된다.
var result = multiply(3, 5);
console.log(result); // 15

반환문은 함수의 실행을 중단하고 함수 몸체를 빠져나간다. 따라서 반환문 이후의 문은 실행되지 않고 무시된다.

1
2
3
4
5
6
7
function multiply(x, y) {
return x * y; // 반환문
// 반환문 이후에 다른 문이 존재하면 그 문은 실행되지 않고 무시된다.
console.log("실행되지 않는다.");
}

console.log(multiply(3, 5)); // 15

return 키워드 뒤에 반환값으로 사용할 표현식을 지정하지 않으면 undefined가 반환된다.

1
2
3
4
5
function foo() {
return;
}

console.log(foo()); // undefined

반환문은 생략 가능하다.

1
2
3
4
5
function foo() {
// 반환문을 생략하면 암묵적으로 undefined가 반환된다.
}

console.log(foo()); // undefined

반환문은 함수 몸체 내부에서만 사용할 수 있다. 전역에서 반환문을 사용하면 문법 에러(SyntaxError: Illegal return statement)가 발생한다.

참조에 의한 전달과 외부 상태의 변경

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 매개변수 primitive는 원시값을 전달받고, 매개변수 obj는 객체를 전달받는다.
function changeVal(primitive, obj) {
primitive += 100;
obj.name = "Kim";
}

// 외부 상태
var num = 100;
var person = { name: "Lee" };

console.log(num); // 100
console.log(person); // {name: "Lee"}

// 원시값은 값 자체가 복사되어 전달되고 객체는 참조값이 복사되어 전달된다.
changeVal(num, person);

// 원시값은 원본이 훼손되지 않는다.
console.log(num); // 100

// 객체는 원본이 훼손된다.
console.log(person); // {name: "Kim"}

원시 타입 인수를 전달받은 매개변수의 경우, 원시값은 변경 불가능한 값이므로 재할당을 통해 할당된 원시값을 새로운 원시값으로 교체했고, 객체 타입 인수를 전달받은 매개변수의 경우, 객체는 변경 가능한 값이므로 직접 변경할 수 있기 때문에 재할당 없이 직접 할당된 객체를 변경했다.

함수가 외부 상태 (위 예제의 경우, 객체를 할당한 person 변수) 를 변경하면 상태 변화를 추적하기 어려워진다. 이는 복잡성을 증가시키고 가독성을 해친다. 이러한 현상은 객체가 변경할 수 있는 값이며, 참조에 의한 전달 방식으로 동작하기 때문에 발생하는 부작용이다. 여러 변수가 참조에 의한 전달 방식을 통해 참조값을 공유하고 있다면 언제든지 참조하고 있는 객체를 직접 변경할 수 있다.

이러한 문제의 해결 방법 중 하나는 깊은 복사를 통해 새로운 객체를 생성하고 재할당을 통해 교체한다. 이를 통해 외부 상태가 변경되는 부수 효과를 없앨 수 있다.

다양한 함수의 형태

즉시 실행 함수

즉시 실행 함수는 단 한 번만 호출되며 다시 호출할 수 없다.

1
2
3
4
5
6
// 익명 즉시 실행 함수
(function () {
var a = 3;
var b = 5;
return a * b;
})();

즉시 실행 함수는 함수 이름이 없는 익명 함수를 사용하는 것이 일반적이다.
즉시 실행 함수는 반드시 그룹 연산자 (…)로 감싸야 한다.

그룹 연산자 (…) 내의 기명 함수는 함수 리터럴로 평가되며 함수 이름은 함수 몸체에서만 참조할 수 있는 식별자이므로 즉시 실행 함수를 다시 호출할 수는 없다.

1
2
3
4
5
6
7
8
// 기명 즉시 실행 함수
(function foo() {
var a = 3;
var b = 5;
return a * b;
})();

foo(); // ReferenceError: foo is not defined

그룹 연산자로 함수를 묶은 이유는 먼저 함수 리터럴을 평가해서 함수 객체를 생성하기 위해서다.
함수 리터럴을 평가해서 함수 객체를 생성할 수 있다면 다음과 같이 그룹 연산자 이외의 연산자를 사용해도 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(function () {
// ...
})();

(function () {
// ...
})();

!(function () {
// ...
})();

+(function () {
// ...
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 즉시 실행 함수도 일반 함수처럼 값을 반환할 수 있다.
var res = (function () {
var a = 3;
var b = 5;
return a * b;
})();

console.log(res); // 15

// 즉시 실행 함수에도 일반 함수처럼 인수를 전달할 수 있다.
res = (function (a, b) {
return a * b;
})(3, 5);

console.log(res); // 15
  • 즉시 실행 함수도 일반 함수처럼 값을 반환할 수 있고 인수를 전달할 수도 있다.
  • 즉시 실행 함수 내에 코드를 모아 두면 혹시 있을 수도 있는 변수나 함수 이름의 충돌을 방지할 수 있다.

재귀 함수

함수가 자기 자신을 호출하는 것을 재귀 호출(recursive call)이라 한다. 재귀 함수는 자기 자신을 호출하는 함수이다.
재귀 함수는 자신을 무한 재귀 호출한다. 따라서 재귀 함수 내에는 재귀 호출을 멈출 수 있는 탈출 조건을 반드시 만들어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var factorial = function foo(n) {
// 탈출 조건: n이 1 이하일 때 재귀 호출을 멈춘다.
if (n <= 1) return 1;
// 재귀 호출
return n * factorial(n - 1); // foo(n-1)도 가능하다.
};

// foo()는 호출 불가
console.log(factorial(0)); // 0! = 1
console.log(factorial(1)); // 1! = 1
console.log(factorial(2)); // 2! = 2 * 1 = 2
console.log(factorial(3)); // 3! = 3 * 2 * 1 = 6
console.log(factorial(4)); // 4! = 4 * 3 * 1 * 1 = 24
console.log(factorial(5)); // 5! = 5 * 4 * 3 * 2 * 1 = 120

함수 이름은 함수 몸체 내부에서만 유효하다. 따라서 함수 내부에서는 함수 이름과 함수를 가리키는 식별자 모두를 이용해 자기 자신을 호출할 수 있다. 단, 함수 외부에서는 함수를 가리키는 식별자를 이용해야한다.

중첩 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 중첩 함수를 포함하는 함수는 외부 함수라 한다.
function outer() {
var x = 1;

// 중첩 함수 또는 내부 함수라 한다.
// 일반적으로 중첩 함수는 자신을 포함하는 외부 함수를 돕는 헬퍼 함수의 역할을 한다.
function inner() {
var y = 2;
// 외부 함수의 변수를 참조할 수 있다.
console.log(x + y); // 3
}

inner();
}

outer();

ES6 이전에는 코드의 최상위 또는 다른 함수 내부에서만 함수 선언문을 정의할 수 있었으나 ES6부터는 if 문이나 for 문 등의 코드 블록 내에서도 정의할 수 있다.
단, 호이스팅으로 인해 혼란이 발생할 수 있으므로 if 문이나 for 문 등의 코드 블록에서 함수 선언문을 통해 함수를 정의하는 것은 바람직하지 않다.

콜백 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 외부에서 전달받은 f를 n만큼 반복 호출한다
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

var logOdds = function (i) {
if (i % 2) console.log(i);
};

// 반복 호출할 함수를 인수로 전달한다.
repeat(5, logOdds); // 1 3

JS의 함수는 일급 객체이므로 매개변수를 통해 함수를 전달할 수 있다. repeat 함수는 외부에서 로직의 일부분을 함수로 전달받아 수행하므로 유연한 구조를 갖는다.
매개변수를 통해 다른 함수의 내부로 전달되는 함수를 콜백 함수라고 하며(위 logAll, logOdds 함수, 매개변수를 통해 함수의 외부에서 콜백 함수를 전달받은 함수를 고차 함수(Higher-Order Function, HOF)라고 한다.(위 repeat 함수)
매개변수를 통해 함수를 전달받거나 반환값으로 함수를 반환하는 함수를 함수형 프로그래밍 패러다임에서 고차 함수라 한다.

중첩 함수가 외부 함수를 돕는 헬퍼 함수의 역할을 하는 것처럼 콜백 함수도 고차 함수에 전달되어 헬퍼 함수의 역할을 한다. 단, 중첩 함수는 내부에 고정되어 있지만 콜백 함수는 함수 외부에서 고차 함수 내부로 주입하기 때문에 자유롭게 교체할 수 있다는 장점이 있다. 즉, 고차 함수는 콜백 함수를 자신의 일부분으로 합성한다.

고차 함수는 매개변수를 통해 전달받은 콜백 함수의 호출 시점을 결정해서 호출한다. 다시 말해, 콜백 함수는 고차 함수에 의해 호출되며 이때 고차 함수는 필요에 따라 콜백 함수에 인수를 전달할 수 있다. 따라서 고차 함수에 콜백 함수를 전달할 때 콜백 함수를 호출하지 않고 함수 자체를 전달해야 한다.

콜백 함수가 고차 함수 내부에만 호출된다면 콜백 함수를 익명 함수 리터럴로 정의하면서 곧바로 고차 함수에 전달하는 것이 일반적이다.

1
2
3
4
5
// 익명 함수 리터럴을 콜백 함수로 고차 함수에 전달한다.
// 익명 함수 리터럴은 repeat 함수를 호출할 때마다 평가되어 함수 객체를 생성한다.
repeat(5, function (i) {
if (i % 2) console.log(i);
}); // 1 3

콜백 함수를 다른 곳에서도 호출할 필요가 있거나, 콜백 함수를 전달받는 함수가 자주 호출된다면 함수 외부에서 콜백 함수를 정의한 후 함수 참조를 고차 함수에 전달하는 편이 효율적이다.

1
2
3
4
5
6
7
// logOdds 함수는 단 한 번만 생성된다.
var logOdds = function (i) {
if (i % 2) console.log(i);
};

// 고차 함수에 함수 참조를 전달한다.
repeat(5, logOdds); // 1 3

순수 함수와 비순수 함수

부수 효과가 없는 함수를 순수 함수(pure function)라 하고, 외부 상태에 의존하거나 외부 상태를 변경하는 부수 효과가 있는 함수를 비순수 함수(impure function)라고 한다.

순수 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
var count = 0; // 현재 카운트를 나타내는 상태

// 순수 함수 increase는 동일한 인수가 전달되면 언제나 동일한 값을 반환한다.
function increase(n) {
return ++n;
}

// 순수 함수가 반환한 결과값을 변수에 재할당해서 상태를 변경
count = increase(count);
console.log(count); // 1

count = increase(count);
console.log(count); // 2

비순수 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
var count = 0; // 현재 카운트를 나타내는 상태: increase 함수에 의해 변화한다.

// 비순수 함수
function increase() {
return ++count; // 외부 상태에 의존하며 외부 상태를 변경한다.
}

// 비순수 함수는 외부 상태(count)를 변경하므로 상태 변화를 추적하기 어려워진다.
increase();
console.log(count); // 1

increase();
console.log(count); // 2

함수 내부에서 외부 상태를 직접 참조하지 않더라도 매개변수를 통해 객체를 전달받으면 비순수 함수가 된다.
함수가 외부 상태를 변경하면 상태 변화를 추적하기 어렵기 때문에 함수 외부 상태 변경을 지양하는 순수 함수를 사용하는 것이 좋다.

함수형 프로그래밍

함수형 프로그래밍은 순수 함수와 보조 함수의 조합을 통해 외부 상태를 변경하는 부수 효과를 최소화해서 불변성(immutability)을 지향하는 프로그래밍 패러다임이다. 로직 내의 흐름을 어렵게하는 조건문과 반복문을 제거해서 복잡성을 해결하며, 변경될 가능성이 있는 변수 사용을 최소화해서 상태 변경을 피해 오류를 최소화하는 것을 목표로 한다.
함수형 프로그래밍은 결국 순수 함수를 통해 부수 효과를 최대한 억제해 오류를 피하고 프로그램의 안정성을 높이려는 노력의 일환이라 할 수 있다. 자바스크립트는 멀티 패러다임 언어이므로 객체지향 프로그래밍뿐만 아니라 함수형 프로그래밍을 적극적으로 활용하고 있다.

참고 도서: 모던 자바스크립트 Deep Dive

Nyong’s GitHub

오늘 한 것

제어문

숫자, 문자열은 객체로 쓰면 암묵적으로 객체 타입으로 변환해 참조한다.

1
2
3
4
5
6
7
8
// 문자열은 유사배열이므로 for 문으로 순회할 수 있다.
for (var i = 0; i < string.length; i++) {
// 문자열의 개별 문자가 'l'이면
if (string[i] === search) {
index = i;
break; // 반복문을 탈출한다.
}
}

문자열은 원시값이지만 유사배열로서 객체로 암묵적 타입 변환 후 length 프로퍼티를 참조할 수 있다.

타입 변환과 단축 평가

boolean 타입 변환

1
2
3
4
5
6
7
8
9
// 전달받은 인수가 Falsy 값이면 true, Truthy 값이면 false를 반환한다.
function isFalsy(v) {
return !v;
}

// 전달받은 인수가 Truthy 값이면 true, Falsy 값이면 false를 반환한다.
function isTruthy(v) {
return !!v;
}

명시적으로 타입을 변환하는 방법

  1. String(), Number() 등 생성자 함수를 new 연산자 없이 호출하는 방법
  2. Object.prototype.toString / parseInt() 메서드를 사용하는 방법
  3. 연산자를 이용하는 방법

명시적인 것은 3 → 2 → 1 순이다.

2번 메서드를 사용하는 것도 좋지만 메서드 마다 사용법도 다르고 3번이 가장 짧고 명시적이다.

1번 생성자 함수는 본래 정석인 사용법이 아니기 때문에 지양한다.

객체 리터럴

객체

1
2
3
4
5
6
var person = {
// 프로퍼티 키는 name, 프로퍼티 값은 'Lee'
name: "Lee",
// 프로퍼티 키는 age, 프로퍼티 값은 20
age: 20,
};

프로퍼티 키는 식별자가 아니기 때문에 식별자 명명 규칙을 안 지켜도 문자열이기만 하면 된다.

1
2
3
4
5
6
7
var person = {
// 프로퍼티 키는 name, 프로퍼티 값은 'Lee'
"first-name": "Lee", // 이런식으로 묶기만해주면 식별자 명명 규칙을 안 지켜도됨
// 프로퍼티 키는 age, 프로퍼티 값은 20
age: 20,
};
console.log(person["first-name"]);

식별자 명명 규칙을 지키지 않을 시에는 [‘first-name’] 대괄호 표기법을 따라야한다.

제일 좋은 건 식별자 명명 규칙을 지키고 편하게 쓰자.

프로퍼티 삭제 → delete 연산자

1
delete person.age;

사실 굳이 프로퍼티를 지울 일은 많지 않음. → 안티패턴(할 수는 있지만, 굳이 하는 것을 지양하는 것)이다.

delete 연산자는 안 쓰는게 좋다.

프로퍼티 축약표현 (중요!)

프로퍼티 키와 변수 이름이 같을 경우 축약할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ES5
var x = 1,
y = 2;

var obj = {
x: x,
y: y,
};

console.log(obj); // {x: 1, y: 2}

// ES6
let x = 1,
y = 2;

// 프로퍼티 축약 표현
const obj = { x, y }; // = {x: x, y: y}

console.log(obj); // {x: 1, y: 2}

객체 리터럴 내부에서 계산된 프로퍼티 키를 동적으로 생성할 수 있다. (ES5에서는 객체 리터럴 외부에서 해야했다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ES5
var prefix = "prop";
var i = 0;

var obj = {};

// 계산된 프로퍼티 이름으로 프로퍼티 키 동적 생성
obj[prefix + "-" + ++i] = i;
obj[prefix + "-" + ++i] = i;
obj[prefix + "-" + ++i] = i;

console.log(obj); // {prop-1: 1, prop-2: 2, prop-3: 3}

// ES6
const prefix = "prop";
let i = 0;

// 객체 리터럴 내부에서 계산된 프로퍼티 이름으로 프로퍼티 키 동적 생성
// 프로퍼티 키로 사용할 표현식을 대괄호([…])로 묶어야 한다.
const obj = {
[`${prefix}-${++i}`]: i,
[`${prefix}-${++i}`]: i,
[`${prefix}-${++i}`]: i,
};

console.log(obj); // {prop-1: 1, prop-2: 2, prop-3: 3}

메서드 축약 표현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ES5
var obj = {
name: "Lee",
sayHi: function () {
console.log("Hi! " + this.name);
},
};

obj.sayHi(); // Hi! Lee

// ES6
const obj = {
name: "Lee",
// 메서드 축약 표현
sayHi() {
console.log("Hi! " + this.name);
},
};

obj.sayHi(); // Hi! Lee

Nyong’s GitHub

오늘 한 것

원시값과 객체의 비교

원시 타입과 객체 타입은 크게 세 가지 측면에서 다르다.

  1. 원시 타입의 값은 변경 불가능한 값(immutable value)이다. 객체(참조) 타입의 값은 변경 가능한 값(mutable value)이다.
  2. 원시값을 변수에 할당하면 변수(확보된 메모리 공간)에는 실제 값이 저장된다. 객체를 변수에 할당하면 변수에는 참조값이 저장된다.
  3. 원시값을 갖는 변수를 다른 변수에 할당하면 원본의 원시값이 복사되어 전달된다. 이를 값에 의한 전달(pass by value)이라 한다. 객체를 가리키는 변수를 다른 변수에 할당하면 원본의 참조값이 복사되어 전달된다. 이를 참조에 의한 전달(pass by reference)이라 한다.

원시값

원시값은 변경 불가 값, 읽기 전용 값이다. 원시값의 이러한 특성을 불변성(immutability)이라 한다. 때문에 원시값은 데이터의 신뢰성을 보장한다.

1
2
3
4
5
6
7
// const 키워드를 사용해 선언한 변수는 재할당이 금지된다. 상수는 재할당이 금지된 변수일 뿐이다.
const o = {};

// const 키워드를 사용해 선언한 변수에 할당한 원시값(상수)은 변경할 수 없다.
// 하지만 const 키워드를 사용해 선언한 변수에 할당한 객체는 변경할 수 있다.
o.a = 1;
console.log(o); // {a: 1}

원시값을 할당한 변수에 새로운 값을 재할당하면 메모리 공간에 저장되어 있는 원시값을 변경하는 것이 아니라 새로운 메모리 공간 확보 후 재할당한 원시값을 저장하고 변수는 새롭게 재할당한 원시값을 가리킨다.

문자열도 원시값이다.

1
2
3
4
5
6
7
var str = "string";

// 문자열은 유사 배열이므로 배열과 유사하게 인덱스를 사용해 각 문자에 접근할 수 있다.
// 하지만 문자열은 원시값이므로 변경할 수 없다. 이때 에러가 발생하지 않는다.
str[0] = "S";

console.log(str); // string

변수에 새로운 문자열을 재할당하는 것은 물론 가능하다. 이는 기존 문자열을 변경하는 것이 아니라 새로운 문자열을 새롭게 할당하는 것이기 때문이다.

객체

객체는 프로퍼티의 개수가 정해져 있지 않으며, 동적으로 추가되고 삭제할 수 있다. 또한 프로퍼티의 값에도 제약이 없다. 따라서 객체는 원시값과 같이 확보해야 할 메모리 공간의 크기를 사전에 정해 둘 수 없다.
객체를 생성하고 프로퍼티에 접근하는 것도 원시값과 비교할 때 비용이 많이 드는 일이다. 따라서 객체는 원시값과는 다른 방식으로 동작하도록 설계되어 있다.

javascript 객체 관리 방식

  • 자바스크립트 객체는 프로퍼티 키를 인덱스로 사용하는 해시 테이블이라고 생각할 수 있다.

  • 자바, C++ 같은 클래스 기반 객체지향 프로그래밍 언어는 사전에 정의된 클래스를 기반으로 객체(인스턴스)를 생성한다. 객체를 생성하기 이전에 이미 프로퍼티와 메서드가 정해져 있으며 그대로 객체를 생성한다. 객체가 생성된 이후에는 프로퍼티를 삭제하거나 추가할 수 없다.

  • 자바스크립트는 클래스 없이 객체를 생성할 수 있으며 객체가 생성된 이후라도 동적으로 프로퍼티와 메서드를 추가할 수 있다. 이는 사용하기 매우 편리하지만 성능 면에서는 이론적으로 클래스 기반 객체지향 프로그래밍 언어의 객체보다 생성과 프로퍼티 접근에 비용이 더 많이 드는 비효율적인 방식이다.

  • 따라서 V8 자바스크립트 엔진에서는 프로퍼티에 접근하기 위해 동적 탐색(dynamic lookup) 대신 히든 클래스(hidden class)라는 방식을 사용해 C++ 객체의 프로퍼티에 접근하는 정도의 성능을 보장한다. 히든 클래스는 자바와 같이 고정된 객체 레이아웃(클래스)과 유사하게 동작한다.

히든 클래스에는 다음과 같은 특징이 있습니다.

  • 객체는 반드시 하나의 히든 클래스를 참조한다.
  • 히든 클래스는 각 프로퍼티에 대해 메모리 오프셋을 가지고 있다.
  • 동적으로 새로운 프로퍼티가 만들어질 때, 혹은 기존 프로퍼티가 삭제되거나 기존 프로퍼티의 데이터 타입이 바뀔 때는 신규 히든 클래스가 생성되며, 신규 히든 클래스는 기존 프로퍼티에 대한 정보를 유지하면서 추가적으로 새 프로퍼티의 오프셋을 가지게 된다.
  • 히든 클래스는 프로퍼티에 대해 변경이 발생했을 때 참조해야 하는 히든 클래스에 대한 정보를 갖는다.
  • 객체에 새로운 프로퍼티가 만들어지면, 현재 참조하고 있는 히든 클래스의 전환 정보를 확인한 후, 현재 프로퍼티에 대한 변경이 전환 정보의 조건과 일치하면, 객체의 참조 히든 클래스를 조건에 명시된 히든 클래스로 변경시킨다.

변경 가능한 값

객체(참조) 타입의 값, 즉 객체는 변경 가능한 값(mutable value)이다.
객체를 할당한 변수가 기억하는 메모리 주소를 통해 메모리 공간에 접근하면 참조값(reference value)에 접근할 수 있다. 참조값은 생성된 객체가 저장된 메모리 공간의 주소, 그 자체이며 참조값을 통해 실제 객체에 접근한다.

1
2
3
4
5
6
7
// 할당이 이뤄지는 시점에 객체 리터럴이 해석되고, 그 결과 객체가 생성된다.
var person = {
name: "Lee",
};

// person 변수에 저장되어 있는 참조값으로 실제 객체에 접근해서 그 객체를 반환한다.
console.log(person); // {name: "Lee"}
  • 객체를 할당한 변수의 경우 “변수는 객체를 참조하고 있다” 또는 “변수는 객체를 가리키고(point) 있다”라고 표현한다.

  • 객체는 크기가 매우 클 수도 있고, 원시값처럼 크기가 일정하지도 않으며, 프로퍼티 값이 객체일 수도 있어서 복사(deep copy)해서 생성하는 비용이 많이 들기 때문에 메모리의 효율적 소비가 어렵고 성능이 나빠진다. 따라서 메모리를 효율적으로 사용하고 객체를 복사해 생성하는 비용을 절약하여 성능을 향상시키기 위해 객체는 변경 가능한 값으로 설계되어 있다. 메모리 사용의 효율성과 성능을 위해 어느 정도의 구조적인 단점을 감안한 설계라고 할 수 있다.

  • 객체는 이러한 구조적 단점에 따른 부작용이 있다. 그것은 원시값과는 다르게 여러 개의 식별자가 하나의 객체를 공유할 수 있다는 것이다.

얕은 복사(shallow copy)와 깊은 복사(deep copy)
객체를 프로퍼티 값으로 갖는 객체의 경우 얕은 복사는 한 단계까지만 복사하는 것을 말하고 깊은 복사는 객체에 중첩되어 있는 객체까지 모두 복사하는 것을 말한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const o = {
a: {
b: 2,
},
f() {},
};

// 얕은 복사
let c = { ...o }; // "35. 스프레드 문법" 참고
console.log(o === c); // false
console.log(o.a === c.a); // true

// 얕은 복사
c = Object.assign({}, o);
console.log(o === c); // false
console.log(o.a === c.a); // true

// JSON.parse와 JSON.stringify를 사용한 깊은 복사
c = JSON.parse(JSON.stringify(o));
console.log(o === c); // false
console.log(o.a === c.a); // false
// 메서드가 사라진다!
console.log(c.f); // undefined

// lodash의 cloneDeep을 사용한 깊은 복사
// "npm install lodash"로 lodash를 설치한 후, Node.js 환경에서 실행
const _ = require("lodash");

c = _.cloneDeep(o);
console.log(o === c); // false
console.log(o.a === c.a); // false
console.log(c.f); // f

얕은 복사와 깊은 복사로 생성된 객체는 원본과는 다른 객체다. 즉, 원본과 복사본은 참조값이 다른 별개의 객체다. 하지만 얕은 복사는 객체에 중첩되어 있는 객체의 경우 참조값을 복사하고 깊은 복사는 객체에 중첩되어 있는 객체까지 모두 복사해서 원시값처럼 완전한 복사본을 만든다는 차이가 있다.

원본과 사본은 저장된 메모리 주소는 다르지만 동일한 참조값을 갖는다. 다시 말해, 원본과 사본 모두 동일한 객체를 가리킨다. 이것은 하나의 객체를 공유한다는 것을 의미한다. 따라서 어느 한쪽에서 객체를 변경하면 서로 영향을 주고받는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var person = {
name: "Lee",
};

// 참조값을 복사(얕은 복사). copy와 person은 동일한 참조값을 갖는다.
var copy = person;

// copy와 person은 동일한 객체를 참조한다.
console.log(copy === person); // true

// copy를 통해 객체를 변경한다.
copy.name = "Kim";

// person을 통해 객체를 변경한다.
person.address = "Seoul";

// copy와 person은 동일한 객체를 가리킨다.
// 따라서 어느 한쪽에서 객체를 변경하면 서로 영향을 주고 받는다.
console.log(person); // {name: "Kim", address: "Seoul"}
console.log(copy); // {name: "Kim", address: "Seoul"}

참고 LINE Engineering
참고 도서: 모던 자바스크립트 Deep Dive

Nyong’s GitHub

오늘 한 것

객체 리터럴

객체

원시값을 제외한 나머지 값들(함수, 배열, 정규표현식 등)은 모두 객체다. 원시값의 경우 변경 불가능한 하나의 값을 나타내지만, 객체는 변경 가능한 다양한 타입의 값으로 하나의 단위로 구성한 복잡한 자료구조다.

객체는 0개 이상의 프로퍼티로 구성된 집합이며, 프로퍼티는 키(key)와 값(value)으로 구성된다.

1
2
3
4
var person = {
name: "Choi", // 프로퍼티 = 프로퍼티 키: 프로퍼티 값
age: 26,
};

자바스크립트에서 사용할 수 있는 모든 값은 프로퍼티 값이 될 수 있다. 함수도 프로퍼티 값으로 사용할 수 있다. 프로퍼티 값이 함수일 경우 일반 함수와의 구분을 위해 메서드(method)라 부른다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var person = {
name: 'Choi',
age: 26
say: function() {
console.log('hello');
}
}
/********************/
var circle = {
radius: 5, // ← 프로퍼티

// 원의 지름
getDiameter: function () { // ← 메서드
return 2 * this.radius; // this는 circle을 가리킨다.
}
};

console.log(circle.getDiameter()); // 10

위처럼 객체는 프로퍼티와 메서드로 구성된 집합체이다. 프로퍼티와 메서드의 역할은 다음과 같다.

  • 프로퍼티: 객체의 상태를 나타내는 값(data)
  • 메서드: 프로퍼티(상태 데이터)를 참조하고 조작할 수 있는 동작(behavior)

객체 리터럴에 의한 객체 생성

자바스크립트는 프로토타입 기반 객체지향 언어로서 클래스 기반 객체지향 언어와는 달리 다양한 객체 생성 방법을 지원한다.

  • 객체 리터럴
  • Object 생성자 함수
  • 생성자 함수
  • Object.create 메서드
  • 클래스 (ES6)

가장 일반적이고 간단한 방법은 객체 리터럴을 사용하는 방법이다. 변수에 할당이 이루어지는 시점에 자바스크립트 엔진은 객체 리터럴을 해석해 객체를 생성한다.
객체 리터럴의 중괄호는 코드 블록을 의미하지 않기 때문에 세미콜론(;)을 붙여줘야한다.

1
2
3
4
5
6
7
8
9
var person = {
name: "Lee",
sayHello: function () {
console.log(`Hello! My name is ${this.name}.`);
},
};

console.log(typeof person); // object
console.log(person); // {name: "Lee", sayHello: ƒ}

프로퍼티

객체는 프로퍼티의 집합이며 프로퍼티는 키와 값으로 구성

  • 프로퍼티 키 : 빈 문자열을 포함하는 모든 문자열 또는 심벌 값
  • 프로퍼티 값 : 자바스크립트에서 사용할 수 있는 모든 값

심벌 값도 프로퍼티 키로 사용할 수 있지만 일반적으로 문자열을 사용한다.
프로퍼티 키는 문자열이므로 따옴표로 묶어야 한다. 하지만 식별자 네이밍 규칙을 준수하는 이름인 경우 따옴표를 생략할 수 있다. 그 외에 식별자 네이밍 규칙을 따르지 않는 이름에는 반드시 따옴표를 사용해야 한다.

1
2
3
4
5
6
7
8
9
10
11
var person = {
firstName: 'Ung-mo', // 식별자 네이밍 규칙을 준수하는 프로퍼티 키
'last-name': 'Lee' // 식별자 네이밍 규칙을 준수하지 않는 프로퍼티 키
};

console.log(person); // {firstName: "Ung-mo", last-name: "Lee"}

var person = {
firstName: 'Ung-mo',
last-name: 'Lee' // SyntaxError: Unexpected token -
};

프로퍼티 키를 동적으로 생성할 수도 있다. 이 경우에는 프로퍼티 키로 사용할 표현식을 대괄호([…])로 묶어야 한다.

1
2
3
4
5
6
7
8
9
var obj = {};
var key = "hello";

// ES5: 프로퍼티 키 동적 생성
obj[key] = "world";
// ES6: 계산된 프로퍼티 이름
// var obj = { [key]: 'world' };

console.log(obj); // {hello: "world"}

프로퍼티 키를 중복 선언하면 나중에 선언한 프로퍼티가 먼저 선언한 프로퍼티를 덮어쓴다.

1
2
3
4
5
6
var foo = {
name: "Lee",
name: "Kim",
};

console.log(foo); // {name: "Kim"}

프로퍼티 접근

프로퍼티에 접근하는 방법은 두 가지다.

  • 마침표 프로퍼티 접근 연산자(.)를 사용하는 마침표 표기법(dot notation)
  • 대괄호 프로퍼티 접근 연산자([…])를 사용하는 대괄호 표기법(bracket notation)
1
2
3
4
5
6
7
8
9
10
var person = {
name: "Lee",
};

// 마침표 표기법에 의한 프로퍼티 접근
console.log(person.name); // Lee

// 대괄호 표기법에 의한 프로퍼티 접근
//대괄호 프로퍼티 접근 연산자 내부에 지정하는 프로퍼티 키는 반드시 따옴표로 감싼 문자열이어야 한다.
console.log(person["name"]); // Lee

객체에 존재하지 않는 프로퍼티에 접근하면 undefined를 반환한다. 이때 ReferenceError가 발생하지 않는 데 주의하자.

1
2
3
4
5
var person = {
name: "Lee",
};

console.log(person.age); // undefined

프로퍼티 키가 숫자로 이뤄진 문자열인 경우 따옴표를 생략 가능.
그 외의 경우 대괄호 내에 들어가는 프로퍼티 키는 따옴표로 감싼 문자열이어야 한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var person = {
'last-name': 'Lee',
1: 10
};

person.'last-name'; // -> SyntaxError: Unexpected string

person.last-name; // -> 브라우저 환경: NaN
// -> Node.js 환경: ReferenceError: name is not defined

person[last-name]; // -> ReferenceError: last is not defined
person['last-name']; // -> Lee

// 프로퍼티 키가 숫자로 이뤄진 문자열인 경우 따옴표를 생략할 수 있다.
person.1; // -> SyntaxError: Unexpected number
person.'1'; // -> SyntaxError: Unexpected string
person[1]; // -> 10 : person[1] -> person['1']
person['1']; // -> 10

Node.js 환경에서는 현재 어디에도 name이라는 식별자(변수, 함수 등의 이름) 선언이 없으므로 “ReferenceError: name is not defined”이라는 에러가 발생한다. 그런데 브라우저 환경에서는 name이라는 전역 변수(전역 객체 window의 프로퍼티)가 암묵적으로 존재한다. 전역 변수name은 창(window)의 이름을 가리키며, 기본값은 빈 문자열이다. 따라서 person.last-nameundefined – ''과 같으므로 NaN이 된다.

프로퍼티 값 갱신

존재하는 프로퍼티에 값을 할당하면 프로퍼티 값이 갱신된다.

1
2
3
4
5
6
7
8
var person = {
name: "Lee",
};

// person 객체에 name 프로퍼티가 존재하므로 name 프로퍼티의 값이 갱신된다.
person.name = "Kim";

console.log(person); // {name: "Kim"}

프로퍼티 동적 생성

존재하지 않는 프로퍼티에 값을 할당하면 프로퍼티가 동적으로 생성되고 값이 할당된다.

1
2
3
4
5
6
7
8
9
var person = {
name: "Lee",
};

// person 객체에는 age 프로퍼티가 존재하지 않는다.
// 따라서 person 객체에 age 프로퍼티가 동적으로 생성되고 값이 할당된다.
person.age = 20;

console.log(person); // {name: "Lee", age: 20}

프로퍼티 삭제

delete 연산자는 객체의 프로퍼티를 삭제한다.
delete 연산자의 피연산자는 프로퍼티 값에 접근할 수 있는 표현식이어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var person = {
name: "Lee",
};

// 프로퍼티 동적 생성
person.age = 20;

// person 객체에 age 프로퍼티가 존재한다.
// 따라서 delete 연산자로 age 프로퍼티를 삭제할 수 있다.
delete person.age;

// person 객체에 address 프로퍼티가 존재하지 않는다.
// 따라서 delete 연산자로 address 프로퍼티를 삭제할 수 없다. 이때 에러가 발생하지 않는다.
delete person.address;

console.log(person); // {name: "Lee"}

ES6에서 추가된 객체 리터럴의 확장 기능

프로퍼티 축약 표현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ES5
var x = 1,
y = 2;

var obj = {
x: x,
y: y,
};

console.log(obj); // {x: 1, y: 2}
/****************************/
//ES6에서는 프로퍼티 값으로 변수를 사용하는 경우 변수 이름과 프로퍼티 키가 동일한 이름일 때, 프로퍼티 키를 생략(property shorthand)할 수 있다. 이때 프로퍼티 키는 변수 이름으로 자동 생성된다.

// ES6
let x = 1,
y = 2;

// 프로퍼티 축약 표현
const obj = { x, y };

console.log(obj); // {x: 1, y: 2}

계산된 프로퍼티 이름

문자열 또는 문자열로 타입 변환할 수 있는 값으로 평가되는 표현식을 사용해 프로퍼티 키를 동적으로 생성할 수도 있다. 단, 프로퍼티 키로 사용할 표현식을 대괄호([…])로 묶어야 한다. 이를 계산된 프로퍼티 이름(computed property name)이라 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ES5
var prefix = "prop";
var i = 0;

var obj = {};

// 계산된 프로퍼티 이름으로 프로퍼티 키 동적 생성
obj[prefix + "-" + ++i] = i;
obj[prefix + "-" + ++i] = i;
obj[prefix + "-" + ++i] = i;

console.log(obj); // {prop-1: 1, prop-2: 2, prop-3: 3}
/*********************************/

// ES6
const prefix = "prop";
let i = 0;

// 객체 리터럴 내부에서 계산된 프로퍼티 이름으로 프로퍼티 키 동적 생성
const obj = {
[`${prefix}-${++i}`]: i,
[`${prefix}-${++i}`]: i,
[`${prefix}-${++i}`]: i,
};

console.log(obj); // {prop-1: 1, prop-2: 2, prop-3: 3}

메서드 축약 표현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ES5
var obj = {
name: "Lee",
sayHi: function () {
console.log("Hi! " + this.name);
},
};

obj.sayHi(); // Hi! Lee
/**************************/

// ES6에서는 function 키워드를 생략한 축약 표현을 사용할 수 있다.
const obj = {
name: "Lee",
// 메서드 축약 표현
sayHi() {
console.log("Hi! " + this.name);
},
};

obj.sayHi(); // Hi! Lee

참고 도서: 모던 자바스크립트 Deep Dive

Nyong’s GitHub