본문 바로가기
web/javascript

[javascript] 모던 자바스크립트 입문 9.객체(프로토타입)

by fien 2021. 1. 21.

모던 자바스크립트 입문 9장 객체 부분을 읽고 정리한 내용입니다.

1. 객체의 생성 

// 1. 객체 리터럴로 생성
const fruit = { name = 'apple', season = 'fall'};

// 2. 생성자로 생성
function Fruit(name, season) {
    this.name = name;
    this.season = season;
}
const fruit = new Fruit('apple', 'fall');

// 3. Object.create로 생성
const fruit = Object.create(Object.prototype, {
    name: {
        value: 'apple',
        writable: true,
        enumerable: true,    
        configuragle: true
    },
    season: {
        value: 'fall',
        writable: true,
        enumerable: true,    
        configuragle: true
    }
});

프로토타입

js는 class 개념 대신 prototype이 존재하는데 이를 통해 상속을 구현함. js 객체는 [[prototype]](__proto__) 속성으로 부모객체를 가리킨다. 자바의 class가 최상위에 Object클래스를 상속 받는 것처럼 js 객체는 __proto__속성으로 Object를 가리킨다. 

  • 생성자로 메서드 정의하는 방식의 문제점

    메서드를 인스턴스 개수만큼 생성하여 메모리를 낭비함

      function Fruit(name, season, price) {
          this.name = name;
          this.season = season;
          this.getPrice = function(season) {
              if (this.season === season) return price;
              else return price*1.2;
          };
      }
    
      const apple = new Fruit('apple','fall',1000);
      const tangerine = new Fruit('tangerine', 'winter', 200);
      const strawberry = new Fruit('strawberry ', 'spring', 300);
    
      console.log(apple.getPrice('winter'));
      console.log(tangerine.getPrice('winter'));
      console.log(strawberry.getPrice('winter'));
  • 프로토타입 객체

    함수는 prototype 속성을 가진다. (__proto__ 속성과 별개)

    prototype 속성은 자신의 prototype 객체로 자식 객체(생성자로 생성한 인스턴스)가 참조하는 객체이다.

    따라서 생성자의 prototype 객체에 속한 속성은 해당 prototype 객체를 프로토타입으로 가지는 모든 인스턴스(생성자로 생성한 인스턴스)에서 사용할 수 있다.

    (말이 참 헷갈리네..코드로 보시고 __proto__ 와 prototype차이는 아래서 다시 설명합니다) 

      function Fruit() {};
      console.log(Fruit.prototype); 
    
      Fruit.prototype.prop = 'proto val';
      const fruit = new Fruit();
      console.log(fruit.prop); // proto val
    
      fruit.prop = 'instance val';
      console.log(fruit.prop); // instance val
      console.log(Fruit.prototype.prop); // proto val
  • 프로토타입에 메서드 정의하기

    인스턴스 안에 메서드가 존재하지 않아도 메서드를 사용할 수 있다.

    또한, 메서드 안의 this는 생성한 인스턴스를 가리킨다.

      // 프로토타입에 메서드 정의하기
      function Fruit(name, season, price) {
          this.name = name;
          this.season = season;
          this.price = price;
      }
    
      Fruit.prototype.getPrice = function(season) {
          if (this.season === season) return this.price;
          else return this.price*1.2;
      };
    
      // this는 생성한 인스턴스를 가리킴
      Fruit.prototype.getSeason = function() {
          console.log(`${this.name}은 ${this.season}에 제철입니다.`);
      };
    
      const apple = new Fruit('apple','fall',1000);
      const tangerine = new Fruit('tangerine', 'winter', 200);
      const strawberry = new Fruit('strawberry ', 'spring', 300);
    
      // 인스턴스 내에 메소드가 없어도 프로토타입에 정의했기 때문에 사용가능
      console.log(apple.getPrice('winter'));
      console.log(tangerine.getPrice('winter'));
      console.log(strawberry.getPrice('winter'));
    
      apple.getSeason();

 

 


2. 프로토타입 상속

js에서 상속은 프로토타입 체인으로 구현된다. 객체의 __proto__속성은 부모 객체(원형 객체)를 가르킨다. 속성값을 찾을 수 없으면 프로토타입을 거슬러 올라가 속성 값을 찾아 실행한다.

const parent = {
    name: 'p name',
    sayHello: function() { console.log(`Hello! I'm ${this.name}`); }
};

