본문 바로가기
코딩일기/TIL

TIL-7 javaScript 실행컨텍스트(스코프, 변수, 객체, 호이스팅) 와 this

by 2pro.e_pro 2024. 6. 21.
728x90
반응형

TIL-7 javaScript 실행컨텍스트(스코프, 변수, 객체, 호이스팅) 와 this

prologue. 변수 올라와라~ 호이~호이스팅~

 

코드를 이렇게 순서대로 해석하면 되겠군?

 

아니오 그건 틀렸습니다

호이스팅의 마법의 세계로 따라오시죠!

 

 

 

 

1. 실행컨텍스트

JavaScript의 실행컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체다.

JavaScript는 어떤 실행 컨텍스트가 활성화 되는 시점에 다음과 같은 일을 한다.

 

1. 선언된 변수를 위로 끌어 올리는 일 = 호이스팅

2. 외부 환경 정보 구성

3. this 값 설정

 

위와같은 현상들 때문에 JavaScript에서는 다른 언어와는 다른 특징들이 나타난다.

 

1- 1 실행 컨텍스트 란?

(이미지 출처 : https://velog.io/@leejuhwan/스택STACK과-큐QUEUE )

 

콜스택(call stack)

실행 컨텍스트란 실행할 코드에 제공할 환경 정보들을 모아놓은 객체다.

즉, 동일환경에 있는 코드를 실행할 때 필요한 환경정볻ㄹ을 모아 컨텍스트를 구성하고,

이것을 위 사진과 같이 스택의 한 종류인 콜스택에 쌓아 올린다.

 

가장 위에 쌓여있는 컨텍스트와 관련된 코드를 실행하는 방법으로 코드의 환경 및 순서를 보장 할 수 있다.

 

1- 2 컨텍스트의 구성

여러가지 구성 방법이 있겠지만, 이번 시간에는 함수만 생각하면 된다.

 

1. 전역공간

2. eval()함수

3. 함수(우리가 흔히 실행컨텍스트를 구성하는 방법)

 

실행 컨텍스트에 관련한 예제는 아래와 같다.

 

// ---- 1번
var a = 1;
function outer() {
	function inner() {
		console.log(a); //undefined
		var a = 3;
	}
	inner(); // ---- 2번
	console.log(a);
}
outer(); // ---- 3번
console.log(a);

 

위 코드는 콜스택에 쌓이는 실행 컨텍스트에 의해 순서가 보장되기 때문에 아래 순서로 진행이 된다.

코드실행 → 전역(in) → 전역(중단) + outer(in) → outer(중단) + inner(in) → inner(out) + outer(재개) → outer(out) + 전역(재개) → 전역(out) → 코드종료

 

결국 특정 실행 컨텍스트가 생성(활성화) 되는 시점이 콜 스택의 맨 위에 쌓이는(노출되는) 순간을 의미하며

곧 현재 실행할 코드에 해당 실행 컨텍스트가 관여하게 되는 시점을 의미한다.

 

 

1- 3 VariableEnvironment, LexicalEnvironment의 개요

VariableEnvironment(변수환경)과 LexicalEnvironment(어휘환경)의 개념을 알고 가면 좋은데

먼저 쉽게 설명을 하자면

 

VariableEnvironment (변수환경) 은 현재 컨텍스트 내의 식별자 정보(=record)를 갖고 있다.

가령 var a = 3 ; 의 경우 var a 를 의미하고, 외부환경정보(=outer)를 갖고있다.

선언시점 LexicalEnvironment(어휘환경) snapshot 이다.

 

스냅샷
  • 스냅샷은 특정 시점의 상태를 캡처하여 고정시키는 것을 의미한다.
  • VariableEnvironment는 함수가 선언될 때의 LexicalEnvironment를 캡처한 것이다.
  • 이는 함수가 호출될 때 참조되는 변수와 함수의 상태를 고정시킨다.

 

LexicalEnvironment(어휘환경) (이하 LE)의 경우 VariableEnvironment (변수환경) (이하 VE) 과 동일하지만,

변경사항을 실시간으로 반영하며,

 

ThisBinding의 경우 this 식별자가 바라봐야할 객체가 된다.

 

우선 VE와 LE의 구성요소는 ‘environmentRecord’와 ‘outerEnvironmentReference’로 서로 같다.

 

  1. environmentRecord(=record)
    1. 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장돼요.
    2. 함수에 지정된 매개변수 식별자, 함수자체, var로 선언된 변수 식별자 등
  2. outerEnvironmentReference(=outer)

 

이 두가지는 담기는 항목은 완벽하게 동일 하지만, 스냅샷 유지 여부는 다음과 같이 다르다.

 

VE의 경우 스냅샷을 유지하지만 LE의 경우 스냅샷을 유지 하지 않고 실시간으로 변경사항을 계속해서 반영한다.

 

결국 실행 컨텍스트를 생성할 떄 VE에 정보를 먼저 담은 다음,

이를 그대로 복사해서 LE를 만들고 이후에는 주로 LE를 활용하게 되는 식이다.

 

 

1- 4 LexicalEnvironment(1) - environmentRecord(=record)와 호이스팅

LE의 경우 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장(수집)되며,

기록된다 라고 이해해본다면 record라는 말과 일맥 상통하다.

수집 대상 정보로는 함수에 지정된 매개변수 식별자, 함수 자체, var로 선언된 변수 식별자 등이며

컨텍스트 내부를 처음부터 끝까지 순서대로 훑어가며 수집된다.

(단, 순서대로 수집될 뿐이지 코드가 실행되는 개념은 아니니 주의해야한다.)

 

1) 호이스팅

이제 우리는 호이스팅의 개념에 대해서 알아볼텐데,

JavaScript의 독특한 동작 방식으로, 변수와 함수의 선언이

그범위의 최상단으로 끌어 올려지는 것처럼 동작하는 메커니즘을 말한다.

 

이로인해 변수를 선언 하기 전에 사용할 수 있는 것처럼 보이지만

