[JavaScript] 3-4. JavaScript object in depth

JavaScript object in depth


객체 자신의 속성

속성 접근자(property accessor)를 통해 객체의 속성에 접근할 때, 객체 자신이 갖고 있는 속성(own property)이 반환될 수도 있고,
혹은 프로토타입으로부터 상속받은 속성(inherited property)이 반환될 수도 있습니다.
in 연산자 역시 마찬가지입니다. in 연산자와 속성 접근자를 가지고는 이 둘을 구분할 수 없습니다.

// 객체 `obj`는 프로토타입의 `inheritedProp` 속성을 상속받습니다.
const obj = Object.create({ inheritedProp: "inheritedProp" });
// 객체 `obj`에 직접 `ownProp` 속성을 만들어주었습니다.
obj.ownProp = "ownProp";

console.log(obj.inheritedProp); // inheritedProp
console.log(obj.ownProp); // ownProp
console.log(obj.constructor); // [Function: Object]

console.log("inheritedProp" in obj); // true
console.log("ownProp" in obj); // true
console.log("constructor" in obj); // true

객체 자신이 어떤 속성을 가지고 있는지를 확인하기 위해 Object.prototype.hasOwnProperty 메소드를 사용할 수 있습니다.

const obj = Object.create({ inheritedProp: "inheritedProp" });
obj.ownProp = "ownProp";

console.log(obj.hasOwnProperty("inheritedProp")); // false
console.log(obj.hasOwnProperty("ownProp")); // true
console.log(obj.hasOwnProperty("constructor")); // false



데이터 속성(data property)과 부수속성(property attribute)

앞서 delete 연산자를 통해 객체의 속성을 지울 수 있다는 것을 배웠습니다.

const obj = { prop: 1 };
delete obj.prop; // true
obj.prop; // undefined;

하지만, 객체의 속성을 항상 삭제할 수 있는 것은 아닙니다.
내장 객체 중에 어떤 속성은 delete 연산자를 통해 삭제하려고 해도 삭제가 되지 않는 것이 있습니다.

delete Math.PI; // false
Math.PI; // 3.141592653589793

이처럼, JavaScript 에서는 각 속성마다 동작 방식이 다를 수 있습니다.
이에 대한 정보는 속성의 부수속성(property attribute)이라고 불리는 곳에 숨겨져 있습니다.
객체의 부수속성을 알아보려면, Object.getOwnPropertyDescriptor 라는 정적 메소드를 사용해 부수속성을 나타내는 객체를 얻을 수 있습니다.
이 객체를 일러 속성 기술자(property descriptor)라고 부릅니다.

const obj = { prop: 1 };

Object.getOwnPropertyDescriptor(obj, "prop");
// { value: 1, writable: true, enumerable: true, configurable: true }

Object.getOwnPropertyDescriptor(Math, "PI");
// { value: 3.141592653589793, writable: false, enumerable: false, configurable: false }

이처럼 ‘데이터 속성(data property)’에 대한 속성 기술자는 네 가지 속성을 갖습니다.

위 코드 예제에서 얻은 속성 기술자를 살펴보면, obj.prop 의 configurable 부수속성은 true 이고
Math.PI 의 configurable 부수속성은 false 입니다.
이 때문에 Math.PI 를 삭제하려고 해도 삭제가 되지 않았던 것입니다.

두 속성의 writable 부수속성에도 차이가 있습니다.
Math.PI 는 속성의 값을 변경하려고 해도 변경이 되지 않습니다.

Math.PI = 10;
Math.PI; // 3.141592653589793

enumerable 부수속성이 false 이면 이 속성을 열거 불가능한 속성이 됩니다.
속성의 열거에 대해서는 잠시 뒤에 알아보겠습니다.
어떤 객체의 전체 속성에 대한 속성 기술자를 얻어오려면,
Object.getOwnPropertyDescriptors 정적 메소드를 사용하면 됩니다.

