본문 바로가기
Javascript

자바스크립트(javascript) 객체 멤버

by 램쥐뱅 2016. 12. 2.

1. 객체 멤버 관리


자바스크립트에서 객체의 멤버를 내부적으로 어떻게 관리하고, 또 어떻게 객체의 구조를 편집할수 있을까.




멤버 구분


구분

 구현

 설명

 비공개
 멤버

 비공개
 속성

 var 변수

 객체 내부에서 선언된 변수.
 비공개 함수에서만 접근 가능

 비공개
 메서드

 function f(){...}
 var f = function{...}

 객체의 생성자에 인라인 방식으로 정의된, 이름이 있는 함수
 또는 익명 함수.
 생성자 내부 코드에서 호출 가능

 인스턴스
 멤버

 인스턴스
 속성

 this.property = 값

 외부에서 인스턴스를 통해 접근 가능
 obj.property

 인스턴스
 메서드

 this.method = function(){...}

 외부에서 인스턴스를 통해 접근 가능
 obj.method();

 프로토타입
 멤버

 프로토타입
 속성

 생성자.prototype.property = 값

 외부에서 생성자 또는 인스턴스를 통해 접근 가능
 생성자.prototype.property (읽기 쓰기 가능)

 obj.property (읽기 전용)

 프로토타입
 메서드

 생성자.prototype.method =
 function(){...}

 외부에서 생성자 또는 인스턴스를 통해 접근 가능
 생성자.prototype.method();
 obj.method();

 생성자
 멤버

 생성자
 속성

 생성자.property = 값

 외부에서 생성자를 통해 접근 가능
 생성자.property

 생성자
 메서드

 생성자.method = function(){...}

 외부에서 생성자를 통해 접근 가능
 생성자.method();




멤버 관리 구조


자바스크립트의 멤버를 관리하는 구조를 이해하면 난해하던 자바그크립트의 많은 구문을 이해할수 있을 것이다.

아래와 같은 두표현을 예로들면


obj.member = 10;

obj["member"] = 10;


첫번째 표현은 익숙할 것이다. 두번째 표현처럼 []와 멤버명 문자열로 접근하는 방법은 약간 어색할지도 모른다.

그러나 두 문장 모두 객체의 멤버에 접근 하고자 할때 사용할 수 있다.




연상 배열 구조


객체별로 자신에게 속한 멤버를 관리하는 내부적인 데이터 구조가 있다.

갹체의 멤버(속성, 메서드)는 (키, 값)의 쌍으로 구성된 집합이다. 그리고 각 멤버는 정렬되어 있지 않다.

이러한 데이터를 관리하는데 가장 적합한 데이터 구조로 해시가 있고, 자바스크립트도 해시 구조의 일종인 연상 배열(associative array)이라는 데이터 구조를 사용한다.



각 요소는 (키, 값)의 쌍으로 구성되고, 여기서 "값"은 문자열, 숫자, 불린값, 또는 배열, 객체도 될 수 있고 함수 객체가 될 수도 있다.

만약 "값"이 함수 객체로 할당되면 해당 멤버는 메서드가 되는 것이고, 그 밖의 값으로 할당되면 속성이 된다.




연상 배열 요소의 접근


요서를 검색할 때는 배열에서처럼 순차적으로 요소를 찾는 것이 아니라 해시 특장처럼 내부 알고리즘에 의해 키를 기반으로 직접 해당 요소에 빠르게 접근할수 있다.


자바스크립트 객체는 멤버를 관리하기 위해 복주머니처럼 요소의 순서가 없는 연상 배열구조를 이용한다.


연상 배열의 각 요소에 접근하고자 할 때는 obj["key"]처럼 []연산자를 이용한다.

일반 배열이 [1]처럼 인덱스를 통해 배열 요소에 접근할 수 있는 반면 연상 배열은 키를 사용해 접근 한다. 일반 해시와 다른점은 키값으로 문자열만 사용할 수 있다는 것이다. 때문에 키로 사용된 문자열은 고유해야 한다. 키의 값으로는 어떤 타입의 데이터도 저장 가능하다.


연상 배열 요소의 키는 고유해야 하고 요소에 접근할 때는 ["키"]를 사용한다.



키의 이름은 멤버의 이름이 된다. obj의 속성으로 number이라는 속성을 정의한다면 해당 객체의 연상 배열 구조에는 "number"라는 키를 가진 요소가 생성되는 것이다.


