본문 바로가기
내일배움 강의/완강-JS 문법 종합반

3주차 : 데이터 타입(심화), 실행 컨텍스트, this

by GREEN나무 2024. 11. 7.
728x90

1. 데이터 타입 심화


(1) 데이터 타입의 종류(기본형과 참조형)

◆ 자바스크립트에서 값의 타입  

▷ 기본형(Primitive Type)
복제의 방식 : 값이 담긴 주소값을 바로 복제
불변성의 여부 : 불변하다(메모리의  데이터영역 변경불가)

 

※ 불변하다 : 메모리의  데이터영역 변경불가. 바뀐 값은 데이터영역의 다른 위치에 저장하고 변수 영역에서 데이터 위치를 바꿉니다.


▷ 참조형(Reference Type)
복제의 방식 : 값이 담긴 주소값들로 이루어진 묶음을 가리키는 주소값을 복제
불변성의 여부 : 불변성을 띄지 않음

참조형은 기본적으로 객체(object)입니다.

(2) 메모리와 데이터에 관한 배경지식

  메모리, 데이터

▷ 비트 : 컴퓨터가 이해할 수 있는 가장 작은 단위.
0과 1을 가지고 있는 메모리를 구성하기 위한 작은 조각.
 바이트 : 8bit = 1byte
  메모리(memo + ry) : byte 단위로 구성
  ★모든 데이터는 byte 단위의 식별자인 메모리 주소값을 통해서 서로 구분이 됩니다.
       ex) 64비트의 정수는 8개의 바이트로 분할하고, 각 바이트를 메모리에 저장합니다. 각 바이트는 8개의 비트를 가지므로 64비트 정수는 메모리에서 8개의 연속된 바이트에 저장된답니다.
  java, c와 다른 javascript의 메모리 관리 방식(feat. 정수형)
    8을 저장하는 방법

let a = 8(8byte) //JS
// JAVA
a = 8(1byte) // byte 
a = 8(2byte) // short 
a = 8(4byte) // int 
a = 8(16byte) // long


java 또는 c언어가 초기에 등장했을 때 숫자 데이터 타입은 크기에 따라 다양하게 지정해줘야 할 만큼 개발자가 handling 할 요소들이 많았어요. 하지만 javascript는 이런 부분에서는 상당히 편리하죠. 메모리 이슈까지는 고민하지 않아도 되니까요 😎​

◆  식별자       변수
var testValue = 3
식별자가 변수명,  변수는 데이터

(3) 변수 선언과 데이터 할당

/** 선언과 할당을 풀어 쓴 방식 */
var str;
str = 'test!';

/** 선언과 할당을 붙여 쓴 방식 */
var str = 'test!';

선언한 변수의 데이터 주소

변수영역은 데이터 영역보다 사용하는 용량이 작습니다. 데이터의 위치 저장.

◆  값을 바로 변수에 대입하지 않는 이유(=무조건 새로 만드는 이유)
 자유로운 데이터 변환
 메모리의 효율적 관리

(4) 기본형 데이터와 참조형 데이터

◆ 메모리를 기준으로 다시한번 생각해보는 두 가지 주요 개념
  변수 vs 상수
변수 : 변수 영역 메모리를 변경할 수 있음
상수 : 변수 영역 메모리를 변경할 수 없음
 불변하다 vs 불변하지 않다
불변하다 : 데이터 영역 메모리를 변경할 수 없음
불변하지 않다 : 데이터 영역 메모리를 변경할 수 있음

 


◆ 불변값과 불변성(with 기본형 데이터)

데이터 영역의 저장값은 바뀌지 않습니다. 변수 값의 변경 시 비어있는 데이터 영역에 저장 후 그 주소갔을 가져와 바꿉니다.

