본문 바로가기

JavaScript

자바스크립트 완벽 가이드 | 객체

객체

객체는 복합된 값이다.
객체는 여러가지 값(기본 값이나 다른 객체)을 모아서 이름을 통해 값을 저장하고 가져올 수 있게 한다.
객체는 프로퍼티의 순서가 없는 집합이며 각 프로퍼티에는 이름과 값이 있다.
포로퍼티 이름은 보통 문자열로 객체가 문자열에 값을 연결한다고 볼 수도 있다.

객체는 단순히 문자열과 값을 연결한 것이 아니다.
자바스크립트 객체는 자신만의 프로퍼티를 가지는 것 외에도, '프로토타입'으로 불리는 다른 객체에서 프로퍼티를 상속하기도 한다.
객체의 메서드는 일반적으로 상속된 프로퍼티이며, 이 프로토타입 상속이 자바스크립트의 중요한 기능이다.

자바스크립트 객체는 동적이기 때문에 일반적으로 프로퍼티를 추가하거나 삭제할 수 있지만, 정적인 객체를 흉내낼 수도 있고 정적 타입을 사용하는 언어의 구조역시 사용할 수 있다.

자바스크립트에서 문자열,숫자,심벌,true,false,null,undefined가 아닌 값은 전부 객체이다. 또한 문자열, 숫자, 불은 객체가 아니지만 불변인 객체처럼 행동 가능하다.
객체를 통해 하는 일은 생성, 삭제, 수정, 검색, 테스트, 프로퍼티 열거로 나눌 수 있다.

프로퍼티에는 일므과 값이 있다. 프로퍼티 이름에는 빈문자열과 심벌을 포함해 어떤 문자열이든 쓸 수 있지만, 같은 이름의 프로퍼티는 존재할 수 없다.
자바스크립트에서 상속되지 않는 프로퍼티를 가리켜 자체 프로퍼티(own property)라고 한다.
모든 프로퍼티에는 이름과 값 외에도 다음과 같은 세가지 프로퍼티 속성이 있다.

  • 쓰기 가능(writable): 속성은 프로퍼티에 값을 설정할 수 있는지 없는지를 나타낸다.
  • 열거 가능(enumerable): for/in루프에 프로퍼티 이름을 반환할지 안할지를 나타낸다.
  • 변경 가능(configurable): 프로퍼티를 삭제할 수 있는지 없는지, 속성을 바꿀 수 있는지 없는지.

JS 내장 객체의 프로퍼티 중 상당수는 읽기 전용이거나 열거 불가이거나 변경 불가능하다. 우리가 만드는 객체의 프로퍼티는 기본적으로 쓰기, 열거, 변경가능하다.

객체 생성

객체를 생성할 때는 객체리터럴, new키워드, Object.create()함수를 사용한다.

객체 리터럴

객체 리터럴의 가장 단순한 형태는 콜론으로 구분한 이름:값 쌍을 콤마로 구분해 중괄호로 감싼 형태.
프퍼티 이름은 자바스크립트 식별자 또는 문자열 리터럴이고, 빈문자열도 허용
프로퍼티 값은 자바스크립트 표현식이면 무엇이든 허용.

 

let book = {
  "main title": "JavaScript", // 프로퍼티 이름에 스페이스와 하이픈이 들어가 있으므로 문자열 리터럴을 사용
  "sub-title": "The Definitive Guide",
  for: "all audiences", // for는 예약어이지만 따옴표를 쓰지 않는다.
  author: {
    firstname: "David",
    surname: "Flanagan",
  },
}

 

객체 리터럴을 평가할 때마다 새 값이 만들어진다.
각 프로퍼티 값 역시 리터럴을 평가할 때마다 평가된다. 따라서 객체 리터럴 자체가 바뀌지 않더라도 반복적으로 호출되는 함수나 루프 바디 안에 있다면 새 객체를 여러 개 만들 수 있으며, 이 객체들의 프로퍼티 값 역시 매번 달라질 수 있다.

