반응형

2편에 이어서 하겠습니다. 

2편 마지막에 생성자함수.prototype = A 를 하면 앞으로 그 생성자 함수로 만들어지는 모든 객체(인스턴스)는 프로토타입으로 A를 가진다고 했었죠? 오늘은 그 점은 코드로 다시 봅시다.

 

const person = {
  talking(){
      console.log(`Hello I'm ${this.name}`);
  }
};

function Nicolas() {
	this.name = 'nicolas'
}
	
Nicolas.prototype = person;		// 생성자함수의 prototype 프로퍼티에 person을 넣습니다.

let nico = new Nicolas();

nico.talking(); 			// Hello I'm nocolas

여러분이 기억해야 할 것이 있습니다. 

1. 자바스크립트의 모든 함수에는 자동으로 prototype이란 프로퍼티가 존재한다.

2. 생성자 함수에 person을 담고,  그 생성자함수로 생성된 모든 객체는 person을 프로토타입으로 가지게된다.

 

 

1번에서 모든 함수에는 prototype이라는 프로퍼티가 존재한다고 했습니다. 그럼 만약에 자동으로 생기는 거니깐 기본값으로는 어떤 객체가 넣어질까요? 

위 코드에서는 사용자가 Nicolas.prototype = person; 하는 순간 prototype에는 person이 넣어지죠.

그렇다면 그 전에 있는 기본값은 무엇일까요? 왜냐면 함수가 만들어질때 prototype 프로퍼티도 자동으로 생성된다고 하니 그럼 처음에는 기본으로 어떤 객체가 prototype프로퍼티에 들어가 있을까? 의문이 있을 수 있겠죠?

 

기본값으로는 바로 constructor함수가 들어 있으며 이 안에는 자기자신인 Nicolas()가 들어있습니다.

즉 다음이 성립됩니다. 

Nicolas.prototype = { constructor : Nicolas }  

따라서 다음도 성립하죠  

console.log(Nicolas.prototype.constructor == Nicolas)      // true

 

자 기본값은 저러한데, 만약에 위의 코드에서처럼 Nicolas.prototype = person; 을 넣어주면 어떻게 될까요? 

당연히 기본값들이 다 사라지죠. 이젠 Nicolas의prototype에는 person객체 하나 밖에 존재하지 않습니다. 

 

그래서 우리는 기본값을 유지해주기 위해 Nicolas.prototype = person 처럼 prototype의 전체를 덮어씌우지 않고, 이렇게 합니다. 

Nicolas.prototype.person = person; 

이렇게 하면 기존의 prototype.constructor 는 사라지지 않고 계속 존재합니다.

 

기존 constructor가 지워지지 않으면 우리는 생성자함수로 인해 만들어진 인스턴스를 이용해서 똑같은 객체를 찍어낼 수 있습니다. 

function Coco() {
	this.say = function(){
    	console.log("I'm Coco");
    }
 }
 
 const coco1 = new Coco();
 
 const coco2 = new coco1.constructor()		// 생성자함수가 아닌 coco1인스턴스로 만들어봅시다.

 console.dir(coco1) 		
 console.dir(coco2) 				// 둘의 구조가 똑같습니다.

 

자 그러면 다음 코드를 봅시다.

function Coco() {
	this.say = function(){
    	console.log("I'm Coco");
    }
 }
 
 const coco1 = new Coco();
 const coco2 = new coco1.constructor()
 
 Coco.prototype = {};			// 여기서 Coco.prototype 의 기본값을 빈객체로 지워봅시다.

 console.log(coco1.constructor);	// 이미 생성된 인스턴스들은 기본값을 가지고 있습니다.
 console.log(coco2.constructor);
 
 const coco3 = new Coco();		// 다시 새로운 인스턴스를 찍어봅시다.
 
 console.log(coco3.costructor);		// 역시 빈객체로 교체된 후 만들어진 인스턴스들은 undefined로 나옵니다.

위 코드각 보여주는 점은 Coco.prototype = {} 가 선언되기 전에 Coco생성자 함수로 생성된 인스턴스들은 기본값을 가지고 있다는 것 입니다. 

 

그럼 다음은 어떨까요?

function Coco() {
	this.say = function(){
    	console.log("I'm Coco");
    }
 }
 
const coco1 = new Coco();

const coco2 = new coco1.constructor();
 
Coco.prototype.name = 'coco';		// 빈객체로 대체하지 않고 name이란 프로퍼티를 추가합니다.

console.log(coco1.name);		// coco
console.log(coco2.name);		// coco

위와는 좀 다르게, 그 전에 만들어진 coco1, coco2 모두 name이라는 프로퍼티를 가지고 있고 값도 올바른게 가지고 있습니다. 

 

그 이유를 알려면 참조 관계에 대해서 알아야 합니다. 그리고 자바스크립트 선언문의 특징에 대해서도 말이죠

C나C++ 를 배우신 분이라면 참조에 대해서 잘 아실 겁니다. 포인터개념을 배우기 때문이죠 

여기서 참조라는 것을 메모리상의 포인터로 설명하자면 너무 길어지고 대충 간략하게만 설명하겠습니다.

 

"A가 B를 참조한다." 라는 뜻은 A가 B의 자원을 사용 할 수 있다는 말이며, 동시에 B가 바뀌면 A도 똑같이 바뀐다는 의미입니다.  상속의 의미와 같죠. 부모에 있는 자원이 바뀌면 그것을 상속하는 자식쪽의 자원도 바뀝니다. (물론 자식만 가지고 있는 자원은 바뀌지 않겠죠? 상속과 관련이 없으니깐요.) 상속도 참조의 개념으로 이루어지는 거니깐요. 

 

