5 Apr 2019

ECMAScript6) 객체의 모든 것

자바스크립트 객체의 모든것

목차

객체 생성하기
프로토타입 상속
접근자 프로퍼티
프로퍼티의 속성
프로퍼티가 있는지 확인하기
객체의 열거
객체 잠그기
Mixin
JSON
ES6부터 추가된 함수 기능

객체 생성하기

자바스크립트의 객체는 key, value의 한 쌍으로 묶은 집합.

1. 객체 생성

  • key값 (이름)을 프로퍼티라고 하고, value값은 원시값, 객체 등 여러 데이터가 저장된다.
  • 프로퍼티 중, 함수의 참조를 값으로 가지는 프로퍼티를 메써드 라고 한다.
  • 객체는 객체 리터럴로 생성, 생성자를 통해 생성, Object.create 로 생성하는 방법이 있다

      // 객체 리터럴로 생성하는 방법
      var card = {name:"A", property: "1"};
    
      // new + 생성자 이용
      function Card(suit, rank) {
          this.suit = suit;
          this.rank = rank;
      }
      var card = new Card("heart", "A");
    
      // Object.create() 이용
      var card = Object.create(Object.prototype, {
          suit: {
              value: "하트",
              writable: true,
              enumerable: true,
              configurable: true
          },
          rank: {
              value: "A",
              writable: true,
              enumerable: true,
              configurable: true
          }
      });
    

2. 프로토타입

  • 생성자 안에서 메서드를 정의하는 방식이 가지는 문제점!!
    • 생성자 안에서 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 객체에 메서드를 정의하는 방식으로 해결할 수 있다.
  • 자바스크립트의 함수는 일급객체임을 기억하자. 함수자체가 프로퍼티를 가질 수 있다. 기본적으로 함수를 정의하면 prototype 객체가 프로퍼티로 정의되어 있다. prototype 프로퍼티의 기본값은 빈 객체의 참조이다.
  • prototype 객체의 프로퍼티는 생성자로 생성한 모든 인스턴스가 함께 공유한다. (자신의 프로퍼티처럼 사용할 수 있다.)
  • prototype 프로퍼티의 값(prototype 객체)의 프로퍼티는 읽기만 가능하고, 수정이 불가능하다.
  • 프로토타입 객체의 프로퍼티를 인스턴스에서 참조할 수 있는 상황을 인스턴스가 프로토타입 객체를 상속하고 있다 고 표현하고, 상속 구조는 “프로토타입 체인” 이라는 메커니즘을 바탕으로 구현되어 있다.
  • 앞서 언급했던 문제를 prototype 의 메서드로 정의하는 방식을 통해 해결하는 모습을 보자.

      function Circle(center, radius) {
          this.center = center;
          this.radius = radius;
      }
      // Circle 함수의 prototype프로퍼티 (프로토타입 객체) 에 메서드 추가
      Circle.prototype.area = function () {
          return Math.PI * this.radius * this.radius;
      };
    
      // Circle 생성자로 인스턴스 생성
      var c1 = new Cricle({x:0, y:0}, 3.0);
      var c2 = new Cricle({x:0, y:1}, 2.0);
      var c3 = new Cricle({x:1, y:0}, 1.0);
    
  • 이렇게 되면, 인스턴스인 c1, c2, c3 에는 area() 메서드가 존재하지 않지만, area() 메서드를 사용할 수 있다. 메서드의 this 또한 해당 인스턴스를 가리킨다.

프로토타입 상속

자바스크립트는 프로토타입 상속에 기반을 둔 객체 지향언어 이다.

1. 상속?

  • 상속(ingeritance) 이란 일반적으로 특정 객체가 다른 객체로부터 기능을 이어 받는 것을 말한다. Javascript 는 다른 언어와 다르게 클래스를 상속하는 것이 아닌 객체를 상속한다.
  • 자바스크립트의 상속은 프로토타입 체인이라고 부르는 객체의 자료구조로 구현되어 있고, 프로토타입 상속이라고 부른다.
  • ES6 부터는 class 구문이 추가되었다. 생성자의 정의를 더 간결하고 명료하게 표현할 수 있다. (내부 동작은 기존과 같다.)

