Thumbnail

4분

JavaScript Object(2) - Class

JavaScript에서 Class 문법은 ES6부터 공식적으로 도입되었습니다. 이전에는 프로토타입을 통해 객체 구조를 구현했습니다. ES6의 Class 문법은 개발자들이 보다 쉽게 학습하고 사용할 수 있는 명료한 구조를 제공합니다.

클래스 구조

Class는 사실 "특별한 함수"입니다. 함수를 함수 표현식과 함수 선언으로 정의할 수 있는 것처럼, 클래스 문법도 클래스 선언과 클래스 표현식 두 가지 방법을 제공합니다.

클래스 선언

함수 선언과 클래스 선언의 중요한 차이점은 호이스팅 여부입니다. 함수 선언의 경우 호이스팅이 일어나지만, 클래스 선언은 그렇지 않습니다.

class Rectangle {
  constructor(height, width) {
    this.height = height
    this.width = width
  }
}

위와 같은 문법으로 Rectangle이라는 Class 선언이 가능합니다. 앞서서도 설명했지만 이전 함수 선언(프로토타입)과 달리 Class는 호이스팅이 일어나지 않습니다. 다시 말해 클래스 사용을 위해서는 반드시 먼저 클래스를 선언해야 합니다.

클래스 표현식

클래스 표현식에는 클래스 선언에서 설명된 것과 동일하게 호이스팅 제한이 적용됩니다.

// unnamed
let Rectangle = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};
console.log(Rectangle.name);
// 출력: "Rectangle"
 
// named
let Rectangle = class Rectangle2 {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};
console.log(Rectangle.name);
// 출력: "Rectangle2"

정적 메소드와 속성

클래스 문법 안에 있는 코드는 항상 strict mode로 실행됩니다. static 키워드를 이용해 메소드를 정적 메소드로 사용할 수 있습니다.

class Animal {
  speak() {
    return this
  }
  static eat() {
    return this
  }
}

정적 메소드는 인스턴스화를 거치지 않고도 사용할 수 있게 해줍니다. (클래스를 사용하지 않더라도 메모리에 올라가게 됩니다.)

다음 예시를 보겠습니다.

let obj = new Animal()
obj.speak() // the Animal object
let speak = obj.speak
speak() // undefined
 
Animal.eat() // class Animal
let eat = Animal.eat
eat() // undefined

위에서도 설명했지만 Class 내부는 항상 strict mode로 동작합니다. non-strict mode라면 this는 기본적으로 전역 객체인 초기 this 값에 자동 바인딩 됩니다. 하지만 strict mode 이기 때문에 this 값은 전달된 대로 유지됩니다.

그래서 위와 같이 this를 가지지 않는 메소드를 호출하게 되면 undefined가 나타나게 됩니다.

인스턴스 속성

인스턴스 속성은 반드시 클래스 메소드 내에 정의되어야 하며, 정적 속성과 프로토타입 속성은 반드시 클래스 선언부 바깥쪽에서 정의되어야 합니다.

class Rectangle {
  constructor(height, width) {
    this.height = height
    this.width = width
  }
}
Rectangle.staticWidth = 20
Rectangle.prototype.prototypeWidth = 25

인스턴스 속성은 인스터스화가 되면서 사용될 수 있도록 해야하며, 정적 속성이나 프로토타입 속성은 인스턴스화가 이루어지지 않아도 사용될 수 있어야 하기 때문에 그렇습니다.

Private Field 선언

클래스 내에 private 필드를 선언하려면 #을 붙이면 됩니다. 일반적인 속성과 다르게 할당과 동시에 만들어질 수 없습니다.

class Rectangle {
  #height = 0
  #width
  constructor(height, width) {
    this.#height = height
    this.#width = width
  }
}

그리고 private 필드를 사용할 때에는 반드시 사용 전 선언되어야 합니다. 일반적인 속성과 다르게 할당과 동시에 만들어질 수 없습니다.

extends를 통한 상속

class Dog extends Animal

JavaScript에서 Class 상속을 구현할 때에는 extends 키워드를 사용합니다. 예를 들어, Dog 클래스가 Animal 클래스를 상속받는다면 다음과 같이 작성할 수 있습니다.

class Animal {
  constructor(name) {
    this.name = name
  }
 
  speak() {
    console.log(`${this.name} makes a noise.`)
  }
}
class Dog extends Animal {
  constructor(name) {
    super(name) // super class 생성자를 호출하여 name 매개변수 전달
  }
 
  speak() {
    console.log(`${this.name} barks.`)
  }
}

위 코드에서 super 키워드는 부모 클래스의 생성자를 호출합니다.

예를 들어 만약 자식 클래스에서 생성자를 구현하고 내부에서 this를 사용한다면, super를 먼저 호출해야 합니다.

this-error.png

bind, call, apply

