0%

201204_TIL(생성자 함수, prototype)

오늘 배운 것

생성자 함수에 의한 객체 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person(name) {
this.name = name;
this.sayHi = function () {
console.log(`Hi, my name is ${this.name}`);
};
}

const me = new Person("Lee");
const you = new Person("Choi");

console.log(me); // Person { name: 'Lee', sayHi: [Function] }
console.log(me.name); // Lee
console.log(me.sayHi); // [Function]

me.sayHi(); // Hi, my name is Lee
you.sayHi(); // Hi, my name is Choi

// foo에 넣어줄 수 있다.
const foo = me.sayHi;
//여기서의 this는 전역객체이다.
foo(); // Hi, my name is undefined

프로토타입

non-constructor는 함수 객체이지만 인스턴스를 생성할 일이 없기 때문에 prototype이 없다.

객체지향 프로그래밍

프로그래밍 방법론 대세가 절차지향 프로그래밍에서 객체지향 프로그래밍으로 바뀌었다.

상태와 행위를 갖는 객체 집합을 만들어서 객체끼리의 소통을 중심으로 어플리케이션을 만드는 방식을 말한다.

대규모 프로젝트에서 사전에 철저하게 준비해야만 가능하다. 객체 지향 프로그래밍을 완벽하게 실행하려면 객체 추상화부터 설계, 제작을 어플리케이션 완성 전에 완벽하게 만들어놔야한다. 때문에 유지보수하기 힘든 단점이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
console.log(`Hi, my name is ${this.name}`);
}

function Person(name) {
this.name = name;
this.sayHi = foo;
}

const me = new Person("Lee");
const you = new Person("Choi");

me.sayHi(); // Hi, my name is Lee
you.sayHi(); // Hi, my name is Choi

위 코드도 동작은 하지만 객체 지향이 깨진다.

프로토타입 객체

모든 객체는 내부 슬롯 [[Prototype]] 을 갖는다.

프로토타입을 상속받은 하위(자식) 객체는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 자유롭게 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
function Person(name) {
this.name = name;
}

Person.prototype.sayHi = function () {
console.log(`Hi my name is ${this.name}`);
};

const me = new Person("Lee");
// 스코프 체인에서 'me'를 찾아서 'me'객체부터 프로토타입 체인을 따라 sayHi를 찾는다.
me.sayHi();

식별자는 스코프 체인에서 찾고 프로퍼티 키는 프로토타입 체인에서 찾는다.(프로토타입 체인 검색 기점은 . 앞 식별자 객체부터)

프로토타입 종점인 최상단 프로토타입 Object.prototype은 전역개체가 만들어지고 만들어진다.

proto 접근자 프로퍼티

__proto__[[Prototype]] 으로 간접적으로 접근할 수 있게 해준다. 하지만 Object.getPrototypeOf(객체) 로 대체하는 것이 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function Person(name) {
this.name = name;
}

const me = new Person("Lee");
console.log(me.__proto__ === Person.prototype); // true
console.log(Object.getPrototypeOf(me) === Person.prototype); // true

// Own은 직접적으로 갖는 프로퍼티를 뜻한다. 하지만 __proto__는 상속받은 프로퍼티이다.
console.log(Object.getOwnPropertyDescriptor(me, "__proto__")); //undefined
console.log(Object.getOwnPropertyDescriptor(Person.prototype, "__proto__")); //undefined

console.log(Object.getOwnPropertyDescriptor(Object.prototype, "__proto__"));
/*
{
get: [Function: get __proto__],
set: [Function: set __proto__],
enumerable: false,
configurable: true
}
*/

// 모든 객체는 Object.prototype의 __proto__를 상속받아 사용한다. = Object.prototype가 최상위 객체이다.
console.log(Object.getOwnPropertyDescriptor(Object.prototype, "__proto__"));
/*
{
get: [Function: get __proto__],
set: [Function: set __proto__],
enumerable: false,
configurable: true
}
*/

in, Reflect, hasOwnProperty

in 연산자는 상속까지 고려해서 프로퍼티 키를 찾지만 hasOwnProperty는 상속을 고려하지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// in 연산자
const person = {
name: "Lee",
address: "Seoul",
};

// person 객체에 name 프로퍼티가 존재한다.
console.log("name" in person); // true
// person 객체에 address 프로퍼티가 존재한다.
console.log("address" in person); // true
// person 객체에 age 프로퍼티가 존재하지 않는다.
console.log("age" in person); // false
// 확인 대상 객체가 상속받은 모든 프로토타입의 프로퍼티를 확인한다.
console.log("toString" in person); // true
/*************************************************/

// Reflect 함수
const person = { name: "Lee" };

console.log(Reflect.has(person, "name")); // true
console.log(Reflect.has(person, "toString")); // true
/**********************************************************/

//hasOwnProperty
const o = {
apple: 1,
};

// 빨간 줄이 뜬다.
// hasOwnProperty를 사용하지 못하는 경우가 있기 때문이다.
console.log(o.hasOwnProperty("apple")); //true

const x = Object.create(null);
// 위 같은 경우 종점이다. hasOwnProperty를 사용하지 못한다.
// 위의 빨간 줄은 이런 경우를 방지하기 위해 아래 방법을 추천하는 것이다.

