스터디 7회차 주간 공부 내용 - JS Class, ES6 함수

잔잔바리디자이너·2022년 4월 9일
0

Study

목록 보기
9/19
post-thumbnail

클래스의 도입?

클래스는 ES6에서 도입된 새로운 객체 생성의 메커니즘이다. 클래스는 기본적으로 생성자 함수와 비슷하게 동작하지만 생성자 함수보다 엄격하며 생성자 함수에서 제공하지 않는 기능도 제공하며 몇가지 차이점이 있다.

클래스의 특징

1. 클래스 정의 방법

클래스는 함수다. 따라서 클래스는 값처럼 사용할 수 있는 일급 객체다. 함수와 마찬가지로 표현식으로도 정의할 수 있다. 일반적으로 파스칼 케이스를 사용한다.

const Person{}
// or
const Person = class {}
// or
const Person = class MycClass{}

2. 클래스 호이스팅

클래스 선언문으로 정의한 클래스는 함수 선언문과 같이 소스코드 평가 과정, 즉 런타임 이전에 먼저 평가되어 함수 객체를 생성한다. 이 함수 객체는 constructor이기 때문에 함수 객체 생성 시점에 프로토타입도 같이 생성된다. (둘은 항상 공존)
클래스 선언문도 모든 선언된 식별자들과 마찬가지로 호이스팅이 발생하지만 클래스 선언문 이전에 일시적 사각지대 TDZ에 빠지기 때문에 호이스팅이 발생하지 않는 것처럼 동작한다.

3. 인스턴스 생성

클래스는 생성자 함수이며 new 연산자와 함께 호출되어 인스턴스를 생성한다. 클래스는 인스턴스를 생성하는 것이 유일한 존재 이유이므로 반드시 new 연산자와 함께 호출한다.

class Person{}
const person = Person()
// new 연산자 없이 호출 시 에러
// TypeError: Class constructor Person cannot be invoked without 'new'

4. 메서드

클래스 몸체에는 0개 이상의 메서드만 선언할 수 있다. 세가지 종류가 있다.

1. constructor(생성자)

클래스 몸체에 정의한 constructor는 단순한 메서드가 아니다. 클래스 정의가 평가되면 constructor의 기술된 동작을 하는 함수 객체가 생성된다.

  • 두개 이상 사용할 수 없고 생략 가능하다.
  • 생략시 암묵적으로 빈 constructor를 정의하고 호출시 빈 객체를 생성한다.
  • 인스턴스에 프로퍼티 추가, 인스턴스 초기화 실행.
  • 클래스가 호출되면 constructor는 this를 암묵적으로 반환하기 때문에 리턴문을 반드시 생략.
class Person{
  constructor(name){
    //고정값으로 인스턴스 초기화
    this.age = 29
    // 인수로 인스턴스 초기화
    this.name = name
  }
}
const person = new Person('bumi')
console.log(person)
/*
Person {
  name: 'bumi',
  age: 29,
  __proto__: { constructor: ƒ Person() }
}
*/

2. 프로토타입 메서드

생성자 함수를 사용하여 인스턴스를 생성하는 경우 프로토타입 메서드를 생성하기 위해서는 명시적으로 프로토타입에 메서드를 추가해야 하지만, 클래스의 몸체에 정의한 메서드는 기본적으로 프로토타입 메서드가 된다.

생성자 함수

function User(name){
  this.name = name
  this.constructor.prototype.sayThanks = function(){
    console.log(`Thanks! ${this.name}`)
  }
}

const user = new User('Jack')
console.log(user.sayThanks()) // 'Thanks! Jack'
console.log(user.__proto__.hasOwnProperty('sayThanks')) // true

클래스