이전 포스트에서 상속 시 Function.prorotype.call을 통해 생성자를 상속하여 사용하는 것을 보았습니다.

bind(), call(), apply() 메소드를 사용하여 this 값을 설정하거나 함수의 인수를 고정할 수 있습니다.

bind

function.bind(thisArg, arg1, arg2, ...)

  • 첫 번째 인자: this 키워드를 설정
  • 이어지는 인자: 바인드된 함수의 인수에 제공

bind()는 새로운 바인딩한 함수를 만듭니다. call()와 비슷하지만 즉시 실행되는데 반해, bind()는 바인딩 된(캡쳐된) 새로운 함수를 만듭니다.

함수의 this 값을 고정하는 것이 주된 목적이지만, 함수의 일부 인수를 미리 지정하고 나머지 인수는 호출 시에 전달할 수 있습니다. 이는 일부 인수가 항상 고정된 함수를 만들 때 유용합니다.

1. this(스코프) 제한

this.x = 9
var module = {
  x: 81,
  getX: function () {
    return this.x
  },
}
 
module.getX() // 81
 
var retrieveX = module.getX
retrieveX()
// 9 반환 - 함수가 전역 스코프에서 호출됐음

만약 마지막 줄에서 retrieveX를 호출하였을 때 this를 module 내부로 한정하고 싶다면 어떻게 해야할까요?

var boundGetX = retrieveX.bind(module)
boundGetX() // 81

retriveX가 bind함수를 호출하면서 첫 번째 인자로 this값인 module을 호출하였습니다. 그리하여 this값이 module이 되어 스코프가 module 내로 한정되며 되는 것입니다.

2. 매개변수를 고정하기 위해

function addArguments(arg1, arg2) {
  return arg1 + arg2
}
var result1 = addArguments(1, 2) // 3

위와 같은 add함수를 만들었다고 생각해봅시다. 그런데 항상 10을 더하는 함수를 생성하고 싶다고 하면 다음과 같이 하면 됩니다.

// 첫 번째 인수를 지정하여 함수를 생성합니다.
var addTen = addArguments.bind(null, 10)

this 는 무시하고, 두 번째 인자부터는 함수에 대한 매개변수를 뜻하므로 arg1에 항상 10이 들어가는 함수를 만들게 되었습니다.

var result2 = addTen(5) // 10 + 5 = 15
 
// 두 번째 인수는 무시됩니다.
var result3 = addTen(5, 10) // 10 + 5 = 15

call

call() 메소드는 주어진 this 값 및 각각 전달된 인수와 함께 함수를 호출합니다.

  • 첫 번째 인자: this
  • 이어지는 인자: 바인드된 함수의 인수에 제공

bind와 마찬가지로 새롭게 this를 한정하거나 매개변수를 한정하면서 함수를 호출할 때 사용합니다.

function Product(name, price) {
  this.name = name
  this.price = price
 
  if (price < 0) {
    throw RangeError("Cannot create product " + this.name + " with a negative price")
  }
}
 
function Food(name, price) {
  Product.call(this, name, price)
  this.category = "food"
}
 
function Toy(name, price) {
  Product.call(this, name, price)
  this.category = "toy"
}
 
var cheese = new Food("feta", 5)
var fun = new Toy("robot", 40)

call.png

apply

apply() 메소드는 주어진 this값과 배열로 제공되는 arguments로 함수를 호출합니다

  • 첫 번째 인자: this
  • 두 번째 인자: array

apply()call()과 거의 유사합니다. 근본적인 차이점은 call()은 함수에 전달될 인수 리스트를 받는데 비해, apply()는 인수들의 단일 배열을 받습니다.

const numbers = [5, 6, 2, 3, 7]
 
const max = Math.max.apply(null, numbers)
 
console.log(max)
// expected output: 7
 
const min = Math.min.apply(null, numbers)
 
console.log(min)
// expected output: 2

마무리하며

Class는 다른 객체 지향 언어에서 이미 많이 사용되고 있기 때문에, JavaScript에서도 이를 지원하면서 객체 지향 프로그래밍의 구현이 보다 쉬워졌습니다. 그러나 prototype 역시 JavaScript에서 객체 지향 프로그래밍을 구현하는 데 매우 중요한 개념 중 하나입니다.

JavaScript를 사용한면서 이번에 조금 자세히 알게된 bind(), call(), apply() 메소드에 대해 충분히 이해할 수 있어서 좋았습니다.

자바스크립트가 좋은 언어든 매력이 떨어지는 언어든, 효율적이지 않든 상관없이, 이런 내용을 알아가는 것이 재미 있습니다.

Reference

마지막 업데이트

3/26/2023


Avatar

JHSeo

배우는 것을 좋아하고 관심이 많은 웹 엔지니어 입니다. 느리더라도 꾸준하게 성장하려고 노력하는 개발자입니다.