💙 들어가며
자바스크립트에서도 구현하려 노력하는 객체지향
생성자와 캡슐화, prototype을 통해서
자바스크립트에서의 객체지향에 대해서 배워보자.
✏️ 학습내용 정리
#자바스크립트의 객체지향(5.0버전)
5.0의 과거 방식에서는 객체지향을 어떻게 표현했을까?
자바스크립트에는 클래스라는 키워드가 없어서
함수를 통해서 객체를 만들어서 반환하는 형식으로
객체지향을 구현하기 위해 노력했다고 한다.
#초기화 함수(자바스크립트의 생성자)
그런데 자바스크립트에서는 함수도 데이터이기 때문에 new연산을 통해서 함수객체를 만들 수 있다.
이렇게 되면 함수를 통해서 데이터구조와 함수형태만을 남긴 캡슐을 비슷하게 구현해볼 수 있겠다.
함수안에 기본 골격인 데이터구조와 함수를 가진
초기화된 캡슐(비슷한 것)을 구현함으로써
객체의 형태로 계속해서 생성할 수 있으므로
생성자 역할의 함수를 만든 효과를 낼 수 있게 된다.
💡 생성자
값을 초기화하고 데이터구조를 정의하는 녀석을 생성자로 만든다.
같은 함수라도 생성자는 new를 통해서 호출한다.
(그러면 마치 자바에서 하나의 클래스가 정의된 것처럼 보여질 수 있다.)
이때 this라는 키워드를 사용하여 Exam의 객체안에 있는 데이터를 지칭할 수 있게 되는데,
여기서 일반함수로 불렀을 때의 this와 객체에서의 this가 다른 것임을 잘 기억해야 한다.
기본적으로 모든 함수는 this를 가지고 있고, this는 기본적으로 전역인 window를 가리킨다.
그러나 객체를 생성하는 순간 this는 window가 아니라 객체 자기자신을 가리키는 것이 된다.
💡 생성자로 기능하는 함수
//그냥 함수로서도 가능하지만 생성자로 기능하는 함수 만들기
function f1() {
console.log("f1: ", this);
this.kor = 20;
}
1)
var x1 = f1();
console.log("x1: ", x1);
2)
var x2 = new f1();
console.log("x2: ", x2);
⭕ 출력결과:
1)
함수는 new를 붙이지 않고도 호출할 수 있기 때문에 var x1 = f1();의 형태가 가능하다.
일반 함수에서 this를 사용하는 경우에는 기본적으로 this를 window라고 인식한다.
그리고, 이 경우, f1()함수가 실행된 결과가 없다. (return값이 없다)
따라서 x1이라고 변수 선언만 된거고 대입될 값이 없어서 undefined라고 출력된다.
(console.log("x1: ", x1); //호이스트 되어서 x1만 위에서 정의되었고, 값은 애초에 없음
리턴값이 없기 때문에 undefined가 나온다.)
2)
new를 붙여서 var x2 = new f1(); 과 같이 호출한 경우에는 결과가 다르다.
이 경우에는 x2에 f1의 객체가 들어갔다!
때문에 this는 f1이라는 객체를 가리킨다.
현재 f1 객체는 console.log가 실행되고 this.kor가 데이터로 있는 상태이다.
초기화된 객체를 만들어서 대입한 것이기 때문에 kor:20이라는 객체를 가지게 된다.

그래서 우리는 일반함수로 쓸 것인지 생성자로 쓸 것인지를 정의할 때 결정해야한다.
만약 함수를 생성자로 사용하고 싶다면
(자바의 클래스처럼) 의미에 맞게 대문자로 시작하는 이름을 지어준다. (Exam)
그리고 함수 안에서는 this.을 써줘야지 객체의 지역변수로 인식을 한다.
this를 쓰지 않으면 전역 window의 변수, 함수로 인식하기 때문에 원하는 값이 나오지 않는다.
💡 new연산을 하면 객체를 한다.
function Exam() {
this.kor = 10;
this.eng = 10;
this.math = 20;
this.total = function () {
return kor + eng + math;
}
}
var exam = new Exam();
console.log("total", exam.total());
⭕ 출력결과:
kor, eng, math가 var로 선언된 것이 아니라 this로 선언되었다는 것에 주의해야 한다.
이 경우에는 total에서 그냥 kor, eng, math라고 했을 때 outer의 this를 인식하지 않는다.
total의 kor와 eng, math가 전역 window의 kor, eng, math로 인식이되고
window에 변수가 정의되어 있지 않기 때문에 오류가 난다.

