Front-end Developer

0%

스코프란?

  • 식별자가 유효한 범위
  • 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효 범위가 결정됨.
  • 스코프 내에서 식별자는 유일해야 하지만 다른 스코프에는 동명의 식별자를 쓸 수 있다.

    • 단, var는 스코프 내의 동일한 식별자를 허용하는데, 이는 의도치 않은 재할당의 부작용이 있다. let, const는 같은 스코프 내의 중복선언을 허용하지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var x = "global";
let y = "global";
//전역변수

function foo() {
var x = "local";
var x = "local2";
let y = "local";
let y = "local2";
console.log(x); //local2
}

foo();

console.log(x); //global
console.log(y); //SyntaxError: Identifier 'y' has already been declared

var는 스코프 내에서 중복선언을 허용하기 때문에 foo라는 함수의 스코프 내에서 var x를 중복선언한 경우 x = ‘local2’로 재할당된 것과 같은 효과가 발생한다. 따라서 의도치 않게 var x의 값이 변경된다. 또한 var는 함수 레벨 스코프를 가지기 때문에 foo 함수 내에서 선언된 변수는 함수 내에서만 유효하다. 따라서 함수 외부에서 console.log(x)를 하면 전역에 선언된 var x의 값인 global이 출력된다.

하지만 let은 같은 스코프 내에 중복 선언을 허용하지 않기 때문에 console.log(y)를 하면 에러가 발생한다. 또한 let은 블록 스코프 레벨을 가지기 때문에 전역에 선언된 변수를 참조할 수 없다.

전역스코프 vs 지역스코프
전역스코프: 코드 가장 바깥 영역. 어디에서나(함수 내부 포함) 참조가능
지역스코프: 함수 몸체 내부. 자신이 선언된 지역과 하위지역에서만 참조가능. 지역변수를 전역에서 참조하면 에러가 발생한다.

전역 스코프 <- outer 함수 <-inner 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var x = "global x";
var y = "global y";
//전역변수는 어디에서나 참조할 수 있음.

function outer() {
var z = "outer's local z";
console.log(x); //global x
console.log(y); // global y
console.log(z); // outer's local z

function inner() {
var x = "inner's local x";

console.log(x); // inner's local x
console.log(y); // global y
console.log(z); // outer's local z
}

inner();
}
outer();

console.log(x); //global x
console.log(z); // referenceError

블록 레벨 스코프 vs 함수 레벨 스코프

  • 블록 레벨 스코프: 모든 코드 블록(함수, if문, for문 등)을 지역 스코프로 인정한다.
  • 함수 레벨 스코프: 함수의 코드 블록만 지역 스코프로 인정한다. 함수에 의해서만 지역스코프가 생성된다.

단, var로 선언된 변수는 코드 블록 내에 선언되었더라도 지역변수가 아닌 전역변수이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var x = "global x";
let y = "global y";
var z = "global z";
let u = "global u";

if (true) {
var x = "local x";
console.log(x); //local x
let y = "local y";
console.log(y); //local y
}

function foo() {
var z = "local z";
console.log(z); //local z
let u = "local u";
console.log(u); //local u
}

foo();

console.log(x); //local x
console.log(y); //global y
console.log(z); //global z
console.log(u); //global u 전역에 u가 없으면 error 발생
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
var x = "global x";
let y = "global y";
var z = "global z";
let u = "global u";

if (true) {
var x = "local x";
let y = "local y";
console.log(x); //local x
console.log(y); //local y
console.log(z); //global z
console.log(u); //global u
}
/*
코드블록 내에서 x를 참조하면 블록내에 지정된 local x가 반환되고, y도 마찬가지이다.
그러나 z,u는 코드 블록 내에 할당되어져 있지 않기 때문에 상위 스코프에 z와 u의 값이 있는지
탐색하고, 값이 있었기 때문에 global z,u의 값을 반환. 만약 여기서도 값이 없었다면 상위스코프로 탐색하러 갈 것.*/

function foo() {
var z = "local z";
let u = "local u";
console.log(x); //local x
console.log(y); //global y
console.log(z); //local z
console.log(u); //local u
}
/*
함수 내에 z,u는 지정되어 있기 때문에 local z, local u가 반환된다. 그러나 x,y는 없기 때문에 상위 스코프로 올라가 값이 있는지 탐색한다. 상위 스코프에 x값이 있기 때문에 local x라는 값을 반환하고, y는 상위스코프에 값이 있지만, 'local y'를 반환하지 않는다. 왜그럴까?
let은 블록레벨 스코프이기 때문에 코드 블록을 지역 스코프로 인정한다. 상위 스코프인 if문에
있는 y의 값은 if문 내에서만 유효한 값이므로 밖에서 참조할 수 없다. 따라서 if문을 건너뛰고
그보다 상위스코프인 전역에 있는 global y의 값을 가져온다.
*/

foo();

console.log(x); //local x
console.log(y); //global y
console.log(z); //global z
console.log(u); //global u 전역에 u가 없으면 error 발생

/*
x는 먼저 상위 스코프인 foo함수 내에 x값이 있는지 찾은 후, 없기 때문에 그보다 상위스코프인 if문 내로 향한다. var는 함수레벨스코프를 가지므로 if문 내에 선언되어도 전역변수이기 때문에 어디서나 참조 가능하다. 그러나 y의 경우 let 블록 레벨 스코프이기 때문에 if문 내에 있는 값을 참조할 수 없고, 그보다 상위인 전역에 있는 global y의 값을 가져온다.

z는 상위 스코프인 foo 함수 내에 값이 설정되어 있어서 값을 참조할 수 있을 것 같지만 함수 내에 var로 선언되어 있기 때문에, 이 값은 함수 내에서만 유효하다. 따라서 그보다 상위에 있는 값을 찾아 올라가서 global z의 값을 가져온다. u역시 마찬가지이다. 만약 전역에 u나 z의 값이 없었다면 error가 발생했을 것이다.*/

렉시컬 스코프
함수는 어디서 호출했는지 혹은 어디서 정의되었는지에 따라 상위 스코프를 결정하는데 전자를 동적스코프, 후자를 정적 스코프라고 말한다. 자바스크립트는 정적 스코프를 따르고 이를 렉시컬 스코프라 부르기도 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
var x = 1;

function foo() {
var x = 10;
bar();
}

function bar() {
console.log(x);
}

foo(); //1
bar(); //1

객체지향 프로그래밍

프로그램을 명령어 또는 함수의 목록으로 보는 전통적인 명령형 프로그래밍(Imperative programming)의 절차지향적 관점에서 벗어나 여러 개의 독립적 단위, 즉 객체(object)들의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임.

  • 속성을 통해 여러 개의 값을 하나의 단위로 구성한 복합적인 자료 구조를 객체라 하며 객체 지향 프로그래밍은 독립적인 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임이다.
  • 객체의 상태(state)를 나타내는 데이터와 상태 데이터를 조작할 수 있는 동작(behavior)을 하나의 논리적인 단위로 묶어 생각한다. 따라서 객체는 상태 데이터와 동작을 하나의 논리적인 단위로 묶은 복합적인 자료 구조라고 할 수 있다. 객체의 상태 데이터를 프로퍼티(property), 동작을 메소드(method)라 부른다.

상속과 프로토타입

  • 상속: 어떤 객체의 프로퍼티 또는 메소드를 다른 객체가 상속받아 그대로 사용할 수 있는 것을 말한다. 자바스크립트는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거한다.

  • 생성자 함수: 동일한 프로퍼티 구조를 갖는 객체를 여러 개 생성할 때 유용하지만, 생성자 함수가 인스턴스를 생성할 때마다 메소드를 중복 생성하고, 모든 인스턴스가 이를 중복 소유한다. 이는 메모리를 불필요하게 낭비하고, 퍼포먼스에 악영향을 준다. 이런 경우 상속을 이용하여 불필요한 중복을 제거할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 생성자 함수
function Circle(radius) {
this.radius = radius;
}

// Circle 생성자 함수가 생성한 모든 인스턴스가 공유할 수 있도록 getArea 메소드를 프로토타입에 추가한다.
// 프로토타입은 Circle 생성자 함수의 prototype 프로퍼티에 바인딩되어 있다.
Circle.prototype.getArea = function () {
return Math.PI * Math.pow(this.radius, 2);
};

// 인스턴스 생성
const circle1 = new Circle(1);
const circle2 = new Circle(2);

// Circle 생성자 함수가 생성한 모든 인스턴스는 부모 객체의 역할을 하는
// 프로토타입 Circle.prototype로부터 getArea 메소드를 상속받는다.
// 즉, Circle 생성자 함수가 생성하는 모든 인스턴스는 하나의 getArea 메소드를 공유한다.
console.log(circle1.getArea === circle2.getArea); // true

console.log(circle1.getArea()); // 3.141592653589793
console.log(circle2.getArea()); // 12.566370614359172

프로토타입 객체