비슷하게 참조 복사와 완전 복사라는 말도 있습니다. 

A가 B를 참조복사 했다. >> B가 수정되면 A도 똑같이 수정됩니다.

A가 B를 완전복사 했다. >> 처음 상태는 서로 똑같지만 B가 이후 수정이 되어도 A는 처음 상태의B를 그대로 간직합니다. 서로 독립적인 데이터가 된 것이죠.

 

자 이제 참조에 대해 이해가 되었으면 자바스크립트의 선언문의 특징에 대해 간단히 알아보겠습니다.

자바스크립트에서는 특이하게도 없는 변수를 호출해도, undefined라는 값이 대입되어 나옵니다. 

그럴정도로 자바스크립트 엔진은 없는 변수를 호출해도 최대한 그 변수를 어떻게든 만들어서라도 뭔가 나오게 합니다. 

그래서 Coco.prototype.name = 'coco' 라는 뜻은 두 가지로 해석됩니다.

1. prototype안에 name에 접근해서 값을 얻어와라

2. prototype안에 name이 없다면 지금 만들어라!

 

따라서 위의 Coco.prototype.name = 'coco'는 이렇게 해석됩니다. 

1. Coco로 간다.

2. prototype이 있는지 찾는다 없으면 생성한다. 여기서는 있으니 거기로 들어간다.

3. name을 찾는다. 없으면 생성한다. 여기서는 name이 없으니 지금 생성한다!

4. 그name의 데이터로 'coco'를 저장한다.

5. 끝 

 

다시 그럼 문제의 두 코드를 봅시다.

Coco.prototype = { }                        // 빈객체로 대체됨

Coco.prototype.name = 'coco'            // prototype안에 name프로퍼티를 또 추가해서 그 안에 coco 를 저장

 

문제의 요지는 

Coco.prototype = { }  를 썼을 때는 이 코드 전에 만들어진 객체의 prototype이 빈객체이지 않고 그 전의 값으로 기본값을 계속 가지고 있었습니다. 이것만 보면 이미 만들어진 객체에 대해서는 바뀌지 않는 것처럼 보입니다.

허나 Coco.prototype.name = 'coco' 를 썼을 때는 이 코드 전에 만들어진 객체에 대해서도 반영이 되어서 name값을 가지고 있었죠.

 

이 이유는 Coco.prototype을 Coco의 생성자로 생성된 모든 인스턴스들의 prototype이 참조하고 있기 때문입니다.

즉 new를 해서 새로운 인스턴스를 만들 때마다 Coco의 prototype를 참조복사해서 가지고 가는 것이죠. 

따라서 순서가 어떻게되든 Coco.prototype이 수정되면 그것을 참조하는 모든 인스턴스들의 prototype도 수정되는 겁니다. 그런데 위에서 Coco.prototype = { } 를 했을땐, 따라가지 않았죠? 그대로 기본값을 계속 들고있었습니다.

 

그 핵심 이유는 그 전의 참조를 끊고, 새로운 참조를 생성한 것이기 때문입니다.

그 전의 참조는 coco1, coco2 의 prototype(= __proto__)들은 Coco.prototype의 기본값을 참조 하고 있었습니다.

하지만 문제의 Coco.prototype = { } 를 만나는 순간 기존의 prototype이 { } 값으로 대체됩니다.

사실상 메모리구조로 이해하면 더 쉬운데, 기존에는 Coco.prototype이 0x123 주소인 메모리로 연결되어 있다가 이제 더이상 이 메모리와 연결이 끊어지고 0x321인 메모리 주소로 ( { } 빈객체가 있는 메모리 ) 로 이어진다는 것이죠. 

 

따라서 Coco.prototype = { } 이후에, 만들어진 객체들은 기본값이 사라지고 빈객체만들 들고있습니다. 

하지만 그 전에 만들어진 객체들은 아직도 0x123 주소에 참조가 이어지고 있는 것이죠. 그래서 계속 기본값을 들고 있는 겁니다.

 

다시 설명하자면

Coco.prototype.name은 prototype에 값을 수정한것이 아니라서, 일단은 기존의 prototype이 있는 메모리 주소로 갑니다. 그리고 그 안에 name를 새로 만드는 것이죠. 따라서 참조가 끊겼다가 다시 만들어지는게 아닌, 그냥 원래 참조했던 메모리 방 안에서, 새로운 방을 하나 만든 것 뿐이죠. 그래서 Coco.prototype.name = 'coco' 코드 이전에 만들어진 객체든, 그 이후에 만들어진 객체든 상관없이 참조를 그대로 따라가는 겁니다. 

 

반면에 Coco.prototype = { }를 만나게 되면, 기존 참조길을 한번 끊고, 새롭게 다른 곳으로 ( { } 빈객체가 있는 방으로) 참조가 이어지기 때문에, Coco.prototype = { } 코드 전에 만들어진 객체들은 이전 참조길을 가지고 있어서 기본값을 들고있지만, Coco.prototype = { } 코드 이후에 만들어진 객체들은 새로운 참조길을 가지고 있어서 빈객체를 들고 있게 되는 겁니다. 

 

휴 설명이 제가 잃어도 좀 난해하네요.. 죄송합니다. 이해가 어려우시면 댓글 부탁드려요. 저도 많이 부족한것 같습니다.

오늘의 포스팅은 여기까지 하겠습니다.

 

 

 

 

 

 

 

 