이제 객체 멤버에 접근할 때 "." 연산자 외에 "[]"도 사용가능 하고 결론은 두 연산자가 동일하다는 것을 알 수 있다.


객체의 멤버에 접근할 때 도트(.) 또는 brackets([])을 사용한다.


앞에서 설명한 멤버 관리에 사용하는 데이터 구조(연상 배열)의 특징 때문에 멤버의 이름은 속성, 메서드에 상관없이 고유해야 한다.

따라서 자바스크립트에서는 일반 객체지향 언어에서 흔히 지원하는 오버로드(overloading) 개념은 지원되지 않는다.

즉 매개변수만 다르다면 동일한 이름의 메서드를 여러 개 정의할 수 있는 일반 객체지향 언어와는 다른다.




객체 멤버 관리 구조


name 이라는 속성와 setNewName 이라는 메서드가 정의되어 있는 mySon이라는 객체를 그림으로 그려보면.



자바스크립트 엔진이 아래와 같은 표현을 만나면 


mySon.setNewName("김삿갓");


mySon에 저장된 값이 참조 타입의 값이라는 것을 알게 되고 그 참조 값이 가리키는 연상 배열 구조의 객체로 이동해 setNewName 멤버를 찾는다. 이 벰버가 다시 참조 타입의 변수라는 사실을 알고 해당 변수 값이 가리키는 곳으로 간다. 그다음 그곳에서 찾은 멤버에 "()" 연산자와 전달된 인자로 코드 블록을 호출하고, 함수의 코드 블록이 파싱과 실행 단계를 거치게 된다.




리플렉션


변수의 데이터를 검색해서 최종 결과값을 걷는 과정은 모두 런타임에 일어난다.

이렇다 보니 mySon을 생성한 타입에 대한 정보가 사전에 필요하지 않다. 예를 들어, Person이라는 타입이 정의되어 있고 Person이 어떤 멤버를 가지고 있고 그 멤버는 어떤 구조로 되어 있는지 등에 대한 정보가 자바스크립트 엔진에게는 필요하지 않다.

mySon이 어떤 타입으로 생성됬든 현재 코드를 실행하는 데는 아무런 문제가 없다.


그러나 프로그래밍을 하다 보면 런타임이라도 변수가 가지고 있는 값이 어떤 타입인지에 대한 정보가 필요할때가 있다.

이처럼 현재 객체를 기반으로 타입을 추적하는 과정을 리플렉션(reflection)이라 한다.


자바스크립트에서도 최소한으로 객체의 타입에 대한 정보를 런타임에 제공할 수 있다.




멤버 === var 변수


멤버가 연상 배열이라는 구조로 관리되기 때문에 객체지향적인 표현상으로는 "속성"과 "메서드"를 구분해서 말하지만 자바스크립트 입장에서는 속성과 메서드를 구분하지 않는다는 것을 이제는 이해할 수 있다. 자바스크립트 입장에서는 멤버는 모두 공개된 var 변수와 다를 바 없다.


즉, 코드가 실행될 때 객체의 멤버를 연산자"()"를 이용해 호출하면 그 멤버는 "메서드"가 되는 것이고, 그렇지 않고 바로 멤버명으로만 접근하면 속성이 되는 것이다.


이러한 언어적 특성 때문에 동일한 멤버에 함수 객체를 할당하든 객체를 할당하든 값을 할당하든 언어상으로는 아무 문제가없다.

물론 현실적으로 프로그램을 개발할 때 그렇게 해서는 안된다. 다른사람은 읽지 못하는 코드가 될 가능성이 많아질 것이기 때문이다.


자바스크립트 객체의 멤버는 속성이든 메서드든 모두 var 변수다.


이런 특성 때문에 자바스크립트에서는 멤버 이름만 보고도 이것이 속성인지 메서드인지 더 나아가서 무슨 일을 하는 멤버인지 유추할 수 있게 적절한 이름을 부여하는것이 다른 언어에서 보다 더욱더 중요하다.




생성자 멤버와 객체 멤버


앞에서 알았듯이 생성자도 객체라서 일반 객체처럼 속성과 메서드를 추가할 수 있다.

