본문 바로가기
Javascript

프로토타입

by jennyiscoding 2022. 10. 2.

1. 프로토타입이란?

어떤 제품을 만드는 과정에 시험용으로 미리 만들어본 물건이다

자바스크립트는 프로토타입을 기반으로 상속을 구현한 언어이다. 

 

빈 {}를 콘솔창에 찍어보면 어떤 Prototype을 상속받았는지 볼수있다

 

빈 []을 콘솔창에 찍어보자. 

엔진에서 자동적으로 new Array()를 쳐준것이다. 밑쪽에 보면은 밑에 [[Prototype]] : Object가 하나 더있다. 

또 펼쳐 보면은, 객체가 또 부모로 있는데, Object가 최상위 객체임을 알수있다. 

모든 객체의 상속이 결국에는 올라가는 구조로 되어있다.(이것을 다중상속, 즉 프로토타입 체인이라고 한다)

 

오토박싱 : 원시타입 객체를 객체타입으로 바꿔주는것.

name이라는 변수를 선언했다. name을 쳤더니 그냥 'hello'만 나온다. 

그렇다면, name. 을하면은 아무것도 안나오는게 정상이다. 

근데 실제로는 . 찍으면 기능들이 많이 나타난다. 

예를들어 name.concate("Lu") ==> 이때만 잠깐 원시 -> 객체타입으로 바꿔준다. 

. 점 찍는순간! 

 

String이라고하는 생성자함수를 name이 상속받고있다. 그렇기때문에 charAt같은 내부연산자를 쓸 수 있는 것이다. 

근데 name = null; 을 대입하면 아예 주소값을 없애버린다. 

 

2. prototype. __proto__

함수객체는 일반객체와 달리 prototype프로퍼티가 추가로 있다. 

모든 일반 함수는 prototype을 가지고 있어서 모든 함수는 생성자함수를 통해 객체를 생성할수있다. 

예)

function machine(){
  this.bolt = "strike";
  this.nut = "snowball";
}
var nunu = new machine()

nunu라는 오브젝트가 만들어진다. machine은 오브젝트 복사 기계인 것이다.

이런식으로 자식에게 물려줄수도 있지만 또 다른 방법으로 프로토타입이라는것을 써도 자식 object에게 데이터를 물려줄수있다

 

machine이라는 함수가 만들어지는데 machine.prototype이라는 비밀공간도 같이 생긴다.

프로토타입은? 그냥 유전자라고 이해를 하자. 

machine.prototype.name = "morae"라고 선언을 했다. 

그냥 유전자에 name이라는 공간에 morae를 저장한거다.

자식도 전부다 가져다 쓸수있다. 

nunu를 쳐봐도 name:"kim"이 나오지는 않는다. 

 

자식에게 변수를 넘겨주는 2가지 방법을 보았는데, 이 두가지 방법의 차이점은

첫번째로, 직접 변수를 넘겨주는 경우

자식이 객체를 직접 가지고있다. 

하지만 prototype으로 넘겨준다면 

부모만 가지고있게 된다. 자식이 그냥 끌어다 쓴다. (출처 : 코딩애플 유튜브)

 

즉, 이 prototype을 통해서 constructor를 만들고 생성자가 객체를 만들고 new를 통해서 생성자객체를 생성을 해서 인스턴스는 생성자객체의 상속을 받는다. 

 

즉, 생성자함수를 객체로 만든것을 프로토타입이라고 한다. 

그 객체 안에 constructor라는 생성자가 있고 new를 통하면은 생성자함수로 호출할수가 있다. 

그 이유가 constructor가 내재되어있기때문에 new에서 작동을 한거다. 

constructor는 생성자함수로 만든 객체를 바인딩한것이다. 

 

obj3는 Person으로 객체를 만든 인스턴스.  __proto__를 통해서 부모객체에 접근할 수 있다. 

 

다시, __proto__와 prototype의 차이를 보면

__proto__ : Object가 가지고있다. 모든 객체들이 상속받았기 때문에 모두가 가지고있다 

get, set접근자 프로퍼티로 만들어져있다 

