본문 바로가기
카테고리 없음

호이스팅, 스코프 체인, this 바인딩

by riversun1 2024. 4. 25.

 (5) 불변 객체 

  1. 불변 객체의 정의개념은 알겠는데, 그 개념이 왜 필요한지 알겠나요? 아래 예시를 통해서 “불변하다”. 혹은, ‘불변객체’의 개념이 왜 필요한지를 한번 알아보도록 합시다 🐱
  2. 우리는 앞선 과정에서, **가변하다**와 **불변하다**의 개념을 배웠습니다. 다시 살짝 정리해서 객체를 예로 들면, 객체의 속성에 접근해서 값을 변경하면 가변이 성립했었죠. 반면, 객체 데이터 자체를 변경(새로운 데이터를 할당)하고자 한다면 기존 데이터는 변경되지 않습니다. 즉, 불변하다라고 볼 수 있습니다.
  3. 불변 객체의 필요성
    1. 다음 예시는 객체의 가변성에 따른 문제점을 보여주고 있어요 🙄
// user 객체를 생성
var user = {
	name: 'wonjang',
	gender: 'male',
};

// 이름을 변경하는 함수, 'changeName'을 정의
// 입력값 : 변경대상 user 객체, 변경하고자 하는 이름
// 출력값 : 새로운 user 객체
// 특징 : 객체의 프로퍼티(속성)에 접근해서 이름을 변경했네요! -> 가변
var changeName = function (user, newName) {
	var newUser = user;
	newUser.name = newName;
	return newUser;
};

// 변경한 user정보를 user2 변수에 할당하겠습니다.
// 가변이기 때문에 user1도 영향을 받게 될거에요.
var user2 = changeName(user, 'twojang');

// 결국 아래 로직은 skip하게 될겁니다.
if (user !== user2) {
	console.log('유저 정보가 변경되었습니다.');
}

console.log(user.name, user2.name); // twojang twojang
console.log(user === user2); // true

 

위의 예제를 아래와 같이 개선할 수 있어요!

// user 객체를 생성
var user = {
	name: 'wonjang',
	gender: 'male',
};

// 이름을 변경하는 함수 정의
// 입력값 : 변경대상 user 객체, 변경하고자 하는 이름
// 출력값 : 새로운 user 객체
// 특징 : 객체의 프로퍼티에 접근하는 것이 아니라, 아에 새로운 객체를 반환 -> 불변
var changeName = function (user, newName) {
	return {
		name: newName,
		gender: user.gender,
	};
};

// 변경한 user정보를 user2 변수에 할당하겠습니다.
// 불변이기 때문에 user1은 영향이 없어요!
var user2 = changeName(user, 'twojang');

// 결국 아래 로직이 수행되겠네요.
if (user !== user2) {
	console.log('유저 정보가 변경되었습니다.');
}

console.log(user.name, user2.name); // wonjang twojang
console.log(user === user2); // false 👍

 

 

(왼) 객체의 속성에 접근해서 변경 시도

(오) 항상 객체를 새로 만들어서 리턴 => 새로운 주소를 할당함.

 

 

  1. 위 방법이 과연 최선일까요?
  2. 안타깝지만, 그렇진 않아요. 다음과 같은 문제점이 있기 때문입니다.
    • changeName 함수는 새로운 객체를 만들기 위해 변경할 필요가 없는 gender 프로퍼티를 하드코딩으로 입력했어요 ⇒ 만일 이러한 속성이 10개라면? 리턴하는것도 10개...
    • 따라서, 다음 제시하는✨얕은 복사  의 방법을 제시할게요!
  3. 더 나은 방법 : 얕은 복사
    1. 패턴과 적용
//위 패턴을 우리 예제에 적용해봅시다.
var user = {
	name: 'wonjang',
	gender: 'male',
};

var user2 = copyObject(user);
user2.name = 'twojang';

if (user !== user2) {
	console.log('유저 정보가 변경되었습니다.');
}

