카테고리 없음

prototype

funnyjs 2022. 2. 26. 01:58

자바스크립트는 어떤 객체를 원형으로 삼고 이를 복제함으로써 상속과 비슷한 효과를 내는 프로토타입 기반 객체지향  프로그래밍 언어이다. 자바스크립트를 이루고 있는 거의 모든 것이 객체이며 원시 값을 제외한 나머지는 모두 객체이다. 프로토타입의 구조에 대해 파악해보자.

 

생성자 함수

function Person(name) {
    this.name = name;
    this.sayName = function () {
       console.log(`내 이름은 ${this.name}이야`);
    }
    return this.name;
}

자바스크립트에서 함수는 일반 함수로도 호출할 수 있지만 new 연산자와 함께 쓴다면 생성자 함수로 사용할 수 있다. 위 함수를 호출해보자.

const name = Person('길동'); // '길동'
const obj = new Person('둘리'); // Person {name : '둘리', sayName : f}

new 연산자와 함께 함수를 호출한다면 자바스크립트는 암묵적으로 인스턴스를 생성하고 그 인스턴스를 this에 바인딩한다. 하지만 new 연산자가 없이 그냥 함수만 호출한다면 this는 전역 객체가 된다. 따라서 생성자로 호출한 obj는 obj.name에 전달한 name이 할당되고, 함수로 호출하면 전역객체 속에 name이 할당된 후 그 name을 리턴하게 된다. 

생성자는 return에 있는 값이 객체라면 그 값을 반환하지만 원시값이라면 무시하고 객체의 인스턴스를 반환한다. 위 코드에서는 원시값이기 때문에 무시한다.

 

non-constructor 

모든 함수는 호출을 할 수 있지만 생성자 함수로 호출은 모두 할 수 있는 것은 아니다.

ES6에서 추가된 메서드 축약형과 화살표 함수는 생성자 함수로 호출할 수 없다.

const obj = {
    f() {
        console.log('함수');
    }
}

new obj.x(); // TypeError

const f = () => {};

new f(); // TypeError

 

프로토타입의 활용

prototype 프로퍼티는 생성자 함수로 호출할 수 있는 함수만이 소유하는 프로퍼티로 함수가 생성자 함수로 호출될 때 생성할 인스턴스의 프로토타입 객체를 가리킨다. 이를 잘 활용하면 불필요한 중복을 막을 수 있다.

function obj(a) {
    this.a = a;
    this.f = function() {
        console.log(this.a);
    }
}
const obj1 = new obj(1);
const obj2 = new obj(2);

console.log(obj1);
{ a : 1, f : f }
console.log(obj2);
{ a : 2, f : f }

이렇게 작성하면 메소드 f는 같은 기능을 하지만 두 번 만들어져 불필요한 중복이 생긴다. 이럴 때 프로토타입을 활용하면 중복을 줄일 수 있다.

function obj(a) {
    this.a = a;
}

obj.prototype.f = function() {
    console.log(this.a);
}

const obj1 = new obj(1);
const obj2 = new obj(2);
console.log(obj1);
// {a : 1}
console.log(obj2);
// {a : 2}
obj1.f(); // 1
obj2.f(); // 2

이렇게 작성하면 메소드 f는 프로토타입 객체에 딱 한 번만 만들어지고 그를 상속하는 모든 객체에서 자유롭게 사용할 수 있다!

 

__proto__ 접근자와 prototype 비교

__proto__는 Object.prototype으로부터 상속받아 모든 객체가 가지고 있고 자신의 프로토타입에 접근하기 위해 사용된다. prototype 프로퍼티는 생성자 함수만이 가지고 있으며 자신이 생성할 객체의 프로토타입을 할당하기 위해 사용된다.

function Obj(a) {
    this.a = a;
}

const obj = new Obj(1);

console.log(Obj.prototype === obj.__proto__); // true

