ES6가 나오면서 JavaScript에 클래스 문법이 생겨 JavaScript를 좀 더 JAVA스럽게 객체 지향적으로 표현이 가능해졌다.
다만, 생김새만 클래스 구조일 뿐 엔진 내부적으로는 프로토타입 방식으로 작동된다.
// 생성자
function Person({name, age}) {
this.name = name;
this.age = age;
}
Person.prototype.introduce = function() {
return `안녕하세요, 제 이름은 ${this.name}입니다.`;
};
const person = new Person({name: '김서영', age: 28});
console.log(person.introduce()); // 안녕하세요, 제 이름은 김서영입니다.
// 클래스
class Person {
// 프로토타입에서 사용하던 생성자 함수는 클래스 안에 `constructor`라는 이름으로 정의
constructor({name, age}) { //생성자
this.name = name;
this.age = age;
}
// 객체에서 메소드를 정의할 때 사용하던 문법을 그대로 사용하면, 메소드가 자동으로 `Person.prototype`에 저장
introduce() {
return `안녕하세요, 제 이름은 ${this.name}입니다.`;
}
}
const person = new Person({name: '김서영', age: 28});
console.log(person.introduce()); // 안녕하세요, 제 이름은 김서영입니다.
constructor는 인스턴스를 생성하고 클래스 필드를 초기화하기 위한 특수한 메서드이다.
constructor는 클래스 안에 1개만 존재할 수 있으며, 2개 이상 존재할 경우 Syntac Error가 발생한다.
class Person {
height = 164; // 인스턴스 변수
// constructor는 이름을 바꿀 수 없다.
constructor(name, age) {
// this는 클래스가 생성할 인스턴스를 가리킨다.
this.name = name;
this.age = age;
}
}
let person1 = new Person('김서영', 28);
console.log(person1.name); // 김서영
console.log(person1.age); // 28
console.log(person1.height); // 164
클래스의 선언과 초기화는 반드시 constructor 내부에서 실시된다.
constructor 내부에 선언한 클래스 필드는 클래스가 생성할 인스턴스에 바인딩 된다.
클래스 필드는 그 인스턴스의 프로퍼티가 되며, 인스턴스를 통해 클래스 외부에서 언제나 참조 가능하다. (public)
JAVA나 Python의 클래스 문법과의 차이점은, JavaScript의 클래스 문법에서는 인스턴스 변수를 반드시 지정하지 않고 생성자(constructor)를 통해 this.변수
문법으로 자동 생성 될 수 있다는 것이다.
클래스의 메소드를 정의할 때는 객체 리터럴에서 사용하던 문법과 유사한 문법을 사용한다.
class Calculator {
add(x, y) {
return x + y;
}
subtract(x, y) {
return x - y;
}
}
let calc = new Calculator();
calc.add(1,10); // 11
객체 리터럴의 문법과 마찬가지로, 임의의 표현식을 대괄호로 둘러싸 메소드의 이름으로 사용 할 수도 있다.
const methodName = 'introduce'; // 클래스 메소드 이름
class Person {
constructor({name, age}) {
this.name = name;
this.age = age;
}
// 아래 메소드의 이름은 `introduce`가 됩니다.
[methodName]() {
return `안녕하세요, 제 이름은 ${this.name}입니다.`;
}
}
console.log(new Person({name: '김서영', age: 28}).introduce()); // 안녕하세요, 제 이름은 김서영입니다.
클래스 내에서 Getter 혹은 Setter를 정의하고 싶을 때는 메소드 이름 앞에 get 또는 set을 붙여주면 된다.
Getter와 Setter는 객체 지향 프로그래밍에서 사용되는 개념으로, 일종의 메서드로 보면 된다.
Getter는 객체의 속성(property) 값을 반환하는 메서드이고, Setter는 객체의 속성 값을 설정/변경하는 메서드로 보면 된다.
const user = {
name: '김서영',
age: 28
}
console.log(user.name); // 김서영
user.name = '뾰롱';
위와 같이 바로 접근하지 말고, getName(), setName()
과 같은 메서드를 경유해 접근할 수 있도록 하는 것이 Getter와 Setter이다.
const user = {
name: '김서영',
age: 28,
// 객체의 메서드(함수)
getName() {
return user.name;
},
setName(value) {
user.name = value;
}
}
console.log(user.getName()); // 김서영
user.setName('뾰롱');
console.log(user.getName()); // 뾰롱
Getter와 Setter를 사용하면 객체 내부 속성에 직접 접근하지 않아 객체의 정보 은닉을 가능하게 해주기 때문에, 보안을 강화할 수 있고 코드의 안정성과 유지보수성을 높일 수 있다는 장점이 있다.
또한 옳지 않은 값을 넣으려고 할 때 이를 미연에 방지할 수 있다.
예를 들자면,
const user = {
name: '김서영',
age: 28,
getAge() {
return user.age;
},
setAge(value) {
// 나이 값이 100 초과일 경우 함수를 리턴해 user.name이 재설정되지 않도록 필터링
if(value > 100) {
console.error('나이는 100을 초과할 수 없습니다.')
return;
}
}
}
user.setAge(400); // 나이는 100을 초과할 수 없습니다.
Getter와 Setter는 이론적인 개념으로는 보통 get프로퍼티명()
, set프로퍼티명
의 형식으로 메서드 이름을 지었지만, ES6부터는 Getter와 Setter를 간단하게 작성할 수 있게 되었다.
객체 리터럴 안에서 속성 이름 앞에 get 또는 set 키워드만 붙이면 된다.
const user = {
name: '김서영',
age: 28,
// userName() 메서드 왼쪽에 get, set 키워드만 붙이면 알아서 Getter, Setter 로서 동작된다.
get userName() {
return user.name;
},
set userName(value) {
user.name = value;
}
}
그런데 이 때의 Getter와 Setter는 함수 호출 형식이 아닌, 일반 프로퍼티처럼 접근해 사용된다.
Getter와 Setter 메서드를 구현하면 객체에 userName
이라는 가상의 프로퍼티가 생성되는데, 이는 읽고 쓸 수 있지만 실제로는 존재하지 않는 프로퍼티이다.
console.log(user.userName); // 김서영
userName = '뾰롱';
JavaScript의 객체의 프로퍼티는 크게 2종류로 나눌 수 있다.
💠 데이터 프로퍼티(data property)
💠 접근자 프로퍼티(accessor property)
데이터 프로퍼티는 객체 내부에 저장된 실제 데이터 값으로, 우리가 일반적인 프로퍼티라고 부르는 것이다.
접근자 프로퍼티는 일반적인 프로퍼티와 달리 key와 value를 가지지 않고, Getter와 Setter라는 함수를 가지는 특수 프로퍼티이다.
즉, 자바스크립트 객체 속성에 접근하듯이 접근자 프로퍼티를 호출하면 함수 호출 문법이 아니더라도 Getter와 Setter 함수가 호출되는 것이다.
=> Getter와 Setter 함수 자체가 접근자 프로퍼티인 것!
let person = {
/* 데이터 프로퍼티 */
firstName: "서영",
lastName: "김",
/* 접근자 프로퍼티 */
get fullName() {
return this.firstName + " " + this.lastName;
},
set fullName(name) {
let names = name.split(" ");
this.firstName = names[0];
this.lastName = names[1];
}
};
console.log(person.firstName); // "서영" 출력
console.log(person.lastName); // "김" 출력
console.log(person.fullName); // "서영 김" 출력
person.fullName = "뾰롱 김"; // Setter 호출
Getter만 선언하고 Setter는 선언하지 않았을 때, 에러가 발생할 수도 있다.
let user = {
get fullName() {
return `...`;
}
};
user.fullName = "테스트"; // Error (setter메서드로 값을 변경해야 하는데 getter메서드만 있어서 에러가 발생)
데이터 프로퍼티명과 접근자 프로퍼티 명이 같을 경우, Setter메서드에서 무한루프에 빠지게 된다.
때문에 접근자 프로퍼티의 이름을 중복이 되게 작성하면 안된다.
let user = {
name : '김서영',
get name() {
return user.name;
},
set name(value) {
user.name = value;
}
}
// user 객체의 name 프로퍼티 값을 변경
user.name = '김뾰롱';
이러한 현상이 발생하는 이유는, Setter 함수내에서 자기 자신을 호출하였기 때문이다.
정적 메서드는 클래스의 인스턴스가 아닌 클래스 이름으로 곧바로 호출되는 메서드이다.
static 키워드를 메서드 이름 앞에 붙여주면 해당 메서드는 정적 메서드가 된다.
class Person {
constructor({ name, age }) { // 생성자 인스턴스
this.name = name;
this.age = age;
}
static static_name = 'STATIC'; // 정적 인스턴스
getName() { // 인스턴스(프로토타입) 메소드
return this.name;
}
static static_getName() { // 정적 메소드
return this.static_name;
}
}
const person = new Person({ name: '김서영', age: 28 });
person.getName(); // 김서영
Person.static_getName(); // STATIC
class Person {
constructor({ name, age }) {
this.name = name;
this.age = age;
}
// 이 메소드는 정적 메소드
static static_sumAge(...people) {
/*
함수 파라미터 people를 전개연산자 ...people를 통해 배열로 만듬
[ {"name": "김서영", age": 28}, { "name": "이루리","age": 7 }]
*/
// 그리고 각 객체의 age값을 얻어와 합침
return people.reduce((acc, person) => acc + person.age, 0);
}
}
const person1 = new Person({ name: '김서영', age: 28 });
const person2 = new Person({ name: '이루리', age: 7 });
Person.static_sumAge(person1, person2); // 35
Generator 메서드를 정의하려면, 메서드 이름 앞에 *기호를 붙여주면 된다.
제너레이터란, 이터러블이며 동시에 이터레이터인 것이다.
=> 이터레이터를 리턴하는 함수
제너레이터 함수는, 일반 함수처럼 함수의 코드블록을 한 번에 실행하지 않고 함수 코드블록의 실행을 일시 정지했다가 필요한 시점에 재시작 할 수 있는 특수한 함수이다.
// 제너레이터 함수 선언식
function* genDecFunc() {
yield 1;
}
let generatorObj = genDecFunc(); // 제너레이터 함수 실행 결과 반환된 제너레이터 객체를 변수에 넣어 사용한다.
// 제너레이터 함수 표현식
const genExpFunc = function* () {
yield 1;
};
generatorObj = genExpFunc();
// 제너레이터 메소드 식
const obj = {
* generatorObjMethod() {
yield 1;
}
};
generatorObj = obj.generatorObjMethod();
// 제너레이터 클래스 메소드 식
class MyClass {
* generatorClsMethod() {
yield 1;
}
}
const myClass = new MyClass();
generatorObj = myClass.generatorClsMethod();
yield는 제너레이터 함수의 실행을 일시적으로 정지시키며, yield 뒤에 오는 표현식은 제너레이터의 caller에게 반환된다.
next 메서드는 {value, done} 프로퍼티를 갖는 이터레이터 객체를 반환한다.
즉, value 프로퍼티는 yield문이 반환한 값이고 done 프로퍼티는 제너레이터 함수 내의 모든 yield문이 실행되었는지를 나타내는 boolean 타입의 값이다.
function* generateSequence(){
...코드
yield 1; // 첫번째 호출 시에 이 지점까지 실행된다.
...코드
yield 2; // 두번째 호출 시에 이 지점까지 실행된다.
return 3;
}
// 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.
// 제너레이터 객체는 이터러블이며 동시에 이터레이터이다.
// 따라서 Symbol.iterator 메소드로 이터레이터를 별도 생성할 필요가 없다
let iter = generateSequence();
//실행 결과가 자기 자신인 Sysmbol.iterator를 가지고 있다.
console.log(iter[Symbol.iterator]() == iter) // true
//value, done 이 있는 객체를 반환하는 next를 호출하면 이터러블 객체를 반환하고 함수는 일시중단 된다.
console.log(iter.next()); // { "value": 1, "done": false } + 함수 실행 중단
console.log(iter.next()); // { "value": 2, "done": false } + 함수 실행 중단
console.log(iter.next()); // { "value": 3, "done": true } + 순회 종료
마지막 yield 문까지 실행된 상태에서 next 메서드를 호출하면 done 프로퍼티 값이 true가 된다.
제너레이터 함수를 호출하면 제너레이터 객체를 반환
제너레이터에서 yield를 통해 몇 번의 next를 통해 값을 꺼낼 수 있는지 정할 수 있다.
next() 함수가 실행되면 yield 순서대로 실행되고 일시 중단된다.
제너레이터의 실행 결과가 이터레이터이기 때문에 for ..of로 사용 가능하다.
return을 하면 리턴값을 value와 함께 done이 true가 되면서 순회를 종료한다.
yield*를 사용하면 제너레이터를 다른 제너레이터에 "끼워 넣을 수" 있다.
yield에 *를 붙여 사용하게 되면, 이와 함께 표현된 이터러블 객체를 순회하게 된다.
function* generateSequence(start, end) { // 시작과 끝을 정해서 순회하는 제너레이터
for (let i = start; i <= end; i++)
yield i;
}
function* generatePasswordCodes() {
/* 제너레이터 함수를 실행할땐 보통 let a = generateSequence(48, 57);
변수에다 널고, a.next()를 통해 순회한다.
하지만 yield* 에 바로 순회가 가능하다.
이는 마치 비동기 파트에서 Promise().then()보다 await Promise() 쓰는 격과 비슷하다고 보면 된다.
*/
// 0..9
yield* generateSequence(48, 57); // generateSequence()함수의 리턴값은 제너레이터 객체이다. yield*는 제너레이터 객체를 쭉 순회시킨다.
// A..Z
yield* generateSequence(65, 90);
// a..z
yield* generateSequence(97, 122);
}
let str = '';
for(let code of generatePasswordCodes()) {
str += String.fromCharCode(code);
}
alert(str); // 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
function* gen() {
while(true) {
var value = yield null; // null값을 보내고, next(인자값)을 통해 값을 받는다.
console.log(value);
}
}
var g = gen();
g.next(1);
// "{ value: null, done: false }"
g.next(2);
// "{ value: null, done: false }"
// 2
해당 예시에서는 next가 인자값과 함께 호출 되었다.
첫 번째 호출에서 아무것도 출력되지 않은 것은 Generator가 아직 아무런 값도 yield 하지 않았기 때문이다.
두 번째 호출에서는 호출과 함께 전달된 정수 2가 Generator 내부의 yield 키워드에 전달되어 value로 할당되었고 console.log로 출력되었다.
제너레이터에는 next 외에도 throw, return 등의 메서드가 있는데, 이 return과 throw를 통해 제너레이터를 종료할 수 있다.
function* increment() {
let i = 0;
try {
while (true) {
yield i++;
}
} catch (e) {
console.log('[ERROR]', e);
}
}
const withReturn = increment();
console.log(withReturn.next()); // { value: 0, done: false } : i++ 라서 0부터 찍힌다.
console.log(withReturn.next()); // { value: 1, done: false }
console.log(withReturn.next()); // { value: 2, done: false }
console.log(withReturn.next()); // { value: 3, done: false }
console.log(withReturn.return(42)); // { value: 42, done: true }
return이 호출되고 나면, value에는 return의 인자가 할당되고, done은 true가 된다.
const withThrow = increment();
console.log(withThrow.next());
console.log(withThrow.next());
console.log(withThrow.next());
console.log(withThrow.next()); // { value: 3, done: false }
console.log(withThrow.throw(-1)); // Uncaught -1
throw가 호출되고 나면, catch 블록에 throw의 인자가 전달된다.
클래스 상속 기능을 통해 한 클래스의 기능을 다른 클래스에서 재사용할 수 있다.
extends 키워드는 클래스를 다른 클래스의 하위 클래스로 만들기 위해 사용된다.
class Parent {
// ...
}
class Child extends Parent {
// ...
}
위 코드에서 extends 키워드를 통해 Child 클래스가 Parent 클래스를 상속했다.
이 관계를 부모 클래스-자식 클래스 관계 혹은 슈퍼 클래스-서브 클래스 관계라고 말한다.
따라서 어떤 클래스 A가 다른 클래스 B를 상속받으면, 다음과 같은 일들이 가능해진다.
💠 자식 클래스 A를 통해 부모 클래스 B의 정적 메소드와 정적 속성을 사용할 수 있다.
💠 부모 클래스 B의 인스턴스 메서드와 인스턴스 속성을 자식 클래스 A의 인스턴스에서 사용할 수 있다.
class Parent {
static staticProp = 'staticProp';
static staticMethod() {
return 'I\'m a static method.';
}
instanceProp = 'instanceProp';
instanceMethod() {
return 'I\'m a instance method.';
}
}
class Child extends Parent {}
// 상속하면 부모의 static요소들을 사용 가능
console.log(Child.staticProp); // staticProp
console.log(Child.staticMethod()); // I'm a static method.
// 상속하면 부모의 인스턴스를 사용 가능
const c = new Child();
console.log(c.instanceProp); // instanceProp
console.log(c.instanceMethod()); // I'm a instance method.
class Person{
constructor(name, first, second){
this.name=name;
this.first=first;
this.second=second;
}
sum(){
return (this.first + this.second);
}
}
class Person2 extends Person{
// override Person
constructor(name, first, second, third){
super(name, first, second); //부모 생성자를 가져와서 행하게 한다.
this.third = third;
}
sum(){
// 부모 메소드를 가져와서 사용.
// 오버로딩 메소드에서 온전한 부모 메소드를 사용하고 싶을때
return super.sum() + this.third;
}
}
var kim = new Person2('kim', 10, 20, 30);
document.write(kim.sum()); // 60
ES2021부터 자바스크립트의 메서드와 필드명 앞에 "#"를 붙여 프라이빗 메서드와 필드 정의가 가능해졌다.
class myClass {
// private 변수
#num = 100
// private 메서드
#privMethod(){
console.log(this.#num); // 프라이빗 변수 호출
}
publicMethod() {
this.#privMethod(); // 프라이빗 메소드 호출
}
}
let newClass = new myClass();
newClass.publicMethod() // 100
public 필드의 경우, 클래스의 존재하지 않은 필드에 접근을 시도하면 undefined가 반환된다.
하지만 private 필드의 경우, undefined 대신 예외를 발생시키기 때문에 특정 객체에 어떤 private 프로퍼티가 있는지 확인이 어려웠다.
때문에 in 키워드를 사용해 객체 안에 private 속성/메서드가 있는지 체크할 수 있다.
class Foo {
#brand = 100;
static isFoo(obj) {
return #brand in obj;
}
}
const foo = new Foo();
const foo2 = { brand: 100 };
console.log('foo : ', Foo.isFoo(foo)); // true
console.log('foo2 : ', Foo.isFoo(foo2)); // false