new

new연산자는 새 객체를 생성하고 초기화한다. new 키워드 뒤에는 반드시 함수 호출이 있어야한다. 이런 형태로 사용하는 함수를 생성자라고 부르고, 새로 생성된 객체를 초기화하는 목적으로 사용한다.

자바스크립트 내장 타입에도 생성자가 있다.

 

let o = new Object() // 빈 객체를 만든다. {}와 동일
let a = new Array() // 빈 배열을 만든다. []와 동일
let d = new Date() // 현재 시간을 나타내는 Date객체를 만듦
let r = new Map() // 키와 값을 연결하는 Map 객체를 만든다.

 

이런 내장 생성자 외에도 직접 생성자 함수를 만들어 객체를 초기화하는데 쓸 수 있다.

프로토타입

자바스크립트 객체 거의 대부분은 자신과 연결된 두 번째 객체를 갖는다. 여기서 두번째 객체를 프로토타입이라고 부르며, 첫번째 객체는 프로토타입에서 프로퍼티를 상속한다.

객체 리터럴을 사용해 생성한 객체는 모두 같은 프로토 타입 객체를 갖는다. 그리고 이 프로토 타입 객체는 Object.prototype이라는 코드로 참조할 수 있다.

new 키워드와 생성자를 통해 만든 객체는 생성자 함수의 prototype 프로퍼티 값을 자신의 프로토타입으로 사용한다. 따라서 new Object()로 생성한 객체는 {}로 생성한 객체와 마찬가지로 Object.prototype에서 상속한다. 마찬가지로 new Array()로 생성된 객체의 프로토타입은 Array.prototype이며, new Date()로 생성한 객체의 프로토타입은 Date.prototype이다.

prototype 프로퍼티를 갖는 객체가 다른 객체의 프로토타입을 정의한다.
Object.prototype은 프로토타입이 없는 드문 객체중 하나이다. 이 객체는 어떤 프로퍼티도 상속하지 않는다.
다른 프로토타입 객체는 일반적인 객체이며 역시 프로토타입이 있다.
예를 들어 Date.prototype은 Object.prototype에서 프로퍼티를 상속하므로 new Date로 생성한 Date객체는 Date.prototype과 Object.prototype에서 프로퍼티를 상속한다.
이렇게 이루어지는 프로토타입 객체 사이의 연결을 프로토타입 체인이라고 부른다.

Object.create()

Object.create()는 첫번째 인자를 프로토타입 삼아 새 객체를 생성.

 

let o1 = Object.create({x: 1, y: 2}) // o1은 x와 y프로퍼티를 상속
o1.x + o1.y = 3

 

인자로 null을 전달해 프로토타입이 없는 객체를 생성할 수도 있지만, 이렇게 생성된 객체는 아무것도 상속하지 않으며 toString()과 같은 기본 메서드조차 없습니다. 따라서 +연산자와 함께 사용할 수도 없습니다.

 

let o2 = Object.create(null) // o2는 프로퍼티나 메서드를 상속하지 않습니다.

 

{}나 new Object()가 반환하는 것처럼 일반적인 빈 객체를 만들고 싶을 때는 다음과 같이 Object.prototype을 전달합니다.

 

let o3 = Object.create(Object.prototype) // o3는 {}나 new Object()와 같습니다.

 

프로퍼티 검색과 설정

프로퍼티 값에 접근할 때에는 점이나 대괄호 연산자를 사용합니다. 이 연산자의 왼쪽은 값이 객체인 표현식이어야합니다. 점연산자를 사용한다면 오른쪽은 반드시 프로퍼티 이름인 단순한 식별자이어야한다. 대괄호를 사용한다면 그 안에 있는 값은 원하는 프로퍼티 이름인 문자열로 평가되는 표현식이어야 한다.