console.log(user.name, user2.name);
console.log(user === user2);
//이런 패턴은 어떨까요?
var copyObject = function (target) {
	var result = {};

	// for ~ in 구문을 이용하여, 객체의 모든 프로퍼티에 접근할 수 있습니다.
	// 하드코딩을 하지 않아도 괜찮아요.
	// 이 copyObject로 복사를 한 다음, 복사를 완료한 객체의 프로퍼티를 변경하면
	// 되겠죠!?
	for (var prop in target) {
		result[prop] = target[prop];
	}
	return result;
}

 

 

 

  1. 얕은 복사 vs 깊은 복사
  2. 이 패턴도 여전히 문제가 있을까요?
  3. 하하….. 네 ㅠㅠ 여전히 있습니다. 왜냐면, 중첩된 객체에 대해서는 완벽한 복사를 할 수 없기 때문이에요. 이것이 얕은 복사의 한계입니다.
    1. 얕은 복사 : 바로 아래 단계의 값만 복사(위의 예제) 문제점 : 중첩된 객체의 경우 참조형 데이터가 저장된 프로퍼티를 복사할 때, 주소값만 복사
    2. 깊은 복사 : 내부의 모든 값들을 하나하나 다 찾아서 모두 복사하는 방법
    3. 중첩된 객체에 대한 얕은 복사 살펴보기
var user = {
	name: 'wonjang',
	urls: {
		portfolio: 'http://github.com/abc',
		blog: 'http://blog.com',
		facebook: 'http://facebook.com/abc',
	}
};

var user2 = copyObject(user);

user2.name = 'twojang';

// 바로 아래 단계에 대해서는 불변성을 유지하기 때문에 값이 달라지죠.
console.log(user.name === user2.name); // false

// 더 깊은 단계에 대해서는 불변성을 유지하지 못하기 때문에 값이 같아요.
// 더 혼란스러워 지는거죠 ㅠㅠ
user.urls.portfolio = 'http://portfolio.com';
console.log(user.urls.portfolio === user2.urls.portfolio); // true

// 아래 예도 똑같아요.
user2.urls.blog = '';
console.log(user.urls.blog === user2.urls.blog); // true
  1. 결국, ser.urls 프로퍼티도 불변 객체로 만들어야 해요.
  2. 중첩된 객체에 대한 깊은 복사 살펴보기
var user = {
	name: 'wonjang',
	urls: {
		portfolio: 'http://github.com/abc',
		blog: 'http://blog.com',
		facebook: 'http://facebook.com/abc',
	}
};

// 1차 copy
var user2 = copyObject(user);

// 2차 copy -> 이렇게까지 해줘야만 해요..!!
user2.urls = copyObject(user.urls);

user.urls.portfolio = 'http://portfolio.com';
console.log(user.urls.portfolio === user2.urls.portfolio);

user2.urls.blog = '';
console.log(user.urls.blog === user2.urls.blog);

 

 결론 

: 객체의 프로퍼티 중, 기본형 데이터는 그대로 복사 + 참조형 데이터는 다시 그 내부의 프로퍼티를 복사 ⇒ 재귀적 수행!

 

재귀적으로 수행한다? 👉함수나 알고리즘이 자기 자신을 호출하여 반복적으로 실행되는 것을 말합니다.

 

[결론] 을 적용한 코드 —> 완벽히 다른 객체를 반환하네요.

var copyObjectDeep = function(target) {
	var result = {};
	if (typeof target === 'object' && target !== null) {
		for (var prop in target) {
			result[prop] = copyObjectDeep(target[prop]);
		}
	} else {
		result = target;
	}
	return result;
}

이렇게 되면, 우리가 그토록 원하던 ‘깊은 복사’를 완벽하게 구현할 수 있습니다.

//결과 확인
var obj = {
	a: 1,
	b: {
		c: null,
		d: [1, 2],
	}
};
var obj2 = copyObjectDeep(obj);

obj2.a = 3;
obj2.b.c = 4;
obj2.b.d[1] = 3;

