본문 바로가기
Javascript study

[JS] 렉시컬 환경, this 정리

by 카누가 좋아요 2023. 8. 23.

📌 참고 사이트

엘리스 3주차 수요일 자료 'This에 대한 활용 예시'

 

변수의 유효범위와 클로저 (javascript.info)

 

변수의 유효범위와 클로저

 

ko.javascript.info

 

 

 

❓ 렉시컬 환경

자바스크립트에서는 실행 중인 함수, 코드 블록 {...}, 스크립트 전체는 렉시컬 환경(Lexical Environment)라고 불리는 내부 숨김 연관 객체를 갖는다.

(렉시컬 환경은 JS 동작 원리 설명을 위해 쓰이는 이론상의 객체로, 코드를 통해 직접 렉시컬 환경을 얻거나 조작하는 것은 불가능하다.)

 

 

📍 렉시컬 환경 객체의 구성

➡️ 환경 레코드 (Environment Record)

모든 지역 변수프로퍼티로 저장하고 있는 객체이다. this값과 같은 기타 정보도 여기에 포함된다.

변수는 특수 내부 객체인 환경 레코드의 프로퍼티이다.

변수를 가져오거나 변경하는 것은 환경 레코드의 프로퍼티를 가져오거나 변경함을 의미한다.

 

➡️ 외부 렉시컬 환경 (Outer Lexical Environment) 에 대한 참조

외부 코드와 연관됨.

 

 

📍 전역 렉시컬 환경 (global Lexical Environment)

스크립트 전체와 관련된 렉시컬 환경을 말한다.

 

아래와 같은 코드에는 렉시컬 환경이 하나만 존재한다.

 

 

네모 상자변수가 저장되는 환경 레코드를 나타내고, 화살표외부 렉시컬 환경에 대한 참조를 나타낸다.

전역 렉시컬 환경은 외부 참조를 갖지 않기 때문에 화살표가 null을 가리킨다.

 

 

📍 렉시컬 환경의 변화

다음과 같이 코드가 실행되고 실행 흐름이 이어져 나가면서 렉시컬 환경은 변화하게 된다.

 

 

1️⃣ 스크립트가 시작되면, 스크립트 내에서 선언한 변수 전체가 렉시컬 환경에 올라간다.

(이때의 변수 상태는 특수 내부 상태인 uninitialized가 된다. JS 엔진은 이 상태의 변수를 인지하지만 아래의 let 예약어를 만나기 전까지는 이 변수를 참조할 수 없다.)

 

2️⃣ let phrase;를 통해 phrase 변수가 선언되었지만 값이 할당되기 전이므로 프로퍼티 값은 undefined가 된다.

 

3️⃣ phrase = "Hello"를 통해 phrase 변수에 값이 할당되고, phrase = "Bye"를 통해 값이 변경된다.

 

 

📍 함수 선언문

변수는 let 같은 예약어를 만나서 선언이 되기 전까지는 사용할 수 없지만, 함수 선언문(function으로 선언)으로 선언한 함수는 렉시컬 환경이 만들어지는 즉시 사용이 가능하다.

(let say = function(name).... 이런 식으로 함수를 변수에 할당한 표현식에서는 해당 X)

이것이 함수가 선언되기도 전에 사용할 수 있는 이유이다.

 

 

위 그림을 보면 execution start(스크립트 시작 부분)에서 렉시컬 환경 레코드 안에 uninitialized 상태의 변수와 say function이 포함되어 있는 것을 볼 수 있다.

 

 

📍 내부와 외부 렉시컬 환경

함수를 호출해서 실행하면 새로운 렉시컬 환경이 자동으로 만들어진다.

이 렉시컬 환경엔 함수 호출 시 넘겨받은 매개변수와 함수의 지역 변수가 저장된다.

 

 

say 함수에 인수로 'John'을 전달하여 호출하면 호출 동안에 호출 중인 함수를 위한 내부 렉시컬 환경내부 렉시컬 환경이 가리키는 외부 렉시컬 환경을 가지게 된다.