*틀린 점이 있다면 댓글로 달아주세요. 아직 배우는 학생이랍니다. 
*질문도 댓글로 적어주시면 답변할 수 있는 한도 내에서 답변해드리겠습니다. 

 

 

 

 

 

 

 

 

 

반응형
반응형

프로토타입1 글은 개념위주로 적었고 이번에는 코드를 좀 봅시다. 

 

1. 연속 상속  ( = 채인닝 상속)

const A = {
	name : 'A',
    old : '10',
    say(){
    	console.log(`Hello, I am A`);
    }
}

const B = {
	name : 'B',
    old : '11',
    say() {
    	console.log(`Hello, I am ${this.name}` )
    },
    __proto__ : A,
}

const C = {
	name : 'C',
    old : '12',
    __proto__: B,
    location : "seoul",
}

// 다음과 같이 호출하면 각각 뭐가 콘솔로 찍힐까요?
A.say();
B.say();
C.say();

한글 순서대로 

"Hello, I am A"

"Hello, I am B"

"Hello, I am C" 

로 나옵니다. 

 

상속 관게를 보면 C -> B -> A 로 됩니다.

여기서 눈여겨 봐야할 것은 C가 B를 상속받았고, B는 A를 상속받았어도,

B와C에서는 say() 메서드를 호출했을때 Hello, I am A 가 나오지 않는다는 것 입니다.

 

이유는 객체에도 say() 메서드가 있고 그 객체의 프로토타입에도 같은 say()메서드가 있다면, 프로토타입의 있는 것이 무시되기 때문입니다. 

 

따라서 B.say() 는 this.name으로 Hello, I am B 가 나오고

A.say()는 B를 상속받았으므로, this.name의 영향으로 마찬가지로 Hello, I am C가 출력됩니다. 

 

또한 위처럼 프로토타입을 내려 받는 형식으로 상속이 이루어지며, 이렇게 줄줄이 연결이 되는 것들을 채이닝이라고 흔히 부릅니다. 줄줄이 연결되어 있다고 해서 입니다. 채이닝은 추상적인 개념이며, 여기서만 보이는게 아니라 다른 부분에서도 줄줄이 연결되는 부분이 있다면 채이닝이라고 볼 수 있습니다.

 

또 주의할 점은 "역참조는 되지 않는다" 입니다. 부모 객체에서 자식 객체로의 접근은 성립되지 않는다 입니다.

console.log(A.location); 를 하게 되면, C에는 location이 있지만 A에는 location이라는 프로퍼티는 없으므로 undefined가 나오게 됩니다.

 

 

 

 

2. 프로토타입 상속 관련 문제

const person = {
	having : [],
    take(something){
    	this.having.push(something);
        // this.having = [something];
    }
}

const A = {
	__proto__ : person,
	name : 'rich',
    //having : [],
}

const B = {
	__proto__ : person,
	name : 'poor',
    //having : [],
}

A.take("money");
console.log(A.having); 		//["money"] 
console.log(B.having); 		//B는 아무것도 없어야 하는데 A랑 똑같이 money을 가지고 있다.

 

위코드를 잘보시면 A와 B 둘 다 person 객체를 상속받는다. 

따라서 A.take() 를 호출한 순간 자바스크립트 엔진은 A에서 먼저 take() 메서드가 있나 확인한 후, 만약에 없으면 A의 프로토타입 안에서 찾아본다. 여기서 A의 프로토타입은 person이므로, 자바스크립트 엔진은 곧 person의 take() 메서드를 찾고 그것을 호출시킨다.

 

그런데 문제는 A와 B에는 having이라는 프로퍼티가 주석처리되었다. 따라서 this.having.push()를 만나게 되면 A와 B는 각각 having이 있는지 찾게되는데 둘 다 having이 없으므로, 프로토타입까지 가서 결국 person의 having을 찾아서 person의 having에 자신이 가진것을 배열로 저장하게된다. 

 

때문에, A가 가진 money 도 having이라는 프로퍼티를 같이 참조하고 있기 때문에 B역시 가지게 되는 것이다.

 

이런 현상을 없애기 위해서는 각각 A,B 둘 다에게 having프로퍼티를 만들어 주는 것이다. 그러면 프로토타입까지 가서 having을 찾을 이유가 없으므로 각각의 having을 가지게 된다. 

 

또 한가지 방법으로는

person에서 주석처리한 코드로 this.having.push()를 대체하는 것이다. 

잘 보면 this.having.push()는 having안에 push라는 메서드가 있으므로 그것을 찾아 호출하라는 뜻이다. 

즉 this.having.push()는 having이라는 프로퍼티가 이미 존재한다는 것에 가정한 코드이다.

 

하지만, this.having = [something]; 이 코드는 전혀 의미가 다르다. 

이 코드는 만약 this가 가르키는 객체에 having이라는 프로퍼티가 있다면, 거기에 [something] 을 저장하라는 의미와

만약 this가 가르키는 객체에 having이라는 프로퍼티가 없다면, 새롭게 having이라는 프로퍼티를 만들고! 거기다가 [something]을 저장하라는 의미이다.

 

위에서는 A,B 둘다 having이 없으므로, 새롭게 having이라는 프로퍼티를 각각의 객체에 새롭게 만들어서 [something]이라는 배열을 저장하라는 의미로 작동하게 되고, 그렇게 되면 A, B둘 다 독립적이 자신만의 having프로퍼티를 가지므로, 서로 참조가 되어 A가 taking할 것을 B도 가질수 없다. 

 

 

 

3. new 키워드와 생성자 함수

자바스크립트에서 일반적으로 객체를 만드는 방법은 두 가지입니다. 

1. 리터럴 문법으로 직접 객체를 적어주는 법