console.log(obj);
console.log(obj2);
  1. 마지막 방법! JSON(=JavaScript Object Notation)을 이용하는 방법도 존재합니다. 하지만 완벽한 방법은 아니에요. 간략히 장/단점을 정리해드리니, 내용만 참고해주세요 😉
    1. 장점:
    • JSON.stringify() 함수를 사용하여 객체를 문자열로 변환한 후, 다시 JSON.parse() 함수를 사용하여 새로운 객체를 생성하기 때문에, 원본 객체와 복사본 객체가 서로 독립적으로 존재합니다. 따라서 복사본 객체를 수정해도 원본 객체에 영향을 미치지 않습니다.
    • JSON을 이용한 깊은 복사는 다른 깊은 복사 방법에 비해 코드가 간결하고 쉽게 이해할 수 있습니다.
  2. 단점:
    • JSON을 이용한 깊은 복사는 원본 객체가 가지고 있는 모든 정보를 복사하지 않습니다. 예를 들어, 함수나 undefined와 같은 속성 값은 복사되지 않습니다.
    • JSON.stringify() 함수는 순환 참조(Recursive Reference)를 지원하지 않습니다. 따라서 객체 안에 객체가 중첩되어 있는 경우, 이 방법으로는 복사할 수 없습니다.
    따라서 JSON을 이용한 깊은 복사는 객체의 구조가 간단하고, 함수나 undefined와 같은 속성 값이 없는 경우에 적합한 방법입니다. 만약 객체의 구조가 복잡하거나 순환 참조가 있는 경우에는 다른 깊은 복사 방법을 고려해야 합니다.

 

결론 : 불변성을 유지하는 방법! 얕은 복사깊은 복사가 있다! 깊은 복사에는 재귀적 수행하는 방법이 제일 알맞다!

 

 

 (6) undefined와 null 

둘 다 없음을 나타내는 값이에요. 하지만 미세하게 다르고, 그 목적 또한 다르답니다. 아래에서 한번 살펴보도록 할께요!

  1. undefined
    1. 사용자(=개발자)가 직접 지정할 수도 있지만 일반적으로는 자바스크립트 엔진에서 값이 있어야 할 것 같은데 없는 경우, 자동으로 부여합니다. 다음 케이스를 생각해 주시면 될 것 같아요.
      1. 변수에 값이 지정되지 않은 경우, 데이터 영역의 메모리 주소를 지정하지 않은 식별자에 접근할 때
      2. .이나 []로 접근하려 할 때, 해당 데이터가 존재하지 않는 경우
      3. return 문이 없거나 호출되지 않는 함수의 실행 결과
var a;
console.log(a); // (1) 값을 대입하지 않은 변수에 접근

var obj = { a: 1 };
console.log(obj.a); // 1
console.log(obj.b); // (2) 존재하지 않는 property에 접근
// console.log(b); // 오류 발생

var func = function() { };
var c = func(); // (3) 반환 값이 없는 function
console.log(c); // undefined

 

2가지 역할을 가진 undefined, 헷갈릴 만도 해요. 그리고 위험해요!

  1. 지금 undefined로 나오는 이 변수가, 필요에 의해 할당한건지 자바스크립트 엔진이 반환한건지 어떻게 알죠? 👉 구분할 수 없어요.
  2. ‘없다’를 명시적으로 표현할 때는 undefined를 사용하지 맙시다!
  1. null
    1. 용도 : ‘없다’를 명시적으로 표현할 때
    2. 주의 : typeof null
    3. typeof null이 object인 것은 유명한 javascript 자체 버그입니다. 조심해야겠죠?
var n = null;
console.log(typeof n); // object

//동등연산자(equality operator)
console.log(n == undefined); // true
console.log(n == null); // true

//일치연산자(identity operator)
console.log(n === undefined);
console.log(n === null);

 

 

 

  2. 실행컨텍스트(스코프, 변수, 객체, 호이스팅)  

자바스크립트의 실행 컨텍스트실행할 코드에 제공할 환경 정보들을 모아놓은 객체입니다. 자바스크립트는 어떤 실행 컨텍스트가 활성화되는 시점에 다음과 같은 일을 합니다.

  1. 선언된 변수를 위로 끌어올리구요 = 호이스팅(hoisting)
  2. 외부 환경 정보를 구성하구요.
  3. this 값을 설정해요.

이런 현상들 때문에 JS에서는 다른 언어랑은 다른 특징들이 나타난답니다.

 

 (1) 실행 컨텍스트란? 

