코딩응애의 개발블로그

JavaScript 객체 지향 프로그밍 - 생활코딩 본문

JavaScript

JavaScript 객체 지향 프로그밍 - 생활코딩

이너멜 2022. 7. 25. 18:35

3.2 객체와 반복문 

console.group() & console.groupEnd()

 웹 콘솔 로그에 새로운 인라인 그룹을 만듭니다. 이는 console.groupEnd()가 호출될 때까지 모든 다음 출력을 추가 수준으로

들여씁니다. 그냥 이런게 있구나 알아만 두기 

위와 비교해서 들여쓰기가 된 모습

4.2 객체 만들어 보기 

객체란 서로 연관된 변수와 함수를 그룹핑 하고 이름을 붙인것이고 정리정돈을 할 수 있다. 

 

5. this 

프로그래밍에서 자기 자신을 가리키는 표현이 있는데 그게 바로 this 이다.

let kim = {
    name:'kim',
    first:10,
    second:20,
    sum:function(f,s){
        return f+s;
    }
}

console.log("kim.sum(kim.first, kim.second)", kim.sum(kim.first,kim.second));
// ------ 1차 개선 ------------
let kim = {
    name:'kim',
    first:10,
    second:20,
    sum:function(){
        return kim.first+kim.second;
    }
}
//console.log("kim.sum(kim.first, kim.second)", kim.sum(kim.first, kim.second));
console.log("kim.sum(kim.first, kim.second)", kim.sum());
// 이것도 아쉬움 왜냐면 kim을 k로 바꾼다면 동작을 안하기 때문에 그러면 일일히 kim을 k로 바꿔줘야한다
// 그리고 sum 이라는 함수가 속해있는 객체가 어떤 이름을 가지게 될지 예상하는건 논리적이지 않다

이러한 경우로 인해 객체 지향을 만든 사람들은 메소드가 있으면 그 메소드가 자신이 속해있는 객체를 가리키는 특수한 키워드를 만들었는데 그것이 바로 this다. 

let kim = {
    name:'kim',
    first:10,
    second:20,
    sum:function(){
        return this.first+this.second;
    }
}
//console.log("kim.sum(kim.first, kim.second)", kim.sum(kim.first, kim.second));
console.log("kim.sum(kim.first, kim.second)", kim.sum());

// 이렇게 해주면 kim을 k든 다른 단어로 바꿔도 잘 동작하는것을 볼 수 있다.

6.1 constructor의 필요성

let kim = {
    name:'kim',
    first:10,
    second:20,
    third:30,
    sum:function(){
        return this.first+this.second+this.third;
    }
}
let lee = {
    name:'lee',
    first:10,
    second:10,
    third:10,
    sum:function(){
        return this.first+this.second+this.third;
    }
}
console.log("kim.sum()", kim.sum());
console.log("lee.sum()", lee.sum());

객체의 기본적인 동작방법이 바뀌면 같은 취지에 객체는 다 바꿔줘야 하는데 갯수가 적다면 일일히 바꿔도 큰 문제는 없겠지만 저런 코드가 만약 1억개가 있다면...  이럴때는 저런 형식의 객체를 찍어내는 공장을 만들어야 한다

 

6.2 constructor의 사례 

 객체를 찍어내는 공장의 사례 중 하나를 들자면 

let d1 = new Date('2019-4-10'); // 내부적으로 2019-4-10 데이터를 가지고 있는 새로운 객체가 만들어져서 d1이 된다
console.log('d1.getFullYear()', d1.getFullYear()); // 2019 출력

저 Date라는 무언가는 앞에 new를 붙였을때 객체를 만들어서 우리한테 리턴해 주고 있다는걸 알 수 있다. 

그렇다면 let kim 객체도 저런식으로 공장을 만든다면 코드도 훨씬 깨끗해 질것이다...

 

6.3 constructor 만들기 

우선 let kim 객체에 내용을 가져와서 새로운 함수 Person()을 만들어보면 

function Person() {
    this.name = 'kim';
    this.first = 10;
    this.second = 20;
    this.third = 30;
    this.sum = function () {
        return this.first + this.second + this.third;
    }
}

