사전적 의미는 계급, 집단, 집합. 공통적인 속성을 모아 한 데 묶은 덩어리 또는 명세.
클래스와 함께 사용되는 영어로 '인스턴스'가 있음. 해당 클래스의 속성을 지닌 구체적인 객체.
배, 사과, 바나나, 감, 오렌지 등이 있음. 이들은 모두 과일이라는 분류, 집합, 집단에 속함. 여기서 과일은 클래스이고, 배, 사과, 바나나 등은 과일에 속하는 인스턴스들임. 과일은 음식이라는 범주에 속하므로, 과일 클래스는 음식 클래스의 하위 클래스가 됨. 배, 사과 등은 과일이면서 음식이기도 함. 음식과 과일은 추상적인 개념이지만, 배, 시과 등은 구체적인 물체임. 즉, 공통된 속성을 지니는 구체적인 대상들을 '인스턴스'라고 하고, 인스턴스들의 공통속성을 모은 추상적인 개념이 '클래스'임.
프로그래밍 언어에서는 우리가 실생활에서 상식으로 알고있는 합의된 규칙이 아무것도 없기 때문에 상위 클래스부터 먼저 정의가 되어야만 하위의 클래스 및 인스턴스를 생성할 수 있음. 음식 클래스가 먼저 정의 되어야만 음식 클래스의 속성을 지니면서 그 중에서 좀 더 구체적인 특성을 지니는 하위 클래스인 과일 클래스를 정의할 수 있고, 그제서야 배, 사과 등의 구체적인 객체들이 음식이기도 하면서 과일이기도 한 인스턴스가 될 수 있음.
음식 클래스와 과일 클래스의 관계를 따져보면, 음식 클래스는 과일 클래스의 상위 클래스(superclass)임. 과일 클래스는 음식 클래스의 하위 클래스(subclass)임. 상위/하위 클래스는 클래스 상속 파트에서 다룸.
배열 리터럴을 생성하면 Array 생성자 함수를 new 연산자와 함께 호출한 결과와 같음.
Array 생성자 함수 부분(노란색)만 보면 클래스의 개념임. Array 생성자 함수는 그 자체로 어떤 특별한 역할을 수행하지 않고, new 연산자를 통해 생성한 배열 객체들의 기능을 정의하는데 주력하고 있음. 배열에서 쓰이는 메서드들은 모두 Array.prototype에 정의되어 있음.
클래스를 통해 생성한 객체(배열객체)를 인스턴스라고 함. 구체적인 데이터를 지니고 실제 코드상에서 동작을 수행하는 실체.
클래스만 떼어놓고 보면, 프로토타입 프로퍼티 내부에 할당되지 않고 Array 생성자함수 객체에 직접 할당되어 있는 프로퍼티들을 스태틱 메서드, 스태틱 프로퍼티라고 함. 이들은 Array 생성자 함수를 new 연산자 없이 함수(객체)로써 호출할 때만 의미있는 값들. 보통 해당 클래스 소속의 인스턴스들의 개별적인 동작이 아닌 소속 여부 확인, 소속 부여 등의 공동체적 판단을 필요로 하는 경우에 사용됨.
프로토타입 내부에 정의된 메서드들을 프로토타입 메서드라고 함. 앞의 프로토타입을 생략하고 메서드라고 하는 경우가 많음.
클래스와 인스턴스의 관계를 보면, 스태틱한 값은 왼쪽, 프로토타입 메서드는 오른쪽에 두었을 때, 이 둘은 인스턴스에서 직접 접근 가능한지 여부가 서로 다름. 프로토타입 프로퍼티는 인스턴스와 [[Prototype]]로 연결되어있고 [[Prototype]]가 인스턴스와 겹쳐서 동작하기 때문에 인스턴스에서 바로 프로토타입 메서드에 접근이 가능함. 생성자 함수 내부에 있는 프로퍼티나 메소드는 다이렉트로 접근할 방법이 없음. 프로토타입의 constructor를 통해 우회가능하나, 인스턴스를 this로 하기 위해서는 별도의 처리가 필요하고, 그런 처리에 의해서도 정상적인 동작을 기대하기는 어려움.
위의 프로토타입 모형에서, 좌상단에 생성자함수 클래스가 있고 그 클래스의 스태틱 멤버는 좌측에 위치함. 프로토타입 메서드들은 우측에 위치. 노란선을 따라 new 연산자로 생성한 인스턴스는 생성자함수의 프로토타입을 상속받아서 자신의 것 처럼 쓸 수 있음.
Person 생성자 함수와, Person.getInformations, Preson.prototype.getName, Preson.prototype.getAge 3개의 메서드를 정의함. 이 중 Person.getInformations는 스태틱 메서드, Preson.prototype.getName, Preson.prototype.getAge는 프로토타입 메서드임.
roy라는 Person의 인스턴스를 생성. roy.getName()
과 roy.getAge()
는 this를 잘 인식해서 결과를 출력함. roy.getInformations(roy)
는 메서드가 존재하지 않는다는 에러가 발생. 프로토타입 체이닝이 대각선으로만 검색하기 때문에 스태틱 메서드를 사용하려면 인스턴스가 아닌 생성자 함수에서 직접 접근해야 함.
클래스는 공통된 속성이나 기능을 정의한 추상적인 개념. 인스턴스는 클래스에 속하는 객체. 클래스에는 스태틱 멤버와 프로토타입 메서드가 있음. 스태틱 맴버는 인스턴스에서 직접 접근할 수 없고, 클래스 자체에서만 접근가능. 프로토타입 메서드는 인스턴스에서 직접활용 가능.
Person
이라는 클래스는 name
, age
라는 프로퍼티가 있고, getName
, getAge
라는 메서드가 있음. Employee
라는 클래스는 name
, age
, position
이라는 프로퍼티가 있고, getName
, getAge
, getPosition
이라는 메서드가 있음.
각각을 코드로 표현하면 위와 같음. 각 생성자 함수가 있고, 각 프로토타입 메서드들이 있음. 여기서 getName
과 getAge
는 중복됨.
앞서봤던 음식클래스와 과일클래스를 떠올려본다면 Person클래스 하위에 Employee클래스가 있는 구조로 만들면 적절해보임.
겹치는 메서드는 상위인 Person에 두고 Employee에는 겹치지 않는 메서드만 남김. Employee의 인스턴스들은 프로토타입 체인을 타고 Employee의 getPosition메서드도 사용이 가능하고, 한번더 프로토타입 체인을 타서 getName, getAge메서드도 사용할 수 있음.
이런 다중상속구조를 만들려면 두 줄의 코드로 가능함.
Person의 아래 꼭지점과 Employee의 우측 꼭지점을 이어주면됨. 코드로 표현하면 Employee.prototype = new Person()
. Employee의 프로토타입에 Person의 인스턴스를 할당. 하지만 이렇게 하면 기존의 Employee.prototype 객체를 완전히 새로운 객체로 대체하게 됨. 여타의 프로토타입과 동일하게 동작하게 하기 위해 본래의 기능을 부여해줘야 함. 프로토타입 객체에는 JS가 기본적으로 constructor 프로퍼티를 생성해줌. constructor에는 생성자 함수가 담겨있음. 이 기능을 Employee.prototype.constructor = Employee
를 통해 만들어줌.
이를 통해 서로 다른 두 개의 클래스가 서브-슈퍼 클래스 관계를 갖게 할 수 있음.
코드로 구현하면 위와 같음. Person에 관련된 내용은 그대로 두고, Emoloyee의 내용은 생성자 함수만 남김. 위에서 봤던 두 줄의 코드로 Employee.prototype과 Person의 인스턴스를 연결하고, Employee.prototype.constructor에 다시 Employee를 부여해 원래있던 프로토타입 객체와 같은 기능을 수행하도록 함.
Employee.prototype.getPosition
은 마지막에 추가함. 우측 2번째 줄과 같은 이유로 먼저 추가해봤자 Employee의 프로토타입을 Person의 인스턴스로 바꾸는 과정(우측 1번째 줄)에서 덮어씌워지면 소용이 없기 때문.
Employee의 인스턴스(roy)를 확대해서 본 모습.
첫번째 구간은 roy라는 객체 자신이 갖는 프로퍼티인데, Employee에 new 연산자를 통해 생성한 Employee의 인스턴스임.
두번째 구간은 roy의 [[Prototype]]영역인데, 이는 Employee.prototye와 같음. 상속관계를 부여하는 핵심 포인트였던 Employee.prototype = new Person()
으로 Person의 인스턴스와 동일함.(age와 name프로퍼티가 있음) constructor은 지정해준 대로 Employee가 확인됨. Employee가 사용할 메서드인 getPosition도 보임.
세번째 구간은 roy.__porto__.__proto__
로 프로토타입 체인을 두 번 건너뛴 내용. 이는 Person.prototype과 같고(따라서 constructor는 Person임), Obect의 인스턴스임. 마지막 줄의 [[Prototype]]에는 Object가 표시되어 있는데, 사실은 Object.prototype를 가리킴.
상속관계는 잘 연결이 됐지만, 하이라이트 친 부분은 추상적이어야 할 클래스의 프로토타입이 담겨있다는 점에서 적절하지 않음. 만약 roy.name을 제거하고 getName메서드를 호출하면 기대했던 undefined가 반환되지 않고, 프로토타입 체인을 타고 '이름없음'이 반환됨. 프로토타입 체이닝 상에는 프로퍼티가 아닌 메서드들만 존재하게 하는 것이 '추상적인 클래스'라고 하는 정의에 부합함.
즉 위 그림의 붉은색 부분을 제거해야 함. 각 클래스의 프로토타입을 [[Prototype]]로 내려받는 것이 필요한 것이고, 반드시 Person의 인스턴스가 필요한 것이 아님. 즉, Person.prototype을 상속받는 별도의 인스턴스가 있고 그 객체에는 아무런 프로퍼티도 존재하지 않도록 하면 됨.
비어있는 객체를 생성하는 Bridge라는 생성자 함수를 만듬. Bridge의 프로토타입에 Person의 prototype을 연결한 상태에서 인스턴스를 생성하면, 그 인스턴스에는 이무런 프로퍼티가 없이 메서드만 상속받는 형태가 됨. 그 상태에서 Employee의 prototype과 Bridge의 인스턴스를 연결하면 됨.
Person.prototype에 Bridge.prototype을 연결하고(우측파란화살표), Bridge의 인스턴스가 Employee의 프로토타입이 되게함(좌측파란화살표).
화살표를 당겨서 연결하면 Bridge와 Person이 겹쳐지고, Employee의 prototye과 연결됨.
화살표를 코드로 나타낸 모습. 우측 첫번째 화살표는 Bridge.prototype과 Person.prototype을 연결한 선. 우측 두번째 화살표는 Employee.prototype에 Bridge의 인스턴스를 연결한 선.(Person의 인스턴스를 넣는 대신에 빈 객체를 넣어줌) 우측 세번째 화살표는 본래의 프로토타입 기능을 되살리기 위해 constructor를 연결해준 것.
코드로 구현한 모습. 이전과 동일한 코드에서 우측 부분만 달라짐. Bridge는 아무런 프로퍼티도 생성하지 않는 비어있는 생성자 함수임(우측 Line 1). Bridge의 prototype에 Person의 prototype을 덮어씌움(우측 Line 2). Employee의 prototype을 Bridge의 인스턴스와 연결(우측 Line 3). Employee의 prototype의 constructor가 다시 Employee를 가리키게끔 복원(우측 Line 4). 이 상태에서 하위 클래스의 고유 메서드를 지정(Line 5~7).
인스턴스를 생성하면 원하는 결과(roy)를 얻을 수 있음.
Bridge라는 매개체를 이용해서 Person의 인스턴스와의 연결관계를 끊음.
이로써 프로토타입 체인 상에 불필요한 프로퍼티가 동작하지 않게 할 수 있음.
이 기능은 ES5에서 클래스 상속을 구현하는데 자주 등장하는 패턴임. Bridge라는 함수는 매개체 역할만 할 뿐 실제 코드상에 영향을 주지는 않음. 더글라스 크락포드는 함수화를 시켜서 활용할 것을 추천함. 클로저를 이용해서 Bridge 생성자 함수는 한 번만 생성해서 재활용을 하고, super와 sub클래스로 쓰일 생성자 함수를 매개변수로 넘겨주면 자동으로 둘 사이의 상속구조를 연결해주는 함수임.
이를 활용하면 extendClass(Person, Employee);
의 간단한 형태로 상속을 구현할 수 있음. extendClass함수는 전체 JS코드 상에 한번만 구현하면 됨.
extednClass에 조금 더 욕심을 내어 메서드 상속 뿐만아니라, 인스턴스의 value들 역시 상속 구조를 활용하면 좀 더 간단한 구현이 가능. Person의 인스턴스와 Employee의 인스턴스 모두 name, age라는 똑같은 프로퍼티를 가짐. 하위클래스에서 this.superClass(name, age);
의 호출만으로 name, age 프로퍼티가 구현될 수 있다면 편리할 것임.
이는 우측코드에서 Child.prototype.superClass = Parent;
를 추가함으로써 구현가능함. 코드의 해석은 아래부터.
생성자 함수 내부의 this는 인스턴스를 가리킴. 위 코드에서는 Employee의 인스턴스 roy임. 그 인스턴스에는 superClass라는 메서드가 없어 프로토타입 체이닝을 타고 Employee.prototype 내부에서 superClass메서드를 검색할 것이고, 있으므로 해당 메서드를 실행함.
superClass메서드에는 Parent, 즉 Person이 연결되어있어 Person생성자함수가 호출됨. 이때 this.superClass로 호출한 것이라 메서드로서 호출됨. 메서드 내에서의 this는 메서드명 앞부분 까지임. 즉, Employee의 인스턴스 roy가 this가 됨. 따라서 this.superClass를 호출하면 Employee의 인스턴스의 name 프로퍼티, age 프로퍼티에 각각 값을 할당하라는 명령이 됨.
최종본. 클래스 상속을 구현하기 위한 extendClass 함수는 위쪽에 정의되어 있고, 실제 superclass와 subclass의 구체적인 내용은 아래에 정의함.
ES6는 굳이 extendClass라는 함수를 직접 만들지 않아도 JS내장 명령으로 손쉽게 클래스 상속이 구현가능함. class Person을 만들고, Employee는 Person을 extends 해주기만 하면됨. 아래에서도 super()이라는 명령어가 쓰이는 것이 보임.
그럼에도 extendClass 구현과정에는 배울점이 많음. 참조형 데이터의 정보 저장 방식, 참조형 변수의 데이터를 수정하면 어떤 결과가 발생하는지, 스코프와 실행컨텍스트, 클로저의 원리, this 바인딩, 프로토타입과 프로토타입 체이닝 등등.