객체를 리턴하는 함수 vs 생성자함수 vs 클래스
객체지향을 모르고 걍 코더로써 코딩을 할때 이 세가지의 차이를 잘 몰랐었다.
근데 사실 계속 코드짜면서 굳이? 왜 클래스로 객체를 만들까? 이런 의문은 있었다.
뭐 이걸 공부하기 전까진 동작이되게 코드를 짜는정도 수준이었던것같다.
그래서 이번에 공부하다가 이 세가지의 차이점에 대해서 명확하게 구분하기위해 포스팅을 해보겠다.
일단 세가지 예시코드를 보자~
# 객체를 리턴하는 함수
const makeDog = (name, age) => {
return { name, age };
};
저 함수를 호출하면 객체가 튀어나온다.
console.log(makeDog("무찌", 2)); //{ name: '무찌', age: 2 }
이렇게 함수에다가 적절한 파라미터를 넣고 함수를 호출하면 객체를 만들 수 있다.
# 생성자 함수
const Dog_fuc = function (name, age) {
this.name = name;
this.age = age;
};
console.log(new Dog_fuc("도리", 2)); // Dog_fuc { name: '도리', age: 2 }
이번엔 생성자 함수다.
생성자 함수 Dog_fuc을 new연산자와 함께 호출하면 인스턴스를 얻을 수 있다.
# 클래스
const Dog_cls = class {
constructor(name, age) {
this.name = name;
this.age = age;
}
};
console.log(new Dog_cls("멍멍이", 100)); //Dog_cls { name: '멍멍이', age: 100 }
클래스를 new 연산자와 함께 호출(클래스도 함수임)해서 인스턴스를 만들수도 있다.
이 세가지의 차이점은 무엇일까?
일단 문법적인 차이 이런건 아예 안쓸거다.
1. 일반함수로 객체를 리턴하기 vs 생성자함수로 인스턴스 만들기.
const makeDog = (name, age) => {
return { name, age };
};
console.log(makeDog("무찌", 2)); // { name: '무찌', age: 2 }
const Dog_fuc = function (name, age) {
this.name = name;
this.age = age;
};
console.log(new Dog_fuc("무찌", 2)); //Dog_fuc { name: '무찌', age: 2 }
둘다 똑같은 프로퍼티를 가진 객체를 리턴한다.
이걸 브라우저 콘솔에서 보면..
뭐 둘다 똑같이 생겼다. 하지만 여기서 내부슬롯 [[Prototype]]을 펴보면?
내부슬롯 Prototype의 모습이 다르다?
https://jacobowl.tistory.com/181
생성자 함수, prototype, __proto__, constructor 관계 구조.
# 기본 코드 const Dog = function (name) { this.name = name; }; const dog1 = new Dog("무찌"); Dog라는 생성자 함수와 이 생성자 함수로 새로운 객체를 만들어, 식별자 dog1에 할당했다. 기본적인 생성자함수의 문법
jacobowl.tistory.com
이전에 쓴 포스팅에서 보면,
저 내부슬롯 [[Prototype]]에 접근하려면 __proto__로 접근한다고 했었다.
접근해서 나온 객체는 그 객체를 만든 생성자 함수객체의 prototype프로퍼티고 여기에 바인딩된 객체에 constructor프로퍼티가 이 인스턴스를 만든 생성자 함수객체다. (객체리터럴은 Object생성자함수로 만든게 아니지만..뭐 걍 그렇다고 생각해도 큰 차이없음)
때문에 위 브라우저 콘솔에서
- 객체를 리턴한 함수 : Object생성자 함수.prototype
- Dog_fuc으로부터 만들어진 인스턴스 : Dog_fuc.prototype
으로 각각 내부슬롯 [[Prototype]]에 바인딩되었다.
이 차이를 기능적으로 예시를 들어 써보자면,
const makeDog = (name, age) => {
return { name, age };
};
const Dog_fuc = function (name, age) {
this.name = name;
this.age = age;
};
이 두가지 객체생성방법의 함수에 의해 생성된 객체들에서 고정적인 메서드를 만들어보자.
//객체리턴 함수에 고정적인 메서드 introduce를 만듬
const makeDog = (name, age) => {
return {
name,
age,
introduce() {
console.log(
`안녕? 난 ${this.name}야. 나이는 ${this.age}살이야ㅎ 반가워~`
);
},
};
};
const Dog_fuc = function (name, age) {
this.name = name;
this.age = age;
};
//생성자 함수에 고정적인 메서드 introduce를 프로토타입에 만듬
Dog_fuc.prototype.introduce = function () {
console.log(`안녕? 난 ${this.name}야. 나이는 ${this.age}살이야ㅎ 반가워~`);
};
introduce메서드를
객체리턴함수에는 직접적인 메서드로 넣고,
생성자 함수에서는 prototype프로퍼티에연결된 객체에 프로퍼티로 넣었다.
const dog1_객체리턴 = makeDog("도리", 2);
const dog2_객체리턴 = makeDog("무찌", 3);
const dog1_생성자 = new Dog_fuc("도리", 2);
const dog2_생성자 = new Dog_fuc("무찌", 3);
dog1_객체리턴.introduce();
dog2_객체리턴.introduce();
console.log(dog1_객체리턴); //{ name: '도리', age: 2, introduce: [Function: introduce] }
dog1_생성자.introduce();
dog2_생성자.introduce();
console.log(dog1_생성자); //Dog_fuc { name: '도리', age: 2 }
이렇게 둘다 introduce메서드를 호출해보면 동작이 잘 된다.
코드상으로 봤을때 각 생성된 객체를 console.log로 출력해본걸 보면 객체리턴한 함수에서 나온 객체에는 introduce 프로퍼티에 함수객체가 담겨있다. 우리가 직접적으로 introduce메서드를 담아서 리턴했기 때문에 그렇다.
+ 물론 생성자 함수에도 직접적으로 this.introduce메서드를 넣을 수 있다.
const Dog_fuc = function (name, age) {
this.name = name;
this.age = age;
this.introduce = function () {
console.log(`안녕? 난 ${this.name}야. 나이는 ${this.age}살이야ㅎ 반가워~`);
};
};
근데 우린 내부슬롯 [[Prototype]]의 차이를 알고싶어서 이렇게는 안함.
다시 프로토타입에 넣은 introduce로 생각하고 읽어보자.
암튼~~ 모두 동작이 잘되는데 무슨 차이가 있을까?
아래 코드를 쳐보자.
console.log(dog1_객체리턴.introduce == dog2_객체리턴.introduce); // false
console.log(dog1_생성자.introduce == dog2_생성자.introduce); // true
각각 방법으로 dog1과 dog2라는 객체를 만들었고 이 객체에서는 introduce메서드를 호출 할 수 있다.
메서드는 곧 함수객체인데, 객체리턴방법으로 나온 introduce함수객체들은 각각 dog1, dog2 객체마다 다르다. 때문에 false가 떴다.
그러나 생성자함수의 prototype에 넣은 메서드는 dog1,dog2객체에 있는것(내부슬롯)이 서로 참조하는게 같다. 때문에 true가 뜬거다.
이걸 메모리 그림으로 표현하면..
이렇게 객체리턴함수로 만든 객체에서의 introduce메서드 호출은 기능 자체는 동일하지만 서로 다른 함수객체가 각각 바인딩되어있고 호출도 서로다른 함수를 각각 호출한것이다.
생성자함수에서 만들어진 객체에서의 introduce메서드 호출은 똑같은 함수를 호출한거다. 정확히 하면, 프로토타입체인으로 검색을 하여 Dog_fuc함수객체의 prototype프로퍼티안에 introduce메서드를 호출하게 된거다. 프로토타입체인으로 시간이 더 걸리겠지만, 메모리에 들어있는 함수객체를 새로 생성하지않고 재사용하니 메모리관점에서 Dog_fuc으로 만든 인스턴스(객체)가 더 잘만든거라는거지..
그림에서 메모리 세칸 네칸은 정확하지않다.. 객체는 더 복잡한 구조고 메모리도 많이쓴다.. 시각적으로 메모리효율이 좋다는걸 표현하고싶었음.
1. 생성자함수로 인스턴스 만들기 vs 클래스로 인스턴스 만들기
# 프로토타입 메서드를 만드는 방법
글이 쓰잘데기 없이 길어졌으므로 다시 코드를 가져오면..
const Dog_fuc = function (name, age) {
this.name = name;
this.age = age;
};
const Dog_cls = class {
constructor(name, age) {
this.name = name;
this.age = age;
}
};
일단 클래스는 생성자함수의 문법적 설탕 신테틱슈가 이 글자만 백만번 본것같다.
걍 원하는 기능을 문법적으로 더 편하게, 가독성좋게 표현할수 있게 만들었다고 한거다.
데이터클래스만 쓸거면 생성자함수 Dog_fuc이 더 단순하긴한데.. 뭐 암튼
위에처럼 introduce라는 메서드를 프로토타입으로 넣어보고 싶다면?
- 생성자 함수의 프로토타입 메서드 만들기
const Dog_fuc = function (name, age) {
this.name = name;
this.age = age;
};
Dog_fuc.prototype.introduce = function () {
console.log(`안녕? 난 ${this.name}야. 나이는 ${this.age}살이야ㅎ 반가워~`);
};
이렇게 생성자 함수 외에 문을 한번더 써줘야한다.
그러나 클래스는 생성자 함수 자체에서 프로토타입 메서드를 만들 수 있다.
- 클래스의 프로토타입 메서드 만들기
const Dog_cls = class {
constructor(name, age) {
this.name = name;
this.age = age;
}
introduce() {
console.log(`안녕? 난 ${this.name}야. 나이는 ${this.age}살이야ㅎ 반가워~`);
}
};
저 constructor함수밖에 introduce(){}~로 만들면 인스턴스에는 직접적으로없는 프로토타입 메서드를 찍어낼 수 있다.
const dog1_생성자 = new Dog_fuc("도리", 2);
const dog2_생성자 = new Dog_fuc("무찌", 3);
const dog1_클래스 = new Dog_cls("도리", 2);
const dog2_클래스 = new Dog_cls("무찌", 3);
dog1_생성자.introduce();
dog2_생성자.introduce();
dog1_클래스.introduce();
dog2_클래스.introduce();
다 작동이 잘된다.
console.log(dog1_생성자.introduce == dog2_생성자.introduce); // true
console.log(dog1_클래스.introduce == dog2_클래스.introduce); // true
생성자함수의 프로토타입 메서드로 introduce메서드를 갖다 넣은거라 찍어낸 인스턴스끼리 같은 함수객체를 공유한다.
# 호출방법 실수에 따른 자바스크립트의 자세..
또 내가 생각하는 중요한점은,
const Dog_fuc = function (name, age) {
this.name = name;
this.age = age;
};
console.log(Dog_fuc("무찌", 2)); //undefined
생성자함수는 일반함수로써 호출이 된다. 그럼 결과는 undefined가 된다.
뉴타겟 뭐시기로 조절할수 있긴 한데 이런 옛날문법은 갖다버리고, 직관적으로 보면 저 Dog_fuc함수는 생성자함수로써만 쓰려고 만든 함수다. 만약 저걸 일반함수로 호출해버리면 전역에 name과 age프로퍼티가 선언할당되어버린다.
const Dog_fuc = function (name, age) {
this.name = name;
this.age = age;
};
console.log(Dog_fuc("무찌", 2)); //undefined
console.log(name); // 무찌래..ㅠㅠ
console.log(age); // 2래 ㅠㅠ
우리 무찌를 전역객체에 방생해버린 이런 극악무도한 짓을 해버린거다 ㄷㄷ
정확히는 저 함수를 생성자함수로 호출하면 내부메서드 [[construct]]가 호출되고, 일반함수로 호출하면 [[call]]이 호출된다.
우린 사람이기때문에 실수를 할 수도 있지않은가?
그러나 클래스는 다르다
const Dog_cls = class {
constructor(name, age) {
this.name = name;
this.age = age;
}
};
console.log(Dog_cls("무찌", 2)); // 타입에러 뜸
클래스는 new 연산자와함께 호출하지않으면(생성자로써 호출하지않으면) 타입에러를 터뜨려준다.
때문에 멍청하게 전역객체에 무찌의 이름과 나이를 둥둥 떠다니게 하는 실수를 안할수 있게 해준다.
애초에 우리는 관습적으로 클래스 이름을 대문자로 시작하는 파스칼케이스로 쓰는데 이또한 클래스가 생기기전에 생성자함수로 쓰려고 만든 함수를 일반함수로 호출해버리는걸 예방하기위해 만들어진 관습이다.
# 상속기능
https://jacobowl.tistory.com/185
프로토타입 체인 찾기 재귀 함수
const checkJokbo = (para, count) => { if (typeof para !== "object") { console.log("객체만 써"); return; } else { if (!count || 0) { console.log(`조상님 찾기 시작!`); count = 1; } else { count = count; } if (para.__proto__ == Object.prototype) {
jacobowl.tistory.com
이 포스팅은 내가 임의로 어떤 객체의 프로토타입 체인을 명시적으로 찾는 재귀함수를 만든거다.
const checkJokbo = (para, count) => {
if (typeof para !== "object") {
console.log("객체만 써");
return;
} else {
if (!count || 0) {
console.log(`조상님 찾기 시작!`);
count = 1;
} else {
count = count;
}
if (para.__proto__ == Object.prototype) {
console.log(`${count}촌 : Object`);
console.log("조상님 찾기 끝!");
return;
} else {
console.log(`${count}촌 : ${para.constructor.name}`);
count++;
checkJokbo(para.__proto__, count);
}
}
};
이 함수다.
여깃다가 저 만들어진 인스턴스들을 넣어보면..
checkJokbo(dog1_생성자);
이렇게 프로토타입체인을 찾아준다.
예를들어 dog1_생성자.introduce()로 introduce메서드를 찾으려고하면
1. 본인 객체의 프로퍼티에서 introduce를 찾음
2. 1에서 못찾으면 1촌 위인 Dog_fuc.prototype에 바인딩된 객체에서 introduce를 찾음
3. 3에서 못찾으면 2촌 위인 Object.prototype에 바인딩된 객체에서 introduce를 찾음
뭐 이렇다는거다..
더 작성중..