더 이상 사용되지 않는 데이터는 가비지컬렉터의 수거 대상이 됩니다.

 

  가변값과 가변성(with 참조형 데이터)

 기본형 데이터의 변수 할당 과정과 차이점 : 객체의 변수(프로퍼티) 영역의 별도 존재 여부

  데이터 영역에 저장된 값은 여전히 계속 불변값이지만, obj1을 위한 별도 영역은 변경이 가능합니다.

   이것 때문에 참조형 데이터를 ‘불변하지 않다(=가변하다)’라고 합니다.

 참조 카운트

      객체를 참조하는 변수나 다른 객체의 수를 나타내는 값입니다.

      참조 카운트가 0인 객체는 더 이상 사용되지 않으므로, 가비지 컬렉터에 의해 메모리에서 제거됩니다.

 가비지컬렉터(GC, Garbage Collector)

      더 이상 사용되지 않는 객체를 자동으로 메모리에서 제거하는 역할을 합니다.

      자바스크립트는 가비지 컬렉션을 수행함으로써 개발자가 명시적으로 메모리 관리를 하지 않아도 되도록 지원합니다.

      가비지 컬렉션에 대한 직접적인 제어 불가

 참조형 데이터의 변수 할당 과정

// 참조형 데이터는 별도 저장공간(obj1을 위한 별도 공간)이 필요합니다!
var obj1 = {
	a: 1,
	b: 'bbb,
};
// 데이터를 변경해봅시다.
obj1.a = 2;

 

 중첩객체의 할당
객체 안에 또 다른 객체가 들어가는 것

객체는 배열, 함수 등을 모두 포함하는 상위개념.

var obj = {
x: 3,
arr: [3, 4, 5],
}

 

 

불변성 유지

 

  중첩객체의 복사 유의점

더보기


 변수 복사의 비교

// STEP01. 선언
var a = 10; //기본형
var obj1 = { c: 10, d: 'ddd' }; //참조형

// STEP02. 복사
var b = a; //기본형
var obj2 = obj1; //참조형

// STEP03. 수정 ( 객체의 프로퍼티 변경 )
b = 15; //기본형
obj2.c = 20; //참조형
객체의 프로퍼티 변경
// 객체의 프로퍼티 변경
// STEP01.  선언.
var a = 10; //기본형
var obj1 = { c: 10, d: 'ddd' }; //참조형

// STEP02. 복사
var b = a; //기본형
var obj2 = obj1; //참조형

b = 15;
obj2.c = 20;
객체 자체를 변경

  기본형

    - 변수 복사

         숫자 15라는 값을 데이터 영역에서 검색 후 없다면 생성

          검색한 결과주소 또는 생성한 주소를 변수 영역 b에 갈아끼움

    -  복사 후 값 변경 ( 객체의 프로퍼티 변경 )

          a와 b는 서로 다른 데이터 영역의 주소를 바라보고 있기 때문에 b의 수정은 a에 영향 없습니다.
    

  참조형

    -  변수 복사

          숫자 20이라는 값을 데이터 영역에서 검색 후 없다면 생성
          검색한 결과주소 또는 생성한 주소 obj2에게 지정되어 있는 별도 영역(7103~)에 갈아끼움

    -  복사 후 값 변경 ( 객체의 프로퍼티 변경 )
          obj1도 똑같은 주소를 바라보고 있기 때문에 obj2를 수정하면 obj1까지 변경이 됩니다.

    -  복사 이후 값 변경(객체 자체를 변경)

           obj2 변수는  obj1와 바라보는 데이터 메모리 영역의 값이 달라졌습니다

           obj2 의 변경은 obj1에게 영향을 주지 않습니다.

 

 

 

 가변성의 문제점(객체의 프로퍼티 변경) 예시

var user ={
  name: "wonjang",
  gender : "male",
};

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

// 변경한 user 정보를 user2 변수에 할당합니다.
// 가변하기 때문에 user이 영향을 받게 됩니다.
var user2 = changeName(user, "twojang");


// user2를 변경하면 user도 동알하게 변경됩니다.
if (user !== user2){ // false
  console.log("유저 정보가 변경되었습니다.");
}

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

 

 해결방법 (객체 자체를 변경) 예시

var user ={
  name: "wonjang",
  gender : "male",
};

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

