본문 바로가기

Web Development/Front-end

[Zerocho-13] 자스스톤 (Call by Value, Constructor, this, 엄격모드)

안녕하세요. 오늘은 이전시간에 배운 내용과 연계해서 이론을 조금 더 깊게 들어가보는시간을 가지겠습니다.

우선 지난 시간에 했던 1단계 복사와, 2단계 깊은 복사에 대해서 잠깐 복습하고 넘어갈꼐요!

 

//1단계 복사
let obj = { a: 1, b: 2 };
let copy = Object.assign({}, obj);
console.log(copy === obj);

//다중단계 복사. *단, 프로토타입은 복사할 수 없음.
let obj2 = { a: 1, b: {c:2} };
let copied = JSON.parse(JSON.stringify(obj2));
console.log(copied === obj2);

객체(객체,배열,함수) 는 이런식으로 복사 할 수 있다고 했었습니다.

 

1. Call by Value

Call by Value 는 뭘까요?

인자와 매개변수 간의 관계를 의미합니다. ( 값을 값으로 불러온다 )

 

우선 간단한 예시를 봅시다.

//매개변수와 인자
const func1 = (x) => {
    console.log(x);
}

func1(5);

이렇게하면 무엇이 찍힐까요?, 당연히 콘솔에는 5가 찍힐 것입니다. 그냥 함수에요~

 

그럼 이번에는 이렇게 해봅시다.

//매개변수가 원시값인 경우,
const func2 = (x) => {
    x = 10;
    console.log(x); // ->> 10;
}

let i = 5;
func2(i); //i 값이 참조가 아닌 복사가 되어 매개변수에 대입됨.
console.log(i); //렉시컬 스코프 ! ->> 5;

func2함수를 만들고 매개변수로 x라고 이름을 붙여줬습니다.

그리고 안에서는, x값을 10으로 바꿔줫고, 콘솔에 출력했습니다.

 

이제 실행하는 아래 부분에서,

i 변수를 선언하고, 5라고 값을 줬습니다.

그리고 함수에 i매개변수를 줘서 실행했습니다. 그럼 i는 x에 대입되서, x값을 10으로 만들었으니 콘솔에는 10이 출력되겠죠.

 

그런데 콘솔로그(i)라고 한다면 무엇이 출력될까요?

이전에 배웠던 렉시컬 스코프에 의해서, i값이 바라보는 값은 ' let i = 5 '이 부분이라는 점이 변치 않아요.

따라서, 5라는 값이 출력됩니다. 함수 내에서 무슨짓을 하던, 복사가 되었기에, i값은 5라고 출력되죠.

 

그럼 다음은, 이렇게 해볼까요?

// //매개변수가 원시값이 아닌경우,
const func3 = (x) => {
    x.a = 10;
    console.log(x);
}

let i = { a : 5 };
func3(i); // i값이 복사가 아닌 참조되어 함수에 대입 !
console.log(i);

위와 같은데, 이번에는 let i 값을 객체를 줬습니다.

그럼, 이를 함수에 대입했을 때, 복사가아닌 참조 ! 가 되겟죠? 그래서,

func3(i)를 실행하든, console.log(i)를 출력하던 변한 i값인 {a : 10} 이렇게 출력됩니다.

 

이런걸보고 이제, 원시값(문자, 수, 불리언)은 call by Value로 동작하고[복사],

객체(객체, 배열, 함수)는 call by Reference로 동작한다[참조] 라고 많이 하는데,

Javascript에는 call by Reference가 없습니다.

 

아래 예제를 봅시다.

// call by Reference가 아닌 이유,
const func4 = (x) => {
    x = 1 ;
    console.log(x);
}

let i = { a : 5 };
func4(i);
console.log(i);

자 여기서 바뀐점은, 객체값을 -> 원시값으로 수정해줬죠?

근데, 이게 call by Reference였다면, 맨아래 콘솔로그(i)를 했을 때, 값은 1;이라고 찍혀야 합니다.

그런데 자바스크립트에서는 이를  { a : 5 } 라고 출력합니다.

 