2. 생성자 함수를 작성하여 new 생성자함수() 로 인스턴스를 찍어내는 것 

 

코드로 보죠

// 리터럴 구문으로 직접 객체 작성
const obj1 = {
	name : 'you nam seng',
    old : '100',
    location : 'seo-ul',
    func1(){
    	console.log("hey I am Y.N.S");
    }
}



// 생성자 함수를 정의하고 new키워드로 인스턴스 찍어내기
function Obj() {
	this.name = 'you nam seng',
    this.old = '100',
    this.location = 'seo-ul',
    this.func1 = function(){
    	console.log("hey I am Y.N.S");
    }
}

// new Obj() 로 인스턴스 찍어내기
const obj2 = new Obj();

 

참고로 생성자함수의 이름을 작성할때는 암묵적인 약속이있는데 함수이름이지만 생성자함수로 쓰일 함수의 이름은 시작을 대문자로 하는것이 약속입니다.

 

위 둘의 방식은 모두 같은 객체를 만들고 있습니다.  이렇게 객체를 만드는데 2가지 방식이 있는데 이번에는 프로토타입과 관련하여 new키워드를 사용하는 방식에 대해서 좀 더 알아보죠 

 

먼저 자바스크립트의 규칙이 있는데 이 부분은 그냥 일단은 외우시는게 좋습니다.

1. 자바스크립트에서 모든 함수는 자동으로 prototype이라는 프로퍼티를 가지게 된다.

2. 생성자 함수에도 prototype이라는 프로퍼티가 존재하고,

3. 이 prototype에 어떤 객체A를 넣어주면 해당 생성자함수로 생성하는 모든 인스턴스(객체)는 프로토타입으로 A을 참조하게된다.

 

3편에서 이어서 포스팅하겠습니다.

 

 

 

 


*틀린 점이 있다면 댓글로 달아주세요. 아직 배우는 학생이랍니다. 
*질문도 댓글로 적어주시면 답변할 수 있는 한도 내에서 답변해드리겠습니다.

 

 

 

반응형
반응형

1. 프로토타입이란 : 

자바스크립트에서는 모든 객체에, 혹은 사용자가 새로운 객체를 생성할 때, 자동으로 Prototype 프로퍼티를 심어 넣습니다. 그리고 이 프로퍼티는 숨겨진 프로퍼티이므로, 함부로 접근할 수 없고, 특정 함수를 통해 접근할 수 있습니다. 

(여기서 프로퍼티란 자바에서의 멤버필드라고 생각하시면 될 것 같습니다. 한글로 설명할 시에는 '속성'이라고 말합니다.)

 

*여기서 살짝 햇갈리는 것이 있습니다.

정확히 무엇을 프로토타입으로 지칭하는가?

저는 두 가지로 생각하고 있습니다. 

첫 번째로는 단순하게, 위 1번에서 설명했듯이 어떤 객체에나 있는 Prototype 프로퍼티를 프로토타입이라고 하는 겁니다. (사실 첫 번째나 두 번째 의미나 둘 다 이해하시면, 결국 같은 것임을 알 수 있습니다.)

두 번째로는 2번을 다 읽고 이어서 설명하겠습니다. 

 

2. 프로토타입의 역할은? 

프로토타입의 역할은 일단 '상속'입니다. 

자바스크립트에서는 객체간의 상속을 이 프로토타입을 서로 상속하는 것으로 이루어집니다. 

 

B라는 객체가 있는데, A라는 객체를 상속받고 싶다. (= 참조하고 싶다.) 라면,

B의 프로토타입 프로퍼티에 A객체를 통으로 넣어버립니다.  

이렇게 되면 B에서 A의 자원을 사용할 수 있습니다. 이것이 프로토타입의 특징중 하나인데, 만약 객체 B에서 없는 자원을 호출하게 되면 자바스크립트 엔진에서는 B의 프로토타입을 뒤져봅니다. 만약에 B에도 없고 B의 프로퍼티인 프로토타입 안을 봤는데도 없다면, 없는 것으로 그제야 에러 메시지를 띄어줍니다.

 

이 특징을 살펴보면 우리가 흔히 쓰는 자바스크립트의 내장함수들을 생각해 봤을 때 이해가 되는 부분이 있습니다. 

대표적으로 배열을 만들고 배열을 유용하게 쓸 수 있는 내장함수들 입니다.

여러분이 자바스크립트에서 배열 하나를 만들게 되면, 자바스크립트 엔진에서 자동으로 프로토타입이 만들어지고 그 안에 배열과 관련한 내장 함수들을 참조하도록 넣어줍니다. 때문에 여러분이 굳이 여러분이 만든 배열 안에 내장 함수를 따로 정의해주지 않아도 자동으로 유용한 배열 관련 함수들을 사용할 수 있는 것입니다.

 

자 그럼 위에 1번에서 못다 한 설명을 이어하겠습니다.

두 번째로는 서로 상속관계에 있을 때, 그러니깐 B의 Prototype 프로퍼티가 참조하는 객체가 A일 때(위 상황과 동일), 

이때 이 A라는 객체를 프로토타입이라고 합니다. (개념상으로는 주로 두 번째로 해석되는 듯합니다.)

따라서 B의 프로토타입은 A라는 객체를 지칭하는 것이 됩니다.

 

3. __proto__ 는 무엇인가요?

지금 당장 인터넷 브라우저 개발자 콘솔 창을 켜서 아무 객체나 console.dir()로 안의 구조를 한번 보시길 바랍니다.

