프론트엔드 개발일지 #12-javascript심화(실행컨텍스트)

조아라·2024년 10월 11일
2
post-thumbnail

실행 컨텍스트란?

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

스택 vs 큐

스택은 바구니라고 생각하고 바구니에 차례대로 담기고 나갈때는 맨 마지막에 담은게 빠져나간다.
는 밑이 뚫려 있어서 담은 순서대로 담기고 가장 먼저 담긴게 가장 처음으로 빠져나간다.

콜 스택

실행컨텍스트, 즉 동일 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고 이것을 위의 '스택'의 한 종류인 콜 스택에 쌓아 올린다. 가장 위에 있는 컨텍스트와 관련된 코드를 실행하는 방법으로 환경 및 순서를 보장 할 수 있다.

  • 컨텍스트 구성
  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) → 코드종료

이렇게 글로 적으니까 헷갈리는데 이걸 내가 콜스택으로 그린다고 생각하면,

이렇게 바구니에 차례대로 들어가고 맨 위부터 나간다라고 생각하면 된다.
결국 특정 실행 컨텍스트가 생성되는 시점 = 콜 스택의 맨위에 쌓이는 순간을 의미한다.

실행 컨텍스트 객체의 담기는 정보

  • VariableEnvironment
    현재 컨텍스트 내의 식별자 정보(record)를 갖고 있다.
    예를들어, var a = 3이라면 var a를 의미한다

외부 환경 정보(outer)를 갖고 있다.

선언 시점 LexicalEnvironment의 snapshot

  • LexicalEnvironment
    VariableEnvironment와 동일하지만, 변경사항을 실시간으로 반영
  • ThisBinding
    this 식별자가 바라봐야할 객체

VE vs LE

이 두가지에 담기는 항목은 완전 동일하다. 그러나 스냅샷 유지 여부가 다르다.

VE : 스냅샷을 유지
LE : 스냅샷을 유지하지 않음. 즉, 실시간으로 변경사항을 계속해서 반영

==> 실행 컨텍스트를 생성할 때, VE에 정보를 먼저 담은 다음, 이를 그대로 복사해서 LE를 만들고 이후에는 주로 LE를 활용

이 둘의 구성요소도 동일하다
‘environmentRecord’와 ‘outerEnvironmentReference’로 구성

-environmentRecord(=record) : 현재 컨텍스트와 관련된 코드의 식별자 정보 저장 / 함수에 지정된 매개변수 식별자, 함수전체, var로 선언된 변수 식별자 등
-outerEnvironmentReference(=outer)


LexicalEnvironment

(1) environmentRocord(=record)와 호이스팅

  • 호이스팅
    변수 정보 수집을 모두 마쳤더라도 아직 실행 컨텍스트가 관여할 코드는 실행 전의 상태이다. 변수 정보 수집 과정을 이해하기 쉽게 설명한 '가상개념'

규칙이 있는데,

  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);

// 이 코드에 매개 변수를 적용하면
function a () {
	var x = 1;
	console.log(x);
	var x;
	console.log(x);
	var x = 2;
	console.log(x);
}
a(1);

//이것을 호이스팅을 적용하면,
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 라는 결과가 나온다.

  1. 함수 선언은 전체를 호이스팅한다.
//action point 1 : 결과 값 예상해보기
//action point 2 : hoisting 적용해본 후 결과를 다시 예상해보기

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

//호이스팅을 적용하면

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

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

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

//함수선언문을 함수 표현식으로 바꿔본다면

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

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

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

이렇게하면 값은 b함수, 'bbb', 'bbb'가 나온다. 호이스팅을 고려하지않으면 처음 값을 undefined로 예상 할 수 있기 때문에 주의해야한다

  1. 함수 선언문, 함수 표현식
// 함수 선언문. 함수명 a가 곧 변수명
// function 정의부만 존재, 할당 명령이 없는 경우
function a () { /* ... */ }
a(); // 실행 ok

// 함수 표현식. 정의한 function을 별도 변수에 할당하는 경우
// (1) 익명함수표현식 : 변수명 b가 곧 변수명(일반적 case에요)
var b = function () { /* ... */ }
b(); // 실행 ok

// (2) 기명 함수 표현식 : 변수명은 c, 함수명은 d
// d()는 c() 안에서 재귀적으로 호출될 때만 사용 가능하므로 사용성에 대한 의문
var c = function d () { /* ... */ } 
c(); // 실행 ok
d(); // 에러!

기명함수 표현식은 거의 사용하지않는다.
함수 선언문과 표현식의 실질적인 차이를 보자면

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;
}

// 함수 선언문은 전체를 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);

//만약에 함수 표현식이었다면?
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);

협업과 복잡한 코드일수록 함수표현식을 활용하는 습관을 들여야한다.

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

  • 스코프
    식별자에 대한 유효범위를 의미

  • 스코프체인
    식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것

  • outer
    스코프 체인이 가능토록 하는 것(외부 환경의 참조정보)

스코프 체인을 좀 더 알아보자면,

outer는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조
예를들자면, A함수 내부에 B함수 선언 → B함수 내부에 C함수 선언(Linked List)한 경우 결국 타고 올라가보면 전역 컨텍스트의 LexicalEnvironment를 참조하게 된다.
항상 outer는 오직 자신이 선언된 시점의 LexicalEnvironment를 참조하고 있으므로, 가장 가까운 요소부터 차례대로 접근 가능하다.

==> 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에게만 접근이 가능

// 아래 코드를 여러분이 직접 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); // 이 값은 뭐가 나올까요? 마찬가지로 이유도!

이걸 내가 풀어본 결과는


this