왜냐하면,

자바스크립트에는 포인터라는 개념이없어요.

그냥 "원시값은 복사가되고, 객체값은 참조가 된다" 입니다.

 

객체 속성을 수정할 때는 참조로써 동작하지만,

객체 자체를 수정할 때는 참조가 성립되지 않습니다.

따라서, call by Reference가 아니며, Javascript에는 '포인터'라는 개념이 없습니다.

모든것이 다 call by Value 입니다.

 

 

2. Constructor

constructor는 함수입니다.

이전에 공부한 팩토리패턴과 상당히 유사하죠.

어떻게 만드는지 살펴보겠습니다.

//프로토타입
let 프로토타입 = {
    type : 'card',
}

//생성자함수(Constructor)
function Card(name, damage, hp){
    this.name = name;
    this.damage = damage;
    this.hp = hp;
}
Card.prototype = 프로토타입;

//생성해볼까요?
let monkey = new Card('monkey', 20, 1200);
let cat = new Card('cat', 10, 500);

//읽어볼까요?
console.log(monkey);
console.log(cat);

이런식으로 생성자 함수를 만들어줍니다. 생성자함수의 이름은 첫글자를 대문자로 시작해요.

그리고 이름과, 데미지, hp등을 받아와서, this에 삽입해주고, 프로토타입을 적용시켜줬습니다.

그리고, 이를 이용해서 카드를 생성할 때, new를 사용해서 카드를 생성해줬습니다.

뭔가 팩토리패턴과 상당히 유사한데요.

무슨차이가 있나 살펴보겠습니다.

팩토리패턴의 코드입니다.

//프로토타입
let 프로토타입 = {
    type : 'card',
}
//팩토리패턴
function card_Factory(name, damage, hp){
    let card = Object.create(프로토타입);
    card.name = name;
    card.damage = damage;
    card.hp = hp;
    return card;
}
let tiger = card_Factory('tiger', 100, 2000);
console.log(tiger);

이전시간에 다루었던 내용을 짬뽕해서 만든 팩토리 패턴입니다.

그리고 두개의 결과값의 차이를 한번 살펴보겠습니다.

 

Constructor의 결과값
팩토리패턴의 결과값

Constructor로 만든 결과값은 앞에 Card라는 이름이 붙습니다. 즉, 어떤 생성자로 생성했는지 나옵니다.

반면에, 팩토리패턴으로 만든 결과값에는 무엇으로 만들었는지 출력되지 않습니다.

그리고, 프로토타입을 적용하는 방식이 조금 다릅니다.

팩토리함수에서는 Object.create(프로토타입함수),

생성자함수에서는 생성자.prototype = 프로토타입함수

이런식으로 말이죠.

 

생성자(Constructor)은 객체지향프로그래밍에 좀 가깝고,

팩토리(Factory)는 함수형 프로그래밍에 조금 더 가까운 느낌입니다.

둘다 실무에서는 많이 사용한다고 하네요.

 

그리고, 생성자(Constructor)를 사용하면서 this. 가 등장하는데요.

this에 대해서 한번 살펴보겠습니다.

 

3. this && 엄격모드

바로 코드를 보겠습니다.

프로토타입을 만들고, Constructor(생성자)를 만들어줬고, 거기에 프로토타입을 적용시켰습니다.

그런데, 새 카드를 생성할 때, new를 붙이지 않고, 한번 만들어보겠습니다.

그러면 , 이런식으로 window에 객체를 생성한 꼴이 되어버리죠.

Constructor의 적용을 받지 않았기 때문입니다. 그냥 Card라는 것을 만든거죠.

 

 

그런데 new를 붙임으로써, 나는 "Card 생성자를 이용해 만들어진 객체야 !"하면서,

this는 더이상 윈도우가 아닌, let monkey << 이렇게 생성해주었으므로,

this === monkey 가 되는겁니다.

이런식으로 말이죠.

그런데 strict 모드를 적용하면요.

monkey의 name값을 출력했으나, 이는 undefined라고 됩니다.

 

즉, this는 본래 window << 를 가르키고 있으며,