console.log(Person()); // undefined 출력
// Person 앞에다가 new를 붙히면 Person이라는 객체가 만들어진다
console.log(new Person()); //Person { name: 'kim', first: 10, second: 20, third: 30, sum: [Function (anonymous)]} 출력

함수를 그냥 호출하면 그냥 함수인데 앞에다가 new를 붙이면 객체를 생성하는 생성자(constructor)가 된다 

앞에 new가 붙어있다면 생성자 함수라고 함 

이제 생성자를 만들어보면 이렇게 만들 수 있다 

function Person(name, first, second){
    this.name=name;
    this.first=first;
    this.second=second;
    this.sum = function(){
        return this.first+this.second;
    }
}
 
let kim = new Person('kim', 10, 20);
let lee = new Person('lee', 10, 10);
console.log("kim.sum()", kim.sum()); // kim.sum() 60
console.log("lee.sum()", lee.sum()); // lee.sum() 30

 

이렇게 'new' 연산자와 생성자 함수를 사용하면 유사한 객체 여러 개를 쉽게 만들 수 있습니다. 

함수 이름의 첫 글자는 대문자로 시작하고 반드시 'new' 연산자를 붙여 실행한다.

생성자함수를 사용하면  객체생성용 함수의 내부코드를 반복적으로 노출시킬 필요도 없고(은닉성/깔끔함),

new 생성자명() 식으로 간단히 객체를 생성할 수 있으며(편리),

모든 객체들의 원본인 생성자함수의 내부 코드만 수정하면 해당 생성자함수로 파생된 모든 객체들에도

용이 적용되는 폭발적인 유지보수의 편리함이 있다.

 

7.1 prototype

JS를 프로토타입기반언어 라고 부르기도 한다.

Person이라고 하는 생성자를 사용하는 모든 객체에 sum 함수를 바꾸고 싶다면 그 개수에 따라 작업을 해야 한다 

1억개라면 1억번...

생성자 함수 안에서 메소드를 정의할 경우 새로운 객체를 생산할 때마다 메소드를 정의하는 코드가 실행되어야 하고

같은 메소드가 중복 실행되면서 메모리를 낭비하게된다.

이것이 생성자 안에서 메소드를 만드는것에 단점이다. 즉 생산성이 많이 떨어진다..

 

7.2 

function Person(name, first, second){
    this.name=name;
    this.first=first;
    this.second=second;   
}
 
Person.prototype.sum = function(){ // 생성자 함수에 공통적으로 사용할 sum 메소드 만들기 
    return 'prototype : '+(this.first+this.second);
}
 
var kim = new Person('kim', 10, 20);
kim.sum = function(){
    return 'this : '+(this.first+this.second);
}
var lee = new Person('lee', 10, 10);
console.log("kim.sum()", kim.sum()); // kim.sum() this : 30
console.log("lee.sum()", lee.sum()); // lee.sum() prototype : 20

저러한 반복 작업과 메모리 낭비로 인해 나온것이 prototype 이다. 

prototype 사전적 의미로는 '원형', constructor 함수에 미리 함수를 정의하면, 객체를 생성할 때마다 함수가 생성되기
때문에 프로그램의 성능 저하와 메모리 사용의 문제가 생긴다.
이를 해결하기 위해 모든 생성자들이 공통적으로 공유하는 생성자 함수의 원형을 prototype 으로 정의할 수 있다.
prototype은 객체가 만들어질 때마다 실행되는 것이 아니라, 한번만 실행되기 때문에 성능과 메모리 상의 문제를 해결할 수 있을 뿐 아니라, 특정 생성자만 따로 컨트롤할 수 있다.
특정 생성자를 컨트롤 할 때, 그 생성자 내에 미리 정의된 함수가 있으면 그 함수를 실행시키고,
그렇지 않으면 prototype 함수를 실행시킨다.

class constructor 

어떤 객체가 생성될때 그 객체에 초기 상태를 지정하기 위한, 또는 객체가 생성되기전에 실행되도록 약속되어 있는 함수 

바로 constructor() 함수이다.  약속된 이름이라 우리가 함부로 다르게 지정할 수 없다.