➡️ 예시의 내부 렉시컬 환경현재 실행 중인 함수인 say에 상응한다. say 함수의 매개변수인 name으로부터 유래한 프로퍼티 하나(name: 'John')만 존재하는 것을 볼 수 있다.

➡️ 내부 렉시컬 환경이 참조하는 외부 렉시컬 환경전역 렉시컬 환경이다. 이 환경에서는 phrase(let으로 선언된 변수)와 함수 say를 프로퍼티로 가진다.

 

💡 코드에서 변수에 접근할 때에는 먼저 내부 렉시컬 환경을 검색 범위로 잡는다. 내부 렉시컬 환경에서 원하는 변수를 찾지 못하면 검색 범위를 내부 렉시컬 환경이 참조하는 외부 렉시컬 환경으로 확장한다. 전역 렉시컬 환경에 도달할 때까지 변수를 찾지 못하면 엄격 모드에서는 에러가 발생한다.

(비엄격 모드에서는 하위 호환성을 위해 새로운 전역 변수가 만들어진다.)

 

바로 위 예시에서 alert문에서 변수 name은 내부 렉시컬 환경에서 찾아 접근할 수 있는데, 변수 phrase는 내부 렉시컬 환경에 존재하지 않기 때문에 외부 렉시컬 환경에서 찾아 접근한다.

 

 

📍 함수를 반환하는 함수

 

 

위의 예시의 경우에서도 makeCounter 함수를 호출하면 2개의 렉시컬 환경(전역 렉시컬 환경, makeCounter 함수 내부 렉시컬 환경)이 만들어진다.

위와 같은 경우 return count++ 의 한줄짜리 중첩 함수가 만들어진다.

함수[[Environment]] 라고 불리는 숨김 프로퍼티를 갖는데, 여기에 함수가 만들어진 곳의 렉시컬 환경에 대한 참조가 저장된다.

이 값은 함수가 생성될 때 딱 한 번 값이 세팅되고, 영원히 변하지 않으므로 함수 호출 장소와 상관 없이 함수가 자신이 태어난 곳을 기억할 수 있다.

 

 

실행 흐름이 중첩 함수 본문으로 들어가면, count 변수를 찾아야 한다. 먼저 해당 함수의 렉시컬 함수에서 찾는데 해당 함수에는 지역 변수가 존재하지 않기 때문에 해당 함수의 렉시컬 환경은 비어있다.

따라서 참조하는 외부 렉시컬 환경에서 count를 찾는다. let으로 선언된 count 변수가 존재하므로 이를 참조하여 count++가 실행되고 count 변수가 저장된 렉시컬 환경에서 값이 변경되게 된다.

 

 

 

 

📋 함수에서 사용되는 This

기본적으로 전역 컨텍스트에서 thiswindow를 가리킨다.

따라서 전역 렉시컬 환경에 있는 name도 접근이 가능하다.

 

const name= 'elice';
console.log(this);     // window {...}
console.log(name);     // elice

 

일반 함수 호출에서도 thiswindow를 가리킨다.

따라서 myFunc 함수 내부에 있는 name에 접근이 가능하다.

 

function myFunc() {
  const name = "elice";   
  console.log(this);
}

myFunc();     // window {...}
console.log(name);     // elice

 

 

 

📋 생성자 함수 호출

생성자 함수 내부의 this는 new 키워드를 통해 앞으로 만들어질 인스턴스 객체를 가리킨다.

 

function myFunc(name) {
  this.name = name;
  this.getName = function () {
    console.log('getName this:', this);
    return this.name;
  };
  console.log('myFunc this:', this);
}

const o = new myFunc('elice');     // myFunc this: myFunc {name: 'elice', getName: ƒ}
o.getName();     // getName this: myFunc {name: 'elice', getName: ƒ}

 

 

 

📋 객체에서 사용되는 this

객체에서의 this메소드를 호출한 객체를 가리킨다.

 

const o = {
  name: 'elice',
  myFunc: function () {
    console.log(this);
  },
};

o.myFunc();     // {name: 'elice', myFunc: ƒ}

 

