JavaScript 객체!

object 개요

출처: 모던 자바스크립트 Deep Dive

참고: https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Working_with_Objects

JS 는 객체기반 언어이다. 원시타입 을 제외한 모든것이 객체라 할 수 있다.
심지어 일부 원시타입 wrapping 한 객체또한 존재한다(String, Number, Boolean)

객체는 원시타입과 다르게 변경가능한 값(mutable value) 으로 깊은복사, 얕은복사 개념이 존재한다.

property

객체는 property(key, value) 의 모음 이라 할 수 있다.

property 는 camel case 를 권장한다.

var person = {
    fistName: "Ko",
    'last-name': "zie" // 가능은 하나 권장 X
}

하이폰은 연산자로 취급되어 향후 property 값을 사용할 때에도 여러 문제가 발생한다.

property 접근법 마침표 표기법과 대괄호 표기법이 있다.

var person = { fistName: "Ko", lastName: "zie" }
console.log(person.fistName)    // 마침표 표기법
console.log(person['lastName']) // 대괄호 표기법

동적으로 property 추가 삭제가 가능

var person = {}
person.fisrName = "Ko"
person["lastName"] = "zie"
console.log(person) // { fisrName: 'Ko', lastName: 'zie' }
var person = { fistName: "Ko", lastName: "zie" }
delete person.fistName
console.log(person.fistName) // undefined

개인적으로 동적으로 추가/삭제 하는것은 권장하지 않는다.

method

property 의 valuefunction 일 때 method 라고 명칭함

var s1 = {
    name: "홍길동",
    sayName1: function () {
        return `hello ${this.name}`
    },
    sayName2() { // 축약표현 method shorthand
        return `hello ${this.name}`
    }
};
console.log(s1.sayName1()) // hello 홍길동
console.log(s1.sayName2()) // hello 홍길동

접근자 함수

getter/setter 함수라 부르기도 함

const person = {
    name: 'lee',
    age: 20,

    get nameHello() {
        return `my name is ${this.name}`
    },

    set nameHi(str) {
        this.name = str + "new"
    }
}

console.log(person.nameHello) // my name is lee
person.nameHi = "Hi"
console.log(person.nameHello) // my name is Hinew

get, set 함수정의를 통해 마치 property 를 정의하듯 함수호출이 가능하다.

내부슬롯, 내부메서드

내부 슬롯과 내부 메서드는 자바스크립트 엔진의 내부 동작을 설명하기 위해 ECMAScript에서 정의한 의사 프로퍼티(pseudo property)와 의사 메서드(pseudo method)

[[...]] 형태로 출력되며 property 와 method 처럼 외부에 공개되진 않지만 디버그창에서 확인할 순 있다.

property 확인 메서드

const person = {
    name: 'Lee',
    address: 'Seoul'
};

console.log('name' in person); // true
console.log(person.hasOwnProperty('name')); // true
console.log(Reflect.has(person, 'name')); // true

console.log(Object.keys(person)); // [ 'name', 'address' ]
console.log(Object.values(person)); // [ 'Lee', 'Seoul' ]
console.log(Object.entries(person)); // [ [ 'name', 'Lee' ], [ 'address', 'Seoul' ] ]

Object.entries(person).forEach(([key, value]) => console.log(key, value));
// name Lee
// address Seoul

객체를 생성하는 방법

리터럴{ } 을 사용한 객체생성

중괄호 {...} 를 사용한 객체 생성

var s1 = {
  name : "홍길동",
  age : 21,
  print: function () {
    return this.name + "/" + this.age;
  }
};
console.log(typeof s1);     //object
console.log(s1.name);       //홍길동
console.log(s1["name"]);    //홍길동
console.log(s1.print());    //홍길동/21

주의 닫는 중괄호에 ; 추가 간으
리터럴은 블록으로 취급되지 않으며 블록스코프가 아님

new Object() 를 사용한 객체 생성

new Obejct() 로 빈 객체를 생성후 property 와 메서드를 추가

var person = new Object();
person.firstName = "Hong";
person.lastName = "gildong";
person.age = "Hong";
person.print = function() {
  return this.firstName + " " + this.lastName;
};