// 변경한 user 정보를 user2 변수에 할당합니다.
// 불변하기 때문에 user는 영향이 없습니다.
var user2 = changeName(user, "twojang");


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

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

 

불변 객체

1.  불변 객체의 정의
객체의 속성에 접근해서 값을 변경하면 가변이 성립하나, 객체 데이터 자체를 변경(새로운 데이터를 할당)하고자 한다면 기존 데이터는 변경되지 않습니다. 즉, 불변하다라고 볼 수 있습니다.


2.  불변 객체의 필요성(가변성의 필요성)

객체 자체를 변경하는 변수 복사에도 문제점이 있기 습니다.
새로운 객체를 만들기 위해 변경할 필요가 없는 프로퍼티를 하드코딩으로 입력해야 합니다.
따라서, 다음 제시하는 얕은 복사의 방법을 사용하세요.
3. ★얕은 복사
 패턴

// 얕은 복사
var copyObject = function (target) { // 객체형태의 변수를 받습니다.
	var result = {};

	// for ~ in 구문을 이용하여, 객체의 모든 프로퍼티에 접근할 수 있습니다.
    // 객체의 프로퍼티에 하나하나 접근합니다.
	// 하드코딩을 하지 않아도 됩니다.
	// 이 copyObject로 복사를 한 다음, 복사를 완료한 객체의 프로퍼티를 변경합니다.

	for (var prop in target) {
		result[prop] = target[prop];
	}
	return result;
}

 

 얕은 복사 적용

// 얕은 복사 패턴 적용
var user = {
	name: 'wonjang',
	gender: 'male',
};

// user를 복사할 때 copyObject를 이용해서 복사합니다.
// user2는 user와 별개의 오브젝트로 존재합니다.
var user2 = copyObject(user);
user2.name = 'twojang';

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

console.log(user.name, user2.name);
console.log(user === user2);


​​
4. 얕은 복사 + 깊은 복사

 얕은 복사 : 중첩되지 않은 객체를 복사할 때만 사용하세

   얕은 복사는 객체의 첫 단계 값만 복사하므로 중첩된 객체나 참조형 데이터의 경우 주소값만 복사하게 되어, 원본과 복사 본이 동일한 참조를 공유하게 됩니다. 

   이로 인해 한쪽을 수정하면 다른 쪽도 영향을 받아 깊이 있는 복사에는 한계가 있습니다.
 깊은 복사 : 내부의 모든 값들을 하나하나 다 찾아서 모두 복사하는 방법으로 중첩 객체의 객체 자체를 변경 하는 복사가 가능합니다.


  중첩된 객체에 대한 얕은 복사 살펴보기

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

// 결국, ser.urls 프로퍼티도 불변 객체로 만들어야 합니다.
// 깊은 복사가 필요합니다.


◆ 중첩된 객체에 대한 깊은 복사 살펴보기

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);
//객체의 프로퍼티 중, 
//기본형 데이터는 그대로 복사 + 참조형 데이터는 다시 그 내부의 프로퍼티를 복사
// ⇒ 재귀적 수행이 필요합니다

 

 

(5) 중첨객체 복사하기

 ◆ 방법1 재귀적 수행 (Recursive)
 함수나 알고리즘이 자기 자신을 호출하여 반복적으로 실행되는 것을 말합니다.

//재귀적으로 수행
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); // { a: 1, b: { c: null, d: [ 1, 2 ] } }
console.log(obj2); // { a: 3, b: { c: 4, d: { '0': 1, '1': 3 } } }

​‘깊은 복사’를 완벽하게 구현할 수 있습니다.

A가 호출될 때 A스스로 다시 한번 안쪽에서 호출하면서 이 객체 안의 깊은 곳 까지 다 훑어 봅니다.

 방법2  JSON(=JavaScript Object Notation)

완벽한 방법은 아닙니다. 

JSON을 이용한 깊은 복사는 객체 구조가 간단하고 함수나 `undefined` 속성이 없는 경우 적합합니다. 복잡한 구조나 순환 참조가 있는 객체는 다른 깊은 복사 방법을 고려해야 합니다.

 

 장점