어떤 객체에도 다 있습니다. 1번에서 말한 Prototype 프로퍼티와 마찬가지로 말이죠. 그 이유는 이 __proto__ 프로퍼티는 사실 Prototype을 그대로 참조하는 녀석입니다. 그런데 왜 Prototype이 직접 있지 않고 왜 숨겨놓고 그것을 참조하는 __proto__를 만들었을까요? 그것에는 이유가 있지만 지금은 일단 이렇게 상황만 알아둡시다.

( 참고로 __proto__ 는 던더 프로토라고 읽습니다.)

 

4. 던더 프로토?

인터넷에서 프로토타입을 설명하는 코드를 보면, 직접 Prototype를 이용하지 않고 이 던더 프로토를 이용해서 보여줍니다. 그 이유는 Prototype으로 직접 하려면 초보자분들이? 보기에는 생소한 함수를 써야 하기 때문입니다. 그래서 일단 집중을 흐리지 않고 설명하기 위해 주로 던더 프로토를 이용해 설명하는 것으로 보입니다. 

원래 자바스크립트 공식에서는 이 던더 프로토의 사용을 지양하라고 하고 있습니다. 때문에 그냥 개념 파악에만 집중하시기 바랍니다. (그냥 "Prototype == 던더 프로토"라고 생각하시고 개념 이해에만 집중하시면 됩니다.)

 

5. 코드를 봅시다. 다음은 상속하는 과정, 프로토타입과 this의 성질을 모두 말해주는 간단한 코드입니다.

const A = {
  name : '나는 A야',
  func1(){
    console.log(this.name);		// 자기 name프로퍼티를 출력해보는 메서드
  }
}

const B = {
  name : '나는 B입니다.',
  __proto__ : A,				// A의 func1()자원을 쓰고 싶어서 상속받아봅니다.
}

A.func1();						// 나는 A야
B.func1();						// 나는 B입니다.

5-1. 프로토타입으로 상속

B에서 보면 던더 프로토에 A객체를 넣어줍니다. 그럼 B의 프로토타입은 A를 참조하게 됩니다. (그래서 결국 B의 프로토타입은 A가 됩니다.)

이렇게 되면 B에서는 비록 func1()를 정의해주지 않았지만 func1()이라는 자원을 상속받아 사용할 수 있습니다.

 

5-2. 프로토타입과 this

위를 잘 보시면 func1() 메서드에서 this.name 이 보이실 겁니다. 이놈의 자바스크립트에서 this는 초보 개발자들을 햇갈리하는 존재입니다. 상황에 따라서 this가 가리키는 객체가 달라지기 때문이죠. 

(나중에 이 this에 대해서도 포스팅 한 번 해보겠습니다.)

 

원래 위 코드에서 func1()를 호출하는 부분에서 생략을 하지 않고 제대로 쓰면 

B.func1() 이 아니라

B.__proto__. func1() 이 되어야 하죠? 맞습니다. 원래 풀로 적으면 이렇게 적어야 하죠 

func1()의. (점 연산자) 앞에 __proto__라는 프로퍼티이자 곧 A라는 객체를 가리키니깐 어떻게 생각해보면 "나는 A야"가 콘솔 창에 뜨는 게 맞다고 생각하실 수 있습니다. 

 

그러나 여기서 프로토타입의 특징이 나옵니다.

1. __proto__ 은 생략해서 쓸 수 있다. 

2. B의 프로토타입을 this로 가리키면 그 this는 B의 프로토타입인 A를 가리키는 것이 아니라, B본인을 가리킨다.

 

 

이상 프로토타입의 개념 설명이었습니다. 코드 부분도 이어서 포스팅하겠습니다.

 

 

 

 

 

 

 

*틀린 점이 있다면 댓글로 달아주세요. 아직 배우는 학생이랍니다.
*질문도 댓글로 적어주시면 답변할 수 있는 한도 내에서 답변해드리겠습니다.

 

반응형
반응형

1. 콜백이란?

비동기 코딩 시 순서를 지키기 위하여 함수 안에 함수를 파라미터를 넣어서 쓰는 방식을 콜백 함수라고 한다.

라고만 대충 알면 그 뒤의 심오한 뜻을 알지 못합니다.

 

이번 포스팅에서는 콜백함수를 익명 함수의 관점으로 바라볼 때 어떤 의미가 있는가?입니다.

 

2. 콜백을 진지하게 다시 봅시다.

 

콜백을 진지하게 보기 전에, 프로미스 함수에 대해서 잠깐 알고 갑시다. 왜냐면 제가 이 프로미스 함수를 예시로 콜백 함수를 설명할 건데 그전에 프로미스 함수에 대해서 함수 스타일을 알고 가야 하기 때문에 이해하기 쉽기 때문입니다. 

(제 블로그에 프로미스에 대한 글도 있으니 궁금하시면 보세요.)

 

일단 그냥 프로미스 함수를 적으면 이렇게 나옵니다. 

const promise = new Promise( (resolve, reject) => {
	// 프로미스에게 할 일을 적고
    console.log();
    
	// 성공시 resolve() 함수 호출
    resolve();
    
    // 실패시 reject() 함수 호출
    reject();
})

	// 나중에 프로미스의 반환된 값을 보고 싶다면 반드시 .then()으로 받아야 함
    .then( (data) => {
    	console.log(data);
    })
    
    // 실패시 catch()문으로 에러값을 받음
    .catch( (error) => {
    	console.error(error);
    })

프로미스를 쓸려면 위에서 처럼 반드시 콜백 함수를 써야합니다. 

그럼 이 프로미스의 콜백함수를 좀 자세히 봅시다.

 