Object() 는 사실 built-in 된 생성자함수로
String, Number, Boolean, Function, Array, Date 등 여러 built-in 생성자함수가 존재한다.

생성자함수를 사용한 객체 생성

JS에서도 클래스기반 언어처럼 Class선언문을 정의해서 인스턴스를 찍어낼 수 있는 문법이 있는데 생성자함수 이다.

this 키워드를 사용해 생성된 객체에 프로퍼티를 정의하고 초기화한다.

생성자함수에선 맨 앞의 문자를 대문자로 쓸 것 을 권장한다.

function Person(name, age, color) {
    this.name = name;
    this.age = age;
    this.color = color;

    this.print = function () {
        return this.name + " / " + this.age + " / " + this.color;
    }
}
var father = new Person("Hong", 30, "blue");
var son = new Person("Kim", 10, "red");

console.log(father.print()); // Hong / 30 / blue
console.log(son.print()); // Kim / 10 / red

new 키워드

생성자함수도 일반 함수이기 때문에 단순 호출시 this.namename 이라는 전역변수를 초기화 하는 문법이 되어버린다.

때문에 함수앞에 암묵적으로 new 연산자를 통해 인스턴스를 생성하고 생성자함수와 this 바인딩 을 진행하고
내부에서 this 키워드가 인스턴스의 공간을 가리키도록 한다.

최종적으로 이미 바인딩된 인스턴스의 주소가 new 연산자의 반환값으로 사용된다.

코드 new Person(...)가 실행될 때 다음과 같은 일이 발생한다

  1. 인스턴스 생성 및 생성자함수와 바인딩
  2. this 를 통한 인스턴스 초기화
  3. new 의 인스턴스 암묵적 반환
function Person(name, age, nickname) {
    // 1. 암묵적인 인스턴스 생성 및 new 와 바인딩
    console.log(this) // Person {}
    // 2. 초기화 진행
    this.name = name;
    this.age = age;
    this.nickname = nickname;

    // 3. 암묵적인 인스턴스 반환
    // return this; // 명시적 객체 반환
}

let p = new Person("kouzie", 20, 'jason');
console.log(p) // Person { name: 'kouzie', age: 20, nickname: 'jason' }

만약 생성자함수가 명시적으로 객체를 반환 하는경우 생성자함수의 최종 반환값은 명시적으로 반환된 객체이다.

특별한 상황 외에 생성자함수에서 return 문을 권장하지 않는다.

재밌는 점은 반환값이 객체가 아닌 원시타입일 경우 암묵적 인스턴스반환이 이루어진다는 것

function Circle(radius) {
    this.radius = radius;
    return 100;
}
const circle = new Circle(1);
console.log(circle); // Circle { radius: 1 }

생성자함수가 new 연산자와 같이 호출되었는지 확인하기 위해 new.target 을 사용한다.

function Circle(radius) {
  // 이 함수가 new 연산자와 함께 호출되지 않았다면 new.target은 undefined다.
  if (!new.target) {
    // new 연산자와 함께 생성자함수를 재귀 호출하여 생성된 인스턴스를 반환한다.
    return new Circle(radius);
  }

  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}

대부분 built-in 생성자함수가 위와같은 패턴을 가지고 있다.
new 연산자와 같이 호출하지 않아도 동일하게 동작함

함수앞에 new 연산자를 붙이는개 생소하지만 문법으로 받아들이자.
JS 에서 함수객체 내부에 일반함수 호출을 위한 [call], 생성자함수 호출을 위한 [constructor] 가 별도로 정의되어있다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/new

Object.create() 로 객체 생성

명시적으로 prototype 객체 를 생성이 가능하다.

아래처럼 prototype 을 null 로 지정하면 내부슬롯도 하나 없는 깔끔한 객체하나가 생성된다.

let obj = Object.create(null);
console.log(Object.getPrototypeOf(obj) === null); // true
// console.log(obj.toString()); 
// TypeError: obj.toString is not a function

리터럴 이나 생성자함수로 만든 객체는 아래와 동일하다.

