[제로초JS] chap10. 텍스트 RPG



깊은 복사하기(원본이 바뀌지 않는 것)
const a = 'b';
const c = ['d', true, 1];
const e = { g: 'h' };
const i = [{ j: 'k' }, { l: 'm' }];
const n = { o: { p: 'q' }};
아래는 정답안
const a2 = a;
const c2 = c.slice();
const e2 = { ...e };
const i2 = JSON.parse(JSON.stringify(i));
const n2 = JSON.parse(JSON.stringify(n));
- string, boolean, number 같은 기본자료형은 다른 변수에 대입만 하면 값이 복사가 되고, 복사한 값을 바꿔도 원본이 변하지 않는다.
- 내부에 객체가 들어있지 않은 배열은 slice 메서드만 사용해도 깊은 복사가 일어난다.
- 내부에 객체가 들어있지 않은 객체 리터럴은 ...(spread operator)를 사용하면 된다.
- !! 내부에 객체가 들어 있는 경우에는 복사하기가 힘들다. slice메서드나 spread는 내부 객체를 복사 대신 참조로 연결한다. 따라서 JSON.parse(JSON.stringify(객체))로 DeepCopy해야한다.
//객체 내부에 객체 리터럴이 있는 경우에 spreadOperator 와 JSON.parse(JSON.stringify(Obj))비교
const i = [{ j: 'k' }, { l: 'm' }];
const iCopy = [...i];
iCopy[0].j = 'KK';
console.log(i[0].j, iCopy[0].j, i[0].j === iCopy[0].j); //KK KK true
const iDeep = JSON.parse(JSON.stringify(i));
iDeep[0].j = 'KKK';
console.log(i[0].j, iDeep[0].j, i[0].j === iDeep[0].j); //KK KKK false
객체 만들기 (함수, class비교)
함수 사용
function createMonster(name, hp, att, xp) {
return { name, hp, att, xp };
}
const monster1 = createMonster('슬라임', 25, 10, 11);
const monster2 = createMonster('슬라임', 26, 10, 10);
const monster3 = createMonster('슬라임', 25, 11, 10);
function Monster(name, hp, att, xp) {
this.name = name;
this.hp = hp;
this.att = att;
this.xp = xp;
}
const monster1 = new Monster('슬라임', 25, 10, 11);
const monster2 = new Monster('슬라임', 26, 10, 10);
const monster3 = new Monster('슬라임', 25, 11, 10);
class 사용
class Monster {
constructor(name, hp, att, xp) {
this.name = name;
this.hp = hp;
this.att = att;
this.xp = xp;
}
}
const monster1 = new Monster('슬라임', 25, 10, 11);
const monster2 = new Monster('슬라임', 26, 10, 10);
const monster3 = new Monster('슬라임', 25, 11, 10);
Factory함수(객체반환함수) vs 생성자 함수(new붙인)를 도입한 클래스 문법
class Monster { //(효율적)클래스 문법에서는 한번 만든 attact과 heal 메서드는 계속 재사용 가능
constructor(name, hp, att, xp) {
this.name = name;
this.hp = hp;
this.att = att;
this.xp = xp;
}
attack(monster) {
monster.hp -= this.att;
this.hp -= monster.att;
}
heal(monster) {
this.hp += 20;
this.hp -= monster.att;
}
}
function createMonster(name, hp, att, xp) {
return { //(비효율)객체 생성때 마다 attact과 heal 메서드도 새로 생성
name, hp, att, xp,
attack(monster) {
monster.hp -= this.att;
this.hp -= monster.att;
},
heal(monster) {
this.hp += 20;
this.hp -= monster.att;
},
};
}
생성자 함수 방식에 메서드 추가
function Monster(name, hp, att, xp) {
this.name = name;
this.hp = hp;
this.att = att;
this.xp = xp;
}
Monster.prototype.attack = function(monster) {
monster.hp -= this.att;
this.hp -= monster.att;
};
Monster.prototype.heal = function(monster) {
this.hp += 20;
this.hp -= monster.att;
};
prototype이라는 속성에 추가해야 메서드 추가가 된다.
공장 함수와 달리 attact과 heal 메서드를 재사용함. 하지만, 생성자 함수와 프로토타입 메서드가 하나로 묶여 있지 않다
이런 문제점을 모두 해결 한 것이 클래스 문법이다.
생성자 함수와 메서드가 묶여 있어서 보기 편하고 메서드 함수를 매번 재생성해야하는 문제도 발생하지 않는다.
this (함수 선언문 vs 화살표 함수 일때 this가 가리키는것?)
document.addEventListener('click', function() {
console.log(this); // document
});
// 함수 선언문일 때만 document가 나오는 이유는
// click 이벤트가 발생하면 addEventListener 메서드가
// 콜백 함수의 this를 event.target으로 바꿔서 호출 하기 때문
document.addEventListener('click', () => {
console.log(this); //window
})
함수 선언문의 this는 bind메서드를 사용해서 직접 바꿀 수 있다
function a() {
console.log(this);
}
a.bind(document)(); // document
화살표 함수는 bind를 할 수 없다. 그러므로 this가 바뀌지 않아서 window가 그대로 나온다.
const b = () => {
console.log(this);
}
b.bind(document)(); //window
이런 이유로 addEventListener 안에서
함수 선언문을 사용하면 document가 출력되고,
화살표 함수를 사용하면 window가 출력된다.
클래스 상속
class Human {
constructor(name, age) {
this.name = name;
this.age = age;
}
printName() {
console.log(this.name);
}
printAge() {
console.log(this.age);
}
}
class developer extends Human {
constructor(name, age, languages) {
super(name, age);
this.languages = languages;
}
writeCode() {
console.log(`${this.languages.join()} 사용 가능!`);
}
}
const personA = new developer('haha', 20, ['html', 'css', 'js']);
personA.printName(); //haha
personA.printAge(); //20
personA.writeCode(); //html,css,js 사용 가능!
정리
1. window
window 객체는 브라우저를 가리키는 객체이다.
window 객체는 브라우저가 제공하는 기본 객체와 함수를 가진다.
document 객체나 console 객체도 실제로는 window.document, window.console을 뜻한다.
2. this
this는 상황에 따라 다른 값을 가진다
this는 기본적으로 window객체를 기리킨다.
그러므로 어떨 때 어떤 값을 가지는지 외워야 한다.
- 객체를 통해서 this를 사용하면 this는 해당 객체를 가리킴
- 특정 메서드는 콜백 함수의 this를 바꾼다. (ex. addEventListener...)
- this가 안바뀌길 원한다면 함수 선언문 말고 화살표 함수를 사용한다.
3. 참조, 깊은 복사, 얕은 복사
복사 - 어떤 값을 다른 변수에 대입할 때 기존 값과 참조 관계가 끊기는 것
객체가 아닌 값은 애초부터 참조 관계가 없으므로 복사만 된다.
얕은 복사 - 중첩된 객체가 있을 떄 가장 바깥 객체만 복사되고 내부 객체는 참조 관계를 유지하는 복사를 의미
깊은 복사 - 내부 객체까지 참조 관계가 끊겨서 복사되는 것을 의미
const array = [{ j: 'k' }, { l: 'm' }];
const reference = array; // 참조
const shallowCopy = [...array]; // 얕은 복사
const deepCopy = JSON.parse(JSON.stringify(array)); // 깊은 복사
console.log(array === reference); // true
console.log(array[0] === reference[0]); // true
console.log(array === shallowCopy); // false
console.log(array[0] === shallowCopy[0]); // true
console.log(array === deepCopy); // false
console.log(array[0] === deepCopy[0]); // false
4. 클래스
객체를 생성하는 템플릿 문법이다.
class 예약어로 클래스 선언, constructor 메서드 안에 기존 코드를 넣는다.
new를 붙여서 호출하면 constructor 함수가 실행되고 객체가 반환된다.
this는 생성된 객체 자신을 가리킨다
5. 클래스 상속
클래스끼리 extends 예약어로 상속 가능하다.
상속하는 클래스는 부모 클래스이고, 상속받는 클래스는 자식 클래스이다.
공통되는 속성이나 메서드는 부모 클래스로부터 상속받는다.
class Hero extends Unit {
constructor(game, name) {
super(game, name, 100, 10, 0); // 부모 클래스의 생성자 호출
this.lev = 1; // 그 외 속성
}
attack(target) {
super.attack(target); // 부모 클래스의 attack
//자식 클래스만의 동작
}
}
자식 클래스에서 super함수는 부모 클래스를 의미한다.
그리고 부모 클래스의 생성자에 인수를 전달한다.
공통되는 속성은 부모 클래스 것을 그대로 사용하고, 공통되지 않는 속성은 자식 클래스에 따로 선언한다.
메서드에서도 super 사용가능하다.
자식 클래스에서 super.메서드를 호출하는 것은 부모 클래스의 메서드를 호출하는 것과 같다.
자식 클래스에 메서드르 생성하지 않은 경우에도 부모 클래스에 메서드가 존재한다면 호출 가능하다.
최종 코드
깃 히스토리 참고: https://github.com/Harimad/FrontEnd/commits/main
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>10장 class 도입한 텍스트RPG</title>
</head>
<body>
<form id="start-screen">
<input type="text" id="name-input" placeholder="주인공 이름을 입력">
<button id="start">시작</button>
</form>
<div id="screen">
<span id="hero-name""></span>
<span id="hero-level"></span>
<span id="hero-hp"></span>
<span id="hero-xp"></span>
<span id="hero-att"></span>
</div>
<form id="game-menu" style="display: none;">
<div id="menu-1">1. 모험</div>
<div id="menu-2">2. 휴식</div>
<div id="menu-3">3. 종료</div>
<input type="text" id="menu-input">
<button id="menu-button">입력</button>
</form>
<form id="battle-menu" style="display: none;">
<div id="battle-1">1. 공격</div>
<div id="battle-2">2. 회복</div>
<div id="battle-3">3. 도망</div>
<input type="text" id="battle-input">
<button id="battle-button">입력</button>
</form>
<div id="message"></div>
<div id="monster-stat">
<span id="monster-name"></span>
<span id="monster-hp"></span>
<span id="monster-att"></span>
</div>
<script>
const $startScreen = document.querySelector('#start-screen');
const $gameMenu = document.querySelector('#game-menu');
const $battleMenu = document.querySelector('#battle-menu');
const $heroName = document.querySelector('#hero-name');
const $heroLevel = document.querySelector('#hero-level');
const $heroHp = document.querySelector('#hero-hp');
const $heroXp = document.querySelector('#hero-xp');
const $heroAtt = document.querySelector('#hero-att');
const $monsterName = document.querySelector('#monster-name');
const $monsterHp = document.querySelector('#monster-hp');
const $monsterAtt = document.querySelector('#monster-att');
const $message = document.querySelector('#message');
class Game {
constructor(name) {
this.monster = null;
this.hero = null;
this.monsterList = [
{name : '슬라임', hp: 25, att: 10, xp: 10},
{name : '스켈레톤', hp: 50, att: 15, xp: 20},
{name: '마왕', hp: 50, att: 35, xp: 50},
];
this.start(name);
}
start(name) {
$gameMenu.addEventListener('submit', this.onGameMenuInput);
$battleMenu.addEventListener('submit', this.onBattleMenuInput);
this.changeScreen('game');
this.hero = new Hero(this, name);
this.updateHeroStat();
}
changeScreen(screen) {
if (screen === 'start') {
$startScreen.style.display = 'block';
$gameMenu.style.display = 'none';
$battleMenu.style.display = 'none';
} else if (screen === 'game') {
$startScreen.style.display = 'none';
$gameMenu.style.display = 'block';
$battleMenu.style.display = 'none';
} else if (screen === 'battle') {
$startScreen.style.display = 'none';
$gameMenu.style.display = 'none';
$battleMenu.style.display = 'block';
}
}
onGameMenuInput = (event) => {
event.preventDefault();
const input = event.target['menu-input'].value;
if (input === '1') { // 모험
this.changeScreen('battle');
const randomIndex = Math.floor(Math.random() * this.monsterList.length);
const randomMonster = this.monsterList[randomIndex];
this.monster = new Monster(
this,
randomMonster.name,
randomMonster.hp,
randomMonster.att,
randomMonster.xp,
);
this.updateMonsterStat();
this.showMessage(`몬스터와 마주쳤다. ${this.monster.name}인 것 같다!`);
} else if (input === '2') { //휴식
} else if (input === '3') { // 종료
}
}
onBattleMenuInput = (event) => {
event.preventDefault();
const input = event.target['battle-input'].value;
if (input === '1') { // 공격
const {hero, monster} = this;
hero.attack(monster);
monster.attack(hero);
if (hero.hp <= 0) {
this.showMessage(`${hero.lev} 레벨에서 전사. 새 주인공을 생성하세요.`);
this.quit();
} else if (monster.hp <= 0) {
this.showMessage(`몬스터를 잡아 ${monster.xp} 경험치를 얻었다.`);
hero.getXp(monster.xp);
this.monster = null;
this.changeScreen('game');
} else {
this.showMessage(`${hero.att}의 데미지를 주고, ${monster.att}의 데미지를 받았다.`);
}
this.updateHeroStat();
this.updateMonsterStat();
} else if (input === '2') { // 회복
} else if (input === '3') { // 도망
}
}
updateHeroStat() {
const {hero} = this;
if (hero === null) {
$heroName.textContent = '';
$heroLevel.textContent = '';
$heroHp.textContent = '';
$heroXp.textContent = '';
$heroAtt.textContent = '';
return;
}
$heroName.textContent = hero.name;
$heroLevel.textContent = `${hero.lev}Lev`;
$heroHp.textContent = `HP: ${hero.hp}/${hero.maxHp}`;
$heroXp.textContent = `XP: ${hero.xp}/${15 * hero.lev}`;
$heroAtt.textContent = `ATT: ${hero.att}`;
}
updateMonsterStat() {
const {monster} = this;
if (monster === null) {
$monsterName.textContent = '';
$monsterHp.textContent = '';
$monsterAtt.textContent = '';
return;
}
$monsterName.textContent = monster.name;
$monsterHp.textContent = `HP: ${monster.hp}/${monster.maxHp}`;
$monsterAtt.textContent = `ATT: ${monster.att}`;
}
showMessage(text) {
$message.textContent = text;
}
quit() {
this.hero = null;
this.monster = null;
this.updateHeroStat();
this.updateMonsterStat();
$gameMenu.removeEventListener('submit', this.onGameMenuInput);
$battleMenu.removeEventListener('submit', this.onBattleMenuInput);
this.changeScreen('start');
game = null;
}
}
// class Hero {
// constructor(game, name) {
// this.game = game;
// this.name = name;
// this.lev = 1;
// this.maxHp = 100;
// this.hp = 100;
// this.xp = 0;
// this.att = 10;
// }
// attack(target) {
// target.hp -= this.att;
// }
// heal(monster) {
// this.hp += 20;
// this.hp -= monster.att;
// }
// getXp(xp) {
// this.xp += xp;
// if (this.xp >= this.lev * 15) { // 경험치를 다 채우면
// this.xp -= this.lev * 15;
// this.lev += 1;
// this.maxHp += 5;
// this.att += 5;
// this.hp = this.maxHp;
// this.game.showMessage(`레벨업! 레벨 ${this.lev}`);
// }
// }
// }
// class Monster {
// constructor(game, name, hp, att, xp) {
// this.game = game;
// this.name = name;
// this.maxHp = hp;
// this.hp = hp;
// this.xp = xp;
// this.att = att;
// }
// attack(target) {
// target.hp -= this.att;
// }
// }
let game = null;
$startScreen.addEventListener('submit', (event) => {
event.preventDefault();
const name = event.target['name-input'].value;
game = new Game(name);
})
class Unit {
constructor(game, name, hp, att, xp) {
this.game = game;
this.name = name;
this.maxHp = hp;
this.hp = hp;
this.xp = xp;
this.att = att;
}
attack(target) {
target.hp -= this.att;
}
}
class Hero extends Unit {
constructor(game, name) {
super(game, name, 100, 10, 0); // 부모 클래스의 생성자 호출
this.lev = 1; // 그 외 속성
}
attack(target) {
super.attack(target); //부모 클래스의 attack
//부모 클래스 attack 외의 동작
}
heal(monster) {
this.hp += 20;
this.hp -= monster.att;
}
getXp(xp) {
this.xp += xp;
if (this.xp >= this.lev * 15) { // 경험치를 다 채우면
this.xp -= this.lev * 15;
this.lev += 1;
this.maxHp += 5;
this.att += 5;
this.hp = this.maxHp;
this.game.showMessage(`레벨업! 레벨 ${this.lev}`);
}
}
}
class Monster extends Unit {
constructor(game, name, hp, att, xp) {
super(game, name, hp, att, xp);
}
}
</script>
</body>
</html>