엄격모드(use strict)를 적용하면,  undefined가 됩니다.

 

생성자 함수내에서 사용할 때는,

생성자를 이용해 만든 각 변수가 this가 됩니다.

라고 이해할 수 있습니다.

 

 

4. CloneNode(true);

cloneNode함수는, 노드(요소)를 복제하는 함수입니다.

요소.cloneNode(true || false)

이걸 이용해서, HTML요소를 전체를 복사할 수도 있고,

혹은, false값을 주면, 자식태그들은 복사가 안됩니다.

 

예제를 보겠습니다.

study.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div class='div'>
        <h1>h1태그</h1>
        <h2>h2태그</h2>
    </div>
    <script src='study.js'></script>
</body>
</html>

이렇게 작성해주고,

study.js

let div = document.querySelector('.div');

let cloning1 = div.cloneNode(true);
let cloning2 = div.cloneNode(false);

document.body.appendChild(cloning1);
document.body.appendChild(cloning2);

이렇게 작성해주면, div태그를 쿼리셀렉터로 선택했고, 그 div태그 안에는 h1과 h2등이있죠.

cloning1에서는 div태그에 .cloneNode(true) << 이런식으로 값을줬어요. 그럼, div태그와, h1,h2태그가 모두 복사됩니다. 똑같이요.

cloning2에서는 div태그에 .cloneNode(false) << 이런식으로 값을줬습니다. 그럼, h1,h2태그는 제외하고, div태그만이 복사됩니다.

결과창

이런식으로 말이죠.

 

5. 삼항연산자

삼항연산자는 조건문이구요.

조금 더 간결하고 짧게 쓰기위해 있습니다.

조건식이 복잡하지 않고, 간단한데, if문을 사용하기에는 코드가 길어질 때 사용합니다.

 

조건 ? 참 : 거짓

기본적으로 위와 같이 생겼습니다.

 

코드로 살펴보겠습니다.

let name = 'blockmonkey';

//기본 조건문 if
if(name === 'blockmonkey'){
    console.log(`Yes!`);
} else {
    console.log(`No !`);
}

//삼항연산자 조건문
name === 'blockmonkey' ? console.log(`Yes!`) : console.log(`No !`);

이렇게 사용할 수 있습니다.

 

6. 자스스톤 영상 & 코드

자스스톤 게임은, 개인적으로 완성에 실패해서, 코드리뷰용도로, 원본코드와 게임영상을 올립니다.

하스스톤게임을 베껴서 만든 것 이라고 합니다.

 

자스스톤

 

stone.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>자스스톤</title>
    <style>
        #my, #rival {
            display: inline-block;
            vertical-align: top;
            margin-right: 50px;
        }
        .turn {
            border: 1px solid blue;
        }
        #rival {
            vertical-align: bottom;
        }
        #my-deck, #rival-deck {
            display: inline-block;
            vertical-align: top;
            width: 300px;
            background: silver;
        }
        #rival-deck, #rival-hero, #rival-cards, #my-cards, #my-deck, #my-hero {
            text-align: center;
        }
        .card {
            width: 75px;
            height: 120px;
            display: inline-block;
            position: relative;
            border: 1px solid black;
            margin-bottom: 10px;
            background: white;
        }
        .card-name {
            text-align: center;
            font-size: 14px;
        }
        .card-att, .card-hp, .card-cost {
            font-size: 16px;
            font-weight: bold;
            text-align: center;
            line-height: 30px;
            width: 30px;
            height: 30px;
            border-radius: 50%;
            display: inline-block;
            position: absolute;
            bottom: 0;
            border: 1px solid black;
        }
        .card-cost {
            bottom: auto;
            top: 0;
            left: 0;
            background-color: blue;
            color: white;
        }
        .card-att {
            left: 0;
            background-color: yellow;
        }
        .card-hp {
            right: 0;
            background-color: red;
            color: white;
        }
        .card-hidden {
            display: none;
        }
        .card-selected {
            border: 2px solid red;
        }
        .card-turnover {
            background: gray;
        }
        #turn-btn {
            float: right;
            position: relative;
            top: -23px;
        }
    </style>