실제로는 변수의 선언만 호이스팅 되고 초기화는 호이스팅 되지 않는다.

 

 

2) 호이스팅 규칙

호이스팅 규칙을 다양한 상황에서 예를 들어 볼 수 있는데

 

법칙 1 : 매개변수 및 변수는 선언부를 호이스팅 한다.

 

[적용 전]

//action point 1 : 매개변수 다시 쓰기(JS 엔진은 똑같이 이해한다)
//action point 2 : 결과 예상하기
//action point 3 : hoisting 적용해본 후 결과를 다시 예상해보기

function a (x) {
	console.log(x);
	var x;
	console.log(x);
	var x = 2;
	console.log(x);
}
a(1);

첫 예제에서 코드를 매개변수를 적용해보면 아래와 같다.

 

[매개변수 적용]

//action point 1 : 매개변수 다시 쓰기(JS 엔진은 똑같이 이해한다)
//action point 2 : 결과 예상하기
//action point 3 : hoisting 적용해본 후 결과를 다시 예상해보기

function a () {
	var x = 1;
	console.log(x);
	var x;
	console.log(x);
	var x = 2;
	console.log(x);
}
a(1);

매개변수를 적용한 코드에 대하여 우리가 각 console.log(x)에서 어떤 값이 나올지 예상해본다면

1, undefined, 2 로 예상 할 수 있는데 아래와 같이 호이스팅 적용을 해본다면 생각이 달라질 수 있다.

 

[호이스팅 적용]

//action point 1 : 매개변수 다시 쓰기(JS 엔진은 똑같이 이해한다)
//action point 2 : 결과 예상하기
//action point 3 : hoisting 적용해본 후 결과를 다시 예상해보기

function a () {
	var x;
	var x;
	var x;

	x = 1;
	console.log(x);
	console.log(x);
	x = 2;
	console.log(x);
}
a(1);

 

실제호이스팅을 적용해 본 뒤 코드를 다시 해석해 본다면

앞서 예상한 1, undefined, 2이 아닌1, 1, 2를 출력하고 있음을 알수 있다.

 

 

 

법칙 2 : 함수선언은 전체를 호이스팅 한다.

 

 

[적용 전]

//action point 1 : 결과 값 예상해보기
//action point 2 : hoisting 적용해본 후 결과를 다시 예상해보기

function a () {
	console.log(b);
	var b = 'bbb';
	console.log(b);
	function b() { }
	console.log(b);
}
a();

첫 예제에서 호이스팅을 적용하기 전 console.log(b)에서 출력하는 각각의 값은 

오류, bbb, function이 찍힐 것으로 예상할 수 있는데 아래 호이스팅을 적용해보면

 

 

 

[호이스팅 적용]

//action point 1 : 결과 값 예상해보기
//action point 2 : hoisting 적용해본 후 결과를 다시 예상해보기

function a () {
	var b; // 변수 선언부 호이스팅
	function b() { } // 함수 선언은 전체를 호이스팅

	console.log(b);
	b = 'bbb'; // 변수의 할당부는 원래 자리에

	console.log(b);
	console.log(b);
}
a();

각각 console.log(b)에서 출력하는 값은 function이 가장 먼저 출력되고

다음 bbb, bbb가 두번째 세번째에서 출력될것이다.

 

 

[함수표현식으로 변경했을 시]

//action point 1 : 결과 값 예상해보기
//action point 2 : hoisting 적용해본 후 결과를 다시 예상해보기

function a () {
	var b; // 변수 선언부 호이스팅
	var b = function b() { } // 함수 선언은 전체를 호이스팅

	console.log(b);
	b = 'bbb'; // 변수의 할당부는 원래 자리에

	console.log(b);
	console.log(b);
}
a();

 

함수 표현식으로 코드 구문을 일부 수정 후에 다시 보니 함수 전체를 호이스팅 하는 구조인것이 잘 드러난다.

 

 

3) 함수 선언문과 함수표현식

 

 

앞서 소개한 내용을 한번 되짚어 보고 그 다음으로 넘어가 보자

 

 

 

 

위 참고자료를 통해 앞서 소개했듯 실행 컨텍스트는 실행할 코드에 제공할 환경정보들을 모아놓은 객체고,

그 객체안에는  VariableEnvironment, LexicalEnvironment, ThisBindings 3가지가 존재했다고 설명했었다.

 

출처 : 스파르타 코딩클럽 [JS 문법 종합반] 3주차 : 데이터 타입(심화), 실행 컨텍스트, this

 

 

예제 몇가지를 통해 함수 선언문, 함수 표현식의 차이를 읽어 보길 바란다.

 

console.log(sum(1, 2));
console.log(multiply(3, 4));

function sum (a, b) { // 함수 선언문 sum
	return a + b;
}

var multiply = function (a, b) { // 함수 표현식 multiply
	return a + b;
}

 

우리가 지금까지 알아본 개념을 복기해 보자면 LE는 record와 outer를 수집하고,

그중 record를 수집하는 과정에서 호이스팅이 일어나고 호이스팅 과정을 통해

위로 쭉 끌어 올려 본 결과를 적용해서 코드를 정리해 보자면 아래와 같을 것이다.

 

// 함수 선언문은 전체를 hoisting
function sum (a, b) { // 함수 선언문 sum
	return a + b;
}

// 변수는 선언부만 hoisting

var multiply; 

console.log(sum(1, 2));
console.log(multiply(3, 4));

multiply = function (a, b) { // 변수의 할당부는 원래 자리
	return a + b;
};

 

눈으로 볼대는 판단하기 어려울 수 있으나 함수 선언문과  함수 표현식은 호이스팅 과정에서 차이를 보인다. 

해당 차이로 주의해야 할 상황이 발생 할 수 있기 때문에 함수 선언문은 주의 할 필요가 있고,

되도록 함수 표현식으로 적용하는 것이 좋겠다.

 