객체간 상속(inheritance)을 구현하기 위해 사용. 어떤 객체의 상위 객체의 역할을 하는 객체이며, 다른 객체에 공유 프로퍼티(메소드 포함)를 제공한다. 또한 모든 객체는 [[Prototype]]이라는 내부 슬롯을 가지며, 객체 생성 시 [[Prototype]] 내부 슬롯의 값으로 프로토타입의 참조를 저장한다. 즉 모든 객체는 하나의 프로토타입을 가진다.프로토타입은 null이거나 객체인데 모든 프로토타입은 생성자 함수와 연결되어 있다.(객체-프로토타입-생성자 함수는 서로 연결되어 있다.)

__proto__ 접근자 프로퍼티

모든 객체는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입, 즉 [[Prototype]] 내부 슬롯에 접근할 수 있다. 하지만 직접 접근할 수 없으며 __proto__ 접근자 프로퍼티를 통해 간접적으로 프로토타입에 접근할 수 있다. 이는 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위함이며 반드시 단방향 링크드 리스트로 구현되어야 한다. 만약 이렇게 하지 않으면 프로토타입 체인의 검색 과정에서 무한 루프에 빠질 수 있다. 또한 __proto__ 접근자 프로퍼티는 객체가 직접 소유하는 프로퍼티가 아니라 Object.prototype의 프로퍼티이다. Object.prototype은 프로토타입 체인의 최상위 객체이며, 이 객체의 프로퍼티와 메서드는 모든 객체에게 상속된다.

\_proto__접근자 프로퍼티는 코드 내에서 직접 사용하지 않는다. 아래와 같이 object.prototype을 상속받지 않는 객체를 만들 수도 있기 때문이다. 이 경우 \_proto__접근자 프로퍼티를 사용하지 않는다.

1
2
3
4
5
6
7
8
// obj는 프로토타입 체인의 종점이다. 따라서 Object.__proto__를 상속받을 수 없다.
const obj = Object.create(null);

// obj는 Object.__proto__를 상속받을 수 없다.
console.log(obj.__proto__); // undefined

// 따라서 Object.getPrototypeOf 메소드를 사용하는 편이 좋다.
console.log(Object.getPrototypeOf(obj)); // null
  • Object.getPrototypeOf 메소드: 프로토타입을 참조하고 싶을 때 사용한다.
  • Object.setPrototypeOf 메소드: 프로토타입을 교체하고 싶을 때 사용한다.

함수 객체의 prototype 프로퍼티

함수 객체는 __proto__ 외에 prototype 프로퍼티를 소유한다. 이는 함수 객체만이 소유하며, 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다. 따라서 생성자 함수로 호출할 수 없는 non-constructor인 화살표 함수와 메소드는 prototype 프로퍼티를 소유하지 않고, 프로토타입도 생성하지 않는다.

1
2
3
4
5
6
7
8
9
const Person = (name) => {
this.name = name;
};

// non-constructor는 prototype 프로퍼티를 소유하지 않는다.
console.log(Person.hasOwnProperty('prototype')); // false

// non-constructor는 프로토타입을 생성하지 않는다.
console.log(Person.prototype); // undefined
  • __proto__접근자 프로퍼티: 모든 객체가 소유하며, 객체가 자신의 프로토타입에 접근 또는 교체하기 위해 사용한다.
  • prototype 프로퍼티: 함수 객체만이 소유하며, 생성자 함수가 자신이 생성할 인스턴스(객체)의 프로토타입을 할당하기 위해 사용한다.
  • 객체의 __proto__접근자 프로퍼티와 함수 객체의 prototype 프로퍼티는 동일한 프로토타입을 가리킨다.
  • 모든 프로토타입은 constructor 프로퍼티를 갖고, 이는 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킨다. 이 연결은 생성자 함수가 생성될 때, 즉 함수 객체가 생성될 때 이루어진다.

리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입

생성자 함수에 의해 생성된 인스턴스는 프로토타입의 constructor 프로퍼티에 의해 생성자 함수와 연결된다. 이처럼 new 연산자와 생성자 함수를 호출하여 인스턴스를 생성하지 않고, 리터럴 표기법(객체 리터럴, 함수 리터럴, 배열 리터럴, 정규표현식 리터럴 등)으로 객체를 생성할 수도 있다.

  • object 생성자 함수: 인수가 전달되지 않으면 추상연산 OrdinaryObjectCreate를 호출하여 빈 객체를 생성하고, 인수가 전달된 경우 인수를 객체로 변환한다.
  • 객체 리터럴: 평가시 OrdinaryObjectCreate를 호출하여 빈객체를 생성하고 프로퍼티를 추가한다.

object 생성자 함수와 객체 리터럴 평가는 OrdinaryObjectCreate를 호출하여 빈 객체를 생성하는 점은 동일하다. 그러나 new.target의 확인, 프로퍼티를 추가하는 처리 등의 세부 내용이 다르다. 따라서 객체 리터럴에 의해 생성된 객체는 object 생성자 함수가 생성한 객체가 아니다.

1
2
3
function foo() {} //생성자 함수가 아닌 함수 선언문(함수 리터럴)로 생성한 함수 foo

console.log(foo.constructor === function); //true

foo()는 생성자 함수로 생성하지 않았지만 constructor를 통해 확인해 보면 함수 foo의 생성자 함수가 function 생성자 함수인 것을 알 수 있다. 리터럴 표기법에 의해 생성된 객체도 상속을 위해 프로토타입이 필요하기 때문에 가상의 생성자 함수를 갖는다. 생성자함수-프로토타입은 항상 쌍으로 연결되며, prototype, constructor에 의해 연결되어 있기 때문이다.

프로토타입의 생성 시점

모든 객체는 생성자 함수와 연결되어 있고, 프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성된다.

사용자 정의 생성자 함수와 프로토타입 생성 시점

생성자 함수로서 호출할 수 있는, 내부 메소드 [[construct]]를 갖는 함수 객체는 new 연산자와 함께 생성자 함수로 호출할 수 있다. 이러한 constructor는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 같이 생성된다. 함수 선언문의 경우 런타임 이전에 객체 생성 및 할당이 완료되기 때문에 프로토타입도 런타임 이전에 생성된다. 그리고 모든 객체는 프로토타입을 가지기 때문에 프로토타입은 자신의 프로토타입인 object.prototype을 가진다. 단, non-constructor는 프로토타입이 생성되지 않는다.

1
2
3
4
5
6
7
// 함수 정의(constructor)가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성된다.
console.log(Person.prototype); // {constructor: ƒ}

// 생성자 함수
function Person(name) {
this.name = name;
}

빌트인 생성자 함수와 프로토타입 생성 시점

빌트인 생성자 함수가 생성되는 시점에 프로토타입이 생성되고, 모든 빌트인 생성자 함수는 전역 객체가 생성되는 시점에 생성된다. 전역 객체는 코드가 실행되기 이전 단계에 자바스크립트 엔진에 의해 생성되는 특수한 객체를 말한다.


객체 생성 방식과 프로토타입의 결정

다음과 같은 다양한 방식으로 객체를 생성할 수 있는데 추상 연산 OrdinaryObjectCreate에 의해 생성된다는 공통점이 있다. 객체 리터럴과 object 생성자 함수는 추상 연산 OrdinaryObjectCreate를 호출한다는 점과 이로 인해 연결되는 구조는 객체 리터럴에 의해 생긴 객체와 동일하다. 단, 객체 리터럴 방식은 리터럴 내부에 프로퍼티를 추가하고 object 생성자 함수는 빈 객체를 먼저 생성한 후 프로퍼티를 추가해야 한다.

  • 객체 리터럴
  • Object 생성자 함수
  • 생성자 함수
  • Object.create 메소드
  • 클래스 (ES6)

생성자 함수에 의해 생성된 객체의 프로토타입

생성자 함수에 의해 생성되는 객체의 프로토타입은 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체이다. 아래 예제에서 사용자 정의 생성자 함수 Person과 더불어 생성된 프로토타입 Person.prototype의 프로퍼티는 constructor 뿐이다. 프로토타입은 객체이기 때문에 일반 객체처럼 프로토타입에도 프로퍼티를 추가/삭제할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name) {
this.name = name;
}

// 프로토타입 메소드
Person.prototype.sayHello = function () {
console.log(`Hi! My name is ${this.name}`);
};

const me = new Person('Lee');
const you = new Person('Kim');

me.sayHello(); // Hi! My name is Lee
you.sayHello(); // Hi! My name is Kim

프로토타입 체인

자바스크립트가 객체 지향 프로그래밍의 상속을 구현하는 메커니즘. 객체의 프로퍼티에 접근하려고 할 때 해당 객체에 접근하고자 하는 프로퍼티가 없으면 [[Protorype]] 내부 슬롯의 참조값을 따라서 스코프체인이 그러하듯 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다.

프로토타입 체인의 최상위: Object.prototype
모든 객체는 Object.prototype을 상속받으며, 이를 프로토타입 체인의 종점이라 한다. Object.prototype의 프로토타입은 null이다. 만약 Object.prototype에서도 프로퍼티를 찾을 수 없다면 undefined를 반환한다.