즉 prototype은 자신을 상속받을 객체의 __proto__이고 생성자 함수만이 객체에게 상속될 수 있기 때문에 생성자 함수만 가질 수 있는 것이고 __proto__는 자신이 상속 받은 객체의 프로토타입이기 때문에 모든 객체가 가진다.

 

생성자 함수와 prototype 관계

Object.prototype.constructor === Object;

 

프로토타입 체인

function Obj(a) {
    this.a = a;
}

Obj.prototype.getA = function () {
    console.log(`a는 ${a}이다`);
}

const myObj = new Obj(5);
myObj.getA(); // a는 5이다

myObj.hasOwnProperty('a'); // true

위 예시에서 Obj 객체를 상속받은 myObj는 당연히 Obj 객체의 getA 메소드를 사용할 수 있다. 그런데 Obj에서 정의하지 않은 hasOwnProperty라는 메소드도 사용할 수 있다? 이는 Obj의 프로토타입인 Object.prototype의 메소드로 myObj에서 해당 메소드를 사용하려고 찾았는데 없어서 그 상위 프로토타입까지 거슬러 올라가 찾아서 사용할 수 있는 것이다. 이렇게 객체의 프로퍼티에 접근하려고 할 때 해당 객체에 그 프로퍼티가 없을 경우 부모 프로토타입을 순차적으로 검색하는 것을 프로토타입 체인이라고 한다. 프로토타입 체인의 최상위는 언제나 Object.prototype이다. 여기에도 없을 경우에는 undefined이다.

식별자는 스코프 체인으로, 프로퍼티는 프로토타입 체인으로 검색한다.

 

정적 메소드/프로퍼티와 프로토타입 메소드/프로퍼티

정적 프로퍼티/메소드는 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메소드를 말한다. 정적 프로퍼티/메소드는 Object.staticProp = 값 형식으로 할당하면 된다. this를 사용하지 않는다면 정적 프로퍼티/메소드로 변경할 수 있다.  

function Obj (a) {
    this.a = a;
}
Obj.prototype.protoMethod = function () {
    console.log(this.a);
}
Obj.staticMethod = function () {
    console.log('정적메소드');
}
const obj = new Obj(1);
obj.protoMethod(); // 1
obj.staticMethod(); // TypeError

 

for ~ in 문

in 연산자는 객체 내에 그 프로퍼티가 존재하는지를 모든 프로토타입 체인에서 검색해 boolean 값으로 평가해준다. ES6에서 추가된 Reflect.has 메서드는 in 연산자와 동일하게 동작한다.

'toString' in Number // true
'filter' in String // false

(for 변수선언문 in 객체) 로 사용하면 프로퍼티 개수만큼 객체를 순회하는 for문을 만들 수 있다. 변수선언문에 쓴 식별자에 객체의 프로퍼티명이 할당된다. 이 때 프로토타입 체인상에 있는 모든 프로퍼티를 출력하지만 Enumerable의 값이  false인 프로퍼티는 참조하지 않는다.

const arr = ['a', 'b', 'c'];
for (const a in arr) {
    console.log(`${a}는 ${arr[a]}`);
}
// 0는 a
// 1는 b
// 2는 c

키가 심벌인 프로퍼티도 출력하지 않는다. 자기 자신의 프로퍼티만 참조하고 싶다면 hasOwnProperty로 키를 확인해 사용하면 된다.

 

Object.key, values, entries

Object.keys는 프로퍼티명들의 배열을, values는 값들의 배열을, entries는 [프로퍼티명, 프로퍼티값]의 배열을 반환한다.

const Person = {
	name : '홍길동',
    age : 27,
    address : '서울',
}

console.log(Object.keys(Person)); //  ['name', 'age', 'address']
console.log(Object.values(Person)); // ['홍길동', 27, '서울']
console.log(Object.entries(Person));
// 0: (2) ['name', '홍길동']
// 1: (2) ['age', 27]
// 2: (2) ['address', '서울']