2023. 3. 28. 09:11ㆍ개발 관련 책 읽기/모던 자바스크립트 Deep Dive
✅ 아래 내용들에 대해서 알아보자
- 객체란?
- 프로퍼티
- 원시 값과 객체의 비교
- 변경 가능한 값
- 얕은 복사/깊은 복사
- 값에 의한 전달/참조에 의한 전달
객체
JS는 객체 기반의 프로그래밍 언어이며, 모든 것이 "객체"이다. 원시 값을 제외한 나머지 값은 모두 객체다.
원시 값은 변경 불가능한 값(Immutable)이지만 객체 타입의 값은 변경 가능한 값(Mutable)이다.
- 객체는 0개 이상의 프로퍼티로 구성된 집합이며, 프로퍼티는 key/value 구조로 구성된다.
- JS에서 사용할 수 있는 모든 값은 프로퍼티 값이 될 수 있다. JS 함수는 일급 객체 이므로 값으로 취급할 수 있다.(프로퍼티 값이 함수일 경우 일반 함수와 구분되게 메서드라고 부른다)
- 프로퍼티 : 객체의 상태를 나타내는 값, 메서드 : 프로퍼티를 참조하고 조작할 수 있는 동작
- 객체는 프로퍼티의 개수가 정해져 있지 않으며, 동적으로 추가/삭제할 수 있다.
JS 객체는 프로퍼티 키를 인덱스로 사용하는 해시 테이블이라고 생각할 수 있다. 대부분의 JS 엔진은 해시 테이블과 유사하지만 높은 성능을 위해 일반적은 해시 테이블보다 더 나은 방법으로 객체를 구현한다.
자바나, C++ 같은 클래스 기반 객체지향 언어에서는 사전에 정의된 클래스를 기반으로 객체를 생성한다. 객체가 생성된 후에는 프로퍼티를 삭제/추가할 수없다.
하지만 JS는 클래스 없이 객체를 생성할 수 있으며 객체가 생성된 이후라도 동적으로 프로퍼티와 메서드를 추가할 수 있다. 사용성인 면에서는 편리하지만 성능면에서는 비용이 더 많이 드는 효율적인 방식이다.
그리고 무분별한 동적 추가 삭제로 인해 유지보수 하기가 힘들어질 수 있다.
객체 생성 방식
- 객체 리터럴 : 가장 일반적이고 간단한 객체 생성 방법
- Object 생성자 함수
- 생성자 함수
- Object.create 메서드
- 클래스(ES6)
//객체 리터럴 방식으로 객체 생성하기
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: f}
//
{name: "Lee", sayHello: ƒ sayHello()}
name: "Lee"
sayHello: ƒ sayHello() {}
<constructor>: "Function"
name: "Function"
프로퍼티
객체는 프로퍼티의 집합이며, 프로퍼티는 key/value로 구성된다.
- 프로퍼티 키는 값에 접근할 수 있는 식별자 역할을 한다.
- 일반적으로 식별자 네이밍 규칙을 따라야 한다. 규칙을 따르지 않은 이름에는 반드시 따옴표를 사용해야 한다. 따옴표 미사용 시 Syntax Error(문법 에러)가 발생한다.
var person={
name:'seok',
age:20,
increase : function (){
this.age++;
}
}
//프로퍼티 접근 방법
console.log(person);
console.log(person['age'])
console.log(person.age)
//객체에 존재하지 않는 프로퍼티 접근 시 참조 에러가 발생하지 않고 undefined가 출력됨
console.log(person.phone)
- 프로퍼티 키에 문자열이나 심벌 값 외의 값을 사용하면 암묵적으로 문자열로 타입 변환이 된다.
- 객체에 존재하지 않는 프로퍼티에 접근하면 undefined를 반환한다. 이때 ReferenceError가 발생하지 않는다.
- 환경(브라우저, Node)에 따른 프로퍼티 접근법에 대한 이슈도 각각 다르다.
var person2={
'last-name':'seok',
1:10
};
/**
* node랑 브라우저 마다 결과값이 달라지는 이유는
* person.last-name을 실행할 때 JS 엔진은 먼저 person.last를 평가한다.
* person 객체에는 프로퍼티 키가 last인 프로퍼티가 없기 때문에 person.last는 undefined로 평가된다.
* 따라서 person.last-name은 undefined-name과 같다. 다음으로 JS엔진은 name이라는 식별자를 찾는다.
* 이때 프로퍼티키가 아니라 식별자로 해석된다.
*
* node환경에서는 현재 어디에서도 name이라는 식별자 선언이 없으므로 "name is not defined" 에러 발생한다.
* 그런데 브라우저 환경에서는 name이라는 전역 변수(전역 객체 window의 프로퍼티)가 암묵적으로 존재한다.
* 전역 변수 name은 창의 이름을 가리키며, 기본값은 빈 문자열이다. 따라서 person.last-name은 undefined-''과 같으므로 NaN이 된다.
*
*/
// console.log(person2.'last-name'); //SyntaxError: Unexpected string
console.log(person2.last-name); //브라우저 환경 : NaN, node.js : ReferenceError: name is not defined
console.log(person2[last-name]); //ReferenceError: last is not defined
console.log(person2['last-name']); //seok 출력
프로퍼티 동적 생성
존재하지 않는 프로퍼티에 값을 할당하면 프로퍼티가 동적으로 생성되어 추가되고 값이 할당된다.
var person={
name:'seok',
age:20,
increase : function (){
this.age++;
}
}
//person 객체에 age 프로퍼티가 존재하지 않는다.
//따라서, person 객체에 age 프로퍼티가 동적으로 생성되고 값이 할당된다
person.phone='010-0000-0000';
console.log(person)
{
name: 'seok',
age: 21,
increase: [Function: increase],
phone: '010-0000-0000'
}
프로퍼티 삭제
delete 연산자는 객체의 프로퍼티를 삭제한다. 이때 delete 연산자의 피연산자는 프로퍼티 값에 접근할 수 있는 표현식이어야 한다. 만약 존재하지 않는 프로퍼티 삭제 시 아무런 에러 없이 무시된다.
delete person.phone;
console.log(person) //{ name: 'seok', age: 21, increase: [Function: increase] }
메서드 축약 표현
ES6에서는 메서드를 정의할 때 function 키워드 생략한 축약 표현을 사용할 수 있다.
//ES5
var obj={
name : 'Lee',
//ES6에서는 function 키워드 축약 표현이 가능하다.
sayHi: function(){
console.log("Hi!");
}
};
//ES6
var obj={
name : 'Lee',
//ES6에서는 function 키워드 축약 표현이 가능하다.
sayHi(){
console.log("Hi!");
}
};
console.log(obj.sayHi())
원시 값과 객체의 비교
JS가 제공하는 7가지 데이터 타입(숫자, 문자열, 불리언, null, undefined, 심벌, 객체타입)은 크게 원시타입과 객체타입으로 구분할 수 있다. 데이터 타입을 구분하는 이유는 무엇일까? 원시 타입과 객체 타입은 근본적으로 다르다는 의미일 것이다.
- 원시 타입의 값, 즉 원시 값은 변경 불가능(Immutable)한 값이다. 이에 비해 객체(참조) 타입의 값, 즉 객체는 변경 가능(Mutable)한 값이다.
- 원시 값을 변수에 할당하면 변수에는 실제 값이 저장된다. 이에 비해 객체를 변수에 할당하면 참조 값(메모리 주소)이 저장된다.
- 원시 값을 갖는 변수를 다른 변수에 할당하면 원본의 값이 복사되어 전달된다.(pass by value)
- 그에 비해 객체를 가리키는 변수를 다른 변수에 할당하면 원본의 참조 값(메모리 주소)이 복사되어 전달된다.(pass by reference)
예시코드
function changeValues(a,b){
a=10;
b.push(10);
console.log(a); //10
console.log(b); //[1, 2, 3, 10]
}
let x = 5;
let y=[1,2,3];
console.log(typeof x, x) //number 5
console.log(typeof y, y) //object [1, 2, 3]
changeValues(x,y);
console.log(x); //5, 원시 타입은 call by value로 값을 복사하여 전달하므로 기존 값은 영향을 받지 않고 그대로이다.
console.log(y); //[1,2,3,10], 객체 타입은 call by reference로 참조 값을 전달하므로 값 변경시 기존 값도 같이 영향을 받게 된다
원시 값
원시 타입의 값은 read-only 값으로서 변경할 수 없다. 즉, 값 자체를 변경할 수 없다는 것이지 변수값을 변경할 수 없다는 것은 아니다. 변수는 언제든지 재할달을 통해 변수 값을 변경(엄밀히 말하자면 교체)할 수 있다.
변수의 상대 개념인 상수는 재할당이 금지된 변수를 의미하며, 상수도 메모리 공간에 할당이 될 것이다.
상수는 단 한 번만 할당이 허용되므로 변수 값을 변경(교체)할 수 없다. 따라서 상수와 변경 불가능한 값을 동일하게 보면 안 된다!
const v1=1;
v1=1; //Attempt to assign to const or readonly variable
//상수 선언, const 키워드를 통해 선언한 변수는 재할당 금지
const o= {};
console.log(typeof o,o) //object, {}
//const 키워드를 사용한 객체는 변경할 수 있다.
o.a=1;
console.log(o);
변수에 값을 변경 시 원시값은 그대로 있고 새로 재할당한 값이 메모리 공간에 적재되어 변수는 해당 메모리 주소를 참조하게 된다.
즉, 원시값은 변경되지 않고 그대로 있게 되고 변수가 가리키는 원시값의 주소 공간만 변하게 된다.
원시값 문자열
ECMAScript 사양에 문자열 타입은 2바이트, 숫자 타입은 8바이트 이외 원시 타입은 크기를 명확히 규정하고 있지 않아 브라우저 제조사 따라 크기가 다를 수 있다.
특징
- 숫자값은 1,100000도 동일한 8바이트의 공간이 필요하지만 문자열은 몇 개의 문자로 이뤄졌느냐에 따라 메모리 공간의 크기가 달라진다. 예를 들면 문자열의 길이가 10이면 20바이트가 필요하다.
- 문자열은 원시값으로 변경할 수 없다. 즉 데이터의 신뢰성을 보장한다.
var str='Hello';
// str='World';
//값 변경 시도
str[0]='z';
console.log(str); //문자열은 원시값이라 변경되지 않음, Hello 출력
변경가능한 값
객체(참조) 타입의 값, 즉 객체는 변경 가능한 값(Mutable)이다. 객체를 할당한 변수를 참고하면 메모리에 저장되어 있는 참조 값을 통해 실제 객체에 접근한다. 즉 해당 변수는 C언어로 봤을 때 포인터 역할을 한다. 코드를 보면서 이해해 보자.
var person = {
name : 'Lee'
}
아래 그림을 보면 person이라는 객체를 담은 변수에는 0x00001332라는 메모리 주소 값을 가지고 있으며, 해당 메모리 주소를 참조하면 실제 name:'Lee'라는 값을 가진 것을 확인할 수 있다.
원시값에 비해 객체는 재할당 없이 직접 변경이 가능하다. 코드를 보자
var person = {
name : 'Lee'
}
//프로퍼티 갱신
person.name = 'Kim';
//프로퍼티 동적 생성
person.address = 'Seoul';
console.log(person); // {name:"Kim", address: "Seoul"}
위 그림을 보면 실제 객체의 프로퍼티들이 변경된 것을 확인할 수 있다. 실제 객체를 담은 변수에의 참조값을 변경되지 않는 것을 확인할 수 있다.
얕은 복사/깊은 복사
얕은 복사: 객체를 복사할 때 원본 객체의 주소값만 복사한다. 따라서 원본 객체와 복사된 객체는 같인 객체를 참조하고 있어서 복사된 객체의 값을 변경하면 원본 객체의 값도 변경된다.
const origin = { a :1, b:{c:2}};
const shallowCopy = Object.assign({},origin);
shallowCopy.b.c=3;
console.log(origin.b.c) //3
console.log(shallowCopy.b.c); //3
깊은 복사 : 객체를 복사할 때 원본 객체의 값을 모두 복사하여 새로운 객체를 만든다.(새로운 공간에 메모리 할당) 따라서 복사된 객체의 값이 변경되더라도 원본 객체의 값은 변하지 않는다.
const co= {x:{y:1}};
//lodash의 cloneDeep을 사용한 깊은 복사
const _=require('lodash');
const c2 = _.cloneDeep(co);
c2.x.y=10; //프로퍼티 값 변경
console.log(co.x.y); //1
console.log(c2.x.y); //10
값에 의한 전달(pass by value)
아래 코드를 보면 score 변수와 copy 변수의 값 80은 각각 메모리 공간에 저장된 별개의 값으로 서로의 메모리 공간의 주소는 다르다.
따라서 변수 값을 바꿔도 영향이 받지 않게 된다.
var score=80;
var copy=score;
console.log(score); //80
console.log(copy); //80
score=100;
console.log(score); //100
console.log(copy); //80
핵심은 결국 "값에 의한 전달"도 사실은 값을 전달하는 것이 아니라 메모리 주소를 전달한다. 단, 전달된 메모리 주소를 통해 메모리 공간에 접근하여 값을 참조할 수 있는 것이다.
결국 score, copy 변수의 메모리 주소는 각각 다름으로 어느 한쪽에서 재할당을 통해 값을 변경하더라도 서로 간섭할 수 없다는 것이다.
참조에 의한 전달(pass by value)
객체를 가리키는 변수를 다른 변수에 할당하면 원본의 참조 값이 복사되어 전달된다. 이를 참조에 의한 전달이라 한다.
var person = {
name : 'Lee'
};
// 참조 값을 복사(얕은 복사)
var copy=person;
person이 가리키는 주소값을 copy에 복사하고(얕은 복사) 이로 인해 두 객체 변수가 동일한 주소값음 참조하고 있다.
이로 인해 person, copy = 0x00001332 같은 주소를 참고하고 있게 되고 둘 중 하나라도 객체 프로퍼티를 변경하게 되면 서로 영향을 주고받게 된다.
아래 코드처럼 변경이 발생하게 되면 둘 다 영향을 받게 된다. 결국 "값에 의한 전달"과 "참조에 의한 전달"은 변수가 가리키는 메모리 공간에 저장되어 있는 값을 복사해서 전달한다는 면에서 동일하다. 다만 변수에 저장되어 있는 값이 원시 값이냐 참조 값이냐의 차이만 있을 뿐..
따라서 JS에서는 "참조에 의해 전달"은 존재하지 않고 "값에 의해 전달"만이 존재한다고 말할 수 있다. (Java랑 비슷하다)
//shallow copy
var copy=person;
console.log(copy === person) //true
copy.name='seok';
person.address='seoul';
console.log(person); //{ name: 'seok', address: 'seoul' }
console.log(copy); //{ name: 'seok', address: 'seoul' }
퀴즈~
var person1 ={
name : "abc"
};
var person2 ={
name : "abc"
};
/**
* 일치 비교 연산자는 값과 타입을 모두 비교한다. 객체를 할당한 변수를 참고하면 참조 값을 비교하고 원시 값 변수이면 원시 값을 비교한다
* 따라서 person1과 person2의 메모리 주소 공간의 참조값은 서로 다르므로 false
* person1.name과 person2.name의 값과 타입은 같으므로 true
*/
console.log(person1 === person2); //?
console.log(person1.name === person2.name) //?
'개발 관련 책 읽기 > 모던 자바스크립트 Deep Dive' 카테고리의 다른 글
모던 자바스크립트 Deep Dive - 7 (0) | 2023.03.30 |
---|---|
모던 자바스크립트 Deep Dive - 6 (0) | 2023.03.29 |
모던 자바스크립트 Deep Dive - 4 (0) | 2023.03.24 |
모던 자바스크립트 Deep Dive - 3 (0) | 2023.03.23 |
모던 자바스크립트 Deep Dive - 2 (0) | 2023.03.22 |