프로토타입 체인이 상속과 프로퍼티 검색을 위한 메커니즘이라면 스코프 체인은 식별자 체인을 위한 메커니즘이다. 단, 스코프 체인과 프로토타입 체인을 별도의 개념이 아니라 서로 협력하면서 식별자와 프로퍼티를 검색한다.

캡슐화

정보의 일부를 외부에 감추어 은닉(정보 은닉(information hiding))하는 것으로 적절치 못한 접근으로부터 정보를 보호하고 객체간의 상호 의존성, 즉 결합도를 낮추는 효과가 있다.

즉시 실행 함수를 사용하여 코드를 깔끔하게 묶는다.

오버라이딩과 프로퍼티 쉐도잉

  • 프로토타입 프로퍼티(메소드 포함): 프로토타입이 소유.
  • 인스턴스 프로퍼티: 인스턴스가 소유.
  • 프로퍼티 쉐도잉: 상속 관계에 의해 프로퍼티가 가려지는 현상.
  • 오버라이딩: 상위 클래스가 가지고 있는 메소드를 하위 클래스가 재정의하여 사용.

프로토타입과 인스턴스에 동일한 이름의 프로퍼티를 추가했을 때, 프로토타입에 정의된 프로퍼티를 덮어쓰는 것이 아니라 프로포타입의 메소드가 인스턴스 프로퍼티에 의해 가려진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

프로토타입의 교체

부모 객체인 프로토타입을 동적으로 변경할 수 있으며, 생성자 함수에 의한 방법과 인스턴스에 의한 교체 방법이 있다. 하지만 프로토타입을 교체하면 constructor 프로퍼티와 생성자 함수 간의 링크가 파괴된다.

  • 생성자 함수에 의한 프로토타입의 교체: 다른 임의의 객체를 바인딩하여 미래에 생성할 인스턴스의 프로토타입을 교체한다.
  • 인스턴스에 의한 프로토타입의 교체: __proto__ 접근자 프로퍼티를 통해 이미 생성된 객체의 프로토타입을 교체한다.

생성자 함수에 의한 프로토타입의 교체

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Person = (function () {
function Person(name) {
this.name = name;
}

Person.prototype = {
/* constructor 프로퍼티와 생성자 함수 간의 링크를 설정하고 싶다면
constructor: Person, 이라고 명시한다.*/
sayHello() {
console.log(`Hi! My name is ${this.name}`);
},
};

return Person;
})();

const me = new Person('Lee');

위와 같이 프로토타입을 객체로 교체하면 Person.prototype에 있던 constructor프로퍼티가 사라지기 때문에 me 객체의 생성자 함수는 Person이 아닌 Object가 나온다.

인스턴스에 의한 프로토타입의 교체

인스턴스의 __proto__ 접근자 프로퍼티(또는 Object.getPrototypeOf 메소드)를 통해 접근할 수 있다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Person(name) {
this.name = name;
}

const me = new Person('Lee');

// 프로토타입으로 교체할 객체
const parent = {
/* constructor 프로퍼티와 생성자 함수 간의 링크를 설정하고 싶다면
constructor: Person, 이라고 명시한다.*/
sayHello() {
console.log(`Hi! My name is ${this.name}`);
},
};

/* 생성자 함수의 prototype 프로퍼티와 프로토타입 간의 링크 설정
Person.prototype = parent; */

// ① me 객체의 프로토타입을 parent 객체로 교체한다.
Object.setPrototypeOf(me, parent);
// 위 코드는 아래의 코드와 동일하게 동작한다.
// me.__proto__ = parent;

me.sayHello(); // Hi! My name is Lee

인스턴스에 의한 프로토타입도 생성자 함수에 의한 프로토타입과 마찬가지로 constructor 프로퍼티와 생성자 함수 간의 연결을 파괴한다. 단, 생성자 함수에 의한 교체는 생성자 함수의 prototype 프로퍼티가 교체된 프로토타입을 가리키고 있다. 하지만 인스턴스에 의한 교체는 생성자 함수의 protorype 프로퍼티가 교체된 프로토타입을 가리키고 있지 않다.


instanceof 연산자

객체 instanceof 생성자 함수

객체가 생성자 함수의 instance인가?

좌변의 객체가 우변의 생성자 함수와 연결된 인스턴스라면 true로 평가되고 그렇지 않은 경우에는 false로 평가된다. instanceof 연산자는 상속 관계를 고려한다는 것에 주의.

Object.create에 의한 직접 상속

명시적으로 프로토타입을 지정하여 새로운 객체를 생성한다. Object.create 메소드도 다른 객체 생성 방식과 마찬가지로 추상 연산 OrdinaryObjectCreate를 호출한다.

객체 리터럴 내부에서 __proto__에 의한 직접 상속

ES6에서는 객체 리터럴 내부에서 __proto__접근자 프로퍼티를 사용하여 직접 상속을 구현할 수 있다.

정적 프로퍼티/메소드

정적(static) 프로퍼티/메소드는 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메소드를 말한다.

프로퍼티 존재 확인

1
2
3
4
5
/**
* key: 프로퍼티 키를 나타내는 문자열
* object: 객체로 평가되는 표현식
*/
key in object;

in 연산자는 확인 대상 객체 내에 프로퍼티가 존재하는지 여부를 확인한다.

프로퍼티 열거

for…in문

for (변수선언문 in 객체) { … }
객체의 모든 프로퍼티를 순회하며 열거(enumeration). 단 프로퍼티 키가 심볼인 프로퍼티는 열거하지 않는다. 순서는 보장되지 않으며 상속받은 프로토타입의 프로퍼티까지 열거한다.

Object.keys/values/entries 메소드

상속받은 프로퍼티를 제외하고 객체 자신의 프로퍼티만을 열거하고자 할 때 사용.

  • Object.keys 메소드: 객체 자신의 열거 가능한(enumerable) 프로퍼티 키를 배열로 반환한다.
  • Object.values 메소드: 객체 자신의 열거 가능한 프로퍼티 값을 배열로 반환
  • Object.entries 메소드: 객체 자신의 열거 가능한 프로퍼티 키와 값의 쌍의 배열을 배열에 담아 반환

References
프로토타입

일급 객체

  • 무명의 리터럴로 생성할 수 있다. 즉, 런타임에 생성이 가능하다.
  • 변수나 자료 구조(객체, 배열 등)에 저장할 수 있다.
  • 함수의 매개 변수에게 함수를 전달할 수 있다.
  • 함수의 결과값으로 반환할 수 있다.

함수는 위의 조건을 만족하는 일급 객체이며, 이는 함수를 객체와 동일하게 사용할 수 있다는 의미이다. 함수는 값을 사용할 수 있는 곳 어디에서든 리터럴로 정의할 수 있으며, 런타임에 함수 객체로 평가된다. 또한 프로퍼티를 가질 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 함수는 무명의 리터럴로 생성할 수 있으며, 변수에 저장할 수 있다.
// 그리고 런타임에 함수 리터럴이 평가되어 함수 객체가 생성되고 변수에 할당된다.
const increase = function (num) {
return ++num;
};

//함수는 객체에 저장할 수 있다.
const predicates = { increas, decrease };

// 함수의 매개 변수에게 전달할 수 있다.
// 함수의 결과값으로 반환할 수 있다.
function makeCounter(predicate) {
let num = 0;

return function () {
num = predicate(num);
return num;
};
}

//함수는 매개 변수에게 함수를 전달할 수 있다.
const increaser = makeCounter(predicates.increase);
console.log(increaser());
console.log(increaser());

arguments 프로퍼티

arguments 객체를 프로퍼티 값으로 갖는 함수 객체를 말하며, 함수 호출 시 전달된 인수(argument)들의 정보를 담고 있는 순회 가능한(iterable) 유사 배열 객체(array-like object)이며 함수 내부에서 지역 변수처럼 사용된다.즉, 함수 외부에서는 사용할 수 없다.

함수를 정의할 때 선언한 매개변수는 함수 몸체 내부에서 변수와 동일하게 취급된다. 즉, 함수가 호출되면 함수 몸체 내에서 암묵적으로 매개변수가 선언되고 undefined로 초기화된 이후 인수가 할당된다. 만약 매개변수의 개수보다 인수를 많이 전달한 경우 초과된 인수는 무시된다.

런타임 시 호출된 함수의 인자 개수를 확인하고 이에 따라 함수의 동작을 달리 정의하고자 할 때 유용하다. 즉 매개변수 개수를 확정할 수 없는 가변 인자 함수를 구현할 때 유용하다.

일부 브라우저에서 지원하고 있으나 ES3부터 표준에서 폐지되었다.

caller 프로퍼티

ECMAScript 스펙에 포함되지 않은 비표준 프로퍼티이다. 이후 표준화될 예정도 없는 프로퍼티이다. 함수 객체의 caller 프로퍼티는 함수 자신을 호출한 함수이다.

length 프로퍼티

함수 객체의 length 프로퍼티는 함수 정의 시 선언한 매개변수의 개수를 가리킨다. arguments 객체의 length 프로퍼티는 인자(argument)의 개수를 가리키고, 함수 객체의 length 프로퍼티는 매개변수(parameter)의 개수를 가리킨다.

name 프로퍼티

