객체?

박정훈·2022년 11월 16일
0

YOU DON'T KNOW JS

목록 보기
6/6

객체가 뭐죠

객체는 선언적(리터럴) 형식과 생성자 형식, 두 가지로 정의한다.

// 리터럴 형식
const obj = {
  key: value
}

// 생성자 형식
const obj = new Object()
obj.key = value

그래서 둘이 달라?

아니다. 결과적으로 생성되는 객체는 같다. 다만 리터럴 형식은 한 번의 선언으로 여러개의 키/값을 프로퍼티로 추가할 수 있고, 생성자 형식은 한 프로퍼티만 추가할 수 있다.

객체는 js를 구성하는 평범한 레고 블록과 같다.

Primitive 타입...

string, number, boolean, null, undefined 는 객체가 아니다.

Complex Primitive...

라는 독특한 객체 하위 타입이 있다. function은 객체의 하위 타입이다. JS함수는 기본적으로 1급이기 때문에 일반 객체와 똑같이 취급한다.

일급이라는 의미는 함수에 인자로 전달할 수 있고 다른 함수로부터 함수를 반환받을 수 있으며, 함수 자체를 변수에 할당하거나 자료구저에 저장할 수 있다.

배열 역시...

추가 기능이 구현된 객체의 일종이다. 좀 더 조직적으로 데이터가 구성되는 특징이 있다.

내장 객체?

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

마치 클래스처럼 느껴지겠지만 아니다! 단지 자바스크립트의 내장 함수 일 뿐 각각 생성자로 사용되어 하위 타입의 새 객체를 생성한다.

const malza = "malza" // 리터럴이며 불변값이다.
typeof malza // 'string'
malza instanceof String // false
const malza = new String("malza")
typeof malza // 'object'
malza instanceof String // true

JS엔진은 상황에 맞게 문자열 원시 값을 String 객체로 자동 강제변환 하므로 명시적으로 객체를 생성할 일은 거의 없다.

null과 undefined, Date

null과 undefined는 그 자체로 유일한 값!
Date는 리터럴 형식이 없기에 반드시 생성자 형식으로 생성해야 한다.

Error 객체는 예외가 던져지면 알아서 생성된다. 생성자 형식으로 생성이 가능하지만 굳이..?

객체의 내용, 프로퍼티

객체는 프로퍼티로 채워진다. 객체 내부에 프로퍼티 값이 들어가는게 아니다. 프로퍼티 값이 있는 곳을 가리키는 포인터(레퍼런스) 역할을 하는 프로퍼티명이 담겨 있다.

. 과 [ ]

. 으로 접근하는 것을 프로퍼티 접근, [ ]구문을 키 접근이라고 한다.

const obj = {
  name: "malza"
}
obj.name
obj["name"]

프로퍼티명은 언제나 문자열이다!

배열도 객체라고 했었지 아마?

배열도 [ ]로 접근한다. 앞에서도 언급했지만 값을 저장하는 방법과 장소가 좀 더 체계적이다. 배열은 숫자 인덱싱이 가능하다. 그렇지만 배열은 계속 얘기하지만 객체다. 그렇기에 놀랍게도 프로퍼티를 추가하는게 가능하다. 그리고 이런식으로 추가해도 배열 길이에는 변함이 없다.

const arr = ["malza", 10, ["hehe"]]
arr.what = "이게되네"
console.log(arr.length) // 3
console.log(arr.what) // "이게되네"

그렇다고 이런 방식을 사용하는건 지양하자. 정해진 용도대로 사용하는게 좋다. 키/값은 객체, 숫자 인덱싱은 배열로 사용하자.

객체를 복사해보자

JSON-Safe(JSON 문자열 <-> 객체 직렬화 및 역직렬화) 객체는 쉽게 복사할 수 있으므로 하나의 대안이 된다.
💥100% JSON-Safe 객체여야만 한다!

const obj = { 
    name: "malza",
    func() {
        console.log(this.name)
    } 
}
const copiedObj = JSON.parse(JSON.stringify(obj))
console.log(copiedObj) // { name: "malza" [[Prototype]]: object}

함수는 사라진 것을 확인 할 수 있다. 왜?