실행 컨텍스트를 이해하기 위해서는, 콜 스택에 대한 이해가 반드시 필요합니다. 자, 그 전에 스택이라는 개념에 대해서 먼저 이해를 해야겠네요.

  • 스택 vs 큐
  • 콜 스택(call stack)
    • 실행 컨텍스트란 실행할 코드에 제공할 환경 정보들을 모아놓은 객체라고 했었죠. 그 객체. 즉, 동일 환경에 있는 코드를 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고 이것을 위에서 설명드린 ‘스택’의 한 종류인  콜스택에 쌓아올립니다. 가장 위에 쌓여있는 컨텍스트와 관련된 코드를 실행하는 방법으로 코드의 환경 및 순서를 보장할 수 있어요.
    1. 컨텍스트의 구성
      1. 구성방법(여러가지가 있지만, 사실 우리는 함수만 생각하면 돼요 👍)
        1. 전역공간
        2. eval()함수
        3. 함수(우리가 흔히 실행컨텍스트를 구성하는 방법)
      2. 실행컨텍스트 구성 예시 코드
// ---- 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);
  1. 실행컨텍스트 구성 순서코드실행 → 전역(in) → 전역(중단) + outer(in) → outer(중단) + inner(in) → inner(out) + outer(재개) → outer(out) + 전역(재개) → 전역(out) → 코드종료
  2. 위 코드는 아래 순서로 진행이 됩니다. (콜 스택에 쌓이는 실행컨텍스트에 의해 순서가 보장되니까요!)
  3. 결국은 특정 실행 컨텍스트가 생성되는(또는 활성화되는) 시점이 콜 스택의 맨 위에 쌓이는(노출되는) 순간을 의미하구요. 곧, 현재 실행할 코드에 해당 실행 컨텍스트가 관여하게 되는 시점을 의미한다고 받아들여주시면 정확합니다! 

 

자바스크립트는 콜스택에 쌓아두기 때문에 실행 순서를 보장할 수 있다.

스택의 가장 밖에서. 공기에 노출되고 있는 시점이 현재코드에 관여하게 되는 시점이다.

 

 

  • 실행 컨텍스트 객체의 실체(=담기는 정보)
    1. VariableEnvironment
      1. 현재 컨텍스트 내의 식별자 정보(=record)를 갖고있어요.
        1. var a = 3
        2. 위의 경우, **var a**를 의미
      2. 외부 환경 정보(=outer)를 갖고있어요.
      3. 선언 시점 LexicalEnvironment의 snapshot
    2. LexicalEnvironment
      1. VariableEnvironment와 동일하지만, 변경사항을 실시간으로 반영해요.
    3. ThisBinding
      1. this 식별자가 바라봐야할 객체
  • 계속 이 객체에 대해 얘기는 해오고 있는데, 그 실체를 한번 알아봅시다. 3가지로 알아볼께요.

 

 (2) VariableEnvironment, LexicalEnvironment의 개요 

  • VE vs LE
    1. VE : 스냅샷을 유지해요. (실시간으로 바뀌지 않는다.)
    2. LE : 스냅샷을 유지하지 않아요. 즉, 실시간으로 변경사항을 계속해서 반영합니다.
    결국, 실행 컨텍스트를 생성할 때, VE에 정보를 먼저 담은 다음, 이를 그대로 복사해서 LE를 만들고 이후에는 주로 LE를 활용합니다.

 

  • 이 두가지는 담기는 항목은 완벽하게 동일해요. 그러나, 스냅샷 유지여부는 다음과 같이 달라요.
  • 구성 요소(VE, LE 서로 같아요!)
    1. VE, LE모두 동일하며, ‘environmentRecord’와 ‘outerEnvironmentReference’로 구성
    2. environmentRecord(=record)
      1. 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장돼요.
      2. 함수에 지정된 매개변수 식별자, 함수자체, var로 선언된 변수 식별자 등
    3. outerEnvironmentReference(=outer)

 

 (3) LexicalEnvironment(1) - environmentRocord(=record)와 호이스팅 

  • 개요
    1. 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장(수집)돼요. 기록된다라고 이해해보면, record라는 말과 일맥상통하죠?
    2. 수집 대상 정보 : 함수에 지정된 매개변수 식별자, 함수 자체, var로 선언된 변수 식별자 등
    3. 컨텍스트 내부를 처음부터 끝까지 순서대로 훑어가며 수집
    4. 순서대로 수집한다고 했지, 코드가 실행된다고 하지는 않았습니다!
  • 호이스팅
    1. 변수정보 수집을 모두 마쳤더라도 아직 실행 컨텍스트가 관여할 코드는 실행 전의 상태에요(JS 엔진은 코드 실행 전 이미 모든 변수정보를 알고 있는 것)
    2. 변수 정보 수집 과정을 이해하기 쉽게 설명한 가상 개념
    3. 💡 가상개념이라는 말은, 실제로는 그렇진 않더라도 사람이 이해하기 쉬운 말로 풀어 표현했다는 것을 의미한다.
    4. 식별자 정보를 모두 위로 끌어올린다.
  • 호이스팅 규칙
    1. 호이스팅 법칙 1 : 매개변수 및 변수는 선언부를 호이스팅 합니다.<적용 전>
    2. 다음 주석에 딸려있는 3가지 action point에 따라 실습을 진행해보세요!