객체지향 언어에서의 this는 곧 클래스로 생성한 인스턴스를 말한다. 그러나 자바스크립트에서는 어디에서나 사용 될 수 있다.

(1) 상황에 따라 달라지는 this

-this는 실행 컨텍스트가 생성 될 때 결정된다. 즉 this를 bind한다라고도 한다. 다시 말하면 this는 함수를 호출할 때 결정된다.

  • 전역 공간에서의 this -> 전역 객체를 가리킨다.
    런타임의 환경에 따라서는 노드는 global / 브라우저는 윈도우
console.log(this);
console.log(window);
console.log(this === window);

console.log(this);
console.log(global);
console.log(this === global);
  • 메서드로서 호출할 때 그 메서드 내부의 this
    함수와 메서드, 상당히 비슷해 보이지만 엄연한 차이가 존재한다.
    기준은 독립성이다. 함수는 그 자체로 독립적인 기능을 수행한다.

-함수명();
-객체.메서드명();


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

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

함수로서의 호출과 메서드로서의 호출 구분 기준 : . , []

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
  • 함수로서의 호출 할 때 그 함수 내부의 this

함수 내부에서의 this
1. 어떤 함수를 함수로서 호출할 경우, this는 지정되지 않는다.(호출 주체가 알 수 없으니까)
2. 실행컨텍스트를 활성화할 당시 this가 지정되지 않은 경우, this는 전역 객체를 의미 한다.
3. 따라서, 함수로서 ‘독립적으로’ 호출할 때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();

(1) : obj1, (2) : 전역객체, (3) : obj2

this 바인딩에 관해서는 함수를 실행하는 당시의 주변 환경(메서드 내부인지, 함수 내부인지)은 중요하지 않고, 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지가 관건

메서드의 내부 함수에서의 this 우회

  • 변수를 활용 : 내부 스코프에 이미 존재하는 this를 별도의 변수(ex : self)에 할당하는 방법
var obj1 = {
	outer: function() {
		console.log(this); // (1) { outer: ƒ }

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

		// TO-BE
		var self = this;
		var innerFunc2 = function() {
			console.log(self); // (3) { outer: ƒ }
		};
		innerFunc2();
	}
};

// 메서드 호출 부분
obj1.outer();
  • 화살표 함수(=this를 바인딩하지 않는 함수) :일반 함수와 화살표 함수의 가장 큰 차이점이라고 할 수 있다.
var obj = {
	outer: function() {
		console.log(this); // (1) obj
		var innerFunc = () => {
			console.log(this); // (2) obj
		};
		innerFunc();
	}
}

obj.outer();

콜백 함수 호출 시 그 함수 내부에서의 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);
});
  1. setTimeout 함수, forEach 메서드는 콜백 함수를 호출할 때 대상이 될 this를 지정하지 않으므로, this는 곧 window객체

  2. addEventListner 메서드는 콜백 함수 호출 시, 자신의 this를 상속하므로, this는 addEventListner의 앞부분(button 태그)

생성자 함수 내부에서의 this

생성자 : 구체적인 인스턴스(어려우면 객체로 이해)를 만들기 위한 일종의 틀

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

명시적 this 바인딩

자동으로 부여되는 상황별 this의 규칙을 깨고 this에 별도의 값을 저장하는 방법

  • call 메서드
    호출 주체인 함수를 즉시 실행하는 명령어
    call명령어를 사용하여, 첫 번째 매개변수에 this로 binding할 객체를 넣어주면 명시적으로 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 obj = {
	a: 1,
	method: function (x, y) {
		console.log(this.a, x, y);
	}
};

obj.method(2, 3); // 1 2 3
obj.method.call({ a: 4 }, 5, 6); // 4 5 6
  • 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

이렇게 두개의 메서드를 활용하면,

  • 유사배열 객체에 배열 메서드를 적용 할 수 있다

//객체에는 배열 메서드를 직접 적용할 수 없어요.
//유사배열객체에는 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' ]

//이걸 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', '삼성');

그리고 여러 인수를 묶어서 하나의 배열로 전달할때 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);

//효율
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);

bind 메서드
call과 비슷해 보이지만 call과는 다르게 즉시 호출하지는 않고 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하는 메서드
1. 함수에 this를 미리 적용
2. 부분 적용 함수 구현할 때 용이

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
//bind 메서드를 적용해서 새로 만든 함수는 name 프로퍼티에 
//‘bound’ 라는 접두어가 붙습니다
//추적하기가 쉬워지겠죠?
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

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

  • 내부함수

elf 등의 변수를 활용한 우회법보다 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();

//bind를 이용하면
var obj = {
	outer: function() {
		console.log(this);
		var innerFunc = function () {
			console.log(this);
		}.bind(this); // innerFunc에 this를 결합한 새로운 함수를 할당
		innerFunc();
	}
};
obj.outer();
  • 콜백함수
    콜백함수도 함수이기 때문에, 함수가 인자로 전달될 때는 함수 자체로 전달
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();
  • 화살표 함수의 예외사항
    this의 할당과정(바인딩 과정)이 아에 없으며, 접근코자 하면 스코프체인상 가장 가까운 this에 접근하게 됨
var obj = {
	outer: function () {
		console.log(this);
		var innerFunc = () => {
			console.log(this);
		};
		innerFunc();
	};
};
obj.outer();

_ 너무 많은 내용들을 배워서 다시 한 번 복습하면서 적었는데 아무래도 실행컨텍스트 강의는 한 번 더 들어야 할 것 같다. this 부분에서 많이 헤맨다._
profile
끄적 끄적 배운 걸 적습니다 / FRONT-END STUDY VELOG

0개의 댓글