본문 바로가기

javascript

TIL no.63 - Object 객체 : JavaScript

` YOU DON'T KNOW JS ` 의 Chapter 3 객체 를 읽으며 배운 내용들을 이해하기 위해 정리한 글입니다.

 

이전 챕터에서 this 가 원하는 객체를 가리키게 하기 위해서 헷갈리는 개념임에도 불구하고 이해하기 위해 노력해보았습니다.

그래서 객체는 정확히 무엇일까요 ?

 

자바스크립트를 처음 배울 때도 그렇고 지금도 그렇고 인지하게 되는 사실 하나는 자바스크립트에서 객체를 빼면 시체라는 것입니다.

그만큼 중요하고 어렵기도 한 개념이고 알면 알수록 더 깊어지는 기분이 드는 것 같습니다.

 

자바스크립트는 객체 지향 프로그래밍 언어입니다. 대표적으로 JAVA 에서는 이러한 객체지향 언어를 구현하기 위해서 클래스 기반으로 객체의 자료구조와 기능을 정의하고 생성자를 통해 인스턴스를 생성합니다. 클래스가 없는 자바스크립트는 기존에 프로토타입 기반에서 프로토타입 체인을 이용해 객체 지향 언어를 구사했으며 ES6 이후로 클래스 개념이 등장했으나 다른 언어의 클래스를 모방한 개념으로 동일하지 않습니다.

 

 

 

객체가 아닌 것

단순 원시 타입은 객체가 아닙니다.

  • string
  • number
  • boolean
  • null (가끔 null 타입이 객체라고 알고 있는 사람들이 있는데 이는 언어 자체의 버그에서 비롯된 오해라고 합니다.)
  • undefined

 

내장 객체

내장 객체라고 부르는 객체 하위 타입이 있습니다. 일부는 이름만 보면 대응되는 단순 원시 타입과 직접 연관되어 보이지만 실제 관계는 뜻밖에 복잡합니다.

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

내장 객체는 진짜 타입처럼 보이는 데다 자바의 String 클래스처럼 타 언어와 유사한 겉모습 때문에 꼭 클래스처럼 느껴집니다.

그러다 이들은 단지 자바스크립트의 내장 함수일 뿐이기 때문에 각각 생성자로 사용되어 주어진 하위 타입의 새 객체를 생성합니다.

//  ✅(1)
var str = '나는 문자열이다';
typeof str;   // "string"
str instanceof String;   // false


//  ✅(2)
var strObj = new String('나는 문자열이다');
typeof strObj;   // "object"
strObj instanceof String;   // true


// 객체 하위 타입을 확인한다.
Object.prototype.toString.call( strObj );   // [object String]

*instanceof : 이 연산자는 생성된 인스턴스 객체가 어떤 생성자 함수를 사용하여 생성되었는지 확인하고 불리언값으로 반환하여 주는 유용한 연산자이다. 이 연산자를 사용해 생성자 함수를 확인하는 과정을 예제를 통하여 알아보고자 한다.

 

(1) 과 (2) 의 차이를 설명하자면, '나는 문자열이다' 라는 원시 값은 객체가 아닌 원시 리터럴 이며 불변값입니다.

우리가 .length 를 사용하듯이 문자 개수를 세는 등 문자별로 접근할 때엔 String 객체가 필요합니다.

다행히도 자바스크립트 엔진은 상황에 맞게 문자열 원시 값을 String 객체로 자동 강제변환하기 때문에 명시적으로 객체를 생성할 일은 거의 없습니다. 여러 자바스크립트 커뮤니티에서도 되도록 생성자 형식은 지양하고 리터럴 형식을 사용하라고 권장하기 때문에 이를 알아두고 넘어가면 되겠습니다.  반대로 Date 값은 리터럴 형식이 없어서 반드시 생성자 형식으로 생성해야 합니다.

 

 

내용

객체는 특정한 위치에 저장된 모든 타입의 값, 즉 프로퍼티로 내용이 채워진다고 했습니다. 엔진이 값을 저장하는 방식은 구현 의존적인데, 이는 객체 컨테이너에 담지 않는 게 일반적입니다. 객체 컨테이너에는 실제로 프로퍼티 값이 있는 곳을 가리키는 포인터 역할을 담당하는 프로퍼티명이 담겨 있습니다.

var Obj = {
	a: 2
};

Obj.a;   // 2
Obj["a"];   // 2

' . ' 연산자( 프로퍼티 접근 ) 또는 ' [] ' 연산자( 키 접근 )를 이용해 조회할 수 있습니다. 실제로는 프로퍼티 접근을 더 사용합니다. 

 

 

배열

배열은 양수지만 배열 자체는 객체여서 배열에 프로퍼티를 추가하는 것도 가능합니다. 프로퍼티를 추가하더라도 배열 길이에은 변함이 없습니다. 하지만 일반적으로 키/값 저장소로는 객체, 숫자 인덱스를 가진 저장소로는 배열을 쓰는 것이 좋습니다.

 

 

 

객체 복사

function anotherFunction() {  /*..*/  }
	var anotherObj = {
    	c: true
    };
    
    var anotherArr = [];
    
    var myObj = {
    	a: 2,
        b: anotherObj,
        c: anotherArr,
        d: anotherFunction
    };
    
    anotherArr.push( anotherObj, myObj );

