JavaScript 문법 종합반 3주차

·2023년 5월 26일

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

1. 데이터 타입 심화

1.1 데이터 타입의 종류

  1. 기본형 (6가지)
  2. 참조형 -> Object 객체 (6가지)

값의 타입(기본형 vs 참조형)을 나누는 기준은 아래 2가지 !

  1. 복제의 방식
  • 기본형 : 값이 담긴 주소값을 바로 복제
  • 참조형 : 값이 담긴 주소값들로 이루어진 묶음을 가리키는 주소값을 복제
  1. 불변성 여부 (메모리 관점에서)
  • 기본형 : 불변성을 띔
  • 참조형 : 불변성을 띄지 않음

1.2 메모리와 데이터에 관한 배경지식

  1. 비트 bit : 컴퓨터가 이해할 수 있는 가장 작은 단위, 0과 1을 가진 메모리를 구성하기 위한 작은 조각
  2. 바이트 byte : 비트를 8개 합쳐 놓은 새로운 단위 (why ? 비트는 너무 작아서)
  3. 메모리 : 모든 데이터는 바이트 단위의 식별자인 메모리 주소값을 통해 서로 구분됨

1.3 변수 선언과 데이터 할당

식별자, 변수 (헷갈림 주의 !!)

var str; // str = 식별자 = 변수명 // 변수 선언 !!!
str = 'test!'; // 'test!' = 데이터 = 변수 // 데이터 할당 !!!
  • 값을 바로 변수에 대입하지 않는 이유
  1. 자유로운 데이터 변환
  2. 메모리의 효율적 관리

1.4 기본형 데이터와 참조형 데이터, 변수 복사의 비교

  1. 변수 vs 상수
    : 변수는 변수 영역 메모리 변경 가능 / 상수는 변수 영역 메모리 변경 불가
  2. 불변하다 vs 불변하지 않다 (= 메모리 영역 중요)
    : '불변하다'는 데이터 영역 메모리 변경 불가 / '불변하지 않다'는 데이터 영역 메모리 변경 가능

'변수이면서 불변하다'라는 말 존재

  1. 불변값과 불변성(with 기본형 데이터)
  2. 가변값과 가변성(with 참조형 데이터)

가비지컬렉터(GC, Garbage Collector) : 필요없는 메모리를 모아주는 기능

1.5 불변 객체 (얕은 복사 깊은 복사)

불변 객체를 위해 얕은 복사라는 방법이 있지만 중첩된 객체에 대해서는 완벽한 복사를 할 수 없기 때문에 깊은 복사를 통해 완벽한 복사를 구현해야 한다 !!

바로 재귀적 함수를 통해 깊은 복사를 할 수 있다.

// 깊은 복사 - 재귀적 함수
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 } } }

+ 주석 추가 +

1.6 undefined와 null

  1. undefined : 일반적으로 값이 있어야 할 거 같은데 (= 정의되어있어야 할 거 같은데) 없을 경우 자바스크립트 엔진에서 자동으로 부여
var a;
console.log(a); // (1) 변수에 값이 지정되지 않은 경우
			   // (= 데이터 영역에 메모리 주소가 없다는 의미 🥸)

var obj = { a: 1 };
console.log(obj.a); // 1
console.log(obj.b); // (2) .이나 []로 객체나 배열에 접근하려고 할 때, 해당 데이터(값)이 존재하지 않는 경우
// console.log(b); // 오류 발생

var func = function() { };
var c = func(); // (3) return 문이 없는데 호출하려고 하는 경우
console.log(c); // undefined
  1. null : 주로 개발자가 '없다'를 명시적으로 표현할 때 사용
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 // 일치연산자는 타입까지 체크하므로 null과 undefined는 구별됨
console.log(n === null); // true

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

2.1 실행 컨텍스트

  • 스택 (stack) : Last In First Out
  • 큐 (queue) : First In First Out

자바스크립트의 실행컨텍스트(스코프, 변수, 객체, 호이스팅) : 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로, 이것을 스택의 한 종류인 콜스택(call stack) 에 쌓음.

즉, 특정 실행컨텍스트가가 생성(=활성화, 실행 ?) 되는 시점이 콜 스택의 맨 위에 쌓이는 순간을 의미한다.