- 독립적 복사 : `JSON.stringify()`로 객체를 문자열로 변환하고 `JSON.parse()`로 다시 객체를 만들기 때문에, 원본과 복사본이 독립적입니다. 복사본을 수정해도 원본에 영향을 미치지 않습니다.
- 간결한 코드 : 다른 깊은 복사 방법에 비해 코드가 간단하고 이해하기 쉽습니다.

 단점
- 제한된 정보 복사 : 함수나 `undefined` 속성값 등은 JSON 변환 과정에서 제외됩니다.
- 순환 참조 미지원 : `JSON.stringify()`는 순환 참조를 처리하지 못하므로 중첩된 객체 구조에서는 사용할 수 없습니다.


(6) undefined와 null

둘 다 없음을 나타내는 값이지만 차이가 있습니다.
  undefined
사용자(=개발자)가 직접 지정할 수도 있지만 일반적으로는 자바스크립트 엔진에서 값이 있어야 할 것 같은데 없는 경우, 자동으로 부여합니다. 

  변수에 값이 지정되지 않은 경우, 데이터 영역의 메모리 주소를 지정하지 않은 식별자에 접근할 때
  '.'나 []로 접근하려 할 때, 해당 데이터가 존재하지 않는 경우
  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

‘없다’를 명시적으로 표현할 때는 null을 사용하세요.


  null
 용도 : ‘없다’를 명시적으로 표현할 때
 주의 : 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); // false
console.log(n === null); // true

 

 

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

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

 

◆ 실행 컨텍스트가 하는 일. 
▷ 호이스팅(hoisting) = 선언된 변수를 위로 끌어올림
  외부 환경 정보를 구성
  this 값을 설정
이런 현상들 때문에 JS에서는 다른 언어랑은 다른 특징들이 나타난답니다.


(1) 실행 컨텍스트란?

실행 컨텍스트를 이해하기 위해서는, 콜 스택에 대한 이해가 반드시 필요합니다.
  스택(LIFO) vs 큐(FIFO)



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

 

  콜 스택(call stack)

실행 컨텍스트: 코드 실행에 필요한 환경 정보(변수, 함수, 스코프 등)를 담은 객체

콜 스택: 실행 컨텍스트를 순서대로 쌓아 관리하는 구조로 실행된 컨텍스트는 사라집니다.

코드 실행 순서 보장: 콜 스택의 최상단 컨텍스트가 현재 실행 중인 코드의 환경과 순서를 유지하여, 올바르게 코드가 실행되도록 합니다.


컨텍스트의 구성
▷ 구성방법
- 전역공간
- eval()함수
- 함수(우리가 흔히 실행컨텍스트를 구성하는 방법)

//실행컨텍스트 구성 예시 코드
// ---- 1번
var a = 1;
function outer() {
	function inner() {
		console.log(`1번 : ${a}`); // 1번 : undefined
		var a = 3;
	}
	inner(); // ---- 2번
	console.log(`2번 : ${a}`); // 2번 : 1
}
outer(); // ---- 3번
console.log(`3번 : ${a}`); // 3번 : 1


  실행컨텍스트 구성 순서
콜 스택에 쌓이는 실행컨텍스트에 의해 순서가 보장됩니다.

 

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

 

항상 '전역'이 가장먼저 in 됩니다.


특정 실행 컨텍스트가 생성되는(또는 활성화되는) 시점이 콜 스택의 맨 위에 쌓이는(노출되는) 순간을 의미하구요. 곧, 현재 실행할 코드에 해당 실행 컨텍스트가 관여하게 되는 시점을 의미한다

 

(2) 실행 컨텍스트 객체의 실체(=담기는 정보)


◆ VariableEnvironment
현재 컨텍스트 내의 식별자 정보(=record)를 갖고있습니다.

 var a = 3   //식별자는 var a

외부 환경 정보(=outer)를 갖고있습니.
선언 시점 LexicalEnvironment의 snapshot (처음 한 시점을 유지합니다.)