2. 상속을 하는 이유

  • 이미 정의된 프로퍼티와 메서드의 재사용
  • 새로운 기능을 추가하여 확장된 객체를 생성할 수 있다.
  • 중복적인 코드를 작성하지 않아도 되므로 유지보수성을 높일 수 있다.

3. 프로토타입 체인 prototype chain

  • 내부 프로퍼티 [[prototype]]
  • 모든 객체는 내부 프로퍼티로 [[prototype]] 을 갖고 있다. 이는 prototype 프로퍼티와는 다른 객체이다. 기존에는 읽고 쓰는것이 불가능하였지만, ES6 부터는 __proto__ vmfhvjxldp [[prototype]] 의 값이 저장된다. (모든 브라우저가 지원하는 것은 아니다.)

      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
    
  • objC의 경우 __proto__ 말고는 어떠한 프로퍼티도 갖고 있지 않지만, sayHello() 메서드를 사용할 수 있다.
  • 이는 __proto__ 가 가리키는 __proto__ 를 거슬러 올라가 sayHello 라는 프로퍼티를 찾아내어 실행하기 때문에 가능하다. 즉, objC 의 프로퍼티에 없으므로 __proto__ 가 가리키는 objB 의 프로퍼티를 확인하고, 또 다시 없으므로 objB__proto__ 가 가리키는 objA 의 프로퍼티를 확인하여 sayHello 프로퍼티를 찾고, 사용하게 된다.
  • name 프로퍼티의 경우 갖고있다면 찾아가지 않는다. (프로퍼티를 소유하고 있는 그곳에서 거슬러 올라가는 것을 멈춘다.) 따라서, objC__proto__ 가 가리키는 objB 까지 올라갔더니 name 프로퍼티가 있어서, 이를 사용하고, objA 까지 name을 찾으러 거슬러 올라가지 않는다.
  • 이런 자신이 가지고 있지 않은 프로퍼티를 __proto__ 프로퍼티가 가리키는 객체를 차례로 거슬러 올라가 검색하는 과정을 프로토타입 체인 (prototype chain) 이라고 한다.
  • 여기서, __proto__ 프로퍼티가 가리키는 객체가 상속을 해준 부모객체이고, 이 객체를 기존 객체의 프로토타입 이라고 한다.
  • 실제 프로그래밍을 하는 경우 proto 프로퍼티의 값을 직접 입력해서 상속하지는 않는다. 일반적으로는 다음과 같은 방법으로 상속한다.
    • 생성자로 객체를 생성할 때, 생성자의 prototype 프로퍼티에 추가하는 방법
    • Object.create() 메서드로 상속받을 프로토타입을 지정하여 객체를 생성하는 방법

3. new 연산자의 역할

  • 생성자를 new 연산자로 호출해서 인스턴스를 생성할 때, 내부적으로는
    1. 해당 인스턴스 이름의 빈 객체를 생성

         var newInstance = new Object();
      
         // 위를 실행 했을 때 다음과 같은 일이 일어난다.
         var newObj = {};
      
    2. 생성자.prototype 을 생성된 객체의 프로토타입(__proto__)으로 설정한다. 이때, 생성자.prototype 이 가리키는 값이 객체가 아니라면 Object.prototype을 프로토타입(__proto__)으로 설정한다.

         newObj.__proto__ = Object.prototype;
      
    3. 생성자를 실행하고, 인스턴스를 초기화한다. 이때, this 는 1번에 생성한 객체로 설정하며, 인수는 new 연산자와 함께 사용한 인수를 사용한다.

         Object.apply(newObj, arguments);
      
    4. 완성된 객체를 결과값으로 반환한다.

         return newObj;
      
  • 중요하게 보아야 하는 부분은 2번 부분이다. 생성자의 prototype 프로퍼티 값을 인스턴스의 __proto__ 값으로 대입하는 부분에서, 인스턴스 체인이 정의되고, 생성자로 생성한 모든 인스턴스가 생성자의 프로토타입 객체의 프로퍼티를 사용할 수 있게 된다.
  • 즉, new 연산자로 생성자를 호출하면 객체의 생성, 프로토타입의 설정, 객체의 초기화를 수행한다.