함수 이름을 나타내는 것으로 ES6 이전까지는 비표준이었지만 ES6에서 정식 표준이 되었다. 단 ES5와 ES6에서 동작을 달리 하므로 주의해야 한다.익명 함수 표현식의 경우, ES5에서 name 프로퍼티는 빈 문자열을 값으로 갖는다. 하지만 ES6에서는 함수 객체를 가리키는 변수 이름을 값으로 갖는다.

__proto__ 접근자 프로퍼티

모든 객체는 [[Prototype]]이라는 내부 슬롯을 갖는다. [[Prototype]] 내부 슬롯은 객체 지향 프로그래밍의 상속을 구현하는 프로토타입 객체를 가리킨다.

prototype 프로퍼티

함수 객체만이 소유하는 프로퍼티이다. 일반 객체에는 prototype 프로퍼티가 없다. prototype 프로퍼티는 함수가 객체를 생성하는 생성자 함수로 사용될 때, 생성자 함수가 생성할 인스턴스의 프로토타입 객체를 가리킨다.


References
poiemaweb

Object 생성자 함수

new 연산자와 함께 호출하여 빈 객체(인스턴스)를 생성하는 함수.
인스턴스(instance): 생성자 함수에 의해 생성된 객체

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//빈 객체를 생성
const fruits = new Object();

//프로퍼티 또는 메소드 추가
fruits.name = "banana";
fruits.price = 2000;
fruits.getPrice = function () {
return this.price;
};
fruits.introduce = function () {
console.log("hello! " + this.name + " is " + this.getPrice() + " won");
};

console.log(fruits); //{name: 'banana', price: 50,
//getPrice: [Function], introduce: [Function]}
fruits.introduce(); //hello! banana is 2000 won

객체 리터럴에 의한 객체 생성 방식의 문제점

객체 리터럴에 의한 객체 생성 -> 싱글 인스턴스 -> 글로벌 오브젝트
생성자 함수에 의한 객체 생성 -> 많은 인스턴스 -> 여러 개의 인스턴스

객체 리터럴에 의한 객체 생성 방식은 단 하나의 객체만을 생성한다. 따라서 동일한 프로퍼티를 갖는 객체를 여러 개 생성해야 하는 경우, 매번 같은 프로퍼티를 기술해야 하기 때문에 비효율적.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const car1 = {
name: "Mercedes-Benz",
price: 5000,
salePrice() {
return this.price - 1000;
},
};

console.log("this " + car1.name + " price is " + car1.salePrice() + "won");

const car2 = {
name: "Mercedes-Benz",
price: 3000,
salePrice() {
return this.price - 1000;
},
};

console.log("this " + car2.name + " price is " + car2.salePrice() + "won");

위의 예제 같은 경우, 객체 리터럴에 의해 생성되었기 때문에 동일한 프로퍼티 키를 가진 객체를 재사용하고 싶으면 객체를 매번 새로 생성해야한다.

  • 객체는 프로퍼티를 통해 객체 고유의 상태(state)를 표현하고, 메소드를 통해 상태 데이터인 프로퍼티를 참조하고 조작하는 동작(behavior)을 표현한다. 그런데 프로퍼티는 객체마다 값이 다를 수 있으나 메소드는 동일한 경우가 일반적이다.
  • 객체 리터럴에 의해 객체를 생성하면 프로퍼티 구조가 동일함에도 불구하고 매번 같은 프로퍼티와 메소드를 기술해야 한다.

생성자 함수에 의한 객체 생성 방식의 장점

객체(인스턴스)를 생성하기 위한 템플릿(클래스)처럼 생성자 함수를 사용하여 프로퍼티 구조가 동일한 객체 여러 개를 간편하게 생성할 수 있다. 생성자 함수를 생성할 때는 파스칼케이스를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//생성자 함수로 객체 생성
function Circle(radius) {
// 생성자 함수 내부의 this는 생성자 함수가 생성할 인스턴스
this.radius = radius;
this.getDiameter = function () {
return 2 * this.radius;
};
}

// 인스턴스 생성
const circle1 = new Circle(5);
const circle2 = new Circle(10);
const circle3 = new Circle(15);

console.log(circle1.getDiameter()); //10
console.log(circle2.getDiameter()); //20
console.log(circle3.getDiameter()); //30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//Factory Function
function createCircle(radius) {
return {
radius,
draw() {
console.log("draw");
},
};
}

const myCircle = createCircle(1);
console.log(myCircle);

//Constructor Function
function Circle(radius) {
this.radius = radius;
// this는 실행할 코드를 참조하는 것. 빈 객체를 참조한다.
// reference to the object that is executing this piece of code.
this.draw = function () {
console.log("draw");
};
//return this;
}

const circle = new Circle(1);
console.log(circle);
  1. new 생성자가 빈 자바스크립트 객체를 만든다. (ex. const x = {};)
  2. this는 new로 생성된 빈 객체를 가리키고, 이 this로 빈 객체에 접근한다.
  • this는 객체 자신의 프로퍼티나 메소드를 참조하기 위한 자기 참조 변수
  • this가 가리키는 값(this 바인딩)은 함수 호출 방식에 따라 동적으로 결정된다.
    • 일반 함수로서 호출 : this가 가리키는 값 -> 전역 객체(브라우저 환경에서는 window, Node.js 환경에서는 global)
    • 메소드로서 호출: this가 가리키는 값 -> 메소드를 호출한 객체
    • 생성자 함수로서 호출: this가 가리키는 값 -> 생성자 함수가(미래에) 생성할 인스턴스
  1. new 생성자가 위의 객체로부터 새로 생성된 객체를 반환한다. (return this;처럼)

일반 함수와 동일한 방법으로 함수를 정의하고, new 연산자와 함께 호출하면 생성자 함수로 동작한다.


생성자 함수의 인스턴스 생성 과정

생성자 함수가 프로퍼티 구조가 동일한 인스턴스를 생성하기 위한 템플릿(클래스)으로서 동작하여 인스턴스를 생성하는 것은 필수, 생성된 인스턴스를 초기화하는 것은 옵션이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Circle(radius) {
// 1. 암묵적으로 빈 객체(인스턴스)가 생성되고 this에 바인딩된다.
// console.log(this) -> Circle {}

// 2. this에 바인딩되어 있는 인스턴스를 초기화한다.
// this + 프로퍼티 or 메소드
this.radius = radius;
this.getDiameter = function () {
return 2 * this.radius;
};

// 3. 암묵적으로 this를 반환한다.
// 명시적으로 원시 값을 반환하면 원시 값 반환은 무시되고, 암묵적으로 this가 반환된다.
return 100;
// return {} -> 명시적으로 객체를 반환하면 암묵적인 this 반환이 무시된다.
}

// 4. 인스턴스 생성.
// return 100의 경우 -> Circle 생성자 함수는 명시적으로 반환한 객체를 반환한다. -> Circle {radius: 1, getDiameter: ƒ}
//return {}의 경우 -> 명시적으로 반환한 {}(객체)를 반환한다. -> {}
const circle = new Circle(1);
console.log(circle);
  1. 인스턴스 생성과 this 바인딩: 암묵적으로 빈 객체가 생성되는데 이는 바로 생성자 함수가 생성한 인스턴스이며, 인스턴스는 this에 바인딩된다. 따라서 this는 생성자 함수가 생성할 인스턴스를 가리키는 것이다.
  2. 인스턴스 초기화: 생성자 함수에 있는 코드가 실행되어 this에 바인딩되어 있는 인스턴스를 초기화한다. 인스턴스에 프로퍼티나 메소드를 추가하고 생성자 함수가 인수로 전달받은 초기값을 인스턴스 프로퍼티에 할당하여 초기화하거나 고정값을 할당한다.
  3. 인스턴스 반환: 생성자 함수 내부의 모든 처리가 끝나면 완성된 인스턴스에 바인딩된 this가 암묵적으로 반환된다. 만약 명시적으로 this가 아닌 다른 객체를 반환하면 this는 반환되지 않고 return문에 명시한 객체가 반환된다. 그러나 명시적으로 원시값을 반환하면 이는 무시되고, 암묵적으로 this가 반환된다. 이처럼 this가 반환하는 값이 변경되기 때문에 return문을 반드시 생략한다.

내부 메소드 [[Call]]과 [[Construct]]

함수는 객체이므로 일반 객체와 동일하게 동작할 수 있고, 일반 객체의 내부 슬롯과 내부 메소드를 모두 가지고 있다. 하지만 함수 객체는 반드시 callable이어야 한다.

  • callable: 내부 메소드 [[Call]]을 가지고 있다.
    호출할 수 있는 객체인 함수
  • constructor: 내부 메소드 [[Construct]]를 가지고 있다.
    생성자 함수로서 호출할 수 있는(new 연산자 또는 super 연산자와 함께 호출)객체
  • non-constructor: 내부 메소드 [[Construct]]를 가지고 있지 않다.
    객체를 생성자 함수로 호출할 수 없는 함수