Object내부에서 만들어졌기 때문에 get, set을 통해 상위객체로 간편하게 객체를 꺼내오고 넣을 수 있다. 

Object로 만든 생성자함수의 부모를 가져온다

예제로 보자. 

function Person(name){
  this.name = name;
}
let foo = new Person('Lee')

1) 그냥 Person(= foo.constructor)

2) Person.prototype(= foo.__proto__)

3) Person.prototype.constructor(= foo.__proto__.constructor)

4) Person.constructor(= Function)

 

3. 생성자 함수에만 prototype가 있다. 

console.log(function () {}.hasOwnProperty('prototype')); //true(생성자함수에만 prototype가 있다)
function arrowtwo(a,b){
  return a+b;
}
console.log(arrowtwo.hasOwnProperty('prototype')); //true

4. 일반객체는 prototype가 없다

console.log({}.hasOwnProperty('prototype')); //false(일반객체는 prototype가 없다)

5. arrow함수에는 prototype가 없다

let arrow = (a, b) => a + b;
console.log(arrow.hasOwnProperty('prototype')); //false(arrow함수는 생성자함수가 아니기때문에 prototype을 갖고있지않다)

6. 일반함수메서드에만 prototype이 있다

class Animal {
	constructor(name, sound) { //이부분은 prototype에있는 constructor를 쓰는것과 똑같다.
		this.name = name;
		this.sound = sound;
		this.sayHello = function () { //ES5버전 일반함수 메서드
			console.log(`안녕`);
		};
	}
	makeSounds() { //ES6버전 메서드 축약방식(prototype을 가지지 않는다)
		console.log(`${this.sound} 소리를 내다`);
	}
}
const cat = new Animal('나비', '미아옹');

console.log(cat.hasOwnProperty('prototype')); // false
console.log(cat.makeSounds.hasOwnProperty('prototype'));// false (메서드로는 생성자함수 쓸 수 없다)
console.log(cat.sayHello.hasOwnProperty('prototype')); // true(일반함수메서드에만 prototype이 있다)

console.log(cat.__proto__) // 결과로 {}가 나오는데, 우리가 object를 상속받았다는 의미이다

7. 객체에 __proto__가 있는게 아니라 객체가 Object를 상속받았기 때문에 __proto__가 있는 것이다. 

const obj = {};

console.log(obj.hasOwnProperty('__proto__')); //false가 나온다. obj가 __proto__를 갖고있는게 아니라 부모객체인 Object에 접근해서 __proto__가져왔던것.
console.log(obj.__proto__.hasOwnProperty('__proto__')); //true가 나온다. __proto__를 통해 Object객체로 접근했으니까.

8. 

function Person(name) {
	this.name = name;
}
const me = new Person('박연미');
console.log(me.constructor === Person); //true
console.log(me.__proto__); // {constructor: ƒ}

그림으로 보면,

내가 그린기린그림

요약 : 

자바스크립트는 특정한 객체의 프로퍼티나 메서드에 접근할려고 할 때 해당 객체의 프로퍼티 메서드가 없으면 프로토타입이라고하는 애가 있어서 그거를 링크로 가서 부모를 참조를 해서 거기에 있는 프로퍼티나  메소드를 찾아서 검색을 한다. 이거를 프로토타입체인이라고 한다. 

 

{}의 부모는 Object이다. 그러므로 아래와 같다. 

let lee = {
	name: 'Lee',
	score: 90,
};
console.log(lee);
console.log(lee.hasOwnProperty('name')); // name값이 있으니까 true
console.log(lee.__proto__ === Object.prototype); // 
console.log(Object.prototype.hasOwnProperty('hasOwnProperty')); //lee한테 없으니까 상위객체로 가서 hasOwnProperty를 찾은것이다

 

9. Object안에 내부 함수 중에 keys, values, entries 객체를 가져올 수 있는 메소드가 있다

//static메서드라서 인스턴스를 만들지 않고 클래스 이름으로 접근할수 있는 요소들.
const dog = { name: '바둑이', age: 3 };
console.log(Object.keys(dog));
console.log(Object.values(dog));
console.log(Object.entries(dog));