※ 원하는 40의 값을 얻으려면 다음과 같이 정의해야 한다.
function Exam() {
this.kor = 10;
this.eng = 10;
this.math = 20;
this.total = function () {
return this.kor + this.eng + this.math;
}
}
var exam = new Exam();
console.log("total", exam.total());

#생성자 오버로드
생성자가 있으니 인자가 추가된 생성자를 오버로드 할 수도 있겠다.
바로 falsy값과 or연산자(||)를 이용해서 초기화를 하는 것!
falsy를 이용해서 값이 없을 경우에만 초기화를 해주고 아닌 경우에는 그 값이 나오게 하는 방법으로
오버로드와 비슷하게 생성자를 구현하였다.
💡 매개변수로 값을 넣는 경우에만 초기화를 해주자!
function Exam(kor, eng, math) {
this.kor = kor || 0;
this.eng = eng || 0;
this.math = math || 0;
this.total = function () {
return this.kor + this.eng + this.math;
}
this.avg = function () {
return this.total() / 3;
}
}
var exam = new Exam();
console.log("total", exam.total());
console.log("avg", exam.avg());
console.log("-------------------------");
var exam = new Exam(10, 10, 10);
console.log("total", exam.total());
console.log("avg", exam.avg());
⭕ 출력결과:
값을 넣지 않은 경우에는 0으로 초기화가 되었다.

※ 참고로 자바스크립트에서는 정수를 정수로 나눠도 소수점 날아가지 않는다.
실수나 정수나 다 같은 Number임
#constructor()
객체와 동일한 형태의 또 다른 객체를 만들어야 할 경우에는
객체에 constructor함수를 사용할 수 있다.
(new Exam한것과 동일한 것으로 생각하면 된다.)
💡 다음을 출력하면?
console.log(Exam === exam1.constructor);
⭕ 출력결과:
두 개가 모습만 다르지 똑같기 때문에 주소를 비교하는 식에서도 true가 나온다.

그렇다면 constructor를 이용해서 객체를 통한 생성자를 만들어보자.
💡 객체.constructor를 활용해보자
var exam = new Exam();
console.log("total", exam.total());
console.log("avg", exam.avg());
var exam = new Exam(10, 10, 10);
console.log("total", exam.total());
console.log("avg", exam.avg());
var exam1 = new exam.constructor(1, 2, 3);
console.log("total:", exam1.total());
console.log("avg:", exam1.avg());
⭕ 출력결과:
new한 객체에 매개변수를 넣은 것과 똑같은 결과가 나온다.
※ new Exam(1, 2, 3)이나 new exam.constructor(1, 2, 3)이나 똑같은 것!

#캡슐화를 위한 노력
데이터 구조와 함수를 같이 가지고 있으면 캡슐이라고 하는 것을 기억하는지?
이때 속성을 바로 내보이게 되면 캡슐을 깨는 행위라고 했다.
(자바에서 private와 getters/setters로 은닉화하려 노력했던 날들)
자바스크립트에서는 생성자를 사용해서 초기화했을 경우에
아래와 같이 exam.total()이라고 직접 속성을 부를 수 있게 되면서
캡슐이 깨지는 경우가 발생할 수 있다.
🚨 캡슐을 깨고 있다.
function Exam() {
this.kor = 10;
this.eng = 10;
this.math = 20;
this.total = function () {
return this.kor + this.eng + this.math;
}
}
var exam = new Exam();
console
console.log("total", exam.total());
// exam.total이라고 속성을 내보이면 안된다. 캡슐이 깨졌음
5.0에서는 캡슐을 유지하기 위한 방법으로 클로저를 사용해보려고 하였다.
var라는 변수선언을 통해서 함수와 클로저의 지역에서만 사용되게끔 바꿔서 속성을 은닉화하였다.
🚨 클로저를 이용한 캡슐유지 노력
function Exam() {
var kor = 10;
var eng = 10;
var math = 20;
this.total = function () {
return kor + eng + math;
}
}
var exam = new Exam();
console.log("total", exam.total());
console.log(exam.kor);
⭕ 출력결과:
var를 사용함을 통해 exam.kor가 출력되지 않도록 은닉화하였다.