일반적인 함수로 호출하면 함수 객체의 내부 메소드 [[Call]]가 호출되고, 생성자 함수로 호출되면 내부 메소드 [[Construct]]가 호출된다. 단, 생성자 함수로 정의하지 않은 일반 함수를 new 연산자와 함께 호출하면 생성자 함수로 동작한다. 즉 [[Construct]]가 호출된다.

함수 객체는 반드시 callable이지만 constructor일 수도 있고 non-constructor일 수 있다. 따라서 모든 함수 객체는 호출이 가능하지만 모든 함수 객체를 생성자 함수로서 호출할 수 있는 것은 아니다.

constructor: 함수 선언문, 함수 표현식, 클래스(클래스도 함수다)
non-constructor: 메서드(ES6 메서드 축약 표현), 화살표 함수


References

Questpond
Programming with Mosh
Rob Merrill
poiemaweb

객체

ECMAScript 사양에 따르면 객체는 다음과 같이 구성된다.

  • 내부 슬롯(Internal slots): 자바스크립트에서 접근할 수 없는 위치에 있는 저장소(storage)이며 only to operations in the specification.
  • 프로퍼티의 집합(A collection of properties): 각각의 프로퍼티는 키를 속성과 연결한다.(fields in a record). 프로퍼티 키는 string이나 symbol 중 하나다.

내부 슬롯과 내부 메소드

자바스크립트 엔진의 알고리즘을 설명하기 위해 ECMAScript 사양에서 사용하는 의사 프로퍼티(Pseudo property)와 의사 메소드(Pseudo method).

내부 슬롯과 내부 메소드는 외부로 공개된 객체의 프로퍼티가 아닌 엔진의 내부 로직이기 때문에 간접적으로 접근할 수 있는 일부 경우를 제외하고는 직접 접근하거나 호출할 수 없고, 이중대괄호([[]])로 묶인 이름으로 식별한다.

  • 내부 슬롯(Internal slots)
  • 메소드 슬롯(Method slot): 객체 조작을 위함 (프로퍼티 가져오기, 설정 등)
  • 데이터 슬롯(Data slot): 저장소(storage)가 있음. [[Prototype]], [[Extensible]], [[PrivateFieldValues]]

프로퍼티 어트리뷰트와 프로퍼티 디스크립터 객체

자바스크립트 엔진은 프로퍼티를 생성할 때, 프로퍼티의 상태[프로퍼티의 값(value), 값의 갱신 가능 여부(writable), 열거 가능 여부(enumerable), 재정의 가능 여부(configurable)]를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의한다.

  • 프로퍼티 어트리뷰트: 프로퍼티 어트리뷰트는 자바스크립트 엔진이 관리하는 내부 상태 값(meta-property)인 내부 슬롯([[Value]], [[Writable]], [[Enumerable]], [[Configurable]])이다. Object.getOwnPropertyDescriptor 메소드를 사용하여 간접적으로 확인할 수 있다.
  • 프로퍼티 디스크립터: 어트리뷰트의 특성을 자바스크립트 객체로 인코딩한다. 새로운 프로퍼티를 만들거나 이미 존재하고 있는 프로퍼티를 바꿀 수도 있다.
  • Object.getOwnPropertyDescriptor 메소드: 호출 시 첫번째 매개변수에는 객체의 참조를 전달, 두번째 매개변수에는 프로퍼티 키를 문자열로 전달한다.
1
2
3
4
5
6
7
const person = {
name: "Lee",
};

// 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체를 반환한다.
console.log(Object.getOwnPropertyDescriptor(person, "name"));
// {value: "Lee", writable: true, enumerable: true, configurable: true}

프로퍼티가 여러 개일 때 프로퍼티 어트리뷰트 정보를 알고 싶다면 getOwnPropertyDescriptors를 사용한다.


데이터 프로퍼티와 접근자 프로퍼티

  • 데이터 프로퍼티: 데이터를 저장. 키와 값으로 구성된 일반적인 프로퍼티. 키에 값을 연결
  • 접근자 프로퍼티: 자체적으로는 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수(Accessor function)로 구성된 프로퍼티. 값을 가져오거나 저장하기 위해 하나 혹은 두 개의 접근자 함수 (get, set)을 연결짓는다.
    getter/setter 함수가 있는데 getter은 get 어트리뷰트, setter은 set 어트리뷰트에 저장됨.

데이터 프로퍼티

데이터 프로퍼티는 프로퍼티 어트리뷰트를 갖는데, JS 엔진이 프로퍼티를 생성할 때 기본값으로 자동 정의된다.

  • [[Value]]: value. 프로퍼티 키로 프로퍼티 값에 접근하면 반환되는 값
  • [[Writable]]: writable. 프로퍼티 값의 변경 가능 여부를 나타내며 불리언 값을 갖는다. false인 경우 [[value]]의 값을 변경할 수 없는 읽기 전용 프로퍼티가 된다.
  • [[Enumerable]]: enumerable. 프로퍼티의 열거 가능 여부를 나타내며 불리언 값을 갖는다. [[Enumerable]]의 값이 false인 경우, 해당 프로퍼티는 for…in 문이나 Object.keys 메소드 등으로 열거할 수 없다.
  • [[Configurable]]: configurable. 프로퍼티의 재정의 가능 여부를 나타내며 불리언 값을 갖는다. 값이 false인 경우, 해당 프로퍼티의 삭제, 프로퍼티 어트리뷰트 값의 변경이 금지된다.

접근자 프로퍼티

자체적으로는 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수(Accessor function)로 구성된 프로퍼티다.

  • [[Get]]: get. 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수. 프로퍼티 어트리뷰트 [[Get]]의 값, 즉 getter 함수가 호출되고 그 결과가 프로퍼티 값으로 반환된다.
  • [[Set]]: set. 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수. 프로퍼티 어트리뷰트 [[Set]]의 값, 즉 setter 함수가 호출되고 그 결과가 프로퍼티 값으로 반환된다.
  • [[Enumerable]]: enumerable. 데이터 프로퍼티의 [[Enumerable]]와 같다.
  • [[Configurable]]: configurable. false라면 이 속성은 제거될 수 없고, 데이터 속성을 수정할 수 없다.

프로퍼티 정의

새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나, 기존 프로퍼티의 프로퍼티 어트리뷰트를 재정의하는 것. 객체의 프로퍼티가 어떻게 동작해야하는지를 명확히 정의할 수 있다. Object.defineProperty 메소드 사용

객체 변경 방지

객체는 변경 가능한 값이므로 재할당없이 직접 변경이 가능하다. 즉, 프로퍼티를 추가하거나 삭제할 수 있고, 프로퍼티의 값을 갱신할 수 있으며 Object.defineProperty 또는 Object.defineProperties 메소드를 사용하여 프로퍼티 어트리뷰트를 재정의할 수도 있다.

객체 확장 금지

Object.preventExtensions 메소드는 객체의 확장을 금지한다. 객체 확장 금지란 프로퍼티 추가 금지를 의미한다. 즉, 확장이 금지된 객체는 프로퍼티 추가가 금지된다. 확장이 금지된 객체인지 여부는 Object.isExtensible 메소드로 확인 할 수 있다.

객체 밀봉

Object.seal 메소드는 객체를 밀봉한다. 객체 밀봉(seal)이란 프로퍼티 추가 및 삭제와 프로퍼티 어트리뷰트 재정의 금지를 의미한다. 즉, 밀봉된 객체는 읽기와 쓰기만 가능하게 된다. 밀봉된 객체인지 여부는 Object.isSealed 메소드로 확인 할 수 있다.

객체 동결

Object.freeze 메소드는 객체를 동결한다. 객체 동결(freeze)이란 프로퍼티 추가 및 삭제와 프로퍼티 어트리뷰트 재정의 금지, 프로퍼티 값 갱신 금지를 의미한다. 즉, 동결된 객체는 읽기만 가능하게 된다. 밀봉된 객체인지 여부는 Object.isFrozen 메소드로 확인 할 수 있다.

불변 객체

지금까지 살펴본 변경 방지 메소드들은 얕은 변경 방지(Shallow only)로 직속 프로퍼티만 변경이 방지되고 중첩 객체까지는 영향을 주지는 못하다. 따라서 Object.freeze 메소드로 객체를 동결하여도 중첩 객체까지 동결할 수 없다.


References

MDN
2ality
ECMAScript

var 키워드로 선언한 변수의 문제점

  1. 변수 중복 선언 허용
1
2
3
4
5
var x = 1;
// var 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언을 허용한다.
// 아래 변수 선언문은 자바스크립트 엔진에 의해 var 키워드가 없는 것처럼 동작한다.
var x = 2;
console.log(x); // 2

var 키워드는 중복 선언을 허용하기 때문에 위와 같이 변수가 이미 선언되어 있는 것을 모르고 중복 선언 및 할당을 하면 의도치 않게 변수값이 변경된다.

  1. 함수 레벨 스코프

함수의 코드 블록만이 지역 스코프로 인정되며, 함수 내부가 아닌 곳에서 var 키워드로 선언한 변수는 코드 블록 내에서 선언된다고 하여도 전역 변수이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var x = 1;

if (x === 1) {
var x = 2;

console.log(x);
// expected output: 2
}