4. prototype 객체의 프로퍼티

  • 함수를 정의하면 함수객체는 기본적으로 prototype 프로퍼티를 갖게 된다. 이 프로퍼티는 프로토타입 객체(기본값은 빈객체)를 가리키고, 이 프로토타입 객체는 기본적으로 constructor 프로퍼티와 내부프로퍼티 __proto__ 를 갖고 있다.
  • constructor 프로퍼티는 함수객체의 참조를 값으로 갖고 있다.

      function F() {};
      console.log(F.prototype.constructor);  // -> Function F() {};
    
  • 생성자의 경우, 생성자의 prototype 프로퍼티가 프로토타입 객체를 가리키고, 이 프로토타입 객체의 constructor 프로퍼티가 생성자를 가리킨다.
  • 생성자로 생성한 인스턴스의 경우, 프로토타입 객체의 참조만 갖고 있고, 생성자와는 직접적인 연관고리는 없다.

    prototype

  • 내부 프로퍼티 [[Prototype]](proto)
  • 기본적으로는 Object.prototype을 가리킨다. (즉, 프로토타입 객체의 프로토타입은 Object.prototype)이다.
  • 또한, Object.prototype 의 프로토타입은 null을 가리킨다.

  • 프로토타입의 교체에서 주의할 부분은 생성자가 가진 prototype 프로퍼티의 값을 프로퍼티만 정의되어 있는 객체로 교체한다면, constructor 프로퍼티가 없기 때문에, 생성자와의 연결고리가 끊기게 된다.
  • 따라서, prototype 프로퍼티를 교체할 경우 constructor 프로퍼티를 가진 객체로 교체하여야한다.

      function Circle(center, radius) {
          this.center = center;
          this.radius = radius;
      }
      Circle.prototye = {
          constructor: Cricle,
          area: function() { return ... }
      }
    
  • 인스턴스를 생성한 이후 프로토타입을 교체하면, 연결고리가 끊겨(인스턴스의 프로퍼티는 인스턴스가 생성되는 시점의 프로토타입에서 상속받기 때문.) 이미 생성된 인스턴스는 프로토타입의 메서드나 프로퍼티를 사용할 수 없게된다. 하지만 인스턴스를 생성한 이후 프로토타입의 프로퍼티를 추가하는 것은 가능하다.

      function Circle(center, radius) {
          this.center = center;
          this.radius = radius;
      }
      c = new Circle({...}, radius);
      Circle.prototye = {
          constructor: Cricle,
          area: function() { return ... }
      }
      c.area();  // -> Uncaught TypeError
    
      // 프로토타입의 프로퍼티로 메서드를 추가하는 경우에는 가능하다
      c1 = new Circle( {...}, radius);
      Circle.prototye.area = function() { return ...};
      c1.area();  // 제대로 동작
    

접근자 프로퍼티

1. 프로퍼티의 종류

  • 객체의 프로퍼티는 데이터 프로퍼티와 접근자 프로퍼티로 나눌 수 있다.
    • 데이터 프로퍼티 : 값을 저장하기 위한 프로퍼티
    • 접근자 프로퍼티 : 값이 없고, 프로퍼티를 읽거나 쓸 때 호출하는 함수를 값 대신 지정할 수 있는 프로퍼티
  • 지금껏 나온 프로퍼티들은 모두 데이터 프로퍼티이다.

2. 접근자 프로퍼티

  • 접근자란 객체 지향 프로그래밍에서, 객체가 가진 프로퍼티값을 객체 바깥에서 읽거나 쓸 수 있도록 제공하는 메서드를 말한다.
  • 객체의 프로퍼티를 외부에서 직접 조작하도록 하는 것은 데이터의 유지/보수성을 해치는 큰 원인이다.
  • 접근자 프로퍼티를 사용하면 데이터를 부적절하게 변경하는 것을 막을 수 있고, 특정 데이터를 외부로부터 숨길 수 있고, 외부에서 읽으려고 시도 시 적절한 값으로 가공해서 넘길 수 있다.
  • 접근자 프로퍼티는 대입할 때와 참조할 때 호출되는 일종의 메서드라고 할 수 있다.
  • 접근자 프로퍼티 하나에 대해, 그 프로퍼티를 읽을 때의 처리를 담당하는 gettersetter 를 정의 할 수 있다.

      var person = {
          // 데이터 프로퍼티 _name 을 정의
          _name: "Tom",
          // _name 프로퍼티의 값을 조정하는 접근자 프로퍼티 name 의 getter, setter 정의
          get name() {
              return this._name;
          },
          set name(value) {
              var str = value.charAt(0).toUpperCase() + value.substring(1);
              this._name = str;
          }
      };
      console.log(person.name);  // Tom
      person.name = "huck";  // 접근자 프로퍼티에 값을 대입
      console.log(person.name);  // huck
    
  • name 이라는 이름의 접근자 프로퍼티를 정의하였다. 접근자 프로퍼티 name 은 데이터 프로퍼티 _name 의 값을 읽고 쓰는 일을 담당한다.
  • 접근자 프로퍼티는 delete 연산자로 삭제할 수 있다.
  • 객체를 생성한 후에 접근자 프로퍼티를 추가하거나, 생성자 안에 접근자 프로퍼티를 정의하려면 Object.defineProperty() 메서드를 사용한다.