Object.getOwnPropertyDescriptors(Math);
// 스압주의
/* { abs: 
   { value: [Function: abs],
     writable: true,
     enumerable: false,
     configurable: true },
  acos: 
   { value: [Function: acos],
     writable: true,
     enumerable: false,
     configurable: true },
  acosh: 
   { value: [Function: acosh],
     writable: true,
     enumerable: false,
     configurable: true },
  asin: 
   { value: [Function: asin],
     writable: true,
     enumerable: false,
     configurable: true },
  asinh: 
   { value: [Function: asinh],
     writable: true,
     enumerable: false,
     configurable: true },
  atan: 
   { value: [Function: atan],
     writable: true,
     enumerable: false,
     configurable: true },
  atanh: 
   { value: [Function: atanh],
     writable: true,
     enumerable: false,
     configurable: true },
  atan2: 
   { value: [Function: atan2],
     writable: true,
     enumerable: false,
     configurable: true },
  ceil: 
   { value: [Function: ceil],
     writable: true,
     enumerable: false,
     configurable: true },
  cbrt: 
   { value: [Function: cbrt],
     writable: true,
     enumerable: false,
     configurable: true },
  expm1: 
   { value: [Function: expm1],
     writable: true,
     enumerable: false,
     configurable: true },
  clz32: 
   { value: [Function: clz32],
     writable: true,
     enumerable: false,
     configurable: true },
  cos: 
   { value: [Function: cos],
     writable: true,
     enumerable: false,
     configurable: true },
  cosh: 
   { value: [Function: cosh],
     writable: true,
     enumerable: false,
     configurable: true },
  exp: 
   { value: [Function: exp],
     writable: true,
     enumerable: false,
     configurable: true },
  floor: 
   { value: [Function: floor],
     writable: true,
     enumerable: false,
     configurable: true },
  fround: 
   { value: [Function: fround],
     writable: true,
     enumerable: false,
     configurable: true },
  hypot: 
   { value: [Function: hypot],
     writable: true,
     enumerable: false,
     configurable: true },
  imul: 
   { value: [Function: imul],
     writable: true,
     enumerable: false,
     configurable: true },
  log: 
   { value: [Function: log],
     writable: true,
     enumerable: false,
     configurable: true },
  log1p: 
   { value: [Function: log1p],
     writable: true,
     enumerable: false,
     configurable: true },
  log2: 
   { value: [Function: log2],
     writable: true,
     enumerable: false,
     configurable: true },
  log10: 
   { value: [Function: log10],
     writable: true,
     enumerable: false,
     configurable: true },
  max: 
   { value: [Function: max],
     writable: true,
     enumerable: false,
     configurable: true },
  min: 
   { value: [Function: min],
     writable: true,
     enumerable: false,
     configurable: true },
  pow: 
   { value: [Function: pow],
     writable: true,
     enumerable: false,
     configurable: true },
  random: 
   { value: [Function: random],
     writable: true,
     enumerable: false,
     configurable: true },
  round: 
   { value: [Function: round],
     writable: true,
     enumerable: false,
     configurable: true },
  sign: 
   { value: [Function: sign],
     writable: true,
     enumerable: false,
     configurable: true },
  sin: 
   { value: [Function: sin],
     writable: true,
     enumerable: false,
     configurable: true },
  sinh: 
   { value: [Function: sinh],
     writable: true,
     enumerable: false,
     configurable: true },
  sqrt: 
   { value: [Function: sqrt],
     writable: true,
     enumerable: false,
     configurable: true },
  tan: 
   { value: [Function: tan],
     writable: true,
     enumerable: false,
     configurable: true },
  tanh: 
   { value: [Function: tanh],
     writable: true,
     enumerable: false,
     configurable: true },
  trunc: 
   { value: [Function: trunc],
     writable: true,
     enumerable: false,
     configurable: true },
  E: 
   { value: 2.718281828459045,
     writable: false,
     enumerable: false,
     configurable: false },
  LN10: 
   { value: 2.302585092994046,
     writable: false,
     enumerable: false,
     configurable: false },
  LN2: 
   { value: 0.6931471805599453,
     writable: false,
     enumerable: false,
     configurable: false },
  LOG10E: 
   { value: 0.4342944819032518,
     writable: false,
     enumerable: false,
     configurable: false },
  LOG2E: 
   { value: 1.4426950408889634,
     writable: false,
     enumerable: false,
     configurable: false },
  PI: 
   { value: 3.141592653589793,
     writable: false,
     enumerable: false,
     configurable: false },
  SQRT1_2: 
   { value: 0.7071067811865476,
     writable: false,
     enumerable: false,
     configurable: false },
  SQRT2: 
   { value: 1.4142135623730951,
     writable: false,
     enumerable: false,
     configurable: false },
  [Symbol(Symbol.toStringTag)]: 
   { value: 'Math',
     writable: false,
     enumerable: false,
     configurable: true } }
*/