vscode에서 javasctipt 관련 플러그인을 설치하고

new Promise() 한 다음 가로 안에 커서를 두고 도움말을 기다리시면, 가로 안에 어떤 형식이 와야 하는지 예시 도움말이 나옵니다. 그것을 복사하면 다음과 같습니다.

 

Promise(executor: (resolve: (value?: any) => void, reject: (reason?: any) => void) => void): Promise <any>

 

 저도 그랬도 우리같이 초보자분들은 은근 이런 도움말을 무시하고 바로 그냥 구글 서치 들어갑니다... 물론 어떤 방식으로든 결과만 같다면 좋습니다. 다만.. 시간이라는 호율성이 문제가 있겠죠. 더 이상 얘기하면 딴 얘기로 빠지므로 이만

하겠습니다. ( 편집기의 도움말을 항상 이해하고 참조해야 한다는 소리는 아닙니다.) 

 

 

순서대로 설명하겠습니다.

executer: 는 함수 이름입니다. 
(꼭 함수 이름을 executer라고 만들어서 쓰라는 소리가 아닙니다. 대부분그냥 익명 함수로 씁니다. 여기서는 개념상 Promise객체가 필요한 파라미터가 있는데 그 파라미터가 함수이고, 그 함수의 역할을 보니 실행? 같은 부분을 맡으므로 함수이름을 executer라고 지어논겁니다. 하지만 프로미스를 쓸 때 대부분 걍 익명함수로 씁니다. )

 

그러니깐 첫 번째 파라미터로 익스큐터 함수라는 것이 오는데, " : " 다음으로는 해당 함수의 형식을 보여줍니다. 

익스큐터의 함수는 어떻게 작성해야 하나 보니 다음과 같습니다.

(resolve: (value?: any) => void, reject: (reason?: any) => void) => void):

 

잘 보면 resolve라는 함수가 파라미터로 들어가 있습니다.

또 reject라는 함수도 파라미터로 들어가 있습니다.

 

resolve: (value?: any) => void,

이 부분이 resolve함수를 설명하는 부분이고 파라미터로 어느 값이든 들어올 수 있고, 리턴되는 값은 void, 즉 없다고 나오네요. (value라고 명명한 이유는 흔히 resolve()에서 데이터 값을 받고 저장하기 때문입니다. 그래서 value값으로 이름을 지었군요.)

 

reject: (reason?: any) => void

이역시 위와 마찬가지, reject라는 함수이고 파라미터도 any, 아무것이나 들어올 수 있다는 것이고, 리턴되는 값은 역시 없습니다. (reject() 함수는 프로미스가 작업 도중 실패했을 때 호출하는 함수입니다. 그러니 실패한 이유를 필요로 하겠죠? 그래서 reason으로 명명했군요.)

 

그리고 끝에 => void 이 남게 되는데 executer() 함수의 리턴 값을 의미하는 부분입니다. 보시다시피 아무것도 반환하지 않는군요.

 

여기까지 (resolve: (value?: any) => void, reject: (reason?: any) => void) => void): 설명이고 마지막에 하나 더 있죠?

끝에 Promise <any>는 이 Promise() 함수가 반환하는 객체를 의미합니다. 프로미스 객체를 반환하는군요. <any>는 제너릭 타입인데 이건 나중에 설명하겠습니다. 

일단 지금 당장은 어느 결괏값이든 Promise() 객체 안에 담겨서 반환된다.라고 생각하시면 되겠습니다.

 

여기까지 이해가 되었다면, 위로 다시 올라가서 프로미스 함수를 다시 보시죠.

콜백 함수의 resolve, reject 인자는 둘 다 함수였구나 라는 것을 알게 됩니다. 

 

이제 콜백 함수에 대해서 천천히 살펴봅시다. 

프로미스라는 객체는 자바스크립트 ES6에서 기본적으로 제공되는 객체지만, 설명을 위해 없다고 치고 제가 프로미스라는 함수를 만들어서 쓴다는 가정을 해봅시다. 

 

자 프로미스라는 함수를 정의해 봅시다.

function myPromise( callBackFunc  ) {
	getApi("https://url요청주소.com/api")	// 무언가 네트워크 통신 작업을 하는 코드라고 칩시다.
	.then(data => console.log(data));       // 결과값을 받고 console.log로 출력하는 코드라고 칩시다. 
	
    return callBackFunc(resolve, reject, data)    // resolve(), reject()는 외부에서 이미 정의되어있는 함수라고 칩시다.
}


헷갈리지 마세요. 위는 함수를 '정의' 한 거라고 생각하세요

이 함수의 역할을 대충 

1. 어떤 곳으로 네트워크 통신 요청을 하고

2. 그 결괏값을 받아 console.log로 찍어줍니다.

3. 그리고 파라미터로 받은 callBackFunc함수에 resolve, reject 함수를 넣고 호출한 뒤 그 값을 리턴하네요. 

(참고로 위의 는 슈도 코드이니 코드 하나하나 이해하려고 하지 마세요.)

(슈도 코드란: 개발자끼리 의사소통만을 위해, 코드 규칙은 지키지 않고 코드의 역할이나 목적만 알 수 있도록 대충 적은 코드를 말합니다. 슈도 코드에서 중요한 건 각각의 코드의 역할과 로직적인 흐름입니다.)

 

그럼 위에 정의된 myPromise() 함수를 호출해서 써 봅시다.

