본문 바로가기
  • soldonii's devlog
Javascript 공부/Advanced Javascript(-)

자바스크립트에서 클로저란? 프로토타입 상속이란?

by soldonii 2019. 9. 10.

*Udemy의 "Advanced Javascript Concepts"강의에서 학습한 내용을 정리한 포스팅입니다.

*자바스크립트를 배우는 단계라 오류가 있을 수 있습니다. 틀린 내용은 댓글로 말씀해주시면 수정하겠습니다. 감사합니다. :)


1. 클로저(Closure)

자바스크립트에는 클로저라는 독특한 개념이 존재한다. 클로저는 '함수'와 '렉시컬 환경' 두가지 개념의 혼합물이다.

 

function a() {
  let grandpa = 'grandpa';
  return function b() {
    let father = 'father';
    return function c() {
      let son = 'son';
      return `${grandpa} > ${father} > ${son}`;
    }
  }
}
a(); // [Function: b]
b(); // [Function: c]
c(); // 'grandpa > father > son'
a()()(); // 'grandpa > father > son'

 

코드를 보면 함수 a 안에서 함수 b를, 함수 b 안에서 함수 c를 리턴시키고 있다. 그리고 최종적으로 리턴되는 함수 c에서는 함수 a에서 선언된 변수 grandpa, 함수 b에서 선언된 변수 father, 함수 c에서 선언된 변수 son에 접근을 시도하고 있다. 이 경우 함수 c에서는 정상적으로 grandpa와 father 변수에 모두 접근이 가능하다.

 

이처럼 자바스크립트에서는 서로 다른 스코프가 형성됨에도 불구하고, 자식 스코프가 부모 스코프의 변수 환경의 정보를 기억하고 이에 접근할 수 있는데, 이를 클로져라고 한다.

 

# 클로저의 이점 : 메모리 효율성과 은닉화

1. 메모리 효율성

function heavyDuty(idx) {
  const bigArray = new Array(100000).fill('1');
  return bigArray[idx];
}
heavyDuty(688);
heavyDuty(688);
heavyDuty(688); // heavyDuty() 함수를 1억번 실행해야 한다고 가정해보자.

 

이 코드의 경우, 함수가 한 번 실행될 때마다 아주 커다란 배열을 만들고 있다. 만약 heavyDuty()라는 함수를 1억번을 실행해야 한다고 가정해보자. 1억번 동안 길이가 10만인 함수를 만들고, 함수 실행이 종료되면 메모리에서 지우는 과정을 반복하는 것은 언뜻 봐도 비효율적이다.

 

function heavyDuty2(index) {
  const bigArray = new Array(100000).fill('1');
  return function(index) {
    return bigArray[index];
  }
}
const getHeavyDuty = heavyDuty2();
getHeavyDuty(788); // 이 함수를 1억번 실행해도 배열은 1번만 만든다.

 

첫번째 코드를 이렇게 바꿔보았다. heavyDuty2라는 함수 내부에서 길이가 10만인 배열을 생성하지만, 이 함수는 내부적으로 또 다른 함수를 리턴시키고 있다. 그리고 내부적으로 리턴시키는 함수를 변수에 담은 뒤, 해당 변수에 담긴 함수를 실행하면?

heavyDuty2()함수는 gotHeavyDuty 변수에 담을 때 단 한번 실행되고, 그 이후에는 실행되지 않는다. 그럼에도 불구하고 내부적으로 리턴되는 함수의 경우 함수가 선언되었을 당시의 렉시컬 환경을 기억하기 때문에 영원히 bigArray 변수에 대한 정보를 기억하고 있다. 이처럼 클로저를 이용하면 메모리를 효율적으로 사용할 수 있다.

 

2. 은닉화(Encapsulation)

const makeNuclearButton = () => {
  let timeWithoutDestruction = 0;
  const passTime = () => timeWithoutDestruction++;
  const totalPeaceTime = () => timeWithoutDestruction;
  const launch = () => {
    timeWithoutDestruction = -1;
    return '💥'
  }
  setInterval(passTime, 1000);
  
  return {
    // launch: launch, 
    totalPeaceTime: totalPeaceTime
  }
}

const ohno = makeNuclearButton();
ohno.totalPeaceTime();

 

위 함수에서 launch 함수가 실행될 경우, 폭발이 실행되고 시계가 초기화되기 때문에 해당 함수의 사용을 원치 않는다고 가정해보자. 이 경우 makeNuclearButton 함수를 리턴할 때, launch가 리턴할 객체 내에 포함되지 않도록 하여 은닉화를 할 수 있다. 이러한 방식으로 외부에서 launch 함수에 접근할 수 없도록 은닉화를 할 수 있다.


2. 프로토타입과 상속(Prototypal Inheritance)

Inheritance(상속)이란, 하나의 객체가 다른 객체의 프로퍼티 또는 메소드에 접근이 가능한 것을 말한다. 

 

const array = [];
array.__proto__; // [constructor: f ...]

 

위 코드는 빈 배열을 만들게 되는데, 이 배열의 생성은 아주 거대한 Array라는 자바스크립트 언어 자체에 정의되어 있는 생성자 함수가 기반이 된다. 생성된 배열(객체, 함수 등 모두)은 기본적으로 __proto__ 라는 속성을 가지고 있다. 이 __proto__를 통해 생성한 배열의 상위 프로토타입 체인의 prototype 객체로 거슬러 올라갈 수 있다. 만약 한 단계 더 거슬러 올라가면 모든 것의 Base가 되는 Object 객체의 prototype에 접근할 수 있다.

 

