모든 객체는 자신의 부모 역할을 하는 프로토타입 객체의 참조 링크를 가지고 있으며, 이 링크를 통해 프로토타입 객체로부터 프로퍼티나 메서드를 상속받을 수 있다.
객체의 프로토타입 객체는 참조 링크 형태로 [[Prototype]] 라는 숨겨진 내부 프로퍼티에 저장된다.
참조 링크 형태로 저장되기 때문에 동일한 프로토타입 객체를 상속받은 객체는 모두 같은 프로퍼티와 메서드를 공유한다.
[[Prototype]]은 자바스크립트 엔진 내부에서만 사용하는 숨겨진 프로퍼티이지만 크롬, 파이어 폭스 같은 모던 브라우저에서 _proto_ 프로퍼티로 접근할 수 있다.
하지만 표준 명세가 아니므로 가급적 사용하지 말자.
그럼에도 불구하고 사용하고 싶다면 _proto_ 프로퍼티 대신에 Object.getPrototypeOf()
를 사용하자.
get이 있으면 set도 있을 것이라고 추측할 수도 있다.
맞다.Object.setPrototypeOf()
메서드도 존재한다.
Object.setPrototypeOf()
는 프로토타입 객체를 설정하는 메서드이다.
프로토타입 체인은 상위 프로토타입 객체와 연쇄적으로 연결된 구조를 의미한다.
그리고 프로퍼티나 메서드에 접근하기 위해 이 연결 구조를 따라 차례대로 검색하는 것을 프로토타입 체이닝이라고 한다.
Object.prototype은 프로토타입 체인의 최상위에 있는 프로토타입 객체이다.
모든 객체가 가진 프로토타입 체인의 종점은 Object.prototype이다.
Object.prototype의 프로토타입 객체는 null
이다.
객체의 프로토타입 그 객체는 객체가 생성되는 시점에 설정된다.
또한 객체 리터럴로 생성한 모든 객체는 최상위 프로토타입 객체인 Object.prototype을 프로토타입 객체로 설정한다.
var o = {a: 1};
// o ---> Object.prototype ---> null
배열 객체는 프로토타입으로 Array.prototype이란 고유의 객체가 설정된다.
여기에 배열 내장 메서드들이 정의되어 있다.
Array.prototype 객체 또한 프로토타입 객체를 갖는데 바로 최상위 프로토타입 객체인 Object.prototype이다.
var a = ["yo", "whadup", "?"];
// a ---> Array.prototype ---> Object.prototype ---> null
배열 외에도 랩퍼 객체, 함수, 정규식과 같은 내장 프로토타입 객체들이 있다.
그렇기 때문에 다양한 메서드나 프로퍼티들을 사용할 수 있는 것이다.
모든 함수에는 prototype ( [[Prototype]] 과는 다르다 ) 이라는 특별한 프로퍼티가 존재한다.
일반적인 함수에서는 쓸모없지만 new
키워드와 함께 생성자 함수로 사용할 경우에는 특별한 역할을 한다.
생성자 함수로 생성된 객체의 프로토타입 객체로는 "생성자 함수의 prototype 프로퍼티"가 설정된다.
function Graph() {
this.vertexes = [];
this.edges = [];
}
Graph.prototype = {
addVertex: function(v){
this.vertexes.push(v);
}
};
var g = new Graph();
// g 'vertexes' 와 'edges'를 속성으로 가지는 객체이다.
// 생성시 g.[[Prototype]]은 Graph.prototype의 값과 같은 값을 가진다.
함수의 prototype 프로퍼티는 constructor 프로퍼티 하나만 가진 객체이다.
constructor 프로퍼티는 자신과 연결된 생성자 함수를 가리키며, 이 프로퍼티를 통해 객체가 어떤 생성자 함수를 통해 생성되었는지 알 수 있다.
즉, 생성자 함수와 생성자 함수의 prototype 프로퍼티는 서로 상호참조하는 관계이다.
객체의 부모가 되는 프로토타입 객체에 메서드나 프로퍼티를 추가하고 싶으면 일반 객체처럼 동적으로 프로퍼티나 메서드를 추가 및 삭제 할 수 있다.
단, 객체가 생성된 이후에 프로토타입 객체의 프로퍼티를 수정하는 것은 지양해야 한다.
Array.prototype이나 Object.prototype과 같은 내장 프로토타입 객체 역시 수정이 가능하지만 절대 건들지 말자.
생성된 모든 객체들과 다른 모듈까지 영향을 줄 수 있다.
function Vehicle(){
console.log("initialize Vehicle");
}
Vehicle.prototype.run = function(){
console.log('run');
}
Vehicle.prototype.stop = function(){
console.log('stop');
}
function Car(type){
this.type = type;
}
function inherit(parent, child){
// 임시 생성자 함수를 만든다.
function F(){}
/*
부모 생성자 함수의 프로토타입 프로퍼티를
임시 생성자 함수의 프로토타입 프로퍼티로 설정한다.
이제 new 키워드를 사용하여 임시 생성자 함수를 호출하면
프로토타입 객체로 F.prototype ( = parent.prototype) 를 가지는 새 객체가 생성된다.
*/
F.prototype = parent.prototype;
/*
F.prototype을 프로토타입 객체로 갖는 새 객체를 만들고
그 객체를 child 생성자 함수의 프로토타입 프로퍼티로 설정한다.
따라서 이제 new 키워드를 사용해 child 생성자 함수를 호출하면 다음과 같이 설정된다.
*/
child.prototype = new F();
child.prototype.constructor = child;
}
inherit(Vehicle, Car);
console.log(new Car("SUV"));
중간에 빈 함수를 징검다리로 삼아 부모와 자식 생성자 함수를 연결했다.
징검다리 없이 프로토타입 체인을 설정하고 싶다면 아래와 같이 구성하면 된다.
const vehicle = new Vehicle();
Car.prototype = vehicle;
Car() 생성자 함수의 prototype 프로퍼티의 상위 프로토타입으로써 Vehicle() 생성자 함수의 prototype 프로퍼티를 찾을 수 있어야 한다.
위의 경우에도 프로토타입 체이닝에 따라 Vehicle() 생성자 함수의 prototype 프로퍼티를 찾을 수 있다.
하지만 이 경우, 문제가 있다.
vehicle.myProperty = "myProperty";
코드처럼 vehicle 객체에 vehicle.myProperty
와 같은 프로퍼티를 설정한다면 필요 없는 프로퍼티까지 상속을 받게 된다.
우리가 상속받고 싶은 것은 Vehicle.prototype
에 정의된 프로퍼티나 메서드이지 특정 vehicle 객체의 프로퍼티나 메서드가 아니다.
이러한 문제를 방지하기 위해 임시 생성자 함수 F() 를 사용하여 부모 클래스의 인스턴스와 자식 클래스의 인스턴스를 독립적으로 만들어 사용하는 것이다.
생성자 빌려쓰기
아직 한 가지 문제가 남아있다.
Car 클래스의 인스턴스를 생성할 때 부모 클래스인 Vehicle() 생성자 함수가 호출되지 않는 것이다.
이 문제는 Car() 생성자 함수에서 apply() 메서드를 사용하여 해결할 수 있다.
function Car(type){
Vehicle.apply(this, arguments);
this.type = type;
}
이러한 방식으로 자식 클래스의 인스턴스를 생성할 때 부모 클래스의 생성자를 호출하는 것을 생성자 빌려쓰기라고 한다.
ECMAScript 5는 새로운 방법을 도입했다.
Object.create라는 메소드를 호출하여 새로운 객체를 만들 수 있다.
생성된 객체의 프로토타입은 이 메소드의 첫 번째 인수로 지정된다.
var a = {a: 1};
// a ---> Object.prototype ---> null
var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (상속됨)
var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null
var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined이다. 왜냐하면 d는 Object.prototype을 상속받지 않기 때문이다.
자바스크립트의 클래스와 상속은 생성자 함수와 프로토타입 프로퍼티 또는 Object.create() 등을 사용하여 구현할 수 있다.
하지만 이런 방식은 직관적이지가 않다.
따라서 class
키워드를 이용한 새로운 문법이 ES2015때 등장했다.
프로토타입을 이용한다는 원리는 이전과 같으나 문법적 설탕으로 좀 더 세련되게 클래스와 상속을 구현할 수 있게 해준다.
class Vehicle {
constructor(){
console.log('initialize Vehicle');
}
run(){
console.log("run");
}
stop(){
console.log('stop!');
}
}
console.log(new Vehicle());
class Vehicle {
constructor(){
console.log('initialize Vehicle');
}
run(){
console.log("run");
}
stop(){
console.log('stop!');
}
}
class Car extends Vehicle{
constructor(type){
super(); // 반드시 this를 사용하기 전에 super()를 먼저 호출해야 한다. 아니면 오류발생
this.type = type;
}
}
console.log(new Car("SUV"));
class 문법에서는 static
키워드를 이용해서 정적 메서드 및를 정의할 수 도 있다.
class Vehicle {
constructor(){
console.log('initialize Vehicle');
}
run(){
console.log("run");
}
stop(){
console.log('stop!');
}
}
class Car extends Vehicle{
constructor(type){
super();
this.type = type;
}
static field = "field"; // 정적 필드
static CreateSUV(){ // 정적 메서드, 팩토리 함수를 정의할 때 많이 사용한다.
return new Car("SUV");
}
}
console.log(new Car("SUV"));
console.log(Car.field);
단, static 메서드에서만 static 변수들을 호출할 수 있다는 제약이 있다.
private 접근 제한자
문법은 다음과 같다.
class ClassWithPrivate {
#privateField;
#privateFieldWithInitializer = 42;
#privateMethod() {
// …
}
static #privateStaticField;
static #privateStaticFieldWithInitializer = 42;
static #privateStaticMethod() {
// …
}
}
Private instance fields를 만들고자 한다면 아래와 같이 한다.
class BaseClass {
#PRIVATE_FIELD;
constructor(){
this.#PRIVATE_FIELD = 'field';
}
getField(){
return this.#PRIVATE_FIELD;
}
}
console.log(new BaseClass().getField())
함수 스코프 : var
블록 스코프 : let, const
프로그래밍 언어의 스코프는 동적 스코프와 렉시컬 스코프 두 가지 방식으로 동작한다.
동적 스코프는 런타임 중 함수의 호출에 의해 결정되고, 렉시컬 스코프는 변수나 함수를 어디에 작성하였는가에 기초하여 결정된다.
자바스크립트는 렉시컬 스코프를 기반으로 동작하는 언어이지만 this 바인딩만 함수를 호출하는 방법에 따라 동적으로 달라진다.
function foo(){
var a = 1;
function bar(b){
console.log(a, b);
}
}
console.log() 메서드에서 참조하고 있는 변수 a를 스코프에서 어떻게 검색하는지 단계별로 보자.
bar() 함수의 스코프부터 검색을 시작한다.
bar() 함수의 스코프에서 찾을 수 없으므로 가장 가까운 상위 스코프인 foo() 함수 스코프로 올라가 검색한다.
발견, 검색 중단
이러한 스코프들의 연결 관계를 스코프 체인 이라 하며 스코프 체인을 따라 검색하는 과정을 스코프 체이닝 이라 한다.
선언문이 스코프 내의 가장 최상단으로 끌어올려지는 것을 의미한다.
console.log(a); // undefined
var a = 1
참조에러가 발생할 것 같지만 호이스팅 때문에 undefined가 출력된다.
자바스크립트의 변수는 세 가지 단계로 나누어 생성된다.
선언 : 스코프에 변수를 선언한다.
초기화 : 변수의 값을 undefined로 초기화하며, 실제로 변수에 접근 가능한 단계이다.
할당 : 변수에 실제 값을 할당한다.
var
키워드로 선언한 변수는 선언과 초기화 단계를 한 번에 실행한다.
그리고 이 두 단계는 스코프의 최상단으로 끌어올려져 실행된다.
따라서 선언하기 전에 변수에 접근하여도 이미 초기화가 되어 접근이 가능한 것이다.
앞의 코드는 아래와 같이 처리된다.
var a;
console.log(a);
a = 1;
function(){
console.log(a);
var a = 1
}
이 코드는 아래와 같이 처리된다.
function(){
var a;
console.log(a);
a = 1
}
let
과 const
키워드로 선언한 변수는 var
키워드와 다르게 선언과 초기화 단계가 분리되어 실행된다.
console.log(a); // 참조 에러
let a = 1;
초기화 단계가 실행하기 전엔 변수에 접근할 수 없다.
// a 변수는 스코프에 선언됨.
console.log(a); // 초기화가 되지 않아서 접근할 수 없다.
let a = 1; // 초기화 및 할당이 실행됨.
선언 단계와 초기화 단계의 중간지점에서는 변수에 접근할 수 없다.
이 구간을 Temporal Dead Zone ( TDZ ) 라고 부른다.
선언 단계가 스코프의 최상단으로 끌어올려지는건 어떻게 알 수 있을까?
다음의 코드를 보자.
let a = 1;
function foo(){
console.log(a);
let a = 2;
}
foo();
만약 선언 단계가 호이스팅 되지 않는다고 가정하자.
foo() 함수 스코프에서 a 변수를 찾을 수 없고 전역 스코프에서 a를 검색할 것이고 1를 출력할 것이다.
하지만 참조 에러가 발생한다.
즉, TDZ 구간에서 지역 변수인 a에 접근하였기 때문에 참조에러가 발생하는 것이다.
함수 선언문 역시 선언문이기 때문에 호이스팅이 발생한다.
다만, 함수 선언문의 호이스팅은 함수 선언, 초기화, 할당 세 가지 단계가 모두 동시에 스코프 최상단에서 실행된다.
단, 함수 선언문이 아닌 함수 표현식은 변수의 호이스팅 규칙에 따라 동작한다.
클로저는 함수의 렉시컬 스코프를 기억하여 함수가 렉시컬 스코프를 벗어난 외부 스코프에서 실행될 때에도 자신의 렉시컬 스코프에 접근할 수 있게 해주는 것이다.
function foo(){
var a = 1;
function bar(){
console.log(a);
}
return bar;
}
const baz1 = foo();
baz1(); // 1
function baz2(){
const fn = foo();
fn(); // 1
}
baz2();
전역 스코프 또는 baz2() 함수의 내부에서 bar() 함수를 호출하여도 클로저에 의해 bar() 함수의 렉시컬 스코프에 접근 할 수 있다.
클로저를 활용하면 모듈을 정의하여 원하는 프로퍼티나 메서드를 캡슐화 할 수 있다.
function myModule(){
let counter = 0;
function increment(){
counter += 1;
}
function decrement(){
counter -= 1;
}
function getCount(){
return counter;
}
return { increment, decrement, getCount };
}
const myCounter = myModule();
myCounter.increment();
console.log(myCounter.getCount()); // 1
ES2015이전에 사용한 방식이다.
이렇게 사용했다는 것만 알아두자.
모듈 생성 함수를 즉시 실행 함수로 하여 변수에 할당해 사용하기도 한다.
자바스크립트 모듈의 진화
ES2015 이전의 자바스크립트에는 모듈이란 개념이 없었다.
자바스크립트는 파일마다 독립적인 스코프를 가즌게 아니라 전역 스코프를 공유하기 때문에 문제가 많았다.
이러한 문제때문에 모듈 패턴을 사용하였고 더 나아가 CommonJS와 AMD라는 ECMAScript 와는 독자적인 명세가 제안되었다.
CommonJS는 초기 명세와는 다르지만 Node.js의 표준으로 여전히 사용되고 있다.
기존의 중구난방이던 모듈 구현 방법을 해결하기 위해 ES2015에서 정식으로 모듈 문법이 등장했다.
하지만 모듈은 IE와 같은 레거시 브라우저에서는 지원하지 않기 때문에 webpack, Babel과 같은 개발 도구를 통해 사용한다.
함수, 변수, 클래스 모두를 내보낼 수 있으며 이들은 반드시 모듈의 최상위 위치에 존재해야 한다.
export
키워드를 사용하여 개별 식별자를 내보내는 것을 named exports라고 부른다.
named exports로 내보낸 식별자들은 import
키워드와 중괄호로 감싸 가져올 수 있다.
또한 모듈이 내보낸 식별자들을 하나의 이름으로 한 번에 가져올 수도 있다.
이 경우 가져오는 모듈의 식별자들은 as
키워드 뒤에 지정한 객체의 프로퍼티로 할당된다.
//a.js
export const a = 1;
export function foo(){}
//b1.js
import {a, foo} from './a.js';
console.log(a);
//b2.js
import * as all from './a.js';
console.log(all.a);
default export는 export 키워드와 default 키워드를 함께 사용하여 식별자를 내보낸다.
또한 named export와는 다르게 모듈에서 하나만 정의할 수 있다.
//a.js
export const a = 1;
export const b = 1;
//b.js
export const a = 2;
export const b = 2;
//c.js
import {a as a1} from './a.js';
import {a as a2} from './b.js';
또는
//a.js
export const a = 1;
export const b = 1;
//b.js
const a = 2;
const b = 2;
export {a as a2, b as b2};
//c.js
import {a} from './a.js';
import {a2} from './b.js';
정의한 모듈을 브라우저 환경에서 사용하기 위해서는 script 태그에 type="module" 속성을 설정하여 모듈임을 명시할 수 있다.
이렇게 정의된 파일은 모듈로 인식되어 전역 스코프를 공유하는 것이 아니라 모듈 스코프로 동작한다.
<script type="module" src="a.mjs"></script>
mjs 확장자는 해당 자바스크립트 파일이 모듈임을 명시하기 위해 사용한다.
[ 참고 ] : 기초부터 완성까지, 프런트엔드 책