대괄호를 사용하면 그 안의 표현식이 반드시 문자열로 평가되어야 한다. 표현식은 문자열 또는 심벌로 변환도리 수 있는 값으로 평가되어야 한다.

문자열을 인덱스로 사용하는 배열을 연관 배열(associative array)라고 부릅니다. 또는 해시, 맵, 딕셔너리라고 부르기도 한다. 자바스크립트 객체는 연관 배열이다.

점 연산자를 사용해 객체 프로퍼티에 접근할 때 프로퍼티 이름은 식별자로 표현된다.
식별자는 만드시 문자 그대로 프로그램에 입력해야한다. 식별자는 데이터 타입이 아니므로 프로그램에서 조작할 수 없다.

대괄호 연산자로 객체 프로퍼티에 접근할 때는 프로퍼티 이름을 문자열로 표현한다. 문자열은 자바스크립트 데이터 타입이므로 프로그램이 실행되는 동안 새로 생성할 수도 있고 조작할 수도 있다.

프로퍼티 접근 에러

존재하지 않는 프로퍼티를 검색하는 것은 에러가 난다.
프로퍼티의 접근 표현식은 점 연산자의 왼쪽이 null이나 undefined면 실패한다.

객체 o에 프로퍼티 p를 설정하려는 시도는 다음과 같은 상황에 실패한다.

  • o에 자체 프로퍼티 p가 있고 읽기 전용일 때: 읽기 전용 프로퍼티의 값은 바꿀 수 없다.
  • o에 상속된 프로퍼티 p가 있고 읽기 전용일 때: 상속된 읽기 전용 프로퍼티를 같은 이름의 자체 프로퍼티로 가릴 수 없다.
  • o에 자체 프로퍼티 p가 없으며 세터 메서드로 프로퍼티 p를 상속하지 않고 o의 확장 가능(extensible)속성이 false일 때: p는 o에 존재하지 않고 호출할 세터 메서드도 없으므로 p를 o에 추가해야 하지만, o는 확장 불가이므로 새 프로퍼티를 정의할 수 없다.

프로퍼티 삭제

delete연산자는 객체에서 프로퍼티를 삭제한다.
피연산자는 프로퍼티 접근 표현식이어야 한다.
값을 삭제하는 것이 아니라 프로퍼티 자체를 삭제한다.

delete 연산자는 자체 프로퍼티만 삭제할 뿐 상속된 프로퍼티는 삭제하지 않는다.
상속된 프로퍼티를 삭제하려면 반드시 해당 프로퍼티를 정의한 프로토타입 객체에서 삭제해야한다. -> 이렇게 하면 해당 프로토타입을 상속한 객체 전체에 영향을 미친다.

delete 표현식은 삭제에 성공했을 때, 또는 존재하지 않는 프로퍼티 삭제를 시도하는 등 효과가 없었을 때 true로 평가된다. delete는 프로퍼티 접근 표현식이 아닌 표현식을 사용했을 때도 true로 평가.

 

let o = { x: 1 } // o에는 자체 프로퍼티 x가 있고 toString을 상속
delete o.x // o에서 x를 삭제한다.
delete o.x // ture -> x가 존재하지 않으므로 아무 일도 일어나지 않지만 어쨌튼 true
delete o.toString // true -> toString은 자체 프로퍼티가 아니므로 아무 일도 일어나지 않지만 어쨌튼 true
delete 1 // true, 의미 없지만 true

 

delete는 변경 가능 속성이 false인 경우 제거하지 않는다.
strict 모드에서 변경 불가능한 프로퍼티를 삭제하려고 하면 TypeError가 난다.
일반 모드에서는 에러가 일어나지 않고 false로 평가된다.

 

// 다음은 모두 일반 모드의 결과이며 스트릭트 모드에서는 모두 TypeError을 일으킨다.
delete Object.prototype // false 프로퍼티가 변경 불가능하다.
var x = 1 // 전역 변수 선언
delete globalThis.x // false 이 프로퍼티는 삭제할 수 없다.
function f() {} // 전역 함수 선언
delete globalThis.f // false 이 프로퍼티 역시 삭제할 수 없다.