10. 프로퍼티가 있는지 없는지 알아보는 방법

//프로퍼티가 있니 없니?
const dog = { name: '바둑이', age: 3 };
console.log('name' in dog); //true 있다
console.log(dog.hasOwnProperty('name'));//true 있다

11. getOwnPropertyDescriptors를 써서 Object안에의 상태정보를 descriptors라는 객체로 받아올수있다

(끝에 s가 붙으면 모든 프로퍼티에 대한 정보를 보는 옵션)

const dog = { name: '바둑이', age: 3 };

const descriptors = Object.getOwnPropertyDescriptors(dog);//Object안에의 상태정보를 descriptors라는 객체로 받아올수있다
console.log(descriptors);

결과

**프로퍼티 어트리뷰트(데이터 프로퍼티)

value : 넣어준 value값

writable : value를 수정할 수 있는지 없는지

enumerable : for in이라던가 Object키를 통해 값들을 열거할 수 있는지 없는지 false면은 속성을 보여주지 않음

configurable : 앞의 설정값들을 수정할 수 있는지 없는지. 

 

11.2. getOwnPropertyDescriptor를 통해 어트리뷰터 하나의 속성값만 보기

const dog = { name: '바둑이', age: 3 };
const desc = Object.getOwnPropertyDescriptor(dog, 'name');
console.log(desc);

12. defineProperty를 통해 Object의 상태정보를 바꿔보고 적용해보자. 

const dog = { name: '바둑이', age: 3 };

const descriptors = Object.getOwnPropertyDescriptors(dog);//Object안에의 상태정보를 descriptors라는 객체로 받아올수있다
console.log(descriptors);

const desc = Object.getOwnPropertyDescriptor(dog, 'name');
console.log(desc);

Object.defineProperty(dog, 'name', {
	value: '멍멍',
	writable: false,
	enumerable: false,
	configurable: false,
});
dog.name = '왈왈'; //writable을 false로 바꿨기때문에 왈왈로 변경이 되지 않는다. 
console.log(dog.name); //멍멍
console.log(Object.keys(dog)); //enumerable를 false로 바꿨기때문에 name은 보이지 않고 age만 보인다 

delete dog.name; //configurable를 false로 했기때문에 삭제도 되지 않는다. 
console.log(dog.name);

 

13. defineProperties을 사용하여 객체를 안에있는 속성값을 추가, 조작해보자 

const teacher = {};

Object.defineProperties(teacher, { //defineProperties로 객체를 안에있는 속성값을 추가해서 만든다. 
	firstName: {
		value: '연미',
		writable: true,
		enumerable: true,
		configurable: true,
	},
	lastName: {
		value: '박',
		writable: true,
		enumerable: true,
		configurable: true,
	},
	fullName: { //은닉화. 
		get() {
			return `${this.lastName} ${this.firstName}`;
		},
		set(name) {
			this.lastName = name[0];
			this.firstName = name.slice(1);
		},
		configurable: true,
		// enumerable: true, //이거를 주석처리하면 fullName함수가 보이지 않는다.
	},
});
console.log('--------------');
console.log(teacher);  

console.log(teacher.fullName); //fullName의 get함수를 호출한것
teacher.fullName = '오은영'; //fullName의 set이 실행이 되고 안에있는 lastName, firstName을 수정한다.
console.log(teacher); //그 후에 teacher을 실행했더니 오 은영으로 바뀐다.

14. 이제 현대방법으로 바꿔보자. 

writable : false대신 freeze를 사용

//속성값 알아보기
const yeonmi = { name: '박연미' };
const dog = { name: '바둑이', age: 5, owner: yeonmi };
Object.freeze(dog); //freeze는 사실 writable을 false로 만드는 것이다. 
dog.name = '누렁이';
console.log(dog); //누렁이로 바뀌지 않고 여전히 바둑이!

freeze를 하면 수정도 되지 않고 삭제도 되지 않는다. 그러나 yeonmi라는 객체는 변경이 가능하다. 주소값을 저장해둔것 뿐이니까. 객체는 얕은 복사가 된다. 