myObj 의 사본을 표현하기 위해서 얕은 복사, 깊은 복사 중 선택해야 합니다.

얕은 복사 후 생성된 새 객체의 a 프로퍼티는 원래 값 2가 그대로 복사되지만 b,c,d 프로퍼티는 원 객체의 레퍼런스와 같은 대상을 가리키는 또 다른 레퍼런스입니다. 깊은 복사를 하면 myObj 는 물론이고 anotherObj 와 anotherArr 까지 모조리 복사합니다. 하지만 여기서 문제는 anotherArr 가 anotherObj 와 myObj 를 가리키는 레퍼런스를 가지고 있으므로 원래 레퍼런스가 보존되는 것이 아니라 이들까지 함께 복사됩니다. 결국, 환형 참조 형태가 되어 무한 복사의 구렁텅이에 빠지고 맙니다.

 

한편, 얕은 복사는 이해하기 쉽고 별다른 이슈가 없기에 ES6 부터는 Object.assign() 메서드를 제공합니다. 

var newObj = Object.assign( {}, myObj );
newObj.a;   // 2
newObj.b === anotherObj;   // true
newObj.c === anotherArr;   // true
newObj.d === anotherFunction;   // true

Object.assgin() 은 순수하게 할당에 의해서만 복사합니다.

 

 

 

 

[[Get]]

프로퍼티에 접근하기까지의 세부 과정은 미묘하면서도 중요합니다.

var myObj = {
	a: 2
};

myObj.a;   // 2

myObj.a 는 누가 봐도 프로퍼티 접근이지만 보이는 것처럼 a 란 이름의 프로퍼티를 myObj 에서 찾지 않습니다. 

명세에 따르면 실제로 이 코드는 myObj 에 대해 [[Get]] 연산을 합니다.

기본으로 [[Get]] 연산은 주어진 이름의 프로퍼티를 먼저 찾아보고 있으면 그 값을 반환합니다. 프로퍼티를 찾아보고 없으면 [[Get]] 연산 알고리즘은 다른 중요한 작업을 하도록 정의되어 있습니다. 

주어진 프로퍼티 값을 어떻게 해도 찾을 수 없으면 [[Get]] 연산은 undefined 를 반환합니다.

 

 

 

[[Put]]

내부적으로 프로퍼티 값을 얻는 [[Get]] 이 있다면 프로퍼티를 세팅/생성하는 [[Put]] 연산도 당연히 정의되어 있을 것입니다. 

[[Put]] 을 실행하면 주어진 객체에 프로퍼티가 존재하는지 등 여러 가지 요소에 따라 이후 작동 방식이 달라집니다.

 

(1) 프로퍼티가 접근 서술자인가? 맞으면 세터를 호출한다.

(2) 프로퍼티가 writable: false 인 데이터 서술자인가? 맞으면 비엄격 모드에서 조용히 실패하고 엄격 모드는 TypeError 가 발생한다.

(3) 이외에는 프로퍼티에 해당 값을 세팅한다.

 

객체에 존재하지 않는 프로퍼티라면 [[Put]] 알고리즘은 훨씬 더 미묘하고 복잡해집니다. 이 내용은 5장의 프로토타입에서 명확히 이해할 수 있을 것입니다.

 

 

게터와 세터

[[Put]] 과 [[Get]] 기본 연산은 이미 존재하거나 전혀 새로운 프로퍼티에 값을 세팅하거나 기존 프로퍼티로부터 값을 조회하는 역할을 각각 담당합니다. 

ES5 부터는 게터와 세터를 통해 프로퍼티 수준에서 이러한 기본 로직을 오버라이드할 수 있습니다. 게터/세터는 각각 실제로 값을 가져오는/세팅하는 감춰진 함수를 호출하는 프로퍼티입니다.

프로퍼티가 게터 또는 세터 어느 한쪽이거나 동시에 게터/세터가 될 수 있게 정의한 것을 '접근 서술자' 라느 합니다. 

접근 서술자에서는 프로퍼티의 값과 writable 속성은 무시되며 configurable, enumerable 과 더불어 프로퍼티의 겟/셋 속성이 중요합니다.

 

게터와 세터는 프로퍼티의 값을 원하는 값으로 통제하기 위한 캡슐화로 사용하기도 하며 공통 포맷을 위해서나 은닉과 필드 통제를 위해 사용합니다.  게터와 세터를 이용해 Read와 Write 권한을 다르게 할 수 있습니다.

class User {
	constructure(firstName, lastName, age) {
    	this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
    
    get age() {
    	return this._age;
    }
    
    set age(value) {
    	// if(value < 0) {
        // 	throw Error('0 이하의 수는 사용될 수 없습니다 !');
        // }
    	this._age = value < 0 ? 0 : value;
    }
}

 

 

 

위 내용 외에도 객체 불변성, 확장 금지, 봉인, 동결 등의 내용을 책에서 다루었습니다. 더 보안적으로 신경써야 하는 코드일 때 객체 지향으로 코드를 작성하면서 객체의 특성들을 잘 알고 있다면 탄탄한 코드를 짤 수 있겠다는 생각이 들었습니다. 공부하면서 객체가 눈에 보이는 것보다도 많은 것들을 할 수 있고 복잡하다는 생각이 들었습니다.