함수 선언 문을 주의 해야 하는 이유는 아래 예제와 같다.

 

...

console.log(sum(3, 4));

// 함수 선언문으로 짠 코드
// 100번째 줄 : 시니어 개발자 코드(활용하는 곳 -> 200군데)
// hoisting에 의해 함수 전체가 위로 쭉!
function sum (x, y) {
	return x + y;
}

...
...

var a = sum(1, 2);

...

// 함수 선언문으로 짠 코드
// 5000번째 줄 : 신입이 개발자 코드(활용하는 곳 -> 10군데)
// hoisting에 의해 함수 전체가 위로 쭉!
function sum (x, y) {
	return x + ' + ' + y + ' = ' + (x + y);
}

...

var c = sum(1, 2);

console.log(c);

한 시니어 개발자에 의해 프로그램내 약 200군데 정도 활용이 가능한 함수 선언문으로 작성된 코드가

호이스팅에 의해서 쭉 올라와서 코드 전반에 걸쳐 영향을 끼친다고 가정할때

 

차후 약 5,000번째 줄 쯤 주니어 개발자가 활용하는 곳 10군데 정도 되는 함수 선언문 코드를 작성했다면

함수 선언문의 특징에 의해 호이스팅 될때 또 쭉 올라가서 코드 전반에 걸쳐 영향을 끼칠 수 있기 때문에 주의 해야한다.

 

 

만약, 아래 예제와 같이 함수 표현식 이었다면

 

...

console.log(sum(3, 4));

// 함수 표현식으로 짠 코드
// 함수 선언부만 위로 쭉!
// 이 이후부터의 코드만 영향을 받아요!
var sum = function (x, y) {
	return x + y;
}

...
...

var a = sum(1, 2);

...

// 함수 표현식으로 짠 코드
// 함수 선언부만 위로 쭉!
// 이 이후부터의 코드만 영향을 받아요!
var sum = function (x, y) {
	return x + ' + ' + y + ' = ' + (x + y);
}

...

var c = sum(1, 2);

console.log(c);

두 코드 모두 함수 표현식으로 작성되었기 때문에 각 코드 아래 뎁스부터만 영향을 받게 된다.

이로써 호이스팅 영향력에서 발생 될 다양한 오류에서 자유로운 코드가 될 수 있다.

 

1- 5 LexicalEnvironment(2) - 스코프, 스코프 체인, outerEnvironmentReference(=outer)

앞선 TIL에서도 스코프의 개념은 가끔 언급이 되었었다.

스코프는 식별자에 대한 유효범위를 의미하며, JavaScript를 포함한 대부분의 언어에서 존재한다.

 

스코프는 스코프 체인의 성격을 띄는데

식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해 나가는 것이다.

 

 

출처 : https://jess2.xyz/JavaScript/scope-chain-closure/

 

우리는 지금까지 LE의 구성요소 record와 outer중 record에 대해서 깊이 알아보았다.

이번에는 outerEnvironmentReference(이하 outer)을 알아보자 \

 

outer는 한마디로 스코프 체인이 가능토록 하는것인데 회부환경의 참조정보라고 할 수 있다.

 

outer는 현재 호출된 함수가 선언될 당시 LE (LexicalEnvironment)를 참조한다.

쉽게 말하면 당시 환경정보 or 정보를 저장한다고 이해해도 무방하다.

 

가령, A 함수 내부에 B 함수 선언을 ===> B 함수  내부에 C 함수 선언(linked list)한 경우에는

결국 타고, 타고 올라가다보면 전역 컨텍스트의 LE를 참조하게 된다는 것이다.

 

항상 outer는 오직 자신이 선언된 시점의 LE를 참조하고 있으므로

가장 가까운 요소부터 차례대로 접근이 가능하다.

 

결론 : 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에게만 접근이 가능하다.

 

// 아래 코드를 여러분이 직접 call stack을 그려가며 scope 관점에서 변수에 접근해보세요!
// 어려우신 분들은 강의를 한번 더 돌려보시기를 권장드려요 :)
var a = 1;
var outer = function() {
	var inner = function() {
		console.log(a); // 이 값은 뭐가 나올지 예상해보세요! 이유는 뭐죠? scope 관점에서!
		var a = 3;
	};
	inner();
	console.log(a); // 이 값은 또 뭐가 나올까요? 이유는요? scope 관점에서!
};
outer();
console.log(a); // 이 값은 뭐가 나올까요? 마찬가지로 이유도!

 

위 예제를 참고하여 설명을 이어보자면

 

맨 처음 출력될 console출력은 outer 함수 내부에 있는 inner 함수의 a 값이고

두번째는 inner()가 끝나는 아래 뎁스에 위치한 outer 함수의 a 값

마지막은 outer()가 끝나는 아래 뎁스에 위치한 전역 컨텍스트의 a 값이다.

 

결론 부터 말하자면 

 

1. undefined

2. 1

3. 1

 

위의 순서대로 출력되게 된다.

 

이제 이유를 설명 하자면 

 

1. undefined의 경우

해당 출력 값은 inner 함수의 출력값을 물어보는 것이다.

우선 짚고 넘어가야 할것이 있는데 바로 여기서의 a 값의 호이스팅을 알아봐야 한다. 

 