그리고 JS는 객체를 생성할때 constructor() 함수를 자동으로 호출해준다. 

class Person{ // => 위에 function Person() 생성자 함수를 class를 이용해서 생성자 함수로 바꿈 
    constructor(name, first, second){
        this.name = name;
        this.first = first;
        this.second = second;
    }
}
 
var kim = new Person('kim', 10, 20);
console.log('kim', kim); // Person { name: 'kim', first: 10, second: 20 } 출력

class에서 객체의 메소드 구현하기 

prototype을 class에서 구현을 할 수 있는데 어떻게 하냐면 일단 첫째 그냥 그대로 쓰는 방법 

class Person{ // => 위에 function Person() 생성자 함수를 class를 이용해서 생성자 함수로 바꿈 
    constructor(name, first, second){
        this.name = name;
        this.first = first;
        this.second = second;
    }
}

Person.prototype.sum = function(){ // 생성자 함수에 공통적으로 사용할 sum 메소드 만들기 
    return 'prototype : '+(this.first+this.second);
}
 
var kim = new Person('kim', 10, 20);
console.log('kim', kim); // Person { name: 'kim', first: 10, second: 20 } 출력

console.log(kim.sum()); // prototype : 30 출력

이거 말고 클래스 안에다가 넣는 방법도 있다. 

class Person{
    constructor(name, first, second){
        this.name = name;
        this.first = first;
        this.second = second;
    }
    sum(){ // 프로토타입과 같은 기능을 한다 
        return 'prototype : '+(this.first+this.second);
    }
}
 
let kim = new Person('kim', 10, 20);
kim.sum = function(){
    return 'this : '+(this.first+this.second);
}
let lee = new Person('lee', 10, 10);
console.log("kim.sum()", kim.sum()); // kim.sum() this : 30
console.log("lee.sum()", lee.sum()); // lee.sum() prototype : 20
똑같이 실행이 되는것을 볼 수 있다. 근데 이럴거면 왜 prototype을 쓰는거고 왜 같은지 의문점이 생기게 됨 
클래스는 ES6 에서부터는 추가되었는데, 우리가 객체 생성자로 구현했던 코드를 조금 더 명확하고, 깔끔하게 구현 할 수 있게
해주고, 추가적으로, 상속도 훨씬 쉽게 해줄 수 있다.

위에 코드 처럼 클래스 내부에 sum 이라는 함수를 선언하면 그 함수는 메소드가 되는건 알고 있지? 

암튼 이렇게 메소드를 만들면 자동으로 prototype으로 등록이 된다고 한다 와우...

 

class 상속  

상속이 없다면 부모 클래스를 기반으로 자식 클래스를 만들때 기반이 되는 클래스의 기능을 모두 옮겨 적어야 되는

불편함과 코드의 중복이 발생하고 부모 클래스를 수정을 하면 부모의 자식 클래스 또한 다 수정을 해야하는 엄청난

중복이 발생한다. 예시 코드를 보자면 

class Person{
    constructor(name, first, second){
        this.name = name;
        this.first = first;
        this.second = second;
    }
    sum(){
        return this.first+this.second;
    }
}
class PersonPlus {
    constructor(name, first, second){
            this.name = name;
            this.first = first;
            this.second = second;
    }
    sum(){
        return this.first+this.second;
    }
    avg(){
        return (this.first+this.second)/2;
    }
}
 
var kim = new PersonPlus('kim', 10, 20);
console.log("kim.sum()", kim.sum()); // kim.sum() 30 출력 
console.log("kim.avg()", kim.avg()); // kim.avg() 15 출력

이렇게 해도 되지만 중복이라는 아쉬움이 발생하게 된다. 중복을 제거해야 하는데 이때 사용하는게 상속이다.

extends 라는 키워드를 이용해서 상속을 한다. 

class Person{
    constructor(name, first, second){
        this.name = name;
        this.first = first;
        this.second = second;
    }
    sum(){
        return this.first+this.second;
    }
}
class PersonPlus extends Person{
    avg(){
        return (this.first+this.second)/2;
    }
}
 