const child = {
    name: 'c name'
};
child.__proto__ = parent;

const grandchild = {
    name: 'g name'
};
grandchild.__proto__ = child;

parent.sayHello();
child.sayHello();
grandchild.sayHello();

debugger;

// 프로토타입 가져오기
console.log(Object.getPrototypeOf(child));

프로토타입 체이닝

new 연산자

function Fruit(name, season, price) {
    this.name = name;
    this.season = season;
    this.price = price;
}

Fruit.prototype.getPrice = function(season) {
    if (this.season === season) return this.price;
    else return this.price*1.2;
};

// new 연산자를 이용한 인스턴스 생성 과정
const apple = new Fruit('apple','fall',1000);
        // 1. 빈 객체 생성
        const apple = {};
        // 2. 프로토타입 설정
        // 이떄, 프로토타입이 객체가 아니면 Object.prototype으로 설정
        apple.__proto__ = Fruit.prototype;
        // 3. 생성자를 싱행하고 객체를 초기화.
        Fruit.apply(apple, arguemnts);
        // 4. 객체 반환
        return apple;

프로토타입 객체의 프로퍼티

함수를 정의 하면 prototype 속성(__proto__와는 다른 속성) 을 가지는데 prototype 속성은 constructor와 __proto__ 를 포함한다.

function myfn() {};
console.log(myfn.prototype.constructor);
// -> function myfn() {}
console.log(myfn.prototype);
// -> constructor
debugger;

함수함수의 프로토타입 객체는 서로를 참조하고 있다. 함수의 prototype 속성이 프로토타입 객체를 가리키고, 이 프로토타입 객체의 constructor 프로퍼티가 함수를 가리킨다.

반면 생성자(함수)로 생성한 인스턴스는 프로토타입의 객체만 참조하고 생성자(함수)와는 연결고리가 없다. 대신 프로토타입을 통해 constructor 속성을 상속받기 때문에 생성자에 접근할 수 있다.

인스턴스는 생성시점의 생성자의 프로토타입을 상속받는다. 즉, 생성 이후 생성자의 프로토타입을 수정하면 인스턴스에는 반영되지 않는다.

생성자의 prototype의 __proto__는 Object의 prototype을 가리킨다.

function Fruit(name, season, price) {
    this.name= name;
    this.season= season;
    this.price= price;
}

const banana = new Fruit('banana', 'spring', 500);
console.log(banana.constructor);
console.log(Fruit.prototype.__proto__); // Object.prototype

Fruit.prototype = {
    constructor: Fruit,
    getPrice: function() { console.log(`가격 : ${this.price}`);}
} 
const apple = new Fruit('apple', 'fall', 1000);

//인스턴스 생성 후에 생성자의 프로토타입을 수정하면 인스턴스에서는 반영되지 않는다.
apple.getPrice();
~~banana.getPrice();~~

__proto__ 와 prototype 속성

__proto__ 는 모든 객체가 가지는 속성이고 부모 객체의 prototype 속성을 가리킨다.

prototype 속성은 함수객체만 가지고 있으며 자신의 prototype 객체로서 자식 객체가 __proto__ 속성으로 이를 참조한다.

 

프로토타입의 확인

// 프로타타입이 특정 객체의 프로토타입 체인에 포함되어있는지 확인
// instanceof 연산자
객체 instanceof 생성자
// insPrototypeOf 메서드
프로토타입.isPrototypeOf(객체)

function F() {};
const obj = new F();

console.log(obj instanceof F);
console.log(F instanceof Object);

console.log(F.prototype.isPrototypeOf(obj));
console.log(Object.prototype.isPrototypeOf(obj));

const apple = {};
const banana = Object.create(apple);
console.log(apple.isPrototypeOf(banana));

Object.prototype

Obejct.prototype은 프로토타입 체이닝으로 모든 객체에 포함된다. 따라서 모든 객체에서 Obejct.prototype의 속성을 사용할 수 있다.

// Object 생성자
const obj = new Object();
                    = Object();
                    = {};

// Object 생성자의 메서드 
// Object.assign(), obj.constuctor.assign() 이런식으로 사용
assign(obj1, obj2, ..., objn) //열거 한 객체들의 속성을 복사하여   객체 반환 
create(proto) // proto를 prototype으로 가지는 객체 생성
getOwnPropertyNames(obj) //obj 객체가 가진 모든 프로퍼티 명을 배열로 반환
keys(obj) // obj객체가 가진 열거 가능한 프로퍼티 명을 배열로 반환
getPrototypeOf(obj) // 인수로 지정한 객체의 프로토타입을 반환