참고로, 엄격 모드가 아닐 때에는 writable: false, configurable: false 인 속성을
변경하거나 삭제하려고 해도 에러가 나지 않고 그냥 무시되지만, 엄격 모드일 때에는 에러가 발생합니다.

function func1() {
  delete Math.PI;
}

function func2() {
  "use strict";
  delete Math.PI;
}

func1(); // 에러가 나지 않습니다.
func2(); // TypeError: Cannot delete property 'PI' of #<Object>



속성 기술자를 통해 객체의 속성 정의하기

속성 기술자는 속성의 부수속성을 얻어올 때에만 사용하는 것이 아닙니다.
우리가 직접 속성 기술자를 이용해 속성을 정의할 수도 있습니다.
프로토타입 상속을 위해 사용했던 Object.create 정적 메소드는,
사실 두 번째 인수로 속성 기술자로 이루어진 객체를 받습니다.

const obj = Object.create(Object.prototype, {
  // Object.create에는 Object의 속성 기술자를 작성할 수 있습니다.

  prop: {
    value: 1,
    writable: false,
    enumerable: true,
    configurable: false
  },
  another: {
    value: 2
    // 안적어주면 다 false로 처리합니다.
    // writable : false
    // enumerable : false
    // configurable : false
  }
});

console.log(obj); // {prop: 1}

obj.prop = 2;
console.log(obj.prop); // 1

delete obj.prop;
console.log(obj.prop); // 1

Object.create 외에, Object.defineProperty 혹은 Object.defineProperties 정적 메소드를
사용해서 이미 만들어진 객체에 대한 속성을 정의할 수도 있습니다.

const obj = {};
Object.defineProperty(obj, "prop", {
  value: 1,
  writable: false,
  enumerable: true,
  configurable: false
});



접근자 속성(accessor property)과 그 부수속성

const obj = {
  get prop() {
    console.log("getter가 호출되었습니다.");
    return this._hidden;
  },
  set prop(arg) {
    console.log("setter가 호출되었습니다.");
    this._hidden = arg;
  }
};

// `set prop` 메소드가 `1`을 인수로 해서 호출됩니다.
obj.prop = 1;

// `get prop` 메소드가 호출되고 해당 메소드의 반환값을 읽어옵니다.
obj.prop; // 1

Object.getOwnPropertyDescriptors(obj);
// {
//   prop: {
//     get: [Function: get],
//     set: [Function: set],
//     enumerable: true,
//     configurable: true
//   },
//   ...
// }

obj 객체 리터럴 안에서 함수 앞에 get 과 set 키워드를 사용했습니다.
이 두 함수는 각각 prop 이라는 속성의 getter 와 setter 가 됩니다. getter 는 속성을 읽어올 때, setter 는 속성을 변경할 때 호출됩니다.
이렇게 getter 와 setter 가 정의된 속성을 접근자 속성(accessor property)이라고 합니다.