obj = Object.create(Object.prototype);

OrdinaryObjectCreate

리터럴, 생성자함수, Object.create 로 객체를 생성할 때 내부에서 공통적으로 추상연산인 OrdinaryObjectCreate 이 호출된다.

Object.prototype 객체를 인스턴스에 연결한다.

data property, accessor property

객체의 property 는 원시타입이나 하위 객체를 참조하는 단순변수가 아니다.

객체의 property 는 data property, accessor property 로 나뉘며
지금까지 정의했던 property 들은 모두 data property이다.

data property, accessor property 에 여러가지 attribute 가 내부슬롯으로 존재하며
이를 이용해 마치 일반 변수처럼 동작한다.

data property attribute

const person = {
    name: 'lee',
    age: 20
}
console.log(Object.getOwnPropertyDescriptor(person, 'name'))
// { value: 'lee', writable: true, enumerable: true, configurable: true }

// getOwnPropertyDescriptors 로 모든 property 한번에 확인 가능  
console.log(Object.getOwnPropertyDescriptors(person));
// {
//     name: {value: 'lee', writable: true, enumerable: true, configurable: true}
//     age: { value: 20, writable: true, enumerable: true, configurable: true }
// }

Descriptor 함수 를 사용해보면 data propertyvalue, writable, enumerable, configurable 4가지 내부슬롯이 있음을 알 수 있다.

value key 를 통해 접근하면 반환되는 value

writable value 의 변경가능 여부를 나타냄

enumerable value 의 열거가능(iterable) 여부를 나타냄, for..in 과 같은 연산자에 열거되지 않음

configurable property 의 삭제, value 의 재정의 가능여부, writable 의 상위개념 property attribute 변경 또한 막아버린다.

accessor property attribute

const person = {
    name: 'lee',
    age: 20,

    get nameHello() {
        return `my name is ${this.name}`
    },

    set nameHi(str) {
        this.name = str + "new"
    }
}

console.log(Object.getOwnPropertyDescriptor(person, 'nameHello'))
// { get: [Function: get nameHello], set: undefined, enumerable: true, configurable: true }

accessor property 역시 내부슬롯을 가지고있다.

get
접근자 함수의 value, 호출시 함수가 실행되고 결과가 반환됨

set
접근자 함수의 value, 호출시 함수가 실행됨

그 외의 속성은 data property attribute 와 동일함

객체 변경 방지

내부슬롯 은 외부로 노출되지 않기때문에 수정하려면 JS 에 built-in 되어있는 함수를 사용하여 수정해야한다.

Object.defineProperty 혹은 Object.defineProperties 를 사용해 property attribute 를 조작 가능

Object.defineProperties(person, {
  // 데이터 프로퍼티 정의
  firstName: {
    value: 'Ungmo',
    writable: true,
    enumerable: true,
    configurable: true
  },
  lastName: {
    value: 'Lee',
    writable: true,
    enumerable: true,
    configurable: true
  },
  // 접근자 프로퍼티 정의
  fullName: {
    // getter 함수
    get() {
      return `${this.firstName} ${this.lastName}`;
    },
    // setter 함수
    set(name) {
      [this.firstName, this.lastName] = name.split(' ');
    },
    enumerable: true,
    configurable: true
  }
});
메서드 프로퍼티 추가 프로퍼티 삭제 프로퍼티 값 읽기 프로퍼티 값 쓰기 프로퍼티 어트 리뷰트 재정의
Object.preventExtensions
Object.seal
Object.freeze

Object.freeze 에 대해서만 살펴보면

const person = { name: 'Lee' };

// person 객체는 동결(freeze)된 객체가 아니다.
console.log(Object.isFrozen(person)); // false

// person 객체를 동결(freeze)하여 프로퍼티 추가, 삭제, 재정의, 쓰기를 금지한다.
Object.freeze(person);

// person 객체는 동결(freeze)된 객체다.
console.log(Object.isFrozen(person)); // true

// 동결(freeze)된 객체는 writable과 configurable이 false다.
console.log(Object.getOwnPropertyDescriptors(person));
// { name: {value: "Lee", writable: false, enumerable: true, configurable: false} }

