Updated:

객체 생성하기

객체의 생성

자바스크립트의 객체는 이름과 값을 한 쌍으로 묶은 집합인다. 이름과 값이 한 쌍을 이룬 것을 프로퍼티라고 하고, 그것의 이름을 프로퍼티 이름 또는 키(key)라고 한다. 값으로는 모든 데이터 타입의 데이터(원시 값, 객체)를 저장할 수 있으며, 함수의 참조를 값으로 가진 프로퍼티는 메서드라는 이름으로 부른다. 자바스크립트로 객체를 생성하려면 다음 세가지 방법을 사용한다.
①객체 리터럴로 생성하는 방법
②생성자로 생성하는 방법
③Object.create로 생성하는 방법

var card1 = { suit: "하트", rank: "A" };  // 객체 리터럴로 생성

function Card2(suit,rank) {
    this.suit = suit;
    this.rank = rank;
}
var card2 = new Card2("하트", "A");       // 생성자로 생성

var card3 = Object.create(Object.prototype,{
    suit: {
        value: "하트",
        writable: true,
        enumerable: true,
        configurable: true
    },
    rank: {
        value: "A",
        writable: true,
        enumerable: true,
        configurable: true
    }
});                                         // Object.create로 생성

프로토타입

생성자 안에서 메서드를 정의하는 방식의 문제점
생성자 안에서 this 뒤에 메서드를 정의하면 그 생성자로 생성한 모든 인스턴스에 똑같은 메서드가 추가된다. 따라서 메서드를 포함한 생성자로 인스턴스를 여러 개 생성하면 같은 작업을 하는 메서드를 인스턴스 개수만큼 생성하게 되며 결과적으로 그만큼의 메모리를 소비하게 된다. 이러한 문제는 프로토타입 객체에 메서드를 정의하는 방식으로 해결할 수 있다.

function Circle(center,radius) {
    this.center = center;
    this.radius = radius;
    this.area = function() {
        return Math.PI*this.radius*this.radius;
    };
    var c1 = new Circle({x:0, y:0}, 2.0);
    var c2 = new Circle({x:0, y:1}, 3.0);
    var c3 = new Circle({x:1, y:0}, 1.0);
}

프로토타입 객체
자바스크립트에서는 함수도 객체이므로 함수 객체가 기본적으로 prototype 프로퍼티를 갖고 있다.

function f() {};
console.log(F.prototype); // Object {}

함수의 prototype 프로퍼티가 가리키는 객체를 그 함수의 프로토타입 객체라고 한다. prototype 프로퍼티는 기본적으로 빈 객체를 가리킨다.


프로토타입 객체의 프로퍼티는 생성자로 생성한 모든 인스턴스에서 그 인스턴스의 프로퍼티처럼 사용할 수 있다.

F.prototype.prop = "prototype value";
var obj = new F();
console.log(obj.prop);  // prototype value

또한 프로토타입 객체의 프로퍼티는 읽기만 가능하고 수정이 불가능하다. 인스턴스의 프로퍼티에 값을 대입했을 때 이름이 같은 프로터티가 있으면 그 프로퍼티에 값을 대입한다. 그렇지 않을 때는 인스턴스에 그 이름으로 프로퍼티를 추가한 후에 값을 대입한다.

obj.prop = "instance value";
console.log(obj.prop);          // insatnce value
console.log(F.prototype.prop);  // prototype value

프로토타입 객체의 프로퍼티를 인스턴스에서 참조할 수 있는 상황을 가리켜 ‘인스턴스가 프로토타입 객체를 상속하고 있다’라고 하며 상속 구조는 ‘프로토타입 체인’이라는 메커니즘을 바탕으로 구현되어 있다.
앞에서 언급한 생성자 안에서 this 뒤에 메서드를 정의할 때 생기는 문제는 생성자의 프로토타입 객체에 메서드를 추가하는 방식으로 해결할 수 있다. 예를 들어 Circle 생성자를 정의할 때 area 메서드를 생성자 Circle의 메서드로 추가했던 코드를 Circle.prototype에 추가하는 코드로 바꾸어 보자.

