- Published on
모던 자바스크립트 Deep Dive 복습 4
- Authors

- Name
- Byeong Jun An
원시 값과 객체의 비교
둘의 차이점은 원시 타입의 값은 변경 불가능한 값이고 객체(참조) 타입의 값은 변경 가능한 값이다. 이에 대해 자세히 알아보려고 한다.
원시 값
값을 변경할 수 없다는 것?
원시 값은 불변성의 특성을 가지고 있어 값을 변경할 수 없다고 한다. 값을 변경할 수 없다는 건 변수가 아니라 변수 안에 있는 값 자체를 변경할 수 없다는 뜻이다. 변수는 하나의 값을 저장하기 위해 확보한 메모리 공간 또는 그 공간을 식별 하기 위해 붙인 이름이다. 그 메모리 공간안에는 표현식이 평가되어 생성된 어떤 결과가 있는데 그걸 값이라 부른다. 만약 그 값을 가리키고 있던 변수의 참조를 다른 메모리 공간에 저장된 값을 가리키면 변수의 값이 변한것 처럼 보이는데 그건 값이 변한게 아니라 변수를 재할당 했다고 보는것이 맞다.
변수는 불변성이라는 특성을 갖는다. 이 불변성을 갖는 원시 값을 할당한 변수는 재할당 이외에 변수 값을 변경 할 수있는 방법이 없다.
문자열과 불변성
문자열은 유사 배열 객체로 배열은 아니지만 배열처럼 인덱스를 통해 각 문자에 접근할 수 있고 length 프로퍼티를 갖고 for문을 통해 순회할 수도 있다. 하지만 아래 예시를 보면 알 수 있듯이 문자열 또한 값을 변경 할 수 없다.
var str = 'string'
str[0] = 'S' // 0번째 인덱스에 접근해서 값을 변경하려고 시도 하고 있다.
console.log(str) // string 하지만 문자열은 원시 값이므로 변경 할 수 없다. 이때, 에러가 발생하지 않는다.
하지만 str에 다른 값을 할당하는 재할당은 물론 가능하다. 기존 문자열을 변경하는것이 아니라 새로운 문자열을 새롭게 할당하는 것이기 때문이다.
값에 의한 전달
어떤 변수에 다른 변수를 할당할 때 발생하는 것으로 기존의 변수가 원시 값을 갖고 있었다면 새로운 변수에 원시값을 복사해서 할당한다. 이때 값을 복사해서 전달하는 과정을 값에 의한 전달이라 부른다. 사실 정확히 말해서 값을 전달하는 게 아니라 메모리 주소를 전달한다. 왜냐하면 변수 이름인 식별자는 메모리 주소를 기억하고 있지 값을 기억하고 있는게 아니기 때문이다. 식별자는 메모리 주소에 붙인 이름일 뿐이다.
객체
객체는 원시 값과는 다르게 변경 가능한 값이다. 객체를 할당한 변수가 기억하는 메모리 주소를 통해 메모리 공간에 접근하면 참조 값에 접근할 수 있다. 참조 값은 생성된 객체가 저장된 메모리 공간의 주소, 그 자체다. 이 참조 값을 통해 실제 객체에 접근한다. 객체를 할당한 변수는 재할당 없이 객체를 직접 변경할 수 있다. 즉, 재할당 없이 프로퍼티를 동적으로 추가할 수도 있고 프로퍼티 값을 갱신할 수도 있으며 프로퍼티 자체를 삭제할 수도 있다.
참조에 의한 전달
값에 의한 전달에서 설명한것 것처럼 참조에 의한 전달도 어떤 변수에 다른 변수를 할당할 때 발생하는 것으로 보면 되는데 기존의 변수가 객체를 갖고 있었을때, 변수에 참조값을 복사해서 할당하는데 이때 참조값을 복사해서 전달하는 과정을 참조에 의한 전달이라고 부른다. 이러한 참조값을 복사해서 전달하게 되면 기존 변수와 카피한 변수가 같은 객체를 가리키게 되는데 이때, 어느 하나의 변수의 값을 변경하려고 시도하면 다른 변수의 값도 같은 객체를 가리키고 있기 때문에 두 변수의 값이 동시에 변경되는 부작용이 발생하게 되는데 이를 예상하고 있어야 부작용을 막을 수 있다.
var person = {
name: 'Kim',
}
var copy = person // 참조 값을 복사, copy와 person은 동일한 참조 값을 갖는다.
console.log(copy === person) // true;
person.age = 13 // person을 통해 객체를 변경한다.
copy.name = 'Lee' // copy를 통해 객체를 변경한다.
//copy와 person은 동일한 객체를 가리킨다. 따라서 어느 한쪽에서 객체를 변경하면 서로 영향을 주고 받는다.
console.log(person) //{name: 'Lee', age: 13}
console.log(copy) //{name: 'Lee', age: 13}
얕은 복사와 깊은 복사
객체를 프로퍼티 값으로 갖는 객체의 경우 얕은 복사는 한 단계까지만 복사하는 것을 말하고 깊은 복사는 객체에 중첩되어 있는 객체까지 모두 복사하는 것을 말한다.
Q&A
Q. 원시 값이 변경 불가능한 이유와 객체 값이 변경 가능한 이유는?
A: 원시 값이 변경 가능하게 되면 값의 변경(상태 변경)을 추적하기 어려워 신뢰성을 떨어뜨릴 수 있기 때문에 값 변경을 불가능하게 만들었고 객체의 경우에는 객체를 생성하는 비용이 많이 들기 때문에 일부만 변경하도록 설계되어 있다. 이게 무슨 말이냐면 만약 객체가 값을 변경하지 못한다면 변수의 변경은 원시값 처럼 재할당 해야 하는데 재할당 할때는 새로운 메모리 공간에 객체를 새로 만들어야 한다. 이 새로 객체를 만드는 비용이 객체의 크기 원시값보다 훨씬 커서 비효율적이기 때문에 원시값은 변경 불가능하고 객체 값은 변경 가능하게 설계했다고 보면 된다.
Q. 얕은 복사와 깊은 복사의 차이는?
A: 얕은 복사는 객체안에 객체가 중첩되어 있을경우 복사할 때 객체의 최상위 레벨(스코프)의 속성만 복사한다. 즉, 참조값만 복사하므로 원본과 복사본이 내부 데이터를 공유하여 어느 하나의 변수의 객체 값을 바꾸면 다른 변수의 객체 값도 바뀌게 된다. 깊은 복사는 객체에 포함된 모든 레벨의 값을 완전히 다 복사하는 걸 말하는데 이는 얕은 복사와는 달리 복사한 객체의 값과 원본 객체의 값이 공유하지 않기 때문에 서로 값을 영향에 주고 받지 않는다.
함수
함수란 수학에서의 함수는 입력을 받아 어떠한 과정을 거친 후 출력을 내보내는 과정을 정의한 것을 말하는데 프로그래밍에서의 함수는 그러한 일련의 과정을 문(statement)으로 구현하고 코드 블록으로 감싸서 하나의 실행 단위로 정의한 것이다.
함수를 사용하는 이유
- 재사용성: 미리 정의한 코드를 여러 번 사용할 수 있다.
- 유지보수 용이성: 함수 정의 부분만 수정하면 모든 호출 부분이 자동으로 변경된다.
- 오류 방지: 반복되는 코드를 직접 타이핑하는 대신 함수를 사용하면 실수를 줄일 수 있다.
- 코드 신뢰성: 유지보수와 코드의 신뢰성을 높이는 효과가 있다.
- 가독성 향상: 함수에 이름(식별자)을 붙여 함수의 역할을 쉽게 파악할 수 있다.
함수 리터럴
자바스크립트에서 함수는 객체 타입의 값이다. 이는 함수 리터럴로 값을 생성할 수 있다는 걸 의미한다. 함수 리터럴은 function 키워드, 함수 이름, 매개변수 목록, 함수 몸체로 구성된다.
- 함수이름:
- 함수 이름은 식별자이므로 식별자 네이밍 규칙을 준수해야 한다.
- 함수 이름은 함수 몸체 내(중괄호 안)에서만 참조할 수 있는 식별자다.
- 함수 이름은 생략할 수 있다. 이름이 있는 함수를 기명함수, 없는 함수를 익명함수라 부른다.
- 매개변수 목록:
- 0개 이상의 매개변수를 소괄호로 감싸서 쉼표로 구분한다.
- 각 매개변수에는 함수를 호출할 때 지정한 인수가 순서대로 할당된다.
- 매개변수는 함수 몸체 내에서 변수와 동일하게 취급된다.
- 함수 몸체:
- 함수가 호출 되었을 때 일괄적으로 실행될 문들을 하나의 실행 단위로 정의한 코드 블록이다.
- 함수 몸체는 함수 호출에 의해 실행된다.
함수는 객체다. 그렇지만 일반 객체와는 다르게 호출할 수 있다는 특징이 있다. 그리고 일반 객체에는 없는 프로토타입 등의 고유한 프로퍼티를 갖는다.
함수 정의
함수 정의란 함수를 호출하기 이전에 인수를 전달받을 매개변수와 실행할 문들, 반환할 값을 지정하는 것을 말하는데 크게 4가지의 정의 방법이 있다.
함수 선언문
- 함수 이름을 생략할 수 없다.
- 표현식이 아닌 문이다. (값으로 평가될 수 없다)
- 함수 선언문과 함수 리터럴은 함수 이름을 제외할 수 있냐 없냐의 차이만 있고 형태는 동일하기 때문에 어떤 변수에 함수 선언문을 넣을 경우 자바스크립트 엔진은 함수 선언문이 아닌 함수 리터럴로 해석할 수 있다.
//함수 선언문은 표현식이 아닌 문으로 변수에 할당할 수 없어야한다.
//하지만 아래는 함수 선언문이 할당되는 것처럼 보인다.
var add = function add(x, y) {
return x + y
}
console.log(add(2, 5)) // 7
위와 같이 해석되는건 자바스크립트 엔진이 코드의 문맥을 파악하여 함수 선언문이 아닌 함수 리터럴로 해석했기 때문이다. 함수 선언문은 이름이 있는 함수 리터럴이 어떤 변수에 할당되지 않고 단독으로 존재하면 그걸 함수 선언문으로 해석하고 어떤 변수에 할당하거나 표현식으로 평가되어야 하는 문맥상에선 함수 이름이 있든 없든 함수 리터럴로 해석한다.
//기명 함수 리터럴을 단독으로 사용하면 함수 선언문으로 해석된다.
//함수 선언문에서는 함수 이름을 생략할 수 없다.
function foo() {
console.log('foo')
}
foo() //foo
//아래는 그룹(())연산자를 사용했는데 그룹 연산자 안에는 표현식이 와야하는 특징이 있다.
//따라서 자바스크립트는 함수 리터럴을 피연산자로 사용해 값이 될 수 있는 함수 리터럴 표현식으로 해석한 것이다.
//함수 리터럴은 함수 이름을 생략할 수 있다.
;(function bar() {
console.log('bar')
})
bar() // ReferenceError : bar is not defined
위의 예시에서 foo라는 함수 선언문은 호출했을 때 정상적으로 실행되지만, bar라는 함수 리터럴은 ReferenceError가 발생한다. 그 이유는 foo(), bar()같은 함수 이름은 해당 함수를 가리키는 식별자가 아니기 때문이다.
위에서 함수 이름에 대해 설명할 때, 함수 이름은 함수 몸체 내에서만 참조할 수 있는 식별자라고 했다. 이는 함수를 가리키는 식별자가 없다는 뜻이고 함수 바깥에서 호출하는 foo(), bar()를 사용해서 해당 함수를 호출 할 수 없다는 뜻이다.
그렇다면 foo()라는 이름을 가진 함수 선언문은 어떻게 오류 없이 호출할 수 있었을까?
그건 함수 선언문의 경우 자바스크립트 엔진이 함수 이름과 같은 이름의 식별자를 암묵적으로 붙여주기 때문이다. 즉, 자바스크립트 엔진은 생성된 함수를 호출하기 위해 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고, 거기에 함수 객체를 할당한다.
지금까지 살펴본 함수 선언문을 의사 코드(코드가 어떤 모양일지 가상으로 코드화 시켜보는 것)로 표현하면 다음과 같다.
// "var add =" 라는 코드를 자바스크립트 엔진이 붙여준다고 생각하면 된다.
var add = function add(x, y) {
return x + y
}
console.log(add(2, 5)) // 7
위의 의사 코드는 사실 함수 표현식과 같다. 하지만 함수 선언문과 함수 표현식은 정확히 동일하게 동작하지는 않는다.
함수 표현식
자바스크립트의 함수는 객체 타입의 값이다. 값처럼 변수에 할당할 수도 있고 프로퍼티의 값이 될 수도 있으며 배열의 요소가 될 수도 있다. 이처럼 값의 성질을 갖는 객체를 일급객체라 부른다. 함수가 일급객체라는 말은 함수를 값처럼 자유롭게 쓸 수 있다는 뜻이다. 함수는 일급 객체이므로 함수 리터럴로 생성한 함수 객체를 변수에 할당할 수 있다. 이러한 함수 정의 방식을 함수 표현식이라고 부른다.
//함수 표현식은 함수 이름을 생략할 수 있다. (변수 이름은 생략 불가능)
var add = function (x, y) {
return x + y
}
console.log(add(2, 5)) // 7
함수 표현식에서 함수 이름은 재귀함수 처럼 함수 내부에서 쓰는 경우가 아니라면 일반적으로 굳이 쓰지 않는데 이처럼 함수 이름을 쓰지 않은 함수 리터럴을 익명함수라 부른다.
Function 생성자 함수
함수도 객체이기 때문에 생성자 함수를 통해 만들어 낼 수 있는데 생성자 함수이기 때문에 new 라는 연산자와 함께 호출해야 한다. (사실 new가 없어도 결과는 동일하다.)
var add = new Function('x', 'y', 'return x + y')
console.log(add(2, 5)) // 7
이러한 Function 생성자 함수를 사용하는 방법은 일반적이지 않고 바람직 하지 않다. 왜냐하면 클로저를 생성하지 않는데 이는 함수 선언문이나 함수 표현식과 동일하게 동작하지 않기 때문이다.
화살표 함수
function 키워드 대신 화살표 "=>" 를 사용해 좀 더 간략한 방법으로 기존 함수와 this 바인딩 방식이 다르고, prototype 프로퍼티가 없으며 arguments 객체를 생성하지 않는다.
const add = (x, y) => x + y
console.log(add(2, 5)) // 7
함수 생성 시점과 함수 호이스팅
함수 선언문과 함수 표현식의 각기 다른 특징이 있는데 그것은 함수 선언문은 함수 선언문 이전에 호출할 수 있으나 함수 표현식은 불가능 하다는 것이다. 이러한 현상은 함수의 생성 시점이 다르기 때문인데 함수 선언문은 런타임 이전 평가단계에서 자바스크립트 엔진에 의해 먼저 선언문을 실행한다. 따라서 런타임에 선언되기 이전에 호출해도 정상적으로 실행할 수 있다.
하지만 함수 표현식은 불가능 하다. 왜냐하면 함수 표현식을 자세히 보면 함수 리터럴을 변수에 할당하는 것으로 볼 수 있는데 이는 함수 호이스팅으로 볼게 아니라 변수 호이스팅으로 봐야하기 때문이다. 변수 호이스팅이 발생하면 변수 선언문이 먼저 실행되는건 런타임 이전에 발생하고 할당되는 함수 리터럴은 런타임 이후에 실행된다. 이처럼 함수 선언문과 함수 표현식은 함수 객체를 생성한다라는 측면에서는 동일한 역할을 하지만 내부 동작에서는 미묘한 차이가 있다.
함수 호출
함수 호출은 함수를 가리키는 식별자와 한 쌍의 소괄호(())인 함수 호출 연산자로 호출한다. 호출 연산자 내에는 0개 이상의 인수(argument)를 쉼표로 구분하여 나열한다. 나열한 인수는 매개변수에 순서대로 할당된다.
매개변수와 인수
함수의 실행을 위해 필요한 값을 외부에서 내부로 전달할 때 매개변수(parameter, 인자)를 통해 인수(argument)를 전달한다. 인수는 값으로 평가될 수 있는 표현식이어야 하며 인수는 함수를 호출할 때 지정하며 개수와 타입에 제한이 없다.
매개변수는 함수를 정의할 때 선언하며 함수 몸체 내부에서 변수와 동일하게 취급된다.
// 함수 선언문
function add(x, y) {
return x + y
}
//함수 호출
//인수 1과 2는 매개변수 x와 y에 순서대로 할당되고 함수 몸체의 문들이 실행된다.
var result = add(1, 2)
반환문
함수는 return 키워드와 반환값으로 이루어진 반환문을 사용하여 실행 결과를 함수 외부로 반환(return)할 수 있다.
function multiply(a, b) {
return a + b //값의 반환
}
// 함수는 반환값으로 평가된다.
var result = multiply(1, 2)
console.log(result) // 2;
반환문은 크게 두가지의 역할을 하는데 -반환문은 함수의 실행을 중단하고 함수 몸체를 빠져나간다. 따라서 반환문 이후에 다른 문이 존재하면 그 문은 실행되지 않고 무시된다. -반환문은 return 키워드 뒤에 지정한 값을 반환한다. 만약 명시적으로 지정하지 않으면 암묵적으로 undefined가 반환된다.
참조에 의한 전달과 외부 상태의 변경
매개 변수도 "원시 값과 객체의 비교"에서 살펴보았듯이 원시값은 값에 의한 전달(pass by value), 객체는 참조에 의한 전달(pass by reference) 방식으로 동작한다.
// 매개 변수 primitive는 원시값을 전달받고, 매개변수 obj는 객체를 전달받는다.
function changeVal(primitive, obj) {
primitive += 100
obj.name = 'kim'
}
//외부 상태
var num = 100
var person = { name: 'John' }
console.log(num) // 100
console.log(person) // {name: "John"}
//원시 값은 값 자체가 복사되어 전달되고 객체는 참조 값이 복사되어 전달된다.
changeVal(num, person)
//원시값은 원본이 훼손되지 않는다.
console.log(num) // 100
//객체는 원본이 훼손됐다.
console.log(person) // {name: "kim"}
위 예시를 보면 알 수 있지만 함수 외부에서 함수 몸체 내부로 전달한 참조 값에 의해 원본 객체가 변경되는 경우가 생기는데 이러한 문제는 나중에 객체를 불변 객체로 만들어 사용하는 방법을 배우면서 알아보자
다양한 함수의 형태
즉시 실행 함수
함수 정의와 동시에 즉시 호출되는 함수(IIFE Immediately invoked Function Expression)를 말한다. 즉시 실행 함수는 단 한번만 호출 되며 다시 호출할 수 없다 .
//익명 즉시 실행 함수
;(function () {
var a = 3
var b = 5
return a + b
})()
// 즉시 실행 함수도 일반 함수처럼 인수를 전달할 수 있다.
var res = (function (a, b) {
return a + b
})(3, 5)
console.log(res) // 15;
재귀 함수
함수가 자기 자신을 호출하는 것을 재귀 호출이라고 한다. 재귀 호출을 하는 함수를 재귀함수라 한다. 재귀함수는 자신을 무한히 호출하기 때문에 탈출 조건을 반드시 만들어야 한다.
var factorial = function foo(n){
//탈출 조건: n이 1 이하일 때 재귀 호출을 멈춘다.
if(n<= 1) return 1;
// 함수를 가리키는 식별자로 자기 자신을 재귀 호출
return n * factorial(n - 1);
}
console.log(factorial(4)); 4! = 4* 3* 2* 1 = 24
중첩 함수
함수 내부에 정의된 함수를 중첩 함수(nested function) 또는 내부 함수 (inner function)라 한다. 일반적으로 중첩 함수는 자신을 포함하는 외부 함수를 돕는 헬퍼 함수의 역할을 한다.
콜백 함수
함수를 정의할때 공통된 부분은 미리 정의해두고 바뀌는 부분을 매개변수로 받아 함수 외부에서 매개변수로 함수를 전달받는 함수 또는 함수내부에서 함수를 반환하는 함수를 고차함수라 부르고 이 고차함수의 매개변수로 전달되는 함수를 콜백함수라 부른다.
순수 함수와 비순수 함수
어떠한 외부 상태에 의존하지도 않고 변경시키지도 않는 즉, 부수효과가 없는 순수함수이다. 비순수 함수는 그와는 반대의 개념이다.
- 비순수 함수
var count = 0 //현재 카운트를 나타내는 상태: increase 함수의 외부에 있으며 increase에 의해 변화된다.
// 함수의 외부 상태에 의존하여 외부 상태에 따라 반환값이 달라짐
// 비순수 함수는 외부 상태를 변경하는 부수 효과(side effect)가 있다.
function increase() {
return ++count
}
// 비순수 함수는 외부 상태(count)를 변경하므로 상태 변화를 추적하기 어려워진다.
increase()
console.log(count) // 1
increase()
console.log(count) // 2
- 순수 함수
var count = 0 //현재 카운트를 나타내는 상태
// 순수 함수 increase는 동일한 인수가 전달되면 언제나 동일한 값을 반환한다.
function increase(n) {
return ++n
}
// 순수 함수가 반환한 결과값을 변수에 재할당해서 상태를 변경
count = increase(count)
console.log(count) // 1
count = increase(count)
console.log(count) // 2
Q&A
Q. 함수 리터럴이란?
A: 리터럴은 값을 생성하기 위한 표기법 인데 함수 리터럴은 함수라는 값을 생성하기 위한 표기법 입니다.
Q. 함수 선언문과 함수 표현식의 차이점은?
A: 함수 선언문은 표현식이 아닌 문이고 함수 표현식은 표현식인 문이다. 함수 선언문은 함수 호이스팅이 발생하고 함수 표현식은 변수 호이스팅이 발생한다.
Q. 매개변수와 인수의 차이는?
A: 매개변수는 함수 내부에서 사용하기 위해 인수를 통해 받아온 변수 이고 인수는 함수 호출할때 함수에 전달하는 실제 값입니다.
Q. 반환문의 기능?
A: 코드의 실행을 중단 하고 함수를 빠져나가거나 실행 결과를 함수 외부로 반환한다.
Q. 함수의 매개변수에 객체 참조를 전달하는 경우 발생할 수 있는 문제와 해결방법은?
A: 함수 내부에서 원본 함수의 데이터를 변경할 수 있게 되는 문제가 발생하는데 이는 불변객체(object.freeze 같은)를 사용하거나 얕은 복사나 깊은복사를 통해 원본 객체를 직접 사용하지 않고 복사해서 사용하면 됩니다.
Q. 콜백함수란?
A: 고차 함수에 매개변수로 들어오는 함수를 말합니다.
Q. 순수 함수와 비순수 함수란?
A: 순수 함수는 외부 상태에 의존하거나 변경하지 않는 함수를 말하고 비순수 함수는 외부 상태에 따라 상태가 변하거나 의존하는 함수를 말합니다.