PracticeEveryday
JavaScript 본문
프로토타입 기반 객체지향 프로그래밍
- 이러한 프로토타입 이론은 그대로 프로토타입 기반 객체 지향 프로그래밍 언어를 통해 구현되었다.
- 1980 _ 1990년대에 이 이론을 토대로 많은 프로토타입 기반 언어가 생겼는데 대표적으로 JavaScript의 모태인
Self 언어와 가족 유사성 (Family Resemblance )을 언어 차원에서 완벽하게 구사한 Kevo가 있다.
prototype 기반 언어 OOP 언어의 특징
1. 개별 객체 ( Instance ) 수준에서 메소드와 변수를 추가
2. 객체 생성은 일반적으로 복사를 통해 이루어짐
3. 확장 ( Extendes )은 클래스가 아니라 위임 ( Delegation )
=> 현재 객체가 메시지에 반응하지 못할 때 다른 객체로 메시지를 전달 할 수 있어 상송의 본질을 지원함.
4. 개별 객체 수준에서 객체를 수정하고 발전시키는 능력은 선엄적 분류의 필요성을 줄이고
반복적인 프로그래밍 및 디자인 스타일을 장려
5. 프로토타입 프로그래밍은 일반적으로 분류하지 않고 유사성을 활용하도록 선택
6. 결과적으로 설계는 맥락에 의해 평가
// 프로토타입 언어에서는 '분류'를 우선하지 않는다. 생성된 객체 위주로 유사성을 정의한다.
// 어휘 쓰임새는 맥락 ( Context )에 의해 평가된다.
=> 실행 컨택스트, 스코프 체인이 여기서 파생되었다.
=> 클로저, this, 호이스트 등 이 모든 어려움이 프로토타입 '맥락'을 표현하기 위한 것이다.
JavaSciprt - Code
function 참새(){
this.날개갯수 = 2;
this.날수있나 = true;
}
const 참새1 = new 참새();
console.log("참새의 날개 갯수 : ", 참새1.날개갯수); // 2
function 닭(){
this.벼슬 = true;
}
닭.prototype = 참새1; // reference(오른쪽이 인스턴스인 점 주목)
// 닭의 프로토타입 객체는 참새의 __proto__가 가리키는 Prototype 객체를 가리키게 된다.
const 닭1 = new 닭();
console.log("닭1 날개 : ", 닭1.날개갯수, ", 날수있나? ", 닭1.날수있나); // 2, true
닭1.날수있나 = false;
console.log("다시 물어본다. 닭1은 날 수 있나? :", 닭1.날수있나); // false
// 아래는 고전적인 방식의 프로토타입 연결
function 펭귄(){
참새.call(this); // copy properties
}
펭귄.prototype = Object.create(참새.prototype); // 프로토타입 연결
console.log(펭귄.prototype === 참새.prototype) false
const 펭귄1 = new 펭귄();
console.log("펭귄1 날개 : ", 펭귄1.날개갯수, ", 날수있나? ", 펭귄1.날수있나); // 2, true
펭귄1.날수있나 = false;
console.log("다시 물어본다. 펭귄1은 날 수 있나? :", 펭귄1.날수있나); // false
1. 날개가 2개, 날 수 있는 참개 1이 있습니다.
2. 참새1을 프로토타입으로 갖는 닭1이 생겼습니다.
여기서 주목할 점은 오른쪽이 참새(함수)가 아니라 참새1(인스턴스)인 점이니다.
프로토타입이론은 이미 존재하는 사물을 통해 범주화 한다는 점에서 일치합니다.
// 참새는 새의 범주로 대표할 만한 전형적인 녀석임 이 참새를 통해 비슷한 아이들을 기준으로 파생됨!!
3. 닭의 정의에는 날개갯수가 없지만 2가 출력됩니다. 프로토타입 체인에 의해 참새 1의 속성에 접근했기 때문입니다.
4. 닭1은 날 수 없다고 합니다. 닭 1은 날수 없어도 프로토타입에 해당하는 참새 1은 날 수 있습니다.
=> 닭1은 참새 1 프로토타입에서 멀어졌습니다.
같은 속성을 변경해도 프로토타입 객체의 속성은 변경되지 않았습니다.
5. 고전적인 방식으로 프로토타입을 사용해봤습니다. 프로토타입에선 객체 생성을 통해 확장한다는 부분이 좀 더
직관적으로 다가옵니다.
닭 1의 원형은 ( 프로토타입 ) 참새1이다.
닭 1에 없는 속성 ( 날개갯수 )는 프로토타입 체인을 통해 참조된다.
닭 1에 동일한 속성명 ( 날 수 있나)를 추가해도 원형은 변하지 않는다. ==> 위임
원리적으로 닭 1을 통해 Prototype을 변경하는 건 불가능 해야한다.
하지만 JS에선 문법적으론 가능 -> 객체의 __proto__ 프로퍼티를 통해 접근 가능하다.!!
자바스크립트 -- 어휘적 범위 ( Lexical Scope )
- 의미 사용 이론에 따르면 단어의 의미는 그 어휘적인, 근처의 환경에서의 의미가 된다.
=> 이는 JavaScript에 다음처럼 적용된다.
" 변수의 의미는 그 어휘적인 ( Lexical ) 실행 문맥 ( Execution Context )에서의 의마가 된다 "
=> 그렇기 때문에 동일 범위 ( 실행 문맥 )의 모든 선언을 참고 ( 호이스팅 )해 의미를 정의한다.
- 호이스팅의 일반적인 대답 : '코드가 로드될 때 선언 부가 끌어올려지는 '
- 이 때까지 가장 훌륭한 대답 : '실행 컨텍스트 생성시 렉시컬 스코프 내의 선언이 끌어올려 지는 게 호이스팅이다'
- 프로토타입 기반 언어인 자바스크립트에서는 '단어의 의미가 사용되는 근처 환경' 에서의 '근처'를
어휘적인 범위 ( Lexical Scope )로 정의했다.
- 자바스크립트 엔진은 코드가 로드 될 때 실행 컨텍스트를 생성하고 그 안에 선언된 변수, 함수를 실행 컨텍스트
최상단으로 호이스팅한다. 이러한 범위를 렉시컬 스코프( Lexical Scope )라고 한다!!
// 전역 실행문맥 생성. 전체 정의(name, init) 호이스팅
var name = 'Kai';
init(); // init 실행문맥 생성. 내부 정의(name, displayName) 호이스팅
function init() {
var name = "Steve";
function displayName() {
console.log(name); // 현재 실행문맥 내에 정의된게 없으니 outer 로 chain
// var name = 'troll?'; // 주석 해제되면 호이스팅
}
displayName(); // displayName 실행문맥 생성. 내부 정의 호이스팅.
}
1. 코드가 로드될 때 전역 실행문맥 ( Execution Scope )가 생성된다.
전역의 선언부를 모두 호이스팅 하게 되는 데 여기선 name과 init이 렉시컬 스코프에 들어가게 된다.
2. 렉시컬 스코프 상의 init 함수가 존재하니 에러 없이 실행 할 수 있다. 코드 로딩 시점에 init 함수를 타고
들어가 실행 문맥을 생성한다.
3. init 함수에 대해 렉시컬 스코프를 생성한다. name과 displayName이 들어오게 된다.
4. displayName 실행 문맥 내에 name이라 선언 된 것이 없다. 이럴 땐 Scope Chain에 의해 상위 실행 컨텍스트로
위임 된다.
// 아래와 같은 구조가 생성된다.
- Global Execution // 1
- Lexical : name, init
- Execution : init // 2
- Lexical : name, displayName
- Outer : global
- Execution : displayName // 3
- Lexical : null
- Outer : init
- 로쉬의 프로토타입 모델과 비슷한데 자바스크립트에서 스코프 체인, 프로토타입 체인 모두 이 그림으로 표현된다.
- 가장 바깥 원에서부터 안쪽 원으로 Scope Chain으로 하게 된다!!
- 여기서 중요한 것은 자바스크립트의 동작 방식보다는, 프로토타입 언어인 자바스크립트에서
" 도대체 왜 ' 실행 문맥 ' , ' 렉시컬 스코프 ' , ' 호이스팅 '이 존재하는 가" 이다.
' 왜 ' 를 이해했다면 이 부분은 더이상 암기 과목이 아니다.
프로토타입 철학의 근원인 비트겐슈타인류에서 가장 중요하게 생각하는 것은 바로 ' 어휘 '이고 이것은
' 문맥 ( Context ) ' 내에서만 의미를 가진다는 것이 핵심이다.
이 핵심을 자바스크립트에서 구현하기 위해 자연스럽게 발생한 특징임을 이해한다면 더 이상 외울 필요가 없다!!
자바스크립트 This!!
- 자바스크립트의 또 다른 특징은 ' this ' 이다.
- 잘 작성된 기술 문서도 대부분 Case By Case로 this가 가리키는 객체를 설명하는 방식이었다.
this 에단 대표적인 오해들
this는 기본적으로 Window이다 ( X )
이벤트 리스너에서 등록한 콜백의 this는 내부에서 bind를 통해 바뀌기 때문에 무엇인지 알 수 없다 ( X )
this는 외워야 한다 ( X )
- 이런 오해를 바로잡기 위해 먼저 프로토타입 철학에서 이런 상황을 어떻게 해석했는지 알 필요가 있다.
- 비트겐슈타인은 ' 철학적 탐구 '에서 단어의 쓰임새가 곧 의미라는 점을 강조했습니다 === 의미사용이론
그는 이를 ' 발화 '라고 이야기했는데 아래의 예시처럼 ' 벽돌 ! '이라고 크게 외칠 떄 이 ' 벽돌 ! '이 어디서
' 발화 ' 되었느냐에 따라서 단어의 의미가 달라진다는 것이다.
누군가 벽돌! 이라고 외쳤을 때 상황마다 그 의미가 달라지게 된다.
1. 벽돌이 필요할 때 => 벽돌을 달라!
2. 벽돌로 보수해야 할 때 => 벽돌을 채우라
3. 벽돌이 떨어질 때 => 벽돌을 피해라!
=> 받아들이는 ' 대상 ' 에 따라서 같은 단어도 의미가 달라진다는 이야기이다!!
- 이것이 바로 프로토타입과 클래스의 대표적인 차이라고 볼 수 있다.
- 전혀 다르게 단어를 보는 방식으로 중요한 세계관의 차이이다.
미리 분류( Classification ) 하고 정의한 클래스를 가장 중요하게 여기는 전통적인 방식과 달리, 프로토타입에서는
받아들이는 주체와 문맥이 가장 중요한 것이다. 프로그래밍으로 보자면 실행 ( Invoke ) 하는 ' 객체 ' 가 가장
중요하다는 것이다!!
- 이것이 바로 프로토타입 기반 언어인 자바스크립트에서 this가 클래스 기반 언어들과 다르게 동작하는 이유이다.
- 프로토타입 기반 언어에서는 this가 정의된 함수가 어떻게 발화 ( Invoke ) 했는지에 따라 가리키는 값이 달라진다.
정확하는 받아들이는 대상의 실행컨텍스트를 가리킨다!!
- 이를 이해하기 위해서 메소드와 메시지를 명확하게 알아야 한다!!
메소드 : 객체의 함수 // method : 방법 방식
메시지 : 메소드를 실행하려는 메시지 전달 // message : 전언 전달하는 언어
- Java에서는 클래스의 메소드를 호출하는 행위를 메시지라고 한다.
JavaScript 개발자에겐 메시지의 용어가 익숙하지 않 을 수 있는데 메시지를 이해하는 것이 아주 중요하다.
Ex 자바스크립트를 예로 들면 foo라는 객체가 있고 그 내부에 bar()라는 함수가 있을 때 다음 처럼
발화 ( invoke )할 객체를 지정할 수 있다.
foo.bar()
bar.call(foo)
var boundBar = bar.bind(foo)
- 위처럼 foo 객체를 통해 발화한 함수는 내부 this가 무조건 foo를 가리킵니다.
만약 아무것도 지정되어 있지 않으면 글로벌 ( 브라우저라면 window )를 가리킨다.
var someValue = 'hello';
function outerFunc() {
console.log(this.someValue); // 첫번째 : ?, 두번째 : ?
this.innerFunc();
}
const obj = {
someValue : 'world',
outerFunc,
innerFunc : function() {
console.log("innerFunc's this : ", this); // 첫번째 : ?, 두번째 : ?
}
}
obj.outerFunc(); // 첫번째 : world
outerFunc(); // 두번째 : hello
1. 첫번 째 outerFunc()는 world가 두번 째 outerFunc()는 hello가 찍히게 된다. outerFunc가 누구를 통해 발화 했는지
알면 this가 무엇이 될 지 알 수 있다.
2. obj통해 발화되면 innerFunc가 존재하기 때문에 호출 되지만, 글로벌에서 발화되면 innerFunc가 없기 때문에 에러가
나게 된다.
3. this가 이중으로 들어가 있어 헷갈릴 수 있는데 복잡하지 않다.
this(obj)를 통해 발화했기 때문에 첫번째는 obj가 된다.
1. 자바스크립트 엔진이 코드를 실행한다. (브라우저에서 use strict 모드가 아닌 경우 this는 window를 가리킨다)
2. obj.outerFunc()를 실행하라는 코드를 만나면 자바스크립트 엔진은 obj에게 outerFunc를 실행하라는 메시지를 보낸다.
3. obj에서 outerFunc를 발화한다. 코드 로드 시 만들어져있는 실행문맥을 참고해 실행하게 되는데 이 때 실행 문맥상의
this는 발화한 obj를 가리키게 된다.
4. outerFunc를 실행중에 this.innerFunc를 만나게 되는데 엔진은 this가 가리키는 obj 속에 있는 innerFunc를 실행하라는
메시지를 보내게 된다
5. obj에 innerFunc가 선언되어 있으니 잘 실행된다.
1. obj.outerFunc가 아니라 outerFunc를 만나게 되었을 때 엔진은 자신 ( global )의 실행 문맥상에 존재하는 outerFunc를
호출하게 된다.
2. 발화한 지점이 엔진 ( global ) 이니 this는 엔진을 가리킨다. 엔진에 innerFunc를 실행하라는 메시지를 보낸다.
3. 글로벌 실행문맥에는 innerFunc가 없기 때문에 에러가 난다.
마무리
- 프로토타입은 '클래스'의 다른 구현이 아닌, 완전히 새로운 인식하에 만들어진 이론입니다.
이러한 차이점을 이해한다면 더 이상 JS의 프로토타입, 호이스팅, this는 암기 과목이 아니게 됩니다.
- 최근 자바스크립트 스펙에서 class, arrow function, let, const 등 여타 일반적인 언어와 보편성을 맞추려는 시도도
많고, 이것들을 정말 편하게 사용중이지만 근본 ( Prototype )은 변하지 않는 다는 것을 알아야 한다.
또한 이것들이 언어적 지원이 아닌 Syntactic Suger인 부분도 언어의 근본 구조 ( Prototype )가 다르기 때문임을
이해할 수 있어야 한다.
- 지금도 많은 구루들이 JS의 디자인 철학 ( 프로토타입 )을 해치지 말자는 주장을 하고 있다.
이런 흐름들을 이해하기 위해선 언어의 디자인 철학을 이해할 필요가 있다!!
자바스크립트는 왜 프로토타입을 선택했을까
프로토타입으로 검색하면 으레 나오는 서두처럼 저 또한 자바스크립트를 처음 접했을 때 가장 당황스러웠던 게 프로토타입이었습니다.
medium.com
'JavaScript' 카테고리의 다른 글
JavaScript (0) | 2022.05.18 |
---|---|
JavaScript (0) | 2022.05.18 |
JavaScript (0) | 2022.05.17 |
JavaScript (0) | 2022.05.15 |
JavaScript (0) | 2022.05.13 |