3. 데이터의 캡슐화

  • 앞의 예제에서 _name 을 간접적으로 읽고, 쓰기는 하였지만 여전히 직접 읽고 쓸 수도 있다.
  • 즉시실행함수로 클로저를 생성하면, 데이터 프로퍼티를 객체 외부에서 읽고 쓸 수 없도록 숨기고, 접근자 프로퍼티로만 읽고 쓸 수 있도록 만들 수 있다.

      var person = (function () {
          var _name = "Tom";  // 클로저를 생성하여 프라이빗 변수로 만듬.
          return {
              get name() {
                  return _name;
              },
              set name(value) {
                  var str = value.charAt(0).toUpperCase() + value.substring(1);
                  _name = str;
              }
          }
      })();
      console.log(person.name);  // -> Tom
      person.name = "huck";
      console.log(person.name);  // -> huck
    

프로퍼티의 속성

  • 프로퍼티는 프로퍼티의 이름/값이 한쌍을 이룬 것이지만, 이와는 별개로 내부적인 속성을 몇개 더 가지고 있다.
  • 이를 조작하여 쓰기 가능여부, 열거 가능여부, 재정의 가능여부를 수정할 수 있고, 접근자 프로퍼티를 수정할 수 있다.
    • 쓰기 가능 : 프로퍼티에 쓰기가 가능한지의 여부의 속성
    • 열거 가능 : 프로퍼티가 for/in 문이나 Obejct.keys 등의 반복문으로 찾을 수 있는 대상인지의 여부를 정하는 속성
    • 재정의 가능 : 프로퍼티의 내부 속성을 수정할 수 있는지의 여부 (false 인 경우 delete 연산자로 삭제가 불가능하다.)
  • 이런 프로퍼티의 속성은 property descriptor 로 설정할 수 있다. 이는 프로퍼티의 속성을 뜻하는 객체이다.

      {
          value: 프로퍼티 ,
          writable: true/false,
          enumerable: true/false
          configurable: true/false
      }
    

1. 프로퍼티 속성 설정하기

  • Object.defineProperty() 메서드를 활용한다.

    ~~~js var obj = {}; Object.defineProperty(obj, “name”, { value: “Tom”, writable: true, enumerable: false, configurable: true, })

  • 이 메서드를 이용할 때, 3번째 인수로 들어간 프로퍼티 디스크립터의 경우 각 프로퍼티들은 생략이 가능하다. 생략 시 false로 설정된다.
  • 재정의 가능 (configurable) 의 경우 한번 false 로 설정하면 되돌릴 수 없으니 주의하여야 한다.
  • Object.create() 메서드를 통해서 어떠한 객체를 상속받는 객체를 생성함과 동시에 두번째 인수로 프로퍼티 디스크립터를 넘겨 프로퍼티 속성을 정의할 수 있다.

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

  • 객체를 생성한 후 프로퍼티를 추가하거나 제거할 수 있으므로 객체에 포함된 프로퍼티가 동적으로 바뀐다.
  • 또한, 객체는 그 객체가 소유한 프로퍼티와 프로토타입으로 상속받은 프로퍼티를 모두 사용할 수 있으므로 이들을 구분해야 하는 경우가 발생할 수 있다.