#생성자를 이용한 캡슐의 문제점
하지만 위의 방식의 캡슐은 올바르지 않다.
만약 Exam 객체를 2개 이상 만드는 경우라면 함수까지 객체가 되기 때문에
데이터구조만 다른 형태가 아니라 아예 함수까지 다른 완전히 다른 객체 되기 때문이다.
💡 자바스크립트에서는 함수도 데이터
값을 가지고 있는 데이터는 상태를 가지고 있는 것이기 때문에 개별적으로 만들어져야 한다.
하지만 함수는 값만 달라질 뿐 연산이 달라지지는 않는다. (객체마다 연산이 다 똑같다는 뜻)
그런데 자바스크립트에서는 함수도 데이터이기 때문에 문제가 생긴다.
new Exam으로 생성을 하면 같은 내용의 함수도 데이터처럼 따로 계속 만들어지기 때문이다.
데이터는 각자 가지고 있는게 맞는 모양새이지만,
total과 avg를 구하는 함수는 각자 가지고 있을 이유가 없다.
따라서 객체를 통해 만들었을 경우에는 함수가 다 각각의 주소를 가리키게 된다. (다 다른 함수)
💡 new한 함수의 함수를 비교해보자!
var exam1 = new Exam(1, 1, 1);
var exam2 = new Exam(2, 2, 2);
console.log(exam1.total === exam2.total);
console.log(exam1.constructor === exam2.constructor);
⭕ 출력결과:
객체를 통해 만든 exam1.total과 exam2.total은 다른 주소를 가리키고 있다.
constructor는 Exam이라는 객체 하나의 형식으로 만들어진 것이기 때문에 true!

#프로토타입 기반의 캡슐화
그렇다면 매번 새롭게 만들어지지 않고 하나의 원형을 만들어서 상속하는 방법으로
기준을 만들고 공유를 하면 어떨까?
이때 사용될 수 있는 개념이 prototype이다.
자바스크립트에서 모든 객체는 prototype을 가지고 있다.
💡 프로토타입이란?
프로토타입(Prototype)은 객체 지향 프로그래밍에서 객체 간의 상속을 구현하는 방법 중 하나이다.
모든 객체는 프로토타입을 가지고 있으며, 이 프로토타입은 해당 객체의 기본 속성 및 메서드를 정의한다.
따라서 모든 생성자도 prototype을 가지고 있어서,
prototype을 이용해서 생성자를 공유하는 방식으로 객체를 만들 수 있다.
이때 주의해야 하는 점이 자바처럼 틀, 형식을 물려받는 것이 아니라는 점이다.
몇 번을 강조해서 말하지만 자바스크립트는 모든 것이 다 객체이다.
때문에 형식을 물려받는 것이 아니라 객체를 공유하는 것이라는 점을 명심해야 한다.
개별적으로 가지고 갈 데이터와 같은 것들은 Exam에 그대로 두고
공유할 녀석들 함수같은 것들만 prototype으로 만든다.
그래서 앞으로 만들게 될 생성자는 이렇게 나눌 수 있겠다.
💡 변경될 데이터와 공유될 함수를 나누자.
function Exam(kor, eng, math) {
this.kor = kor || 0;
this.eng = eng || 0;
this.math = math || 0;
}
Exam.prototype = {
total: function () {
return this.kor + this.eng + this.math;
},
avg: function () {
return this.total() / 3;
}
}
var exam5 = new Exam(2, 2, 2);
var exam6 = new Exam(4, 5, 6);
console.log(exam5.total === exam6.total);
⭕ 출력결과:
이제는 객체를 만든 상태에서 함수의 주소를 비교해도
같은 함수객체를 가리키고 있음을 확인할 수 있다.
※ 그리고 꼭 생성자와 prototype에 들어가는 모든 변수와 함수는
this.를 넣어서 전역과 구분을 시켜주어야 한다.

💙 마치며
1.
캡슐을 구현하고 그것을 깨지 않기위해 노력하는 과정에서
언어에 대한 이해도와 숙련도가 올라가는 것 같다.
부단히 노력해야겠다..
2.
자바스크립트에서는 모든 것이 객체라서
생성자마저 객체를 공유하는 식으로 구현된다는 것이 신기했다.
헷갈리지 말고 잘 기억해두어야겠다.
틀을 물려받아서 쓰는 것이 아니라
객체를 만들어서 공유하는 것!!!