객체 생성하기
프로토타입 상속
접근자 프로퍼티
프로퍼티의 속성
프로퍼티가 있는지 확인하기
객체의 열거
객체 잠그기
Mixin
JSON
ES6부터 추가된 함수 기능
자바스크립트의 객체는 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
}
});
따라서 인스턴스를 여러개 생성하면 같은 작업을 하는 메서드를 생성한 인스턴스 숫자만큼 생성하게 되어 메모리에 낭비가 되고, 성능저하로 이어진다.
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 의 메서드로 정의하는 방식을 통해 해결하는 모습을 보자.
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
또한 해당 인스턴스를 가리킨다.자바스크립트는 프로토타입 상속에 기반을 둔 객체 지향언어 이다.
모든 객체는 내부 프로퍼티로 [[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__
프로퍼티가 가리키는 객체가 상속을 해준 부모객체이고, 이 객체를 기존 객체의 프로토타입 이라고 한다.prototype
프로퍼티에 추가하는 방법Object.create()
메서드로 상속받을 프로토타입을 지정하여 객체를 생성하는 방법해당 인스턴스 이름의 빈 객체를 생성
var newInstance = new Object();
// 위를 실행 했을 때 다음과 같은 일이 일어난다.
var newObj = {};
생성자.prototype
을 생성된 객체의 프로토타입(__proto__
)으로 설정한다. 이때, 생성자.prototype
이 가리키는 값이 객체가 아니라면 Object.prototype
을 프로토타입(__proto__
)으로 설정한다.
newObj.__proto__ = Object.prototype;
생성자를 실행하고, 인스턴스를 초기화한다. 이때, this
는 1번에 생성한 객체로 설정하며, 인수는 new
연산자와 함께 사용한 인수를 사용한다.
Object.apply(newObj, arguments);
완성된 객체를 결과값으로 반환한다.
return newObj;
prototype
프로퍼티 값을 인스턴스의 __proto__
값으로 대입하는 부분에서, 인스턴스 체인이 정의되고, 생성자로 생성한 모든 인스턴스가 생성자의 프로토타입 객체의 프로퍼티를 사용할 수 있게 된다.new
연산자로 생성자를 호출하면 객체의 생성, 프로토타입의 설정, 객체의 초기화를 수행한다.prototype
프로퍼티를 갖게 된다. 이 프로퍼티는 프로토타입 객체(기본값은 빈객체)를 가리키고, 이 프로토타입 객체는 기본적으로 constructor
프로퍼티와 내부프로퍼티 __proto__
를 갖고 있다.constructor
프로퍼티는 함수객체의 참조를 값으로 갖고 있다.
function F() {};
console.log(F.prototype.constructor); // -> Function F() {};
생성자로 생성한 인스턴스의 경우, 프로토타입 객체의 참조만 갖고 있고, 생성자와는 직접적인 연관고리는 없다.
[[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(); // 제대로 동작
접근자 프로퍼티 하나에 대해, 그 프로퍼티를 읽을 때의 처리를 담당하는 getter
와 setter
를 정의 할 수 있다.
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
의 값을 읽고 쓰는 일을 담당한다.Object.defineProperty()
메서드를 사용한다.즉시실행함수로 클로저를 생성하면, 데이터 프로퍼티를 객체 외부에서 읽고 쓸 수 없도록 숨기고, 접근자 프로퍼티로만 읽고 쓸 수 있도록 만들 수 있다.
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
이런 프로퍼티의 속성은 property descriptor 로 설정할 수 있다. 이는 프로퍼티의 속성을 뜻하는 객체이다.
{
value: 프로퍼티 값,
writable: true/false,
enumerable: true/false
configurable: true/false
}
Object.defineProperty() 메서드를 활용한다.
~~~js var obj = {}; Object.defineProperty(obj, “name”, { value: “Tom”, writable: true, enumerable: false, configurable: true, })
false
로 설정된다.객체가 소유한 프로퍼티와 상속받은 프로퍼티 모두가 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()
메서드를 사용하여 객체의 확장을 막는 방법이 있다.Object.seal()
메서드로 프로퍼티의 추가를 금지하고, 모든 프로퍼티를 재정의 할 수 없도록 밀봉할 수 있다. 프로퍼티의 추가, 삭제, 수정 을 할 수 없고, 값의 읽기와 쓰기만 가능해진다.Object.freeze()
메서드를 통해 프로퍼티의 추가를 금지하고, 모든 프로퍼티의 재정의를 할 수 없도록 만들며, 값의 읽기만 가능해진다. 단, 접근자 프로퍼티가 정의되어 있으면 게터와 세터 함수를 모두 호출할 수 있다.객체를 Mixin(믹스인) 하는 방법. 객체의 코드 상속과는 다른 방식으로 재사용하는 방법
상속을 사용하지 않는 대신 특정 객체의 프로퍼티를 동적으로 다른 객체에 추가한다.
function mixin(target, source) {
for(var property in source) {
if(source.hasOwnProperty(property)) {
target[property] = source[property];
}
}
return target;
}
mixin
은 원본 객체가 소유하면서 열거할 수 있는 프로퍼티 모두를 타깃객체에 복사하여 다시 반환한다.Object.assign()
메서드를 사용하면 mixin 작업을 쉽게 할 수 있다.다른 프로그램 언어와 데이터 송수신을 간단하게 만드는 데이터 포맷
JSON 포맷을 자바스크립트의 객체 리터럴 표현법에 기반을 두고 있다. 즉, JSON 표기법은 자바스크립트 리터럴 표현법의 부분 집합이다.
{name: "Tom", age: 17, marriage: false, data: [2, 5, null]};
이런 자바스크립트 객체 리터럴은 다음의 JSON과 같다.
'{"name":"Tom", "age":17, "marriage": false, "data": [2, 5, null]}'
이처럼 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
, -Infinity
는 null
로 변환된다.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.parse('{"x": 0, "y": 1}'); // {x: 0, y: 1}
ES6 는 대격변이라고 할수 있다.
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]
프로퍼티 정의의 약식 표기
var prop = 2;
var obj = {prop};
console.log(obj); // -> Object {prop:2}
함수 선언문을 사용
function Card(suit, rank) {
this.suit = suit;
this.rank = rank;
}
Card.prototype.show = function() {
console.log(this.suit + this.rank);
};
함수 리터럴로 정의
var Card = function(suit, rank) {
this.suit = suit;
this.rank = rank;
};
Card.prototype.show = function() {
console.log(this.suit + this.rank);
};
클래스 선언문으로 정의
class Card {
constructor(suit, rank) {
this.suit = suit;
this.rank = rank;
}
show() {
// 이곳에 선언하는 것은
// prototype 객체에 메서드를 선언하는 것과 같다.
console.log(this.suit + this.rank);
}
}
클래스 표현식으로 정의
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);
이렇게 생성자로 인스턴스를 생성시 프로토타입 체인은 다음과 같다.
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.prototype
은 constructor
프로퍼티를 갖고, 그것이 생성자 함수객체 자체를 가리킨다. 이것이 위의 class 구문에서도 드러난것이 보인다.
클래스선언문은 함수선언문과 다음과 같은 차이를 가진다.
클래스 표현식
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 를 통해 생성되는 _name
과 constructor
가 인자로 받은 name
은 다른것임을 알아야 한다._name
프로퍼티를 생성하며 대입한다.클래스 멤버 앞에 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
키워드를 활용하면 부모의 메서드를 호출할 수 있다.
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");
}
}