JSON에 대해서 좀 더 알아볼까

MDN JSON
JSON이 뭐에용? 🤔
JavaScript Object Notation은 JS 객체 문법으로 구조화된 데이터를 표현하기 위한 문자 기반의 표준 포맷이다. 항상 느끼지만 역시 뭔가 한줄 설명은 딱딱해
웹 어플리케이션에서 데이터를 전송할 때 일반적으로 사용! (서버 to 클라, 혹은 그 반대)
JS의 객체문법과 매우 유사하지만 JS가 아니더라도 JSON을 읽고 쓸 수 있다! (호환성이겠지?)

  • JSON은 오직 프로퍼티만 담을 수 있는 데이터 포맷입니다. 메서드는 ❌

  • 오직 큰 따옴표만을 사용해서 작성합니다.

  • 콤마나 콜론을 잘못 배치하는 사소한 실수로도 JSON파일이 작동하지 않을 수 있어요! JSON 유효성 검사

  • 배열, 오브젝트 외에도 단일 문자열, 숫자또한 유효한 JSON 오브젝트가 됩니다.

JSON.stringify()

  • JS 값이나 객체를 JSON 문자열로 반환한다. replacer를 함수로 전달할 경우 반환 전 값을 변형할 수 있고, 배열로 전달한다면 지정한 속성만 결과에 포함한다. space는 가독성을 목적으로 JSON 문자열 출력에 공백을 삽입한다. replacer, space 둘 다 길이 10이 최대다.

    JSON.stringify(value[, replacer[, space]])

const replacer = (key, value) => {
    if(typeof value === "number") {
        return undefined
    }
    return value
}
const obj = { 
    name: "malza",
    no: 20
}
console.log(JSON.stringify(obj, replacer, " "))
//{
// "name": "malza"
//}
const obj = { 
    name: "malza",
    func() {
        console.log(this.name)
    } 
}
console.log(JSON.stringify(obj, null, 10))
//{
//          "name": "malza"
//}
const obj = { 
    name: "malza",
    func() {
        console.log(this.name)
    } 
}
console.log(JSON.stringify(obj, null, "jumpjumpjump"))
//{
//jumpjumpju"name": "malza"
//}

JSON.parse()
JSON 문자열의 구문을 분석해서 JS값이나 객체를 생성한다. 선택적으로 reviver 함수를 인수로 전달한다면, 결과를 반환하기 전에 변형 가능하다.

JSON.parse(text[, reviver])
const reviver = (key, value) => {
    if(typeof value === "number") {
        return value * 100
    }
    return value
}
const obj = { 
    name: "malza",
    no: 20
}
console.log(JSON.parse(JSON.stringify(obj), reviver))
// {name: 'malza', no: 2000} 2000이 되서 뽑힌것이 확인된다!

얕은 복사로 ES6부터는 Object.assign() 메서드를 제공한다. enumerable과 Owned keys를 순회하면서 타깃 객체로 복사해준다.

const obj = { 
    name: "malza",
    func() {
        console.log(this.name)
    },
    arr: [1, 2, 3]
}
const copiedObj = Object.assign({}, obj)
obj.arr.push(4)
copiedObj.arr.push(5)
console.log("obj", obj) // {name: 'malza', arr: Array(5), func: ƒ}
console.log("copiedObj", copiedObj) // {name: 'malza', arr: Array(5), func: ƒ}

배열의 경우 참조값을 복사한 것을 확인할 수 있다. 참조하는 애가 같기 때문에 한곳에서 넣어도 같이 바뀐다!

프로퍼티 서술자?

이름이 어렵다. 프로퍼티의 특성을 표현하는거 같다.

getOwnPropertyDescriptor

const obj = { 
    name: "malza",
    func() {
        console.log(this.name)
    },
    arr: [1, 2, 3]
}

Object.getOwnPropertyDescriptor(obj, "name")
// {value: 'malza', writable: true, enumerable: true, configurable: true}

뭐가 쥬르륵~ 나온다. 이 특성을 임의로 내가 줄 수 있는데 다음과 같다.

defineProperty