// ---- 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. VariableEnvironment : 현재 컨텍스트 내의 식별자(= 변수명, record) 정보와 외부 환경 정보(= outer)를 갖고 있음, 변경 사항을 반영하지 않아 처음 모습과 똑같음 = 스냅샷을 유지함
2. LexicalEnvironment : VariableEnvironment와 동일한 것, 다른 점은 시간이 갈수록 L.E는 변경사항을 실시간으로 반영함 (모습이 바뀜)
3. ThisBinding

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

LE와 VE에는 모두 현재 실행 컨텍스트와 관련된 식별자 정보(record)들이 수집(<=> 기록) 되는데, 이 수집 대상 정보에는 (1) 함수에 지정된 매개변수 식별자, (2) 함수 자체, (3) var로 선언된 변수 식별자 등이 있고, 컨텍스트 내부를 처음부터 끝까지 순서대로 훑어가며 수집함. (물론, 코드 실행은 아직 아님)

즉, 이 위에 과정을 호이스팅이라고 한다.

호이스팅(hoisting) : 식별자 정보만 위로 다 끌어올림
(그러나 실은 변수 정보 수집 과정을 이해하기 쉽게 설명한 ‘가상 개념’임)

  • 호이스팅 규칙
  1. 매개변수 및 변수는 선언부는 선언부를 호이스팅한다.
// 매개변수 적용 (호이스팅 적용 전)
function a () {
	var x = 1;
	console.log(x); // 1
	var x;
	console.log(x); //undefined
	var x = 2;
	console.log(x); // 2
}
a(1);

// 호이스팅 적용
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. 함수 선언은 전체를 호이스팅한다.
// 호이스팅 적용 전
function a () {
	console.log(b); // undefined
	var b = 'bbb';
	console.log(b); // bbb
	function b() { }
	console.log(b); // ~~undefined~~ function()
}
a();

// 호이스팅 적용 후
function a () {
	var b; // 변수 선언부 호이스팅
    function b() { } // 함수 선언부 전체 호이스팅
    
	console.log(b); // ~~b~~ function()
	b = 'bbb';
	console.log(b); // bbb
	console.log(b); // bbb
}
a();

이 역시 결과 예측이 어려움.

  1. 함수 선언문, 함수 표현식

    함수 정의의 3가지 방식 : (1) 함수 선언문 : 함수명이 곧 변수명 / 함수 표현식 : 정의한 함수를 별도 변수에 할당 - (2) 익명 함수 표현식, (3) 기명 함수 표현식
    예시 코드는 참고 링크

그로 인해 함수 선언문은 함수 전체를 호이스팅하고, 함수 표현식은 변수 부분만 호이스팅한다.

주의할 점 !
협업을 할수록 함수 선언문은 코드 중복 등 오류가 내포되어있을 가능성이 큼.
따라서 코드 협업 때는 함수 선언문보다 함수 표현식을 활용하는 것이 더 좋다.

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

  • 스코프 : 식별자(= 변수명)에 대한 유효한 범위, 대부분 언어에 존재
  • outerEnvironmentReference(= outer) : 스코프 체인이 가능토록 하는 것, 즉 외부 환경의 참조정보

스코프 체인 : outer는 ⭐️현재 호출된 함수가 선언될 당시⭐️의 LexicalEnvironment를 참조, 결국 타고, 타고 올라가다 전역 컨텍스트의 LexicalEnvironment를 참조하게 됩니다. 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에게만 접근이 가능하다.

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); // 이 값은 뭐가 나올까요? 마찬가지로 이유도!


// 호이스팅 후 스코프 관점에서 결과
var a = 1;

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

Q. 그럼 전역 컨텍스트의 outer는 뭘 참조하지 ?
A. 전역 컨텍스트는 가장 최상위 컨텍스트이므로 outer는 null이다. (this: 전역객체)
참고 링크

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


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

3.1 상황에 따라 달라지는 this

  1. this는 실행 컨텍스트가 생설될 때 결정된다(= bind 한다).
  • 전역 공간에서 this는 전역 객체를 가리킴. 런타임 환경에 따라 this는 window 객체(브라우저 환경) 또는 global 객체(node 환경)를 각각 가리킴.
  1. 메서드로서 호출할 때 그 메서드 내부에서의 this
  • 차이는 독립성 ! => 함수는 스스로 호출됨(ex. 함수명() ), 메소드는 누군가 나를 실행시켜줘야 함(ex. 객체.메소드명()) !
  • 함수는 this가 전역 객체 (why? 호출의 주체를 명시할 수 없어서), 메소드는 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
  • 호출 주체를 알 수 없으니, 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();	