◆ LexicalEnvironment
VariableEnvironment와 동일하지만, 변경사항을 실시간으로 반영합니.


 ThisBinding
this 식별자가 바라봐야할 객체


① VariableEnvironment, LexicalEnvironment의 개요

◆ VE vs LE
이 두가지는 담기는 항목은 완벽하게 동일하나 스냅샷 유지여부는 다릅니다.
VE : 스냅샷 유지
LE : 스냅샷을 유지하지 않음. 즉, 실시간으로 변경사항을 계속해서 반영합니다.
결국, 실행 컨텍스트를 생성할 때, VE에 정보를 먼저 담은 다음, 이를 그대로 복사해서 LE를 만들고 이후에는 주로 LE를 활용합니다.


◆ 구성 요소(VE, LE 서로 동일)
VE, LE모두 동일하며, ‘environmentRecord’(식별자)와 ‘outerEnvironmentReference’(외부환경정보)로 구성
  environmentRecord(=record)
현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장됩니다,.
함수에 지정된 매개변수 식별자, 함수자체, var로 선언된 변수 식별자 등
  outerEnvironmentReference(=outer)

외부환경정보

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

현재 컨텍스트와 관련된 코드의 식별자 정보들이 기록 record (저장, 수집)됩니다.
수집 대상 정보 : 함수에 지정된 매개변수 식별자, 함수 자체, var로 선언된 변수 식별자 등
컨텍스트 내부를 처음부터 끝까지 순서대로 훑어가며 수집(실행 X)


◆ 호이스팅

JS 엔진이 코드 실행 전에 모든 변수와 함수의 선언 정보를 미리 알고 있는 '가상 개념'으로, 코드가 실제로 실행되기 전에 식별자정보(변수나 함수)가 호이스팅(상단으로 올림)니다.
(가상개념 : 실제로는 그렇진 않더라도 사람이 이해하기 쉬운 말로 풀어 표현한 것)​


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

      1 : 매개변수 다시 쓰기(JS 엔진은 똑같이 이해한다)

      2 : 결과 예상하기

      3 : hoisting 적용해본 후 결과를 다시 예상해보기


<적용 전>

// 적용 전

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


<매개변수 적용>

// 매개변수 적용
//action point 1 : 매개변수 다시 쓰기(JS 엔진은 똑같이 이해한다)

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


<호이스팅 적용>

// 호이스팅 적용
//action point 3 : hoisting 적용해본 후 결과를 다시 예상해보기

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

	x = 1;
	console.log(x); // 1
	console.log(x); // 1
	x = 2;
	console.log(x); // 2
}
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();


<호이스팅 적용>

// 호이스팅 적용
//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함수라고 나올 것 같았지만, 실제로는
[Function: b]
bbb
bbb
라는 결과가 나왔습니다.


◆함수 선언문, 함수 표현식
▷함수 정의의 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(); // 에러!


개념 정리

더보기

- 실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체이다.
- 그 객체 안에는 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;
};

 

 

...

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


함수선언문은 아래에서 선언 한 변수가 코드 전체에 영향을 미칩니다.

함수 표현식은 그 아래로만 영향을 받습니다.


협업을 많이 하고, 복잡한 코드일 수록. 전역 공간에서 이루어지는 코드 협업일 수록 함수 표현식을 활용하는 습관이 필요합니다.


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

실행컨텍스트 관점에서의 스코프

◆ 주요 용어
▷ 스코프
      식별자에 대한 유효범위.  대부분 언어에서 존재

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

출처 : https://jess2.xyz/JavaScript/scope-chain-closure/
outerEnvironmentReference(이하 outer)
      스코프 체인이 가능토록 하는 것(외부 환경의 참조정보)

 

  스코프 체인
함수는 선언된 시점의 외부 환경(Lexical Environment)을 참조하며, 호출 시에는 이 환경이 고정됩니다.

