객체 지향 JavaScript : ES6 클래스 심층 분석 - Lee-hyuna/33-js-concepts-kr GitHub Wiki
우리는 자동차 엔진, 컴퓨터 파일, 라우터, 온도 판독기 같은 프로그램에서 아이디어나 개념을 말해야 한다.
코드에서 이러한 개념을 직접 나타내는 것은 상 두 부분으로 나뉘어 있다.
즉, 상태를 나타내는 데이터와 동작을 나타내는 함수이다.
ES6 클래스는 개념을 나타내는 객체의 상태와 동작을 정의하기 위한 편리한 구문을 제공한다.
ES6 클래스는 초기화 함수가 호출 되도록 보장함으로써 코드를 보다 안전 하게하며,
해당 데이터에서 작동하고 유효한 상태를 유지하는 고정 함수 세트를 보다 쉽게 정의 할 수 있도록 합니다.
만약 당신이 어떤 것을 별개의 실체로 생각할 수 있다면, 당신의 프로그램에서 그 "무엇을" 나타내는 클래스를 정의해야 할 것 같다.
이 비 클래스 코드를 고려하십시오. 얼마나 많은 오류를 찾을 수 있습니까? 그것들을 어떻게 고치습니까?
// set today to December 24
const today = {
month: 24,
day: 12,
};
const tomorrow = {
year: today.year,
month: today.month,
day: today.day + 1,
};
const dayAfterTomorrow = {
year: tomorrow.year,
month: tomorrow.month,
day: tomorrow.day + 1 <= 31 ? tomorrow.day + 1 : 1,
};
오늘 날짜가 유효하지 않습니다. 24개월이 없습니다. 또한 오늘이 완전히 초기화되지 않았습니다. 연도가 없습니다.
잊을 수없는 초기화 기능이 있다면 더 좋습니다. 또한 하루를 추가 할 때 31을 넘어서도 다른 곳에서 그 수표를 놓치면 한 곳에서 체크인 했습니다.
각각 유효한 상태를 유지하는 작고 고정 된 함수 집합을 통해서만 데이터와 상호 작용하는 것이 좋습니다.
class SimpleDate {
constructor(year, month, day) {
// Check that (year, month, day) is a valid date
// ...
// If it is, use it to initialize "this" date
this._year = year;
this._month = month;
this._day = day;
}
addDays(nDays) {
// Increase "this" date by n days
// ...
}
getDay() {
return this._day;
}
}
// "today" is guaranteed to be valid and fully initialized
const today = new SimpleDate(2000, 2, 28);
// Manipulating data only through a fixed set of functions ensures we maintain valid state
today.addDays(1);
Constructors
생성자 메서드는 특별하하며 첫 번째 문제를 해결합니다.
그 일은 인스턴스를 유효한 상태로 초기화하는 것이며, 객체를 초기화하는 것을 잊지 않도록 자동으로 호출됩니다.
개인 정보 비공개
우리는 그들의 상태가 유효하도록 우리의 수업을 설계하려고 노력한다.
우리는 유효한 값만 생성하는 생성자를 제공하고, 항상 유효한 값만 남기는 메서드를 설계합니다.
그러나 클래스의 데이터를 모든 사람이 액세스 할 수 있는 상태로 두면 누군가가 데이터를 엉망으로 만들 수 있습니다.
우리는 우리가 제공하는 기능을 제외하고는 데이터에 접근 할 수 없도록 하여 보호합니다.
개인 정보 보호 협약
안타깝게도 은밀한 객체 속성은 JavaScript에 존재하지 않습니다. 우리는 그것들을 가짜로 만들어야 합니다.
가장 일반적인 방법은 간단한 규칙을 따르는 것입니다. 속성 이름 앞에 밑줄을 붙인 경우 비공개로 취급해야합니다.
이전 코드 예제에서 이 방법을 사용했습니다. 일반적으로 이 간단한 컨벤션은 효과가 있지만 기술적으로 모든 사람이 데이터에 액세스 할 수 있으므로
올바른 일을 하려면 자신의 규율에 의존해야 합니다.
권한있는 방법으로 개인 정보 보호
은밀한 객체 속성을 위조하는 가장 일반적인 방법은 생성자에서 일반 변수를 사용하여 클로저로 캡처하는 것입니다.
이 방법은 외부에서 액세스 할 수 없는 개인 정보를 제공합니다.
그러나 작동하게 하려면 클래스의 메소드 자체를 생성자에서 정의하고 인스턴스에 첨부해야 합니다.
class SimpleDate {
constructor(year, month, day) {
// Check that (year, month, day) is a valid date
// ...
// If it is, use it to initialize "this" date's ordinary variables
let _year = year;
let _month = month;
let _day = day;
// Methods defined in the constructor capture variables in a closure
this.addDays = function(nDays) {
// Increase "this" date by n days
// ...
}
this.getDay = function() {
return _day;
}
}
}
Symbols 개인 정보
Symbols은 ES6부터 JavaScript의 새로운 기능이며 은밀한 객체 속성을 위조하는 다른 방법을 제공합니다.
밑줄 속성 이름 대신 고유 한 Symbols 개체 키를 사용할 수 있으며 클래스는 해당 키를 클로저로 캡처 할 수 있습니다. 하지만 누출이 있습니다.
JavaScript의 또 다른 새로운 기능은 Object.getOwnPropertySymbols이며,
외부에서 비공개로 유지하려고 시도한 Symbols 키에 액세스 할 수 있습니다.
const SimpleDate = (function() {
const _yearKey = Symbol();
const _monthKey = Symbol();
const _dayKey = Symbol();
class SimpleDate {
constructor(year, month, day) {
// Check that (year, month, day) is a valid date
// ...
// If it is, use it to initialize "this" date
this[_yearKey] = year;
this[_monthKey] = month;
this[_dayKey] = day;
}
addDays(nDays) {
// Increase "this" date by n days
// ...
}
getDay() {
return this[_dayKey];
}
}
return SimpleDate;
}());
Weak Maps을 이용한 개인정보
Weak Maps은 JavaScript의 새로운 기능입니다. 인스턴스를 키로 사용하여 프라이빗 객체 속성을 키 / 값 쌍으로 저장할 수 있으며
클래스는 해당 키 / 값 맵을 클로저로 캡처 할 수 있습니다.
const SimpleDate = (function() {
const _years = new WeakMap();
const _months = new WeakMap();
const _days = new WeakMap();
class SimpleDate {
constructor(year, month, day) {
// Check that (year, month, day) is a valid date
// ...
// If it is, use it to initialize "this" date
_years.set(this, year);
_months.set(this, month);
_days.set(this, day);
}
addDays(nDays) {
// Increase "this" date by n days
// ...
}
getDay() {
return _days.get(this);
}
}
return SimpleDate;
}());
다른 접근 수정자
"비공개" 외에 "보호", "내부", "패키지 개인" 또는 "친구"와 같은 다른 언어로 볼 수 있는 가시성 수준이 있습니다.
JavaScript는 여전히 다른 수준의 가시성을 강제할 수 있는 방법을 제공하지 않습니다. 필요한 경우 컨벤션과 본인 규율에 의존해야합니다.
현재 객체 참조
getDay()를 다시 보십시오. 매개 변수를 지정하지 않으므로 호출 된 개체를 어떻게 알 수 있습니까?
object.function 표기법을 사용하여 함수를 메서드로 호출하면 객체를 식별하는 데 사용하는 암시적 인수가 있으며,
이 암시 적 인수는 this라는 암시적 매개 변수에 할당됩니다. 다음은 객체 인수를 암시적이 아닌 명시적으로 보내는 방법입니다.
// Get a reference to the "getDay" function
const getDay = SimpleDate.prototype.getDay;
getDay.call(today); // "this" will be "today"
getDay.call(tomorrow); // "this" will be "tomorrow"
tomorrow.getDay(); // same as last line, but "tomorrow" is passed implicitly
정적 속성 및 메소드
클래스의 일부이지만 해당 클래스의 인스턴스가 아닌 데이터와 함수를 정의 할 수 있는 옵션이 있습니다.
이러한 정적 속성과 정적 메서드를 각각 호출합니다. 인스턴스당 새 복사본이 아닌 정적 속성의 복사본 하나만 있을 것입니다.
class SimpleDate {
static setDefaultDate(year, month, day) {
// A static property can be referred to without mentioning an instance
// Instead, it's defined on the class
SimpleDate._defaultDate = new SimpleDate(year, month, day);
}
constructor(year, month, day) {
// If constructing without arguments,
// then initialize "this" date by copying the static default date
if (arguments.length === 0) {
this._year = SimpleDate._defaultDate._year;
this._month = SimpleDate._defaultDate._month;
this._day = SimpleDate._defaultDate._day;
return;
}
// Check that (year, month, day) is a valid date
// ...
// If it is, use it to initialize "this" date
this._year = year;
this._month = month;
this._day = day;
}
addDays(nDays) {
// Increase "this" date by n days
// ...
}
getDay() {
return this._day;
}
}
SimpleDate.setDefaultDate(1970, 1, 1);
const defaultDate = new SimpleDate();
서브 클래스
우리는 간혹 클래스간에 공통성을 발견합니다. 반복 코드입니다.
서브 클래스는 우리가 다른 클래스의 상태와 행동을 우리 자신의 것으로 통합하도록 합니다.
이 프로세스를 상속 이라고하며 하위 클래스는 상위 클래스 (수퍼 클래스라고도 함)에서 "상속" 한다고 합니다.
상속은 중복을 피하고 다른 클래스와 동일한 데이터 및 기능이 필요한 클래스의 구현을 단순화 할 수 있습니다.
상속은 또한 공통 슈퍼 클래스가 제공하는 인터페이스에만 의존하여 서브 클래스를 대체 할 수 있게 합니다.
중복 방지를 위한 상속
class Employee {
constructor(firstName, familyName) {
this._firstName = firstName;
this._familyName = familyName;
}
getFullName() {
return `${this._firstName} ${this._familyName}`;
}
}
class Manager {
constructor(firstName, familyName) {
this._firstName = firstName;
this._familyName = familyName;
this._managedEmployees = [];
}
getFullName() {
return `${this._firstName} ${this._familyName}`;
}
addEmployee(employee) {
this._managedEmployees.push(employee);
}
}
_firstName 및 _familyName 데이터 속성과 getFullName 메소드는 클래스 간에 반복 됩니다.
Manager클래스가 Employee클래스에서 상속 되도록하여 반복을 제거 할 수 있습니다.
그렇게하면 Employee클래스의 상태와 동작 (데이터 및 기능)이 Manager 클래스에 통합됩니다.
상속을 사용하는 버전이 있습니다. super 사용을 주목하십시오 :
// Manager still works same as before but without repeated code
class Manager extends Employee {
constructor(firstName, familyName) {
super(firstName, familyName);
this._managedEmployees = [];
}
addEmployee(employee) {
this._managedEmployees.push(employee);
}
}
IS-A와 WORKS-LIKE-A
상속이 적절한 시기를 결정하는데 도움이 되는 디자인 원칙이 있습니다. 상속은 항상 IS-A와 WORKS-LIKE-A 관계를 모델링 해야합니다.
즉, 관리자는 a와 a같은 특정 유형의 직원이며, 따라서 우리가 슈퍼클래스 인스턴스에서 운영되는 모든 곳에서 우리는 서브클래스 인스턴스로
대체할 수 있어야 하며, 모든 것은 여전히 작동해야 한다. 이 원칙을 위반하는 것과 준수하는 것의 차이는 때때로 미묘 할 수 있습니다.
미묘한 위반의 전형적인 예는 Rectangle 슈퍼클래스와 Square 서브클래스입니다.
class Rectangle {
set width(w) {
this._width = w;
}
get width() {
return this._width;
}
set height(h) {
this._height = h;
}
get height() {
return this._height;
}
}
// A function that operates on an instance of Rectangle
function f(rectangle) {
rectangle.width = 5;
rectangle.height = 4;
// Verify expected result
if (rectangle.width * rectangle.height !== 20) {
throw new Error("Expected the rectangle's area (width * height) to be 20");
}
}
// A square IS-A rectangle... right?
class Square extends Rectangle {
set width(w) {
super.width = w;
// Maintain square-ness
super.height = w;
}
set height(h) {
super.height = h;
// Maintain square-ness
super.width = h;
}
}
// But can a rectangle be substituted by a square?
f(new Square()); // error
정사각형은 수학적으로 직사각형일 수 있지만 정사각형은 동작 방식으로 직사각형처럼 작동하지 않습니다.
슈퍼클래스 인스턴스를 서브클래스 인스턴스로 대체 할 수 있어야 한다는 규칙을 Liskov 대체 원칙이라고 하며
이는 객체 지향 클래스 디자인의 중요한 부분입니다.
남용을 조심하라
어느 곳에서나 공통점을 쉽게 찾을 수 있으며, 숙련된 개발자에게도 완벽한 기능을 제공하는 수업을 받을 수 있다는 전망은 매력적입니다.
그러나 상속에도 단점이 있습니다. 작고 고정 된 함수 집합을 통해서만 데이터를 조작하여 유효한 상태를 유지한다는 것을 기억하십시오.
그러나 상속을 받으면 데이터를 직접 조작 할 수 있는 함수 목록을 늘리고 이러한 추가 함수는 유효한 상태를 유지해야합니다.
너무 많은 함수가 데이터를 직접 조작 할 수 있는 경우 해당 데이터는 전역 변수만큼 나빠집니다.
상속이 너무 많으면 캡슐화를 희석하고 수정하기가 어렵고 재사용하기 어려운 단일적 클래스가 만들어집니다.
대신 하나의 개념을 구현하는 최소한의 클래스를 디자인하는 것을 선호하십시오.
코드 복제 문제를 다시 살펴 보겠습니다. 상속받지 않고 해결할 수 있을까요?
다른 방법은 참조를 통해 객체를 연결하여 부품 전체 관계를 나타내는 것입니다. 우리는 이것을 구성이라고 부릅니다.
상속이 아닌 구성을 사용하는 관리자-직원 관계의 버전:
class Employee {
constructor(firstName, familyName) {
this._firstName = firstName;
this._familyName = familyName;
}
getFullName() {
return `${this._firstName} ${this._familyName}`;
}
}
class Group {
constructor(manager /* : Employee */ ) {
this._manager = manager;
this._managedEmployees = [];
}
addEmployee(employee) {
this._managedEmployees.push(employee);
}
}
여기서 관리자는 별도의 클래스가 아닙니다. 대신 관리자는 그룹 인스턴스가 참조하는 일반 직원 인스턴스입니다.
상속이 IS-A 관계를 모델링하는 경우 구성은 HAS-A 관계를 모델링합니다. 즉, 그룹에는 관리자가 있습니다.
상속이나 구성이 프로그램 개념과 관계를 합리적으로 표현할 수 있다면 구성을 선호합니다.
서브클래스로 상속
또한 상속을 통해 공통 서브클래스가 제공하는 인터페이스를 통해 서로 다른 서브클래스를 상호 교환 가능하게 사용할 수 있습니다.
슈퍼클래스 인스턴스를 인수로 예상하는 함수는 함수가 서브클래스에 대해 알 필요없이 서브클래스 인스턴스에 전달 될 수도 있습니다.
공통 슈퍼클래스가 있는 클래스를 대체하는 것을 다형성이라고 합니다.
class Cache {
get(key, defaultValue) {
const value = this._doGet(key);
if (value === undefined || value === null) {
return defaultValue;
}
return value;
}
set(key, value) {
if (key === undefined || key === null) {
throw new Error('Invalid argument');
}
this._doSet(key, value);
}
// Must be overridden
// _doGet()
// _doSet()
}
// Subclasses define no new public methods
// The public interface is defined entirely in the superclass
class ArrayCache extends Cache {
_doGet() {
// ...
}
_doSet() {
// ...
}
}
class LocalStorageCache extends Cache {
_doGet() {
// ...
}
_doSet() {
// ...
}
}
// Functions can polymorphically operate on any cache by interacting through the superclass interface
function compute(cache) {
const cached = cache.get('result');
if (!cached) {
const result = // ...
cache.set('result', result);
}
// ...
}
compute(new ArrayCache()); // use array cache through superclass interface
compute(new LocalStorageCache()); // use local storage cache through superclass interface
More than Sugar
JavaScript의 클래스 구문은 종종 구문 설탕이라고 한다. 우리는 ES5에서 할 수 없는 것들을 ES6에서 할 수 있지만 실제적인 차이점이 있습니다.
정적 속성의 상속
ES5에서는 생성자 함수 간에 진정한 상속을 만들 수 없었습니다.
Object.create는 일반 객체를 만들 수 있지만 함수 객체는 만들 수 없습니다. 정적 속성의 상속을 수동으로 복사하여 위조했습니다.
이제 ES6 클래스를 사용하면 서브클래스 생성자 함수와 슈퍼클래스 생성자 간에 실제 프로토타입 링크가 제공됩니다.
// ES5
function B() {}
B.f = function () {};
function D() {}
D.prototype = Object.create(B.prototype);
D.f(); // error
// ES6
class B {
static f() {}
}
class D extends B {}
D.f(); // ok
내부 생성자가 서브클래스로 될 수 있다.
일부 개체는 "이국적"이며 일반 개체처럼 동작하지 않습니다. 예를 들어, 배열은 길이 속성을 가장 큰 정수 인덱스보다 크게 조정합니다.
ES5에서 Array의 서브클래스를 만들려고 할 때 새로운 연산자는 슈퍼클래스의 이국적인 객체가 아닌 서브클래스에 일반 객체를 할당합니다.
// ES5
function D() {
Array.apply(this, arguments);
}
D.prototype = Object.create(Array.prototype);
var d = new D();
d[0] = 42;
d.length; // 0 - bad, no array exotic behavior
ES6 클래스는 언제 그리고 누구에게 객체가 할당되는지를 변경함으로써 이 문제를 해결했습니다.
ES5에서 서브클래스 생성자를 호출하기 전에 오브젝트가 할당 되었으며 서브클래스는 해당 오브젝트를 슈퍼클래스 생성자로 전달합니다.
이제 ES6 클래스를 사용하면 슈퍼클래스 생성자를 호출하기 전에 객체가 할당되고 슈퍼클래스는 해당 객체를 서브클래스 생성자가 사용할 수 있게합니다.
이를 통해 서브클래스에서 new를 호출 할 때도 Array가 이국적인 객체를 할당 할 수 있습니다.
// ES6
class D extends Array {}
let d = new D();
d[0] = 42;
d.length; // 1 - good, array exotic behavior
여러가지 종류
다른 것에는 약간의 차이가 있지만 덜 중요한 차이가 있습니다.
클래스 생성자는 함수 호출 할 수 없습니다. 이것은 new로 생성자를 호출하는 것을 잊어 버리지 않도록 보호합니다.
또한 클래스 생성자의 프로토 타입 속성을 다시 할당 할 수 없습니다. 이것은 JavaScript엔진이 클래스 객체를 최적화하는데 도움이 될 수 있습니다.
마지막으로 클래스 메서드에는 프로토타입 속성이 없습니다. 불필요한 오브젝트를 제거하여 메모리를 절약 할 수 있습니다.
새로운 특징을 상상력에 활용하는 방법
여기와 다른사이트 포인트 기사에서 설명된 많은 기능들은 JavaScript에 처음 등장하며,
커뮤니티는 이러한 기능을 새롭고 상상력 있는 방법으로 사용하기 위해 현재 실험하고 있다.
프록시를 통한 다중 상속
이러한 실험 중 하나는 다중 상속을 구현하기 위한 JavaScript의 새로운 기능인 프록시를 사용합니다.
JavaScript의 프로토타입 체인은 단일상속만 허용합니다. 객체는 하나의 다른 객체에만 위임 할 수 있습니다.
프록시를 사용하면 접근 속성을 여러 다른 객체에 위임 할 수 있습니다.
const transmitter = {
transmit() {}
};
const receiver = {
receive() {}
};
// Create a proxy object that intercepts property accesses and forwards to each parent,
// returning the first defined value it finds
const inheritsFromMultiple = new Proxy([transmitter, receiver], {
get: function(proxyTarget, propertyKey) {
const foundParent = proxyTarget.find(parent => parent[propertyKey] !== undefined);
return foundParent && foundParent[propertyKey];
}
});
inheritsFromMultiple.transmit(); // works
inheritsFromMultiple.receive(); // works
ES6 클래스와 함께 작동하도록 이것을 확장 할 수 있습니까?
클래스의 프로토타입은 접근 속성을 다른 여러 프로토타입에 전달하는 프록시 일 수 있습니다. 현재 JavaScript 커뮤니티가 이 작업을 하고 있습니다.
알아낼 수 있습니까? 토론에 참여하고 아이디어를 공유하십시오.
클래스 팩토리를 사용한 다중 상속
JavaScript 커뮤니티가 실험해 온 또 다른 접근 방식은 가변 슈퍼클래스를 확장하는 주문형 클래스를 생성하는 것입니다.
각 클래스에는 여전히 하나의 부모가 있지만 흥미로운 방식으로 부모를 연결할 수 있습니다.
function makeTransmitterClass(Superclass = Object) {
return class Transmitter extends Superclass {
transmit() {}
};
}
function makeReceiverClass(Superclass = Object) {
return class Receiver extends Superclass
receive() {}
};
}
class InheritsFromMultiple extends makeTransmitterClass(makeReceiverClass()) {}
const inheritsFromMultiple = new InheritsFromMultiple();
inheritsFromMultiple.transmit(); // works
inheritsFromMultiple.receive(); // works
이러한 기능을 사용하는 다른 상상력있는 방법이 있습니까? 이제 JavaScript 세계에 발자국을 남겨야 할 때입니다.
결론
아래 그림에서 알 수 있듯이 클래스 지원은 꽤 좋습니다. es6 클래스 지원 브라우저
이 기사가 ES6에서 클래스가 어떻게 작동하는지에 대한 통찰력을 제공하고 클래스를 둘러싼 전문 용어를 이해하기를 바랍니다.
이 기사는 Nilson Jacques와 Tim Severien에 의해 피어 리뷰되었습니다.
SitePoint 컨텐츠를 최상의 상태로 만드는 SitePoint의 모든 동료 검토 자에게 감사합니다!