생성자 멤버는 해당 생성자가 생성한 객체의 멤버와는 별도의 연상 배열 구조에서 관리된다. 객체 멤버는 다시 인스턴스 멤버와 프로토타입 멤버로 구분된다. 이 멤버가 정의되는 영역은 다르다.



생성자 멤버가 정의되는 곳과 프로토타입 멤버가 정의되는 곳은 각각 함수별로(생성자별로) 하나만 존재한다.

그리고 인스턴스 멤버가 정의 되는 곳은 인스턴스별로 존재한다. 


생성자 멤버, 프로토타입 멤버, 인스턴스 멤버는 각 영역에서 연상 배열 구조를 이용해 관리된다.




멤버 접근


객체의 멤버에 접근하는 방법으로 흔히 "." 연산자를 주로 사용 했지만, 자바스크립트 객체의 멤버가 연상 배열 구조로 관리되는 사실을 안다면 "[]"표현을 이용하는 것이 자연스럽다.


var a = obj["propertyA"];

var b = obj.propertyA;


멤버 관리 구조만 생각하면"[]"을 이용해 멤버에 접근하는 것은 당연한 것이고 멤버 접근 연산자로 흔히 알고 있는 "."은 오히려 객체지향적인 표현을 위해추가된 부가적인 기능으로 볼 수 있다.


생성자 멤버는 해당 생성자가 생성한 객체의 멤버와는 별도의 연상 배열 구조에서 관리된다.

따라서 다음과 같은 코드를 실행하면 에러가 발생한다.


//Person 생성자에 Description 속성 추가

Person.Description = "I am a person function";

Person.Description; // 출력 OK !


//Person 으로 생성한 인스턴스 obj에도 Description 속성이 추가됨?

var obj = new Person();

console.log(obj.Description); //undefined. 다른 공간에 정의되어 있음


Person 생성자 멤버에 접근하는 방법도 아래와 같이 정리할 수 있다.


Person.Description

Person["Description"]




멤버 추가, 제거, 대체


연상 배열의 또 다른 특징 중 하나는 동적으로 요소를 추가하거나 삭제할 수 있다는 것이다.

이런 특징을 근거로 런타임시에도 객체의 멤버를 추가하거나 삭제할 수 있다.


var obj;

obj.propertyA = new Date();    //propertyA 속성 추가

delete obj.propertyBl          //propertyB 속성 추가


위 코드처럼 마치 그 속성이 존재하는 것 처럼 값을 설정하기 만하면 속성이 추가된다.

속성이 obj에 없다면 그 속성은 연상 배열 객체에 추가되고 이미 존재한다면 새로운 값이 할당된다.


기존에 존재하는 속성값을 보존해야 하는 경우라면 존재 여부를 판단하는 절차를 아래와 같이 추가할 수 있다.


if(!obj.propertyA)

obj.propertyA = new Date();


속성을 삭제할 때는 delete 키워드를 사용한다. 이 시점 이후부터는 해당 객체에서 지운 속성을 사용할 수 없다.


속성 뿐 아니라 메서드 멤버도 마찬가지로 추가 제거가 가능한데, 메서드의 경우는  기존에 정의된 메서드의 행동을 다른 로직으로 대체할 수도 있다.


function Hello(){

this.hello = function(){ return "Hey" ;};

}


var obj = new Hello();

obj.hello()    // "Hey"

obj.hello = function() { return "Hello"; };

obj.hello()    // "Hello"


자바스크립트가 제공하는 생성자 Object, Array, String 등에도 멤버를 추가, 제거, 대체할 수 있다.




멤버 순회, 존재 확인


객체가 어떤 멤버를 가지고 있는지 확인하거나 현재 정의된 멤버에 차례로 접근 하는 방법을 제공한다.


Object 객체에 특정 멤버가 포함되어 있는지 알 수 있는 방법은 아래와 같다.


var bool = obj.hasOwnProperty("멤버명");


hasOwnProperty 메서드는 Object의 프로토타입 멤버로 정의된 메서드로서 모든 객체에서 상속받는다.


var obj = {

prop : "exist",

change : function() {

this.newProp = this.prop;

delete this.prop;

}

}


obj.hasOwnProperty("prop");        //true

obj.hasOwnProperty("change");     //true

obj.hasOwnProperty("newProp");    //false

obj.change();

obj.hasOwnProperty("prop");        //false