myPromise( (resolve, reject, data) => {
    
    // 이번엔 getApi받아온 데이터를 다른 서버에 보내서 잘 갔나 응답도 받아봅시다.
    let result = 0;               // 우선 result 변수를 하나 잡고 여기에다 서버의 응답을 받아봅시다.
    postApi("https://어디가에요청.com/api", data).then(serverResult => {
        result = serverResult;    // 위의 result변수에다가 서버가 준 응답결과를 저장한다 칩시다.
                                  // 성공해서 서버에서 데이터를 잘 받았다 치면 1을 준다 치고, 실패하면 -1를 준다고 합시다.
    });
    // result의 내용물을 보면 1, 0, -1 에 따라 결과를 알 수 있겠죠?
    if(result) {
        resolve(result);          // 성공했으므로 resolve()함수를 호출해서 값을 저장해줍시다.
    }else{
        reject(new Error("0, -1 이든 쨋든 서버에서 데이터를 잘 받지 못했네요")); // 실패시 에러문구를 보여줍시다.
    }
    return result;
    
});

 

 여기서 핵심은 상세하게 제가 쓴 의미 없는 코드들을 이해하는 게 아니라, 

제가 callBackFunc 함수에 들어갈 것들을 정의했다입니다. 즉 익명 함수를 즉석에서 만들어서 myPromise() 함수의 콜백 함수로 넣어준 것이죠. 이게 핵심입니다.

 아시다싶히, myPromise() 함수를 정의하는 곳에서는 익명함수를 정의하지 않습니다. 그냥 그 익명함수를 호출하고 그 결과값을 반환해라 라고 정의되어있죠. 

 

myPromise()함수를 정의한 것을 다시 보면 

1. 이 함수는 어찌 되었든 무조건 getApi를 통해 어디선가로부터 데이터를 일단 받아옵니다.

2. 그리고 다시 reject(), resolve(), 함수들과 함께 위에서 받아온 데이터를 callBackFunc 함수의 파라미터로 넣어서 호출하죠.

3. 그리고 그 리턴 값을 리턴합니다. 

 

그리고 호출해서 쓸 때를 보면, callBackFunc자리에 냅다 바로 익명 함수를 정의합니다. 

 

해석해보면 myPromise()는 데이터를 받아서 익명 함수에 다시 줍니다.

그리고 익명 함수가 하는 일은? 우리가 호출할 때 정의해줬죠?  즉석에서 바로, 이렇게 정의하는 겁니다. 그럼 굳이 myPromise() 함수를 정의할 때 콜백 함수까지 정의하지 않아도 되죠? 그렇게 하려고 익명 함수를 쓰는 거고요.

 이런 식의 콜백 함수는 즉석에서 상황에 맞게 알아서 쓰라고 둬 야하기 때문에 미리 사전에 정의할 수 없습니다. 

 

결과적으로 저렇게 호출하게 되면 getApi로 일단 데이터를 받아와서 다시 postApi로 보내서 해당 서버에서 데이터가 잘 왔으면, 응답을 해줄 것이고 그 응답을 받아서 잘 받았으면 resolve() 호출하고, 뭔가 문제로 인해 잘 못 받았으면 reject()를 호출하는 함수가 됩니다. 

 

이상 포스팅을 마치겠습니다.

 

 

 

 

 

 

 

 

 

 

*틀린 점이 있다면 댓글로 달아주세요. 아직 배우는 학생이랍니다.
*질문도 댓글로 적어주시면 답변할 수 있는 한도내에서 답변해드리겠습니다.

 

 

 

 

 

 

 

 

 

 

 

반응형
반응형

1. 우선 Promise 는 ES6 에서 새롭게 추가된 내장 객체입니다.

2. 이 객체가 무엇을 하는 객체냐? >> 어떤 일을 하되, 비동기적으로 해야할 경우 편리하게 할 수 있도록 도와주는 객체입니다.

3. 예를 들어 비동기 통신을 할 경우, 콜백함수로 순서를 지키고자 합니다. 그러다가 콜백지옥 현상이 나타나게되죠?

이런 콜백 지옥 현상을 피할 수 있도록 하는게 Promise 객체의 역할입니다. 한마디로 편리한 비동기 코딩을 위해 제공된것 입니다.

 

 

기본 사용법

const promise = new Promise((resolve, reject) => {
	// 프로미스를 사용하려면 반드시 내장 콜백함수도 같이 써줘야 합니다.
    // (resolve, reject) => { 이 안에다가 할 일 작성 }
    
	// 여기에다가 할 일을 정의해주면 됩니다. 
    // 대부분 비동기 관련한 작업들이 여기에 오겠죠?
    
    // 여기는 예시이므로 그냥 console.log만 찍어보겠습니다.
    console.log("여기에 할 일을 작성해 주세요");
    
    
    // 이제 할 일을 다하고 만약 성공적으로 결과값을 받았다고 가정한다면, 
    // resolve 함수를 호출해서 성공했음을 promise객체에 알려줘야 합니다.
    // (프로미스 객체는 자기가 한 일이 실패했는지, 성공했는지까지는 모릅니다.)
   
    // 일을 잘 수행해서 결과값이 왔다면
    resolve(response);			
    
    // 만약 요청이 실패해서 결과값이 오지 않았다면
    reject(new Error("요청 실패")); 
});

 위에서 처럼 프로미스가 할 일을 정의해주고, 성공/실패시에 대해 로직을 짜서 resolve(), reject() 함수를 호출합니다.

그리고 성공했다면 받은 데이터는 어떻게 처리을 해야 할까요? 혹은 실패했다면 오류에 대한 처리는 어떻게 해야 할까요?

 

 then() catch() 함수

// 이제 위에서 만든 promise 객체에서 결과값을 받아봅시다.

