[JS] Core Javascript (3)

RINM·2024년 3월 21일
0

Study

목록 보기
3/7

Closure

Closure는 lexical environment와 함수의 조합이다. 사실 위의 정의만 보아서는 closure가 어떤 개념인지 잘 와닿지 않는다. 어떤 한 컨텍스트 A가 있고 그 안에 내부함수 B가 있다고 가정하자. 여기서 Closure는 A와 B를 모두 포함하는 개념이다. 그렇다면 Closure가 어디서 사용될까? Closure라는 개념은 주로 A의 Lexical environment와 B가 조합할 때 나타나는 특수한 현상을 지칭하기 위하여 사용된다.
A와 B가 서로에게 영향을 미치는 범위를 살펴보면 사실 그렇게 크지 않다. B를 실행시킨 A의 environmentRecord와 B의 outerEnvironmentReference 뿐이다. 즉, B가 실행하면서 outerEnvironmentReference를 통하여 A의 environmentRecord에 접근하는 것, 다시 말해 A에서 선언한 변수를 내부 함수 B가 참조하는 경우이다.

var outer = function () {
	var a = 1;
  	var inner = function () {
    	return ++a;
    }
    return inner;
}
var outer2 = outer();
console.log(outer2());
console.log(outer2());

위의 예제가 어떻게 동작되는지 살펴보자. outer 함수 안에는 지역변수 a와 함수 inner가 선언되어 있다. inner는 outer의 지역변수 a의 값을 하나 증가시킨 뒤 return한다. 그리고 outer는 이 inner 함수 자체를 return한다. 전역공간에서 outer2는 outer()의 결과값을 담는다. 즉, outer가 반환한 inner 함수를 받았다. 그리고 이것을 두 번 실행하고 그 값을 살펴보자.

outer함수가 실행되면 lexical environment에 지역변수 a와 내부 함수 inner가 선언된다. 실행이 끝나면 inner가 반환되어 전역 변수 outer2에 담기고 outer의 실행 컨텍스트는 사라져야한다. 하지만 inner가 outer의 지역변수 a를 참조하고 있기 때문에 a의 참조카운트가 0이 아니게 되어 사라지지 않는다. 이제 outer2를 실행하면 outerEnvironmentReference에서 a를 찾아 값을 증가시킨다. 이렇게 증가시킨 a의 값 2가 반환되고 outer2의 실행컨택스트는 사라진다. 또 다시 outer2를 실행시키면 하나 더 증가한 a의 값 3이 반환되고 또 outer2의 실행컨택스트는 사라진다. 이렇게 두 번의 호출이 끝나고 해당 함수의 실행 컨택스트가 모두 사라졌지만 a는 여전히 메모리 상에 존재한다. 신기하게도 따지고 보면 outer의 실행컨택스트에 불과했던 a는 전역 컨택스트가 끝나기 전까지 계속 존재한다. 전역변수 outer2에 담긴 inner가 자신의 outerEnvironmentReference에서 a를 참조하기 때문이다. a를 메모리 상에서 내리려면 다시 말해 a의 참조 카운트를 0으로 만드려면 a를 참조하는 inner를 담은 전역변수 outer2에 다른 값을 넣으면 된다. 이렇게 되면 a를 그 어떤 변수도 a를 참조하지 않게 되고, a는 가비지 콜랙팅의 대상이 되어 사라진다.

이처럼 한 컨택스트 A에서 선언한 변수 a를 참조하는 내부함수 B를 A의 외부로 전달할 경우 A가 종료된 이후에도 a가 사라지지 않는 현상이 나타난다. Closure에 의하여 지역변수가 함수 종료 후에도 사라지지 않는 것이다. 즉, Closure를 사용하면 함수 종료 후에도 사라지지 않는 지역 변수를 만들 수 있다. Closure를 활용하면 정보를 은닉하거나 캡슐화할 수 있다.

[MDN] Closures
[Poiemweb] Closure

Prototype

객체를 사용하거나 외부 API의 응답 또는 Promise 등을 log에 찍어본 경험이 있다면 Prototype을 본 적이 있을 것이다.

생성자를 사용하여 new로 새로운 인스턴스를 생성하면 생성자의 prototype이라는 프로퍼티가 [[Prototype]]이라고 하는 프로퍼티로 참조를 전달한다. 즉, 생성자의 prototype 프로퍼티와 인스턴스의 [[Prototype]] 프로퍼티는 같은 것을 참조한다. [[Prototype]]는 특수하다. 접근 가능한 것이 아니라 정보를 보여주는 것만 가능해서 실제 동작에 있어서는 인스턴스와 동일시된다.

예를 들어 Array의 prototype을 살펴보자. 생성자 Array 속에 prototype이 들어있다.