TEST =>  { outer: [Function: outer] }
TEST =>  <ref *1> Object [global] {
  global: [Circular *1],
  queueMicrotask: [Function: queueMicrotask],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  structuredClone: [Getter/Setter],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  atob: [Getter/Setter],
  btoa: [Getter/Setter],
  performance: [Getter/Setter],
  fetch: [AsyncFunction: fetch],
  crypto: [Getter]
}
TEST =>  { innerMethod: [Function: innerFunc] }
  • 메서드의 내부 함수에서의 this 우회
    (1) 변수 활용 : 이미 존재하는 this를 별도의 변수에 할당하는 방법
    (2) 화살표 함수 활용 : 만약 화살표 함수가 아닌 함수라면 this가 전역 객체를 바라볼텐데, 화살표 함수는 화살표 함수는 this binding 과정을 생략하기에 (코드 진행 과정 중) 이전 this를 유지 (ex. 메소드의 this인 객체라던지)

일반 함수와 화살표 함수의 가장 큰 차이 ? => this binding 여부 !

  1. 콜백 함수 호출 시 그 함수 내부에서의 this
  • 콜백함수도 함수다 ! 그러므로 콜백함수 또한 this binding하면 일반적으로 전역 객체(window or global)를 바라보게 되어있음.
  • 단, addEventListner 메소드는 콜백함수에 별도로 this를 지정하기 때문에 this binding하면 addEventListner를 호출한 주체를 바라보게 되어있음.
  1. 생성자 함수 내부에서의 this

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

  • 생성자 함수 내부에서는 this binding 시 새로운 인스턴스를 바라봄. (새로운 인스턴스를 만들 시 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

즉, 위 코드에서는 새로운 Cat이라는 인스턴스가 (var choco에 담겨) 생성되었는데 this는 이 인스턴스를 지칭함 !

3.2 명시적 this 바인딩

  • call 메소드 : 호출 주체인 함수를 즉시 실행, 첫번째 매개 변수에 this로 binding할 객체를 넣어주면 됨
  • apply 메소드 : call 메소드와 완전 동일하나, 나머지 부분을 배열 형태로 넘겨줘야 함
var func = function (a, b, c) {
	console.log(this, a, b, c);
};

// call() 메소드
func.call({ x: 1 }, 4, 5, 6}; // { x: 1 } 4 5 6

// apply() 메소드
func.apply({ x: 1 }, [4, 5, 6]}; // { x: 1 } 4 5 6
  • call / apply 메소드 활용
  1. 유사배열객체(array-like-object)에 배열 메소드 적용

유사 배열의 조건 : 배열과 마찬가지로 length와 index가 꼭 필요함. 특히 index는 0번부터 시작해서 1씩 증가해야함.

유사 배열은 push()나 slice()와 같은 진짜 배열 메소드는 사용하지 못하지만, call과 apply 메소드를 이용해 배열 메소드를 차용할 순 있음.

  1. Array.from 메소드(ES6 등장) : 객체를 배열로 쉽게 바꿔주는 메소드
// 유사배열
var obj = {
	0: 'a',
	1: 'b',
	2: 'c',
	length: 3
};

// 객체 -> 배열
var arr = Array.from(obj); // Array.from에 유사 배열을 넣으면 객체가 배열로 출력

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

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

  3. bind 메서드 : call과 다르게 즉시 호출하지 않고 미리 적용함, 또한 부분 적용 함수 구현할 때 용이

  • name 프로퍼티
  • 상위 컨텍스트의 this를 내부함수나 콜백 함수에 전달하기 : 요즘은 self 등의 변수를 활용한 우회보다는 call, apply, bind를 사용해 더 깔끔하게 처리함
    (self 등의 변수를 활용하면 하드코딩되기 때문에)
  1. 화살표 함수의 예외사항 (화살표 함수는 this를 binding하는 과정이 없음 주의)
var obj = {
  outer: function () {
    console.log(this); // { outer: [Function: outer] }
    var innerFunc = () => {
      console.log(this); // { outer: [Function: outer] }
    };
    innerFunc();
  },
};
obj.outer();

위 코드처럼 화살표함수 내부에서 this는 절대 전역객체를 가리키지 않음


숙제

  1. 문제

  2. 해설

0개의 댓글