function test() {
var x = 3;
console.log(x);
}

test();

console.log(x);
// expected output: 2

위 코드에서 if문 내의 var x = 2;는 코드 블록 내에 있어서 지역 변수인 것처럼 보이지만 var 키워드는 함수의 코드 블록만을 지역 스코프로 인정하기 때문에, 전역변수이다. for문이나 if문 등에서 선언된 var 키워드도 전역 변수이다. 따라서 전역 변수 x가 중복 선언된 것이 되므로 의도치 않게 x의 값이 1에서 2로 변경된다. 하지만 test()는 함수이므로 지역 스코프로 인정되기 때문에 중복선언이 이루어지지 않고, 함수 지역 스코프내의 값인 3이 그대로 출력된다.

  1. 변수 호이스팅

var키워드로 선언한 변수는 어디에 선언되어 있든지 런타임 이전에 먼저 실행되는 변수 호이스팅이 발생한다. 따라서 선언문 이전에도 값을 참조할 수 있다. 다만 할당문 이전에 변수를 참조하면 초기화된 값인 undefined를 반환한다.

1
2
3
4
5
6
7
console.log(foo); //undefined

foo = 123;

console.log(foo); //123

var foo;

var 키워드로 선언된 변수는 런타임 이전에 먼저 실행되므로 할당 전에 호출하여도 에러가 발생하지 않고, undefined라는 초기화된 값을 출력한다. 변수에 값을 할당하고, 값을 호출하면 그때부터는 할당된 값이 출력된다.


let 키워드

  1. 변수 중복 선언 금지

var키워드는 동일한 이름의 변수를 중복 선언해도 에러가 발생하지 않고, 마치 재할당되는 것처럼 동작한다. 그러나 let은 동일한 이름의 변수를 중복 선언하면 이미 선언되었다는 에러가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
//var 키워드
var x = 1;
var x = 2;
console.log(x); //2
//동일한 변수를 중복 선언해도 에러 발생 x

//let 키워드
let y = 1;
let y = 2;
console.log(y); //SyntaxError
//동일한 변수 중복 선언 시 에러 발생
  1. 블록 레벨 스코프

모든 코드 블록(함수, if 문, for 문, while 문, try/catch 문 등) 을 지역 스코프로 인정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//var 키워드
var x = 1;
if (x > 0) {
console.log(x); //1
var x = 2;
}
console.log(x); //2

//let 키워드
let x = 1;
if (x > 0) {
console.log(x); //undefined
let x = 2;
}
console.log(x); //1

var 키워드는 함수 레벨 스코프를 가지기 때문에 if문의 코드블록 내에 있어도 전역변수이다. x의 값이 2로 출력된다. let 키워드는 블록 레벨 스코프를 가지기 때문에 if문을 지역 스코프로 인정한다. 따라서 if문 블록 내의 코드블록은 지역변수이다. 따라서 if문 내의 x는 코드블록 내에서만 값이 유효하다. 그런데 let x = 2라고 선언되기 이전에 호출되었으므로 undefined가 출력된다. 그러나 if문 밖에 있는 x는 if문 내의 x와 다른 값이기 때문에 1이라는 값이 출력된다.

  1. 변수 호이스팅

let 키워드를 런타임 이전에 참조하면 에러가 발생해서 변수 호이스팅이 발생하지 않는 것처럼 보이지만 그렇지 않다. let 키워드는 선언 단계와 초기화 단계가 분리되어 실행된다. 런타임 이전에 암묵적으로 선언이 실행되고, 초기화 단계는 변수 선언문에 도달했을 때 실행된다. (var 키워드는 런타임 이전에 선언과 초기화 단계가 한번에 진행되기 때문에 런타임 이전에 참조가 가능하고, undefined를 반환한다.)

일시적 사각지대 (Temporal Dead Zone; TDZ)
스코프의 시작 지점부터 초기화 시작 지점(변수 선언문)까지는 변수를 참조할 수 없고, 이 구간을 일시적 사각지대라고 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// var 키워드로 선언한 변수
console.log(foo); // undefined 런타임 이전에 참조가능
//선언과 초기화가 런타임 이전에 동시에 이루어진다.

var foo;
console.log(foo); // undefined

foo = 1; // 할당문에서 할당 단계가 실행된다.
console.log(foo); // 1

// let 키워드로 선언한 변수

console.log(bar); // ReferenceError: bar is not defined
//런타임 이전에 선언 단계만 실행된다.
//변수 초기화가 아직 이루어지지 않았으므로 참조가 불가하다.

let bar; // 변수 선언문에서 초기화 단계가 실행된다.
console.log(bar); // undefined

bar = 1; // 할당문에서 할당 단계가 실행된다.
console.log(bar); // 1

선언단계는 scope의 가장 상단에서 실행되며, scope에 식별자를 등록하는 것이다. var의 경우 식별자를 등록하고, undefined라는 초기값을 할당하는 것까지의 과정이 런타임 이전에 이루어진다. 하지만 let은 변수를 선언하여 scope에 식별자를 등록하는 것까지는 동일하게 런타임 이전에 이루어지고, 변수 선언문을 만났을 때 비로소 undefined로 초기화된다. 그 전까지는 undefined도 아니고, 참조도 할 수 없는 일시적 사각지대라는 구간을 가지고 있다. 따라서 참조할 수 있는 값이 존재하지 않는 상태이기 때문에 변수 선언문 이전에 호출하면 referenceError가 발생한다.

  1. 전역 객체와 let

전역 객체: 코드가 실행되기 이전 단계에 자바스크립트 엔진에 의해 어떤 객체보다도 먼저 생성되는 특수한 객체이며 어떤 객체에도 속하지 않은 최상위 객체

클라이언트 사이드 환경(브라우저)에서는 window, 서버 사이드 환경(Node.js)에서는 global 객체를 가리킨다.

  • var 키워드로 선언한 전역 변수는 전역 객체 window의 프로퍼티 ⭕️
  • let 키워드로 선언한 전역 변수는 전역 객체 window의 프로퍼티 ❌
1
2
3
4
5
6
7
8
//브라우저 환경의 경우
var x = 1;
console.log(window.x); //1

let x = 1;
console.log(window.x); //undefined
//let은 전역 객체의 프로퍼티가 아니기 때문에 참조불가
//const도 마찬가지로 전역 객체의 프로퍼티가 아니다.

const 키워드

  1. 선언과 초기화

const 키워드는 상수를 선언하기 위해 사용하고, 반드시 선언과 동시에 할당이 이루어져야 한다. let처럼 블록 레벨 스코프이며, 변수 호이스팅이 발생하지 않는 것처럼 동작하지만 실질적으로 발생하는 것이다.

  1. 재할당 금지

const는 재할당이 자유로운 var,let과 달리 재할당이 금지된다. 그러나 변수에 객체를 할당한 후 객체의 프로퍼티 값을 변경(추가, 삭제, 변경 등)하는 것은 가능하다.

  1. const가 마주할 수 있는 에러
1
2
3
4
5
6
7
8
9
10
11
12
13
console.log(goo); //1️⃣

const goo; //2️⃣
console.log(goo);

goo = 1; //3️⃣

const goo = 1;
console.log(goo);
const goo = 2; //4️⃣
console.log(goo);

console.log(goo);
  • 1️⃣Uncaught SyntaxError: Missing initializer in const declaration
    선언 전에 참조하면 초기화 되지 않았다는 에러가 발생한다.
  • 2️⃣Uncaught ReferenceError: Cannot access ‘goo’ before initialization
    const는 선언과 할당이 동시에 이루어져야 한다. 따라서 선언만 이루어졌을 경우 이러한 에러를 만난다.
  • 3️⃣Uncaught TypeError: Assignment to constant variable
    앞서 말했듯이 선언과 할당이 동시에 이루어져야 하는데 할당만 이루어지고 있으므로 에러가 발생한다.
  • 4️⃣Uncaught SyntaxError: Identifier ‘goo’ has already been declared
    const는 재할당을 금지하므로 에러가 발생한다.

var vs let vs const

var: 함수 블록만을 지역 스코프로 인정, 그외의 경우 모두 전역 스코프를 가짐.
let: 블록 레벨 스코프를 가지므로, 선언된 블록 내 및 하위 블록에서만 값이 유효.
const: 블록 레벨 스코프를 가진다. 재할당 금지, 그러나 객체는 재할당 가능.

세 가지 키워드 모두 변수 호이스팅이 발생한다. let, const는 선언 이전에 참조하면 ReferenceError가 발생하여 변수 호이스팅이 발생하지 않는 것처럼 보이지만 사실 변수 호이스팅이 발생하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function varTest() {
var x = 1;
if (true) {
var x = 2; // 상위 블록과 같은 변수
console.log(x); // 2
}
console.log(x); // 2
}

function letTest() {
let x = 1;
if (true) {
let x = 2; // 상위 블록과 다른 변수
// x = 3; -> 재할당 허용
console.log(x); // 2
}
console.log(x); // 1
}

function constTest() {
const x = 1;
if (true) {
const x = 2; //상위 블록과 다른 변수
// x = 3; => TypeError 재할당 불가
console.log(x); // 2
}
console.log(x); // 1
}