1. in 연산자

  • 객체 안에 지명한 프로퍼티가 있는지 검색한다.
  • 객체가 소유한 프로퍼티와 상속받은 프로퍼티 모두가 true 이다.

      var person = {name: "Tom"};
      console.log("name" in person);  // true
      console.log("toString" in person);  // true
    
  • hasOwnProperty() 메서드를 사용하여 프로퍼티가 있는지 확인 할 수 있다.
  • 객체가 소유한 프로퍼티인 경우 true, 상속받은 프로퍼티인 경우 false 를 반환한다.

      var person = {name: "Tom"};
      console.log(person.hasOwnProperty("name"));  // true
      console.log(person.hasOwnProperty("toString"));  // false
    
  • propertyIsEnumerable() 메서드를 사용하면 그 객체가 소유한 (상속 x) 프로퍼티이며, 열거가 가능한 프로퍼티인지를 확인하여 ture로 반환한다

      var person = {name: "tom", age: 17};
      var person1 = Object.create(person);  // person을 상속받는 person1 객체 생성
    
      person1.name = "huck";
    
      console.log(person1.propertyIsEnumerable("name"));  // true
      console.log(person1.propertyIsEnumerable("age"));  // false 상속받은 프로퍼티
      console.log(Object.prototype.propertyIsEnumerable("toString"))  // false 열거할 수 없는 프로퍼티
    

객체의 열거

  • Object.keys() 메서드를 통해 객체가 소유하면서, 열거 가능한 모든 프로퍼티의 이름을 배열로 만들어 반환하게 할 수 있다.

객체 잠그기

객체를 잠구어 수정할 수 없도록 만드는 방법.

  • Object.preventExtension() 메서드를 사용하여 객체의 확장을 막는 방법이 있다.
  • 객체의 확장 가능 속성(extensible) 을 false로 설정하여 프로퍼티를 추가할 수 없도록 할 수 있다.
  • Object.seal() 메서드로 프로퍼티의 추가를 금지하고, 모든 프로퍼티를 재정의 할 수 없도록 밀봉할 수 있다. 프로퍼티의 추가, 삭제, 수정 을 할 수 없고, 값의 읽기와 쓰기만 가능해진다.
  • Object.freeze() 메서드를 통해 프로퍼티의 추가를 금지하고, 모든 프로퍼티의 재정의를 할 수 없도록 만들며, 값의 읽기만 가능해진다. 단, 접근자 프로퍼티가 정의되어 있으면 게터와 세터 함수를 모두 호출할 수 있다.

Mixin

객체를 Mixin(믹스인) 하는 방법. 객체의 코드 상속과는 다른 방식으로 재사용하는 방법

1. Mixin?

  • 믹스인이란 특정 객체에 다른 객체가 가지고 있는 프로퍼티를 붙여넣어 뒤섞는 기법이다.
  • 상속을 사용하지 않는 대신 특정 객체의 프로퍼티를 동적으로 다른 객체에 추가한다.

      function mixin(target, source) {
          for(var property in source) {
              if(source.hasOwnProperty(property)) {
                  target[property] = source[property];
              }
          }
          return target;
      }
    
  • 함수 mixin 은 원본 객체가 소유하면서 열거할 수 있는 프로퍼티 모두를 타깃객체에 복사하여 다시 반환한다.
  • 주의할 점은, 믹스인 시 복사하는 행위는 얕은 복사로, 객체 자체를 복사하는 것이 아니라 프로퍼티의 참조만을 복사하는 행위로, 원본과 사본이 같은 객체를 참조하게 되어 수정시 둘 모두 수정된다.
  • ES6 부터는 Object.assign() 메서드를 사용하면 mixin 작업을 쉽게 할 수 있다.

JSON

다른 프로그램 언어와 데이터 송수신을 간단하게 만드는 데이터 포맷

  • JSON(JavaScript Object Notation) 은 자바스크립트 객체를 문자열로 표현하는 데이터 포맷이다. JSON을 사용하면 객체를 직렬화 할 수 있다.

