*** 아래의 글은 <모던 자바스크립트 deep dive>(이웅모 저) 책을 대부분 참고하였습니다.***
변수 데이터 불변성
원시 타입과 불변성
원시 타입 은 정수, 실수, 문자, 논리 리터럴등의 실제 데이터 값을 저장하는 타입이다.
자바스크립트가 제공하는 7가지 데이터 타입(숫자, 문자열, 불리언, null, undefined, 심벌, 객체 타입)은 크게 원시 타입(primitive type)과 객체타입(object/reference type)으로 구분할 수 있다.
원시타입과 객체타입은 크게 세 가지 측면에서 다르다.
첫째, 원시 타입의 값, 즉 원시 값는 변경 불가능한 값(immutable value)이다. 이에 비해 객체(참조)타입의 값, 즉 객체는 변경 가능한 값(mutable value)이다.
둘째, 원시 값은 변수에 할당하면 변수(확보된 메모리 공간)에는 실제 값이 저장된다. 이에 비해 객체를 변수에 할당하면 변수(확보된 메모리 공간)에는 참조 값이 저장된다.
셋째, 원시 값을 갖는 변수는 다른 변수에 할당되면 원본의 원시 값이 복사되어 전달된다. 이를 값에 의한 전달(pass by value)라고 한다. 이에 비해 객체를 가리키는 변수를 다른 변수에 할당하면 원본의 참조 값이 복사되어 전달된다. 이를 참조에 의한 전달(pass by reference)라고 한다.