일반모드에서 전역 객체의 변경 가능 프로퍼티를 삭제할 때는 다음과 같이 전역 객체에 대한 참조를 생략하고 delete 연산자뒤에 프로퍼티 이름을 써도 된다.

 

globalThis.x = 1 // 변경 가능한 전역 프로퍼티를 생성. (let이나 var는 불가능)
delete x // true 이 프로퍼티는 삭제할 수 있다.

 

스트릭트 모드에서 delete에 x같은 유효하지 않은 식별자를 피연산자로 쓰면 SyntaxError가 일어나므로 프로퍼티 접근도 명시적으로 해야함.

 

delete x // 스트릭트 모드에서는 SyntaxError
delete globalThis.x // 명시적으로 전역 객체를 참조해야 동작.

프로퍼티 테스트

in연산자, hasOwnProperty(), propertyIsEnumerable()

 

let o = { x: 1 }
"x" in o // true
"y" in o // false
"toString" in o // true

 

hasOwnProperty()는 객체에 주어진 이름을 가진 자체 프로퍼티가 있는지 테스트.
상속된 프로퍼티에는 false를 반환

 

let o = { x: 1 }
o.hasOwnProperty("x") // true
o.hasOwnProperty("y") // false
o.hasOwnProperty("toString") // false: toString은 상속된 프로퍼티이다.

 

propertyIsEnumerable()는 hasOwnProperty()를 더 제한한 버전.
이 메서드는 지정된 프로퍼티가 자체 프로퍼티이며 열거 가능 속성이 true일 때만 true를 반환.
일부 내장 프로퍼티는 열거 불가.
일반적으로 자바스크립트 코드로 생성한 프로퍼티는 모두 열거 가능

 

let o = { x: 1 }
o.propertyIsEnumerable("x") // true : o에는 열거 가능 프로퍼티 x가 있다.
o.propertyIsEnumerable("toString") // false: 자체 프로퍼티가 아니다.
Object.prototype.propertyIsEnumerable("toString") // false: 열거 불가능하다.

in연산자 대신 프로퍼티를 검색하고 !==를 써서 undefined가 아님을 확인하는 경우도 많다.

let o = { x: 1 }
o.x !== undefined // true
o.y !== undefined // false
o.toString !== undefined // true

 

in연산자에는 앞에서 예로 든 단순 프로퍼티 접근으로는 불가능한 기능이 하나 더 있다.
in은 존재하지 않는 프로퍼티와 존재하지만 값이 undefined인 프로퍼티를 구분할 수 있다.

 

let o = { x: undefined }
o.x !== undefined // false
o.y !== undefined // false
"x" in o // true
"y" in o // false
delete o.x
"x" in o // false

프로퍼티 열거

for/in루프는 지정된 객체의 상속여부를 구분하지 않고 열거 가능 프로퍼티마다 그 이름을 루프 변수에 할당하면서 루프 바디를 실행한다.
객체가 상속하는 내장 메서드는 열거 불가이지만, 여러분이 추가한 프로퍼티는 열거 가능하다.

let o = { x: 1, y: 2, z: 3 }
o.propertyIsEnumerable("toString") // false
for (let p in o) {
  consoel.log(p) // x, y, z 를 출력하지만 toString은 출력하지 않는다.
}