1. 표기 방법

  • JSON 포맷을 자바스크립트의 객체 리터럴 표현법에 기반을 두고 있다. 즉, JSON 표기법은 자바스크립트 리터럴 표현법의 부분 집합이다.

      {name: "Tom", age: 17, marriage: false, data: [2, 5, null]};
    
  • 이런 자바스크립트 객체 리터럴은 다음의 JSON과 같다.

      '{"name":"Tom", "age":17, "marriage": false, "data": [2, 5, null]}'
    
  • 이처럼 JSON 데이터는 그 전체를 작은 따옴표로 묶은 문자열이다. 객체의 프로퍼티의 이름은 큰 따옴표로 묶은 문자열로 표기한다.

2. JSON 객체의 메서드 (변환, 환원)

JSON.stringify()

  • JSON.stringify() 는 인수로 받은 객체를 JSON 문자열로 변환하여 반환해준다.

      JSON.stringify(value[, replacer[, space]])
    
  • 두번째 인수로 배열을 지정하는 경우, 배열 요소와 이름이 같은 프로퍼티만 걸러진다.
  • 세번째 인수는 들여쓰기의 단위를 결정한다.

      JSON.stringify({x:1, y:2, z:3}, [x, z]);
      // -> {"x": 1, "z": 3}
    
      JSON.stringify({x:1, y:2}, null, '\t');
      // -> {
      //        "x": 1,
      //        "y": 2
      //    }
    
  • JSON.stringify 메서드를 사용할 때 주의할 점.
    • NaN, Infinity, -Infinitynull로 변환된다.
    • Date 객체는 ISO 포맷(2016-10-27T17:13:40+00:00   2016-10-27T17:13:40Z)의 날짜 문자열로 직렬화 된다. (JSON.parse로 자바스크립트 객체로 변환할 시, ISO 포맷 그대로의 문자열로 반환됨.)
    • 객체 자신이 갖고 있는 열거 가능한 프로퍼티만 변환된다.
    • Function, RegExp, Error 의 객체, undefined, Symbol 타입은 변환될 수 없다.

JSON.parse()

  • JSON.parse() 메서드는 인수로 받은 문자열을 자바스크립트 객체로 반환한다.

      JSON.parse(text[, reviver])
    
  • 첫번째 인수는 자바스크립트 객체로 바꾸고자 하는 JSON 문자열.
  • 두번째 인수는 프로퍼티의 키와 값을 인수로 받는 함수를 지정할 수 있다. (리턴 값은 환원될 객체의 프로퍼티 값)

      JSON.parse('{"x": 0, "y": 1}');  // {x: 0, y: 1}
    

ES6부터 추가된 함수 기능

ES6 는 대격변이라고 할수 있다.

1. 프로퍼티의 이름으로 심벌 사용

  • 심벌은 그 어떤 값과도 다른 유일무이한 값이다. 따라서 함수 안에서 심벌을 생성하여 그것을 속성이름으로 사용하고 그 프로퍼티에 값을 할당하면 함수 바깥에서 값을 읽거나 쓸 수 없다. 즉, 명시적으로 객체의 프로퍼티를 숨길 수 있다.
  • 일반적으로 Array, Date 등 기본 생성자의 prototype 에 메서드를 추가해서 확장하는 방법은 권장되지 않는다. 이러한 상황에서도 심볼을 사용한 프로퍼티 이름을 설정하면 타 라이브러리의 프로퍼티 이름과 겹칠 가능성이 없어 안전한 코드가 될 수 있다.
  • 심볼을 메서드의 이름으로 사용하여 Array.prototypeㅇ 에 메서드를 정의하는 예제 (Fisher-yates 셔플 알고리즘)

      Array.prototype[Symbol.for("shuffle")] = function() {
          var a = this;
          var m = a.length, t, i.
          while (m) {
              i = Math.floor(Math.random() * m--);  // m미만의 인덱스 i 를 무작위로 골라 하나씩 삭제
              t = a[m]; a[m] = a[i]; a[i] = t; // a[i]와 a[m]을 교환
          }
          return this;
      };
      var array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
      console.log(array[Symbol.for("shuffle")]());
      // [3, 5, 8, 4, 1, 9, 2, 6, 7, 0]
    

2. 객체 리터럴에 추가된 기능

  • 프로퍼티 정의의 약식 표기

      var prop = 2;
      var obj = {prop};
      console.log(obj);  // -> Object {prop:2}
    