// Circle 생성자
function Circle(center,radius) {
    this.center = center;
    this.radius = radius;
}
// Circle 생성자릐 prototype 프로퍼티에 area 메서드를 추가
Circle.prototype.area = function() {
    return Math.PI*this.radius*this.radius;
};
// Circle 생성자로 인스턴스를 생성
var c1 = new Circle({x:0, y:0}, 2.0);
var c2 = new Circle({x:0, y:1}, 3.0);
var c3 = new Circle({x:1, y:0}, 1.0);
console.log("넓이 = " + c1.area());   // 넓이 = 12.566370614359172

이 코드의 인스턴스 c1, c2, c3 안에는 area 메서드가 존재하지 않지만 area 메서드를 사용할 수 있다. 메서드 안의 this 또한 생성자로 생성한 인스턴스를 가리킨다. 이처럼 생성자 프로토타입 객체에 메서드를 추가하면 인스턴스에 메서드를 추가하지 않아도 인스턴스가 프로토타입 객체의 메서드를 사용할 수 있으므로 메모리의 낭비를 피할 수 있다.
다음 예제는 공이 canvas 요소의 상하좡 벽에 충돌했을 때 튕겨내는 애니메이션을 시뮬레이션하는 프로그램이다. Ball 생성자를 정의해서 공 객체를 여러개 생성한다. 공의 속도, 색상, 운동의 발생(공의 이동과 벽과 공의 충돌), 그리기 처리를 담당하는 메서드를 Ball 생성자의 프로토타입 객체에 정의해서 모든 공 객체가 공유하도록 한다. 프로토타입 객체의 메서드를 기존의 프로토타입 객체에 추가하는 대신 객체 리터럴로 만들어서 Ball.prototype에 대입한다.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>볼 에니메이션</title>
    <script>
      window.onload =function() {
        // 각종 매개변수
        var NBALL = 200;        // 공의 개수
        var R = 5;              // 공의 반지름
        var TIME_INTERVAL = 33; // 애니메이션의 시간 간격(ms 단위)
        var BACK_ALPHA = 0.3;     // 배경의 Alpha 값
        // canvas의 렌더링 컨텍스트 가져오기
        var canvas = document.getElementById('mycanvas');
        var ctx = canvas.getContext('2d');
        // 벽을 표현하는 객체
        var wall = { left: 0, right: canvas.width, top: 0, bottom: canvas.height };
        // 공 객체를 NBALL개 만들어 배열 balls에 저장
        var balls = [];
        for(var i=0; i<NBALL; i++) {
          balls[i] = new Ball(wall.right/2,wall.bottom/2,R);
          balls[i].setVelocityAsRandom(2,7).setColorAsRandom(50,255);
        }
        // 애니메이션 실행 : TIME_INTERVAL(ms)마다 drawFrame을 실행
        setInterval(drawFrame, TIME_INTERVAL);
        // 애니메이션의 프레임 그리기
        function drawFrame(){
            // 배경을 검은색으로 칠한다
            ctx.fillStyle = 'rgba(0,0,0,'+BACK_ALPHA+')';
            ctx.fillRect(0,0,canvas.width,canvas.height);
            // 공의 위치를 갱신하여 그린다
            for(i=0; i<balls.length; i++) {
              balls[i].move().collisionWall(wall).draw(ctx);
           }
        }
      };
      // Ball 생성자
      function Ball(x,y,r,vx,vy,color) {
        this.x = x;         // 공의 x좌표
        this.y = y;         // 공의 y좌표
        this.r = r;         // 공의 반지름
        this.vx = vx;       // 공 속도의 x 성분
        this.vy = vy;       // 공 속도의 y 성분
        this.color = color; // 공의 색상
      }
      // Ball 생성자의 프로토타입
      Ball.prototype = {
          setVelocityAsRandom: function(vmin,vmax) {
              var v = vmin + Math.random()*(vmax-vmin);
              var t = 2*Math.PI*Math.random();
              this.vx = v*Math.cos(t);
              this.vy = v*Math.sin(t);
              return this;
          },
          // 색상을 임의로 설정
          setColorAsRandom: function(lmin,lmax) {
              var R = Math.floor(lmin+Math.random()*(lmax-lmin));
              var G = Math.floor(lmin+Math.random()*(lmax-lmin));
              var B = Math.floor(lmin+Math.random()*(lmax-lmin));
              this.color = 'rgb('+R+','+G+','+B+')';
              return this;
          },
          // 공 그리기
          draw: function(ctx) {
               ctx.fillStyle = this.color;
               ctx.beginPath();
               ctx.arc(this.x,this.y,this.r,0,2*Math.PI,true);
               ctx.fill();
               return this;
          },
          // 공의 위치를 갱신
          move: function() {
              this.x += this.vx;
              this.y += this.vy;
              return this;
          },
          // 벽과 공의 충돌
          collisionWall: function(wall) {
              if( this.x - this.r < wall.left ) {      // 왼쪽 벽과 충돌했을 때
                  this.x = wall.left + this.r;
                  if( this.vx < 0 ) this.vx *= -1;
              }
              if( this.x + this.r > wall.right ) {     // 오른쪽 벽과 충돌했을 때
                  this.x = wall.right - this.r;
                  if( this.vx > 0 ) this.vx *= -1;
              }
              if( this.y - this.r < wall.top ) {       // 위쪽 벽과 충돌했을 때
                  this.y = wall.top + this.r;
                  if( this.vy < 0 ) this.vx *= -1;
              }
              if( this.y + this.r > wall.bottom ) {    // 아래쪽 벽과 충돌했을 때
                  this.y = wall.bottom - this.r;
                  if( this.vy > 0 ) this.vy *= -1;
              }
              return this;
          }
      };
    </script>
