자바스크립트 객체 지향(JavaScript OOP)
자바스크립트는 멀티 패러다임 언어로 명령형, 함수형, 프로토타입 기반 객체 지향 언어
- 멀티 패러다임 프로그래밍 언어
프로그래밍 언어는 설계 될 때 각자 지향하는 패러다임을 설정함
멀티 패러다임 프로그래밍 언어란 하나 이상의 프로그래밍 패러다임을 지원하는 프로그래밍 언어
- 명령형
수행할 명령들을 순서대로 써 놓은 수행 기법(How)
자바스크립트는 기본적으로 명령형 프로그래밍 언어이지만, 함수형 프로그래밍의 요소를 포함하고 있어서 선언형 프로그래밍 (무엇을 하는지에 초점을 맞춤 - What) 도 가능함
예를들어 map() 함수는 선언형 프로그래밍의 예시. 어떻게 만들지가 아닌 무엇을 하는지를 명시함
- 프로토 타입 기반 언어
모든 객체들이 메소드와 속성들을 상속 받기 위한 템플릿으로써 프로토타입 객체(prototype object)를 가진다는 의미
▶ 객체지향
객체 지향은 객체 안에서 상태(속성)를 나타내는 변수들과 객체의 행동(메서드)으로 이루어져 있고, 각각의 객체들은 협력하는 과정을 통해서 대규모 소프트웨어을 만듬
즉, 객체 지향 프로그래밍(Object-Oriented Programming, OOP)은 프로그래밍에서 필요한 데이터를 추상화 시켜 상태와 행위를 가진 객체로 만들고, 객체들간의 상호작용을 통해 로직을 구성하는 프로그래밍 방법
객체 지향 프로그래밍의 특징
객체 지향 프로그래밍은 크게 추상화, 캡슐화, 상속, 다형성의 네가지 특징을 가지고 있음
추상화
- 객체에서 공통된 속성과 행위를 추출
- 공통된 속성과 행위를 찾아서 타입을 정의
- 추상화는 불필요한 정보는 숨기고 중요한 정보만을 표현함으로써 프로그램을 간단하게 만듬
-
파일을 저장하는 기능이 있을때, 안에 어떻게 동작하는지 로직은 알 필요 없이 saveFile()이라는 함수를 호출function saveFile(data) { // 복잡한 로직 }
-
캡슐
- 외부에서 알 필요 없는 것들은 숨김
- 외부에 노출해야 하는 값과 내부에서만 사용하는 값을 구분하는 기능
- 객체가 맡은 역할을 수행하기 위해 관련 상태와 메서드를 묶는 행위
- 객체 내 상태 데이터가 외부에서 접근이 불가능하고 오로지 함수를 통해서만 접근해야하는 데 이를 바로 캡슐화라고 함
상속
- 클래스에서 extends를 사용(ES6부터 가능, ES5는 프로토타입 지정)
- 상위 클래스의 속성을 하위 클래스에게 물려주는 것을 의미
- 상속을 통해서 기존 코드를 재사용, 확장 가능
장점 | 단점 |
재상용으로 인한 코드가 줄어듬 | 상위 클래스의 변경이 어려워짐 |
범용적인 사용이 가능함 | 불필요한 클래스가 증가할 수 있음 |
자료와 메서드의 자유로운 사용 및 추가가 가능 | 상속이 잘못 사용될 수 있음 |
다형성
- 같은 메서드를 호출하더라도 객체에 따라 다르게 동작
- 대표적으로 재정의(오버라이딩(Overiding))으로 구현 가능
- 오버라이딩
- 오버라이딩은 상위 클래스에서 가져온 메서드를 재정의해서 하위 객체의 특성에 맞게 변형하는 것
- 부모 객체의 메소드를 덮어씌운 것
- 부모 클래스에서 이미 정의된 함수 등을 자식 클래스에서 같은 이름으로 사용하되 안에 들어가는 내용(기능, 속성 등)을 바꿔서 사용
오버로딩은 하나의 함수를 유연하게 활용하고 싶을 때 사용
new 연산자
new 연산자와 함께 함수를 호출하면 해당 함수는 생성자 함수로 동작함
다시 말해, 함수 객체의 내부 메서드 [[Call]]이 호출되는 것이 아니라 [[Contruct]]가 호출되는 것
따라서 new 연산자와 함께 호출되는 함수는 non-constructor가 아닌 constructor이어야 함
생성자 함수는 일반 함수와 특별한 형식적 차이가 없음
따라서 생성자 함수는 일반적으로 첫 문자를 대문자로 기술하는 파스칼 케이스로 명명하여 일반 함수와 구별할 수 있도록 함
생성자 함수를 이용해 객체를 생성하면, 프로퍼티 구조가 동일한 객체 여러 개를 간편하게 생성할 수 있음
function 생성자함수명(매개변수) {
this.속성 = 값;
}
let 변수명 = new 생성자함수명(매개변수);
만약 매개변수가 들어가야하는데 안넣으면 해당 값은 undefined
▶ 내부 메서드 [[Call]]과 [[Construct]]
함수 객체는 일반 객체가 가지고 있는 내부 슬롯과 내부 메서드는 물론, 함수로서 동작하기 위한 함수 객체만을 위한 [[Enviornment]], [[FormalParameter]] 등의 내부 슬롯과 [[Call]], [[Construct]] 같은 내부 메서드를 추가로 가지고 있음
// 함수는 객체
function foo() {}
// 함수는 객체이므로 프로퍼티를 소유할 수 있음
foo.props = 10
// 함수는 객체이므로 메서드를 소유할 수 있음
foo.method = function () {
console.log(this.props);
};
foo.method(); // 10
함수가 일반 함수로서 호출되면 함수 객체의 내부 메서드 [[Call]]이 호출되고, new 연산자와 함께 생성자 함수로서 호출되면 내부 메서드 [[Construct]]가 호출
function foo() {}
// 일반적인 함수로서 호출: [[Call]]이 호출
foo();
// 생성자 함수로서 호출: [[Construct]]가 호출
new foo();
내부 메서드 [[Call]]을 갖는 함수 객체를 callable(호출할 수 있는 객체 = 함수)이라 하며, [[Contruct]]를 갖는 함수 객체를 constructor(생성자 함수로서 호출할 수 있는 함수), [[Contruct]]를 갖지 않는 함수 객체를 non-constructor(생성자 함수로서 호출할 수 없는 함수)라고 부름
모든 함수 객체는 callabe이지만 모든 함수 객체가 constructor인 것은 아님
자바스크립트 엔진은 함수 정의를 평가하여 함수 객체를 생성할 때 함수 정의 방식에 따라 함수를 constructor와 non-constructor로 구분
- constructor : 함수 선언문, 함수 표현식, 클래스
- non - constructor : 메서드, 화살표 함수
▶ 생성자 함수의 인스턴스 생성 과정
생성자 함수의 역할은 프로퍼티 구조가 동일한 인스턴스를 생성하기 위한 템플릿(클래스)으로서 동작하여 인스턴스를 생성하는 것과 생성된 인스턴스를 초기화(인스턴스 프로퍼티 추가 및 초기값 할당)하는 것
1. 인스턴스 생성과 this 바인딩
암묵적으로 빈 객체가 생성되며 이 빈 객체가 생성자 함수가 생성한 인스턴스
인스턴스는 this에 바인딩 됨
- 바인딩
식별자와 값을 연결하는 과정을 의미
예를 들어, 변수 선언은 변수 이름(식별자)과 확보된 메모리 공간의 주소를 바인딩하는 것
function Circle(radius) {
// 1. 암묵적으로 인스턴스가 생성되고 this에 바인딩
console.log(this);
this.readius = radius;
this.getDiameter = function () {
return 2 * this.radius;
};
}
2. 인스턴스 초기화
this에 바인딩되어 있는 인스턴스에 프로퍼티나 메서드를 추가하고 생성자 함수가 인수로 전달받은 초기값을 인스턴스 프로퍼티에 할당하여 초기화하거나 고정값을 할당
function Circle(radius) {
// 1. 암묵적으로 인스턴스가 생성되고 this에 바인딩
console.log(this);
// 2. this에 바인딩되어 있는 인스턴스를 초기화
this.readius = radius;
this.getDiameter = function () {
return 2 * this.radius;
};
}
3. 인스턴스 반환
생성자 함수 내부의 모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환됨
function Circle(radius) {
// 1. 암묵적으로 인스턴스가 생성되고 this에 바인딩
console.log(this);
// 2. this에 바인딩되어 있는 인스턴스를 초기화
this.readius = radius;
this.getDiameter = function () {
return 2 * this.radius;
};
// 3. 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환
}
// 인스턴스 생성. Circle 생성자 함수는 암묵적으로 this를 반환
const circle = new Circle(1);
console.log(circle); // Circle {radius: 1, getDiameter: f}
▶ new.target
생성자 함수가 new 연산자 없이 호출되는 것을 방지하기 위해 파스칼 케이스 컨벤션을 사용한다 하더라도 실수는 언제나 발생할 수 있음
이러한 위험성을 피하기 위해 ES6에서는 new.target을 지원
new.target은 this와 유사하게 constructor인 모든 함수 내부에서 암묵적인 지역 변수와 같이 사용되며 메타 프로퍼티라고 부름
함수 내부에서 new.target을 사용하면 new 연산자와 함께 생성자 함수로서 호출되었는지 확인할 수 있음
new 연산자와 함께 생성자 함수로서 호출되면 함수 내부의 new.target은 함수 자신을 가리킴
new 연산자 없이 일반 함수로서 호출된 함수 내부의 new.target은 undifinded
function Person(name, age) {
if (!new.target) {
// new.target이 undefined이면 new 키워드 없이 호출된 것
return new Person(name, age);
}
this.name = name;
this.age = age;
}
const person1 = new Person('John', 30); // new 키워드를 사용한 호출
const person2 = Person('Jane', 25); // new 키워드를 사용하지 않은 호출
console.log(person1); // Person { name: 'John', age: 30 }
console.log(person2); // Person { name: 'Jane', age: 25 }
function Target(){
console.log(new.target)
}
let target = new Target() // ƒ Target()
Target() // undefined
Prototype chain
▶ 프로토타입
프로토타입(prototype)이란 생성자 함수로 생성한 객체들이 속성(properties)과 메서드(mehtod)를 공유하기 위해 사용하는 객체(프로토타입은 특정 객체에 대한 참조로, 어떠한 공간을 가르키고 있음)
javascript에서 함수를 생성할 때 프로토타입 속성이 함수에 붙여짐
예를 들어 new 키워드로 함수를 호출할 때마다 생성되는 인스턴스는 함수 프로토 타입의 모든 속성을 상속
- 프로토타입은 어떤 객체의 상위(부모) 객체의 역할을 하는 객체로서 다른 객체에 공유 프로퍼티(메서드 포함)를 제공
- 프로토타입을 상속받은 하위(자식) 객체는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 자유롭게 사용
- 모든 객체는 [[prototype]]이라는 내부 슬롯을 가지며, 객체가 생성될때 객체 생성 방식에 따라 프로토타입이 결정되고 [[prototype]]에 저장
- 객체와 프로토타입과 생성자 함수는 서로 연결
const Hello = function(name) {
this.name = name;
}
속성과 메소드들은 각 객체 인스턴스가 아니라 객체 생성자의 prototype 속성에 정의되어 있음
위의 사진을 보면 Hello에 정의한 name 멤버 외에 프로토 타입 객체인 Object의 다른 멤버들도 존재함을 알 수 있음
이렇게 자바스크립트의 모든 객체는 자신의 부모 역할을 하는 객체와 연결되어있고 이 부모 객체를 프로토타입이라고 하는 것
이런 방식으로 클래스를 상속하여 사용하는 것 같이 객체 지향 프로그래밍 방식을 사용할 수 있음
생성되는 모든 객체는 이런 프토토타입 객체에 접근할 수 있고 동적으로 런타임 시에 멤버를 추가할 수도 있음
prototype에 접근하기
프토토타입에 접근하기 위해 __proto__(deprecated)를 인스턴스에 사용하거나 Object.getPrototypeOf(instance)를 사용할 수 있음
function Hello(name) {
this.name = name;
}
const hello = new Hello('hello');
Object.getPrototypeOf(hello) === Hello.prototype; // true
네이티브 객체 프로토타입
자바스크립트의 Array, String 등의 내장 객체 역시 자신의 프로토타입을 가지고 있음
배열을 선언해 늘 사용하던 map, filter 등을 사용해 조작하고 문자열을 선언해 split 등의 연산을 할 수 있게 해주는 이유
const arr = [1,2,3,4,5];
console.log(Object.getPrototypeOf(arr)) //Array prototype
const str = "Hello world!";
console.log(Object.getPrototypeOf(str)) //String prototype
const date = new Date();
console.log(Object.getPrototypeOf(date)) //Date prototype
▶ 프로토타입 체인
모든 객체들은 메소드와 속성을 상속받기 위한 명세로 프로토타입 객체를 가짐
이는 프로토타입 객체도 또 다시 상위 프로토타입 객체로부터 상속받을 수 있고 그 상위도 마찬가지인데 이를 프로토타입 체인이라고 함(다른 객체에 정의된 메소드와 속성을 한 객체에서 사용할 수 있게 해줌)
객체 자신의 것 뿐 아니라 [[Prototype]]가 가리키는 링크를 따라 부모 역할을 하는 모든 프로토 타입 객체의 속성이나 메소드에 접근할 수 있음
특정 객체에서 특정 속성이나 메소드에 접근할 때 자바스크립트 엔진에서 먼저 객체 본인이 가진 것인지 파악하고 없는 경우 프로토타입 체이닝이 일어나며 부모 역할이 되는 상위 객체를 향해 속성이나 메소드를 탐색해 나가고 존재하는지 찾다가 마지막에는 Object.prototype까지 찾음
본질적으로 모든 프로토타입이 객체이기 때문에 다른 모든 프로토타입에서도 발생하며 최상위의 Object.prototype에서도 찾지 못하게 된다면 undefined를 리턴
모든 속성, 메소드가 상속되지는 않는 이유
상속받는 멤버들은 prototype 속성에 정의되어있고 이들만 상속됨
Object.prototype.로 시작하는 속성들을 말하며 생성자로 생성되는 인스턴스 뿐 아니라
Object.prototype을 상속받는 객체라면 접근할 수 있음
배열을 선언하고 push pop 등의 메서드를 사용할 수 있음(Array.prototype의 메서드이기 때문)
const a = [];
a.isArray(); // Uncaught TypeError: a.isArray is not a function
프로토타입에 정의되지 않은 멤버들은 상속되지 않기 때문에 직접 사용할 수 없으며 Array 전역 객체를 이용해 직접 접근할 수 밖에 없음
오버라딩과 프로퍼티 섀도잉
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(`Hey! My name is ${this.name}`);
};
// 인스턴스 메서드가 호출된다. 프로토타입 메서드는 인스턴스 메서드에 의해 가려짐
me.sayHello(); // Hey! My name is Lee
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(`Hey! My name is ${this.name}`);
};
me.sayHello(); // Hey! My name is Lee
프로토 타입 프로퍼티와 같은 이름의 프로퍼티를 인스턴스에 추가하면 프로토타입 체인을 따라 프로토타입 프로퍼티를 검색하여 프로토타입 프로퍼티를 덮어쓰는 것이 아니라 인스턴스 프로퍼티로 추가
이때 인스턴스 메서드 sayHello는 프로토타입 메서드 sayHello를 오버라이딩했고, 프로토타입 메서드 sayHello는 가려짐
이처럼 상속 관계에 의해 프로퍼티가 가려지는 현상을 프로퍼티 섀도잉이라고 함
- 오버라이딩(overriding): 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의하여 사용하는 방식
하위 객체를 통해 프로토타입의 프로퍼티를 변경/삭제하는 것은 불가능
프로토타입의 프로퍼티를 변경/삭제하여면 프로토타입에 직접 접근해야 함
// 프로토타입 메서드 변경
Person.prototype.sayHello = function () {
console.log(`Hey! My name is ${this.name}`);
};
me.sayHello(); // Hey! My name is Lee
// 프로토타입 메서드 삭제
delete Person.prototype.sayHello;
me.sayHello(); // TypeError: me.sayHello is not a function
서브클래싱(Subclassing)
서브클래싱이란 기존 클래스를 확장하여 새로운 하위 클래스를 만드는 과정
새로운 클래스는 상위 클래스의 모든 속성과 메서드를 상속받아 사용할 수 있고 추가적인 속성과 메서드를 정의할 수 있음
상속은 자식 클래스(서브 클래스)에서 부모클래스(슈퍼 클래스)의 속성과 동작 등의 기능을 사용할 수 있도록 해줌
상속을 받는 자식 클래스의 경우 부모의 소스 코드를 복사할 필요 없이 부모 클래스의 프로퍼티와 메서드를 모두 사용할 수 있을 뿐 아니라 필요한 기능을 추가해 확장할 수 있음
상속은 코드의 재사용성을 높이기, 계층 구조를 포현하여 객체 간의 관계를 명확하게 표현하게 해줌
또한 같은 메서드를 호출하더라도 객체의 실제 타입에 따라 다른 동작을 하도록 하므로 프로그램의 유연성을 높이고 객체 간의 관계를 더욱 유연하게 다룰 수 있게 함
ES5에서의 서브클래싱은 기본적으로 프로토 타입 체인과 생성자 함수를 이용하여 구현됨
// 부모 클래스 생성자 함수
function Animal(name) {
this.name = name;
}
// 부모 클래스의 메서드를 프로토타입에 추가
Animal.prototype.sayName = function() {
console.log('My name is ' + this.name);
};
// 자식 클래스 생성자 함수
function Dog(name, breed) {
// 부모 클래스 생성자 함수 호출 및 상속
Animal.call(this, name);
this.breed = breed;
}
// 자식 클래스의 프로토타입을 부모 클래스의 인스턴스로 설정하여 상속
Dog.prototype = Object.create(Animal.prototype);
// 자식 클래스의 프로토타입 생성자를 자식 클래스로 설정
Dog.prototype.constructor = Dog;
// 자식 클래스의 메서드 추가
Dog.prototype.bark = function() {
console.log('Woof! Woof!');
};
// 인스턴스 생성
var myDog = new Dog('Buddy', 'Golden Retriever');
// 부모 클래스의 메서드 호출
myDog.sayName(); // "My name is Buddy"
// 자식 클래스의 메서드 호출
myDog.bark(); // "Woof! Woof!"
function Animal(name) {
this.name = name;
}
Animal.prototype.sayName = function() {
console.log('My name is ' + this.name);
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log('Woof! Woof!');
};
var myDog = new Dog('Buddy', 'Golden Retriever');
myDog.sayName(); // "My name is Buddy"
myDog.bark(); // "Woof! Woof!"
기존에는 클래스(class)가 없었기 때문에 따로 프로토타입을 지정해서 써주어야 했지만 ES6에서 클래스를 도입하여 자바스크립트의 객체 지향 프로그래밍을 더욱 간편하게 만듬
class Person {
// 생성자
constructor(name) {
// 인스턴스 생성 및 초기화
this.name = name;
this.greet = function() {
console.log(`Hi, I am ${this.name}`);
};
}
// 프로토타입 메소드
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
}
// 인스턴스 생성
const person = new Person('Ella');
// 인스턴스 메서드 호출
person.greet(); // "Hi, I am Ella"
// 프로토타입 메소드 호출
person.sayHello(); // "Hello, my name is Ella"
ES6 상속 시에는 클래스로부터 extends 키워드를 통해 상속을 받음
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
}
// Student 클래스는 Person 클래스를 상속받음
class Student extends Person {
constructor(name) {
// 부모 클래스의 생성자 호출
super(name);
}
// 자식 클래스에서 새로운 메서드를 추가
study() {
console.log(`${this.name} is studying in grade ${this.grade}`);
}
}
// Student 클래스의 인스턴스 생성
const student = new Student('John', 10);
// 부모 클래스의 메서드 호출
student.sayHello(); // "Hello, my name is John"
// 자식 클래스의 메서드 호출
student.study(); // "John is studying in grade 10"
자식 클래스에서 super 함수는 부모 클래스를 의미하며, 부모 클래스의 생성자에 인수를 전달
공통되는 속성은 부모 클래스의 것을 사용하고, 공통되지 않는 속성은 자식 클래스에 따로 선언
참고
[ 모던 자바스크립트 Deep Dive ] 17장 : 생성자 함수에 의한 객체 생성
[Javascript] Prototype 기본 이해하기
'JS' 카테고리의 다른 글
템플릿 리터럴(template literal) (1) | 2024.06.11 |
---|---|
Symbol (1) | 2024.06.11 |
Call Stack (0) | 2024.06.10 |
실행 컨텍스트(Execution Context) (1) | 2024.06.10 |
Scope & Closure (Function Scope, Lexical Scope) (0) | 2024.06.07 |
댓글