출처: Accelerated JavaScript Training Course (Udemy, Maximilian Schwarzmüller)
var aNumber = 5;
console.log(aNumber);
var anotherNumber = aNumber;
console.log(anotherNumber);
aNumber = 12;
console.log(aNumber); //12 출력됨
console.log(anotherNumber); //5 출력됨
원시 타입의 값, 즉 원시 값은 변경 불가능한 값이다. 다시 말해 한 번 생성된 원시값은 읽기 전용 값으로서 변경할 수 없다. 먼저 변수와 값은 구분해서 생각해야 한다. 변수는 하나의 값을 저장하기 위해 확보된 메모리 공간 자체 또는 메모리 공간을 식별하기 위해 붙인 이름이고 값은 변수에 저장된 데이터로서 표현식이 평가되어 생성된 결과를 말한다. 변경 불가능하다는 것은 변수가 아니라 값에 대한 진술이다.
원시값은 변경 불가능하다라는 말은 원시 값 자체를 변경할 수 없다는 것이지 변수 값을 변경할 수 없다는 것이 아니다. 변수는 언제든지 재할당을 통해 변수 값을 변경(엄밀하게 말하면 교체)할 수 있다. 그렇기 때문에 변수라고 부른다.
변수의 상대개념인 상수는 재할당이 금지된 변수를 말한다. 상수도 값을 저장하기 위한 메모리 공간이 있어야 하므로 변수라고 할 수 있다. 단, 변수는 언제든지 재할당을 통해 변수 값을 변경할 수 있지만 상수는 단 한번만 할당을 허용하므로 변수 값을 변경할 수 었다. 따라서 상수와 변경 불가능한 값을 동일시하는 것은 곤란하다. 상수는 재할당이 금지된 변수이다.
// const 키워드를 사용해 선언한 변수는 재할당이 금지된다. 상수는 재할당이 금지된 변수일 뿐이다.
const o = {};
// const 키워드를 사용해 선언한 변수에 할당한 원시값(상수)은 변경할 수 없다.
// 하지만 const 키워드를 사용해 선언한 변수에 할당한 객체는 변경할 수 있다.
o.a = 1;
console.log(o); // {a: 1}
원시 값은 변경 불가능한 값, 즉 읽기 전용 값이다. 원시 값은 어떤 일이 있어도 불변한다. 이러한 원시값의 특성은 데이터의 신뢰성을 보장한다.
원시값을 할당한 변수에 새로운 원시값을 재할당하면 메모리 공간에 저장되어 있는 재할당 이전의 원시값을 변경하는 것이 아니라 새로운 메모리 공간을 확보하고 재할당한 원시 값을 저장한 후 변수는 새롭게 재할당한 원시 값을 가리킨다. 이때 변수가 참조하던 메모리 공간의 주소가 바뀐다.
변수가 참조하던 메모리 공간의 주소가 변경된 이유는 변수에 할당된 원시 값이 변경 불가능한 값이기 때문이다. 만약 원시 값이 변경 가능한 값이라면 변수에 새로운 원시 값을 재할당했을 때 변수가 가리키던 메모리 공간의 주소를 바꾸지 않고 원시값 자체를 변경하면 그만이다. 그러면 변수가 참조하던 메모리 공간의 주소는 바뀌지 않는다.
하지만 원시값은 변경 불가능한 값이기 때문에 값을 직접 변경할 수 없다. 따라서 변수 값을 변경하기 위해 원시값을 재할당하면 새로운 메모리 공간을 확보하고 재할당한 값을 저장한 후 변수가 참조하던 메모리 공간의 주소를 변경한다. 값의 이러한 특성을 불변성이라고 한다.
불변성을 갖는 원시값을 할당한 변수는 재할당 이외에 변수 값을 변경할 수 있는 방법이 없다. 만약 재할당 이외에 원시 값인 변수 값을 변경할 수 있다면 예기치 않게 변수 값이 변경될 수 있다는 것을 의미한다. 이는 값의 변경, 상태 변경을 추적하기 어렵게 만든다.
원시값인 문자열은 다른 원시값과 비교할 때 독특한 특징이 있다. 문자열은 0개이상의 문자로 이루어진 집합을 말하면 1개의 문자는 2바이트의 메모리 공간에 저장된다. 따라서 문자열은 몇 개의 문자로 이뤄졌느냐에 따라 메모리 공간의 크기가 결정된다. 숫자 값은 1도, 10000000도 동일한 8비트가 사용되지만 문자열의 경우 1개의 문자로 이루어진 문자열은 2바이트, 10개의 문자로 이루어진 문자열은 20바이트가 사용된다.
// 문자열은 0개 이상의 문자들로 이뤄진 집합이다.
var str1 = ''; // 0개의 문자로 이뤄진 문자열(빈 문자열)
var str2 = 'Hello'; // 5개의 문자로 이뤄진 문자열
이 같은 이유로 C에는 하나의 문자를 위한 데이터 타입만 있을 뿐 문자열 타입은 존재하지 않는다. 문자열을 문자의 배열로 처리한다. 하지만 자바스크립트에서는 개발자의 편의를 위해 원시 타입인 문자열 타입을 제공한다. 자바스크립트의 문자열은 원시 타입이면 변경 불가능하다.
아래의 예제에서 첫째문이 실행되면 문자열 'Hello'가 실행되고 식별자 'str'은 문자열 'Hello'가 저장된 메모리 공간의 첫번째 메모리 셀주소를 가리킨다. 그리고 두번째 문이 실행되면 이전에 생성된 문자열 'Hello'를 수정하는 것이 아니라 새로운 문자열 'world'를 메모리에 생성하고 식별자 'str'은 이것을 가리킨다. 이때 문자열 'Hello'와 'world'는 모두 메모리에 존재한다. 식별자 str은 문자열 'Hello'를 가리키고 있다가 'world'를 가리키도록 변경되었을 뿐이다.
var str = 'Hello';
str = 'world';
두 변수의 원시값은 서로 다른 메모리 공간에 저장된 별개의 값이 되어 어느 한쪽에서 재할당을 통해 값을 변경하더라도 서로 간섭할 수 없다.
var score = 80;
// copy 변수에는 score 변수의 값 80이 복사되어 할당된다.
var copy = score;
console.log(score, copy); // 80 80
console.log(score === copy); // true
// score 변수와 copy 변수의 값은 다른 메모리 공간에 저장된 별개의 값이다.
// 따라서 score 변수의 값을 변경해도 copy 변수의 값에는 어떠한 영향도 주지 않는다.
score = 100;
console.log(score, copy); // 100 80
console.log(score === copy); // false
객체 타입과 변경가능한 값
참조 타입 은 객체(Object)의 번지를 참조(주소를 저장)하는 타입으로 메모리 번지 값을 통해 객체를 참조하는 타입이다. 객체(참조) 타입의 값, 즉 객체는 변경 가능한 값(mutable value)이다.
원시값을 할당한 변수가 기억하는 메모리 주소를 통해 메모리 공간에 접근하면 원시값에 접근할 수 있다. 즉, 원시값을 할당한 변수는 원시값 자체를 값으로 갖는다. 하지만 객체를 할당한 변수가 기억하는 메모리 주소를 통해 메모리 공간에 접근하면 참조값(reference value)에 접근할 수 있다. 참조 값은 생성된 객체가 저장된 메모리 공간의 주소 자체이다.
var person = {
name: 'Lee'
};
일반적으로 원시값을 할당한 변수의 경우 '변수는 ~의 값을 갖는다.'라고 표현하지만 객체를 할당한 변수의 경우 '변수는 객체를 참조하고 있다.'라고 표현한다. 아래 예제에서 person 변수는 객체 { name : 'Lee' } 를 참조하고 있다.
// 할당이 이뤄지는 시점에 객체 리터럴이 해석되고, 그 결과 객체가 생성된다.
var person = {
name: 'Lee'
};
// person 변수에 저장되어 있는 참조값으로 실제 객체에 접근해서 그 객체를 반환한다.
console.log(person); // {name: "Lee"}
원시값은 변경 불가능한 값이므로 원시 값을 갖는 변수의 값을 변경하려면 재할당 외에는 방법이 없다. 하지만 객체는 변경 가능한 값이다. 따라서 객체를 할당한 변수는 재할당 없이 객체를 직접 변경할 수 있다. 즉 재할당 없이 프로퍼티를 동적으로 추가할 수도 있고 프로퍼티 값을 갱신할 수도 있으며 프로퍼티 자체를 삭제할 수도 있다.
var person = {
name: 'Lee'
};
// 프로퍼티 값 갱신
person.name = 'Kim';
// 프로퍼티 동적 생성
person.address = 'Seoul';
console.log(person); // {name: "Kim", address: "Seoul"}
원시값은 변경 불가능한 값이므로 원시 값을 갖는 변수의 값을 변경하려면 재할당을 통해 메모리에 원시 값을 새롭게 생성해야 한다. 하지만 객체는 변경 가능한 값이므로 메모리에 저장된 객체를 직접 수정할 수 있다. 이 때 객체를 할당한 변수에 재할당을 하지 않으므로 객체를 할당한 변수의 참조값은 변경되지 않는다.