</head>
<body>
    <canvas id="mycanvas" width=640 height=480></canvas>
</body>
</html>

실행 결과는 다음과 같다.


프로토타입 상속

자바스크립트는 프로토타입 상속에 기반을 둔 객체 지향 언어이다. 이 절에서는 프로토타입 상속의 메커니즘을 배운다.

상속

상속(inheritance)이란 일반적으로 특정 객체가 다른 객체로부터 기능을 이어받는 것을 말한다. C나 Java는 클래스를 상속할 수 있지만 자바스크립트에서는 클래스가 아닌 객체를 상속한다. 자바스크립트의 상속은 프로토타입 체인이라고 부르는 객체의 자료 구조로 구현되어 있으며, 프로토타입 상속이라고 부른다.

상속을 하는 이유

상속을 사용하면 이미 정의된 프로퍼티와 메서드의 코드를 재사용할 수 있고 새로운 기능을 추가해서 확장된 객체를 만들 수도 있다. 중복 코드를 작성하지 않아도 되므로 유지 보수성이 높은 프로그램을 만들 수 있다.

프로토타입 체인

내부 프로퍼티[[Prototype]]
모든 객체는 내부 프로퍼티[[Prototype]]을 가지고 있다. 이것은 함수 객체의 prototype 프로퍼티와는 다른 객체이다. ECMAScript 6부터는 __proto__프로퍼티에 [[prototype]]의 값이 저장된다. 현재의 주요 웹 브라우저는 __proto__프로퍼티를 지원한다.

var obj = {};
console.log(obj.__proto__); // Object {}

프로토타입 체인
겍체의 __proto__ 프로퍼티는 그 객체에게 상속을 해 준 부모 객체를 가리킨다. 따라서 객체는 __proto__프로퍼티가 가리키는 부모 객체의 프로퍼티를 사용할 수 있다.