var outer = function() {
	var inner = function() {
		console.log(a); // 예상 출력: undefined
		var a = 3;
	};

위 코드 중 이너 함수의 호이스팅 된 형태는 아래와 같다.

var inner = function() {
  var a; // 선언이 호이스팅됨
  console.log(a); // undefined (a는 아직 초기화되지 않음)
  a = 3; // a가 3으로 초기화됨
};

 

즉, inner 함수에서 출력하려는 console.log(a);의 값은 호이스팅 결과로 확인 했을때

var a;를 선언한 이후 console.log(a)의 값을 물어보기 때문에

var a = ? 할당 값이 없는 상태 이므로 undefined가 출력되는 것이다.

 

2.  두번째 1의 경우

두번째 출력값은 outer 함수의 console.log(a); 값을 출력하는 것인데,

이때는 outer  함수의 a 변수가 정의 되어 있지 않으므로, 전역 스코프의 var a = 1;을 참조하게 된다.

 

그래서 출력 값이 1이 된다.

 

 

3. 세번째 1의 경우

전역 컨텍스트의 console.log(a); 값을 출력하는것인데,

이것은 var a = 1; 이라는 값이 이미 할당 되어있기 때문에,

 

당연히 1이 출력되게 된다.

 

 

💡최종 정리

TIL - 6에서 다룬 데이터 타입의 이해도가 있어야

실행 컨텍스트의 개념을 알게 되고

실행컨텍스트와 호이스팅의 개념을 완벽히 이해하려면

 

어느정도 기본적인 코드 리딩이 가능한 정도까지 숙련을 해야한다.

 

그렇지 않으면 원리 이해에서 문이 막히게 된다.

 

독학으로 언어 공부를 하는 분이 있다면

꼭 VSCODE와 같은 실행 프로그램과 GPT(이제 무료다..)를 활용해서

원리에 대한 이해를 꼭 실행하고 요청해서 이해를 완벽하게 할 수 있도록 해야 한다.

 

2. this(정의, 활용방법, 바인딩, call, apply, bind)

다른 객체지향 언어에서의 this는 곧 클래스로 생성한 인스턴스를 말한다.

그러나 JavaScript에서는 this가 어디에서나 사용 될 수 있다.

 

  • 인스턴스 참조:
    • this는 현재 클래스의 인스턴스를 참조
    • 이는 클래스 내부에서 현재 객체의 프로퍼티와 메서드에 접근하거나 수정할 때 사용
    • 예: Java에서 this.name = name;은 현재 객체의 name 프로퍼티를 설정
  • 메서드 호출:
    • 클래스의 메서드 내부에서 다른 메서드를 호출할 때 this를 사용하여 동일한 인스턴스의 메서드를 호출
    • 예: C++에서 this->otherMethod();은 현재 객체의 다른 메서드를 호출
  • 생성자 호출:
    • 생성자 오버로딩 시, 하나의 생성자에서 다른 생성자를 호출할 때 this를 사용
    • 예: Java에서 this(anotherParameter);는 다른 생성자를 호출
  • 메서드 체이닝:
    • 메서드 체이닝을 구현하기 위해 메서드에서 this를 반환하여 연속된 메서드 호출을 지원.
    • 예: C++에서 return *this;는 현재 객체를 반환하여 메서드 체이닝이 가능
  • 다형성(Polymorphism):
    • 상속 구조에서, 재정의된 메서드가 호출될 때 this는 실제 인스턴스를 참조하여 올바른 메서드가 호출되도록 함
    • 예: C#에서 base 키워드를 사용하여 부모 클래스의 메서드를 호출할 때도 this는 여전히 현재 인스턴스를 참조

 

이와 같은 이유들로, 대부분의 객체지향 언어에서 this 키워드는 현재 클래스 인스턴스를 참조하는 데 사용되며,

이는 객체의 상태와 동작을 관리하고, 클래스 내부에서 인스턴스에 대한 접근을 제공하기 위해 필수적 이다.

2- 1 상황에 따라 달라지는 this

우리는 해당 포스팅을 통해 실행컨텍스트실행할 코드에 제공할 환경정보들을 모아놓은 객체라는 것을

지속적으로 읽어왔고 그 안에는 

 

VE(VariableEnvironment) = 변수환경

LE(LexicalEnvironment) = 어휘 환경

그리고 우리가 이번 섹션에서 알아볼 ThisBindings이 있었다.

 

여기서 this는 실행 컨텍스트가 생성될 때 결정 되며 <=== 이 현상을 this를 bund한다.(=묶는다)라고 하는것이다.

정리 하자면 this는 함수를 호출할 때 결정된다 라고 할 수 있다.

 

1) 전역 공간에서의 this

전역 공간에서 this는 전역 객체를 가리킨다.

 

런타임 환경에 따라 this는

브라우저(웹) 환경에서는 window로

node 환경에서는 global로 불리우고,

 

브라우저는 해당 웹페이지에서 [F12] or 마우스 오른쪽 클릭 후 검사를 클릭해서 나오는 개발자 도구로 확인 할 수 있는데

 

위와 같이 window로 출력되며

 

VSCODE 상에서 node의 this를 알아보자면

터미널에서 node를 입력(엔터)한 이후 

console.log(this)를 입력하게 되면 아래와 같이 global로 출력되는 것을 확인 할 수 있다.

 

 

 

 

2) 메서드 로써 호출할때 그 메서드 내부에서의 this

메서드 로써 호출 할 때 메서드 내부에서의 this를 알아보기 전

함수와 메서드의 차이를 알아봐야 하는데

둘은 상당히 비슷해 보이지만 엄연한 차이가 존재한다.

 

기준은 독립성으로 판단 할 수 있는데

 

함수는 그 자체로 독립적인 기능을 수행하고

 

메서드는 자신을 호출한 대상 객체에 대한 동작을 수행한다.

 

 

그래서 this를 할당하는 예제를 알아보자면,

// CASE1 : 함수
// 호출 주체를 명시할 수 없기 때문에 this는 전역 객체를 의미해요.
var func = function (x) {
  console.log(this, x);
};
func(1); // global or window { ... } 1

 

함수의 경우 호출 주체를 명시 할 수 없기 때문에 this는 전역 객체를 의미하지만

 

// CASE2 : 메서드
// 호출 주체를 명시할 수 있기 때문에 this는 해당 객체(obj)를 의미해요.
// obj는 곧 { method: f }를 의미하죠?
var obj = {
  method: func,
};
obj.method(2); // { method: f } 2

 

메서드의 경우 호출 주체를 명시 할 수 있기 때문에 this는 해당 객체(obj)를 의미 한다.

 

아래의 예제도 동일한데,