// Object.prototype의 메서드 : 모든 객체에서 사용가능
hasOwnProperty(key) // key 값에 해당하는 프로퍼티의 존재 유무 확인
isPrototypeOf(obj) // obj로 지정한 객체의 프로토타입인지 확인
valueOf() 

Object.create로 객체 생성

Object.create( {프로토타입}, {생성할 객체의 프로퍼티});

const apple = { 
    name: 'apple', 
    season: 'fall', 
    sayHello: function() {
        console.log(`${this.name}은 ${this.season}에 제철입니다.`);
    }
}

const banana = Object.create(apple);
banana.name = 'banana';
banana.sayHello();

//빈 객체{} 생성 
const obj = Object.create(Object.prototype);

3. 접근자 Getter, Setter

객체는 값을 저장하기 위한 데이터 속성과 속성을 읽고 쓸 때 호출하는 함수를 지정하는 접근자 속성(getter, setter)이 있다.

특정 속성에 대해 getter와 setter를 정의 하면 해당 속성을 사용할 때 getter와 setter를 호출해서 사용한다.

const fruit = {
    _name: 'apple',
    get name() { 
        console.log('getter 호출');
        return this._name; },
    set name(name) { 
        console.log('setter 호출');
        this._name = name; }
}
console.log(fruit.name);  // getter 호출
fruit.name = 'banana';  // setter 호출
console.log(fruit.name);  // getter 호출

// 접근자가 있어도 여전히 데이터를 직접 읽고 쓸 수 있다
console.log(fruit._name);

//클로저를 생성해 데이터 캡슐화
// -> 데이터 직접 접근 불가
const fruit = (function() {
    let _name = 'ff';
    return {
        get name() { 
            console.log('getter 호출');
            return _name; 
        },
        set name(name) { 
            console.log('setter 호출');
            _name = name; 
        }
    };
})();

console.log(fruit.name);  // getter 호출
fruit.name = 'banana';  // setter 호출
console.log(fruit.name);  // getter 호출
console.log(fruit._name); // undefined

4. 프로퍼티의 속성

/*
프로퍼티의 속성
    writable (쓰기 여부)
    enumerable (열거 여부)
    configurable (프로퍼티의 내부 속성의 재정의 여부) 
        ->false이면 configurable 속성도 변경 불가. writable만 한번 true->false 변경 가능
*/    
// **내장 생성자의 프로토타입 객체의 프로퍼티는 대부분 쓰기 가능/열거 불가능/재정의 가능 

// 1. PropertyDescriptor : 프로퍼티 속성 기술자
//데이터 속성
{
    value: 속성 값,
    writable: true/false,
    enumerable: true/false,
    configurable: true/false
}
//접근자 속성
{
    get: getter 함수값,
    set: setter 함수값,
    enumerable: true/false,
    configurable: true/false
}

// 2. PropertyDescriptor 가져오기
//Object.getOwnPropertyDescriptor
const apple = { name: 'apple' };
Object.getOwnPropertyDescriptor(apple,'name');
// {value: "apple", writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptors(apple);

// 3. 객체의 프로퍼티 설정(변경)하기
// Object.defineProperty
const banana = {};
Object.defineProperty(banana, 'name', {
    value: 'banana',
    writable: true,
    enumerable: false,
    configurable: true
});

// 4. 객체의 프로퍼티 복수개 설정하기
// Object.defineProperties
const cherry = Object.defineProperties({},{
    _name: { value:'cherry', writable:true, enumerable:true, configurable:true },
    name: { get: function() {return this._name;},
                    set: function(value) { this._name = value;},
                    enumerable:true, configurable:true }
});

Object.create()

//Object.create({prototype}, {PropertyDescriptor});
const apple = Object.create(Object.prototype, {
    name: {
        value: 'apple',
        writable: true,
        enumerable: true,    
        configuragle: true
    },
    season: {
        value: 'fall',
        writable: true,
        enumerable: true,    
        configuragle: true
    }
});

5. 프로퍼티 확인

const apple = { name: 'apple'};

// 1. in 연산자
console.log('name' in apple); //true
console.log('price' in apple); //false
console.log('valueOf' in apple); // true 상속 받은 속성