//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);

 

 

<호이스팅 적용>

//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 라는 결과가 나왔네요

 

호이스팅이라는 개념을 모르면 예측이 불가능한 어려운 결과입니다.

  1. 호이스팅 법칙 2 : 함수 선언은 전체를 호이스팅합니다. 마찬가지로, 2가지 action points에 따라 진행해봅시다.
  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();

✨function a() 바로 밑에 console.log(b); 가 b가 선언되어 있지 않아서 에러가 날 줄 알았지만

호이스팅이 적용되었기 때문에 식별자 정보를 모두 위로 끌어올려서. 에러가 나지 않는다.

 

 

<호이스팅 적용>

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

 

 

해석을 편하게 하기 위해서 함수선언문을 함수 표현식으로 바꿔볼게요!

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

이번에도 우리의 예상은 틀렸네요.

에러(또는 undefined), ‘bbb’, b함수

라고 나올 것 같았지만, 실제로는

b함수, ‘bbb’, ‘bbb’

 

라는 결과가 나왔어요. 이 또한 호이스팅을 고려하지 않고는 결과를 예측하기가 매우 어려웠어요.

 

근데 함수라고 다 호이스팅 되는 건 아님!!

 

호이스팅을 다루는 김에, 함수의 정의방식 3가지주의해야 할 내용을 살짝 짚고 넘어가 보도록 하겠습니다 😉

  1. 함수 선언문, 함수 표현식
    1. 함수 정의의 3가지 방식
// 함수 선언문. 함수명 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(); // 에러!

 

 

  1. 주의해야 할 내용
    1. 함수 선언문, 함수 표현식
    2. 자, 정신없이 달려오느라 내용이 정리가 안됐을 수 있어요. 다시 잠깐 교통정리를 해드릴께요! 지금 우리는 아래 내용을 하고 있어요 😎

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

📌 그 객체 안에는 3가지가 존재한다.

    👉 VariableEnvironment

    👉 LexicalEnvironment

    👉 ThisBindings

 

📌 VE와 LE는 실행컨텍스트 생성 시점에 내용이 완전히 같고, 이후 스냅샷 유지 여부가 다르다.

📌 LE는 다음 2가지 정보를 가지고 있다.

    👉 record(=environmentRecord) ← 이 record의 수집과정이 hoisting

    👉 outer(=outerEnvironmentReference)

 

길을 잃지 마세요 여러분 😅😅

 

 

계속해서 가 봅시다. 함수 선언문과 표현식을 배웠으니, 실질적인 차이를 예시를 통해 배워볼께요.

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이 일어나고, 우리가 익히 알고있는대로 위로 끌어올려본 결과를 다시 써보면 아래와 같겠네요.

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

어떤가요? 눈으로 볼 때는 몰랐지만, 함수 선언문과 함수 표현식은 hoisting 과정에서 극명한 차이를 보입니다.

차이는 알겠지만 이게 왜 위험한지 모르겠다구요? 네. 그러면 아래 예를 통해 그 이유를 확인해보죠.

 

  1. 함수 선언문을 주의해야 하는 이유
...

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

 

협업을 많이 하고, 복잡한 코드일 수록. 전역 공간에서 이루어지는 코드 협업일 수록

함수 표현식을 활용하는 습관을 들이도록 합시다!!

 

 

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