//속성값 알아보기
const yeonmi = { name: '박연미' };
const dog = { name: '바둑이', age: 5, owner: yeonmi };
Object.freeze(dog); //freeze는 사실 writable을 false로 만드는 것이다. 

dog.play = function () {
	console.log('뛰어놀기 ');
}; //추가도 되지 않음
console.log(dog);
delete dog.name;  //삭제도 되지 않음
console.log(dog);
yeonmi.name = '아이유'; //owner부분의 yeonmi는 yeonmi의 주소값을 갖고있는것이다. 얕은복사가 된다! 
console.log(dog);

15. Object를 복제한 다음 복제한 객체의 수정은 가능하다. 

//속성값 알아보기
const yeonmi = { name: '박연미' };
const dog = { name: '바둑이', age: 5, owner: yeonmi };
Object.freeze(dog); //freeze는 사실 writable을 false로 만드는 것이다. 

const cat = { ...dog }; //객체의 깊은복사. 스프레드 연산자를 이용해 cat이라는 새로운 방을 만들었다(복제함!)
//Object.assign(cat, dog); //위와 같은 의미. 
cat.owner = '유재석';
console.log(dog);

16. seal을 이용한 밀봉 후 데이터수정, 삭제, 추가, 속성재정의 

seal의 의미 : 

 writable : true, //수정만 가능
 enumerable : true, 
 configurable : false //삭제, 추가, 속성재정의는 불가능

const yeonmi = { name: '박연미' };
const dog = { name: '바둑이', age: 5, owner: yeonmi };
Object.freeze(dog); //freeze는 사실 writable을 false로 만드는 것이다. 

const cat = { ...dog };
Object.seal(cat); //seal:밀봉. 

cat.name = '나비'; //변경은 가능
console.log(cat);
delete cat.owner; //삭제는 불가능
console.log(cat);

17. preventExtensions로 확장을 막는다. 

const rabbit = { name: '토순이' };
Object.preventExtensions(rabbit); //확장을 막는다. 이름을 바꿀수는 있지만 새로운 요소를 추가할수는 없다. 

rabbit.name = '토깽이';
console.log(rabbit);
rabbit.age = 1; //age를 넣을 수 없다. 
console.log(rabbit);
delete rabbit.name; //삭제는 가능함 
console.log(rabbit);

 

18. isFrozen, isSealed, isExtensible를 통해 현재 상태를 볼수도 있다. 

const dog = { name: '바둑이', age: 5};
Object.freeze(dog); //freeze는 사실 writable을 false로 만드는 것이다. 

const cat = { ...dog };
Object.seal(cat); //seal:밀봉. 

const rabbit = { name: '토순이' };
Object.preventExtensions(rabbit); //확장을 막는다. 이름을 바꿀수는 있지만 새로운 요소를 추가할수는 없다. 

console.log(Object.isFrozen(dog)); //true
console.log(Object.isSealed(cat)); //true
console.log(Object.isExtensible(rabbit)); //false

19. 생성자함수의 상속과 활용

// 프로토타입을 베이스로한 객체지향 프로그래밍
function Person(name, age) { //생성자함수
	this.name = name;
	this.age = age;
}

Person.prototype.eat = function () { //Person이라는 객체에 eat이라는 메서드를 추가. 
	console.log(`${this.name}은 밥을 먹는다 `);
};

function Teacher(name, age, teacherNo) {
	// super(name, age)
	Person.call(this, name, age); //name하고 age를 외부에서 받는게 아니라 Person을 부를꺼야! 
	//Person생성자함수에 있는 this를 가져온다 즉, name이랑 age는 Person으로 넘겨준다. 
	this.teacherNo = teacherNo;
}

Teacher.prototype = Object.create(Person.prototype); 
//원래 object.prototype을 Person.prototype으로 변경하겠다.
//Teacher생성자함수를 Person생성자함수에 상속해준다. 
//class Teacher extends Person과 같음. 
console.log(Teacher);