// 2. hasOwnProperty 메서드
console.log(apple.hasOwnProperty('name'));
console.log(apple.hasOwnProperty('price'));

// 3. propertyIsEnumerable 메서드
// 객체가 소유한 속성이며 열거 가능한지 확인
const fruit = { price: 100 };
const banana = Object.create(fruit);
banana.name = 'banana';
console.log(banana.propertyIsEnumerable('name')); //true
console.log(banana.propertyIsEnumerable('price')); //false

6. 프로퍼티 열거

const parent = { pName: 'p name'};
const child = Object.create(parent);
child.name = 'c name';
child.attr = 'c attr';
child.sayHello = function() { console.log(`hello! I'm ${this.name}`);};
Object.defineProperty(child, 'sayHello', {enumerable:false});

// 속성 열거
// 1. for-in문 
// 프로토타입의 속성도 열거
for(let p in child) console.log(p);

// 2. Object.keys 메서드
// 해당 객체가 소유한 프로퍼티명 조회
const p = Object.keys(child);
for(let i=0 ;i<p.length; i++){
    console.log(`${p[i]} : ${child[p[i]]}`);
}

// 3. Object.getOwnPropertNames 메서드
// 해당 객체가 소유한 프로퍼티명 조회
// 열거할수 없는 프로퍼티 명도 조회
const p = Object.getOwnPropertyNames(child);
for(let i=0 ;i<p.length; i++){
    console.log(`${p[i]} : ${child[p[i]]}`);
}

7. 객체 잠그기

객체를 수정할 수 없게 만듦

// 1. 확장 방지 extensible : 객체의 새로운 속성을 추가 할 수 있는지 여부
//Object.preventExtensions(obj) extensible  -> false
const fruit = { name: 'apple'};
Object.preventExtensions(fruit);
fruit.price = 1000;
console.log('price' in fruit); // false
console.log(Object.isExtensible(fruit)); // false

// 2. 밀봉 
// seal : 속성을 추가할 수도 재정의 할 수도 없음. 속성 값 읽고 쓰기만 가능
const fruit = { name: 'apple'};
Object.seal(fruit);
// 아래 소스는 적용안됨
fruit.price = 1000;
delete fruit.name;
Object.defineProperty(fruit,'name',{enumerable:false}); 

//읽기 쓰기만 가능
fruit.name = 'banana';
console.log(fruit.name);

// 3. 동결
// freeze : 속성 추가, 재정의, 쓰기 불가. 읽기만 가능
const fruit = { name: 'apple'};
Object.freeze(fruit);
fruit.name = 'banana';
console.log(fruit.name); // apple

8. Mixin

서로다른 객체의 속성들을 뒤섞는 것 ↔ 상속

function mixin(target, source) {
    const keys = Object.keys(source);
    for(let i=0; i<keys.length; i++) {
        let descriptor = Object.getOwnPropertyDescriptor(source, keys[i]);
        Object.defineProperty(target, keys[i], descriptor);
    }
    return target;
}

const fruit = {
    _name: 'apple',
    get name() {
        return this._name;
    }
};
const fruit2= {};
mixin(fruit2, fruit);
fruit2.name = 'banana';
console.log(fruit2.name);
console.log(fruit2);

9. JSON

네트워크를 통해 데이터를 주고받을 떄 사용하는 데이터 포맷

//객체 리터럴
{name : 'apple', price: 1000, months: [9,10,11] };
//JSON 데이터
'{"name":"apple", "price":1000, "month":[9,10,11]}'

// JSON.stringify : js Object -> JSON 
const fruit = {name : 'apple', price: 1000, months: [9,10,11] };
data = JSON.stringfity(fruit);

// JSON.parse : JSON -> js Object
JSON.parse(data); // -> fruit과 동일

// 주의사항
// 1. NaN, Infinity 는 null로 직렬화
// 2. Date 객체는 날짜 문자열로 직렬화
// 3. 함수, 에러, undefined, 심볼은 직렬화 불가
// 4. 열거 가능한 속성만 직렬화

 

 

 


__proto__와 prototype의 차이를 알자.

__proto__는 모든 객체가 가지는 속성으로 원형 객체를 가리킨다. 

prototype은 함수객체만 가지는 속성으로 자신의 prototype 객체를 가리킨다.

함수객체의 prototype도 결국 Object.prototype으로 만들어졌다.

모든 객체의 뿌리에는 Obejct생성자가 존재

댓글