이 그림은 모든 것을 설명합니다.
const person = {
name: "Lee",
address: "Seoul"
};
console.log(person); // {name: "Lee", address: "Seoul"}
이름과 주소 속성으로 표현된 객체인 person을 다른 객체와 구별하여 인식할 수 있다.
속성을 통해 여러 개의 값을 하나의 단위로 구성한 복합적인 자료구조를 객체라고 하며, 객체 지향 프로그래밍은
독립적인 객체의 집합으로 프로그램을 표현하려는 패러다임이다.
const circle = {
radius: 5, // 반지름
getDiameter() {
return 2 * this.radius;
},
getPerimeter() {
return 2 * Math.PI * this.radius;
},
getArea() {
return Math.PI * this.radius ** 2;
},
};
console.log(circle);
// {
// radius: 5,
// getDiameter: [Function: getDiameter],
// getPerimeter: [Function: getPerimeter],
// getArea: [Function: getArea]
// }
console.log(circle.getDiameter()); // 10
객체는 상태(state) 데이터와 이를 조작하는 동작을 하나의 논리적인 단위로 묶은 복합적인 자료구조라고 말할 수 있다.
각 객체는 고유의 기능을 가지는 독립적인 부품으로 볼 수 있지만, 고유한 기능을 수행하며 다른 객체와 관계성을 가진다. 다른 객체와 메시지를 주고받거나, 데이터를 처리할 수도 있다. 또는 다른 객체의 상태 데이터나 동작을 상속받아 사용하기도 한다.
상속은 객체지향 프로그래밍의 핵심 개념이다. 어떤 객체의 프로퍼티 또는 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것을 말한다.
상속을 이용하면 인스턴스 사이 불필요한 중복을 제거할 수 있다.
중복 존재하는 상황
function Circle(radius) {
this.radius = radius;
this.getArea = function(){
return Math.PI * this.radius ** 2;
}
}
const circle1 = new Circle(1);
const circle2 = new Circle(2);
// 인스턴스 생성 시 내용이 같은 getArea 메서드를 매번 생성해 메모리를 낭비한다.
상속을 통한 중복 제거 예시
function Circle(radius) {
this.radius = radius;
}
// 생성자 함수 안에 getArea 함수를 정의하는 것이 아니라
// 생성자 함수가 생성한 모든 인스턴스들의 부모 객체 역할을 하는 프로토타입 객체에
// getArea 메서드를 추가하여 모든 인스턴스들이 사용할 수 있도록 한다.
// 프로토타입은 Circle 생성자 함수의 prototype 프로퍼티에 바인딩되어있다. (==Circle.prototype)
Circle.prototype.getArea = function() {
return Math.PI * this.radius ** 2;
// this는 Circle.prototype 을 가리킨다.
}
const circle1 = new Circle(1);
const circle2 = new Circle(2);
console.log(circle1.getArea === circle2.getArea); //true
// radius 만 개별적으로 소유하고, 내용이 동일한 메서드는 상속을 통해 공유하여
// 사용하도록 해야 중복을 제거할 수 있다.
상속은 코드의 재사용이라는 관점에서 매우 유용하다. 생성자 함수가 생성할 모든 인스턴스가 공통적으로 사용할 프로퍼티나 메서드를 프로토타입에 미리 구현해두면 생성자 함수가 생성할 모든 인스턴스는 별도의 구현 없이 상위 객체인 프로토타입의 자산을 공유해 사용할 수 있다.
프로토타입 객체(프로토타입) 이란 객체간 상속을 구현하기 위해 사용된다. 프로토타입은 어떤 객체의 부모 역할을 하는 객체로서 다른 객체에 공유 프로퍼티를 제공한다. 프로토타입을 상속받은 자식은 부모의 프로퍼티를 자유롭게 사용할 수 있다.
모든 객체는
이게 뭔 말이냐면, 예를 들어서
person = {name: "lee"}
console.log(person.__proto__);
// person 객체의 프로토타입인 Object.prototype 에 접근한다.
// person 객체의 [[Prototype]] 내부 슬롯이 가리키는 객체(==Object.prototype)에 접근한다.
위 코드에서 접근자 프로퍼티로 프로토타입에 접근할 수 있었다.
내부 슬롯은 프로퍼티가 아니기 때문에, 직접적으로 접근할 수는 없다. 그러나, __proto__ 접근자 프로퍼티를 통해 간접적으로 그 내부슬롯의 값, 즉 프로토타입에 접근할 수 있었다.
접근자 프로퍼티는 자체적으로 Value 속성은 가지지 않는다고 앞에서 배웠다.
대신, Get, Set 과 같은 접근자 함수 프로퍼티 어트리뷰트로 구성되어 있다고 배웠다.
접근자 프로퍼티를 통해 프로토타입에 접근하면 내부적으로 getter 함수가 호출되고,
새로운 프로토타입을 할당하면 setter 함수가 호출된다.
__proto__ 접근자 프로퍼티는
즉, Object.prototype.__proto__ 를 상속받아 사용하는 것이다.
console.log(person.hasOwnProperty('__proto__')); // false
console.log(Object.getOwnPropertyDescriptor(Object.prototype, '__proto__'));
// {get: f, set: f, enumerable: false, configurable: true}
console.log({}.__proto__ === Object.prototype); // true
프로토타입 체인이 생성되는 것을 방지하기 위해서다.
__proto__ 는 표준이 아니었다. 지금은 표준임. 그래서 직접 사용하는 것을 권장하지 않는다.
대체재로 Object.create()로 상속을 표현할 수 있다.
const subObj = Object.create(superObj);
function Person(name) {
this.name = name;
}
const me = new Person("Lee");
console.log(me.constructor === Person);
const obj = new Object();
console.log(obj.constructor === Object); // true
// 왜 ? obj 는 객체, Object는 생성자 함수. 즉, obj는 Object.prototype의 constructor를 상속받아 쓰고,
// 이는 Object 생성자 함수를 가리키므로 둘은 같다.
const add = new Function("a", "b", "return a+b");
console.log(add.constructor === Function);
Person: 생성자 함수. prototype 프로퍼티는 프로토타입을 가리킨다.
me: 객체. __proto__ 프로퍼티는 프로토타입을 가리킨다.
Person.prototype: constructor를 가지고, 이는 Person 생성자 함수를 가리킨다.
me 객체에는 constructor 프로퍼티가 없다. 그러나, me객체의 프로토타입에 constructor 프로퍼티가 있기 때문에 이를 상속받아 사용할 수 있다. 그리고 이는 Person을 가리킨다.
const obj2 = {};
const add2 = function (a, b) {
return a + b;
};
const arr = [1, 2, 3];
const regexp = /is/gi;
console.log(obj2.constructor === Object); // true
console.log(add2.constructor === Function); // true
Object 생성자 함수에 인수를 전달하지 않거나, undefined or null 을 인수로 전달하면서 호출하면
그리고 객체 리터럴로 생성된 객체는 평가될 때에,
Object 생성자 함수 호출과 객체 리터럴 평가는 둘 다
만든다
또한, 함수 객체의 경우에도 Function 생성자 함수를 호출하여 만든 함수는 전역 함수인 것처럼 스코프를 만들고, 클로저를 만들지 않는다고 한다. 따라서 생성자 함수를 썼냐 안썼냐에 따라 이들은 다른게 맞다.
그러나, console문 찍은 것에서 알 수 있듯이 constructor 프로퍼티를 사용해서 확인해보면 이들은 같다고 나온다.
리터럴 표기법에 의해 생성된 객체도 “상속”을 위해서 프로토타입이 필요하다. 따라서, 리터럴 표기법에 의해 생성된 객체도 가상적인 생성자 함수를 갖게 된다.
프로토타입은 생성자 함수와 더불어 생성이 되며, prototype, constructor 프로퍼티에 의해 연결되어 있기 때문이다.
⇒ 즉, 모든 객체는 생성자 함수와 연결되어 있다,
⇒ 객체 리터럴로 생성된 객체와 Object 함수로 생성된 객체를 구분하지 않아도 무리가 없다.
생성자 함수로서 호출할 수 있는 함수, 즉 constructor 는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성됨
console.log(Person.prototype); // {constructor: f}
function Person(name) {
this.name = name;
}
생성자 함수로서 호출할 수 없는 함수, 즉 non-constructor는 프로토타입이 생성되지 않는다.
⇒ 위 두 방식은 동일한 구조를 갖는다.
Object 생성자 함수의 prototype 프로퍼티는 Object.prototype 을 가리키고,
Object.prototype의 constructor 프로퍼티는 Object 생성자 함수를 가리킨다.
그리고 생성된 객체 obj는 Object.prototype을 프로토타입으로 갖게 되며, 이로써 Object.prototype을 상속받는다.
function Person(name) {
this.name = name;
}
const me = new Person("Lee");
이 경우, 위 코드가 실행 되었을 때, Person 생성자 함수와 그 생성자 함수의 prototype 프로퍼티에 바인딩된 객체 (Person.prototype) 과, 생성된 객체(me) 사이의 연결이 만들어진다.
1,2 번 방식의 Object.prototype 은 다양한 빌트인 메서드 (hasOwnProperty, propertyIsEnumerable) 등 을 가지고 있다.
3번 방식의 사용자 정의 생성자 함수 Person과 더불어 생성된 Person.prototype 의 프로퍼티는 constructor 뿐이다.
이 Person.prototype도 객체이기 때문에, 얘도 자신의 프로토타입을 가진다. 그것은 바로 Object.prototype이다. 따라서, me도 hasOwnProperty 와 같은 빌트인 메서드를 사용할 수 있다.
const me = new Person("Lee");
console.log(me.hasOwnProperty("name")); // true
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
console.log(`Hi! My name is ${this.name}`);
};
const me = new Person("Lee");
const you = new Person("Kim");
me.sayHello();
you.sayHello();
// Hi! My name is Lee
// Hi! My name is Kim
sayHello 메서드는 me와 you에 생기는 것이 아니라, Person.prototype 에 생김
function Person(name){
this.name = name;
}
Person.prototype.sayHello = funtion(){
console.log(`Hi! My name is ${this.name}`);
};
const me = new Person("Lee");
console.log(me.hasOwnProperty("name")); // true
위에서 썼던 코드를 그대로 가져와보자.
me.hasOwnProperty(”name”) 에서, 이 메서드를 호출할 때 자바스크립트 엔진은 다음과 같은 과정을 거친다.
프로토타입 체인의 최상위 객체는 언제나 Object.prototype 이다.
따라서, 모든 객체는 Object.prototype을 상속받는다.
Object.prototype을 프로토타입 체인의 종점
Object.prototype의 프로토타입, 즉 [[Prototype]] 내부 슬롯의 값은 null 이다.
최상위 객체에서도 프로퍼티를 검색할 수 없을 때에는 undefined를 반환한다. 이 때, 에러는 발생하지 않는다.
결론:
프로퍼티가 아닌 식별자 (ex. me) 는 스코프 체인에서 검색한다.
자바스크립트 엔진은 함수의 중첩 관계로 이루어진 스코프의 계층적 구조에서 식별자를 검색한다.
me.hasOwnProperty(”name”) 에서 스코프 체인에서 me 식별자를 검색한다. me는 전역에서 선언되었으므로, 전역 스코프에서 검색된다. 그 다음, me 객체의 프로토타입 체인에서 hasOwnProperty 메서드를 검색한다.
따라서, 스코프체인과 프로토타입 체인은 별도로 연관없이 동작하는게 아니라, 서로 협력하여 프로퍼티와 식별자를 찾는데 사용된다.
const Person = (function () {
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
console.log(`Hi My name is ${this.name}`);
};
return Person;
})();
const me = new Person("Lee");
me.sayHello = function () {
console.log(`Hey, My name is ${this.name}`);
};
me.sayHello();
프로토타입이 소유한 프로퍼티(메소드) ⇒ 프로토타입 프로퍼티(메서드)
인스턴스가 소유한 프로퍼티(메서드) ⇒ 인스턴스 프로퍼티(메서드)
프로토타입 메서드(프로퍼티)와 같은 이름의 메서드(프로퍼티) 를 인스턴스에 추가하면 (위와 같은 상황)
프로토타입 체인을 따라 프로토타입 프로퍼티를 검색하여 프로토타입 프로퍼티를 덮어쓰는 것이 아니라
인스턴스 프로퍼티로 추가한다.
이 때, 인스턴스 메서드 sayHello 는 프로토타입 메서드
오버라이딩: 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의 하여 사용하는 방식
+) 오버로딩: 함수의 이름은 동일하지만, 매개변수의 타입 또는 개수가 다른 메서드를 구현하고, 매개변수에 의해 메서드를 구별하여 호출하는 방식. 자바스크립트는 오버로딩을 지원하지는 않지만, arguments 객체를 사용해 구현할 수는 있다.
프로토타입 프로퍼티를 변경 및 삭제하려면 하위 객체를 통해 프로토타입 체인으로 접근하는 것이 아니라, 프로토타입에 직접 접근해야 한다.
Person.prototype.sayHello = function () {
console.log(`Hi My name is ${this.name});
}
me.sayHello();
delete Person.prototype.sayHello;
me.sayHello(); // TypeError: me.sayHello is not a function
1번과 2번의 별다른 차이가 없어 보이지만 미묘한 차이가 있다.
1번 생성자 함수에 의한 교체 방식은 Person 생성자 함수의 prototype 프로퍼티가 교체된 프로토타입을 여전히 가리키고 있다는 것이고,
2번 인스턴스에 의한 프로토타입 교체 방식은 Person 생성자 함수의 prototype 프로퍼티가 교체된 프로토타입을 가리키지 않는다는 것이다.
즉, 1번과 2번 모두 교체된 프로토타입이 constructor 이 없어서 constructor와 생성자 함수간의 연결이 끊겼다는 것은 같으나,
2번 방식은 생성자 함수의 prototype 프로퍼티와 프로토타입 사이 연결도 끊긴 것이다.
constructor 추가하고, prototype 프로퍼티 재설정해서 연결을 되살려보자.
const parent = {
constructor: Person,
sayHello () {
console.log(`Hi, My name is ${this.name});
}
};
Person.prototype = parent;
Object.setPrototypeOf(me, parent);
me.sayHello();
console.log(me.constructor === Person); // true // constructor 추가해서 연결 완료
console.log(me.constructor === Object); // false
console.log(Person.prototype === Object.getPrototypeOf(me)); // true. 연결 완료
결론: 매우 번거롭기 때문에, 프로토타입은 직접 교체하지 않는 것이 좋다. 상속 관계를 인위적으로 설정하려면 “직접 상속” 방식이 가장 편리하다. 또는, 클래스를 사용하는 것이 가장 바람직하다.
instanceof 연산자는 좌변 객체가 우변 생성자 함수와의 관계를 판단한다.
단순히 생성자 함수가 생성한 객체이냐 를 판단하는 것이 아니라,
생성자 함수의 prototype 프로퍼티에 바인딩된 객체, 즉 프로토타입이 좌변의 객체의 프로토타입 체인 상에 존재하는 지를 판단한다.
// 프로토타입 교체 후
console.log(me instanceof Person); // false
console.log(me instanceof Object); // true
Person.prototype = parent;
// 연결해줬음
console.log(me instanceof Person); // true
console.log(me instanceof Object); // true
// 둘 다 프로토타입 체인상에 존재하므로
따라서, 위에서 다뤘던 프로토타입 교체 방식 중에
생성자 함수에 의해 교체했을 때는, constructor 프로퍼티만 없어졌기 때문에 instanceof 에는 영향을 안준다.
여전히 prototype 프로퍼티는 Person.prototype 을 가리키고 있고, 이는 프로토타입 체인 상에 존재하기 때문이다.
그러나, 인스턴스에 의해 교체했을 때에는, constructor 프로퍼티가 없어졌을 뿐만 아니라 prototype 프로퍼티의 연결 자체도 끊기기 때문에 instanceof에 영향을 준다.
생성자 함수로 인스턴스를 생성하지 않아도, 참조/호출 할 수 있는 프로퍼티/메서드 이다.
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
console.log(`Hi my name is ${this.name}`);
};
Person.staticProp = "staticProp";
Person.staticMethod = function () {
console.log("staticMethod");
};
const me = new Person("Lee");
Person.staticMethod(); // staticMethod
me.staticMethod(); // TypeError
staticMethod 와 staticProp은 인스턴스를 만들지 않고도 생성자함수로 젒근이 가능하다.
생성자 함수도 하나의 객체이기 때문에 자기만의 프로퍼티를 가지는 것이다.
prototype 프로퍼티 말고, staticProp과 staticMethod 갖게된다.
this를 참조하지 않는 프로토타입 메서드는 정적 메서드로 변경해도 된다.
그러면 인스턴스 만들지 않고도 호출할 수 있게 된다 !