const obj = {}
Object.defineProperty(obj, "name", {
  value: "malza",
  writable: true,
  configurable: true,
  enumerable: true
})

obj.name // 'malza'

writable
이름에서 알 수 있듯이 쓰기 기능이다.

const obj = {}
Object.defineProperty(obj, "name", {
  value: "malza",
  writable: false, // 쓰기 금지야!
  configurable: true,
  enumerable: true
})

obj.name = "change malza"
obj.name // 그대로 malza다.

use strict 모드에선 TypeError가 난다.

configurable
이 친구가 true면 프로퍼티 서술자를 변경할 수 있다.

"use strict"
const obj = {
  name: "말자"
}

obj.name // "말자"

Object.defineProperty(obj, "name", {
  value: "malza",
  writable: true,
  configurable: false,
  enumerable: true
})

obj.name = "change malza"
obj.name // "change malza"

configurable를 다시 true로 돌려보자

Object.defineProperty(obj, "name", {
  value: "malza",
  writable: true,
  configurable: true,
  enumerable: true
})
// Uncaught TypeError: Cannot redefine property: name

설정 불가능한 프로퍼티의 서술자를 변경하려고 하니 error가 딱! 떠버렸다.

configurable는 한번 false가 되면 돌아올 수 없는 강을 건넌 것이다. 유의해서 사용하자. 심지어 이 친구는 delete 연산자도 금지한다.

enumerable
이름에서 알 수 있듯 for...in 루프처럼 객체 프로퍼티를 열거하는 구문에서 해당 프로퍼티를 보여줄지 여부를 결정한다. false라면 지정된 프로퍼티로 접근은 가능하지만 루프 구문에서 감춰진다.

"use strict"
const obj = {
  no: 10
}

Object.defineProperty(obj, "name", {
  value: "malza",
  writable: true,
  configurable: true,
  enumerable: true
})

Object.defineProperty(obj, "print", {
  value: function () {
    console.log("hey!") 
  },
  writable: true,
  configurable: true,
  enumerable: true
})

for(const key in obj) {
  console.log(key)
}
// no
// name
// print

=========================================================================
  
// 감추면?
"use strict"
const obj = {
  no: 10
}

Object.defineProperty(obj, "name", {
  value: "malza",
  writable: true,
  configurable: true,
  enumerable: false // false로 바꿈
})

Object.defineProperty(obj, "print", {
  value: function () {
    console.log("hey!") 
  },
  writable: true,
  configurable: true,
  enumerable: false // false로 바꿈
})

for(const key in obj) {
  console.log(key)
}
// no

불변성

프로퍼티/객체를 변경되지 않게 해야 할 경우가 있다. ES5부터 다양한 방법을 제공하며, 이는 얕은 불변성만 지원한다. 즉, 객체 자신과 직속 프로퍼티 특성만 불변으로 만들 뿐 다른 객체를 가리키는 레퍼런스는 해당 객체의 내용까지 불변으로 만들지는 못한다.

객체 상수

앞서 살펴봤던 writable: false, configurable:false를 같이 쓰면 객체 프로퍼티를 상수처럼 사용 가능하다.

확장 금지

객체에 더는 프로퍼티를 추가할 수 없게 차단하고 현재 프로퍼티는 그대로 두고 싶다면?

const obj = {
  no: 10
}

Object.preventExtensions(obj)
obj.name = "말자"
obj.name // undefined. strict모드라면 TypeError

seal

Object.preventExtensions + 모든 프로퍼티의 configurable:false 처리한다. 더는 프로퍼티를 추가할 수 없고, 기존 프로펀티를 재설정하거나 삭제할 수도 없다.

freeze

seal을 적용하고, writable:false까지 해버린다. 가장 높은 단계의 불변성이다.

[[Get]]

객체에서 obj.a 는 프로퍼티 접근으로 보이지만, 실제로는 [[Get]]연산을 내부적으로 하고 있다. 이는 주어진 이름의 프로퍼티를 찾고 있다면 그 값을 반환, 없다면 undefined를 반환한다. 더 자세한건 5장 프로토타입에서 다룬다고 한당.

const obj = {
  get a() {
    return 10
  }
}

obj.a // 10