obj.hasOwnProperty("change");     //true

obj.hasOwnProperty("newProp");    //true


hasOwnProperty 메서드는 Object가 가지고 있는 원래의 멤버에 대해서는 작동하지 않는다.

Object에는 toString 메서드가 정의되어 있는데, hasOwnProperty("toString")은 false를 반환 한다.

즉 인자로 넘어온 이름의 멤버가 사용자가 추가한 멤버 중에 있는지만을 판단한다.


hasOwnProperty 메서드는 현재 객체에 추가된 사용자 정의 멤버를 대상으로 한다.




in 연산자


Object의 메서드인 hasOwnProperty와 유사한 목적으로 사용할 수 있는 연산자로 in 이라는 것이 있다.

연산자의 좌측에 놓인 속성이 객체에 포함되어 있는가를 확인할 수 있는 연산자다.


var bool = "멤버명" in 객체;


var myCar = {make: "Hyundai", model: "포니", year: 1970};

"make" in myCar;     //true

delete myCar.model;

"model" in myCar;    //false


in 연산자도 hasOwnProperty 처럼 delete로 삭제된 멤버에 대해서 false를 반환한다.

그러나 in 연산자는 hasOwnProperty 와는 다르게 Object의 원래 멤버도 연산의 대상으로 포함한다.


"toString" in myCar;    //true


in은 Object 원래의 멤버도 연산 대상에 포함시킨다.


in 연산자는 다음처럼 배열과 같이 사용할 수도 있다.


var bool = 숫자 in 배열객체;

var bool - "멤버명" in 배열객체;


배열 객체도 객체이기 때문에 배열 객체의 멤버를 대상으로 연산을 수행한다.


var myArray = [1,2];

myArray.prop = "나의 속성";


"prop" in myArray;    //true


in 연산자 앞에 숫자가 오는 경우는 숫자를 배열의 인덱스로 여겨서 인덱스에 해당하는 요소가 존재하는지 여부를 알려준다.


var myArray = new Array("myCar", null, undefined)    //0, 1, 2 요소가 정의됨

0 in myArray;    // true

1 in myArray;    // true

2 in myArray;    // true

3 in myArray;    // false


요소의 값이 null,  undefined 값으로 설정된 경우도 참으로 인식한다.


숫자 in 배열 객체, "멤버명" in 배열 객체가 모두 가능하다면, 배열 객체의 멤버 데이터 관리 구조는 일반 객체와는 다를 것이다.

배열 객체의 내부 구조는 이후 내장 객체 글에서 정리할 것이다.




멤버 순회 : for/in


객체의 멤버 관리 구조가 연상 배열 구조를 이용하고 있다보니 자바스크립트 에서는 for/in 구문을 통해 모든 멤버에 접근하는 방법도 지원한다.


for (var property in obj) {

// property 사용

}


function MyFunc(){

this.a = "";

this.b = function(){ return ;};

this.c = { "k" : "v" };

}


var mf = new MyFunc();


for(var prop in mf){

console.log(prop)    //a,b,c

}


다음과 같은 식으로 객체의 멤버 이름을 모두 배열a에 넣을수도 있다.


var a = [];

for (var propertyName in o)

a.push(propertyName);


객체를 생성할때부터 추가된 멤버든 실행시 동적으로 추가된 멤버든 모든 멤버를 for/in 문으로 접근할 수 있다.

상속에 의해 부모 객체에서 물려받은 멤버도 모두 출력해준다.

그러나 자바스크립트에서 미리 정의해둔 내장(built-in) 멤버( ex. Object의 toString, prototype, constructor 등)는 출력되지 않는다.


for/in은 프로토타입 멤버, 인스턴스 멤버, 상속에 의해 받은 멤버를 출력한다. 그러나 내장(built-in) 멤버에는 접근하지 않는다.






2. prototype, constructor 인스턴스


생성자의 정의는 위 생성자 멤버와 객체 멤버에서 정리하였다.

함수가 정의될 때 그 함수와 연결된 프로토타입이라는 객체도 함께 정의된다는 내용이였는데, 생성자도 함수로써 이와 동일한 구조로 생성된다.




생성자 모델


다음과 같이 정의 된 생성자 Person을 보자.

function Person(name){

this.name = name;

}


Person을 파싱해서 정의하면 최종적으로 생성자 객체와 프로토타입 객체가 함께 정의된다.