but 객체 안 함수의 내부 함수에서의 this동적 바인딩으로 전역 객체를 가리킨다.

 

const o = {
  name: 'elice',
  myFunc: function() {
    return function () {console.log(this)}
  },
};

o.myFunc()();     // window {...}

 

 

 

📋 this 고정하기

1️⃣ 화살표 함수로 고정하기

생성자 함수로 만든 객체, 객체 리터럴 방식으로 만든 객체 안에서의 함수는 모두 내부에서 this자기 자신의 객체 (객체 최상위 스코프)를 가리키고 있다.

but 함수 내부에 함수를 만들고 this를 확인해 보면 둘 다 window를 가리키는 것을 볼 수 있다.

 

// 생성자 함수 방식
function createObejct() {
  this.myFunc = function () {
    console.log('myFunc this:', this);
    return function () {console.log('myFunc return this:', this)};
  };
}

const o = new createObject();     
o.myFunc()();     
// myFunc this: createObject {myFunc: ƒ}
// myFunc return this: Window {...}

 

// 객체 리터럴 방식
const o = {
  myFunc: function () {
    console.log('myFunc this:', this)
    return function () {console.log('myFunc return this:', this)}
  },
};

o.myFunc()();     
// myFunc this: {myFunc: ƒ}
// myFunc return this: Window {...}

 

다음과 같이 화살표 함수를 사용해 함수 안에 함수를 작성하면 객체 최상위 스코프를 가리키도록 유지한다.

 

// 생성자 함수 방식
function createObject() {
  this.myFunc = function () {
    console.log('myFunc this:', this);
    // 다음 부분을 화살표 함수로 변경함.
    return () => {console.log('myFunc return this:', this)};
  };
}

const o = new createObject();
o.myFunc()();

// myFunc this: createObject {myFunc: ƒ}
// myFunc return this: createObject {myFunc: ƒ}

 

// 객체 리터럴 방식
const o = {
  myFunc: function () {
    console.log('myFunc this:', this)
    // 다음 부분을 화살표 함수로 변경하였다.
    return () => {console.log('myFunc return this', this)};
  },
};

o.myFunc()();     
// myFunc this: {myFunc: ƒ} (o 객체의 내용 자체가 반환된 것이다.)
// myFunc return this {myFunc: ƒ}

 

* 화살표 함수를 가지고 메서드를 정의한 경우에는 this가 전역 객체를 가리킨다.

 

// 외부 함수를 작성하지 않고 화살표 함수만 작성한 경우
let user = {
  firstName: "보라",
  sayHi: () => {console.log(this.firstName);}
}

user.sayHi();     // undefined 
// (화살표 함수는 일반 함수와 달리 고유한 this를 가지지 않음. this를 출력해 보면 전역 객체 window가 나옴.)

 

2️⃣ bind 메서드로 고정하기

bind 메서드를 사용해서도 원하는 객체를 가리킬 수 있다.

 

// 생성자 함수 방식
function createObject() {
  this.myFunc = function () {
    console.log('myFunc this:', this);
    return function () {console.log('myFunc return this:', this)};
  };
}

const o = new createObject();
o.myFunc().bind(o)();     // bind 메서드를 사용해 o 객체로 고정시킨다.
// myFunc this: createObject {myFunc: ƒ}
// myFunc return this: createObject {myFunc: ƒ}

 

// 객체 리터럴 방식
const o = {
  myFunc: function () {
    console.log('myFunc this:', this);
    return function () {console.log('myFunc return this:', this)}
  },
}

o.myFunc().bind(o)();     // bind 메서드를 사용해 o 객체로 고정 후 함수를 실행

'Javascript study' 카테고리의 다른 글

[JS] 실행 컨텍스트 (2)  (0) 2023.08.25
[JS] 실행 컨텍스트 (1)  (0) 2023.08.25
[JS] FileReader  (0) 2023.08.21
[JS] innerText, innerHTML, textContent  (0) 2023.08.19
[JS] getElement~, querySelector~로 요소 검색하기  (0) 2023.08.18

댓글