스코프 체인에 따라 가장 가까운 범위에서부터 변수를 찾고, 가장 먼저 발견된 식별자에만 접근할 수 있습니다.

      ex) A함수 내부에 B함수 선언 → B함수 내부에 C함수 선언(Linked List)한 경우 
      결국 타고, 타고 올라가다 보면 전역 컨텍스트의 LexicalEnvironment를 참조하게 됩니다.

// 아래 코드를 여러분이 직접 call stack을 그려가며 scope 관점에서 변수에 접근해보세요!
var a = 1;
var outer = function() {
	var inner = function() {
		console.log(a); // undefined   
		var a = 3;
	};
    // 3. var a = 3;이 호이스팅 되면서 a의 선언부분만 위로 올라갑니다.
    /*
    var a;
    console.log(a); // undefined  
    a = 3;
    */
    
	inner(); // 2. inner함수 실행
	console.log(a); // 1  // 4. inner함수 종료시 함수 inner내의 a 값은 사라집다.
};
outer();  // 1. outer함수 실행
console.log(a); // 1   // 5. 전역영역의 a 사용

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

 

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


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

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

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

실행 컨텍스트는 코드 실행에 필요한 환경 정보를 모은 객체입니다. 이 객체의 this는 함수 호출 시 결정되며, 이를 바인딩이라 합니다.


◆ 전역공간에서의 this

전역 영역에서 this는 전역 객체를 가리키며, 실행 환경에 따라 브라우저에서는 window, Node.js에서는 global을 가리킵니다.

<브라우저 환경 this 확인>

관리자 창에서 한 줄 씩 실행하세요.

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

 

<node 환경 this 확인>

터미널에서 작성하세요.

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


메서드로서 호출할 때 그 메서드 내부에서의 this
▷ 함수 vs 메서드
   함수는 그 자체로 독립적인 기능을 수행.

      함수명(); 

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

      객체.메서드명();


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: ƒ } 2

  함수로서의 호출과 메서드로서의 호출 구분 기준 :    '.'    []
아래 예시도 같아요! 점(.)으로 호출하든, 대괄호([])로 호출하든 결과는 같습니다 😉​

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


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


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


위 코드의 실행 결과 (1), (2), (3)을 예측하기

더보기

(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를 바인딩하지 않는 함수)
일반 함수와 화살표 함수의 가장 큰 차이점은 this 바인딩의 여부와 방식입니다.

ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제(this가 유실되는 문제) 때문에 화살표함수를 도입했습니다.

var obj = {
	outer: function() {
		console.log(this); // (1) obj
		var innerFunc = () => {
			console.log(this); // (2) obj   this바인딩 과정이 없기에 이전의 this인 obj가 유지됩니다.
		};
		innerFunc();
	}
}

obj.outer();


 콜백 함수 호출 시 그 함수 내부에서의 this
콜백 함수 : “어떠한 함수, 메서드의 인자(매개변수)로 넘겨주는 함수”
콜백 함수도 함수기 때문에 this는 전역 객체를 참조하지만(호출 주체가 없잖아요), 콜백함수를 넘겨받은 함수에서 콜백 함수에 별도로 this를 지정한 경우는 예외적으로 그 대상을 참조합니다.

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

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

// 2. 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는 달라집니다. 


(2) 명시적 this 바인딩

자동으로 부여되는 상황별 this의 규칙을 깨고 this에 별도의 값을 저장하는 방법입니다.
크게, call / apply / bind에 대해 알아보겠습니다.

  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


예상되는 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    call로 a의 this를 4로 바인딩함


  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 메서드 활용
​물론 this binding을 위해 call, apply method를 사용하기도 하지만 더 유용한 측면도 있습니다.

call과 apply 메서드는 즉시 실행함수로, 호출과 동시에 this 바인딩을 수행합니다. 

이때 this 자리에 유사 배열 객체를 전달함으로써, 해당 메서드가 유사 배열 객체에 적용되어 원하는 기능을 수행할 수 있게 됩니다.

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

      유사배열객체(array-like-object) : 배열과 유사한 객체
   유사 배열의 조건
      1.반드시 length가 필요해야한다. 이 조건은 필수, 없으면 유사배열이라고 인식하지 않는다.
      2. index번호가 0번부터 시작해서 1씩증가해야한다. 안그래도 되긴하는데 예상치 못한 결과가 생긴다.
