Ch3. 객체
3.1 구문
var myObj = {
key: value,
}
var myObj = new Object()
myObj.key = value
- 객체는 선언적 형식(리터럴 형식)과 생성자 형식, 두 가지로 정의한다.
- 거의 리터럴 형식을 사용하여 객체를 생성한다.
3.2 타입
- 자바스크립트 주요타입은 7개가 있다. (null, undefined, boolean, number, string, object, symbol)
- null의 타입은 객체가 아니고, 이렇게 와전된것은 언어 자체의 버그 때문이다.
- 복합 원시 타입은 객체의 하위 타입이다. function, array가 그렇다. function은 호출 가능한 객체이고, array 역시 추가 기능이 구현된 객체의 일종이다.
내장 객체
- 내장 객체도 객체의 하위 타입이다. 이것들은 단지 자바스크립트의 내장 함수이고 각각 생성자로 사용되어 주어진 하위타입의 새 객체를 생성한다.
var strPrimitive = "나는 문자열"
typeof strPrimitive // 'string'
strPrimitive instanceof String // false
var strObject = new String("나는 문자열")
typeof strObject // 'object'
strObject instanceof String // true
Object.prototype.toString.call(strObject) // [object String]
- 자바스크립트 엔진은 상황에 맞게 문자열 원시 값을 String 객체로 자동 강제변환하므로 명시적으로 객체를 생성할 일은 거의 없다 (1권에서 자세히 다룸)
- Object, Arrays, Functions, RegExps는 형식과 무관하게 모두 객체다.
3.3 내용
- 객체는 특정한 위치에 저장된 모든 타입의 값, 프로퍼티로 내용이 채워진다.
- 자바스크립트 엔진이 값을 저장하는 방식은 구현 의존적인데, 이는 객체 컨테이너에 담지 않는 게 일반적이다. 객체 컨테이너에는 실제로 프로퍼티 값이 있는 곳을 가리키는 포인터 역할을 담당하는 프로퍼티명이 담겨 있다.
var myObject = {
a: 2,
}
myObject.a // 2
myObject["a"] // 2
- 객체에서 a에 접근하려면 '.' 연산자 또는 '[]' 연산자를 사용한다. .a 접근을 프로퍼티 접근, [ " a " ] 접근을 키 접근이라고 한다.
- . 연산자는 뒤에 식별자 호환 프로퍼티명이 와야 하지만 [ " " ] 구문은 utf-8/유니코드 호환 문자열이라면 모두 프로퍼티명으로 쓸 수 있다는 점에서 차이가 있다.
- 객체 프로퍼티명은 언제나 문자열이다. 문자열 이외의 다른 원시 값을 쓰면 우선 문자열로 변환된다.
var myObject = {}
myObject[true] = "foo"
myObject[3] = "bar"
myObject[myObject] = "baz"
myObject["true"] // 'foo'
myObject["3"] // 'bar'
myObject["[obejct Object]"] // 'baz'
3.3.1 계산된 프로퍼티명
var prefix = "foo"
var myObject = {
[prefix + "bar"]: "hello",
[prefix + "baz"]: "world",
}
myObject["foobar"] // hello
myObject["foobaz"] // world
- 객체 리터럴 선언 구문의 키 이름 부분에 해당 표현식을 넣고 []로 감싸 선언할 수도 있다.
3.3.2 프로퍼티 vs 메서드
- 엄밀히 말해 함수는 결코 객체에 속하는 것이 아니며, 객체 레퍼런스로 접근한 함수를 그냥 메서드라 칭하는 것은 그 의미를 지나치게 확대해서 해석한 것이다.
- 객체에 존재하는 프로퍼티에 접근할 때마다 반환 값 타입에 상관없이 항상 프로퍼티 접근을 하고 이런 식으로 함수를 가져왔다고 해서 저절로 함수가 메서드가 되는 건 아니다.
function foo() {
console.log("foo")
}
var someFoo = foo
var myObject = {
someFoo: foo,
}
foo // function foo() {}
someFoo // function foo() {}
myObject.someFoo // function foo() {}
- 모두 같은 함수를 가리키는 개별 레퍼런스일 뿐, 뭔가 특별한 다른 객체가 소유한 함수라는 의미는 아니다.
- 결론은, 자바스크립트에서 함수와 메서드는 서로 바꿔 사용할 수 있다.
3.3.3 배열
- 배열도 [ ]로 접근하는 형태지만 값을 저장하는 방법과 장소가 더 체계적이다.
- 배열은 숫자 인덱싱, 양수로 표기된 위치에 값을 저장한다.
- 인덱싱은 양수지만 배열 자체는 객체여서 배열에 프로퍼티를 추가하는 것도 가능하다. 이름 붙은 프로퍼티를 추가해도 배열 길이에는 변함이 없다. 하지만 객체와 배열 나름대로 정해진 용도에 맞게 최적화되어 작동하므로 키/값 저장소로는 객체, 숫자 인덱스를 가진 저장소로는 배열을 쓰는게 좋다.
- 배열에 프로퍼티를 추가할 때 프로퍼티명이 숫자와 유사하면 숫자 인덱스로 잘못 해석되어 배열 내용이 달라질 수 있다.
var myArray = ["foo", 42, "bar"]
myArray.baz = "baz"
myArray.length // 3
myArray.baz // 'baz'
myArray["3"] = "baz"
myArray.length // 4
myArray[3] // 'baz
3.3.4 객체 복사
function anotherFunction() {}
var anotherObject = {
c: true,
}
var anotherArray = []
var myObject = {
a: 2,
b: anotherObject,
c: anotherArray,
d: anotherFunction,
}
anotherArray.push(anotherObject, myObject)
- 사본을 표현할 때 얕은 복사인지 깊은 복사인지를 선택해야 한다. 얕은 복사일 경우, a는 그대로 복사되고, 나머지 프로퍼티는 원 객체의 레퍼런스와 같은 대상을 나타내는 또 다른 레퍼런스이다.
- 깊은 복사를 하게 되면 myObject는 물론이고 anotherObject와 anotherArray까지 모조리 복사한다. 하지만 문제는 anotherArray가 anotherObject와 myObject의 레퍼런스를 갖고 있으므로 함께 복사되는데, 결국 환형 참조 형태가 되어 무한 복사를 하게 된다.
- 환형 참조가 감지되면 환형 순회의 루프를 벗어나야 할까, 아니면 에러를 던져야할까, 아니면 적절히 절충해야할까에 대한 문제는 오랫동안 뾰족한 답이 없었다.
- JSON 안전한 객체는 쉽게 복사할 수 있으므로 하나의 대안이 될 수 있지만, 정말 100% 안전한 JSON 객체여야 한다. (function 같은 경우 제대로 복사가 되지 않음)
- 얕은 복사는 이해하기 쉽고 별다른 이슈가 없기에 ES6부터는 Object.assign() 메서드를 제공한다.
var newObj = Object.assign({}, myObject)
newObj.a //2
newObj.b === anotherObject // true
newObj.c === anotherArray // true
newObj.d === anotherFunction // true
var myObject = {
a: 2,
}
Object.getOwnPropertyDescriptor(myObject, "a")
/*
{
value: 2,
writable: true,
enumerable: true,
configurable: true
}
*/
- Object.defineProperty()로 새로운 프로퍼티를 추가하거나 기존 프로퍼티의 특성을 원하는 대로 수정할 수 있다 (단 configurable이 true일때만 가능)
쓰기 가능
- 프로퍼티 값의 쓰기 가능 여부는 writable로 조정한다. writable이 false로 되어 있으면 해당 프로퍼티를 수정할 수 없다. 실제로 값을 수정하려고 하면 조용한 실패 처리가 되고, 엄격 모드에서는 에러가 난다.
설정 가능
- configurable이 true라면 defineProperty로 프로퍼티 서술자를 변경할 수 있다. false로 되어있다면 defineProperty 함수를 사용할 수 없다. (단 writable은 true에서 false로 변경이 가능하다. 하지만 다시 되돌리는 것은 불가능하다.)
- configurable이 false라면 delete 연산자로 프로퍼티 삭제도 금지된다.
열거 가능
- 프로퍼티에 접근은 가능하지만 for ~ in 처럼 객체 프로퍼티를 열거하는 구문에서 해당 프로퍼티의 표출 여부를 나타낸다. 감추고 싶은 특별한 프로퍼티에 한하여 enumerable을 false로 세팅하자.
var myObject = {}
Object.definedProperty(myObject, "a", { enumerable: true, value: 2 })
Object.definedProperty(myObject, "b", { enumerable: false, value: 3 })
myObject.propertyIsEnumerable("a") // true
myObject.propertyIsEnumerable("b") // false
Object.keys(myObject) // ["a"]
Object.getOwnPropertyNames(myObject) // ["a", "b"]
- propertyIsEnumerble()은 어떤 프로퍼티가 해당 객체의 직속 프로퍼티인 동시에 enumerable:true 인지 검사한다. Object.keys()는 모든 '열거 가능한' 프로퍼티를 배열 형태로 변환한다.
3.3.6 불변성
- 프로퍼티/ 객체가 변경되지 않게 해야 할 경우가 있다. ES5에서는 여러가지 방법들을 지원한다.
객체 상수
- writable: false, configurable: false를 같이 쓰면 객체 프로퍼티를 상수처럼 쓸 수 있다.
확장 금지
- 객체에 더는 프로퍼티를 추가할 수 없게 차단하고 현재 프로퍼티는 그대로 놔두고 싶을때 Object.preventExtensions()를 호출한다.
var myObject = {
a: 2,
}
Object.preventExtensions(myObject)
myObject.b = 3
myObject.b // undefined
봉인 / 동결
- Object.seal()은 봉인된 객체를 생성한다. 이것은 어떤 객체를 확장 금지를 하고, 프로퍼티를 전부 configurable: false로 처리하는 것이다. 이렇게 되면 프로퍼티 추가, 삭제가 안되고 이 설정을 변경할 수도 없다. 다만 프로퍼티 값은 얼마든지 바꿀 수 있다.
- Object.freeze()는 객체를 얼린다. seal()을 적용한 후에 writable:false로 값도 못바꾸게 한다. 하지만 이 객체가 참조하는 다른 객체의 내용까지 봉쇄하지는 않는다. 바로 아래쪽에 부딪히는 내용이 있는데 이거는 이해가 가지 않습니다.
var a = { a: "a" }
var b = { b: "b" }
a.b = b
Object.freeze(a)
a.a = "aa"
a // 'a' 변경안됨
a.b.b = "bb"
a.b.b // 'bb' 변경됨
- 자바스크립트에서 뼛속까지 고정된 불변 객체를 쓸 일은 거의 없다. 객체 값이 변경되어도 문제가 없는 견고한 프로그램을 설계할 다른 방법은 없는지 일반적인 디자인 패턴 관점에서 재고해야 한다.
3.3.7 [[Get]]
var myObject = {
a: 2,
}
myObject.a // 2
- 명세에 따르면 위 코드는 myObject에 대해 '[[Get]]' 연산을 한다. 기본적으로 이 연산은 주어진 이름의 프로퍼티를 먼저 찾아보고 있으면 그 값을 반환한다. 프로퍼티를 찾아보고 없으면 다른 중요한 작업을 하도록 정의되어 있는데, 이 부분은 5장에서 자세히 다룬다.
- 주어진 프로퍼티를 결국 못찾아내면 undefined를 반환한다.
- 하지만 명시적으로 값이 undefined인 프로퍼티인지 값이 없어서 undefined를 반환한건지 구별할 수가 없는데, 가능한 방법이 뒤에 나온다.
3.3.8 [[Put]]
- 이 메서드를 실행하면 주어진 객체에 프로퍼티가 존재하는지에 따라 작동 방식이 달라진다. 만약 존재한다면 아래 절차를 밟는다.
- 프로퍼티가 접근 서술자인가? 맞으면 세터를 호출한다.
- 프로퍼티가 writalble:false인 데이터 서술자인가? 맞으면 비엄격 모드에서는 조용히 실패하고, 엄격모드에서는 타입에러를 뱉는다.
- 이외에는 프로퍼티에 해당 값을 세팅한다.
- 객체에 존재하지 않는 프로퍼티라면 좀 더 복잡한데, 이에대한 내용도 5장에서 다룬다.
3.3.9 게터와 세터
- ES5부터는 게터/세터를 통해 프로퍼티 수준에서 이러한 기본 로직을 오버라이드 할 수 있다.
- 프로퍼티가 게터 또는 세터 어느 한쪽이거나 동시에 게터/세터가 될 수 있게 정의한것을 접근 서술자라고 한다. 접근 서술자에서는 프로퍼티의 값과 writable 속성은 무시되며 대신 프로퍼티의 겟/셋 속성이 중요하다.
var myObject = {
get a() {
return 2
},
}
Object.defineProperty(myObject, "b", {
get: function () {
return this.a * 2
},
enumerable: true,
})
- a의 게터가 정의되어 있으므로 할당문으로 값을 세팅하려고 하면 에러 없이 조용히 무시된다.
- 게터와 세터는 항상 둘 다 선언하는 것이 좋다. (한쪽만 선언하면 예상외의 결과가 나올 수 있다.)
var myObject = {
get a() {
return this._a_;
},
set a() {
this._a_ = val * 2;
}
}
myObject.a = 2;
myObject.a // 4
3.3.10 존재 확인
- 3.3.7에서 본 것처럼, 명시적으로 값이 undefined인 프로퍼티인지 값이 없어서 undefined를 반환한건지 구별할 수가 없는데, in 연산자와 hasOwnProperty 메서드를 통해서 확인할 수 있다.
var myObject = {
a: 2,
}
"a" in myObject // true
"b" in myObject // false
myObject.hasOwnProperty("a") // true
myObject.hasOwnProperty("b") // false
- in 연산자는 어떤 프로퍼티가 해당 객체에 존재하는지 아니면 이 객체의 [[Prototype]] 연쇄를 따라갔을 때 상위 존재에 있는지까지 확인한다. 반면 hasOwnProperty는 단지 프로퍼티에 객체가 있는지만 확인하고 프로토타입 연쇄는 찾지 않는다. 자세한 내용은 5장에서 이어진다.
- in 연산자는 프로퍼티명이 존재하는지만 본다. 그래서 4 in [2,4,6]처럼 써도 동작하지 않는다. 왜냐면 해당 배열의 프로퍼티는 0,1,2 까지만 존재하기 때문이다.
3.4 순회
- for ~ in 루프는 열거 가능한 객체 프로퍼티를 (프로토타입 연쇄 포함) 차례로 순회한다. 그런데 프로퍼티 값을 순회하려면 어떻게 할까?
var myArray = [1, 2, 3]
for (var i = 0; i < myArray.length; i++) {
console.log(myArray[i])
}
// 1 2 3
- 위 코드는 사실상 인덱스를 순회하면서 해당 값에 접근하는 것이고 값 자체를 순회하는 것은 아니다.
- ES5부터는 forEach, every, some 등의 배열 관련 순회 헬퍼가 도입됐다. 이 함수들은 배열의 각 원소에 적용할 콜백 함수를 인자로 받고, 원소별로 반환 값을 처리하는 로직만 다르다.
- ES6부터는 배열 순회용 for of 구문을 제공한다. for~of 루프는 순회할 원소의 순회자 객체가 있어야 한다. 순회당 한 번씩 이 순회자 객체의 next() 메서드를 호출하여 연속적으로 반환 값을 순회한다.
- 배열은 @@iterator가 내장된 덕분에 다음 예제에서 손쉽게 for~of 루프를 사용할 수 있다.
- @@iterator는 순회자 객체를 반환하는 함수다.
var myArray = [1, 2, 3]
var it = myArray[Symbol.iterator]()
it.next() // {value: 1, done: false};
it.next() // {value: 2, done: false};
it.next() // {value: 3, done: false};
it.next() // {value: undefined, done: true};
var myObject = {
a: 2,
b: 3,
}
myObject[Symbol.iterator] = function () {
var o = this
var idx = 0
var ks = Object.keys(o)
return {
next: function () {
return {
value: o[ks[idx++]],
done: idx > ks.length,
}
},
}
}
var it = myObject[Symbol.iterator]()
it.next() // {value:2, done:false};
it.next() // {value:3, done:false};
it.next() // {value:undefined, done:true};