var obj = {
  method: function (x) { console.log(this, x) }
};
obj.method(1); // { method: f } 1
obj['method'](2); // { method: f } 2

 

점 ( . )으로 호출하거나 대괄호 ( [] )로 호출해도 결과는같다.

 

메서드 내부에서의 this는 아래 예제와 같으며, 호출을 누가 했는지에 대한 정보가 메서드에담긴다.

var obj = {
  methodA: function () { console.log(this) },
  inner: {
    methodB: function() { console.log(this) },
  }
};

obj.methodA();             // this === obj
obj['methodA']();          // this === obj

obj.inner.methodB();       // this === obj.inner
obj.inner['methodB']();    // this === obj.inner
obj['inner'].methodB();    // this === obj.inner
obj['inner']['methodB'](); // this === obj.inner

 

3) 함수로써 호출할때 그 함수 내부에서의 this

어떤 함수를 함수로서 호출할 경우 this는 지정되지 않게 되는데, 이유는 호출 주체를 알 수 없기 때문이다.

실행 컨텍스트를 활성화할 당시 this가 지정되지 않는 경우의 this는 전역객체를 의미하기 때문에,

따라서 함수로써 독립적으로 호출 할때는 this는 항상 전역 객체를 가리키니 주의 할 필요가 있다.

 

메서드의 내부 함수에서의 this도 마찬가지인데

메서드의 내부라고 해도, 함수로써 호출될때는 this는 전역객체를 의미하게 된다.

 

var obj1 = {
    outer: function () {
        console.log(this); // (1)
        var innerFunc = function () {
            console.log(this); // (2), (3)
        };
        innerFunc();

        var obj2 = {
            innerMethod: innerFunc,
        };
        obj2.innerMethod();
    },
};
obj1.outer();

 

위 예제를 통해 설명을 이어보자면,

obj1.outer();  obj1의 outer 함수를 호출할 때 obj1 객체의 메서드로 호출되었으므로 this는 obj1을 참조한다.

그래서 console.log(this)는 obj1을 출력하고

 

innerFunc();    innerFunc()를 호출할 경우에는 단독으로 호출되기 때문에, this는 전역 객체를 참조한다.

 

따라서 (2)의 console.log(this)는 global or window를 출력한다.

 

obj2.innerMethod();   innerMethod의 경우 obj 객체의 메서드로 호출되었으므로 this는 obj2를 참조한다.

 

따라서 innerFunc(); (2)의 위치와 동일한 곳에서 출력되지만 (3)의 console.log(this)는 obj2를 출력한다.

 

💡 정리

 

순서대로 출력되는 값은 object / global or window / object 가 되며

VSCODE 상에서 진행해본 결과도 위 설명과 동일하게 나온다.

(아래 사진 참고)

 

 

그래서 아래 예제와 같이 우회 할 수 있는 방법도 찾아 볼 수 있는데,

내부 스코프에 이미 존재하는 this를 별도의 self 변수에 할당 할 수 있다.

 

var obj1 = {
    outer: function () {
        console.log(`1번` + this); // (1) outer

        // AS-IS
        var innerFunc1 = function () {
            console.log(`2번` + this); // (2) 전역객체
        };
        innerFunc1();

        // TO-BE
        var self = this;
        var innerFunc2 = function () {
            console.log(`3번` + self); // (3) outer
        };
        innerFunc2();
    },
};

// 메서드 호출 부분
obj1.outer();

 

위와 같이 내부 스코프에 이미 존재하는 this를 별도의 self 변수에 할당할 수 있는데,

 

obj1.outer();    호출시 outer 메서드는 obj1 객체의 메서드로 호출되므로, (1)의 this는 obj1 을 참조하며,

따라서 (1)의 console.log(this)는 obj1 을 출력한다.

 

 

innerFunc1();   호출시 innerFunc1 함수는 단독으로 호출되므로 (2)의 this는 전역 객체를 참조하며,

따라서 (2)의 console.log(this)는 전역객체를 출력한다.

 

그렇기 때문에 TO-BE 단에 있는 코드구문과 같이 

self 변수에 this를 할당하여 this를 outer 메서드의 컨텍스트 obj1에 바인딩하게 되면,

이제 self는 obj1을 참조하게 되고,

 

var innerFunc2    호출시 innerFunc2 함수의 (3)은 더이상 전역 객체를 바라보지 않게 되기 때문에

따라서 (3)의 console.log(self)는 obj1을 출력한다.

 

 

 

또한 화살표 함수의 예시도 참고 해볼 만 한데,

 

var obj = {
    outer: function () {
        console.log(this); // (1) obj
        var innerFunc = () => {
            console.log(this); // (2) obj
        };
        innerFunc();
    },
};

obj.outer();

 

위와 같은 화살표 함수의 예제를 해석해 보자면

 

obj.outer();   호출시 outer 메서드는 obj 객체의 메서드로 호출되며,

메서드로 호출된 함수의 this는 호출한 객체 obj 를 참조하게된다.

 

따라서 (1)의 console.log(this)는 obj를 출력한다.

 

var innerFunc = () => { }   선언을 통해 innerFunc는 화살표 함수로 정의되며,

화살표 함수는 자신을 포함하는 외부 함수의 this를 상속 받기 때문에,

이경우 outer 함수의 this는 obj 이므로 내부 의 this도 obj를 참조하게 된다.

 

innerFunc();    호출시 innerFunc는 단독으로 호출되지만 화살표 함수는 자신의 this를 가지지 않기 때문에,

외부 스코프 즉, outer 함수의 this를 참조한다.

 

따라서 (2)의 console.log(this)도 obj를 출력한다.

 

 

 

 

 

 

 

 

 

4) 콜백함수 호출 시 그 함수 내부에서의 this

콜백함수는 어떠한 함수, 메서드의 인자(매개변수)로 넘겨주는 함수이며,

