기존에는 프로토타입 체인으로 객체지향 언어의 상속과 캡슐화(은닉화) 등의 OOP 문법을 구현 —> 낯설고 어렵다
class Person {
// 생성자
constructor(name, age) {
this.name = name;
this.age == age;
}
testHello() {
console.log(`안녕하세요~ ${this.name}님`);
}
}
const p1 = new Person("홍길동", 20);
console.log(p1.name); // 홍길동
console.log(p1.age); // 20
p1.testHello(); // 안녕하세요~ 홍길동님
const p2 = new Person("이순신", 30);
console.log(p2.name); // 이순신
console.log(p2.age); // 30
p2.testHello(); // 안녕하세요~ 이순신님
console.log(p1 instanceof Person); // true
클래스 타입체크 --> function
console.log(typeof Class); // function
// --> 함수도 객체
클래스 선언문의 내부동작
함수 레벨 스코프 vs 블록 레벨 스코프
var a = 111;
console.log(a);
{
var a = 333; // 전역변수 --> 함수레벨 스코프, var 키워드는 중복이 허용(선언이 가능), 호이스팅 O. 함수가 아닌 변수 선언은 모두 전역
}
console.log(a); // 333
Hoist = (국기를, 닻을) 끌어올리다
자바스크립트는 기본적으로 모든 선언문(var, let, const, function, class)을 호이스팅
=== 스코프 안의 어디서든 변수 선언은 최상위에서 선언한 것과 동일
var testA;
console.log("testA 값은" + testA); // undefined
console.log("testAA 값은" + testAA); // undefined --> hoisting과 값 할당을 동시에
var testAA;
let testB;
console.log("testB 값은" + testB); // undefined
console.log("testBB 값은" + testBB); // Error --> hoisting은 되었지만 값은 변수 선언시에 할당됨
let testBB;
console.log(Person); // 초기화 오류
class Person {}
// var vs let, const == class 호이스팅 에러 비교
var str1 = "Hello, World";
const testFun = function () {
console.log(str1); // undefined
var str1 = "Hello Korea";
console.log(str1); // Hello Korea
console.log(str2);
let str2 = "Helloo Koreaa"; // Error --> 초기화 Reference Error(참조 에러) 발생
};
testFun(); // undefined
class Parent {}
class Child extends Parent {} // 정상 작동
class Child2 extends Parent2 {} // Error --> 초기화 Reference Error(참조 에러) 발생
class Parent2 {}
무명 표현식
const Person = class {
constructor(name, age) {
this.name = name;
this.age = age;
}
};
const p1 = new Person("홍길동", 20);
console.log(p1.name);
console.log(p1.age);
console.log(Person.name); // Person --> 클래스명이 없으므로 암묵적으로 변수명이 name 속성값이 된다
유명 표현식
const Person = class namedPerson {
constructor(name, age) {
this.name = name;
this.age = age;
}
};
const p1 = new Person("이순신", 30);
console.log(p1.name);
console.log(p1.age);
console.log(Person.name); // namedPerson --> 클래스명이 있으므로 name 속성값이 클래스명이 된다
const p2 = new namedPerson("강감찬", 40); // Error
// 생성자 함수
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function () {
console.log("안녕하세요");
};
const p1 = new Person("홍길동", 20);
p1.say(); // 안녕하세요
// class
class Person2 {
constructor(name, age) {
this.name = name;
this.age = age;
}
say() {
console.log("안녕");
}
}
const p2 = new Person2("이순신", 30);
p2.say(); // 안녕
클래스는 기본적으로 엄격 모드(use strict) —> 자동적용
클래스 메서드는 열거 대상이 X —> 클래스와 같은 이름의 ‘프로토타입 객체’의 속성에 추가된 메서드들 열거 X
for (let i in p1) {
console.log(i); // name, age, say
}
for (let i in p2) {
console.log(i); // name, age
}
객체의 모든 특성을 보고자 할때 사용
정적 메서드 —> Object.getOwnPropertyNamse(객체명)
—> Object.getPrototype() > 지정된 객체의 내부 Prototype 속성값을 반환
const p1 = {
eat() {},
run() {},
rest() {},
};
class Person2 {
constructor(name, age) {
this.name = name;
this.age = age;
}
eat() {}
run() {}
rest() {}
}
new p2 = new Person2('홍길동', 20)
// 열거 대상 X --> 메서드가 보이지않음
for(let i in p2){
console.log('p2의 멤버들 = '+ i); // name, age
}
console.log(p2); // Person2{} --> 메서드는 보이지 않음
console.log(Object.getPropertyOf(p2)); // 지정된 객체의 내부 Prototype 속성값을 반환
console.log(Object.keys(p1)); // ['eat', 'run', 'rest']
console.log(Object.keys(Object.getPropertyOf(p2))); // []
// -------------------------- //
console.log(Object.getOwnPropertyNames(p1)) // ['eat', 'run', 'rest']
console.log(Object.getOwnPropertyNames(Object.getPrototypeOf(p2))) // ['constructor', 'eat', 'run', 'rest']
class Animal {
constructor(name) {
this.name = name;
}
cry() {
console.log(this.name + "가 웁니다");
}
move() {
console.log(this.name + "가 이동합니다.");
}
}
const ani1 = new Animal("호랑이");
console.log(ani1.name);
ani1.cry();
ani1.move();
// 속성과 메서드 추가
Animal.prototype.age = 4;
Animal.prototype.run = function () {
console.log(this.name + "가 뛰어갑니다");
};
console.log(Animal.name); // Animal
console.log(Animal === Animal.prototype.constructor); // true
class Animal {}
const ani1 = new Animal();
ani1.name = "악어";
ani1.age = 4;
부모가 가진 자원을 그대로 상속받아 자식 클래스를 생성 및 확장해서 만들 수 있다.
—> extends 키워드 사용
// 부모 클래스
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
eat() {
console.log(this.name + "먹다");
}
}
// 자식 클래스
class Tiger extends Animal {}
const t1 = new Tiger("호랑이", 2);
console.log(t1.name); // 호랑이
console.log(t1.age); // 2
class Animal {
constructor(group) {
this.group = group;
}
getGroup() {
return this.group;
}
eat() {}
sleep() {}
bark() {
return "짖다";
}
}
class Mammal extends Animal {
constructor(name, finger, toe, eyesight) {
super(Mammal.name); // 상위 클래스 호출해줘야함 --> this보다 super가 당연히 먼저 호출되어야함 > 참조 오류
this.name = name;
this.finger = finger;
this.toe = toe;
this.eyesight = eyesight;
}
run() {
return `${this.name} (${this.group}) 뛴다`;
}
bark() {
return `${this.name} (${this.group}) 크게 짖다`;
}
}
const tiger = new Mammal("호랑이", 10, 10, 1.5);
console.log(tiger.name); // super 호출 안하면 Error
console.log(tiger.getGroup()); // undefined --> super('') 인자값을 넣어주면 '' 안에 내용이 출력 > Mammal
console.log(tiger.eyesight); // 1.5
console.log(tiger.bark()); // override --> 기각하다, 무시하다/ ~보다 더 우선하다, 우선시하다
자식 클래스에서 constructor를 정의해주면 super();를 꼭 작성해주어야 한다. —> constructor 정의 안하면 super 안써도 에러나지 않음
class Mammal extends Animal{
~
move() {
console.log(`${this.name} 이동하면서 ${super.bark()}`);
}
~
}
tiger.move(); // 호랑이 이동하면서 짖다
console.log(Object.getOwnPropertyNames(tiger));
// ['group', 'name', 'finger', 'toe', 'eyesight']
console.log(Object.getOwnPropertyNames(Object.getPrototypeOf(tiger)));
// ['constructor', 'run', 'bark', 'move']
메서드 앞에 static 키워드를 붙여주면 따로 인스턴스(객체)를 생성하지 않아도 메서드 호출이 가능 <—> 반대로 인스턴스(객체)를 통해서 호출하는 것은 불가능
class Animal {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
sleep() {
console.log("잠자다");
}
static sleep2() {
console.log("Zzz...");
}
}
const tiger = new Animal("호랑이");
tiger.sleep();
console.log(tiger.getName());
tiger.sleep2(); // Error
Animal.sleep2(); // Zzz...
—> constructor 사용
tiger.constructor.sleep2(); // Zzz...
class Add {
static plus(x) {
x = x || 100;
return x + 1000;
}
}
class ChildAdd extends Add {
static plus(x) {
return super.plus(x) + super.plus(x) + super.plus(x);
}
}
console.log(Add.plus()); // 1100
console.log(Add.plus(500)); // 1500
console.log(ChildAdd.plus()); // 3300
console.log(ChildAdd.plus(300)); // 3900
console.log(ChildAdd.plus(30)); // 3090
const add1 = new Add();
console.log(add1.plus()); // Error
console.log(add1.constructor.plus()); // 1100
console.log(add1.constructor.plus(30)); // 1030
const cass1 = new ChildAdd();
console.log(cass1.plus()); // Error
console.log(cass1.constructor.plus(30)); // 3090
클래스 문법을 지원하기 전, object.create로 상속을 구현
첫 번째 인자(부모객체)로 들어온 해당 객체(부모)의 '프로토타입 객체'를 복제
이렇게 복제된 것을 --> '자식객체.prototype'에 할당
부모객체.call(=apply)(this, 인자값)
class의 super 역할
—> class에서는 extends와 super를 통해서 상속을 구현, class 이전에는 Object.create(), prototype, call 등을 이용해서 구현하고 있다.
// 부모 클래스
function ParentClass(name, age) {
this.name = name;
this.age = age;
}
ParentClass.prototype.sayHello = function () {
console.log(`Hello ${this.name}`);
};
// 자식 클래스
function ChildClass(name, age, power) {
ParentClass.call(this, name, age); // class의 super 역할
this.power = power;
}
ChileClass.prototype = Object.create(ParentClass.prototype);
ChildClass.prototype.constructor = ChildClass;
ChildClass.prototype.move = function () {
console.log(`${this.name} is moving...`);
};
const c1 = new ChildClass("batman", 20, 900);
console.log(c1);
console.log(c1.name); // batman
console.log(c1.age); // 20
console.log(c1.power); // 900
c1.sayHello(); // Hello batman
c1.move(); // batman is moving...
console.log(c1.___proto__); // ChildClass의 prototype 객체
console.log(c1.___proto__.___proto__); // ParentClass의 prototype 객체
자바스크립트에는 전통적인 다른 OOP에서와 같은 접근자(private, public, protected)가 없고, 기본적으로 모두 public
—> 해당 클래스의 인스턴스(객체)를 통해 외부에서 항상 ‘참조’가 가능
—> 생성자 메서드내에서 속성명 앞에 ‘_’를 임의로 붙여 private라고 암묵적으로 표시 —> this._name
class Person {
age = 20;
power = 900;
#finger = 10; // private --> 외부에서 직접 접근할 수 없다
#toe = 10; // private
}
const p1 = new Person();
console.log(p1); // {age:20, power:900}
console.log(p1.age); // 20
console.log(p1.power); // 900
p1.age = 10;
console.log(p1.age); // 10
console.log(p1.finger); // undefined
console.log(p1.#finger); // Private Error
console.log(p1.#toe); // Private Error
class Animal {
#age = 4;
bark() {
this.#age = 8; // 클래스 내부에서의 접근은 가능
return `${this.#age}살 짜리 개가 짖고있다`;
}
}
const ani1 = new Animal();
console.log(ani1.#age); // Private Error
console.log(ani1.bark()); // 8살 짜리 개가 짖고있다
class Animal2 {
#age = 4;
bark() {
this.#age = 8;
return `${this.#age}살 짜리 개가 짖고있다`;
}
#privateMethod() {
return `Hello, Private Method`;
}
getPrivateMethod() {
return this.#privateMethod(); // get메서드를 통해 private를 호출
}
}
const ani2 = new Animal2();
console.log(ani2.#privateMethod()); // Private Error
console.log(ani2.getPrivateMethod()); // Hello, Private Method
class Animal3 {
static #privateStaticMethod() {
return "Hello, Private Static Method";
}
// this로 접근
static getPrivateStaticMethod() {
return this.#privateStaticMethod();
}
// 클래스명으로 접근
static getPrivateStaticMethod_ClassName() {
return Animal3.#privateStaticMethod();
}
}
// static 메서드는 바로 호출 가능
console.log(Animal3.#privateStaticMethod()); // Private Error
console.log(Animal3.getPrivateStaticMethod()); // Hello, Private Static Method
console.log(Animal3.getPrivateStaticMethod_ClassName()); // Hello, Private Static Method
class Person {
age = 100;
constructor(id, name, email) {
this._id = id;
this._name = name;
this._email = email;
}
get id() {
return this._id;
}
get name() {
return this._name;
}
get email() {
return this._email;
}
info() {
return `ID : ${this.id}, NAME : ${this.name}, EMAIL : ${this.email}`;
}
}
const p1 = new Person("batman", "배트맨", "batman@gmail.com");
console.log(p1.age); // 100
console.log(p1.name()); // Error
console.log(p1.name); // 배트맨 --> getter 사용시 괄호 생략!
console.log(p1.info()); // ID : batman, NAME : 배트맨, EMAIL : batman@gmail.com
p1._name = "슈퍼맨";
console.log(p1.info()); // ID : batman, NAME : 슈퍼맨, EMAIL : batman@gmail.com
class Person {
age = 100;
constructor(id, name, email) {
this._id = id;
this._name = name;
this._email = email;
}
get id() {
return this._id;
}
get name() {
return this._name;
}
get email() {
return this._email;
}
set id(value) {
this._id = value;
}
set name(value) {
this._name = value;
}
set email(value) {
this._email = value;
}
info() {
return `ID : ${this.id}, NAME : ${this.name}, EMAIL : ${this.email}`;
}
}
const a1 = new Person("antman", "앤드맨", "antman@gmail.com");
console.log(a1.id); // antman
a1.id = "aaaaaaaaaaaantman";
console.log(a1.id); // aaaaaaaaaaaantman
console.log(a1.age); // 100
a1.age = 200;
console.log(a1.age); // 200
console.log(a1.name); // antman
a1.name = "개미맨";
console.log(a1.name); // 개미맨
console.log(a1.email); // antman@gmail.com
a1.email = "test@test.com";
console.log(a1.email); // test@test.com
💡 getter는 값을 취할시 —> 해당 get 메서드 호출
setter는 값을 할당시 —> 해당 set 메서드 호출
—> 부모가 잘 설계해놓은 것을 자식이 받아서 설계대로 구현한 후 각 인스턴스(객체)별로 다양하게 사용하는 것
다른 전통적인 언어들의 클래스와 ES6 클래스의 차이점
for ...of 반복
—> 사용자 정의 객체의 경우에도 특정 인터페이스 규격을 맞춰준다면 사용 가능
// Array
const fruits = ["apple", "banana", "pear", "watermelon", "orange"];
for (let value of fruits) {
console.log(value);
/*
apple
banana
pear
watermelon
orange
*/
}
// String
const str = "KOREA";
for (let value of str) {
console.log(value);
/*
K
O
R
E
A
*/
}
// Number > 반복 가능한 것이 아니므로 Err
const num = 12345;
for (let i of num) {
console.log(value);
}
// Map
const map1 = new Map([
["seoul", 1],
["busan", 2],
["jeju", 3],
]);
for (let city of map1) {
console.log(city);
/*
["seoul", 1]
["busan", 2]
["jeju", 3]
*/
}
for (let [key, value] of map1) {
// 구조분해 할당 Destructuring assignment
console.log(key, value);
/*
seoul 1
busan 2
jeju 3
*/
}
toStirng() 메서드
// Date
const dateObj = new Date(2030, 5, 25, 20, 40, 8);
strObj = dateObj.toString();
console.log(strObj);
console.log(typeof strObj); // string
// Array
const arrObj = [1, 2, 3, 4, 5, 6, 7, 8, 9];
console.log(arrObj.toString()); // join처럼... --> 1, 2, 3, 4, 5, 6, 7, 8, 9
console.log(typeof arrObj.toString()); // string
// Object
const obj = new Object();
console.log(obj.toString()); // [object Object]
toString() 메서드 재정의의 목적 : 보다 객체의 정보를 잘 보여주기 위해서
function Person(name, age, hp, gender) {
this.name = name;
this.age = age;
this.hp = hp;
this.gender = gender;
}
const p1 = new Person("홍길동", 20, "010-1234-5678", "남성");
console.log(p1.toString()); // [object Object]
Person.prototype.toString = function () {
let result = `객체정보 --> 이름:${this.name}, 나이:${this.age}, 핸드폰:${this.hp}, 성별:${this.gender}`;
return result;
};
console.log(p1.toString()); // 객체정보 --> 이름:홍길동, 나이:20, 핸드폰:010-1234-5678, 성별:남성
String.prototype.toString 재정의
String.prototype.getLength = function () {
return this.length;
};
String.prototype.toString = function () {
return "나의 좌우명 -->" + this.valueOf();
};
let testObj = "일단 해보고 안되면 다시 또 해보자!";
console.log(testObj.getLength()); // 20
console.log(testObj); // 일단 해보고 안되면 다시 또 해보자!
console.log(testObj.toString()); // 나의 좌우명 --> 일단 해보고 안되면 다시 또 해보자!
let testObj = "일단 해보고 안되면 다시 또 해보자!";
alert(testObj); // 일단 해보고 안되면 다시 또 해보자!
let testObj = new String("일단 해보고 안되면 다시 또 해보자!");
alert(testObj); // 나의 좌우명 --> 일단 해보고 안되면 다시 또 해보자!
이터러블 객체와 이터레이터 객체
—> 이를 ‘반복 가능한 객체’, ‘이터러블 객체’ 등으로 부른다.
결국, 이터러블 객체라면 for ...of 구문으로 반복시키면서 값의 열거가 가능 > 이때, 각 객체별로 열거되는 방식에는 차이가 있다.
ex) 문자열(String) → 한 글자씩 따로 열거
배열(Array) → 요소 단위로 하나씩 열거
이러한 차이를 공통화하기 위해서 이터러블 객체는 내부적으로 @@iterator 메서드를 구현한다.
이때, @@iterator 메서드는 객체의 속성이나 체이닝 상의 객체 중 하나가 Symbol.iterator 키를 속성으로 가져야만 한다.
이터러블 객체(반복 가능한 객체)는 Symbol.iterator 키를 가진다.
const str1 = "KOREA";
const iterator = str1[Symbol.iterator]();
console.log(iterator);
// 이터레이터 객체는 '이터레이터 규약'에 따라 내부의 next() 메서드를 통해 하나씩 값을 순차적으로 열거
// 이때, 열거는 객체이며 속성으로 vlaue, done 2가지 속성을 가짐
// value(값), done(열거가 끝이면 true, value는 undefined, 아니면 false)
console.log(iterator.next()); // K, done: false
console.log(iterator.next()); // O, done: false
console.log(iterator.next()); // R, done: false
console.log(iterator.next()); // E, done: false
console.log(iterator.next()); // A, done: false
console.log(iterator.next()); // undefined, done: true
for (let value of iterator) {
console.log(value);
/*
K
O
R
E
A
*/
}
// Array
const ar1 = ["seoul", "busan", "kwangju", "jeju", "dokdo"];
const iter = ar1[Symbol.iterator]();
console.log(iter); // Array Iterator
console.log(iter.next()); // value: seoul, done: false
console.log(iter.next()); // value: busan, done: false
console.log(iter.next()); // value: kwangju, done: false
console.log(iter.next()); // value: jeju, done: false
console.log(iter.next()); // value: dokdo, done: false
console.log(iter.next()); // value: undefined, done: true
// --------------------- //
for (let value of ar1) {
console.log(value);
}
let testIterObj = {
i: 1,
[Symbol.iterator]: function () {
return this;
},
next: function () {
// next() 메서드는 객체를 리턴한다.
if (this.i < 5) return { value: this.I++, done: false };
else return { value: undefined, done: true };
},
};
// const str = "KOREA";
// const iter = str[Symbol.iterator]();
console.log(testIterObj.next()); // value: 1, done: false
console.log(testIterObj.next()); // value: 2, done: false
console.log(testIterObj.next()); // value: 3, done: false
console.log(testIterObj.next()); // value: 4, done: false
console.log(testIterObj.next()); // value: undefined, done: true
const iter = testIterObj[Symbol.iterator]();
for (let value of iter) {
console.log(value); // 1,2,3,4
}
다형성의 예
다형성을 구현하기 위한 기본적인 OOP 문법
// 생성
class Animal {
constructor(name) {
this._name = name;
}
bark() {
return "짖다";
}
}
class Dog extends Animal {
constructor(name, age) {
super(name);
this._age = age;
}
bark() {
return `${this._age}살 짜리 ${this._name}가 짖다. 멍멍`;
}
}
class Cat extends Animal {
constructor(name, age) {
super(name);
this._age = age;
}
bark() {
return `${this._age}살 짜리 ${this._name}가 짖다. 야옹`;
}
}
class Bull extends Animal {
constructor(name, age) {
super(name);
this._age = age;
}
bark() {
return `${this._age}살 짜리 ${this._name}가 짖다. 음메`;
}
}
// 사용
const d1 = new Dog("개", 2);
console.log(d1.bark()); // 2살짜리 개가 짖다. 멍멍
const c1 = new Cat("고양이", 4);
console.log(c1.bark()); // 4살짜리 개가 짖다. 야옹
const b1 = new bull("소", 8);
console.log(b1.bark()); // 8살짜리 개가 짖다. 음메
// 배열
const animals = [new Dog("개", 2), new Cat("고양이", 4), new Bull("소", 8)];
// for ...of
for (let v of animals) {
console.log(v);
}
function + prototype 기반으로
상속을 받은 자식 클래스에서 메서드가 미구현되었다면 에러를 던져준다 > 강제하는 효과
const Animal = function (name) {
this._name = name;
};
Animal.prototype.move = function () {
// 코드 구현
throw new Error("move 메서드가 구현되지 않았습니다");
};
자식 클래스에서 move 메서드를 구현하지 않으면 부모 클래스에서 에러를 던져줌으로써 구현을 강제
// implement : 구현하다
const DogImpl = function (name, age) {
Animal.call(this, name); // Class의 super 역할
this._age = age;
};
DogImpl.prototype = Object.create(Animal.prototype);
DogImpl.prototype.constructor = DogImpl;
DogImpl.prototype.move = function (args) {
// 오버라이딩
console.log(`${args}(${this._name} ,${this._age}살) 이동중`);
return "멍멍!";
};
// 객체 생성
const d1 = new DogImpl("바둑", 2);
console.log(d1.move("개"));
/*
개(바둑, 2살) 이동중
멍멍!
*/
다형성 체크
객체가 특정 클래스의 인스턴스인지 여부를 확인 > instanceof
console.log(d1 instanceof Object); // true
console.log(d1 instanceof Animal); // true
console.log(d1 instanceof DogImpl); // true