function constTest() {
const x = 1;
if (true) {
const x = {
num: 1,
};
x.num = 2; // 객체는 재할당 가능
console.log(x);
}
console.log(x);
}

References

MDN
MDN
Poiemaweb

함수란?

함수는 함수 정의와 호출, 실행의 일련의 과정을 거쳐 값을 반환하는 것으로 이 일련의 과정은 문(statement)들로 구현하고, 코드 븍록으로 감싸서 하나의 실행단위로 정의한다.

1
2
3
4
5
6
7
8
9
function 함수명 (매개변수) {
return 반환값;
} //함수 정의(함수 몸체)
함수명(인수) //함수 호출

function add(x,y) {
returen x + y;
}
add(2,5);
  1. 함수 정의를 통해 함수를 생성한다. 위와 같이 호출하기 이전에 인수를 전달받을 매개변수와 실행할 문들, 반환할 값을 지정하는 것을 말한다.
  • 함수 선언문
  • 함수 표현식
  • 생성자 함수
  • 화살표 함수
  1. 함수 객체를 가리키는 식별자(함수 이름으로 호출 x)로 호출한다.
  2. 함수 외부에서 내부로 값을 전달할 필요가 있다면, 인수(argument)는 매개변수(parameter)를 통해 전달된다.
  3. 값을 전달받으면 함수 내부에서 미리 정의된 문들을 실행하여 값을 반환한다.
  • 반환문은 함수의 실행을 중단하고, 함수 몸체를 빠져나간다.
  • return 키워드 뒤에 지정한 값을 반환한다.

함수선언문과 함수표현식

함수선언문과 함수표현식은 함수 호이스팅 vs 변수 호이스팅의 차이가 있다. 또한 생성시점이 다르기 때문에 함수선언문은 선언문 이전에 호출이 가능하고, 함수표현식은 표현식 이전에 호출이 불가능한다.

  • 함수선언문: 런타임 이전에 함수 객체가 먼저 생성되고, 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고 생성된 함수 객체를 할당한다. 따라서 선언문 이전에 함수의 참조와 호출 모두 가능하다.
1
2
3
4
5
6
console.dir(add); // ƒ add(x, y) 함수참조
console.log(add(2, 5)); // 7 함수호출

function add(x, y) {
return x + y;
}

위와 같이 함수를 선언하면, 런타임 이전에 이미 함수 객체가 생성되어 할당까지 완료된 상태이므로, 선언문을 실행하는 런타임 이전에 값을 참조 또는 호출하여도 값이 반환된다.

  • 함수표현식: 함수표현식은 변수 호이스팅과 동일하다. 변수 선언문과 할당문의 축약표현과 동일하게 동작한다. 런타임 이전에 함수표현식은 함수 객체가 아닌 undefined로 초기화된다. 그리고 런타임 때 평가되어 함수 리터럴은 할당문이 실행되는 시점에 비로소 함수 객체가 된다.
1
2
3
4
5
6
console.dir(sub); // undefined 함수참조
console.log(sub(2, 5)); // TypeError: sub is not a function 함수호출

var sub = function (x, y) {
return x - y;
};

위와 같이 런타임 이전에 함수를 참조하면 undefined가 반환된다. 런타임 이전에 함수표현식은 undefined라는 값으로 초기화되어 있기 때문이다. 또한 값의 할당은 런타임 때 이루어지기 때문에 표현식 이전에 함수를 호출하면 TypeError가 발생한다.

변수의 생명 주기

변수는 자신이 선언된 위치에서 생성하고 소멸된다.

  • 전역변수: 전역 변수의 생명 주기 = 어플리케이션의 생명 주기. 전역변수는 런타임 이전에 코드가 어디 있던지 상관 없이 가장 먼저 실행된다.
  • 지역변수: 함수 내부에 선언된 지역 변수는 함수 호출 시 생성되어 함수가 종료되면 소멸된다. 따라서 함수를 호출하지 않으면 함수 내부의 변수 선언문은 실행되지 않는다. 지역변수는 함수가 호출된 직후 함수 몸체의 다른 코드가 실행되기 이전에 먼저 실행된다.

지역 변수의 생명 주기 = 함수의 생명 주기

1
2
3
4
5
6
7
8
function foo() {
var x = "local";
console.log(x); // local
return x;
}

foo();
console.log(x); // ReferenceError: x is not defined

함수 내부에 선언된 지역 변수 x는 foo 함수가 호출되어 실행되는 동안에만 유효하다.

호이스팅: 스코프를 단위로 동작
전역 변수 호이스팅: 전역 변수의 선언이 전역 스코프의 선두로 끌어올려진 것처럼 동작
지역 변수 호이스팅: 지역 변수의 선언이 지역 스코프의 선두로 끌어올려진 것처럼 동작

1
2
3
4
5
6
7
8
9
10
11
var x = "global";

function foo() {
console.log(x); //여기 위치한다면 x는 undefined
var x = "local";
console.log(x); // 여기 위치하면다면 x는 local
return x;
}

foo();
console.log(x); // global

전역 변수는 런타임 이전에 가장 먼저 실행되고, 지역 변수는 함수 몸체의 다른 문들이 실행되기 전에 변수 x가 선언되어 undefined로 초기화된다. 따라서 변수 할당문이 실행되기 이전에는 undefined, 할당문이 실행된 후에는 할당된 값이 출력된다.


전역 변수의 생명 주기

전역 코드는 함수 호출과 같이 전역 코드를 실행하는 특별한 진입점(entry point)이 없고 코드가 로드되자마자 곧바로 해석되고 실행된다. 전역 코드에는 return 문을 사용할 수 없으므로 마지막 문이 실행되어 더 이상 실행할 문이 없을 때 종료한다. 전역 변수는 전역 객체의 프로퍼티가 되기 때문에 전역 변수의 생명 주기는 전역 객체의 생명 주기와 일치한다. 전역 객체(Global Object)는 코드가 실행되기 이전 단계에 자바스크립트 엔진에 의해 생성되는 특수한 객체이다.

전역 객체
코드가 실행되기 이전 단계에 자바스크립트 엔진에 의해 생성되는 특수한 객체

  • 브라우저: window
  • Node.JS: global
    (window와 global을 합친 globalThis도 있다.)

모든 전역함수는 전역스코프에 등록되고, 복잡한 과정을 거쳐 전역객체의 메소드가 된다.

전역 변수의 문제점

  • 암묵적 결합: 모든 코드가 전역 변수를 참조하고 변경할 수 있다.
  • 긴 생명 주기: 메모리 리소스를 오랜 기간 소비하므로 의도치않은 재할당이 이루어질 수 있다.
  • 느린 검색 속도: 스코프 체인 상에서 종점에 존재하기 때문에 변수를 검색할 때 가장 마지막에 검색된다.
  • 네임 스페이스 오염: 파일이 분리되어 있어도 하나의 전역 스코프를 공유하므로 다른 파일 내에 동일한 이름의 변수나 함수가 같은 스코프 내에 있다면 예상치 못한 결과가 있을 수 있다.
1
2
3
4
5
6
7
var x = 1;

//코드가 여러 줄 있다고 가정.

var x = 2;

console, log(x); //2

전역변수는 생명주기가 길고, 전역 어디에서든 참조할 수 있으므로 의도치않게 값을 변경할 가능성이 높다. 위와 같이 var x = 1;이라고 선언한 후, 여러 줄의 코드를 작성했다고 가정했을 때, var x로 선언된 것을 모르고, 다시 한번 var x를 선언하면, var는 중복선언을 허용하기 때문에 값이 재할당되는 부작용이 발생한다. 즉, var x = 1;이라고 선언한 후 var x =2;라고 다시 한번 중복 선언하게 되면 x =2;인 것처럼 동작하여 기존 변수에 값을 재할당한다. 하지만 var x;와 같이 사용하면 이는 무시된다.

전역 변수 사용 억제 방법

전역 변수가 필요한 특별한 이유가 없다면 변수의 스코프는 좁을수록 좋기 때문에 지역 변수를 사용한다.

  1. 즉시 실행 함수: 함수 정의와 동시에 단 한 번 호출되는 함수. 모든 코드를 즉시 실행 함수로 감싸면 모든 변수는 즉시 실행 함수의 지역 변수가 된다.
1
2
3
4
(function () {
console.log("hello!");
})();
//따로 호출하지 않아도 바로 실행되어 "hello!"라는 값을 반환한다.
  1. 전역에 네임 스페이스(Namespace) 역할을 담당할 객체를 생성하고 전역 변수처럼 사용하고 싶은 변수를 프로퍼티로 추가
  2. 모듈 패턴: 클래스를 모방하여 관련이 있는 변수와 함수를 모아 즉시 실행 함수로 감싸 하나의 모듈을 만든다.
  3. ES6 모듈: 파일 자체의 독자적인 모듈 스코프를 제공한다. script태그에 type="module"를 추가한다.
    1
    2
    <script type="module" src="app.mjs"></script>
    <!-- 확장자는 mjs를 쓰는 것이 좋다. -->