생성자와 프로토타입 객체는 다음 그림 처럼 별도로 존재하면서 서로에 대한 참조를 가지고 연결되어 있다.



생성자 객체와 프로토타입 객체가 생성되면 각각 prototype, constructor 공개 속성이 추가되고 여기에 서로에 대한 참조가 할당된다.


생성자와 프로토 타입 객체는 별도의 객체로서 서로의 참조값을 prototype, constructor에 가지고 있다.


이제 new Person으로 객체를 생성하자.


//Person 인스턴스 생성

var mySon = new Person("달봉이");


위 코드가 실행되고 나서 생성되는 인스턴스와 prototype, constructor의 관계를 그려보면 다음과 같다.



mySon 인스턴스는 Person의 프로토타입 멤버를 상속하는 것을 알수있다.

프로토타입 멤버 상속에 관해서는 다음번 정리 글인 자바스크립트 상속에서 정리할 것이다.


생성자, 프로토타입 객체는 함수를 정의하면 함께 정의되는 객체이고 인스턴스는 생성을 해서 얻게 되는 객체다.

자바스크립트 객체의 개념을 이해할 때는 이 3가지 요소를 혼동하지 않게 주의해야 한다.


생성자, 프로토타입 객체, 인스턴스는 구분되어야 한다.




프로토타입 객체



prototype 속성


Person 생성자에는 prototype이라는 공개 속성이 있는데, 이 속성을 통해 프로토타입 객체에 접근할 수 있다.


Person.prototype


생성자의 프로토타입 객체에 접근해 멤버를 추가 또는 삭제하려면 반드시 생성자의 prototype 속성을 이용해야 한다.

프로토타입 객체는 생성자별로 하나만 정의되므로 프로토타입 객체에 접근하는 데 생성자를 이용하고 있다.


*생성자 멤버도 다음과 같이 생성자를 통해 접근한다.


Person.Description


생성자로 부터 만들어진 인스턴스에서 직접 프로토타입 객체에 접근하는 방법은 없다.


function Person{ .. }

var mySon = new Person();

mySon.prototype // 잘못된 코드


중요한 것은 인스턴스를 통해서도 생성자별로 정의되는 프로토타입 멤버에 접근할 수 있다는 것이다.


mySon.프로토타입멤버


프로토타입 멤버에는 생성자의 prototype 속성과 인스턴스를 통해서 접근할 수 있다.




프로토타입 객체의 특징


프로토타입 객체도 앞서 설명한 "멤버 관리 구조"의 특징을 그대로 가져 (키,값)쌍으로 된 멤버를 추가할 수 있는 연상 배열 구조이다.

따라서 프로토타입 객체에도 런타임에 멤버를 동적으로 추가할 수 있다. 프로토타입 객체에 포함된 멤버프로토타입 멤버라고 한다.


다음은 prototype을 통해 Person의 프로토타입 메서드 멤버를 추가하는 방법이다.


Person.prototype.getName = function(){ return this.name; };


중요한 것은 이처럼 Person.prototype을 통해 추가해준 프로토 타입 객체멤버는 Person 생성자로 만든 모든 인스턴스에서 사용할 수 있다.


생성자의 모든 인스턴스에서는 상속을 통해 자신의 프로토타입 멤버에 접근할 수 있다.


function Person(name) {

this.name = name;

}

Person.prototype.getName = function(){ return this.name; };


var mySon = new Person("김씨");

var yourSon = new Person("박씨");


mySon.getName();     //김씨

yourSon.getName();   //박씨


프로토타입 멤버에서 this를 사용한다면 이 this는 현재 인스턴스를 나타낸다.


프로토타입 멤버에서의 this는 현재 인스턴스를 나타낸다.


프로토타입 멤버는 "생성자.prototype.프로토타입멤버" 처럼 접근할 수도 있고 "인스턴스.프로토타입멤버"처럼 접근할 수도 있다.


하지만 생성자를통해 접근하느냐 인스턴스를 통해 접근하느냐의 차이는 아래 프로토타입 멤버 편집의 비대칭에서 정리한다.




프로토타입 멤버와 인스턴스 멤버 비교


각 인스턴스도 연상 배열 구조이므로 멤버를 동적으로 추가할 수 있다.


mySon.nickName = "";