우리는 이미 앞서 ‘스코프’라는 용어에 대해 몇차례 언급을 해 왔습니다. 실행컨텍스트 관점에서의 스코프를 같이 이해해 보도록 합시다.

  • 주요 용어
    1. 스코프
      1. 식별자에 대한 유효범위를 의미해요
      2. 대부분 언어에서 존재하구요, 당연하게도 JS에서도 존재하죠
    2. 스코프 체인
      1. 식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것
    3. outerEnvironmentReference(이하 outer)스코프 체인이 가능토록 하는 것(외부 환경의 참조정보)라고 할 수 있어요
      • 외부 환경의 참조정보 라는 말에 집중해주세요.
      위에 나온 개념을 좀 더 자세히 살펴보도록 하겠습니다.
    4. 우리는 지금까지 LE의 구성요소 record와 outer 중 record에 대해서 깊이 알아오고 있었어요. 이번 시간의 주인공은 그 두번째인 outer입니다. outer의 역할을 한 마디로 정의하자면
  • 스코프 체인
    1. outer는 현재 호출된 함수가 선언될 당시(이 말이 중요해요!)의 LexicalEnvironment를 참조해요. 참조한다는 말이 어려우면, 그 당시의 환경 정보를 저장한다. 정보로 이해해도 괜찮습니다.
    2. 예를 들어, **A함수 내부에 B함수 선언 → B함수 내부에 C함수 선언(Linked List)**한 경우 어떻게 될까요?
    3. 결국 타고, 타고 올라가다 보면 **전역 컨텍스트의 LexicalEnvironment를 참조**하게 됩니다.
    4. 항상 outer는 오직 자신이 선언된 시점의 LexicalEnvironment를 참조하고 있으므로, 가장 가까운 요소부터 차례대로 접근 가능
    5. 결론 : 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에게만 접근이 가능
  • c가 생성될 때는 그때의 환경은 b잖아요.
    b의 LE를 c의 outer로 가지고 있는다.
    왜 가지고 있냐? 그때의 변수정보 같은 것들을 참조하기 위해서.
    그래서 스코프 체이닝이 가능하게 한다. 
  • (대부분의 경우 text는 예시보다 항상 어렵죠 예시를 통해 볼께요)
// 아래 코드를 여러분이 직접 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.log => inner는 이미 사라졌고 전역에 있는 a가 찍혀서 1이 된다.

 

 

위의 과정을 통해 이제는 아래의 한 문장이 이해가 되었으면 좋겠어요 🙏🙏

각각의 실행 컨텍스트는 LE 안에 record와 outer를 가지고 있고, outer 안에는 그 실행 컨텍스트가 선언될 당시의 LE정보가 다 들어있으니 scope chain에 의해 상위 컨텍스트의 record를 읽어올 수 있다.


 

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

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

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

this는 상황별로 어떻게 달라집니다.

 

 

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

잊지 않으셨겠죠? 또 리마인드!!

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

  • 그 객체 안에는 3가지가 존재한다.
  • VariableEnvironment
  • LexicalEnvironment
  • ThisBindings

 

  1. this는 실행 컨텍스트가 생성될 때 결정돼요. 이 말을 this를 bind한다(=묶는다) 라고도 하죠. 다시 말하면. this는 함수를 호출할 때 결정된다. 라고 할 수 있습니다.
    1. 전역 공간에서의 this
      1. 전역 공간에서 this는 전역 객체를 가리켜요.
      2. 런타임 환경에 따라 this는 window(브라우저 환경) 또는 global(node 환경)를 각각 가리킵니다.
      3. 런타임( run + time ) 👉  코드가 돌아가는 그 시간

 

💡 런타임 환경?

여러분들이 javascript로 만들어놓은 프로그램이 구동중인 환경을 말하죠. 우리는 node 파일이름.js로 vscode 상에서 구동하고 있으니 node 환경이라고 할 수 있구요. html 파일 안에 숨겨놓아서 크롬브라우저 등에서 연다고 한다면 브라우저 환경이라고 할 수 있겠네요.

 

 

<브라우저 환경 this 확인>

console.log(this);
console.log(window);
console.log(this === window);

 

 

<node 환경 this 확인>

console.log(this);
console.log(global);
console.log(this === global);

 

전역 환경에서 this는