class Person{
  constructor(name){
	this.name = name![](https://velog.velcdn.com/images/bumikim26/post/ba9cab92-a300-4fe1-b93d-e9c9c4970f9a/image.png)

  }
  sayThanks(){
    console.log(`Thanks! ${this.name}`)
  }
}
const person = new Person('bumi')
console.log(person.sayThanks()) // 'Thanks! bumi'
console.log(Object.getPrototypeOf(person).hasOwnProperty('sayThanks')) // true

3. 정적 메서드

정적메서드란 인스턴스를 생성하지 않아도 호출할 수 있는 메서드.
생성자 함수의 정적 메서드 추가 방법

const Person = (function(){
  function Person(name){
    this.name = name
  }
  //정적 메서드
  Person.typeOf = function(name){
    console.log(typeof name)
  }
  Person.prototype.sayBye = function(){
    console.log('Bye')
  }
  
  return Person
}())

const person = new Person(1)
console.log(person)
Person.typeOf(person.name)
person.sayBye()

클라스의 경우

class Person {
  constructor(name){
    this.name = name
  }
  // 정적 메서드
  static typeOf(name){
    console.log(typeof name)
  }
  sayBye(){
    console.log(`Bye! ${this.name}`)
  }
}
const person = new Person('lee')
console.log(person)
Person.typeOf(person.name)
person.sayBye()

이처럼 정적 메서드와 프로토타입 메서드는 속한 프로토타입 체인이 다르고 프로토타입 메서드는 인스턴스 생성 후 인스턴스로 호출한다. 정적 메서드는 인스턴스의 프로퍼티를 참조할 수 없기 때문에 인스턴스 프로퍼티를 참조해야한다면 프로토타입 메서드를 사용해야한다. this 바인딩이 다르기때문.

정적 메서드는 앱 저너역에서 사용할 유틸리티 함수들을 전역으로 정의하지 않고 메서드로 구조화할때 유용하다. (충돌 방지, 공간 정리)

5. 클래스의 인스턴스 생성 과정

1. 인스턴스 생성과 this 바인딩

  • new 연산자와 함께 클라스 호출 시 constructor 내부 코드 실행 전에 빈 객체를 생성한다. 이 빈 객체가 클래스가 생성한 인스턴스다. 클라스의 prototype 프로퍼티가 가리키는 객체로 인스턴스의 프로토타입을 설정한다. 암묵적으로 인스턴스를 this에 바인딩한다.

2. 인스턴스 초기화

  • constructor 내부 코드가 실행되어 this에 바인딩되어 있는 인스턴스를 초기화한다. this에 바인딩 되어있는 인스턴스에 프로퍼티를 추가하고 constructor 인수로 전달받은 값으로 프로퍼티 값을 초기화한다.

3. 인스턴스 반환

클래스의 모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this를 반환한다.

6. 프로퍼티

인스턴스 프로퍼티:

인스턴스 프로퍼티는 constructor 내부에서 정의해야 한다.

접근자 프로퍼티:

접근자 프로퍼티는 자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티이다.

class Person {
  constructor(firstName, lastName){
    this.firstName = firstName
    this.lastName = lastName

  }
  get fullName(){
    return `${this.firstName} ${this.lastName}`
  }
  set fullName(name){
    [this.firstName,this.lastName] = name.split(' ')
  }
}
const person = new Person('bumi','kim')
person.fullName = 'bumi lee'
person.fullName

클래스 필드 정의 제안:

인스턴스 프로퍼티를 선언하기 위해 클래스 몸체에 this를 생략하고 클래스 필드로 선언할 수 있다.최신 브라우저와 최신 Node.js에서 사용 가능하다.하지만 클래스 필드에 함수를 할당하는 경우에는 함수가 인스턴스의 메서드가 되기때문에 권장하지 않는다.(화살표 함수 참조)

class Person {
  // 클래스 필드에 수 할당
  age = 10
  constructor(name, age){
    this.name = name
    // 외부 값으로 초기화
    this.age = age
  }
// 클래스 필드에 함수 할당
// !!! 클래스 필드에 함수를 할당할 경우 프로토타입 메서드가 아닌 인스턴스 메서드가 된다.
  getInfo = function(){
    return `${this.name} ${this.age}`
  }
}
const person = new Person('bumi',29)
console.log(person.getInfo())

private 필드 정의 제안:

자바스크립트의 인스턴스 프로퍼티는 언제나 public 하다. 즉 외부에서 참조 가능하다. 하지만 최신 브라우저나 최신 Node.js에서 private 필드를 정의할 수 있다.

// private 앞에 #을 붙여 씀
class Person{
    #name = ''