[[Put]]

주어진 객체에 프로퍼티가 존재하는지 등에 따라 동작이 달라진다.
1. 프로퍼티가 접근 서술자(게터와 세터)인가? 맞다면 세터를 호출!
2. 프로퍼티가 writable:false 인가? 그렇다면 strict모드에선 Type Error!
3. 이외에는 프로퍼티에 해당 값을 세팅한다.
더 자세한건 5장 프로토타입에서 다룬다고 한당.

게터와 세터

게터 세터는 실제로 값을 가져오는/세팅하는 감춰진 함수를 호출하는 프로퍼티다.
프로퍼티가 게터 또는 세터 어느 한쪽이거나 동시에 게터/세터가 될 수 있게 정의한 접근 서술자라고 한다.
프로퍼티의 값과 writable속성은 무시되며 대신 프로퍼트의 get/set 속성이 중요하다.

const obj = {
  get a() {
    return 10
  },
}

obj.a = 20
obj.a // 10. 20을 넣은게 무시된다. 그렇다고 세터가 있어도 상관없다. return 10으로 되어있으니 반드시 10이 return 된다.

게터와 세터는 항상 둘 다 선언하자.

"use strict"
const obj = {
  get a() {
    return this.num
  },
  set a(val) {
    this.num = val
  }
}

obj.a = 20
obj.a // 20

존재를 확인해보자

const obj = {
  name: "malza"
}

console.log("some" in obj) // false
console.log("name" in obj) // true

obj.hasOwnProperty("some") // false
obj.hasOwnProperty("name") // true

in 연산자는 어떤 프로퍼티가 해당 객체에 존재하는지 아니면 객체의 [[Prototype]] 연쇄를 따라갔을 때 상위 단계에 존재하는지까지 체크한다.
반면, hasOwnProperty()는 단지 프로퍼티가 객체에 있는지만 확인한다.

in 연산자는 프토퍼티명이 있는지 확인하는 것이다. 배열에서 4 in [2, 4, 6] 해도 예상대로 동작하지 않을 것이다.

앞서도 말했듯 for...in 루프에서는 enumerable: false 라면 출력되지 않는다. 이는 enumerable 하다는 것은 순회 리스트에 포함된다는 의미이다.

또한 enumerable 여부를 확인할 수 있는 다른 방법도 있다.

const obj = {
  no: 10
}

Object.defineProperty(obj, "name", {
  value: "malza",
  writable: true,
  configurable: true,
  enumerable: false // false로 바꿈
})

Object.defineProperty(obj, "print", {
  value: function () {
    console.log("hey!") 
  },
  writable: true,
  configurable: true,
  enumerable: false // false로 바꿈
})

obj.propertyIsEnumerable("name") // false
obj.propertyIsEnumerable("print") // false
obj.propertyIsEnumerable("no") // true
Object.keys(obj) // ['no']
Object.getOwnPropertyNames(obj) // (3) ['no', 'name', 'print']

keys는 열거 가능한 프로퍼티를 배열 형태로 반환한다.
getOwnPropertyNames는 열겨형 및 열거할 수 없는 속성들을 배열로 반환한다.
다만 둘 다 [[Prototype]] 연쇄 확인은 하지 않는다.

순회

배열 인덱스가 아닌 값을 직접 순회하기 위해 ES6부터 for...of를 제공한다.

const arr = [1, 2, 3]
for(value of arr){
  console.log(value)
}
//1
//2
//3

이 방식은 순회할 원소의 iterator Object가 있어야 한다. 순회당 한 번씩 해당 iterator Object의 next()메소드를 호출하여 연속적으로 반환 값을 순회한다!
배열은 @@iterator가 내장되어 있기 때문에 for...of 루프 사용이 가능하다.

const arr = [1, 2, 3]

const it = arr[Symbol.iterator]();

console.log(it.next()) // {value: 1, done: false}
console.log(it.next()) // {value: 2, done: false}
console.log(it.next()) // {value: 3, done: false}
console.log(it.next()) // {value: undefined, done: true}

일반 객체에는 @@iterator가 없다!

profile
그냥 개인적으로 공부한 글들에 불과

0개의 댓글