[JavaScript][TIL] LexicalEnvironment (2) - 스코프, 스코프 체인, outerEnvironmentReference(=outer)

Trippy·2023년 10월 22일
0

JavaScript

목록 보기
12/28
post-thumbnail

LexicalEnvironment

주요 용어

스코프

  1. 식별자에 대한 유효범위를 의미한다
  2. 대부분 언어에서 존재한다.

스코프 체인

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

outerEnvironmentReference(outer)

스코프 체인이 가능토록 하는 것 (외부 환경의 참조정보)라고 할 수 있다.


스코프 체인

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

  2. A함수 내부에 B함수 선언 -> B함수 내부에 C함수 선언(Linked List) 한 경우 어떻게 될까?

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

  4. 항상 outer는 오직 자신이 선언된 시점의 LexicalEnvironment를 참조하고 있으므로, 가장 가까운 요소부터 차례대로 접근 가능

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

// 아래 코드를 여러분이 직접 call stack을 그려가며 scope 관점에서 변수에 접근해보세요!
// 어려우신 분들은 강의를 한번 더 돌려보시기를 권장드려요 :)
var a = 1;
var outer = function() {
	var inner = function() {
		console.log(a); // undefined
		var a = 3;
	};
	inner();
	console.log(a); // 1  inner은 사라짐 따라서 outer나 -> 전역 순서로 참조함
};
outer();
console.log(a); // 1  outer없음, 전역에서 참조해야함

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

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

그러나 자바스크립트에서는 this가 어디에서나 사용될 수 있다.

[1] 상황에 따라 달라지는 this

실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체이다.
그 객체 안에는 3가지가 존재한다.
✓ VariableEnvironment
✓ LexicalEnvironment
✅ ThisBindings

[1] this는 실행 컨텍스트가 생성될 때 결정된다. 이 말을 this를 bind한다(=묶는다) 라고도 한다. 다시 말하면 this는 함수를 호출할 때 결정된다. 라고 할 수 있다.

a. 전역 공간에서의 this
1. 전역 공간에서 this는 전역 객체를 가리킨다.
2. 런타임 환경에 따라 this는 window(브라우저 환경) 또는 global(node 환경)를 각각 가르킨다.

런타임 환경?

javascript를 구동중인 환경을 말한다.

[2] 메서드로서 호출할 때 그 메서드 내부에서의 this

a. 함수 vs 메서드

함수와 메서드, 상당히 비슷해 보이지만 엄연한 차이가 존재한다. 기준은 독립성이다. 함수는 그 자체로 독립적인 기능을 수행한다.

함수명();

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

객체.메서드명();

b. 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: f } 2

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

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

d. 메서드 내부에서의 this
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

a. 함수 내부에서의 this

  1. 어떤 함수를 함수로서 호출할 경우, this는 지정되지 않는다(호출 주제가 알 수 없다)

  2. 실행컨텍스트를 활성화 할 당시 this가 지정되지 않은 경우, this는 전역 객체를 의미한다.

  3. 따라서 함수로서 '독립적으로'호출할 때는 this는 항상 전역객체를 가리킨다는 것을 주의

b. 메서드 내부함수에서의 this

  1. 메서드 내부라고 해도, 함수로서 호출한다면 this는 전역 객체를 의미한다
var obj1 = {
	outer: function() {
		console.log(this); // (obj1)
		var innerFunc = function() {
			console.log(this); // (전역객체), (obj2)
		}
		innerFunc();

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

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

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

  1. 변수를 활용하는 방법

내부 스코프에 이미 존재하는 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();
  1. 화살표 함수(=this를 바인딩하지 않는 함수)

ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제 때문에 화살표 함수를 도입했다.
일반 함수와 화살표 함수의 가장 큰 차이점은?
=> this binding 여부

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

obj.outer();

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

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

[5] 생성자 함수 내부에서의 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

[2] 명시적 this 바인딩

자동으로 부여되는 상활별 this의 규칙을 깨고 this에 별도 값을 저장하는 방법
call apply bind에 대해 알아보자.

1. call 메서드

a. 호출 주체인 함수를 즉시 실행하는 명령어이다.
b. 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

아래 예시는, 예상되는 this가 있어도 일부러 바꾸는 연습

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

2. apply 메소드

a. 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

3. call / apply 메서드 활용

물론 this binding을 위해 call, apply, method를 사용하기도 하지만 더 유용한 측면도 있다.

  1. 유사배열객체(array-like-object)에 배열 메서드 적용


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

사실, call / apply를 통해 this binding을 하는 것이 아니라 객체 -> 배열로의 형 변환 만을 위해서도 쓸 수 있지만 원래 의도와는 거리가 먼 방법이라 할 수 있다.
따라서 ES6에서는 Array.from이라는 방법을 제시했는데 아주 편리하다

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

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

// 찍어보면 배열이 출력됩니다.
console.log(arr);
  1. 생성자 내부에서 다른 생성자를 호출(공통된 내용의 반복 제거)

Student, Employee 모두 Person이다. name과 gender 속성 모두 필요하다. 그러니 StudentEmployee 인스턴스를 만들 때 마다 세 가지 속성을 모두 각 생성자 함수에 넣기 보다는 Person이라는 생성자 함수를 별도로 빼는게 ‘구조화’에 도움이 더 도움이 된다.

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', '삼성');
  1. 여러 인수를 묶어 하나의 배열로 전달할 때 apply를 사용할 수 있다.

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

4. bind 메서드

a. call과 비슷해 보인다. 하지만 즉시 call과는 다르게 즉시 호출하지는 않고 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하는 메서드라고 볼 수 있다.

b. 목적
1. 함수에 this를 미리 적용한다
2. 부분 적용 함수 구현할 때 용이하다.

c. 예시

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

d. name 프로퍼티

  1. 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

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

1) 내부함수

  1. 메서드의 내부함수에서 메서드의 this를 그대로 사용하기 위한 방법이다.

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

이번엔 call이 아니라 bind를 이용한다

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

2) 콜백함수

  1. 콜백함수도 함수이기 때문에, 함수가 인자로 전달될 때는 함수 자체로 전달한다. (this유실)

  2. 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. 화살표 함수의 예외사항

  1. 화살표 함수는 실행 컨텍스트 생성 시, this를 바인딩하는 과정이 제외된다고 했다.

  2. 이 함수 내부에는 this의 할당과정(바인딩 과정)이 아에 없으며, 접근코자 하면 스코프체인상 가장 가까운 this에 접근하게 됨

  3. this우회, call, apply, bind보다 편리한 방법

var obj = {
	outer: function () {
		console.log(this); // obj
		var innerFunc = () => {
			console.log(this); // obj
		};
		innerFunc();
	};
};
obj.outer();
profile
감금 당하고 개발만 하고 싶어요

0개의 댓글