yourSon.nickName = "";


이렇게 인스턴스에 새롭게 추가된 nickName 멤버는 각 인스턴스별로 관리된다.



반면 프로토타입 객체는 함수를 메모리에 정의할 때 한 번만 생성된다. 프로토타입 맴버는 인스턴스 별로 복사본이 존재하는 것이 아니라 해당 생성자에 하나만 존재하면서 그 생성자의 모든 인스턴스가 함께 공유하게 된다.




프로토타입 멤버 편집의 비대칭


function Person(){}


var mySon = new Person();

var yourSon = new Person();


Person.prototype.age = 6;




프로토타입 멤버 읽


console.log(Person.prototype.age)  //6

console.log(mySon.age)            //6

console.log(yourSon.age)          //6


Person 프로토타입 객체에 정의된 멤버의 값을 읽을 때는 위에서 봤듯이 두가지 표현을 모두 사용할수 있다.



프로토타입 멤버 추가


mySon.age = 7;

yourSon.age = 8;


age는 인스턴스 멤버가 아니므로 mySon과 yourSon에서 age를 찾지못한다. 그러면 age를 mySon 인스턴스에 속성으로 추가할까, 아니면 프로토타입 객체의 멤버를 검색할까?


console.log(Person.prototype.age)  //6

console.log(mySon.age)            //7

console.log(yourSon.age)          //8


위의 결과로 알수 있듯이 인스턴스를 통해 속성에 값을 쓸 때는 프로토타입 객체를 검색하지 않고 인스턴스에서만 속성을 검색한다는 사실을 확인할 수 있다.


즉, mySon.age에 값을 할당하면 Person.prototype.age 값을 업데이트하는 것이 아니라 mySon에 age속성을 추가해서 값을 할당한다.


인스턴스를 통해 멤버에 값을 쓰는 경우 멤버 검색 시 프로토타입 객체로까지 거슬러 올라가지 않는다.


모든 인스턴스의 값을 동시에 수정하고 싶을때는 어떻게 할까?


그럴때는 인스턴스를 통합 접근이 아닌 생성자의 prototype을 이용해 프로토타입 객체에 직접 접근해서 값을 변경하면 된다.


Person.prototype.age = 6;


console.log(Person.prototype.age)  //6

console.log(mySon.age)            //6

console.log(yourSon.age)          //6


프로토타입 멤버에 값을 쓰고 싶다면 인스턴스가 아니라 생성자의 prototype 속성을 이용해야 한다.




프로토타입 멤버 삭제


멤버를 삭제할 때나 메서드를 다른 내용으로 변경할 때도 값을 쓸때와 마찬가지다.


mySon.name = "김씨";

delete mySon.name;


인스턴스를 통해서는 인스턴스의 멤버만 삭제할 수 있다. 상속을 받은 프로토타입 멤버는 인스턴스를 통해 삭제할 수 없다. 

프로토타입 멤버를 삭제하려면 생성자.prototype을 이용해 프로토타입 객체에 직접 접근해서 삭제해야 한다.


delete에 의해 삭제된 멤버는 단순히 어떤 플래그 값만 변경되는것이 아니라 메모리상의 객체에서 실제로 완전히 사라져 버린다.


delete mySon.age;            //프로토타입의 age 삭제되지 않음

delete person.prototype.age.  //프로토타입의 age 삭제됨


프로토타입 멤버를 삭제하려면 생성자의 prototype 속성을 이용해야 한다.


정리해보면.


프로토타입 멤버를 사용하는 경우에는 생성자.prototype 또는 인스턴스를 이용할 수 있다. 그러나 프로토타입 멤버를 정의하거나 제거, 대체할 때는 반드시 생성자.prototype을 이용한다.




프로토타입 객체 대체


함수를 정의하면 자동으로 객체를 하나 생성해서 그 함수의 prototype 속성에 할당한다.

자동으로 생성되는 이 객체가 마음에 들지 않는다면 직접 원하는 객체를 만들어서 prototype 속성에 할당 할 수 있다.

이러한 방법이 다음 블로그 글에 정리할 자바스크립트 상속의 방법이 된다.


function Person(name){

this.name = name;

}


var personProto = {};

personProto.specises = "human";

personProto.countOfLegs = 2;


Person.prototype = personProto;


var mySon = new Person("김씨");