    constructor(name){
        this.#name = name
    }
}

const person = new Person('bumi')

console.log(person.name)
// undefined
console.log(person.#name)
// 참조 불가넝
// Uncaught SyntaxError: Private field '#name' must be declared in an enclosing class

클래스 외부에서 private 필드에 직접 접근할 수 있는 방법은 없지만, 접근자 프로퍼티를 이용해 간접적으로 접근 가능하다.

class Person{
    #name = ''

    constructor(name){
        this.#name = name
    }

    get name(){
        return this.#name.trim()
    }
}

const person = new Person('bumi')
undefined
console.log(person)
// Person {#name: 'bumi'}

console.log(person.name)
// bumi

static 필드 정의 제안:

클래스 몸체에서 static 키워드를 통해 정적 필드, 정적 메서드 등을 정의할 수 있다. 혼란하다 혼란해.

class User {
// static private 필드
    static #limitation = 20;

// static 메서드
    static limitationInfo(){
        return `the limitation is ${User.#limitation}`
    }
// static public 필드
    static sex = 'female'
// static 메서드
    static toString(num){
        num = num.toString()
        return num
    }
    constructor(age){
        this.age = age
    }
}

const user = new User(20)
console.log(user)
// User {age: 20}
User.toString(user.age)
//'20'
User.#limitation
// Uncaught SyntaxError: Private field '#limitation' must be declared in an enclosing class
User.limitationInfo()
// 'the limitation is 20'
User.sex
// 'female'

상속에 의한 클래스 확장

상속에 의한 클래스 확장이란 기존 클래스를 상속받아 새로운 클래스로 확장하여 정의하는 것. 코드의 재사용 관점에서 매우 유용하다.

class SuperClass {
  constructor(name){
    this.name = name
  }
}
class SubClass extends SuperClass {}
const person = new SubClass('bumi')
console.log(person)

/*
SubClass {
  name: 'bumi',
  __proto__: { constructor: ƒ SubClass() }
}
*/

클래스간의 프로토타입 체인도 생성된다.

super 키워드

super 키워드는 함수처럼 호출할 수도 있고, this 처럼 식별자처럼 참소할 수 있는 특수한 키워드.

  1. super 호출: 수퍼클래서의 constructor를 호출한다.
class SuperClass {
  constructor(a,b){
    this.a = a
    this.b = b
  }
}
class SubClass extends SuperClass {
  constructor(a,b,c){
    super(a,b) // 여기서 호출을 통해 수퍼클래스 constructor에 a,b인수를 전달한다.
    this.c = c
  }
}
const person = new SubClass(1,2,3)
console.log(person)

/*
SubClass {
  a: 1,
  b: 2,
  c: 3,
  __proto__: { constructor: ƒ SubClass() }
}
*/

❗️서브클래스의 constructor에서 super를 호출하기 전에는 this를 참조할 수 없다. super는 서브클래스의 constructor에서만 호출된다.

왜냐하면, 서브클래스는 자신이 직접 인스턴스를 생성하지 않고, 수퍼클래스에 인스턴스 생성을 위임하는데 그렇기 때문에 서브클래스의 constructor에서 반드시 super를 호출해야 하는 이유다. super가 호출되지 않으면 인스턴스가 생성되지도, 반환되지도 않는다.

  1. super 참조: 수퍼클래서의 메서드를 호출할 수 있다.
class SuperClass {
  constructor(a,b){
    this.a = a
    this.b = b
  }
  sayA(){
    return this.a
  }
}
class SubClass extends SuperClass {
  constructor(a,b,c){
    super(a,b)
    this.c = c
  }
  // 메서드 오버라이딩
  sayA(){
    return `${super.sayA()} + 10`
  }
}
const person = new SubClass(3,2,3)
console.log(person.sayA())

상속 클래스의 인스턴스 생성 과정

  1. new 연산자와 함께 서브클래스를 호출했을 때 서브클래스의 super가 호출된다. -> 수퍼클래스의 constructor가 호출되며 객체를 생성한다.
  2. 수퍼클래스가 생성한 빈 객체 즉 인스턴스는 수퍼클래스의 constructor내부의 this에 바인딩 된다. 이때 인스턴스는 수퍼클래스가 생성했지만 서브클래스로 new 연산자와 호풀했기 때문에 인스턴스의 프로토타입은 서브클래스의 prototype이 가리키는 객체다.
  3. 수퍼클래스의 인스턴스 초기화. this에 바인딩되어 있는 인스턴스에 프로퍼티를 추가하고 인수로 받은 초기값으로 인스턴스의 프로퍼티를 초기화한다.
  4. super 호출이 종료되면 제어 흐름이 서브클래스의 constructor로 돌아온다. super가 반환한 인스턴스가 this에 바인딩 되고 그 인스턴스를 그대로 사용한다.
  5. 서브클래스의 인스턴스 초기화
  6. 클래스의 모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this가 반환된다.

표준 빌트인 생성자 함수 확장

extends 키워드 다음에는 클래스뿐 아니라 [[Construct]]내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식을 사용할 수 있기에 표준 빌트인 객체도 확장할 수 있다.

ES6 함수 추가 기능

배경: ES6 이전의 함수는 사용 목적에 따라 구분되지 않는다. 모든 함수는 일만 함수로 호출할 수 있고, 생성자 함수로도 호출할 수 있다. 즉 모두 callable하고 constructor 이다.

var obj = {
  x: 10,
  f: function(){ return this.x }
}
// 함수를 메서드로서 호출
console.log(obj.f()) // 10

// f에 바인딩된 함수를 일반 함수로서 호출
var bar = obj.f 
console.log(bar()) // undefined

//생성자 함수로서 호출
console.log(new obj.f()) // f{}

이러한 자유는 실수를 유발할 가능성도 있지만 성능면에서도 문제가 있다. 왜냐하면 객체에 바인딩된 함수나 콜백 함수가 constructor 라는것은 prototype 프로퍼티를 가지며, 불필요하게 프로토타입 객체도 생성한다는 것을 의미하기 때문이다.

세가지 함수 종류

이를 해결하기 위해 ES6에서는 함수를 사용 목적에 따라 세가지 종류로 구분했다.
1. 일반함수 constructor
2. 메서드 non constructor
3. 화살표 함수 non constructor

메서드

ES6 사양에서 메서드는 축약 표현으로 정의된 함수만을 의미한다.

var obj = {
  x: 10,
  f: function(){ return this.x },
  bar() {return this.x}
}
console.log(obj.f())
console.log(obj.bar())
new obj.f()
new obj.bar() // TypeError: obj.bar is not a constructor
obj.bar.hasOwnProperty('prototype') // false

ES6 메서드는 자신을 바인딩한 객체를 가리키는 내부슬롯 [[HomeObject]]를 갖는다. super 참조는 내부슬롯 [[HomeObject]]를 사용하여 수퍼클래스의 메서드를 참조하므로 [[HomeObject]]를 갖는 ES6 메서드는 super 키워드를 사용할 수 있는 이유이다.

class Base {
  name = 'bumi'
  sayName(){
    return this.name
  }
}
class Derived extends Base{
  sayName(){
    return super.sayName()
  }
}

const person = new Derived()
person.sayName() // 'bumi'

화살표 함수

화살표 함수는 기존 함수 정의 방식보다 간략하게 함수를 정의할 수 있는데, 내부 동작도 간략하게 동작한다. ⭐️콜백 함수 내부에서 this가 전역 객체를 가리키는 문제를 해결하기 위한 대안으로 유용하다.⭐️
또 화살표 함수도 일급 객체이므로 고차함수에 인수로 전달할 수 있다. 가독성면에서 간결하여 좋다.

[1,2,3].map(v => v * 2) // [2,4.6]

특징:

  • non constructor이다. 즉 생성자 함수로서 호출 불가넝. prototype 프로퍼티도 없다.
  • 화살표 함수는 함수 자체의 this, arguments, super, new.target 바인딩을 갖지 않는다. => 화살표 함수 내부에서 얘네를 참조하면 스코프 체인을 통해 상위 스코프의 this, arguments, super, new.target을 참조한다.
  • ⭐️⭐️⭐️ 화살표 함수의 this는 일반 함수와 다르게 동작한다. 아래 예제를 참조.

화살표함수와 this

class Prefixer {
  constructor(prefix){
    this.prefix = prefix
  }
  add(stringArr){
    return stringArr.map(function (item){
          return this.prefix + item
    })
  }
}

const prefixer = new Prefixer('pre')
prefixer.add(['one','two'])

// TypeError: Cannot read properties of undefined (reading 'prefix')

위의 예제에서 map 인수로 전달한 콜백함수의 내부의 this는 전역객체를 가리킨다. 메서드가 콜백함수를 일반 함수로 호출했기 때문이다.

class Prefixer {
  constructor(prefix){
    this.prefix = prefix
  }
  add(stringArr){
    return stringArr.map(item => this.prefix + '-' + item)
  }
}

const prefixer = new Prefixer('pre')
prefixer.add(['one','two'])
// [ 'pre-one', 'pre-two' ]

❗️위처럼 화살표 함수를 사용해서 이 문제를 해결할 수 있다. 물론 다른 방법으로도 해결 가능하다.

화살표 함수는 함수 자체의 this 바인딩을 갖지 않는다. 따라서 화살표 함수 내부에서 this를 참조하면 상위 스코프의 this를 그대로 참조한다. 화살표 함수의 this는 함수가 정의된 위치에 의해 결정된다.

화살표 함수와 arguments

화살표 함수는 자체 arguments 바인딩을 갖지 않기 때문에 마찬가지로 상위 스코프의 arguments를 참조한다.

const foo = () => {console.log(arguments)}
foo(1,2)
//ReferenceError: arguments is not defined

화살표 함수로 가변 인자 함수를 구현해야 할 때는 반드시 Rest 파라미터를 사용한다.

Rest 파라미터

매개변수 이름 앞에 세개의 점 ...을 붙이셔 정의한 매개변수를 의미한다. Rest 파라미터는 함수에 전달된 인수들의 목록을 배열로 전달받는다.

function foo(...rest){
  console.log(rest)
}

foo(1,2,3,4,5)
// [ 1, 2, 3, 4, 5 ]

요롷게 사용 가능.

function sum(...args){
  return args.reduce((pre,cur)=> pre + cur,0)
}
console.log(sum(1,2,3,7,9,33))

예시

const test = () => { return arguments }
test(1,2,3) 
// ReferenceError: arguments is not defined

const test2 = (...args) => { return args }
test2(1,2,3)
// [ 1, 2, 3 ]


const sum = (...args) => {
  return args.reduce((pre,cur)=> pre + cur, 0)
}
sum(3,1,3)
// 7

매개변수 기본값

function sum(a,b){
  return a + b
}

sum(1) // NaN
sum.length //. 2

function es6Sum(a = 0, b =0){
  return a + b
}

es6Sum(1) // 1
es6Sum.length // 0

0개의 댓글