JS에서는 class 키워드가 존재하지만, 그것은 C#
이나 Java
등의 객체지향프로그래밍을 기반으로 하는 언어의 class패턴
을 어떻게든 흉내낸것에 불과하며 실제 class패턴
의 개념과는 궤를 달리한다.(JS의 객체는 프로토타입 기반의 객체라고 한다.)
그래서 Interface
도 없고 C++
같은 다중상속 방식을 사용할 수도 없으며 (단, mixin
이나 Proxy Object
등을 사용하여 비슷하게 구현할 수 있다고 한다.관련 링크)
virtual(부모에서 선언되어있어 그대로 쓸수도, 오버라이드 할 수도 있음)
도,
abstract(부모는 있다고만 하고 구현은 자식들이)
도
private(외부에서 찾을 수 없음)
와
public(누구나 접근 가능)
과
protected(관련자(자식)만 접근 가능)
등의 선언도 무언가 모호하거나 아예 없다. (TypeScript에는 비슷하게 구현할 수 있다고 한다.)
그러면 JS의 class키워드는 실제로 무슨 작업을 하는지에 대하여 유추해본다.
다음은 class를 사용하는 예시이다.
class OBJ{
constructor(){
this.i = 10;
this.arrow = () => console.log(this.i, this);
this.expression = function() {
console.log( this.i, this);
};
}
};
let obj = new OBJ();
obj.arrow();
obj.expression();
다음 코드는 NodeJS
의 Babel
트랜스파일러
를 통하여 모던하지 않은 JS만 읽을 수 있는 환경에서도 코드가 구동될 수 있게 바꾸어준 코드중의 일부이다. (실제로는 더 많은 작업을 해놓는다.)
이것이 완전히 JS
가 class
키워드가 하는 일을 대변할 수는 없겠지만, 최소한 Babel
이 class
를 어떻게 생각하는지, 그리고 그것을 받은 파이어폭스 브라우저
가 어떻게 생각하는지는 알 수 있을 것이다.
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var OBJ = function OBJ() {
var _this = this;
_classCallCheck(this, OBJ);
this.i = 10;
this.arrow = function () {
return console.log(_this.i, _this);
};
this.expression = function () {
console.log(this.i, this);
};
};
;
var obj = new OBJ();
obj.arrow();
obj.expression();
기본적으로, class라는 키워드 자체가 없다.
class로 선언했던 OBJ는 어떤 함수
이다.
var OBJ = function OBJ() {
...
}
이 함수는 new
연산자와 함께 호출된다. new
연산자도 다른 언어들이 수행하는 new
와 하는 행동(추상화되어있는 class를 인스턴스화 하여 메모리에 할당하는 과정)이 완전히 다르다.
var obj = new OBJ();
JS에서 함수에서만 사용할 수 있는 new
키워드를 사용한 new OBJ()가 하는 행동은 다음과 같다.
new 연산자
OBJ.prototype
을 상속하는 새로운 객체가 하나 생성된다. window/global
이다.bind
된 this
와 함께 생성자 함수(constructor
) OBJ
가 호출된다.this
키워드를 bind
한다.override
)하기 원한다면, 그렇게 하도록 선택할 수 있다.함수 안의 내용은 다음과 같다.
var OBJ = function OBJ() {
var _this = this;
_classCallCheck(this, OBJ);
this.i = 10;
this.arrow = function () {
return console.log(_this.i, _this);
};
this.expression = function () {
console.log(this.i, this);
};
};
_this라는 변수를 통해 this에 접근할 수 있게 된다.
_classCallCheck를 수행한다. 이 함수는 다음과 같다.
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
인자로 받은 instance
와 Constructor
의 instanceof
연산이 false
인 경우 타입에러를 발생시키는 함수이다.
즉, Constructor
가 instance
의 프로토타입 체인
중에 찾을 수 없는 것
이라면 오류를 발생시킨다.
여기서는 OBJ
가 this
의 프로토타입 체인
에 속한다
면, true
이다.
즉,
class
키워드가 기본적으로 수행하는 것은 해당class
키워드를함수
로 취급하고,new
로 해당 함수를 호출하되, 호출한 함수의prototype
과 같은prototype
을 갖는 새로운 객체를 만들고, 그것을this
로bind
하면서 호출한다. 따라서 함수 안의 모든this
는 다른 어떤것을 return하지 않는 이상 그 새로운 객체가 되며, 그것을 return하는 것이다.
번거로우면서도 이게 전부가 아니라는 것으로 완벽한 분석은 힘들지만, 최소한 기존 객체지향프로그래밍의 class패턴과는 완전히 다르다는것정도는 알 수 있다.
기타사항으로 특이한 점을 하나 발견할 수 있는데,
this.arrow = function () {
return console.log(_this.i, _this);
};
this.expression = function () {
console.log(this.i, this);
};
arrow
는 화살표함수
였고, expression
은 익명함수
였다. 내용은 비슷했다.
하지만 arrow
에서 표시하던 this
는 이전에 만든 자신을 지칭하는 _this
가 되어있고, expression
에서 표시하던 this
는 말 그대로 this
가 되어있다.
이 경우 실제로 출력하는 차이가 없어보인다. 사실 이전의 _this
변수가 이 화살표 함수때문에 생긴 것이다. 관련 정보 링크
객체는 extends
키워드를 통하여 부모 class를 상속받을 수 있는 것처럼 보이게 할 수 있다.
class Parent{
constructor(){
}
doParentAction(){
console.log("DO PARENT");
}
doOverrideAction(){
console.log("THIS IS PARENT FUNCTION");
}
}
class Child extends Parent{
constructor(){
super();
}
doChildAction(){
console.log("DO CHILD");
}
doOverrideAction(){
console.log("THIS IS CHILD FUNCTION");
}
}
let child1 = new Child();
child1.doChildAction(); // DO CHILD
child1.doOverrideAction(); // THIS IS CHILD FUNCTION
이 함수를 NodeJS의 development환경에서 Babel 트랜스파일러를 통하여 변환하고, 파이어폭스 브라우저를 통하여 감지된 소스의 일부이다. 다음은 실제로 동일하게 동작시킬 수 있는 최소한의 코드이며 실제로는 이것보다 많은 작업을 수행한다.
var _createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor)
descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
} return function (Constructor, protoProps, staticProps) {
if (protoProps)
defineProperties(Constructor.prototype, protoProps);
if (staticProps)
defineProperties(Constructor, staticProps); return Constructor; }; }();
function _possibleConstructorReturn(self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype,
{
constructor:
{
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
}
);
if (superClass)
Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Parent = function () {
function Parent() {
_classCallCheck(this, Parent);
}
_createClass(Parent, [{
key: "doParentAction",
value: function doParentAction() {
console.log("DO PARENT");
}
}, {
key: "doOverrideAction",
value: function doOverrideAction() {
console.log("THIS IS PARENT FUNCTION");
}
}]);
return Parent;
}();
var Child = function (_Parent) {
_inherits(Child, _Parent);
function Child() {
_classCallCheck(this, Child);
return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this));
}
_createClass(Child, [{
key: "doChildAction",
value: function doChildAction() {
console.log("DO CHILD");
}
}, {
key: "doOverrideAction",
value: function doOverrideAction() {
console.log("THIS IS CHILD FUNCTION");
}
}]);
return Child;
}(Parent);
var child1 = new Child();
child1.doChildAction(); // DO CHILD
child1.doOverrideAction(); // THIS IS CHILD FUNCTION