Person을 정의하면 기본적으로 프로토타입 객체가 생성되어 prototype 속성을 통해 생성자와 연결된다. 이를 species, countOfLegs 속성을 가지고 있는 personProto 객체를 만들어서 대체 하고있다.

Person 생성자를 이용해 생성된 mySon 인스턴스의 속성을 출력해보면 아래와 같은 결과를 얻을수 있다.


for(var item in mySon)

console.log(item+":"+mySon[item])    //name:김씨, specises:human, countOfLegs:2


for(var item in Person.prototype)

console.log(item+":"+mySon[item])    //specises:human, countOfLegs:2




생성자


함수를 정의하면 프로토타입 객체가 정의되고 그 객체의 멤버로 constructor 가 있다고 했다.

constructor 속성은 프로토타입 객체를 생성했던 함수에 대한 참조를 나타낸다.


이 프로토타입 객체의 constructor 속성은 상속을 통해 인스턴스에서도 아래와 같이 접근 가능 하다.


mySon.constructor


Person 예제의 예로들면 mySon.constructor 값은 Person을 가리키므로 아래와 같은 표현은 참이된다.


mySon.constructor == Person    //true


앞에서 본 생성자, 프로토타입 객체, 그리고 인스턴스 관계도를 보면서 따져보면 다음과 같은 객체비교는 모두 참이 된다.


mySon.constructor == Person.prototype.constructor    // true

mySon.constructor == Person                         // true


constructor 속성은 단순히 함수명을 나타내는 것이 아니라 함수 객체에 대한 참조이다.

따라서 이 값을 통해 함수를 호출할 수도 있고 객체를 생성할 수도 있다.


var myGrandSon = new mySon.constructor("홍길동");

myGrandSon.name;    //홍길동


mySon.constructor는 생성자 객체를 가리키는 참조값이므로 ()연산자를 이용해 직접 호출 할 수도 있고 new와 함께 사용되어 객체를 생성할수도 있다.


프로토타입 객체의 멤버인 constructor 속성은 해당 프로토타입 객체를 생성한 생성자 객체를 가리킨다.


prototype 속성처럼 constructor에 할당된 기본값도 런타임에 언제든지 변경될 수 있다.

다음과 같이 mySon의 constructor에 Object 생성자를 할당할 수도 있다


mySon.constructor = Object;


인스턴스의 constructor를 통해서도 프로토타입 객체에 접근하는 방법이 있다.


mySon.constructor.prototype;


 mySon.copnstructor가 생성자인 Person을 가르키고, 결국은 생성자.prototype 형태가 된다. (읽기 쓰기 모두가능)


constructor는 프로토타입 객체안 속성으로 존재하고 때문에 앞에서 봤듯이 자동적으로 상속을 통해 mySon에서 사용할 수 있게 된다.

따라서 다음과 같은 비교는 모두 프로토타입 객체를 나타내는 표현이 된다.


mySon.constructor.prototype == Person.prototype    // true






3. 멤버 접근 제어 구조


일반 객체지향 언어에서는 멤버에 대한 접근을 제어하기 위해 public, private 같은 한정자를 제공한다.

그러나 자바스크립트에서는 멤버에 대한 접근 제어 및 정보 은닉을 위한 특별한 키워드를 언어 차원에서 제공하지 않는다.

자바스크립트에서의 공개 속성은 값을 직접 가지고 있는 변수와 같다. 따라서 객체의 내부 상태를 속성을 통해 그대로 노출하고 이것은 정보 은닉이 되지 않는다는 의미다.

때로는 값을 외부에서 직접 접근하지 못하도록 비공개 멤버(private member)를 만들고 싶은 경우가 있는데 이는 개발자가 직접 구현해야 한다. 이장에서는 접근 제어를 위한 특별한 패턴 2가지를 정리해 본다.



비공개 멤버 구현


함수의 지역 변수는 외부에서 접근할수 없는 비공개 변수이다.


function Person(){

var localVar = "a';    //내부변수

this.publicVar = "b";  //공개변수

}


var person = new Person();

person.localVar //undefined

person.publicVar // b


따라서 이러한 지역 변수가 객체의 상태값을 가지고 있으면 된다.


문제는 공개 멤버가 비공개 변수에 직접 접근할수 없다는 것이다. 그래서 공개 멤버는 매부 변수에 접근 할수있는 내부 함수를 두고서 이 함수를 통해 내부 상태값에 접근해야 한다.