노드(global 객체), 브라우저(window 객체)

 

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

 

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

객체.메서드명();

 

 

함수 : this는 전역 객체 (호출의 주체를 명시할 수 없기 때문에)

메서드 : this는 호출의 주체

 

 

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

 

 

  1. 함수로서의 호출과 메서드로서의 호출 구분 기준 : . [ ]
  2. 아래 예시도 같아요! 점(.)으로 호출하든, 대괄호([ ])로 호출하든 결과는 같습니다 😉
var obj = {
	method: function (x) { console.log(this, x) }
};
obj.method(1); // { method: f } 1
obj['method'](2); // { method: f } 2

 

 

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

 

  1. 함수로서 호출할 때 그 함수 내부에서의 this
    1. 함수 내부에서의 this
      1. 어떤 함수를 함수로서 호출할 경우, this는 지정되지 않아요(호출 주체가 알 수 없으니까요)
      2. 실행컨텍스트를 활성화할 당시 this가 지정되지 않은 경우, this는 전역 객체를 의미하죠
      3. 따라서, 함수로서 ‘독립적으로’ 호출할 때는 **this는 항상 전역객체를 가리킨다**는 것을 주의하길 바래요 👍
    2. 메서드의 내부함수에서의 this
      1. 예외는 없습니다! 메서드의 내부라고 해도, 함수로서 호출한다면 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), (2), (3)을 예측해볼까요?

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

 

맞았다면 여러분은 this에 대해 정말 많이 이해하신 거에요 👍👍

❗ this 바인딩에 관해서는 함수를 실행하는 당시의 주변 환경(메서드 내부인지, 함수 내부인지)은 중요하지 않고,

오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지가 관건이라는 것을 알 수 있습니다.

 

  1. 메서드의 내부 함수에서의 this 우회
    1. 변수를 활용하는 방법
    2. 내부 스코프에 이미 존재하는 this를 별도의 변수(ex : self)에 할당하는 방법이에요!
    3. this에 대해서 이해는 하겠지만… 사용자 입장에서. 즉, 개발자 입장에서 이게 쉽게 받아들여지시나요? 그렇지 않죠? 그렇기 때문에 우회할 수 있는 방법을 우리는 찾아볼 수 있습니다.
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를 바인딩하지 않는 함수)
    1. ES6에서 처음 도입된 화살표 함수는, 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 없습니다(따라서, this는 이전의 값-상위값-이 유지돼요 / ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제 때문에 화살표함수를 도입했어요!)
    2. 일반 함수와 화살표 함수의 가장 큰 차이점은 무엇인가요? this binding 여부가 가장 적절한 답입니다.
    3. 화살표 함수는 this binding 과정 자체가 없다!
var obj = {
	outer: function() {
		console.log(this); // (1) obj
		var innerFunc = () => {
			console.log(this); // (2) obj
		};
		innerFunc();
	}
}

obj.outer();

 

 

  1. 콜백 함수 호출 시 그 함수 내부에서의 this
  2. 우리는 앞선 과정에서 콜백 함수를 다음과 같이 정의한 적이 있어요.
  3. “어떠한 함수, 메서드의 인자(매개변수)로 넘겨주는 함수” 
  4. 이 때, 콜백함수 내부의 this는 해당 콜백함수를 넘겨받은 함수(메서드)가 정한 규칙에 따라 값이 결정된답니다. 콜백 함수도 함수기 때문에 this는 전역 객체를 참조하지만(호출 주체가 없잖아요), 콜백함수를 넘겨받은 함수에서 콜백 함수에 별도로 this를 지정한 경우는 예외적으로 그 대상을 참조하게 되어있어요. 다음 예시를 통해 구체적으로 알아봅시다.
  5.  로직을 이해하는 것 보다는, 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);
});

 

콜백함수도 함수고 함수로써의 호출이기 때문에 얘네는 this를 다 잃어 버리게 된다.

this binding을 하면 다 전역 객체를 바라보게 된다.