출처: Accelerated JavaScript Training Course (Udemy, Maximilian Schwarzmüller)
var array = [1,2,3];
var anotherArray = array;
console.log(array);
console.log(anotherArray);
array.push(4);
console.log(array); //[1,2,3,4] 출력됨.
console.log(anotherArray); //[1,2,3,4] 출력됨.
객체는 원시값과 다르게 여러 개의 식별자가 하나의 객체를 공유할 수 있다.
var person = {
name: 'Lee'
};
// 참조값을 복사(얕은 복사)
var copy = person;
객체를 가리키는 변수(원본, person)를 다른 변수(사본, copy)에 할당하면 원본의 참조값이 복사되어 전달된다.
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"}
아래 예제에서 == 일치 비교 연산자는 변수에 저장되어 있는 값을 타입 반환하지 않고 비교한다.
객체를 할당한 변수는 참조값을 가지고 있고, 원시 값을 할당한 변수는 원시값 자체를 가지고 있다. 따라서 == 일치 비교 연산자를 통해 객체를 할당한 변수를 비교하면 참조 값을 비교하고, 원시 값을 할당한 변수를 비교하면 원시 값을 비교한다.
객체 리터럴은 평가될 때마다 객체를 생성한다.
var person1 = {
name: 'Lee'
};
var person2 = {
name: 'Lee'
};
console.log(person1 === person2); // ①
console.log(person1.name === person2.name); // ②
person1 변수와 person2변수가 가리키는 객체는 비록 내용은 같지만 다른 메모리에 저장되는 별개의 객체이다. person1 변수와 person2 변수의 참조 값은 전혀 다른 값이다. 따라서 ①의 값은 false이다.
하지만 프로퍼티의 값을 참조하는 person1.name과 person2.name은 값으로 평가될 수 있는 표현식이다. 두 표현식 모두 원시값 'Lee'로 평가된다. 따라서 ②는 true이다.
함수의 매개 변수
call by value vs call by reference
함수의 매개변수를 값으로 전달하는 방식이 Call by Value이고 함수의 매개변수를 참조로 전달하는 방식이 Call by Reference 이다.
const flight = 'LH234';
const jane = {
name: 'Jane Austin',
passport: 2933621416
};
const checkIn = function (flightNum, passenger) {
flightNum = 'LH999';
passenger.name = 'Ms. ' + passenger.name;
if (passenger.passport === 2933621416) {
alert('Checked in');
} else {
alert('Wrong passport!');
}
};
checkIn(flight, jane);
console.log(flight); //LH234 출력됨(값이 변경되지 않음) --> call by value
console.log(jane); //{ name: 'Ms.Jane Austin', passport: 2933621416} 출력됨(값이 변경됨) --> call by reference
// 아래와 동일한 방식으로 볼 수 있다.
// const flightNum = flight;
// const passenger = jane;
call by sharing
함수의 매개변수를 속성 공유로 전달하는 방식이 Call by Sharing이다. (정확하지 않을 수 있음) 참조로 전달하는 방식(Call by Reference)과 차이점은 함수 안에서 인자를 새로 할당했을 때 호출한 곳에서 접근할 수 없다는 점이다. 함수에 인자를 넘기면 값을 복사한 지역 변수로 사용하는데 객체 형태의 인자를 넘기면 속성은 공유하지만 속성은 새로운 메모리 주소를 사용하고 지역변수에는 변동이 없게 된다. 원본객체는 재할당되지 않게 된다. (정확하지 않을 수 있음)
const flight = 'LH234';
const jane = {
name: 'Jane Austin',
passport: 2933621416,
ticket : 00000563
};
const checkIn = function (flightNum, passenger) {
flightNum = 'LH999';
//( passenger.name = 'Ms. ' + passenger.name;) //과 비교해보면 값이 다름
passenger = {
name: 'James Austin',
passport: 2933621416,
ticket : 00000003
}
if (passenger.passport === 2933621416) {
alert('Checked in');
} else {
alert('Wrong passport!');
}
};
checkIn(flight, jane);
console.log(flight); //LH234 출력됨(값이 변경되지 않음) ---> call by value
console.log(jane); //{ name: 'Jane Austin', passport: 2933621416, ticket : 00000563
} 출력됨(값이 변경되지 않음.) ---> call by sharing