이를 정리하면 객체의 상태값으로는 내부의 지역 변수를 사용하고 외부의 접근 메서드로는 공개 메서드를 사용하되, 공개 메서드는 지역변수에 접근하는 내부 함수를 정의해서 사용한다는 것이다.


내부 변수(상태값) <-> 내부 함수 <-> 공개 메서드


function A(){

//내부 지역 변수

var localX = 7;

//공개 접근 메서드(this.getX, this.setX) 내부함수 (익명함수 function())

this.getX = function() { return localX; };

this.setX = function(x)

if(x<10){

localX = x;

}

return localX;

}

}


지역 변수 localX의 읽기, 쓰기 전용의 공개 메서드인 getX, setX를 정의 했다.

getX, setX에서는 다시 지역 변수에 접근하기 위해 익명함수를 내부에서 사용하고 있다.


이처럼 직접 속성의 상태값에 접근하지 않고 내부의 정의된 함수를 통해 접근하는 구조가 되면, 내부 상태값을 반환할 때는 그 값을 필요한 대로 가공해서 건네줄 수 있고, 값을 할당할 때도 입력받은 값을 가공하거나 유효성 검사 등을 수행한 후 상태값을 변경할 수 있다.


이제 다음과 같이 A의 객체를 생성한 후 공개 메서드를 통해 값에 접근할 수 있다.


var obj = new A();

var x = obj.getX();    //7

obj.setX(8);           //8

obj.setX(10);          //8

obj.localX           //지역변수에 접근불가




클로저 인스턴스 2


비공개 멤버와 공개 멤버를 가진 객체를 구현하는 패턴화된 기법을 알아본다.

이 기법을 이용하면 자바스크립트에서도 일반 객체지향 언어에서의 객체와 거의 유사한 구조의 객체를 만들어 낼 수 있다.


비공개 변수를 가질 수 있는 함수 인스턴스의 생성자가 클로저 이다.


function outer(){

var x = 0;

function private01(){

return ++x;

}

function private02(){

return x += 2;

}

return { public01 : private01, public02 : private02 };

}


위와 같이 outer 라는 클로저를 정의했다. 이객체에는 비공개로 보호된 변수 x 그리고 x를 외부에 공개하는 메서드인 public01, public02가 있고, public01과 public02가 내부 변수 x에 접근할 수 있게 만들어 주는 내부 함수인 private01과 private02도 설계되어 있다.

이 내부함수를 각각 호출하는 함수 public01, public02는 return 문에서 반환 객체를 통해 외부에 공개된다.


return 문으로 반환된 객체가 외부에 나가서 사라지지 않는 한 내부의 x값도 계속 유지된다.

그리고 클로저인 outer를 호출할 때마다 반환되는 객체별로 내부 변수 x가 초기화 된다.


var o1 = outer();    // 인스턴스 o1생성

o1.public01();      // 1

o1.public02();     // 3


var o2 = outer();    // 인스턴스 o2생성

o2.public01();      // 1

o2.public02();      // 3


클로저를 호출하면 단순히 객체가 반환되는 것이 아니다. 객체와 함께 그것과 연결된 닫힌 공간이 함께 반환되는 것이다. 그리고 그 닫힌 공간에 내부 변수가 존재한다.


클래스와 new를 사용해 여러 개의 닫혀진 공간을 가진 인스턴스를 만들어 내듯이 클로저와()를 이용하면 닫힌 공간을 가진 인스턴스를 여러 개 반복해서 만들어낼 수 있다. 즉, 클로저는 클래스와 유사한 존재로 볼 수 있다.


outer 클로저를 이용해 인스턴스 o1과 o2를 생성하는 것을 그림으로 보면.



o1과 o2는 독립적인 변수 공간을 가진 인스턴스다.


클로저를 호출할 때마다 새로운 클로저 인스턴스가 생성된다. 새로운 클로저 인스턴스는 내부 상태가 초기화된 새로운 닫힌 환경을 가진다.


클로저를 외부에 공개되지 않는 비공개 멤버와 외부에서 접근 가능한 공개 멤버를 가진 객체를 구현하는 데 사용할 수 있다.













참고.


자바스크립트 객체지향 프로그래밍.

댓글