for/in 루프를 사용하는 것보다는 객체의 프로퍼티 이름을 배열에 저장해서 for/of 루프를 사용하는 것이 더 쉬울 때가 많다. 프로퍼티 이름을 배열로 저장할 수 있는 함수는 네가지이다.

  • Object.keys(): 객체의 열거 가능한 자체 프로퍼티 이름을 배열로 반환. 이 메서드는 열거 불가 프로퍼티, 상속된 프로퍼티, 이름이 심벌이 프로퍼티는 내보내지 않는다.
  • Object.getOwnPropertyNames(): Object.keys()와 비슷하지만 이름이 문자열이기만 하면 열거 불가인 자체 프로퍼티 이름도 배열로 반환.
  • Object.getOwnPropertySymbols(): 열거 가능 여부를 따지지 않고 이름이 심벌인 자체 프로퍼티를 배열로 반환.
  • Reflect.ownKeys(): 열거 가능 여부를 따지지 않고, 문자열인지 심벌인지도 구분하지 않고 자체 프로퍼티 이름은 전부 배열로 반환.

열거 방법

  • 이름이 음이 아닌 정수인 문자열 프로퍼티가 첫 번째로 나열되며 작은 수에서 큰 수 순으로 열거. 따라서 배열 및 배열 비슷한 객체의 프로퍼티도 순서대로 열거.
  • 배열 인덱스와 비슷한 프로퍼티를 모두 열거한 다음에는 음수나 부동 소숫점 숫자처럼 보이는 프로퍼티를 포함해 이름이 문자열인 프로퍼티를 열거. 이 프로퍼티는 객체에 추가된 순서대로 열거. 객체 리터럴로 정의된 프로퍼티는 리터럴에 쓰인 순서를 따른다.
  • 마지막으로, 이름이 심벌인 프로퍼티를 객체에 추가된 순서로 열거

for/in은 자체 프로퍼티를 앞서 설명한 순서대로 열거한 후 프로토타입 체인을 따라 올라가면서 각 프로포타입 객체에 대해 열거 가능한 프로퍼티를 같은 순서로 열거.

객체 확장

 

let target = { x: 1 },
  source = { y: 2, z: 3 }
for (let key of Object.keys(source)) {
  target[key] = source[key]
}
target => {x: 1, y: 2, z: 3}

 

ES6에서 이런 기능을 Object.assign()이라는 이름으로 자바스크립트 코어에 도입.
Object.assign()은 인자로 두개 이상의 객체를 받는다. 첫번째는 수정해서 반환할 대상, 두번째 또는 그 이후의 인자는 소스 객체이므로 수정하지 않는다.
각 소스 객체의 열거 가능한 자체 프로퍼티를 (이름이 심벌인것 포함) 대상 객체에 복사한다.
복사할 때 소스 객체를 인자 순서대로 처리하는데, 첫 번째 소스 객체의 프로퍼티는 대상 객체이 있는 같은 이름의 프로퍼티를 덮어쓰고, 두번째 소스 객체가 있다면 그 객체의 프로퍼티가 첫번째 소스 객체에 있는 같은 이름의 프로퍼티를 덮어 쓴다.

소스 객체에 게터 메서드가 있거나 대상 객체에 세터 메서드가 있다면 복사 도중에 호출되긴 하지만 메서드 자체는 복사 안함.

 

o = Object.assign({}, defaults, o)

 

 

// Object.assign과 마찬가지이지만 기본 프로퍼티는 덮어 쓰지 않습니다.
// 심벌 프로퍼티를 복사하지 않는 것은 똑같다.
function merge(target, ...sources) {
  for (let source of sources) {
    for (let key of Object.keys(source)) {
      if (!(key in target)) {
        // Object.assign과 다른 점.
        target[key] = source[key]
      }
    }
  }
}
Object.assign({ x: 1 }, { x: 2, y: 2 }, { y: 3, z: 4 }) // => {x:2, y:3, z: 4}
merge({ x: 1 }, { x: 2, y: 2 }, { y: 3, z: 4 }) // => {x:1, y:2, z:4}

객체 직렬화