출처: Tech Talk: Pass By Sharing with Javascript (Fullstack AcademyFullstack Academy) https://youtu.be/1YFss_4B_o4
함수 호출과 매개변수
함수는 함수를 가리키는 식별자와 한 쌍의 소괄호인 함수 호출 연산자로 호출한다. 함수 호출 연산자 내에는 0개 이상의 인수를 쉼표로 구분하여 나열한다. 함수를 호출하면 현재의 실행 흐름을 중단하고 호출된 함수로 실행 흐름을 옮긴다. 이 때 매개 변수에 인수가 순서대로 할당되고 함수 몸체의 문들이 실행되기 시작한다.
함수를 실행하기 위해 값을 함수 외부에서 함수 내부로 전달해야 하는 경우 매개변수를 통해 인수를 전달한다. 인수는 값으로 평가될 수 있는 표현식이어야 한다. 인수는 함수를 호출할 때 지정하며 개수와 타입에 제한이 없다.
// 함수 선언문
function add(x, y) {
return x + y;
}
// 함수 호출
// 인수 1과 2는 매개변수 x와 y에 순서대로 할당되고 함수 몸체의 문들이 실행된다.
var result = add(1, 2);
매개변수는 함수 몸체 내부에서만 참조할 수 있고 함수 몸체 외부에서는 참조할 수 없다. 매개변수의 스코프는 함수 내부이다.
function add(x, y) {
console.log(x, y); // 2 5
return x + y;
}
add(2, 5);
// add 함수의 매개변수 x, y는 함수 몸체 내부에서만 참조할 수 있다.
console.log(x, y); // ReferenceError: x is not defined
자바스크립트 함수는 매개 변수와 인수의 개수가 일치하는지 확인하지 않는다. 매개 변수의 개부만큼 인수를 전달하지 않는 경우에도 에러가 발생하지 않는다. 인수가 부족해서 인수가 할당되지 않은 매개변수의 값은 undefined이다.
function add(x, y) {
return x + y;
}
console.log(add(2)); // NaN
매개변수보다 초과된 인수는 무시되나 버려지는 것은 아니다. 모든 인수는 암묵적으로 arguments 객체의 프로퍼티로 보관된다.
function add(x, y) {
console.log(arguments);
// Arguments(3) [2, 5, 10, callee: ƒ, Symbol(Symbol.iterator): ƒ]
return x + y;
}
add(2, 5, 10);
자바스크립트 함수는 동적 타입 언어이다. 매개변수의 타입을 사전에 지정할 수 없다.
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: 인수는 모두 숫자 값이어야 합니다.
이상적인 함수는 한가지 일만 해야 하면 가급적 작게 만들어야 한다. 매개변수는 최대 3개 이상을 넘지 않는 것을 권장한다. 만양 3개 이상의 매개변수인 경우에는 하나의 매개변수를 선언하고 객체를 인수로 전달하는 것이 유리하다. 다음은 jQuery의 ajax 매서드에 객체를 인수로 전달하는 예이다.
$.ajax({
method: 'POST',
url: '/user',
data: { id: 1, name: 'Lee' },
cache: false
});
arguments
arguments 객체는 함수 호출시 전달된 인수들의 정보를 담고 있는 순회 가능한 유사 배열 객체이다. 함수 내부에서 마치 지역 변수처럼 사용되어 함수 외부에서는 참조할 수 없다.
arguments 객체는 인수를 프로퍼티 값으로 소유하며 프로퍼티 키는 인수의 순서를 나타낸다.
arguments 객체는 매개변수 개수를 확정할 수 없는 가변 인자 함수를 구현할 때 유용하다.
function sum() {
let res = 0;
// arguments 객체는 length 프로퍼티가 있는 유사 배열 객체이므로 for 문으로 순회할 수 있다.
for (let i = 0; i < arguments.length; i++) {
res += arguments[i];
}
return res;
}
console.log(sum()); // 0
console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3)); // 6
arguments 객체는 배열 형태로 인자 정보를 담고 있지만 실제 배열이 아닌 유사 배열 객체(length 프로퍼티를 가진 객체로 for문으로 순회할 수 있는 객체)이다.
배열은 아니기 때문에 배열 메서드를 사용하는 경우에는 에러가 발생한다. 따라서 Function.prototype.call, Function.prototype.apply를 사용해 간접 호출해야 한다. 이런 어려움을 해결하기 위해 ES6에서는 Rest 파라미터를 도입했다.
//간접호출
function sum() {
// arguments 객체를 배열로 변환
const array = Array.prototype.slice.call(arguments);
return array.reduce(function (pre, cur) {
return pre + cur;
}, 0);
}
console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3, 4, 5)); // 15
// ES6 Rest parameter
function sum(...args) {
return args.reduce((pre, cur) => pre + cur, 0);
}
console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3, 4, 5)); // 15
참조
<모던 자바스크립트 deep dive>(이웅모 저)
http://milooy.github.io/TIL/JavaScript/call-by-sharing.html
https://velog.io/@aiden/call-by-value-vs-call-by-reference-feat.-call-by-sharing
'javascript' 카테고리의 다른 글
| [Javascript] 실행 컨텍스트(Execution Context) (0) | 2021.11.16 |
|---|---|
| [Javascript] Scope, Hoisting (0) | 2021.11.16 |
| [javascript] object & property descriptor, prototype (0) | 2021.11.16 |
| [javascript] scope, closure, execution context (0) | 2021.11.16 |
| [javascript] this, arguments, function, arrow function, callback (0) | 2021.11.11 |