배열을 생성하면 여기 있는 이 prototype이 [[Prototype]]과 연결된다. prototype 객체를 더 살펴보면 배열을 다룰 때 사용하는 메서드나 length와 같은 멤버 변수 발견할 수 있다.

이번엔 배열을 하나 생성하여 구조를 살펴보자. [[Prototype]]이 존재하고 그 내용은 앞서 살펴본 생성자의 prototype 객체와 같다.

그럼 객체가 아닌 리터럴은 어떻게 될까? 아래처럼 숫자 10은 객체가 아니기 때문에 [[Prototype]]을 갖고 있지 않다.

하지만 우린 이런 리터럴 변수에서도 해당하는 객체가 가진 메서드를 사용할 수 있다.

그러나 여전히 리터럴은 객체가 아니다. 그럼 어떻게 위의 toFixed() 메서드를 사용할 수 있었던 것일까. JS에서는 내부적으로 리터럴에 해당하는 생성자 함수의 인스턴스를 만들어 메서드를 실행하고 실행이 끝나면 다시 인스턴스를 삭제한다. 즉, 리터럴에 맞는 객체의 인스턴스를 일시적으로 생성하여 메서드를 실행한다. 이렇게 하면 null과 undefined를 제외한 모든 타입은 그 자신에 메서드 함수가 정의되어 있지 않더라도 prototype를 참조하여 메서드를 실행할 수 있다.

[[Prototype]]은 접근가능한 것이 아니라 console에 찍어 내부를 들여다볼 수만 있다. 직접 접근을 하려면 다른 방식을 써야한다.

instance.__proto__
Object.getPrototypeOf(instance)
Constructor.prototype

따라서 아래의 표현들은 모두 생성자 Person을 가리킨다.

function Person(n,a){
  this.name = n;
  this.age = a;
};

var person = new Person('name',10)

// 모두 다 동일한 인스턴스 생성자
var person1 = new person.__proto__.constructor('name1',10)
var person2 = new person.constructor('name2',10)
var person3 = new Object.getPrototypeOf(person).constructor('name3',10)
var person4 = new Person.prototype.constructor('name4',10)

Prototype Chaining

인스턴스의 변수를 찾을 때에는 가까운 것부터 먼 [[Prototype]]으로 넘어가는 체인이 존재한다. 아래 예제를 살펴보자.

function Person(){};
var person1 = new Person()
console.log(person1)

Person이라는 빈 생성자를 선언한 뒤 이것으로 person1이라는 인스턴스를 만들었다. person1을 콘솔에 찍어보면 [[Prototype]]만 있을 뿐 비어 있는 것을 알 수 있다.

Person.prototype으로 프로토타입에 접근해서 name이라는 변수를 추가해보자.

Person의 프로토타입에 name을 선언하여 값을 넣어도 person1에는 변함이 없다. 그러나 person1.name을 콘솔에 찍어보면 프로토타입에 선언한 name 값이 출력된다. 이를 통해 JS가 peron1의 [[Prototype]]을 타고 Person의 프토토타입에 접근하여 name에 담겨있는 값을 읽어냈다는 것을 알 수 있다. 이처럼 생성자의 프로토타입에 직접 접근하면 이 생성자로 생성된 모든 인스턴스가 같은 [[Prototype]]를 참조하기 때문에 같은 값을 넣어주는 효과를 얻을 수 있다.
그럼 반대로 인스턴스 person2에 name이 정의되어 있다면 어떨까?

위의 예제에서 person2 인스턴스에는 이미 멤버변수 name이 정의되어 있다. 그러니 Person의 프로토타입에서 name을 따로 정의해도 person2.name을 콘솔에 찍으면 [[Prototype]]을 참조하지 않고 멤버변수에 선언된 값이 찍힌다.

이렇게 해당 멤버 변수나 메서드를 호출하기 위하여 점점 멀리 있는 prototype으로 찾아가는 것을 prototype chaining이라고 부른다.

[Medium] Prototypes in JavaScript

Class

클래스는 공통된 속성과 기능을 정의한 추상적인 개념을 말하며, 인스턴스는 이러한 클래스를 구체적으로 표현하는 객체이다.
클래스 안에 정의되는 프로퍼티 중 prototype 안에 포함된 것은 프로토타입 메서드, prototype이 아닌 클래스 안에서 정의된 메서드는 static method, 변수는 static protperty라 부른다. statict method는 클래스를 new와 생성자 없이 함수로서 호출될 때만 의미가 있다. 주로 인스턴스 개별의 동작보다는 소속 여부 확인, 소속 부여 등의 동작이 이루어진다.
prototype 메서드와 달리 생성자 함수 내부에 정의된 static method나 static protperty는 인스턴스에서 직접 접근할 수 없다. 따라서 constructor.method(instance) 형태로 instance 자체를 넘겨주어 실행된다.

[javascript.info] Static properties and methods

0개의 댓글