</head>
<body>
    <div id="rival">
        <div>코스트: <span id="rival-cost">10</span>/<span>10</span></div>
        <div id="rival-hero"></div>
        <div id="rival-cards" style="height: 150px">
    
        </div>
    </div>
    <div id="rival-deck">
        <div>덱</div>
    </div>
    <hr />
    <button id="turn-btn">턴넘기기</button>
    <div id="my" class="turn">
        <div id="my-cards" style="height: 150px">
    
        </div>
        <div id="my-hero">
        </div>
        <div>코스트: <span id="my-cost">10</span>/<span>10</span></div>
    </div>
    <div id="my-deck">
        <div>덱</div>
    </div>
    <div class="card-hidden">
        <div class="card">
            <div class="card-cost"></div>
            <div class="card-att"></div>
            <div class="card-hp"></div>
        </div>
    </div>
    <script src=./stone.js></script>
</body>
</html>

 

stone.js

var 상대 = {
    영웅: document.getElementById('rival-hero'),
    덱: document.getElementById('rival-deck'),
    필드: document.getElementById('rival-cards'),
    코스트: document.getElementById('rival-cost'),
    덱data: [],
    영웅data: [],
    필드data: [],
    선택카드: null,
    선택카드data: null,
  };
  
  var 나 = {
    영웅: document.getElementById('my-hero'),
    덱: document.getElementById('my-deck'),
    필드: document.getElementById('my-cards'),
    코스트: document.getElementById('my-cost'),
    덱data: [],
    영웅data: [],
    필드data: [],
    선택카드: null,
    선택카드data: null,
  };
  
  var 턴버튼 = document.getElementById('turn-btn');
  var 턴 = true; // true면 내턴, false면 니턴
  
  function 덱에서필드로(데이터, 내턴) {
    var 객체 = 내턴 ? 나 : 상대; // 조건 ? 참 : 거짓; 삼항연산자.
    var 현재코스트 = Number(객체.코스트.textContent);
    if (현재코스트 < 데이터.cost) {
      return 'end';
    }
    var idx = 객체.덱data.indexOf(데이터);
    객체.덱data.splice(idx, 1);
    객체.필드data.push(데이터);
    필드다시그리기(객체);
    덱다시그리기(객체);
    데이터.field = true;
    객체.코스트.textContent = 현재코스트 - 데이터.cost;
  }
  
  function 필드다시그리기(객체) {
    객체.필드.innerHTML = '';
    객체.필드data.forEach(function(data) {
      카드돔연결(data, 객체.필드);
    });
  }
  function 덱다시그리기(객체) {
    객체.덱.innerHTML = '';
    객체.덱data.forEach(function(data) {
      카드돔연결(data, 객체.덱);
    });
  }
  function 영웅다시그리기(객체) {
    객체.영웅.innerHTML = '';
    카드돔연결(객체.영웅data, 객체.영웅, true);
  }
  
  function 화면다시그리기(내화면) {
    var 객체 = 내화면 ? 나 : 상대; // 조건 ? 참 : 거짓; 삼항연산자
    필드다시그리기(객체);
    덱다시그리기(객체);
    영웅다시그리기(객체);
  }
  
  function 턴액션수행(카드, 데이터, 내턴) {
    // 턴이 끝난 카드면 아무일도 일어나지 않음
    var 아군 = 내턴 ? 나 : 상대;
    var 적군 = 내턴 ? 상대 : 나;
    if (카드.classList.contains('card-turnover')) {
      return;
    }
    // 적군 카드면서 아군 카드가 선택되어 있고, 또 그게 턴이 끝난 카드가 아니면 공격
    var 적군카드 = 내턴 ? !데이터.mine : 데이터.mine;
    if (적군카드 && 아군.선택카드) {
      데이터.hp = 데이터.hp - 아군.선택카드data.att;
      if (데이터.hp <= 0) { // 카드가 죽었을 때
        var 인덱스 = 적군.필드data.indexOf(데이터);
        if (인덱스 > -1 ) { // 쫄병이 죽었을 때
          적군.필드data.splice(인덱스, 1);
        } else { // 영웅이 죽었을 때
          alert('승리하셨습니다!');
          초기세팅();
        }
      }
      화면다시그리기(!내턴);
      아군.선택카드.classList.remove('card-selected');
      아군.선택카드.classList.add('card-turnover');
      아군.선택카드 = null;
      아군.선택카드data = null;
      return;
    } else if (적군카드) { // 상대 카드면
      return;
    }
    if (데이터.field) { // 카드가 필드에 있으면
      //  영웅 부모와 필드카드의 부모가 다르기때문에 document에서 모든 .card를 검색한다
      // 카드.parentNode.querySelectorAll('.card').forEach(function (card) {
      document.querySelectorAll('.card').forEach(function (card) {
        card.classList.remove('card-selected');
      });
      카드.classList.add('card-selected');
      아군.선택카드 = 카드;
      아군.선택카드data = 데이터;
    } else { // 덱이 있으면
      if (덱에서필드로(데이터, 내턴) !== 'end') {
        내턴 ? 내덱생성(1) : 상대덱생성(1);
      }
    }
  }
  
  function 카드돔연결(데이터, 돔, 영웅) {
    var 카드 = document.querySelector('.card-hidden .card').cloneNode(true);
    카드.querySelector('.card-cost').textContent = 데이터.cost;
    카드.querySelector('.card-att').textContent = 데이터.att;
    카드.querySelector('.card-hp').textContent = 데이터.hp;
    if (영웅) {
      카드.querySelector('.card-cost').style.display = 'none';
      var 이름 = document.createElement('div');
      이름.textContent = '영웅';
      카드.appendChild(이름)
    }
    카드.addEventListener('click', function() {
      턴액션수행(카드, 데이터, 턴);
    });
    돔.appendChild(카드);
  }
  function 상대덱생성(개수) {
    for (var i = 0; i < 개수; i++) {
      상대.덱data.push(카드공장());
    }
    덱다시그리기(상대);
  }
  function 내덱생성(개수) {
    for (var i = 0; i < 개수; i++) {
      나.덱data.push(카드공장(false, true));
    }
    덱다시그리기(나);
  }
  function 내영웅생성() {
    나.영웅data = 카드공장(true, true);
    카드돔연결(나.영웅data, 나.영웅, true);
  }
  function 상대영웅생성() {
    상대.영웅data = 카드공장(true);
    카드돔연결(상대.영웅data, 상대.영웅, true);
  }
  
  function Card(영웅, 내카드) {
    if (영웅) {
      this.att = Math.ceil(Math.random() * 2);
      this.hp = Math.ceil(Math.random() * 5) + 25;
      this.hero = true;
      this.field = true;
    } else {
      this.att = Math.ceil(Math.random() * 5);
      this.hp = Math.ceil(Math.random() * 5);
      this.cost = Math.floor((this.att + this.hp) / 2);
    }
    if (내카드) {
      this.mine = true;
    }
  }
  function 카드공장(영웅, 내카드) {
    return new Card(영웅, 내카드);
  }
  
  function 초기세팅() {
    [상대, 나].forEach(function (item) {
      item.덱data = [];
      item.영웅data = [];
      item.필드data = [];
      item.선택카드 = [];
      item.선택카드data = [];
    });
    상대덱생성(5);
    내덱생성(5);
    내영웅생성();
    상대영웅생성();
    화면다시그리기(true); // 상대화면
    화면다시그리기(false); // 내화면
  }
  
  턴버튼.addEventListener('click', function() {
    var 객체 = 턴 ? 나 : 상대;
    document.getElementById('rival').classList.toggle('turn');
    document.getElementById('my').classList.toggle('turn');
    필드다시그리기(객체);
    영웅다시그리기(객체);
    턴 = !턴; // 턴을 넘기는 코드
    if (턴) {
      나.코스트.textContent = 10;
    } else {
      상대.코스트.textContent = 10;
    }
  });
  
  초기세팅(); // 진입점