이때, 콜백함수 내부의 this는 해당 콜백함수를 넘겨받은 함수(메서드)가 정한 규칙에 따라 값이 결정된다.

 

콜백함수도 함수이기 때문에 호출 주체가 없어 this는 전역 객체를 참조하지만

콜백함수를 넘겨받은 함수에서 콜백함수에 별도로 this를 지정한 경우는 예외 적으로 그 대상을 참조하게 되어있다.

 

그래서 이때는 로직을 이해하기보다는 현재 코드구문 내 this 의 상태를 이해하는 것이 더 중요하다.

 

// 별도 지정 없음 : 전역객체
setTimeout(function () { console.log(this) }, 300);

// 별도 지정 없음 : 전역객체
[1, 2, 3, 4, 5].forEach(function(x) {
	console.log(this, x);
});

// addListener 안에서의 this는 항상 호출한 주체의 element를 return하도록 설계되었음
// 따라서 this는 button을 의미함
document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a').addEventListener('click', function(e) {
	console.log(this, e);
});

 

setTimeout 함수 or forEach 메서드는 콜백 함수를 호출 할 때 대상이 될 this를 지정하지 않으므로

this는 곧 global or window를 바라보게 되며,

 

addEventListener 메서드의 경우 콜백 함수 호출 시 자신의 this를 상속하기 때문에,

this는 addEventListener의 앞부분(button 태그) 를 참조한다.

 

좀더 설명을 더하자면,

addEventListener 메서드는 이벤트 리스너를 등록하는 메서드로 첫번째 인수는 이벤트 유형,

가령 click과 같은 것이고 두번째 인수는 이벤트가 발생했을 때 호출 될 콜백함수가 된다.

 

때문에, 콜백함수 내부의 this는 콜백함수가 호출될때 이벤트를 등록한 요소를 참조하게 되며,

이는 addEventListener의 this 바인딩을 결정하게 된다.

그래서 콜백 함수 호출 시 this는 이벤트를 등록한 요소로 설정한다.

 

5) 생성자 함수 내부에서의 this

생성자의 개념이 아직 어렵다면 구체적인 인스턴스(사례)를 만들기 위한 일종의 틀 or 객체로 이해하면 좋다.

 

var Cat = function (name, age) {
    this.bark = '야옹';
    this.name = name;
    this.age = age;
};

var choco = new Cat('초코', 7); //this : choco
var nabi = new Cat('나비', 5); //this : nabi

 

위 예제와 같은 코드 구문을 객체 분야를 다룰때 이미 경험 했었다.

 

그래서 Cat = function    Cat 함수는 생성자 함수로 정의 되며, this는 생성자 함수 내부에서 새로 생성된 객체를 참조한다.

생성자 함수는 인수로 name과 age를 받아 새로운 객체의 name과 age 프로퍼티를 초기화 하며,

 

var choco = new Cat('초코', 7); //this : choco   호출될때 새로운 객체가 생성되는데,

생성자 함수 Cat가 호출되면서 this는 새로 생성된 객체를 참조하게 되며,

 this.bark = '야옹'    은 새로 생성된 객체에 bark 프로퍼티를 추가하고 값을 야옹으로 설정한다.

이후에는 새로 생성된 객체 newCat의 정보를 가져오기 때문에 

아래와 같은 결과가 나오게 된다.

console.log(choco), console.log(nabi)의 출력결과

 

 

2- 2 명시적 this 바인딩

명시적 this 바인딩은 자동으로 부여되는 상황별 this의 규칙을 깨고,

this에 별도의 값을 저장 하는 방법이다.

 

크게 call, apply, bind 정도를 소개 해보겠다.

 

1) call 메서드

call 메서드는 호출 주체인 함수를 즉시 실행하는 명령이다.

call 명령어를 사용하여 첫 번째 매개 변수에 this로 bindinf 할 객체를 넣어주면,

명시적으로 binding 할 수 있다.

 

var func = function (a, b, c) {
    console.log(this, a, b, c);
};

// no binding
func(1, 2, 3); // Window{ ... } 1 2 3

// 명시적 binding
// func 안에 this에는 {x: 1}이 binding돼요
func.call({ x: 1 }, 4, 5, 6); // { x: 1 } 4 5 6

 

위 예제 코드로 설명하자면

 

var func = function (a, b, c) {
    console.log(this, a, b, c);
};

 func 는 세개의 매개변수 a, b, c를 받는 익명함수이며

함수 내부에서 this와 a, b, c를 console.log로 출력한다.

 

func(1, 2, 3); // Window or Global{ ... } 1 2 3    이 부분에서는 해당 func함수가 단독으로 호출되기 때문에

자바스크립트에서 단독으로 호출된 함수의 this는 전역객체를 참조하며, window orglobal 을 바라본다.

따라서 this는 window 객체를 참조하고, 함수의 인수 1, 2, 3이 각각 abc에 전달된다.

 

func.call({ x: 1 }, 4, 5, 6); // { x: 1 } 4 5 6    이 부분에서는 call 메서드를 사용하여 func 함수를 호출한다.

그러므로 call 메서드의 첫 번째 인수는 함수 내부의 this로 바인딩 될 객체를 지정하며,

여기에서는 {x:1} 객체가 this로 바인딩된다.

 

그 뒤 , 4, 5, 6이 func 함수의 a, b, c 를 바라보기 때문에

따라서 출력값은 // { x: 1 } 4 5 6 이 된다.

 

2) apply 메서드

apply메서드도 원리는 call 메서드와 동일하다.

다만, this에 binding 할 객체는 똑같이 넣어주고 나머지 부분만 배열 형태로 넘겨주게 되는데,

var func = function (a, b, c) {
    console.log(this, a, b, c);
};
func.apply({ x: 1 }, [4, 5, 6]); // { x: 1 } 4 5 6

var obj = {
    a: 1,
    method: function (x, y) {
        console.log(this.a, x, y);
    },
};

obj.method.apply({ a: 4 }, [5, 6]); // 4 5 6

 