그래서 콜백함수는 기본적으로 전역객체를 바라보고 있다!

 

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

 

 

  1. setTimeout 함수, forEach 메서드는 콜백 함수를 호출할 때 대상이 될 this를 지정하지 않으므로, this는 곧 window객체
  2. addEventListner 메서드는 콜백 함수 호출 시, 자신의 this를 상속하므로, this는 addEventListner의 앞부분(button 태그)

 

 

  1. 생성자 함수 내부에서의 this
    1. 생성자 : 구체적인 인스턴스(어려우면 객체로 이해!)를 만들기 위한 일종의 틀
    2. 공통 속성들이 이미 준비돼 있어요.
    3. 객체를 생성하는 방법에서 이미 언급한 적이 있었죠. 거기서 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 메서드
    1. 호출 주체인 함수를 즉시 실행하는 명령어에요.
    2. call명령어를 사용하여, 첫 번째 매개변수에 this로 binding할 객체를 넣어주면 명시적으로 binding할 수 있어요. 쉽죠?
    3. 예시를 통해 확인해봅시다.
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

 

 

  1. apply 메서드
    1. call 메서드와 완전히 동일해요! 다만, this에 binding할 객체는 똑같이 넣어주고 나머지 부분만 배열 형태로 넘겨줍니다.
    2. 예시
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

 

 

 

  1. call / apply 메서드 활용
    1. 유사배열객체(array-like-object)에 배열 메서드를 적용
  2. 물론 this binding을 위해 call, apply method를 사용하기도 하지만 더 유용한 측면도 있습니다.
//객체에는 배열 메서드를 직접 적용할 수 없어요.
//유사배열객체에는 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)따라서, ES6에서는 **Array.from**이라는 방법을 제시했는데요. 아주 편리해요!
  2. 사실, call/apply를 통해 this binding을 하는 것이 아니라 **객체 → 배열**로의 형 변환 만을 위해서도 쓸 수 있지만 원래 의도와는 거리가 먼 방법이라 할 수 있습니다.
// 유사배열
var obj = {
	0: 'a',
	1: 'b',
	2: 'c',
	length: 3
};

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

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

 

 

 

생성자 내부에서 다른 생성자를 호출(공통된 내용의 반복 제거)

 

Student, Employee 모두 Person입니다. name gender 속성 모두 필요하죠. 그러니 Student와 Employee 인스턴스를 만들 때 마다 세 가지 속성을 모두 각 생성자 함수에 넣기 보다는 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', '삼성');

 

 

여러 인수를 묶어 하나의 배열로 전달할 때 apply 사용할 수 있어요.

apply를 통해 비효율적인 예시를 효율적인 예시로 바꿔봅시다!

//비효율
var numbers = [10, 20, 3, 16, 45];
var max = min = numbers[0]; // 10
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);

 

 

 

 bind 메서드 

👉 this를 바인딩하는 메서드, call, apply와는 좀 다름. 즉시 호출되지 않는다.

 

  1. call과 비슷해 보입니다. 하지만, 즉시 call과는 다르게 즉시 호출하지는 않고 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하는 메서드라고 보시면 돼요!
  2. 목적
    1. 함수에 this를 미리 적용해요!
    2. 부분 적용 함수 구현할 때 용이합니다.
  3. 예시
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

 

 

 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

 

 

 

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

  1. 내부함수
    1. 메서드의 내부함수에서 메서드의 this를 그대로 사용하기 위한 방법이에요(이전에는 내부함수에 this를 전달하기 위해 self를 썼었던 것 기억 나시나요?)
    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);
		var innerFunc = function () {
			console.log(this);
		}.bind(this); // innerFunc에 this를 결합한 새로운 함수를 할당
		innerFunc();
	}
};
obj.outer();

 

 

 콜백함수 

  1. 콜백함수도 함수이기 때문에, 함수가 인자로 전달될 때는 함수 자체로 전달해요.
  2. (this가 유실되죠!)
  3. 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();

 

 

 화살표 함수의 예외사항 

  1. 화살표 함수는 실행 컨텍스트 생성 시, this를 바인딩하는 과정이 제외된다고 했었죠!
  2. 이 함수 내부에는 this의 할당과정(바인딩 과정)이 아에 없으며, 접근코자 하면 스코프체인상 가장 가까운 this에 접근하게 됨
  3. this우회, call, apply, bind보다 편리한 방법
var obj = {
	outer: function () {
		console.log(this);
		var innerFunc = () => {
			console.log(this);
		};
		innerFunc();
	};
};
obj.outer();