개발/Javascript

[You don't know JS] Part3 - 3장. 객체

lanace 2020. 9. 6. 06:41

3.1 구문



객체는 두가지 형태로 정의할 수 있다.

  • 리터럴형
  • 선언형
var obj = {
    key: 1
}

한번에 여러 필드 정의 가능

var obj = new Object();

obj.key = 1;

한번에 하나의 필드를 정의할 수 있음

3.2 타입


원시타입 7가지

  • null
  • undefined
  • boolean
  • number
  • string
  • object
  • symbol

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

null이 객체라고 아는 사람도 있는데, 이는 typeof의 버그로 인해 object가 나오는것일뿐 실제론 객체가 아니다.
javascript의 모든것은 객체이다는 말은 잘못된 말이다.

3.2.1 내장 객체

내장객체란 객체의 하위 타입이다.

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

내장 객체는 내장 함수일 뿐이며, new를 사용해 새로운 객체를 만들어낸다.

var strPrimitive = 'string';
typeof strPrimitive; // string
strPrimitive instanceof String; // false

var strObject = new String('string');
typeof strObject; // object
strObject instanceof String; // true

Object.prototype.toString.call(strObject);  // [object String]

우리가 이러한 차이를 잘 못느끼는 이유는 javascript가 알아서 Boxing을 해주기 때문이다.

원시값에 대한 프로퍼티 / 메서드를 호출하면 String 객체로 변환해준다.

객체 래퍼 형색이 없는 null과 undefined는 그 자체로 유일 값이다.

Date는 형식이 없어서 반드시 생성자를 통해 생성해야 한다.

3.3 내용


엔진이 값을 저장하는 방식은 구현에 의존적이다.

일반적으론 객체 컨테이너에 담지 않는것이 일반적이다.

객체 컨에이너란 실제로 프로퍼티 값이 있는곳을 가리키는 포인터 역할을 담당하는 프로퍼티명이 담겨있다.

var myObject = {
    a: 2
};

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

객체의 값에 접근하기 위해선 . 연산자 또는 [] 연산자를 사용한다.

  • 프로퍼티 접근
    • 식별자 호환 프로퍼피 명만 올 수 있음
  • 키 접근
    • 유니코드면 다 가능

객체의 프로퍼티명은 언제나 문자열이다.

var myObject = {};
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";

myObject["true"];  // "foo"
myObject["3"];  // "bar"
myObject["[object Object]"];  // "baz"

3.3.1 계산된 프로퍼티명 (computed property Names)

var prefix = "foo";

var myObject = {
    [prefix + "bar1"]: 'hello',
    [prefix + "bar2"]: 'world'
};

myObject[prefix + "bar1"];  // hello
myObject[prefix + "bar2"];  // world

계산된 프로퍼티에선 심볼이 가장 많이 사용될듯 하다.

var myObject = {
    [Symbol.Something]: 2
};

3.3.2 프로퍼티 vs 메소드

객체 프로퍼티 값이 함수이면 메서드라 한다.

엄밀하게는 객체 프로퍼티에 함수는 객체에 소유된게 아니다.

function foo() {
    console.log("foo");
}

var someFoo = foo;

var myObject = {
    someFoo: foo
}

foo;

someFoo;

myObject.someFoo;

someFoo나 myObject.someFoo모두 같은 함수를 가리키는 개별 레퍼런스일뿐 뭔가 객체가 소유하고 있다는 의미가 아니다.

따라서 메서드라 부르는건 옳치 않다.

3.3.3 배열

배열도 [] 로 접근하는 형태

저장하는 방법과 장소가 더 체계적이다.

배열은 숫자 인덱싱으로 특정 위치에 값이 저장된다.

인덱스는 양수지만 배열 자체는 객체여서 배열에 프로퍼티를 추가하는 것도 가능하다.

var myArray = ['', '', ''];
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: anotherFunction,
    c: anotherObject,
    d: anotherArray
}

anotherArray.push(anotherObject, myObject);

myObject는 어떻게 표현될까?

Case1 깊은복사

환영참조가 되어버림

에초에 함수를 복사한다는건 애매함...

정답은 없고, 각기 고유한 해석으로 구현.

JSON 의 경우엔 안전한 객체만 뽑아낸다.

var newObj = JSON.parse(JSON.stringify(object));

Case2 얕은 복사

별다른 이슈 없을듯

Object.assign 메서드를 제공

첫번쨰 인자는 타깃 객체, 둘쨰인자 이후부턴 소스 객체

소스객체는 열거가능하거나 키를 순회하면서 타깃 객체로 복사한다.

var newObj = Object.assign({}, myObject);

newObj.a; // 2
newObj.b === anotherObject;  // true
newObj.c === anotherArray;  // true
newObj.d === anotherArray;  // true

Object.assign은 순수하게 = 할당에 의해서 복사하므로 writable같은 특수 프로퍼티의 속성은 복사되지 않는다.