var objA = {
    name: "Tom",
    sayHello: function() { console.log("Hello!" + this.name); }
};
var objB = {
    name: "Huck"
};
objB.__proto__ = objA;
var objC = {};
objC.__proto__ = objB;
objC.sayHello();  // "Hello! Huck"


이처럼 자신이 갖고 있지 않은 프로퍼티를 __proto__프로페티가 가리키는 객체를 차례대로 거슬러 올라가며 검색하는 객체의 연결 고리를 프로토타입 체인이라고 한다. 여기서 객체의 __proto__프로퍼티가 가리키는 객체가 바로 상속을 해 준 객체이며, 이 객체를 그 객체의 프로토타입이라고 한다. 객체는 자신이 가지고 있지 않은 특성(프로퍼티와 메서드)을 프로토타입 객체에 위임(delegate)한다고 할 수 있다.
이처럼 자바스크립트는 프로토타입 체인을 사용하여 객체의 프로퍼티를 다른 객체로 전파한다. 이를 프로토타입 상속이라고한다. 프로토타입 상속을 하는 객체 지향 언어를 가리켜 프로토타입 기반 객체 지향 언어라고 한다.

프로토타입 가져오기
객체의 프로토타입은 Object.getPrototypeOf 메서드로 가져올 수 있다.

function F() {}
var obj = new F();
console.log(Object.getPrototypeOf(obj));  // Object {}

new 연산자의 역할

이 절에서는 new 연산자가 수행하는 내부적인 작업을 설명한다.

function Circle(center,radius) {
    this.center = center;
    this.radius = radius;
}
Circle.prototype.area = function() {
    return Math.PI*this.radius*this.radius;
};
var c = new Circle({x:0, y:0}, 2.0);

new 연산자로 Circle 생성자를 사용하면 내부적으로는 다음과 같은 작업을 수행한다.

var newObj = {}                       //① 빈 객체를 생성한다.<br/>
newObj.__proto__ = Circle.prototype;  //② Circle.prototype을 생성된 객체의 프로토타입으로 설정한다.
Ciecle.apply(newObj, arguments);      //③ Circle 생성자를 실행하고 newObj를 초기화한다.
return newObj;                        //④ 완성된 객체를 결괏값으로 반환한다.

이처럼 생성자를 new연산자로 호출하면 객체의 생성, 프로토타입의 설정, 객체의 초기화를 수행한다.

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

함수를 정의하면 함수 객체는 기본적으로 prototype 프로퍼티를 갖게된다. 그리고 이 prototype 프로퍼티는 프로토타입 객체를 가리키며, 이 프로토타입 객체는 기본적으로 constructor 프로퍼티와 내부 프로퍼티 [[Prototype]](__proto__)을 가지고 있다.

constructor 프로퍼티
constructor 프로퍼티는 함수 객체의 참조를 값으로 갖고 있다.

function F() {};
console.log(F.prototype.constructor); // Function F() {}

생성자와 생성자의 프로토타입 객체는 서로를 참조한다.
내부 프로퍼티[[Prototype]]
함수 객체가 가진 프로토타입 객체의 내부 프로퍼티 [[Prototype]]는 기본적으로 Object.prototype을 가리킨다. 즉, 프로토타입 객체의 프로토타입은 Object.prototype이다.

function F() {};
console.log(F.prototype.__proto__); // Object {} : Object.prototype

이 덕분에 생성자로 생성한 인스턴스가 Object.prototype의 프로퍼티를 사용할 수 있다. 또한 Object.prototype의 프로토타입은 null을 가리킨다.
프로토타입 객체의 교체 및 constructor 프로퍼티
인스턴스 생성 후에 생성자의 프로토타입을 수정하거나 교체한 경우

접근자 프로퍼티

프로퍼티의 속성

프로퍼티가 있는지 확인하기

프로퍼티의 열거

객체 잠그기

Mixin

JSON

ECMAScript 6부터 추가된 객체의 기능

댓글남기기