위 예제 코드로 설명하자면,

 

var func = function (a, b, c) {
    console.log(this, a, b, c);
};
func.apply({ x: 1 }, [4, 5, 6]); // { x: 1 } 4 5 6

 

여기 까지는 앞서 call 메서드와 비슷하다고 볼 수 있고

 

var obj = {
    a: 1,
    method: function (x, y) {
        console.log(this.a, x, y);
    },
};

obj.method.apply({ a: 4 }, [5, 6]); // 4 5 6

 

그 아래 에 있는 해당 코드에서의 obj 객체는 a 프로퍼티와  method 라는 메서드를 포함한다.

method 라는 메서드는 x, y 라는 두개의 매개변수를 받으며,

obj.method.apply({ a: 4 }, [5, 6]); // 4 5 6    apply 메서드를 사용하여 obj.method를 호출할 때,

첫번째 인수로 this를 {a:4}로 설정하고 두번째 인수로 [5, 6] 배열을 전달하는데,

이처럼 apply 메서드는 배열의 요소들을 함수의 인수로 분리하여 전달한다.

 

따라서, 함수 호출시 this는 {a:4} 객체를 참조하고,   x와 y에는 각각 5, 6이 할당되기 때문에

 

출력 값은 4, 5, 6이 나온다.

 

(출처 : https://kamang-it.tistory.com/entry/JavaScript15유사배열-객체Arraylike-Objects )

 

이와 같이 call과 aplly도 자주 쓰이는 메서드 이지만,

유사배열객체에 배열 메서드를 적용해 볼 수도 있고

 

(출처 : https://www.daleseo.com/js-array-slice-splice/ )

 

 

아래 예제와 같이 slice 함수를 통해 구문을 작성 할 수도 있다.

 

//객체에는 배열 메서드를 직접 적용할 수 없어요.
//유사배열객체에는 call 또는 apply 메서드를 이용해 배열 메서드를 차용할 수 있어요.
var obj = {
	0: 'a',
	1: 'b',
	2: 'c',
	length: 3
};
Array.prototype.push.call(obj, 'd');
console.log(obj); // { 0: 'a', 1: 'b', 2: 'c', 3: 'd', length: 4 }

var arr = Array.prototype.slice.call(obj);
console.log(arr); // [ 'a', 'b', 'c', 'd' ]

 

또한 call/apply를 통해 this binding을 하는 것이 아니라,

객체 ===> 배열 로의 형 변환 만을 위해서도 쓸 수 있지만,

원래 의도와는 거리가 먼 방법이 될 수 있기 때문에 Array.from 메서드를 통해 적용해 볼 수 있다.

 

// 유사배열
var obj = {
	0: 'a',
	1: 'b',
	2: 'c',
	length: 3
};

// 객체 -> 배열
var arr = Array.from(obj);

// 찍어보면 배열이 출력됩니다.
console.log(arr);

 

 

생성자 내부에서 다른 생서자를 호출하는 경우네는 공통된 내용의 반복 제거를 할 수 있으며

function Person(name, gender) {
	this.name = name;
	this.gender = gender;
}
function Student(name, gender, school) {
	Person.call(this, name, gender); // 여기서 this는 student 인스턴스!
	this.school = school;
}
function Employee(name, gender, company) {
	Person.apply(this, [name, gender]); // 여기서 this는 employee 인스턴스!
	this.company = company;
}
var kd = new Student('길동', 'male', '서울대');
var ks = new Employee('길순', 'female', '삼성');

위 예제의 경우에도 student, Employee 모두 person이기 때문에, name과 gender 속성 모두 필요하다.

그렇기 때문에 student와 Eployee 인스턴스를 만들 때 마다 세가지 속성을 함수에 넣기 보다는

person 이라는 생성자 함수를 변도로 빼는게 구조화에 더 도움이 된다.

 

 

끝으로 여러 인수를 묶어 하나의 배열로 전달 할 때 apply를 사용할 수도 있는데

//비효율
var numbers = [10, 20, 3, 16, 45];
var max = min = numbers[0];
numbers.forEach(function(number) {
  // 현재 돌아가는 숫자가 max값 보다 큰 경우
  if (number > max) {
    // max 값을 교체
    max = number;
  }

  // 현재 돌아가는 숫자가 min값 보다 작은 경우
  if (number < min) {
    // min 값을 교체
    min = number;
  }
});

console.log(max, min);

 

apply를 통해 위 제시된 코드를 아래와 같이 간결하게 정리하고, 가독성을 높일 수 있다.

 

//효율
var numbers = [10, 20, 3, 16, 45];
var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);
console.log(max, min);

// 펼치기 연산자(Spread Operation)를 통하면 더 간편하게 해결도 가능해요
const numbers = [10, 20, 3, 16, 45];
const max = Math.max(...numbers);
const min = Math.min(...numbers);
console.log(max, min);

 

 

3) bind 메서드

bind 메서드는 call 메서드와 비슷해 보이지만, call과는 다르게 즉시 호출 하지는 않고,

넘겨 받은 this 및 인수들을 바탕으로 새로운 함수를 반환하는 메서드로 인식해야한다.

 

bind 메서드의 사용 목적은 함수에 미리 this를 적용할 수 있고, 부분 적용 함수를 구현할 때 용이하다.

var func = function (a, b, c, d) {
    console.log(this, a, b, c, d);
};
func(1, 2, 3, 4); // window객체

// 함수에 this 미리 적용
var bindFunc1 = func.bind({ x: 1 }); // 바로 호출되지는 않아요! 그 외에는 같아요.
bindFunc1(5, 6, 7, 8); // { x: 1 } 5 6 7 8

// 부분 적용 함수 구현
var bindFunc2 = func.bind({ x: 1 }, 4, 5); // 4와 5를 미리 적용
bindFunc2(6, 7); // { x: 1 } 4 5 6 7
bindFunc2(8, 9); // { x: 1 } 4 5 8 9

 

위 예제를 살펴보면,