// 프로퍼티 추가가 금지된다.
person.age = 20; // 무시. strict mode에서는 에러
console.log(person); // {name: "Lee"}

// 프로퍼티 삭제가 금지된다.
delete person.name; // 무시. strict mode에서는 에러
console.log(person); // {name: "Lee"}

// 프로퍼티 값 갱신이 금지된다.
person.name = 'Kim'; // 무시. strict mode에서는 에러
console.log(person); // {name: "Lee"}

// 프로퍼티 어트리뷰트 재정의가 금지된다.
Object.defineProperty(person, 'name', { configurable: true });
// TypeError: Cannot redefine property: name

Object.freeze 사용하여 객체의 중첩 객체까지 불변으로 만드려면 재귀함수를 돌며 필드까지 모두 freeze 해야한다.

function deepFreeze(target) {
  // 객체가 아니거나 동결된 객체는 무시하고 객체이고 동결되지 않은 객체만 동결한다.
  if (target && typeof target === 'object' && !Object.isFrozen(target)) {
    Object.freeze(target);
    /*
      모든 프로퍼티를 순회하며 재귀적으로 동결한다.
      Object.keys 메서드는 객체 자신의 열거 가능한 프로퍼티 키를 배열로 반환한다.
      forEach 메서드는 배열을 순회하며 배열의 각 요소에 대하여 콜백 함수를 실행한다.
    */
    Object.keys(target).forEach(key => deepFreeze(target[key]));
  }
  return target;
}

const person = {
  name: 'Lee',
  address: { city: 'Seoul' }
};

// 깊은 객체 동결
deepFreeze(person);

console.log(Object.isFrozen(person)); // true
// 중첩 객체까지 동결한다.
console.log(Object.isFrozen(person.address)); // true

person.address.city = 'Busan';
console.log(person); // {name: "Lee", address: {city: "Seoul"}}

Prototype

Prototype: 원형 https://medium.com/@bluesh55/JavaScript-prototype-이해하기-f8e67c286b67

JS는 프로토타입 기반 객체지향 프로그래밍 언어이다.

Porototpye 은 클래스기반 언어보다 효율적으로 상속과 객체 선언 등의 문법을 제공한다.

JS 의 모든 객체는 Prototype 객체 를 내부 슬롯으로 가지고있으며
모든 생성자함수에서 인스턴스 생성시 Prototype 객체 를 배부한다.

// 생성자함수
function Circle(radius) {
  this.radius = radius;
  this.getArea = function () {
    return Math.PI * this.radius ** 2;
  };
}

const circle1 = new Circle(1);
const circle2 = new Circle(2);
console.log(circle1.getArea === circle2.getArea); // false

2

Circle 생성자함수로 만들어진 인스턴스마다 계속하여 getArea 메서드가 정의된다(메모리낭비).

function Circle(radius) {
    this.radius = radius;
}
Circle.prototype.getArea = function () {
    return Math.PI * this.radius ** 2;
};

2

생성자함수는 prototype 라는 특별한 property 를 가지고 있으며
인스턴스생성시 prototype 객체 주소를 발부할 때 사용한다.

생성자함수.prototype === prototype 객체

효율적인 메모리 관리와 static 변수와 같은 기능구현도 가능하다.

prototype 객체 라고 특별한 객체가 아니다.
constructor property 가 존재하는(생성자함수 참조) 일반 객체이다.

일반적으로 가독성을 위해 생성자함수 정의와 protoype 정의를 하나의 함수 리터럴에서 처리한다.

const Circle = (function () {
    function Circle(radius) {
        this.radius = radius;
    }
    Circle.prototype.getArea = function () {
        return Math.PI * this.radius ** 2;
    };
    return Circle;
})();

let c1 = new Circle(1);
console.log(c1.getArea()) // 3.141592653589793

__proto__

인스턴스는 __proto__ 라는 accessor property 를 통해 Prototype 객체 에 접근 가능하다.