__proto__ 키워드를 통해서, 상위 프로토체인에 존재하는 prototype 객체에 접근할 수 있게 되는 것이다.(참고로 prototype 객체 내부에 __proto__가 존재한다.)

__proto__와 prototype 체인

# 상속이란?

let dragon = {
  name: 'Tanya',
  fire: true,
  fight() {
    return 5;
  },
  sing() {
    if (this.fire) {
	    return `I am ${this.name}, the breather of fire`  
    }  
  }
}

let lizard = {
  name: 'Kiki',
  fight() {
    return 1;
  }
}


dragon 객체는 name 뿐 아니라 fire, fight, sing 등의 프로퍼티(메소드)가 지정되어 있는 반면, lizard는 name과 fight만 프로퍼티로 지정되어 있다. 이 때, lizard가 dragon의 능력을 가져다 쓰려면 어떤 방법이 있을까?

우선 전에 배웠던 .bind() 메소드를 이용할 수 있을 것이다. => dragon.sing.bind(lizard); 코드는 dragon 객체의 메소드인 sing을 가져와서 사용하되, 이 때 this값을 lizard로 부여하게 된다. 

하지만 dragon 객체의 많은 다른 능력들고 가져다 쓰려면, .bind()를 이용하는 것은 번거롭다. 이 때 프로토타입 체인 개념을 이용할 수 있다.

 

let dragon = {
  name: 'Tanya',
  fire: true,
  fight() {
    return 5;
  },
  sing() {
    if (this.fire) {
	    return `I am ${this.name}, the breather of fire`  
    }  
  }
}

let lizard = {
  name: 'Kiki',
  fight() {
    return 1;
  }
}

lizard.__proto__ = dragon; // lizard의 상위 프로토타입을 드래곤으로 설정한 것이다. 

 

lizard.__proto__ = dragon; 코드는 lizard 객체의 상위 프로토타입을 dragon으로 설정함으로써, dragon의 프로퍼티를 lizard 객체가 상속받도록 한다. 이 경우 name, fight 같이 lizard에 이미 정의되어 있는 프로퍼티는 상속받지 않지만, 그 외에 sing, fire 같은 lizard에게는 없는 프로퍼티는 상속받게 된다.

 

주의할 점은, dragon 객체의 프로퍼티를 복사해서 lizard 객체에게 붙여넣기 하는 것이 아니다. 즉 lizard 객체가 sing, fire 프로퍼티를 실제로 가지게 되는 것은 아니다. 다만 dragon 객체의 프로퍼티를 상속받도록만 지정한 것이다.

 

따라서 자바스크립트 엔진에서는 다음과 같은 프로세스로 동작한다. 만약 lizard.sing()을 할 경우, 1) lizard 객체에 sing 프로퍼티가 존재하는지를 찾는다. 2) 찾지 못했기 때문에 프로토타입 체인이 형성된 상위 프로토타입에 방문하여(=dragon 객체로 가서) 그 곳에서 sing 프로퍼티가 존재하는지 찾는다. 만약에 dragon 에서도 찾지 못할 경우, dragon의 상위 프로토타입 체인인 Object 생성자 함수 내의 prototype 객체를 찾게 된다.

 

이 방식은 메모리 효율성을 증대시킨다. 예를 들면, dragon 객체가 가지고 있는 property를 prototypal inheritance 개념을 이용하여 lizard 객체가 상속받도록 할 수 있는데, 이 때 상속받는 프로퍼티를 lizard 객체에게 복사하는 것이 아니다. 다만 상속받을 뿐이다. 따라서 lizard 객체가 상속받을 프로퍼티는 dragon 객체 내의 프로퍼티로 메모리 공간 중 한 곳에만 저장되어 있는 것이다. lizard 객체의 메모리 공간에는 저장되지 않는 것이다.

 

다른 예시로, 모든 객체에서 __proto__ 키워드를 통해 상위의 base 객체인 Object 객체의 prototype 객체에 접근할 수 있다. base Object 객체에는 hasOwnProperty, isPrototypeOf 등의 프로퍼티가 존재한다. 이 프로퍼티는 어떤 객체에서도 사용할 수 있는데, 그 이유는 모든 객체가 이 프로퍼티를 프로퍼티로 가지고 있어서가 아니라, base 객체에게서 상속받았기 때문이다. 즉, hasOwnProperty, isPrototypeOf 등의 프로퍼티는 메모리 딱 한 곳에만 저장되어 있지만, 모든 객체에서 이를 상속받기 때문에 메모리 공간을 추가로 사용하지 않고도 해당 프로퍼티를 사용할 수 있는 것이다.

 

# 프로토타입 상속받기

__proto__로 상속받게 할 수는 있으나, 사용하면 안되는 코드이다. 그러면 어떤 객체가 프로토타입을 상속받도록 하려면?

let human = {
  mortal: true
}

let socrates = Object.create(human);
console.log(socrates.mortal); // true
console.log(human.isPrototypeOf(socrates)); // true

이렇게 Object.create를 이용하면 원하는 객체의 프로퍼티를 상속받도록 지정할 수 있다.

댓글