3. 클래스 구문 Class

  • 생성자는 new 연산자와 함께 사용하여 객체를 생성하는(초기화 및 구축) 함수이다. 생성자를 사용하면 이름이 같은 메서드와 프로퍼티를 가진 객체를 효율적으로 찍어낼 수 있다.
  • 이때, 생성자의 프로토타입 객체에 메서드를 추가하면 메모리낭비를 방지할 수 있고, 다른 생성자에 그 메서드를 상속할 수 있다.
생성자를 정의하는 방법
  1. 함수 선언문을 사용

       function Card(suit, rank) {
           this.suit = suit;
           this.rank = rank;
       }
       Card.prototype.show = function() {
           console.log(this.suit + this.rank);
       };
    
  2. 함수 리터럴로 정의

       var Card = function(suit, rank) {
           this.suit = suit;
           this.rank = rank;
       };
       Card.prototype.show = function() {
           console.log(this.suit + this.rank);
       };
    
  3. 클래스 선언문으로 정의

       class Card {
           constructor(suit, rank) {
               this.suit = suit;
               this.rank = rank;
           }
           show() {
               // 이곳에 선언하는 것은
               // prototype 객체에 메서드를 선언하는 것과 같다.
               console.log(this.suit + this.rank);
           }
       }
    
  4. 클래스 표현식으로 정의

       var Card = class {
           constructor(suit, rank) {
               this.suit = suit;
               this.rank = rank;
           }
           show() {
               console.log(this.suit + this.rank);
           }
       }
    
생성자 상속
  • 자바스크립트에서는 생성자가 클래스 역할을 대신한다. 자바스크립트는 생성자 상속 메커니즘 대신 프로토타입 상속 메커니즘을 채택한 언어이다. 하지만 생성자 또한 객체이므로 객체의 프로토타입 상속을 활용하면 생상자 상속을 구현할 수 있다.
  • 상속을 해주는 부모를 super 생성자, 상속을 받는 자식을 sub 생성자로 일컫을 수 있다.

      function Ellipse(a, b) {
          this.a = a;
          this.b = b;
      }
      // 타원 넓이를 계산하는 메서드
      Ellipse.prototype.getArea = function () {
          return Math.PI * this.a * this.b;
      };
      // toString 메서드를 덮어쓴다
      Ellipse.prototype.toString = function () {
          return "Ellipse " + this.a + " " + this.b;
      }
    
      var ellipse = new Ellipse(5,3);
    
  • 이렇게 생성자로 인스턴스를 생성시 프로토타입 체인은 다음과 같다. chain

  • 이를 이용해 인스턴스 ellipse는 자신이 갖고 있지 않은 메서드를 Ellipse.prototype 과 Obejct.prototype에서 상속박아 사용할 수 있다.
  • Circle.prototype 을 Ellipse.prototype 을 프로토타입으로 가지는 객체로 바꾼다.

      console.log(ellipse.getArea());  // 47.123889
      console.log(ellipse.toString());  // Ellipse 5 3
    
  • 생성자의 프로토타입 상속하기

    ~~~js // 생성자 정의 function Circle(r) { this.a = r; this.b = r; } // 인스턴스 생성 var circle = new Circle(2);

    // Circle.prototype 을 Ellipse.prototype 으로 바꾸어 상속 Circle.prototype = Object.create(Ellipse.prototype, { constructor: { configurable: true, enumerable: true, value: Circle, writable: true } });

    // Object.prototype 으로부터의 메서드 toString을 덮어씀(오버라이딩) Circle.prototype.toString = function () { return “Circle “ + this.a + “ “ + this.b; }

    // 새로운 Circle 객체의 인스턴스 생성 circle2 = new Circle(2);

    console.log(circle2.toString()); // Circle 2 2

클래스 구문