(출처 : https://kamang-it.tistory.com/entry/JavaScript15유사배열-객체Arraylike-Objects)

 

  slice() 함수
slice() 함수는 배열로 부터 특정 범위를 복사한 값들을 담고 있는 새로운 배열을 만드는데 사 •용합니다. 첫번째 인자로 시작 인덱스(index), 두번째 인자로 종료 인덱스를 받으며, 시작 인덱스 부터 종료 인덱스까지 값을 복사하여 반환합니다.
(출처 : https://www.daleseo.com/js-array-slice-splice/)

//객체에는 배열 메서드를 직접 적용할 수 없어요.
//유사배열객체에는 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 메서드(ES6)
 ES6에서는 Array.from이라는 방법을 제시.

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

// 객체 -> 배열
var arr = Array.from(obj); 
// Array.from 중요. Array.from()에 배열을 넣으면 알아서 배열이 나오는 편리한 기능이 있습니다.

// 찍어보면 배열이 출력됩니다.
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 사용할 수 있읍니다

//비효율
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 numbers1 = [10, 20, 3, 16, 45];
const max1 = Math.max(...numbers1);
const min1 = Math.min(...numbers1);
console.log(max1, min1);
var numbers = [10, 20, 3, 14, 65];
var max = (min = numbers[0]); // 10

numbers.forEach(function (number, idx){
	console.log(idx, "번째 값 =>", number);
})
/*
0 번째 값 => 10
1 번째 값 => 20
2 번째 값 => 3
3 번째 값 => 14
4 번째 값 => 65
*/


  bind 메서드

this를 바인딩하는 매서드.

call / apply 과 다르게 즉시 호출하지 않습니다.

'.bind' 하면 해당하는 함수를 this 바인딩해서 새로운 함수를 반환하는 메서드입니다.
▷ 목적
   함수에 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


▷ name 프로퍼티
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.   메서드의 내부함수에서 메서드의 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();



- 콜백함수
콜백함수도 함수이기 때문에, 함수가 인자로 전달될 때는 함수 자체로 전달 (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();



화살표 함수의 예외사항
▷ 화살표 함수는 실행 컨텍스트 생성 시, this를 바인딩하는 과정이 제외된다
▷ 이 함수 내부에는 this의 할당과정(바인딩 과정)이 아에 없으며, 접근코자 하면 스코프체인상 가장 가까운 this에 접근하게 됨
▷ this우회, call, apply, bind보다 편리한 방법

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

 


 

4. 숙제
01. 나이든 유저

// 가장 아래의 코드가 실행 되었을 때, 
//“Passed ~” 가 출력되도록 getAge 함수를 채워주세요
var user = {
  name: "john",
  age: 20,
}

var getAged = function (user, passedTime) { // 3. 유저랑 6
  // user를 다른 객체로 복사
  // 유저 나이에 passedTime 추가하기
  // 수정한 user 반환
  let newUser = {...user}
  newUser.age += passedTime;

  return newUser

}


var agedUser = getAged(user, 6); // 2. 유저랑 6

var agedUserMustBeDifferentFromUser = function (user1, user2) {
  if (!user2) {
      console.log("Failed! user2 doesn't exist!");
  } else if (user1 !== user2) { 
      console.log("Passed! If you become older, you will be different from you in the past!")
  } else {
      console.log("Failed! User same with past one");
  }
}

agedUserMustBeDifferentFromUser(user, agedUser); // 1.


02. 어떤 매치가 성사될까?

 

출력 :

Not Francis Ngannou VS John Jones
It is John Jones VS Ciryl Gane


 

※Tip

◆  AS-IS   기본의 것이라는 뜻
  TO-BE    이후의 것이라는 뜻

  Array.from()에 배열을 넣으면 알아서 배열이 나오는 편리한 기능이 있습니다.