var kim = new PersonPlus('kim', 10, 20);
console.log("kim.sum()", kim.sum()); // kim.sum() 30 출력 
console.log("kim.avg()", kim.avg()); // kim.avg() 15 출력

똑같은 결과가 나오는 것을 볼 수 있다.

 

super

부모클래스가 갖고 있는 기능을 실행할 수 있다.

 

객체간의 상속 

자바같은 유동적인 주류 객체 지향 언어에서는 부모 클래스로 부터 상속을 받은 자식 클래스를 통해 객체를 생성을 하는데

자바스크립트 에서는 객체가 직접 다른 객체에 상속을 받을 수 있고 얼마든지 상속 관계를 바꿀 수 있다.

이 때 상속을 하는 객체를 prototype ojbect라고 하며 상속을 받는 객체와 prototype link로 연결되어 있다.

 

let superObj = {superVal:'super'}
let subObj = {subVal:'sub'}
subObj.__proto__ = superObj; // 이렇게 하면 subObj는 superObj 객체의 자식이 됨
console.log('subObj.subVal =>', subObj.subVal); // subObj.subVal => sub 출력
console.log('subObj.superVal =>', subObj.superVal); // subObj.superVal => super 출력
subObj.superVal = 'sub';
console.log('superObj.superVal =>', superObj.superVal); // superObj.superVal => super 출력

console.log('subObj.superVal =>', subObj.superVal); // subObj.superVal => super 출력 하는 이유 

__proto__ 라는 프로토타입 링크를 통해서 subObj가  superObj의 자식이다 라고 링크를 걸어주니까

제일 먼저 subObj에서 superVal이라는 속성이 있는지를 찾는데 없으니까 그러면 __proto__ 속성이 담고 있는 객체에서 

superVal라는 속성이 있는지 찾고 있으니 그 값을 쓰는것임.

그렇다면 subObj.superVal = 'sub'; 이렇게 바꿔주면 값이 바뀔까? 정답은 아니다 

왜냐면 subObj에 객체 값을 바꾼것이지 __proto__가 가리키는 superObj를 바꾼게 아니다

 

__proto__를 대신할 수 있는 방법이 있는데 Object.create를 이용하는 것이다. 

let superObj = {superVal:'super'}
// let subObj = {subVal:'sub'}
// subObj.__proto__ = superObj;
let subObj = Object.create(superObj);
subObj.subVal = 'sub';
debugger;
console.log('subObj.subVal =>', subObj.subVal);
console.log('subObj.superVal =>', subObj.superVal);
subObj.superVal = 'sub';
console.log('superObj.superVal =>', superObj.superVal); // 바로 위에 코드 결과와 같은 결과가 나옴

이제 위에 개념들을 바탕으로 실습에서 활용할 예제들을 한번 살펴 보자면 

let kim = {
    name:'kim',
    first:10, second:20,
    sum:function(){return this.first+this.second}
}
 
// let lee = {
//     name:'lee',
//     first:10, second:10, 
//     avg:function(){
//         return (this.first+this.second)/2;
//     }  부모 객체에는 없는 메소드를 새로 만들어 줄 수도 있다
// }
// lee.__proto__ = kim;
 
let lee = Object.create(kim);
lee.name = 'lee';
lee.first = 10;
lee.second = 10;
lee.avg = function(){
    return (this.first+this.second)/2;
}
console.log('lee.sum() : ', lee.sum()); // lee에 sum이 없어도 kim객체를 상속하기 때문에 결과는 20이 나온다
console.log('lee.avg() : ', lee.avg());

객체와 함수 

call()

let kim = {name:'kim', first:10, second:20}
let lee = {name:'lee', first:10, second:10}
function sum(prefix){
    return prefix+(this.first+this.second);
}
// sum();
console.log("sum.call(kim)", sum.call(kim, '=> ')); // sum.call(kim) => 30 출력 
console.log("lee.call(lee)", sum.call(lee, ': ')); // lee.call(kim) : 20 출력

위에 적힌 코드를 보면 sum()은 어떤 객체에도 속해있지 않음. 그렇다면 객체 안에있는 first,second 는 어떻게 더할 수 있을까