// Circle 클래스의 prototype 객체 출력
console.log(Object.getOwnPropertyDescriptors(new Circle(1).__proto__))
/* 
{
  constructor: {
    value: [Function: Circle],
    writable: true,
    enumerable: false,
    configurable: true
  },
  getArea: {
    value: [Function (anonymous)],
    writable: true,
    enumerable: true,
    configurable: true
  }
}
*/

// Object 클래스의 prototype 객체 출력
console.log(Object.getOwnPropertyDescriptors({}.__proto__))
/* 
{
  constructor: {
    value: [Function: Object],
    writable: true,
    enumerable: false,
    configurable: true
  },
  ...
  hasOwnProperty: {
    value: [Function: hasOwnProperty],
    writable: true,
    enumerable: false,
    configurable: true
  },
  ...
  isPrototypeOf: {
    value: [Function: isPrototypeOf],
    writable: true,
    enumerable: false,
    configurable: true
  },
  propertyIsEnumerable: {
    value: [Function: propertyIsEnumerable],
    writable: true,
    enumerable: false,
    configurable: true
  },
  ...
  ['__proto__']: {
    get: [Function: get __proto__],
    set: [Function: set __proto__],
    enumerable: false,
    configurable: true
  },
}
*/

자바스크립트 엔진은 접근하려는 property 가 없다면 __proto__ 로부터 자신의 prototype 객체, 이어서 자신부모의 prototype 객체 순차적으로 검색하는데 이를 Prototype Chain 이라 한다.

__proto__circle 객체의 prototype 객체에 존재하지 않아서 Object__proto__ 를 호출하여 circleprototype 객체에 접근한다.

상속 - Prototype Chain

prototype 객체 는 특별한 객체가 아닌 생성자함수를 참조하는 constructor property 가 있는 개체라 하였는데

역시 내부슬롯으로 prototype 객체를 가지게 되는데 마치 상속처럼 동작하게 된다.

실제 디버그창에서 circle 인스턴스를 출력해보면 아래와 같다.

2

circle 객체의 prototype 객체 에도 prototype 객체 가 있는데
Object 클래스의 Prototype 객체 이다

대부분의 객체는 prototype 객체 을 가지고 있으며 연결된 prototype 객체 를 타고 올라가면 상위는 Object 클래스의 prototype 객체 이다.

생성자함수 - prototype property

함수객체만이 소유하는 property
생성자함수가 발급할 prototype 객체의 주소와 동일하다.

// 생성자함수
function Person(name) {
    this.name = name;
}
const me = new Person('Lee');

// 결국 Person.prototype과 me.__proto__는 결국 동일한 프로토타입을 가리킨다.
console.log(Person.prototype === me.__proto__);  // true

prototype 객체 는 은 생성자함수가 생성되는 시점에 생성된다 생성자함수과 prototype 객체 는 항상 한쌍으로 구성된다 (constructor - prototype)

2

화살표 함수는 prototype property 를 사용하지 않음으로 생성자함수로 사용할 수 없다. non-constructor 함수라 한다.

생성자함수의 prototype property 자체를 교체가능하다.

const Person = (function () {
    function Person(name) {
        this.name = name;
    }

    Person.prototype = {
        constructor: Person,
        sayHello() {
            console.log(`Hi! My name is ${this.name}`);
        }
    };

    return Person;
}());

즉시실행함수로 Person 생성자함수를 반환
즉시실행함수내부에서 생성자함수 정의와 prototype 객체 교체가 같이 이루어진다.

이번에는 생성된 객체의 prototype 객체 교체를 진행한다.

function Person(name) {
    this.name = name;
}

const me = new Person('Lee');
const you = new Person('Ko');

// 프로토타입으로 교체할 객체
const parent = {
    sayHello() {
        console.log(`Hi! My name is ${this.name}`);
    }
};

Object.setPrototypeOf(me, parent);

console.log(me.constructor === Person);  // false
console.log(you.constructor === Person);  // true

객체 하나에 대해서만 교체가 진행되고 constructor 필드의 연결이 파괴된다.
교체할 prototype 객체에 기존 constructor 까지 재설정하는 것을 권장한다.

instanceof 와 prototype

