[TIL]2023.05.24 자바스크립트 스터디, 데이터타입/호이스팅/실행컨텍스트/this🔥

Nick·2023년 5월 25일
0

TIL: 오늘을 돌아보자

목록 보기
25/95
post-thumbnail
post-custom-banner

TIL, 요즘 바닐라 자바스크립트 공부에 매진한다. 솔직히 오늘 공부는 에러를 맞닥뜨리기 보단, 생소한 개념과 용어들이 너무 많아, 내 두되가 못받아 드려 에러 투성이다. 오늘은 문제점 /시도/ 해결/ 알게된점 보단, 오늘 공부한 자바스크립트 헷갈리는 개념을 정리해 보고자 한다.

데이터 타입

기본형 vs 참조형

기본형과 참조형을 나누는 기준은 뭘까?

  • 값의 저장 방식불변성 여부이다. ( 어떻게 저장되느냐, 어떻게 복제 되느냐)

복제 방식
기본형 : 값이 담긴 주소값을 바로 복제
참조형 : 값이 담긴 주소값들로 이루어진 묶음을 가리키는 주소값을 복제

불변성 여부
기본형: 불변성 ( 메모리 관점에서 볼때 불변하는 것을 뜻함)
참조형: 불변성을 띄지 않음

비트, 바이트, 메모리

비트: 컴퓨터가 이해할 수 있는 가장 작은 단위 ( 0, 1로 이루어진 조각)

바이트: 0과 1만 표현하는 비트를 찾는게 부담스러워서 2^8 (비트 8개) 단위로 만들어버림

메모리: 바이트 단위로 구성되어있고, 모든 데이터는 바이트 단위의 식별자인 메모리 주소값을 가지고 있다.

java, c와 js는 메모리 관리 방식이 달라

8을 저장한다고 가정하자.

JS : let a = 8(8byte)
vs
JAVA
1. byte a = 8(1byte)
2. short a = 8(2byte)
3. int a = 8(4byte)
4. long a = 8(16byte)

java 나 c는 오래된 언어인데, 그 시절엔 지금처럼 빵빵한 메모리가 없었기에 데이터 타입 크기에 따라 다양하게 지정해 줬다고 한다! handling 할 요소들이 많아서 힘들 듯...

식별자, 변수

let testValue = 3
식별자 = 변수 명 / 변수 = 데이터

기본형 데이터와 참조형 데이터

메모리를 기준으로 다시한번 생각해보는 두 가지 주요 개념

  1. 변수 vs 상수
    1) 변수 : 변수 영역 메모리를 변경할 수 있음
    2) 상수 : 변수 영역 메모리를 변경할 수 없음

  2. 불변하다 vs 불변하지 않다
    1) 불변하다 : 데이터 영역 메모리를 변경할 수 없음
    2) 불변하지 않다 : 데이터 영역 메모리를 변경할 수 있음

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

더 이상 사용되지 않는 객체를 자동으로 메모리에서 제거하는 역할을 한다. 자바스크립트는 가비지 컬렉션 때문에 개발자가 명시적으로 메모리 관리 하지 않게 지원해준다. 자바스크립트 엔진에서 내부적으로 수행되며, 개발자는 가비지 컬렉션에 대한 직접적인 제어를 할 수 없다!

가변성의 문제점!

분명히 user 이름이랑 user2 (바뀐 이름) 이 같으면 안되는데! 코드를 보고 문제점을 찾아보자

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

// 이름을 변경하는 함수, 'changeName'을 정의
// 입력값 : 변경대상 user 객체, 변경하고자 하는 이름
// 출력값 : 새로운 user 객체
// 특징 : 객체의 프로퍼티(속성)에 접근해서 이름을 변경 했음 -> 가변

var changeName = function(user, newUser) {
    var newUser = user;
    newUser.name = newUser;
    return newUser;
};

// 변경한 user정보를 user2 변수에 할당하겠습니다.
// 가변이기 때문에 user도 영향을 받게 될거에요.
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",
};

// 이름을 변경하는 함수, 'changeName'을 정의
// 입력값 : 변경대상 user 객체, 변경하고자 하는 이름
// 출력값 : 새로운 user 객체
// 특징 : 객체의 프로퍼티(속성)에 접근해서 이름을 변경 했음 -> 가변

var changeName = function(user, newUser) {
    return {
        name: newName,
        gender: user.gender,
    };
};

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

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

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

하지만 이 방법은 최선이 아니야... 속성값이 어마어마하게 많으면, return 값이 어마어마하게 많아지고, 하드코딩으로 매꿔야 하기 때문에 유지보수 면에서 너무 엉망이다 ㅠㅠ

그래서 필요한게 얕은복사!

var copyObject = function (target) {
    var result = {};

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

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

이런식으로 만들어 주면 되지만... 객체안에 또 객체 또 객체 또 객체... 이렇게 많으면 깊은 복사를 또 많이해줘야 해?... 이런 문제점을 위해 고안해 낸것이...!

재귀적수행(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);
console.log(obj2);

불변성을 유지할 수 있게 되었다!

undefined vs 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);
console.log(n === null);

실행컨텍스트 🔥(어려워)