promise.then( (data) => {
	// then함수의 파라미터로 콜백이 들어오고 이 콜백의 파라미터에는 자동으로 위에
    // resolve(response)에서 저장된 결과값이 저장되어 있습니다.
    
    // 따라서 받아온 데이터는 다음과 같이 로그로 나타낼 수 있죠
    console.log(data);
    
})

// 만약 실패했다면 어떻게 될까요? 오류메시지라도 받아봐야겠죠?
promise.catch( (error) => {
	// 만약 reject() 함수를 위에서 호출했었다면 catch() 함수로 이어집니다.
    // then() 함수는 작동하지 않죠.. 
    // 반대로 성공하면 catch() 함수가 실행되지 않습니다. 
    
    // 에러가 자동으로 콜백함수의 파라미터인 error로 저장됩니다.
    // 따라서 우리는 이렇게 에러메세지를 확인 할 수 있습니다.
    console.log(error);
})


// 좀 코드를 이쁘고 간단하게 다시 짜볼까요?
promise
	.then(data => {
    	console.log(data);
    })
    .catch(error => {
    	console.log(error);
    }
 // 이렇게 하면 좀 간단하고 보기도 쉽죠

여기서 알아야 할 것은 2가지 입니다.

1. reject() 함수를 위에서 호출했다면 catch()로 이어진다.

2. resoleve() 함수를 위에서 호출했다면 then()으로 이어진다.

 

3. 추가로 .finally() 함수도 있습니다. 이것은 실패하든 성공하든 반드시 실행하는 부분이죠. 사용방법은 똑같이 쓰면 됩니다. 마치 자바에서 try catch finally문과 비슷하죠?

반응형
반응형

객체 리터럴이란? 

객체를 정의하는 방법이 2가지가 있습니다.

 

1. new 키워드로 생성자 함수로 객체 생성.

2. 객체 리터럴로 직접 객체의 속성과 값을 문자로 적는 것.

 

여기서 객체 리터럴이란?

// 객체리터럴 
const obj = {
	prop1 : "첫번째 속성값",
    prop2 : "두번째 속성값",
    prop3 : "세번째 속성값",
}

위와 같이 객체를 정의할 때 직접 속성명과 속성값을 하나하나 문자로 적어서 객체를 정의하는 것이 객체 리터럴 방식입니다. 

 

ES6에서는 이 객체 리터럴이 좀 더 편한게 향상된 것이 있다고 해서 "향상된 객체리터럴"으로 자주 설명되곤 합니다. 

지금 부터 그 향상된 객체리터럴 표기법을 알아봅시다.

 

크게 3가지 정도가 있습니다.

1. 속성명과 속성값의 변수명이 같다면, 하나만 기입가능.

2. 메서드 속성을 정의할 때 function() 키워드 생략가능.

3. 동적 속성명을 바로 정의가능.

 

그냥 이렇게만 본다면 모르시겠죠. 하나씩 코드를 보면서 설명해보겠습니다.

1. 속성명과 속성값의 변수명이 같다면, 하나만 기입가능.

// ES5에서는 이렇게 써야합니다.
var myName = 'cucuhama';
var obj = {
	myOld : '23',
    myLocation : 'GangNam',
    myName : myName  		// 여기서 myName이라는 속성명(key값)의 값(value값)으로 변수 myName을 지정했습니다.   
}

 

위와 같이 myName : myName 처럼 myName이 두 번 중복되고 있죠. 이런 현상을 없애기 위해서 ES6 부터는 만약 저렇게 속성명과 그 값의 변수명이 서로같다면, myName 한번만 적어도 됩니다. 

const myName = 'cucuhama';
const obj = {
	myName,				// 이렇게 한번만 적어도 myName : myName 처럼 동작합니다.
    myOld : '23',
}

 

 

 

2. 메서드 속성을 정의할 때 function() 키워드 생략가능.

// ES5 기존 문법
var obj = {
	prop1 : "something",
    prop1 : "anything",
    method1 : function(){					// 반드시 메서드일 경우 function() 키워드 필요
    	console.log("이것은 메소드입니다");
    },
};

 

더 이상 function 키워드를 쓰지 않아도 됩니다. 이렇게 말이죠.

const obj = {
	porp1 : "somethind",
    prop2 : "else",
    method1() {								// 바로 함수명을 적으면 됩니다.
    	console.log("이것은 메서드입니다.");
    }
 };
 

 

 

 

 

3. 동적 속성명을 바로 정의가능.

동적 속성명이란, 속성명으로 변수를 사용해서 동적으로 속성명을 정의할 수 있도록 하는 것 입니다. 

// ES5 에서 동적 속성명 하기

// 우선 먼저 객체를 정의하고
var obj = {
	prop1 : "lulu",
    prop2 : "lala",
    method1 : function(){
    	console.log("룰루 랄라");
    },
};
 
// 객체를 먼저 생성하고 그 다음에 동적 속성명을 정의한다.
var value = "prop3";
obj[value] = '동적이군';   // obj에 prop3이란 속성이 생기고 값으로 "동적이군"이 들어간다.
 
 
 

하지만 ES6 부터는 위와는 다르게 객체를 정의할 때 같이 동적속성명을 정의할 수 있습니다.

let value = "prop3";			// 동적으로 정의할 속성명을 변수 "value"에다가 저장합니다. 
const obj = {
	prop1 : "룰루",
    prop2 : "랄라",
    hoho() {
    	console.log("호호 함수");
    },
    [value] : '동적이군',		// 이렇게 바로 객체리터럴에서 동적으로 정의할 수 있습니다.
};

 

반응형

+ Recent posts