ES6 에서 추가된 클래스 구문, 문법이 추가된 것이지, 내부가 클래스구조로 바뀐것은 아니다.
따라서, 자바스크립트 객체의 메커니즘과 프로토타입 상속에 대한 이해가 무조건적으로 필요하다.

  • 클래스 구문의 기본
    • 클래스 선언문

      class Circle {
          // python 의 __init__ 과 같다.
          constructor(center, radius) {
              this.center = center;
              this.radius = radius;
          };
          // prototype method
          area() {
              return Math.PI * this.radius * this.radius;
          };
      }
      

      이를 함수를 통해 생성자로 만드는 과정으로 보여주면 다음과 같다.

      function Circle (center, radius) {
          this.center = center;
          this.radius = radius;
      };
      Circle.prototype.area = function() {
          return Math.PI * this.radius * this.radius;
      };
      

      이 생성자를 생성시, Circle.prototypeconstructor 프로퍼티를 갖고, 그것이 생성자 함수객체 자체를 가리킨다. 이것이 위의 class 구문에서도 드러난것이 보인다.

      클래스선언문은 함수선언문과 다음과 같은 차이를 가진다.

      • 클래스 선언문은 자바스크립트 엔진이 hoisting 하지 않는다.
      • 같은 이름을 가진 클래스 선언문은 선언할 수 없다.
      • 클래스 선언문에 정의한 생성자만 따로 호출할 수 없다.
    • 클래스 표현식

      var Circle = class {
          constructor(center, radius) {
              this.center = center;
              this.radius = radius;
          }
          area() {
              return Math.PI * this.radius * this.radius;
          }
      }
      
  • 클래스 구문에서 접근자 작성하기
    • 게터와 세터 설정

      class Person {
          constructor(name) {
              this.name = name;
          }
          // prototype methods
          // define getter
          get name() {
              return this._name;
          }
          // define setter
          set name(value) {
              this._name = value;
          }
          // method
          sayname() {
              console.log(this.name);
          }
      }
      
    • 여기서 접근자 프로퍼티 name 과 setter 를 통해 생성되는 _nameconstructor가 인자로 받은 name 은 다른것임을 알아야 한다.
    • setter 를 호출시 받은 값을 _name 프로퍼티를 생성하며 대입한다.
    • 여기서 정의한 setter 와 getter 는 Person.prototype 에 정의된다는 것도 기억해야 한다.
  • 정적 메서드 작성하기
    • 클래스 멤버 앞에 static 키워드를 붙여 정의할 수 있다.

      class Person {
          constructor(name) {
              this.name = name;
              Person.personCount++;
          }
          // prototype methods
          // define getter
          get name() {
              return this._name;
          }
          // define setter
          set name(value) {
              this._name = value;
          }
          // method
          sayname() {
              console.log(this.name);
          }
          static count() {
              return Person.personCount
          }
      }
      
      person1 = Person("hcuk");
      console.log(Person.count());  // 1
      person2 = Person("tom");
      console.log(Person.count());  // 2
      
  • 상속으로 생성자 확장하기
    • extends 키워드를 클래스 선언문이나 클래스 표현식에 붙여주면 다른 생성자를 상속받을 수 있다. 상속받은 이후 새로운 메서드를 추가해서 확장할 수 있다. (프로퍼티 속성까지 바꾸는 기능은 지원하지 않는다.)
    • Circle 생성자를 상속받아 move 메서드를 추가하는 Ball 생성자를 정의해보자.

      class Circle {
          constructor(center, radius) {
              this.center = center;
              this.radius = radius;
          }
          area() {
              return Math.PI * this.radius * this.radius;
          }
          toString() {
              return `Circle 중심정 (${this.center.x}, ${this.center.y}) 반지름 = ${this.radius}`;
          }
      }
      
      // Ball 생성자 생성
      class Ball extends Circle {
          move(dx, dy) {
              this.center.x += dx;
              this.center.y += dy;
          }
      }
      
      // Ball 의 인스턴스 생성
      var ball = Ball({x:0, y:0}, 2);
      console.log(ball.toString());  // Circle 중심점 (0, 0), 반지름 = 2
      console.log(ball.area());  // 12.566370
      ball.move(1, 2);
      console.log(ball.toString());  // Circle 중심점 (1, 2), 반지름 = 2
      
  • 부모(super)의 메서드 호출하기
    • 자식 생성자는 부모 생성자의 메서드를 덮어쓸 수 있다. 이때, super 키워드를 활용하면 부모의 메서드를 호출할 수 있다.

      class Ball extends Circle {
          move(dx, dy) {
              this.center.x += dx;
              this.center.y += dy;
          }
          toString() {
              var str = super.toString();
              return str.replace("Circle", "Ball");
          }
      }
      

Tags: