자바스크립트 제대로 배우자4

2023. 4. 4. 01:47Javascript/실전 자바스크립트

클래스

클래스 1

class로 클래스를 정의하고, constructor로 생성자를 정의할 수 있다.

생성자 안에서 this를 이용해서, 멤버 변수를 정의할 수 있다.

밑에는 메서드를 정의하고있다. sayHello

메서드 안에서는 this로 멤버 변수에 접근할 수 있다. $(this.name)

클래스를 이용해 객체를 만들 때에는 new 키워드를 만들면된다.

new Person의 'mike'는 생성자의 매개변수로 전달이된다.

이렇게 만들어진 객체로 위에서 정의한 메서드를 호출할수있다.

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    console.log(`hello~ ${this.name}!`);
  }
}

const person = new Person('mike');
person.sayHello();
// hello~ mike!
class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    console.log(`hello~ ${this.name}!`);
  }
}
// 사실 클래스는 함수와 프로토타입을 기반으로 만들어져있다.
// 클래스 타입 검사를 해보면, function이라고 나온다.
console.log(typeof Person); //function
// 모든 함수에는 prototype 객체가 있는데,
// 클래스 내부에서 정의한 이러한 메서드는 프로토타입 객체에 저장됨 
// 지금은 빈배열
// 클래스의 prototype은 조회할 수 없게 설정되어있어서 Object.keys는 안보이고
console.log(Object.keys(Person.prototype)); // []
// getOwnPropertyNames로는 볼수있음
console.log(Object.getOwnPropertyNames(Person.prototype)); // [ 'constructor', 'sayHello' ]
// constructor에서 멤버변수를 만들면, 이 값은 프로토타입 객체에 들어가는 것이 아니고
// 각 객체에 할당된다.
const person = new Person('mike');
console.log(Object.keys(person)); //[ 'name' ]
// 클래스 자체는 이 constructor함수이다 그래서 이는 true가 나온다
console.log(Person.prototype.constructor === Person); //true
// sayHello메소드도 프로토타입 객체에 있는지 확인해보면, 함수가 있는것을 확인할 수 있다.
console.log(Person.prototype.sayHello); //[Function: sayHello]

class에서 getter와 setter를 정의할수있다

// name이라는 getter와 setter를 정의했는데, 실제 값은 숨기기 위해서 _로 정의를했다
// 언더바로 정의를 해도, 실제 객체에 노출이 되기는한다
class Person {
  constructor(name) {
    this._name = name;
  }
  get name() {
    return this._name;
  }
  set name(value) {
    if (value.length < 4) { //4보다 작으면 값을 반환하지않는 것
      console.log('name is too short');
      return;
    }
    this._name = value;
  }
}

const person = new Person('mike'); // mike라는 이름으로 객체 생성
console.log(person.name); //name 출력  >> getter 호출
person.name = 'ab'; //ab입력, setter 호출 >> 길이가 4보다 작아서 이 값은 사용되지않을 예정
console.log(person.name); //name 출력, getter 호출
/*
mike
name is too short
mike    
*/
// 마지막것 중에 ab가 사용되지않은 것을 확인할 수 있다.

getter를 이용하면, read only처럼 사용할 수 있다.

// getter를 이용하면, read only처럼 사용할 수 있다.
class Person {
  constructor(name) {
    this._name = name;
  }
  get name() {
    return this._name;
  }
}

const person = new Person('mike');
person.name = 'jane'; // 그래서 값을 할당할때 적용되지않는것
console.log(person.name); // mike
console.log(Object.keys(person)); // 실제 정의한 name은 안되고, 생성자에서 생성한 _name을 출력한다.
/*
mike
[ '_name' ]
*/

클래스의 상속 >> extend 키워드 사용

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    console.log(`hello~ ${this.name}!`);
  }
}
// Person를 상속받아서, Programmer라는 클래스를 만들고있다.
// Programmer라는의 생성자부분을 한번 보면, super를 호출하고있다.
// 이러면 부모의 constructor가 호출된다.
// 그러면 부모에서 name이라는 멤버변수를 만들것이고,
// 밑에서는 그 이후에 language라는 멤버변수를 붙이고있다.
// 그래서  p1.sayHello();와 같이 부모의 메서드를 호출할 수 있다
// 자식의 생성자에서는 무조건 super를 호출해야한다
class Programmer extends Person {
  constructor(name, language) {
    super(name);
    this.language = language;
  }
}

const p1 = new Programmer('mike', 'javascript');
p1.sayHello();

//hello~ mike!

constructor를 반드시 정의할 필요는 없는데,

class Programmer extends Person {
  constructor(name, language) {
    super(name);
    this.language = language;
  }
}

정의하지않는다면 다음과 같은 constroctor를 기본으로 사용된다.

->>>

class Programmer extends Person {
  constructor(...args){
    super(...args);
  }
}

클래스 상속은 프로토타입을 기반으로 구현되어있다.

모든 클래스는 이렇게 프로토타입 객체를 가지고있는데, 상속받는 쪽의 프로토타입 객체는 부모의 프로토타입 객체를 프로토타입 체인으로 연결을 해서 갖고있다.

클래스 자기 자신도, 프로토타입 체인으로 연결이 되어있다.

생성자 함수의 프로토타입 체인은 static 멤버변수와 satic 메서드에서 사용이되는데, 나중에 알아보자

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    console.log(`hello~ ${this.name}!`);
  }
}

class Programmer extends Person {
  constructor(name, language) {
    super(name);
    this.language = language;
  }
}

console.log(Object.getPrototypeOf(Programmer.prototype) === Person.prototype); // true
console.log(Object.getPrototypeOf(Programmer) === Person); // true

메서드 오버라이딩(overriding) : 자식 클래스 선언할 때, 부모에 있는 메서드와 같은 이름으로 정의하는 것

부모의 메서드를 호출하고 싶다면, super에 점찍어서 호출하면됨!

자식의 메서드를 호출할 때는, this를 이용하면됨

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    console.log(`hello~ ${this.name}!`);
  }
  getRandom() {
    return Math.floor(Math.random() * 10); //10보다 작은값
  }
}

class Programmer extends Person {
  constructor(name, language) {
    super(name);
    this.language = language;
  }
  sayHello() {
    super.sayHello(); // 부모라서 super
    console.log(`I like ${this.language}.`);
    console.log(`Your lucky number is ${this.getRandom()}`); // 자식이라서 this
  }
  getRandom() {
    return 20 + Math.floor(Math.random() * 10); //20=<x<30
  }
}

const p1 = new Programmer('mike', 'javascript');
p1.sayHello();
/*
hello~ mike!
I like javascript.
Your lucky number is 28
*/

클래스 필드(class fields)

age = 23; //등호를 사용하면, 프로토타입이아니라, 객체에 할당이된다.

멤버 변수 뿐만아니라, 메서드에서도 등호를 사용할 수 있다.

printName = () => { ~~~ }

메서드도 프로토타입이 안니 객체에 할당이 됨

class Person {
  age = 23; //등호를 사용하면, 프로토타입이아니라, 객체에 할당이된다.
  constructor(name) {
    this.name = name;
  }
  // 메서드도 프로토타입이 안니 객체에 할당이 됨
  printName = () => {
    console.log(this.name);
  };
}

// 둘다 없게 된다
console.log(Person.prototype.age, Person.prototype.printName); //undefined undefined

const person1 = new Person('mike');
const person2 = new Person('jane');
person1.age = 100;
console.log(person1.age, person2.age); // 100 23 >> 각각 객체별로 관리가 됨

setTimeout(person1.printName, 100); //mike

클래스 필드와 화살표 함수를 같이 사용하면,  setTimeout 같은 함수의 매개변수로 메서드를 전달할 수 가 있다.

  printName = () => {
    console.log(this.name);
  };
setTimeout(person1.printName, 100); //mike

여기서 사용한 this가 별 문제 없이 사용될 수 있는데

 

화살표함수가 아니라 일반함수로 표현하게 된다면! undefined가 출력된다.

class Person {
  age = 23; //등호를 사용하면, 프로토타입이아니라, 객체에 할당이된다.
  constructor(name) {
    this.name = name;
  }
  // 메서드도 프로토타입이 안니 객체에 할당이 됨
  printName = function() {
    console.log(this.name);
  };
}

// 둘다 없게 된다
console.log(Person.prototype.age, Person.prototype.printName); //undefined undefined

const person1 = new Person('mike');
const person2 = new Person('jane');
person1.age = 100;
console.log(person1.age, person2.age); // 100 23 >> 각각 객체별로 관리가 됨

setTimeout(person1.printName, 100); //undefined

 

부모클래스에서 클래스 필드로 메서드를 정의하면,

자식에서 super를 사용해서 호출할 수가 없다.

super는 프로토타입을 기반으로 동작하는데, 

클래스필드로 정의가 되면, 이 메서드가 프로토타입에 추가되기보다 객체에 추가된다.

그래서 super로 접근하면 에러가 난다.

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    console.log(`hello~ ${this.name}!`);
  }
  getRandom = () => {
    return Math.floor(Math.random() * 10);
  };
}

class Programmer extends Person {
  constructor(name, language) {
    super(name);
    this.language = language;
  }
  sayHello() {
    super.sayHello();
    console.log(`I like ${this.language}.`);
    console.log(`Your lucky number is ${super.getRandom()}`);
  }
  getRandom() {
    return 20 + Math.floor(Math.random() * 10);
  }
}

const person1 = new Programmer('mike', 'javascript');
person1.sayHello();

근데 여기서 super를 this로 바꾸게 된다면,

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    console.log(`hello~ ${this.name}!`);
  }
  getRandom = () => {
    return Math.floor(Math.random() * 10);
  };
}

class Programmer extends Person {
  constructor(name, language) {
    super(name);
    this.language = language;
  }
  sayHello() {
    super.sayHello();
    console.log(`I like ${this.language}.`);
    console.log(`Your lucky number is ${this.getRandom()}`);
  }
  getRandom() {
    return 20 + Math.floor(Math.random() * 10);
  }
}

const person1 = new Programmer('mike', 'javascript');
person1.sayHello();
/*
hello~ mike!
I like javascript.
Your lucky number is 5
*/

근데 this로 했다면, 자식의 메서드를 출력해야하는데 여기서는 10이하의 숫자가 ! 부모의 메서드가 출력되었다.

getRandom 속성으 찾을 때, 객체에서 먼저 찾고 없으면 프로토타입에서 찾는다.

근데 객체에는 부모에서 정의한 메서드가 있기 때문에, 그 메서드가 먼저 찾아진 것이다.

혼란스러운 상황에서는 자식에서도 클래스 필드로 정의하면된다!

getRandom = () => {
    return 20 + Math.floor(Math.random() * 10);
  }
class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    console.log(`hello~ ${this.name}!`);
  }
  getRandom = () => {
    return Math.floor(Math.random() * 10);
  };
}

class Programmer extends Person {
  constructor(name, language) {
    super(name);
    this.language = language;
  }
  sayHello() {
    super.sayHello();
    console.log(`I like ${this.language}.`);
    console.log(`Your lucky number is ${this.getRandom()}`);
  }
  getRandom = () => {
    return 20 + Math.floor(Math.random() * 10);
  }
}

const person1 = new Programmer('mike', 'javascript');
person1.sayHello();
/*
hello~ mike!
I like javascript.
Your lucky number is 29
*/

클래스의 프로토타입 체인을 한번 더 설명해보자.

자식의 프로토타입 객체는 부모의 프로토타입 객체를 프로토타입 체인으로 연결한다.

super를 사용했을 때, 프로토타입을 기반으로 동작한다

 

여기서는 프로토타입을 변경하고있다. __proto__ 값을 변경하고있는것이다.

Object.setPrototypeOf(Programmer.prototype, {});

빈 객체로 할당하고있다! {}

 

프로토타입 체인의 연결을 끊으면, super를 사용한 곳에서 문제를 일으킬것이다.

super를 호출하는 코드는 이와같이 this를 이용해서 프로토타입 체인을 이용한다고 생각할 수 있다.

class Person {
  sayHello() {
    console.log(`I'm Person`);
  }
}
class Programmer extends Person {
  sayHello() {
    // super.sayHello();
    this.__proto__.__proto__.sayHello.call(this);
    console.log(`I'm Programmer`);
  }
}

// console.log(Programmer.prototype.__proto__ === Person.prototype);
// Object.setPrototypeOf(Programmer.prototype, {});

const person1 = new Programmer();
person1.sayHello();
// 부모의 메서드가 다음과 같이 잘 출력된다
/*
I'm Person
I'm Programmer
*/

const f1 = person1.sayHello;
f1();

super를 사용하면 자기 자신의 클래스를 기억하기 때문에

아래에서처럼 메서드를 호출할수도있다.

this와는 상관 없기 때문에 잘 동작한다.

class Person {
  sayHello() {
    console.log(`I'm Person`);
  }
}
class Programmer extends Person {
  sayHello() {
    // super.sayHello();
    // this로 동작하는 것이 아니고
    // this와 상관없이 , 이 super를 사용한 메서드는 
    // 자기자신의 클래스를 기억하고있다
    // this.__proto__.__proto__.sayHello.call(this);
    // 클래스를 기억하기 위해서 사용하는 변수를 
    // HomeObject라고 부른다
    // Programmer.__proto__.__proto__.sayHello.call(this);
    console.log(`I'm Programmer`);
  }
}

// console.log(Programmer.prototype.__proto__ === Person.prototype);
// Object.setPrototypeOf(Programmer.prototype, {});

const person1 = new Programmer();
person1.sayHello();
// 부모의 메서드가 다음과 같이 잘 출력된다
/*
I'm Person
I'm Programmer
*/

const f1 = person1.sayHello;
f1();

/*
I'm Programmer
I'm Programmer*/
class Person {
  sayHello() {
    console.log(`I'm Person`);
  }
}
class Programmer extends Person {
  sayHello() {
    // super.sayHello();
    // this로 동작하는 것이 아니고
    // this와 상관없이 , 이 super를 사용한 메서드는 
    // 자기자신의 클래스를 기억하고있다
    // this.__proto__.__proto__.sayHello.call(this);
    // 클래스를 기억하기 위해서 사용하는 변수를 
    // HomeObject라고 부른다
    // Programmer.__proto__.__proto__.sayHello.call(this);
    console.log(`I'm Programmer`);
  }
}

// console.log(Programmer.prototype.__proto__ === Person.prototype);
// Object.setPrototypeOf(Programmer.prototype, {});

const person1 = new Programmer();
person1.sayHello();
// 부모의 메서드가 다음과 같이 잘 출력된다
/*
I'm Person
I'm Programmer
*/

const f1 = person1.sayHello;
f1();

/*
I'm Programmer
I'm Programmer*/

여기서는 anme을 클래스 필드로 정의했다.

자식에서 오버라이드 하고있다.

부모쪽 constructor에서 사용하는 name은 부모의 값을 참조하게 된다.

class Person {
  name = 'mike';
  constructor() {
    console.log(this.name); //mike
  }
}

class Programmer extends Person {
  name = 'jane';
}

const person1 = new Programmer();
console.log(person1.name); //jane

/*
mike
jane
*/

부모의 constructor가 호출 된 이후에는 자식의 멤버변수가 사용된다.

 


클래스 2
static 키워드를 이용하면, 정적 멤버변수나 정적 메서드를 정의할 수 있다.

class Person {
  static CURRENT_ID = 1;
  constructor(name, age) {
    this.name = name;
    this.age = age;
    this.id = Person.CURRENT_ID++;
  }
  static getYounger(p1, p2) {
    return p1.age <= p2.age ? p1 : p2;
  }
}

는 아래와 동일하다

Person.getYounger = function(p1, p2){
  return p1.age >= p2.age ? p1 : p2;
}
Person.CURRENT_ID = 1;

생성자 함수에 직접 속성값을 붙이는 것이다!

그래서 클래스 이름에 점을 찍어서 사용하면된다!

//Person.CURRENT_ID

메서드를 호출할 때도, 클래스 이름에 점을 찍어서 사용하면된다

const younger = Person.getYounger(person1, person2);

 

정적멤버변수나 정적 메서드는 객체별로 만들어지는 것이 아니고, 클래스에 하나만 만들어지는 것이다.

class Person {
  static CURRENT_ID = 1;
  constructor(name, age) {
    this.name = name;
    this.age = age;
    this.id = Person.CURRENT_ID++;
  }
  static getYounger(p1, p2) {
    return p1.age <= p2.age ? p1 : p2;
  }
}

Person.getYounger = function(p1, p2){
  return p1.age >= p2.age ? p1 : p2;
}
Person.CURRENT_ID = 1;

const person1 = new Person('mike', 23);
const person2 = new Person('jane', 32);
const younger = Person.getYounger(person1, person2);
console.log(younger.name);
console.log(Person.CURRENT_ID);
/*
jane
3
*/

정적 멤버변수나 정적 메서드도 상속이된다.

지금 부모에서 정적 메서드를 하나 가지고있다. 이것을 자식에서도 접근이 가능할까?

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  static getYounger(p1, p2) {
    return p1.age <= p2.age ? p1 : p2;
  }
}
class Programmer extends Person {}

console.log(Programmer.getYounger);
console.log(Object.getPrototypeOf(Programmer) === Person);
/*
[Function: getYounger]
true
*/

여기서 보면, 자식도 함수를 가지고있다.

생성자 함수들이 프로토타입 체인으로 연결되어있기 대문에 TRUE가 나오늘 것이다.

// public, protected, private
// 객체지향 언어에서는 public, protected, private
// 접근범위를 아래와 같이 세가지로 설정할 수 있다

// 접근범위는 객체에서 접근할 수 있는지, 자식에서 접근할 수 있는지 

// public은 둘다 접근할 수 있는것
// private는 둘다 접근할 수 없는 것
// protected는 객체에서는 접근할 수 없고, 자식에는 접근할 수 있는것

// javascript에서는 모든 멤버 변수와 메서드는 public이다

// private는 #을 이용하면 됨, 내부에서도 #사용가능 this.#name 처럼!

// 실제 외부에서 접근할 수 있는지 살표보겠다.
// protected는 자바스크립트 언어차원에서 지원하지않는다
// _로 시작하는 이름으로 작성하는게 잘 알려진 컨벤션이다
// 물론 _로도 객체에서 접근할 수 있다는 문제가 있다
class Person {
  #name = '';
  _age = 23;
  constructor(name) {
    this.#name = name;
  }
  // #sayHello() {
  //   console.log(`hello~ ${this.#name}!`);
  // }
}
class Programmer extends Person {
  sayHello() {
    console.log(this.name);
    console.log(this['#name']);
  }
}

const person1 = new Programmer('mike');
person1.sayHello();
console.log(person1.name);
console.log(person1['#name']);
/*
undefined
undefined
undefined
undefined
*/

 

4:16

 

 

 

 

 

 

 

 

 

모듈 시스템


ESM 알아보기
모듈 실행 순서와 순환 참조

자바스크립트 생태계


웹팩, 바벨, 폴리필
eslint, prettier