// hasOwnProperty 메소드의 this로 o를 사용하는 것이다.
// Object.prototype.hasOwnProperty를 call 해주는 것이다.
console.log(Object.prototype.hasOwnProperty.call(o, "apple")); //true
/***********************/

const o1 = {
x: 1,
};

// 프로토타입 체인 종점을 뜻함.
const o2 = Object.create(null);
o2.x = 1;

// 이렇게 찾아야한다.
console.log(Object.prototype.hasOwnProperty.call("x"));

// 찾으려해도 프로토타입 종점이라 에러 발생
console.log(o2.hasOwnProperty("x"));
console.log(o2.hasOwnProperty("y"));

프로토타입이 소유한 프로퍼티(메서드 포함)를 프로토타입 프로퍼티, 인스턴스가 소유한 프로퍼티를 인스턴스 프로퍼티라고 부른다. 생성자함수가 소유한 프로퍼티는 정적 프로퍼티라고 부른다.

인스턴스에는 프로퍼티만 있고 메서드는 프로토타입에 두는게 기본이다.

객체 생성 방식과 프로토타입의 결정

1
2
3
// 생성 방식은 다르지만 결과적으로는 같다고 봐도 무방하다.
const o1 = new Object();
const o2 = {};
1
2
3
4
5
6
7
8
// 내부동작은 다르지만 프로토타입 체인은 같다.
const f1 = new Function("x", "return x");
const f2 = function (x) {
return x;
};
// 내부동작은 다르지만 프로토타입 체인은 같다.
const arr1 = new Array();
const arr2 = [];

프로토타입의 constructor 프로퍼티와 생성자 함수

프로토타입 객체는 constructor 프로퍼티를 갖는다. 참조값은 생성자 함수이다.

프로토타입의 교체

인스턴스에 의한 프로토타입의 교체

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Person(name) {
this.name = name;
}

const me = new Person("Lee");
const p = {
constructor: Person,
age: 30,
};
me.__proto__ = p;

console.log(me.age); // 30 / 프로토타입 프로퍼티
console.log(me.name); // Lee / 인스턴스 프로퍼티
console.log(me.constructor); // [Function: Object] / Object.prototype까지 올라가서 찾아온다.
console.log(me instanceof Person); // false
console.log(me instanceof Object); // true

console.log(me instanceof Person); // false
// Person 생성자 함수의 prototype이 me.__proto__와 다르다.

Person.prototype = p; // Person의 prototype이 me.__proto__와 같게 만들어준다.
console.log(me instanceof Person); // true

프로토타입 교체는 지양한다.

오버라이딩과 프로퍼티 섀도잉

1
2
3
4
5
6
7
8
9
// 생성자 함수
function Person(name) {
this.name = name;
}

//프로토타입 메서드
Person.prototype.sayHello = function () {
console.log(`Hi, My name is ${this.name}`);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 이게 더 깔끔하다.
const Person = (function () {
// 생성자 함수
function Person(name) {
this.name = name;
}

//프로토타입 메서드
Person.prototype.sayHello = function () {
console.log(`Hi, My name is ${this.name}`);
};

return Person;
})();

const me = new Person("Lee");
me.sayHello(); // Hi, My name is Lee

섀도잉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const Person = (function () {
// 생성자 함수
function Person(name) {
this.name = name;
}

//프로토타입 메서드
Person.prototype.sayHello = function () {
console.log(`Hi, My name is ${this.name}`);
};

return Person;
})();

const me = new Person("Lee");

me.sayHello = function () {
console.log(`Hello, My name is ${this.name}`);
};

me.sayHello(); // Hello, My name is Lee

상속 관계에 의해 프로퍼티가 가려져서 섀도잉됐다.

오버라이딩(overriding)

상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의하여 사용하는 방식이다.

오버로딩(overloading)

함수의 이름은 동일하지만 매개변수의 타입 또는 개수가 다른 메서드를 구현하고 매개변수에 의해 메서드를 구별하여 호출하는 방식이다. 자바스크립트는 오버로딩을 지원하지 않지만 arguments 객체를 사용하여 구현할 수는 있다.

정적 프로퍼티/메서드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function Person(name) {
this.name = name;
}

// 프로토타입 메서드
// 반드시 인스턴스로 호출해야함.
Person.prototype.sayHello = function () {
console.log(`Hi, I am ${this.name}`); // Hi, I am Lee

// 인스턴스 굳이 만들어서 Hi 출력할 바에 정적 메서드로 쓴다.
console.log(`Hi!`); // Hi!
};

// 정적 메서드
Person.sayHello = function () {
// this는 인스턴스를 위한 것이기 때문이다.
console.log(`Hi, I am ${this.name}`); // Hi, I am Person

// 정적 메서드는 인스턴스 생성 없고, this 없는 것을 위한 메서드이다.
console.log(`Hi!`); // Hi!
};

const me = new Person("Lee");
me.sayHello(); // Hi, I am Lee

// 정적 메서드에서는 this를 안 쓴다. this를 쓴다는 것은 인스턴스를 본다는 의미이다.
// 정적 메서드는 인스턴스를 호출하는 것이 아니라 생성자 함수를 가져오기 때문에 this를 쓰지 않는다.
Person.sayHello(); // Hi, I am Person

쉽게 말해서 this를 쓰면 프로토타입 메서드, 안 쓰면 정적 메서드 정도로 정리하자.

Nyong’s GitHub