3.3.5 프로퍼티 서술자

읽기 전용 프로퍼티의 특성이 있다.

var myObject = {
    a: 2
}

Object.getOwnPropertyDesciptor(myObject, "a");

/*
=> 결과
{
    value: 2,
    writable: true,
    enumerable: true,
    configurable: true
}

/*

writable와 enumerable, configurable 라는 속성이 추가되어있다.

이렇게 프로퍼티 생성시 프로퍼티 서술자에 담긴 기본 특성값을 확인할 수 있다.

Object.defineProperty로 새로운 프로퍼티를 생성하거나 기존 프로퍼티의 특성을 수정할 수 있다.

(configurable이 true일때만 가능...)

var myObject = {};
Object.defineProperty(myObject, "a", {
    value: 2,
    writable: true,
    configurable: true.
    enumerable: true
});

myObject.a;  // 2

writable

프로퍼티 값의 쓰기 가능 여부는 writable를 사용한다.

var myObject = {};
Object.defineProperty(myObject, "a", {
    value: 2,
    writable: false,
    configurable: true,
    enumerable: true
});

myObject.a = 3;
myObject.a;  // 2

엄격모드에선 에러가남

configurable

var myObject = {};
Object.defineProperty(myObject, "a", {
    value: 2,
    writable: true,
    configurable: false,
    enumerable: true
});

myObject.a = 3;
myObject.a;  // 3

Object.defineProperty(myObject, "a", {
    value: 6,
    writable: true,
    configurable: true,
    enumerable: true
});  // Type Error

다시 재설정 하는게 불가능함

delete 연산자로 지우는것도 불가능하다.

enumerable

for...in 처럼 열거하는 구문에서 해당 프로퍼티의 표출 여부를 결정한다.

지정된 프로퍼티에 접근할 순 있지만 루프 구문에서 감춰진다.

기본값은 true이고 다음에 자세히 알아보자.

3.3.6 불변성

프로퍼티/객체가 우연이든 의도적이든 변경되지 않게 유지해하 알 경우가 있다.

앝은 불변셩을 지원하는데, 객체 자신과 직속 프로퍼티 특성만 불변으로 만들뿐 다른 객체를 가리키는 레퍼런스가 있을때 해당 객체의 내용까지 불변으로 만들진 못한다.

myImmutableObject.foo;  // [1, 2, 3]
myImmutableObject.foo.push(4);
myImmutableObject.foo;  // [1, 2, 3, 4]

불변 객체로 생성되어 보호된다.

내용까지 보호하려면 뒤에서 나열할 방법으로 foo를 불변 객체로 나타내야 한다.

방법1. 객체 상수

writable: false와 configurable: false를 같이 사용하여 객체 프로퍼티를 다음과 같이 상수처럼 사용할 수 있다.

var myObject = {};
Object.defineProperty(myObject, "FAVORATE_NUMBER", {
    value: 42,
    writable: false,
    configurable: false
});

방법2. 확장 금지

객체에 더는 프로퍼티를 추가할 수 없게 차단하고, 현재 프로퍼티는 있는 그대로 놔두고 싶을때 Object.preventExtenstions를 호출한다.

var myObject = {
    a: 2
}

Object.preventExtenstion(myObject);

myObject.b = 3;  // 엄격모드에선 에러 발생
myObject.b;  // undefined

방법3. 봉인

Object.seal은 봉인된 객체를 생성한다.

어떤 객체에 대해서 Object.preventExtenstions를 실행하고, 프로퍼티 전부 configurable: false를 처리한다.

결과적으로 더이상 프로퍼티를 추가할 수 없을뿐더러 기존 프로퍼티를 재설정하거나 삭제할 수 없다.

물론 값은 얼마든지 변경 가능....

????? 값 변경이 가능하다는게 뭐지??

방법4. 동결

Object.freeze 는 객체를 얼린다.

seal을 적용하고 데이터 접근자 프로퍼티 모두 writeable: false를 처리하여 값을 바꾸지도 못하게 한다.

가장 높은 단계의 불변성을 제공

객체를 참조하는 모든 객체를 재귀 순회하면서 freeze를 적용시켜 문제가 될 수 있으니 조심

3.3.7 [[Get]]

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

var myObject = {
    a: 2
}

myObject.a; // 2

myObject.a 는 프로퍼티 접근이지만 a란 이름을 myObject에서 찾지 않고 myObject에 대해 [[Get]] 연산을 한다.

기본적으로 [[Get]] 연산은 주어진 이름의 프로퍼티를 먼저 찾아보고 있으면 그 값을 반환한다.

없으면 [[Get]] 알고리즘은 프로토타입을 통해 찾는다.

주어진 프로퍼티 값을 어떻게도 찾아낼 수 없다면 undefined를 반환한다.

식별자명으로 변수를 참조할 땐 동작방식이 다르다. 해당 렉시컬 스코프 내에 없는 변수를 참조하면 객체 프로퍼티처럼 undefined가 반환되지 않고 ReferenceError가 발생한다. ⇒ 이건 무슨 뜻이지?

3.3.8 [[Put]]

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

  1. 프로퍼티 접근 서술자인가? 맞으면 세터를 호출
  2. 프로퍼티가 writable: false인 데이터 서술자 인가? 맞으면 조용히 실패 또는 Type Error
  3. 이외에 프로퍼티에 해당 값을 세팅

만약 객체에 존재하지 않는 프로퍼티면 [[Put]] 알고리즘은 더 어렵다.

이는 5장에서 설명한다.

3.3.9 Getter / Setter

프로퍼티가 Getter 또는 Setter가 정의되어 있으면 접근 서술자 라고 한다.

접근 서술자에서는 프로퍼티의 값과 writable속성은 무시되며 대신 configurable과 enumerable과 프로퍼티의 Get Set속성이 중요하다.

var myObject = {
    get a() {
        return 2;
    }
}

Object.defineProperty(
    myObject,
    "b",
    {
        get: function() {
            return this.a * 2;
        },
        enumerable: true
    }
)

myObject.a; // 2;
myObject.b; // 4

a의 Getter가 정의되어 있으니 할당문으로 값을 세팅하려고 해도 무시된다.

따라서 getter와 setter를 동시에 정의해주는것이 좋다.

var myObject = {
    get a() {
        return this._a_;
    }

    set a(val) {
        this._a_ = val * 2;
    }
}

myObject.a = 2;
myObject.a;  // 4

3.3.10 존재 확인

myObject.a처럼 프로퍼티에 접근했을때 실제로 a의 값이 undefined인지, 아니면 진짜 a가 없어서 undefined인지 구별할 수 있다.

var myObject = {
    a: 2
};

"a" in myObject;  // true
"b" in myObject;  // false

myObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("b"); // false

in 연산자는 해당 프로퍼티가 해당 객체에 있는지 아니면 [[prototype]] 체인을 따라 상위단계에 존재하는지 확인한다.

hasOwnProperty는 단지 프로퍼티가 객체에 있는지만 확인하고, 체인까지 검사하진 않는다.

Object.prototype과 연결되지 않으면 사용할 수 없다. (ex. Object.create(null));

열거

var myObject = {

}

Object.defineProperty(
    myObject,
    "a",
    {
        enumerable: true,
        value: 2
    }
);

Object.defineProperty(
    myObject,
    "b",
    {
        enumerable: false,
        value: 3
    }
);

myObject.b; // 3
"b" in myObject;  // true
myObject.hasOwnProperty("b");  // true

for(var k in myObject) {
    console.log(k, myObject[k]);  // a, 2
}

myObject.b는 존재하는 프로퍼티지만 for in 루프에서는 사라진다.

propertyIsEnumerable을 통해서도 확인할 수 있다.

myObject.propertyIsEnumerable("a"); // true
myObject.propertyIsEnumerable("b"); // false

Object.keys(myObject); // [a]
Object.getOwnPropertyNames(myObject);  // [a, b]

propertyIsEnumerable는 직접적으로 enumerable 속성에 접근하고

keys는 열거형 키값을 가져온다.

hasOwnProperty가 프로토타입까지 모두 뒤지지만

keys와 getOwnPropertyNames는 자기 자긴까지만 확인한다.

3.4 순회


for ... in 은 property를 포함하여 순회한다.

var arr = [1, 2, 3];

for (var i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}

위 코드는 배열을 순회한게 아니라 index를 순회하면서 출력한것이다.

forEach, every, some 등의 배열 관련 순회 헬퍼가 있다.

이 함수들은 콜백 함수를 인자로 받으며, 원소별로 반환값을 처리하는 로직만 다르다.

forEach는 배열 전체 값을 순회하지만 콜백 함수의 반환값은 무시한다.

every는 배열끝까지 또는 콜백 함수가 false 값을 반환할때까지 순회하며

some은 반대로 true값을 반환할때까지 순회한다.

for ... of 구문도 있다.

이는 Iterator object가 있어야한다.

순회당 한번씩 next 메서드를 호출하여 연속적으로 반환값을 순회한다.

배열은 @@iterator 를 이용하여 동작되며, 수동으로는 다음과 같이 할 수 있다.

var arr = [1, 2 ,3];
var it = arr[Symbol.iterator]();

it.next();  {value: 1, done: false}
it.next();  {value: 2, done: false}
it.next();  {value: 3, done: false}
it.next();  {done: true}

genarator가 들어가있어서 이렇게 된것임. 나중에 알아보자

일반 객체엔 @@iterator가 없는데, 수동으로 구현해줄 순 있다.

자세한 코드는 생략...

3.5 정리하기


객체는 두가지 형태를 가진다.

  • 리터럴
  • 생성자

대부분 리터럴을 추천, 옵션이 필요하다면 생성자

모든것은 객체다 라는 말은 틀린말이다.