References
poiemaweb

암묵적 타입 변환과 명시적 타입 변환

암묵적 타입 변환: 자바스크립트 엔진이 개발자의 의도와는 상관없이 문맥에 따라 타입을 문자열, 숫자와 같은 원시타입의 값으로 강제 변환하는 것.

1
2
3
4
1 + '1' = ?
//숫자 1과 문자열 1을 더하면 에러가 날 것 같지만
//자바스크립트 엔진은 피연산자가 모두 문자열일 것이라고 간주하여
//둘을 더한 `문자열 11`이라는 결과를 반환한다.

명시적 타입 변환: 개발자의 의도에 따라 타입을 강제 변환하는 것으로 표준 빌트인 생성자 함수(string,number,boolean)을 new 연산자 없이 호출하는 방법, 빌트인 메소드를 사용하는 방법, 암묵적 타입 변환을 이용하는 방법 등이 있다.

1
2
console.log(1 + "");
//숫자 타입 1에 빈 문자열을 더하면 `문자열 1`이라는 결과가 출력된다.

단축평가: 논리곱, 논리합

단축평가는 표현식을 평가하는 도중에 결과가 확정되면 나머지 평가 과정을 중단하는 것이다. 논리곱과 논리합은 언제나 2개의 피연산자 중 어느 한쪽으로 평가된다.

논리곱(&&)
논리곱은 두 개의 피연산자가 모두 true일 때만 true를 반환한다. 둘 중 하나라도 false일 때는 false가 반환된다.

true && anything //anything
false && anything //false

1
"cat" && "dog"; //dog

위의 예시와 같은 상황에서 첫 번째 피연산자인 cat을 평가했을 때 true로 반환되지만 논리곱(&&)은 두 피연산자 모두 true일때 true이므로 두 번째 피연산자까지 평가를 해보아야한다. 따라서 두 번째 피연산자인 dog을 검사한 후 dog를 반환한다.

논리합(||)
논리합은 두 개의 피연산자 중 하나만 true여도 true를 반환한다. 두 개의 피연산자 모두 false인 경우에는 false를 반환한다.

true || anything //true
false || anything //anything

1
"Cat" || "Dog"; // 'Cat'

위의 예시와 같은 상황에서 첫 번째 피연산자인 cat을 평가했을 때 true이기 때문에 두 번째 피연산자인 Dog를 평가하지 않고 바로 Cat을 반환한다. 논리합(||)은 두 피연산자 중 하나만 true여도 true를 반환하기 때문이다.

객체-객체 리터럴

객체: 다양한 타입의 값(원시 값 또는 다른 객체)들을 하나의 단위로 구성한 복합적인 자료 구조로 키(key) 값(value)으로 구성된 프로퍼티(property)들의 집합이라고 할 수 있다.

1
2
3
키워드 식별자 = {
key(속성) : value(값) //프로퍼티
}

객체 리터럴: 객체를 생성하는 가장 간단한 방법이다. 중괄호 내에 0개 이상의 프로퍼티를 정의하고, 값으로 평가되는 표현식이기 때문에 중괄호 뒤에 세미콜론을 붙여야한다.

1
var empty = {};

값에 의한 전달, 참조에 의한 전달

원시 값은 변경 불가능한 값이기 때문에 값을 복사하여 전달하고, 객체는 변경이 가능한 값이기 때문에 원본의 참조 값을 복사하여 전달한다.

값에 의한 전달

1
2
3
4
5
var score = 80;
copy = score;

console.log(score); //80
console.log(copy); //80

score라는 변수에 숫자값 80을 할당하고, copy라는 변수에 변수 score를 할당하면 원래 있던 score가 가지고 있던 숫자값 80이 복사되어 새롭게 할당되는 변수 copy에 전달된다. 원시값은 변경이 불가능하기 때문에 재할당 시 새로운 메모리 공간을 차지하는 것이다. 즉 score의 80과 copy의 80은 다른 메모리 공간에 저장된 다른 값이다.

1
2
3
4
socre = 100;

console.log(score); //100
console.log(copy); //80

score에 새로운 숫자 값 100을 재할당하면 100이라는 값을 가진 변수 score가 새로운 메모리 공간에 저장된다. 따라서 score의 값을 변경했다고 하여 copy도 변경되는 것이 아니라 score의 값만 변경된다.

참조에 의한 전달
객체는 변경 가능한 값이기 때문에 변수에 객체를 할당하면 생성된 객체는 참조값을 가진다. 이 참조값은 변수에 생성된 객체가 실제로 저장된 메모리 공간의 주소를 말한다. 따라서 변수 참조 시 원시 값에 바로 접근하는 원시값과 달리 객체는 메모리에 저장되어 있는 값을 참조하여 실제 객체에 접근한다.

1
2
3
var person = {
name: "lee",
};

person이라는 식별자가 붙은 공간에 참조값이 생성되고, 이 참조값은 객체를 가리키고 있다. 따라서 console.log(person);라고 하면 참조값이 객체에 접근하여 {name:'lee'}를 반환한다. 재할당이 이루어지지 않는다면 객체에 동적으로 값을 추가하여도 참조값은 여전히 동일하며 객체에만 값이 추가된다.

그런데 만약 참조 값을 복사한다면 어떻게 될까?

1
2
3
4
5
var person = {
name: "lee",
};

var copy = person;

위와 같이 person을 다른 변수 copy에 할당하면 객체이기 때문에 원본 값이 아닌 원본의 참조 값이 복사되어 전달된다. 새롭게 생성된 변수 copy는 copy라는 식별자가 붙은 새로운 참조값(메모리 주소가 다르다.)을 가지고 있지만 동일한 객체 {name:'lee'}를 가리키고 있다. 따라서 어느 한쪽이 값을 변경하면 서로에게 영향이 생긴다.

다음과 같은 중첩함수가 있을 때, console.log에는 어떤 값이 출력될까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function outer() {
let x = 3;
// 중첩 함수
function inner() {
let y = 2;
// 외부 함수의 변수를 참조할 수 있다.
console.log(x + y);
}

return inner;
}

const content = outer();
content();

outer 함수는 변수 content의 이름으로 할당되었는데, 이 함수가 반환하는 값은 let x = 3이 아니라 inner이다. outer 내부의 inner 함수가 return값으로 설정되어 있기 때문이다. 따라서 inner 함수내부에서 실행한 x+y의 값인 5가 콘솔에 출력된다.

그런데 의아한 부분이 있다.

inner 함수 내부에는 let y = 2의 y값만 할당되어 있고, x값은 할당되어 있지 않다. 그런데 어떻게 두 값이 x+y로 연산되어 출력된 것일까?

가장 먼저 생성된 outer 함수가 호출되면 이 함수는 stack의 가장 아래에 쌓인다. (stack 가장 마지막에 들어온 것이 가장 먼저 나가는 구조이다.)
그 다음 inner 함수를 호출하면 이 함수는 outer의 위에 쌓인다. 그런데 변수 content에 outer 함수를 호출했을 때 반환하는 값은 inner 함수이다. 결국 content = inner 함수가 되는 셈이다.

그래서 실행 컨텍스트에 따라 inner함수를 살펴본다. inner 함수 내부에는 콘솔에 x+y의 값이 찍히도록 되어 있지만 y의 값만 존재한다. 그럼 이 함수는 바로 상위로 향하여 x값이 존재하는지를 살펴본다. 만약 존재한다면 그 값을 참조하여 x + y의 값을 출력한다. 하지만 만약 여기서도 x값이 존재하지 않는다면 그보다 상위 개념에 x값이 존재하는지를 살펴보았을 것이다. 이처럼 자바스크립트의 스코프는 함수의 중첩에 따라 계층적 구조를 가지기 때문에 하위 스코프인 inner 함수에 필요로 하는 식별자가 없다면 그보다 상위 스코프이자 outer environment인 outer 함수에 식별자가 있는지 확인하고 있다면 그 값을 참조한다.

그리고 모든 스코프의 가장 최상위에는 전역 스코프가 존재한다.

그렇다면, 전역 스코프에 변수가 할당되어 있다면 어떻게 될까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let x = 1;
function outer() {
let x = 3;
// 중첩 함수
function inner() {
let y = 2;
// 외부 함수의 변수를 참조할 수 있다.
console.log(x + y);
}

return inner;
}

const content = outer();
content();

outer 함수의 상위인 전역 스코프에 let x = 1이라는 값이 할당되어 있다. 이때 x+y의 값은 콘솔에 어떻게 출력될까? stack의 구조와 상위 구조로 가면서 변수 식별자를 찾는 것에 따라 x + y의 값은 이제 1 + 3을 한 4라는 값이 찍힐까?

이 경우에는 함수가 한 번 호출되어 실행 컨텍스트를 한 번 돌아서 종료되면 stack에 저장되어 있는 값은 사라진다는 개념을 알아야한다. (해제된다는 개념이 더 정확할 것이다.) 만약 let x = 1;이 전역 스코프로 작성되고, outer 함수 내부에는 변수 x에 대한 할당이 따로 없었다면 3이라는 결과가 출력되었을 것이다.