//자식 클래스의 메서드 추가 
Teacher.prototype.teach = () => {
	console.log('가르친다');
};
const yeonmi = new Teacher('박연미', 30, 12341234);
yeonmi.teach(); //가르친다
yeonmi.eat(); //박연미은 밥을 먹는다 

function Student(name, age) {
	Person.call(this, name, age); //Student에 값을 넘겨줬지만 실은 Person에서 실행이 되는것. 
}

//Student클래스를 Person에 상속 
Student.prototype = Object.create(Person.prototype);
Student.prototype.study = () => { //메서드 추가. 
	console.log(`공부하자!! `);
};

Teacher에는 eat이 없고 Person에만 eat이 있는데, Teacher.prototype = Object.create(Person.prototype); 에서 
Person.prototype으로 Teacher.prototype을 변경했기때문에 Person에 접근할수있다 

 

19-2. 두번째 예

function Person(name, age) { //생성자함수
	this.name = name;
	this.age = age;
}

Person.prototype.eat = function () { //Person이라는 객체에 eat이라는 메서드를 추가. 
	console.log(`${this.name}은 밥을 먹는다 `);
};

function Teacher(name, age, teacherNo) {
	// super(name, age)
	Person.call(this, name, age); //name하고 age를 외부에서 받는게 아니라 Person을 부를꺼야! 
	//Person생성자함수에 있는 this를 가져온다 즉, name이랑 age는 Person으로 넘겨준다. 
	this.teacherNo = teacherNo;
}

Teacher.prototype = Object.create(Person.prototype); 
//원래 object.prototype을 Person.prototype으로 변경하겠다.
//Teacher생성자함수를 Person생성자함수에 상속해준다. 
//class Teacher extends Person과 같음. 
console.log(Teacher);

//자식 클래스의 메서드 추가 
Teacher.prototype.teach = () => {
	console.log('가르친다');
};

function Student(name, age) {
	Person.call(this, name, age); //Student에 값을 넘겨줬지만 실은 Person에서 실행이 되는것. 
}

//Student클래스를 Person에 상속 
Student.prototype = Object.create(Person.prototype);
Student.prototype.study = () => { //메서드 추가. 
	console.log(`공부하자!! `);
};

const hgd = new Student('홍길동', 20);
hgd.eat(); //이것도 마찬가지로 Student.prototype = Object.create(Person.prototype);을 했으니 가능!
hgd.study();

이것도 마찬가지로 Student.prototype = Object.create(Person.prototype);을 했으니 상속했다!

 

19-3. 세번째

function Person(name, age) { //생성자함수
	this.name = name;
	this.age = age;
}

Person.prototype.eat = function () { //Person이라는 객체에 eat이라는 메서드를 추가. 
	console.log(`${this.name}은 밥을 먹는다 `);
};

function Teacher(name, age, teacherNo) {
	// super(name, age)
	Person.call(this, name, age); //name하고 age를 외부에서 받는게 아니라 Person을 부를꺼야! 
	//Person생성자함수에 있는 this를 가져온다 즉, name이랑 age는 Person으로 넘겨준다. 
	this.teacherNo = teacherNo;
}

Teacher.prototype = Object.create(Person.prototype); 
//원래 object.prototype을 Person.prototype으로 변경하겠다.
//Teacher생성자함수를 Person생성자함수에 상속해준다. 
//class Teacher extends Person과 같음. 
console.log(Teacher);

//자식 클래스의 메서드 추가 
Teacher.prototype.teach = () => {
	console.log('가르친다');
};

function Student(name, age) {
	Person.call(this, name, age); //Student에 값을 넘겨줬지만 실은 Person에서 실행이 되는것. 
}

//Student클래스를 Person에 상속 
Student.prototype = Object.create(Person.prototype);
Student.prototype.study = () => { //메서드 추가. 
	console.log(`공부하자!! `);
};


console.log(yeonmi instanceof Teacher); //true
console.log(yeonmi instanceof Person); //true
console.log(yeonmi instanceof Student); //false
console.log('-------------');
console.log(hgd instanceof Teacher); //false
console.log(hgd instanceof Person); //true
console.log(hgd instanceof Student); //true

그림으로 보면,