접근자 속성에 대한 속성 기술자는 네 가지 속성을 갖습니다.

데이터 속성의 속성 기술자와 비교해보면 value 와 writable 이 빠진 대신에 get, set 이 포함되어 있습니다.

다음은 접근자 속성을 사용할 때와 안할 때의 예입니다.

// 접근자 속성 미사용시 - 코드가 보기 어렵고, 속성을 사용하기 위해 매번 메소드를 호출해야 합니다.
function Money(won = 0) {
  Object.defineProperty(this, "_won", {
    value: won,
    writable: true
  }); // enumerable: false, configurable: false
}
Money.prototype.getWon = function() {
  return this._won;
};
Money.prototype.setWon = function(amount) {
  this._won = amount;
};
Money.prototype.getDollar = function() {
  return this._won / 1086;
};
Money.prototype.setDollar = function(amount) {
  this._won = amount * 1086;
};

const m = new Money();

m.setWon(1086);
m.getDollar(); // 1

m.setDollar(2);
m.getWon(); // 2172

// 접근자 속성 사용시 - 코드가 한결 보기 쉬워졌습니다.
function Money(won) {
  this._won = won;
}

Object.defineProperties(Money.prototype, {
  won: {
    get: function() {
      return this._won;
    },
    set: function(arg) {
      this._won = arg;
    }
  },
  dollar: {
    get: function() {
      return this._won / 1086;
    },
    set: function(arg) {
      this._won = arg * 1086;
    }
  }
});

const w = new Money(1086);

w.won += 1086;
console.log(w.dollar); // 2

w.dollar += 1;
console.log(w.won); // 3258



객체의 속성 열거하기

앞에서 속성의 부수속성 중에 enumerable 이라는 것이 있다는 것을 살펴봤습니다.
이 부수속성은 객체의 속성을 열거할 때에 그 결과에 영향을 미칩니다.

객체의 속성을 열거할 때에 사용할 수 있는 방법에는 여러 가지가 있습니다.

대개의 경우 Object.keys 를 사용하면 되지만, 상속받은 속성까지 열거하고 싶을 때는 for…in 을,
열거 불가능한 속성도 열거하고 싶을 때는 Object.getOwnPropertyNames 를 사용하세요.

const obj = {
  a: 1,
  b: 2,
  c: 3
};

// for (let name of Object.keys(obj)) {
//   console.log(name);
// }

for (let [name, value] of Object.entries(obj)) {
  console.log(name);
  console.log(value);
}



얕은 복사(shallow copy) & 깊은 복사(deep copy)

Object.assign 정적 메소드는 인수로 받은 객체들의 모든 열거 가능한 속성을 대상 객체에 복사합니다.

const obj = {};
Object.assign(obj, { a: 1 }, { b: 2 });

console.log(obj); // { a: 1, bs: 2 }

Object.assign 은 객체를 복제하는 수단으로도 사용됩니다.

const obj = {
  a: 1,
  b: 2
};

// 빈 객체를 대상으로 `Object.assign`을 사용하면, 객체를 간단히 복제할 수 있습니다.
const obj2 = Object.assign({}, obj);
console.log(obj2); // { a: 1, b: 2 }

다만, 여기서 주의해야 할 점이 있습니다. 객체가 중첩되어 있다면, 내부에 있는 객체는 복제되지 않습니다.
Object.assign 을 통해 속성이 값이 복사될 때, 실제로 복사되는 것은 중첩된 객체라 아니라 그에 대한 참조이기 때문입니다.

const obj = {
  innerObj: {
    a: 1,
    b: 2
  }
};

const obj2 = Object.assign({}, obj);

// `innerObj`는 복제되지 않았습니다.
obj.innerObj === obj2.innerObj;
obj.innerObj.a = 3;
obj2.innerObj.a; // 3