위에서 prototype 객체를 교체한 me 객체의 경우 instanceofPerson 객체인지 검사하면 false 로 나온다.

function Person(name) {
    this.name = name;
}

const me = new Person('Lee');
const you = new Person('Ko');

// 프로토타입으로 교체할 객체
const parent = {
    sayHello() {
        console.log(`Hi! My name is ${this.name}`);
    }
};

Object.setPrototypeOf(me, parent);

console.log(me.constructor === Person);  // false
console.log(me instanceof Person) // false

instance of 명령어는 객체의 prototype 객체와, 생성자함수의 prototype 객체가 일지하는지 여부를 검사하는 함수라 할 수 있다.

function Person(name) {
    this.name = name;
}

let obj = Object.create(Person.prototype);
console.log(obj instanceof Person) // true
console.log(obj instanceof Object) // true

실제 Person 생성자함수로 만든 객체가 아니고 prototype 객체만 일치하면 true 로 반환한다.

prototype chain 으로 인해 상위 prototype 객체instance of 로 검사해도 true 가 출력됨

주의사항

__proto__ 사용을 권장하지 않는다.
상속구조에 따라 Object.prototype 을 상속받지 않았을 때 __proto__ 이 오류를 야기한다.
set __proto__ 로 바라보고 있는 prototype 객체 를 변경시키는 것도 비정상적 상황을 야기한다.

Object.prototype 의 빌트인 메서드 hasOwnProperty, isPrototypeOf, propertyIsEnumerable 등을 객체가 직접 호출하는 것을 권장하지 않는다.
마찬가지로 Object.prototype 을 상속받지 않았을 떼 오류를 야기한다.
아래처럼 직접호출을 권장

Object.prototype.hasOwnProperty.call(obj, 'a')

표준 built-in 객체

ECMAScript 사양에 정의된 40 여개의 표준 built-in 객체

  • Object
  • String
  • Number
  • Boolean
  • Symbol
  • Date
  • Math
  • RegExp
  • Array
  • Map/Set
  • WeakMap/ WeakSet
  • Function
  • Promise
  • Reflect
  • Proxy
  • JSON
  • Error

원시타입과 래퍼객체

const str = "hello"
console.log(Object.getPrototypeOf(str) === String.prototype) // true

const num1 = 1.5;
console.log(num1.toFixed()) // 2

JS 엔진이 일시적으로 원시값을 연관된 객체로 변환하고 작업이 끝나면 다시 원시값으로 되돌린다.

이때 String, Number, Boolean 처럼 접근하면 생성되는 임시객체를 래퍼객체라 한다.

임시사용한 래퍼객체는 가비지 컬렉터의 대상이 됨으로 성능하락의 원인이 됨으로 권장하지 않는다.

전역객체

전역객체(global object) 는 코드가 실행되기전 JS 엔진에 의해 제일 먼저 생성되는 특수한 객체

브라우저 환경에서는 window
Node.js 환경에서는 global

local scope 밖에서 정의된 var 변수, 전역함수는 전역객체의 property 로 포함되게 된다(암묵적 전역).

var foo = 1; // 전역 변수
bar = 2; // 암묵적 전역, 전역객체의 프로퍼티로 사용됨
console.log(window.foo); // 1
console.log(window.bar); // 2

// 전역 함수
function baz() { return 3; }
console.log(window.baz()); // 3

전역객체 property

사실 전역객체는 위에서 말한 표준 built-in 객체들을 property 로 가지고 있다.
지금까지 built-in 함수를 바로 호출하는 건 전역객체 참조이름을 생략하고 호출했던 것

const num1 = new global.Number(2)
console.log(num1) // [Number: 2]
global.parseInt === parseInt; // -> true
global.parseInt('F', 16); // -> 15

표준 built-in 객체들을 제외하고 아래와 같은 전역객체 property(객체, 함수) 가 존재한다.

  • Infinity
  • NaN
  • undefined
  • eval
  • isFinite
  • isNaN
  • parseFloat
  • parseInt
  • encodeURI, decodeURI
  • encodeURIComponent, decodeURIComponent