이때 sum.call();을 이용하는데 이건 sum(); 과 같은 의미이다. 말그대로 함수를 실행해준다는 의미

이 call 메소드를 호출할때 인자로 kim을 준다면 저 this는 kim이 되는것임 그래서 함수가 객체에 속해 있지 않아도 

kim의 메소드가 되는 효과를 얻을 수 있다. 

call은 인자를 몇개를 받을 수 있는데 첫번째 인자로는 this를 대체하는 인자가 오고 두번째 인자로는 우리가 호출할려고 하는 

함수에 파라미터(매개변수)에 들어갈 인자값들이 들어오게 된다. 

console.log("sum.call(kim)", sum.call(kim, '=> '));

그래서 '=> '이 인자가 prefix가 있는 값으로 들어가게 되는것이다 

 

bind()

call()은 호출될때마다 this를 바꾸는데 비해 bind()는 실행되는 함수의 this값을 원하는 객체로 고정시키는 새로운 함수를 만들어낸다. call 처럼 bind도 그 함수가 호출될때마다 사용될 인자를 지정할 수 있다.

let kim = {name:'kim', first:10, second:20}
let lee = {name:'lee', first:10, second:10}
function sum(prefix){
    return prefix+(this.first+this.second);
}
// sum();
let kimSum = sum.bind(kim, '-> ');
console.log('kimSum()', kimSum()); // kimSum() -> 30 출력

즉 정리하자면 call은 실행되는 함수의 this값을 원하는 객체로 바꿔서 실행할 수 있게 해주고

bind는 실행되는 함수의 this값을 원하는 객체로 고정시키는 새로운 함수를 만들어낸다.

 

prototype vs __proto__

함수는 자바스크립트에서는 객체다. 객체이기 때문에 property(속성)을 가질 수 있다.

예를 들어서 이러한 함수를 정의를 했다고 가정을 해보자 

function Person(name,first,second) {
    this.name = name;
    this.first = first;
    this.second =second;
}

Person 이라는 새로운 객체가 생성이 되고 근데 객체가 하나 더생긴다 Person의 prototype 객체가 생긴다.

Person 뿐만이 아니라 모든 객체들이  메소드와 속성들을 상속 받기 위한 템플릿으로써 

프로토타입 객체(prototype object)를 가진다.

이 두개의 객체는 서로 연관되고 관련되어 있기 때문에 Person이라는 객체에는 내부적으로 prototype 이라는 속성이 

생기고 이 속성은 Person의 prototype 객체를 가리킨다

한마디로 Person.prototype 을 입력하면 Person의 prototype 객체를 의미한다는 것이다.

Person에 prototype 객체도 자신이 Person에 소속이라는 것을 표시하기 위해 constructor라는 속성을 만들고

이 속성은 Person을 가리키게 된다. 

 

이제 인스턴스 하나를 생성을 해주면 

function Person(name,first,second) {
    this.name = name;
    this.first = first;
    this.second =second;
}

Person.prototype.sum = function(){}

let kim = new Person('kim',10,20)

이때 kim이라는 객체에 __proto__ 가 생성이 된다. 이 속성이 누구를 가리키냐면 kim이라는

객체를 생성한 Person의 prototype을 가리키게 된다.

그러면 Person.prototype 을 입력해도 kim.__proto__ 을 입력해도 Person의 prototype을 가리키게 되는것이다.

 

그리고 코드를 몇줄 추가해 보자면

function Person(name,first,second) {
    this.name = name;
    this.first = first;
    this.second =second;
}

Person.prototype.sum = function(){}

let kim = new Person('kim',10,20)

console.log(kim.name)

kim.sum()
console.log(kim.name)

kim.name 이라는 것을 콘솔에 출력을 하려고 하면 일단 kim이라는 객체에 name 속성이 있는지 찾아보고 있다면 출력 

만약 없다면 kim.__proto__가 가리키는 객체에 name이 있는지 다시 찾아봄 

마지막줄 처럼 코딩을 하면 kim에 sum이 없으니 __proto__ 를 통해서 kim.__proto__가 가리키는 객체에

sum이 있는지를 찾는다

 

이미지를 보면 더 이해하기 쉬울듯 싶다

Comments