프로그래밍 분야에서는 중첩된 자료구조까지 모두 복사하는 것을 가지고 깊은 복사(deep copy)라고 합니다.
JavaScript 에는 깊은 복사를 위한 기능이 내장되어 있기 않기 때문에, 직접 구현을 해서 사용해야 합니다.
그런데 깊을 복사를 할 때 고려해야 할 것들이 많아서 (순환참조, 프로토타입, 열거 불가능한 속성, getter/setter 등)
정말로 직접 구현하기는 어렵고, 관련 라이브러리를 사용하는 것을 추천합니다.

비슷한 객체의 복제가 빈번하게 이루어져야 하는 경우에는 Immutable.js 와 같은 라이브러리의 사용도 고려해 보시길 바랍니다.

값을 바꿀 일이 없다면 얕은 복사도 문제가 없지만 영향을 안미치면서 값을 바꿔야 한다면 깊은 복사를 해야한다.



Object.preventExtentions

JavaScript 는 특정 객체에 더 이상 속성을 추가하지 못하도록 막아버리는 기능을 제공합니다.

const obj = {};

// 객체에 속성이 추가되는 것을 막습니다.
Object.preventExtensions(obj);

function func() {
  "use strict";
  obj.a = 1;
}

func(); // TypeError: Cannot add property a, object is not extensible

JavaScript 의 모든 객체에는 [[Extensible]]이라는 숨겨진 속성이 있습니다.
이 속성의 기본값은 true 인데, 이 값이 false 가 되면 해당 객체에 속성을 추가하는 것이 불가능해집니다.
Object.preventExtensions 정적 메소드는 [[Extensible]] 속성을 false 로 바꿔주는 역할을 합니다.

객체의 [[Extensible]] 속성 값은 Object.isExtensible 정적 메소드를 통해 알아볼 수 있습니다.

const obj = {};
Object.isExtensible(obj); // true
Object.preventExtensions(obj);
Object.isExtensible(obj); // false

Object 생성자의 정적 메소드 중에 [[Extensible]] 속성을 바꿔버리는 메소드가 두 개 더 있습니다.


아래의 표는 앞에서 다뤘던 세 정적 메소드를 호출한 뒤에 객체가 어떻게 변하는지를 나타냅니다.
O 는 가능, X 는 불가능을 나타냅니다.

메소드 속성 추가 속성 읽기 속성 변경 속성 삭제 및 재정의
Object.preventExtentions X O O O
Object.seal X O O X
Object.freeze X O X X


객체에 Object.seal 혹은 Object.freeze 가 호출되었는지를 확인하기 위해 다음 메소드를 사용할 수 있습니다.
단, 객체에 대해 Object.seal 혹은 Object.freeze 가 호출된 적이 없더라도 아래 조건만 충족하면 이 메소드들은 true 를 반환할 수 있습니다.

Object.isSealed - 객체가 확장 불가능하고 객체 자신의 모든 속성에 대한 부수속성이
configurable: false 에 해당하면 true 를 반환합니다. 아니면 false 를 반환합니다.

Object.isFrozen - 객체가 확장 불가능하고 객체 자신의 모든 속성에 대한 부수속성이
configurable: false, writable: false 에 해당하면 true 를 반환합니다. 아니면 false 를 반환합니다.



2. Today I Found Out

수업시간에 들었던 내용을 다시 한번 적어보면서
이해를 하려고 노력했습니다.
다소 생소한 부분이 많았지만
그래도 '어떻게 쓰일까?'라는 생각을 하면서
공부에 임하였습니다.
또한 기능적인 부분에서 다소 부족한 부분이 있을 경우
라이브러리로 기능을 보완해나가는 모습을 보면서
개발의 생산성과 편의성이 점점 많이 좋아지는 것을 느꼈고
다양한 소식과 지식을 접해서 편리하면서도
개발에 더욱 집중할 수 있도록 해야겠다고 생각했습니다.



3. refer

https://helloworldjavascript.net/pages/240-object-in-depth.html

http://wanna-b.tistory.com/18

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Property_Accessors