객체 직렬화(serialize)는 객체를 문자열로 변환하는 작업이다. 이 문자열은 나중에 다시 객체로 되돌릴 수 있다.
JSON.stringify()와 JSON.parse()는 자바스크립트 객체를 직렬화하고 되돌리는 함수이다.
이 함수는 데이터 교환 형식인 JSON(JavaScript Obejct Notation)을 사용한다.

JSON 문법은 자바스크립트 문법의 부분 집합이며 자바스크립트 값 전체를 포함하지는 못한다.
객체, 배열, 문자열, 유한한 숫자, true, false, null은 모두 지원되고 직렬화와 복원이 가능하다.
Nan, Infinity, -Infinity는 null로 직렬화된다.
Date 객체는 ISO형식 날짜 문자열로 직렬화되지만 JSON.parse는 이 문자열을 그대로 문자열로 둘뿐 Date 객체로 복원하지는 않는다.
함수, RegExp, Error 객체, undefined 값은 직렬화하거나 복원할 수 없다.
JSON.stringify()는 열거 가능한 자체 프로퍼티만 직렬화한다.
프로퍼티 값을 직렬화할 수 없다면 해당 프로퍼티는 결과 문자열에서 생략.

객체 메서드

toString()

이 메서드를 호출한 객체의 값을 나타내는 문자열을 반환.

 

let s = { x: 1, y: 1 }.toString() // s == "[object Object]"

 

기본 메서드에서 유용한 정보를 제공하지 않으므로 여러 클래스에서 자신만의 toString()을 정의하곤 한다.
예를 들어 배열을 문자열로 변환하면 배열 요소 각각을 문자열로 변환한 리스트를 얻고, 함수를 문자열로 변환하면 소스코드를 얻는다.

 

let point = {
  x: 1,
  y: 2,
  toString: function () {
    return `(${this.x}, ${this.y})`
  },
}
String(point) // "(1, 2)"

프로퍼티 이름인 심벌

심벌을 변수나 상수에 할당하면 계산된 프로퍼티 문법을 통해 심벌을 그 프로퍼티 이름으로 쓸 수 있다.

 

const extensin = Symbol("my extension symbol")
let o = {
  [extension]: {},
}
o[extension].x = 0 // o 의 다른 프로퍼티와 충돌하지 않는다.

심벌은 프로퍼티 이름이외에 다른 용도로 쓸 수 없다.
심벌은 다른 심벌과도 같지 않고 고유한 프로퍼티 이름을 만들 때 사용.
심벌은 객체가 아니라 기본 값이므로, Symbol()은 new와 함께 호출되는 생성자 함수가 아니다.
Symbol이 반환하는 값은 다른 어떤 심벌과도 다른 어떤 값과도 같지 않다.
Symbol()에 문자열을 전달할 수 있는데, 이 문자열은 심벌을 문자열로 반환할 때 사용.
같은 문자열을 인자로 삼아 심벌을 생성해도 그들은 서로 다른 심벌.
심벌의 목적은 보안이 아니라 자바스크립트 객체가 사용할 수 있는 안전한 확장 메커니즘을 정의하는 것

여러분이 제어할 수 없는 서드 파티 코드에서 객체에 프로퍼티를 추가하고 싶지만, 추가한 프로퍼티가 이미 존재하는 프로퍼티와 충돌하지 않는다고 확신할 수 없을 때 프로퍼티 이름에 심벌을 사용하면 안전.

분해 연산자

분해 연산자는 자체 프로퍼티만 분해할 분 상속된 프로퍼티에는 적용되지 않는다.

 

let o = Object.create({ x: 1 })
let p = { ...o }
p.x // undefined

 

기본 값이 아닌 자바스크립트 값은 모두 객체이다.

'JavaScript' 카테고리의 다른 글

변수의 유효범위와 클로저  (0) 2023.02.27
자바스크립트 완벽 가이드 | 비동기 자바스크립트  (0) 2022.10.02
closure  (0) 2022.06.02
prototype의 명확한 이해  (0) 2022.05.31
함수와 일급 객체  (0) 2022.05.25