실행컨텍스트가 뭐야?
실행할 코드에 제공할 환경 정보들을 모아놓은 "객체"다!
1.선언된 변수를 위로 끌어올리구 = 호이스팅(hoisting)
2.외부 환경 정보를 구성하구
3.this 값을 설정
이런 현상들 때문에 JS에서는 다른 언어랑은 다른 특징들이 나타난다!

실행 컨텍스트란?

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

스택 vs 큐 구성

간단명료 하게! 스택은 후입 선출 / 큐는 선입 선출, 우린 스택 개념을 사용하기 때문에, 후입 선출 형태를 갖고 있다고 생각해 놓자!

콜스택

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

VariableEnvironment, LexicalEnvironment

VariableEnvironment

  1. 현재 컨텍스트 내의 식별자 정보(=record)를 갖고있음.
    1. var a = 3
    2. 위의 경우, var a를 의미
  2. 외부 환경 정보(=outer)를 갖고있어요.

    요약하자면 V/E는 record 와 outer를 갖고 있다. (식별자 와 외부환경 정보), 그리고 L/E랑 거의 같다

LE와 VE 뭐가 다르지?

담기는 항목은 완벽하게 동일하다. 처음에 생성될땐 똑같지만, 시간이 지나면서 변경사항이 생겼을때 차이가 생김 ! 스냅샷 유지여부는 다음과 같이 다르다!

선언 시점의 snapshot 을 유지하냐 vs 유지하지 않고 실시간 반영 되냐!

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

결론

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

Record 와 호이스팅

개요

  • Record : 식별자 정보들이 저장(수집)되고. 기록된다

  • 수집 대상 정보 : 함수에 지정된 매개변수 /식별자/ 함수 자체/ var로 선언된 변수/ 등

  • 컨텍스트 내부를 처음부터 끝까지 순서대로 훑어가며 수집
    - 💡 순서대로 수집한다고 했지, 코드가 실행된다고 하지는 않음!!!

🔥호이스팅

hoist : 끌어올리다.
🔥식별자정보를 맨 위로 끌어 올리는 것을 말한다 !-> record를 수집하는 과정

호이스팅 3가지 법칙

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

호이스팅 법칙 2 : 함수 선언은 전체를 호이스팅합니다.

함수 선언문, 함수 표현식

// 함수 선언문. 함수명 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)

스코프, 스코프 체인

outerEnvironmentReference(=outer)

outer의 역할을 한 마디로 정의하자면
스코프 체인이 가능토록 하는 것(외부 환경의 참조정보)라고 할 수 있다.
"외부 환경의 참조정보 라는 말에 집중"

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

스코프체인

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

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

먼저 학습하는데 길을 잃지 말자

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

this가 뭐야?

전역환경에서의 this

런타임, 코드가 돌아가는 환경
전역 환경에서 this -> 노드(global 객체), 브라우저(window 객체) 이다.

메서드와 함수에서의 this

함수 : this -> 전역 객체 (why? 호출의 주체를 명시할 수 없기 때문에)
메서드 : this -> 호출의 주체

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

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

obj1 에 대한 this를 self에 저장 해놓고 콘솔에 self 를 찍음 -> 흐름을 유지하는 this를 찍는것과 같은 효과! 이런 방식으로 우회해줌!

AS-IS : 기존 것
TO-BE : 이후 것

⭐️⭐️⭐️화살표 함수(=this를 바인딩하지 않는 함수) 사용하는 방법

ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제 때문에 화살표함수를 도입했다!!!

면접시⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️
일반 함수와 화살표 함수의 가장 큰 차이점은 무엇인가요? 라고 물으면
-> this binding 여부가 가장 적절한 답안이 될것임!!!

콜백 함수 호출 시 그 함수 내부에서의 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);
});

생성자 함수 호출 시 그 함수 내부에서의 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 binding

call 메서드

  • 호출 주체인 함수를 즉시 실행하는 명령어
  • call명령어를 사용하여, 첫 번째 매개변수에 this로 binding할 객체를 넣어주면 명시적으로 binding할 수 있음
// 명시적 this binding
//call
var func = function (a, b, c) {
    console.log(this, a, b, c);
};

// no binding
// func(1, 2, 3);   // 글로벌 객체 ~ 1 2 3 찍힐것임

//명시적 binding
func.call({x: 1},4, 5, 6);  // {x : 1} 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 메서드 활용)

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

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

사실, call/apply를 통해 this binding을 하는 것이 아니라 객체 → 배열로의 형 변환 만을 위해서도 쓸 수 있지만 원래 의도와는 거리가 먼 방법이라 할 수 있다 ES6는 이를 보안해서!!!

⭐️⭐️⭐️ES6에서는 Array.from이라는 방법을 제시해 객체를 배열로 전환해줌

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

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

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

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

function Student(name, gender, school) {
    this.name = name;
    this.gender = gender;
    this.school = school;
};


function Employee(name, gender, school) {
    this.name = name;
    this.gender = gender;
    this.company = company;
};

var kd = new Student("길동","male","서울대")
var ks = new Student("길순","female","삼성")

중복되는 인자 name, gender

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

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

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

느낀점...

내용이 머릿속에 아웃라인만 그려진 느낌, 입밖으로 뱉기 힘든데, 일단 복습해 보자,
다음 TIL에도 이어질 예정

profile
배우고 도전하는것을 멈추지 않는 개발자 입니다.
post-custom-banner

0개의 댓글