var func = function (a, b, c, d) {
    console.log(this, a, b, c, d);
};
func(1, 2, 3, 4); // window객체

 

func는 네개의 매개변수 a, b, c, d를 받는 익명함수이고,

단독으로 호출된 함수의 this는 기본적으로 전역객체(window or global)을 참조한다.

 

따라서, Window { ... } 1 2 3을 출력한다.

// 함수에 this 미리 적용
var bindFunc1 = func.bind({ x: 1 }); // 바로 호출되지는 않아요! 그 외에는 같아요.
bindFunc1(5, 6, 7, 8); // { x: 1 } 5 6 7 8

 

bind 메서드를 사용하여 this를 설정하게 되면

bindFunc1은 func 함수의 this를 {X:1}로 바인딩한 새로운 함수이다.

bindFunc1이 호출될때 전달된 인수 5, 6, 7, 8은 각각 a, b, c, d에 할당되기 때문에,

 

따라서, { x: 1 } 5 6 7 8 을 출력한다.

 

// 부분 적용 함수 구현
var bindFunc2 = func.bind({ x: 1 }, 4, 5); // 4와 5를 미리 적용
bindFunc2(6, 7); // { x: 1 } 4 5 6 7
bindFunc2(8, 9); // { x: 1 } 4 5 8 9

 

해당 부분에 대해서도 부분적용 함수를 구현하는 개념이지만,

결국 전역객체를 바라보지 않는 this로 설정하는 작업이기 때문에,

 

따라서, 결과는 아래 사진과 같이 출력되게 된다.

 

 

 

아래 예제를통해 추가적으로 알면 좋은것은,

var func = function (a, b, c, d) {
    console.log(this, a, b, c, d);
};
var bindFunc = func.bind({ x: 1 }, 4, 5);

// func와 bindFunc의 name 프로퍼티의 차이를 살펴보세요!
console.log(func.name); // func
console.log(bindFunc.name); // bound func

function 객체는 name 이라는 프로퍼티를 가지고 있다.

이 프로퍼티는 함수의 이름을 문자열로 나타낸다.

 

bind 메서드를 사용하여 새로운 함수를 생성하면,

이 새로운 함수는 name 프로퍼티에 원래 함수 이름앞에 bound 라는 접두사가 붙게 된다.

이를 통해 함수가 원래 함수에서 바인딩된 함수라는 것을 알 수 있다.

 

4) 상위 컨텍스트의 this를 내부함수나 콜백 함수에 전달하기

또한 상위 컨텍스트의 this를 내부함수나 콜백 함수에 전달 할 수 있는데

앞서 다뤄봤던 self this 등의 변수를 활용한 우회법 보다

call, apply, bind를 사용하면 깔끔하게 처리 가능하다.

 

아래 내부 함수에 대한 적용 예제 두가지를 참고 하자

var obj = {
	outer: function() {
		console.log(this); // obj
		var innerFunc = function () {
			console.log(this);
		};

		// call을 이용해서 즉시실행하면서 this를 넘겨주었습니다
		innerFunc.call(this); // obj
	}
};
obj.outer();

 

 

var obj = {
	outer: function() {
		console.log(this);
		var innerFunc = function () {
			console.log(this);
		}.bind(this); // innerFunc에 this를 결합한 새로운 함수를 할당
		innerFunc();
	}
};
obj.outer();

 

 

또한 콜백함수도 함수이기 때문에 함수가 인자로 전달될 때는 this가 유실 되기 때문에 함수 자체로 전달하며

bind 메서드를 이용해 this를 입맛에 맞게 변경이 가능한데 아래 예제를 참고하자.

 

var obj = {
	logThis: function () {
		console.log(this);
	},
	logThisLater1: function () {
		// 0.5초를 기다렸다가 출력해요. 정상동작하지 않아요.
		// 콜백함수도 함수이기 때문에 this를 bind해주지 않아서 잃어버렸어요!(유실)
		setTimeout(this.logThis, 500);
	},
	logThisLater2: function () {
		// 1초를 기다렸다가 출력해요. 정상동작해요.
		// 콜백함수에 this를 bind 해주었기 때문이죠.
		setTimeout(this.logThis.bind(this), 1000);
	}
};

obj.logThisLater1();
obj.logThisLater2();

 

 

5) 화살표 함수의 예외사항

다만, 화살표 함수는 실행 컨텍스트 생성시 this를 바인딩 하는 과정이 제외되기 때문에,

이 함수 내부에는 this의 할당과정(this 바인딩 과정)이 아예 없으며, 

접근하고자 할때는 스코프 체인 상 가장 가까운 this에 접근하게 된다.

 

아래 예제를 참고하자.

 

var obj = {
	outer: function () {
		console.log(this);
		var innerFunc = () => {
			console.log(this);
		};
		innerFunc();
	};
};
obj.outer();

 

 

 

 

 

💡 총정리

TIL을 지속 할수록  새로운 언어를 배운다는 것이 얼마나 깊게 파고들어야 하는 일인지를 새삼깨닫는다.

그리고 지금은 어렵지만 JavaScript와 같은 컴퓨터 언어중 유연한 언어를 배운다는것이

오히려 나중엔 행운일지도 모른다는 생각이 든다.

 

TIL 6부터 이번 TIL7 까지 다룬 데이터, 실행컨텍스트와 this

쉽지 않은 개념이었고 그래서 예상보다(하루) 더 많은 시간이 걸렸지만(3일)

TIL로 정리하며 이렇게 까지 물고 늘어져서 정리 하지 않았으면

 

지금 나는 이 부분을 어떻게 이해할 수 있었을까? 싶다.

 

물론 완벽한 이해라고 장담할 순 없지만

이정도 노력에도 100% 이해하기 어려운 개념이라면

이 노력은 무조건 해야하는게 맞다고 생각된다.

 

 

 

 

그럼 다음에도 빡센.. TIL로 돌아오겠다..

 

728x90
반응형

댓글