자바스크립트의 객체 리터럴 형식의 객체 생성은 직관적이고, 간편하다. 프로퍼티, 메소드(객체에서 함수형태의 프로퍼티 의미)를 한눈에 알아볼 수 있다.
const obj = {
// Property
radius : 5,
// Method
getDiameter(){
return 2 * this.radius
}
}
객체 리터럴은 하나의 객체만 생성하게된다. 만약 동일한 프로퍼티들을 가진 객체를 또 생성하고 싶다면 객체 리터럴로 객체를 생성해 주어야한다.
const obj = {
// Property
radius : 5,
// Method
getDiameter(){
return 2 * this.radius
}
}
const obj2 = {
radius : 10,
getDiameter(){
return 2 * this.radius
}
}
객체는 프로퍼티를 통해 각각의 고유 상태를 저장한다. obj와 obj2는 각각의 프로퍼티를 가지지만 getDiameter
라는 메소드는 동일한 기능을 하는 어떻게 보면 중복코드가 된다. 객체 리터럴로 위와 같이 일일히 만들어 줄 수 있지만, 만약 이 객체 리터럴을 백개 이상 만든다고 가정하면, 골치아프다.
생성자 함수
라고 한다면, Java, C++와 같이 객체지향 언어에서, 인스턴스를 만들때 인스턴스 변수 및 설정을 초기화 후 인스턴스 참조 주소를 반환하는 역할을 한다. JS에서 생성자 함수도 비슷한 느낌의 의미를 가진다. 생성자 함수를 이용하면, 하나의 생성자 함수로, 여러 객체를 생성할 수 있다.
function Circle(radius){
this.radius = radius;
this.getDiameter = function(){
return 2 * this.radius;
}
}
const c1 = new Circle(10);
const c2 = new Circle(20);
console.log(c1.getDiameter());
console.log(c2.getDiameter());
this
를 잠시 짚고 넘어가자면, this는 함수 호출 방식에 따라 가리키는 값이 다를 수 있다(이를 this binding 이라고 한다).
함수 호출 방식 | this 바인딩 |
---|---|
일반 함수로서 호출 | 전역객체(Browser : Window Node.js : global) |
메소드로서 호출 | 메소드를 호출한 객체 |
생성자 함수로서 호출 | 생성자가 생성할 인스턴스 |
생성자 함수 또한 객체를 생성하는 함수이다. JS에서 생성자 함수는, 일반적인 함수를 정의하듯 함수를 정의하고 new 연산자와 함께 호출하면 해당 함수가 생성자 함수로 작동
한다. 인스턴스가 반환되는 과정은 아래와 같다.
인스턴스 생성과 this 바인딩
인스턴스가 될 빈 객체가 생성된다. 그리고 이 인스턴스는 this
에 바인딩 된다. 이 처리는 Runtime 이전에 실행
된다.
인스턴스 초기화
생성자 함수에 기술된 코드가 한줄씩 실행되어 this에 바인딩 되어있는 인스턴스를 초기화 한다. 즉, this에 바인딩 되어있는 인스턴스에 프로퍼티, 메소드를 추가하고 생성자 함수가 전달받은 매개변수를 통해 인스턴스 프로퍼티에 할당해 초기화를 진행한다.
인스턴스 반환
위 과정이 끝났다면 인스턴스가 바인딩된 this가 암묵적으로 반환된다. 단, 생성자 함수가 다른 객체를 반환하는 코드가 있다면, this가 정상적으로 반환되지 않는다(단 원시값인 경우 this 반환).
함수 또한 객체이다. 그렇기에 객체가 가진 내부 슬롯, 메소드 모두 동일하게 가지고 있다. 다만 일반 객체와 함수 객체는 차이점이 있다. 함수객체는 호출이 가능하다
라는 점에서 차이점이 있다. 그렇기에, 함수객체만을 위한 [[Environment]]
, [[FormalParameters]]
와 같은 내부슬롯과 [[Call]]
,[[Construct]]
와 같은 내부 메소드를 추가로 가지고 있다.
함수 객체를 일반 함수로 호출하면, [[Call]]
이 호출되고, new 연산자로 호출하면 [[Construct]]
가 호출된다. [[Call]]
을 갖는 함수 객체를 callable이라고 하며, 내부 메소드 [[Construct]]
를 갖는 함수 객체를 constructor, 갖지 않는 함수를 non-constructor 이라고 한다. 당연한 이야기지만, non-constructor 객체는 생성자 함수로 호출할 수 없다(new 연산자와 함께 사용이 불가능하다는 뜻이다). 함수에도 여러 형태가 있는데, constructor, non-constructor의 종류는 아래와 같다
자바스크립트에서 함수는 일급 객체
이다.일급 객체
는 아래 조건들을 만족해야한다
function test(){
return 2 + 2;
}
function functionTest(fn){
return function(number) {
return fn() + number;
}
}
const number = functionTest(test);
console.log(number(10));
자바스크립트는 여러 패러다임을 가진다.
자바스크립트는 프로토타입 기반 객체지향
언어이다. 자바스크립트 자체적인 문법으로 클래스가 존재하지만, 이 또한 프로토타입 패턴 기반으로 동작하게 된다.
상속이란 어떤 객체의 프로퍼티 혹은 메소드를 다른 객체가 상속받아 동일하게 사용하는 개념을 의미한다. 하나의 생성자 함수를 만들고, 인스턴스 두개를 생성해보자.
function Circle(radius){
this.radius = radius;
this.getDiameter = function(){
return 2 * this.radius;
}
}
const c1 = new Circle(10);
const c2 = new Circle(20);
console.log(c1.getDiameter === c2.getDiameter); // false
위 코드같은 경우, 각각의 인스턴스가 radius라는 프로퍼티와 getDiameter라는 메소드를 가진다. 그리고 위 코드의 다이어그램을 그리면 아래와 같다.
getDiameter같은 경우, 모든 함수들이 동일하게 가지는 함수이다. 하지만 인스턴스마다 가지고 있으니, 이는 메모리 낭비의 요소가 될 수 있다. 이런 경우 프로토타입 기반의 상속을 구현하여 방지할 수 있다.
function Circle(radius){
this.radius = radius;
}
Circle.prototype.getDiameter = function(){
return 2 * this.radius;
}
const c1 = new Circle(10);
const c2 = new Circle(20);
console.log(c1.getDiameter === c2.getDiameter); // true
위 코드와 같이 구현하면, 아래와 같은 다이어그램으로 변경된다.
Circle 생성자 함수가 생성한 모든 인스턴스는 상위 객체를 하는 Circle의 Circle.prototype의 모든 프로퍼티와 메소드를 상속받는다. 이와 같이 하면, getDiameter메소드는 하나만 생성된다. 각 인스턴스는 독립적인 상태값을 가져야하는 radius만 개별적으로 가지고 있으면 되기에, 메모리 적으로 더 효율적이다.
프로토타입은 특정 객체의 상위 객체 역할을 하는 객체로서, 다른 객체에 공유 메소드, 프로퍼티를 제공한다. 프로퍼티를 상속받은 하위 객체는 상위 객체의 프로퍼티, 메소드를 마음대로 사용할 수 있다. 모든 객체는 [[prototype]]이라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토타입의 참조이다
. 객체가 생성될때 객체 생성 방식에 따라 프로토타입이 결정, [[Prototype]]
에 저장된다. 내부슬롯은, 자바스크립트의 내부 원리이므로 접근이 불가능하지만 __proto__
라는 접근자 프로퍼티를 사용하여 내부슬롯이 가리키는 프로토타입에 간접적으로 접근할 수 있다.
__proto__
접근자 프로퍼티를 통해 자신의 [[Prototype]]
내부 슬롯이 가리키는 프로토타입에 간접적으로 접근할 수 있게되고, constructor프로퍼티를 통해 생성자 함수에 접근, 생성자 함수는 자신의 prototype프로퍼티를 통해 프로토타입에 접근하게 되는것이다.
constructor 프로퍼티
는 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킨다. 한가지 더 알아야할 사실은 __proto__
접근자 프로퍼티는 Object.property의 프로퍼티라는 것이다. 그리고 __proto__
를 코드 내에서 직접 사용하는것은 권장되지 않는다. 아래 사진을 보면, __proto__
는 Object의 프로토타입인것을 알 수 있다.
크롬 브라우저에서 Object객체의 프로토타입을 출력해보면 아래와 같이 나오는것을 알 수 있다.
그리고 새로운 생성자 함수를 만들고, 프로토타입에 메소드를 추가한 뒤 객체를 생성해 보자. 그 다음에 hasOwnProperty
를 통해 객체에 name
이라는 프로퍼티가 있는지 확인해보자
function Person(name){
this.name = name;
}
Person.prototype.introduce = function(){
console.log(`My name is ${this.name}`)
}
const me = new Person("Hoplin");
console.log(me.hasOwnProperty('name'));
위 사진에서 봤듯이 hasOwnProperty()
는 Object의 메소드이다. 어떻게 사용할 수 있는것일까? 우리가 주목해야할것은 인스턴스 객체인 me
의 관점이다
객체 me
의 데이터를 console.dir로 살펴보자. me 객체의 프로토타입은 Person.prototype 다음 Object.prototype을 참조하고있는것을 볼 수 있다. 자바스크립트는 결국 Person.prototype에 존재하지 않기때문에 Object.prototype에 있는 메소드를 호출하는것이다.
위의 개념과 별개지만, 생성자함수 Person
의 관점을 살펴보자. 물론 이 관점에서는 Person이 일반함수로 사용되었을때에 해당한다. Person
의 프로토타입은 Function의 프로토타입
을 가리키고, Function의 프로토타입
은 Object의 프로토타입
를 가리키는것을 볼 수 있다. 이를 통해 프로토타입 계층은 Person - Function - Object
로 되어있는것을 확인할 수 있다. 결론적
한가지 참고할 점은, Object.prototype.__proto__는 null이다.
(프로토타입 체인의 끝이기 때문)
결론적으로 이러한 것을 프로토타입 체인
이라고 하며, 자바스크립트가 객체지향을 구현하는 매커니즘이다. 그리고 위 예제를 다이어그램으로 나타내면 아래와 같이 나타낼 수 있다.
객체지향에서 오버라이딩
이란 상위 클래스의 크래스를 하위클래스에서 재정의하는것을 의미한다. 아래와 같은 코드가 있다고 가정하자.
function Person(name){
this.name = name;
}
Person.prototype.introduce = function(){
console.log(`My name is ${this.name}`)
}
const me = new Person("Hoplin");
me.introduce = function(){
console.log(`Overrided Function`)
}
me.introduce();
실행을 해보면, "Overrided Function"이 출력되는것을 볼 수 있다. 이유는 자바스크립트는 객체의 프로퍼티, 메소드를 시작으로 프로토타입을 검색하게 되는데, 생성자 함수의 프로토타입을 검색하기 전에 introduce 메소드가 발견되어 프로토타입의 introduce를 사용하지 않게 되는것이다. 이와 같이 상속관계에 의해 프로퍼티 혹은 메소드가 가려지는것을 프로퍼티 섀도잉
이라고 부른다
흔히 자바 및 C++에서 클래스 메소드
혹은 클래스 변수
라고 하는것들이 있다. 이들의 공통점은, 인스턴스 선언 없이 클래스를 통해 바로 접근이 가능하다는것이다. 자바스크립트에서도 기본적으로 생성자 함수로 인스턴스를 생성하지 않는다면, 프로토타입 체인을 사용할 수 없다(생성자 함수.prototype을 통해 접근은 가능하다). 생성자 함수 또한 객체이므로, 자신만의 프로퍼티 메소드를 소유할 수 있으며, 이는 인스턴스를 생성하는것과 별개로 사용할 수 있다. 그리고 이를 정적 프로퍼티/메소드라고 부른다.
function Person(name){
this.name = name;
}
Person.prototype.introduce = function(){
console.log(`My name is ${this.name}`)
}
Person.staticProperty = "Static Property";
Person.staticMethod = function(){
console.log("Static method")
}
Person.staticMethod();
console.log(Person.staticProperty);
// Person.introduce() // TypeError: Person.introduce is not a function