JS(JavaScript) 기본 구조 개념 [호이스팅, 스코프, TDZ, 객체 복사 ... ]
JavaScript의 자료형과 JavaScript만의 특성은 무엇일까 ?
느슨한 타입(loosely typed)의 동적(dynamic) 언어
- 자료형
- Number(숫자열) :
+infinity
,-infinity
,NaN(Not a Number)
의 상징적인 값을 가질 수 있고 전반적인 숫자 타입 - String(문자열) : 문자로 구성된 타입, 배열은 아니지만 각 자리에 인덱스 번호가 존재한다. 따라서 index 번호로 해당 자리 글자를 호출 할 수 있고,
length
를 통해 문자열 길이를 알 수 있다. - Boolean(불리언) : 논리 요소로
true
,false
두 가지 값을 갖는다. - Null :
undefined
랑 헷갈릴 수 있다. 값은null
하나만 가지며, '객체가 없음'을 나타냅니다. - undefined: 변수는 선언했지만, 값을 할당하지 않으면
undefined
값을 갖는다.forEach
도 변수에 할당 할 수는 있지만, 반환하는 값이 없기 때문에undefined
를 갖는다. - symbol : 유일성을 보장 받을 수 있는 값으로, 객체의 키값으로 사용할 수 있다. (
hasOwnProperty
로 사용도 가능하다. ) - BigInt : 임의 정밀도로 정수를 나타낼 수 있는 원시 값으로
Number
의 안전한계를 넘어서는 큰 정수도 안전하게 저장하고 연산. 정수 끝에n
을 추가하거나 생성자를 호출해 생성. - Object : 배열과 객체를 나타내는 타입이다.
- JS가 느슨한 타입을 가지고 있는 동적 언어라 하는 이유는 무엇인가? - 우선 JS에서 변수를 할 당할 때 자료형을 명시할 필요가 없다. 또한 자료형이 변할 수 있다.
위 예시를 보면 특정 자료형을 선언할 필요 없이 변수 foo에 숫자열을 할당 할 수 있고, 재 할당 값에 따라 동적으로let foo = 42 // foo가 숫자 foo = 'bar' // foo가 이제 문자열 foo = true // foo가 이제 불리언
typeof
가 변경된다.
JavaScript 형변환
암시적변환 - +연산자는 문자열에서도 사용할 수 있기 때문에 문자형을 우선 시 한다. 다른 연산자의 경우 숫자형을 우선시 한다.
// + 연산자 2 +2 //4 num 2 + "2" // "22" str "2" + "2" // "22" str "2" + true // "2true" str 2 + true // 3 num > true는 숫자열로 1, flase는 0 // *연산자 "2" * 2 // 4 num "2" * "2" // 4 num 2 * 2 // 4 num "2" * true // 2 num 2 * true // 2 num
명시적 변환 - 매서드를 사용하여 형변환이 가능하다.
- Number() : 숫자열 변환
- String() : 문자열 변환
- Boolean() : 불리언 타입으로 변환 // 0 은 false
==, ===
조건문에서 값을 비교할 때 사용한다.
- == :
typeof
타입은 상관없이 값만 동일하면 true를 반환한다. - === : 값과 타입이 모두 동일할 경우 true를 반환한다.
느슨한 타입(loosely typed)의 동적(dynamic) 언어의 문제점은 무엇이고 보완할 수 있는 방법에는 무엇이 있을지 생각해보세요.
런타임에 타입이 결정되는 언어이기 때문에 실행 후 에러가 발생하는 경우가 많다. 타입을 써줄 필요가 없기 때문에 빠르게 코드를 작성할 수 있지만, 실행 도중 예상하지 못한 타입이 들어와 에러를 발생할 수 있다.
내가 생각한 보완 방법으로는 중요한 연결고리에는 명시적으로 형변환을 신경쓰고 조건문에서는 '==='로 값을 비교해 주는 것이 바람직하다.
undefined와 null의 미세한 차이
undefined
는 변수를 선언하고 값을 할당하지 않은 상태,null
은 변수를 선언하고 빈값을 할당한 상태.
즉 undefined는 변수를 선언과 동시에 할당되는 값이라 하면 null을 빈값을 ( null 로 할당) 할당하는 형태이다. 초기화 할 때 자주 사용한다.
JavaScript 객체와 불변성이란 ?
기본형 데이터와 참조형 데이터
- 기본형 데이터(원시적) : 변수에 바로 할당하는 데이터이다. Number, String, Boolean, null, undefined 등의 자료형이 있으며, 변수에 고유한 값이 된다.
- 참조형 데이터 : 값이 저장된 주소를 할당하는 데이터라 보면 편하다. Object 즉 배열, 객체 등이 이에 해당한다.
let a = [1,2,3] let b = a;
배열은 참조형 데이터이기 때문에 b을 바꾸면 저장된 주소에 데이터가 바뀌고 자연스럽게 변수 a의 배열도 변한다.
불변 객체를 만드는 방법
const, Object.freeze 사용const
는 변수를 상수 즉 바꿀 수 없는 값으로 선언 할 때 사용한다. 다만 그 값이 객체라면 속성은 바꿀 수 있다.
Object.freeze
는 JS에서 제공하는 메서드인데, 객체를 동결하기 위해 사용한다. 즉 이 메서드를 사용해 객체를 동결 시켰다면, 속성값을 바꾸기 위한 시도는 무시한다. 하지만 변수에 새로운 객체를 할당한다면 객체가 바뀔 수 있다.
그래서 불변 객체를 만들기 위해서는 값을 바꿀 수 없도록 const
로 변수 선언을 해준 다음Object.freeze
를 통해 속성을 변경할 수 없도록 동결해야 한다.
// const 만 사용 속성 변경 가능
const test = {};
test.name = "mingyo";
console.log(test); // {"mingyo"}
// Object.freeze만 사용 재 할당 가능
let test = { name : 'kim' }
Object.freeze(test);
test = { age : 15 };
console.log(test); // {age: 15}
// 둘다 시용 불변 객체
const test = { 'name' : 'jung' };
Object.freeze(test);
얕은 복사와 깊은 복사
보통 객체나 배열을 복사할 때 얘기를 많이 하는것 같다.
얕은 복사는 참조형과 유사하게 다른 변수에 복사를 했지만 참조값을 복사해 하나의 변수에 값이 변경되면 둘 다 변경된다.
깊은 복사는 기본형처럼 같은 값을 가진 객체를 복사하지만 그 자체가 하나의 객체로 되어 값을 변경해도 다른 객체에 영향을 끼치지 않는 복사 방법이다.
//얕은 복사
let a = {...};
let b = a; // a===b
// 깊은 복사
let a = {...};
let b = Object.assign({},a);
이렇게 전개 구문 등 여러 방법을 통해 깊은 복사를 할 수 있는데, 2차원 객체(객체 안에 객체)는 깊은 복사가 안된다는 걸 알아야 한다.
const obj = { a: 1 };
const newObj = Object.assign({}, obj);
newObj.a = 2;
console.log(obj); // { a: 1 }
console.log(obj === newObj); // false
이 2차원 객체도 깊은 복사를 하기 위해서는 JS 메서드 JSON.stringify() 또는 재귀함수, lodash
모듈에 있는 cloneDeep()메서드를 사용하면 가능하다.
호이스팅과 TDZ는 무엇일까 ?
스코프, 호이스팅, TDZ
- 스코프
- 전역 스코프 : 코드의 어느곳에서든 참조할 수 있는 범위. 이곳에 선언된 변수는 어디에서든 사용 할 수 있다. 대표적으로 var
- 지역 스코프 : 코드블록, 함수내에서의 범위이며 자기자신과 하위 범위에서만 참조. 선언된 변수는 지역 변수가 된다. 대표적으로 let, const
- 호이스팅
- 변수명을 최상단으로 옮기는 것,
위 코드와 같이 변수가 사용된 코드 밑에 작성된 경우 undefined가 반환된다(var 경우)//var num ; console.log(`내 나이는 ? : ${num}`) >> 내 나이는 ? : undefined var num = 27;
이유는 호이스팅으로var num;
이라는 변수를 선언한 내용(변수 할당 전)을 먼저 읽고, 아래 코드를 동작하기 때문이다. 그래서 undefined가 반환되고 이후에 num이라는 변수에 27이 할당되는 것이다.
let, const 도 호이스팅은 일어나지만, TDZ 개념 때문에 에러가 발생한다. - let, const를 사용해야하는 이유 중 한 가지
TDZ (Temporal Dead Zone)
let, const가 대표적으로 이에 해당하는데(Class, super()등 더 있다.) 변수를 선언하기 전에 변수에 접근하는 것을 금지한다는 내용이다.console.log(b); let b;
간단한 예제지만 이 코드는 ReferenceError를 발생한다.
쉽게 이해한대로 설명해보자면 변수를 선언하기 전에는 (해당 라인보다 위에있는) 변수에 접근 할 수 없다는 의미이다.반대로 var, function, import등 TDZ에 적용받지 않는 것도 있다.
함수 선언문과 함수 표현식에서 호이스팅 방식의 차이
function fn(){...};//호이스팅 O / 함수 선언문
var name = function (){...}; // 호이스팅 O / 함수 표현식
let func = function(){...}; // 호이스팅 X / 함수 표현식
함수와 var는 호이스팅이 된다. TDZ 영향을 안받는다. 따라서 var로 선언한 함수도 호이스팅이 된다. 하지만 let, const로 함수 표현식을 작성 할 경우 호이스팅이 되지 않는다.
실행 컨텍스트와 콜 스택
실행 컨텍스트
자바스크립트에서 실행 컨텍스트란, 코드를 실행하기 위해 필요한 정보들을 가진 범위를 객체 형태로 나타낸 것이다. 실행 컨텍스트를 구성하는 LexicalEnvironment는 현재의 실행 컨텍스트가 실행되기 위한 여러 정보를 담고 있다. LexicalEnvironment는 식별자들에 대한 정보를 담은 EnvironmentRecord와, 상위 LexicalEnvironment를 참조해 스코프 체인을 가능하게 하는 OuterEnvironmentReference 정보로 구성되어 있다. EnvironmentRecord는 식별자 바인딩을 관리하고, binding object라 불리는 특정 객체의 속성으로 선언된 식별자들을 관리한다. OuterEnvironmentReference는 현재의 실행 컨텍스트를 구성한 함수가 선언되는 시점에서의 상위 LexicalEnvironment를 참조하기 때문에 식별자의 유효 범위를 상위로 거슬러 올라가 찾게 되는 스코프 체인이 가능하다.콜 스택
call은 호출을 뜻한다.
stack은 출입구가 하나뿐인 깊은 우물 같은 데이터 구조다.
따라서 callstack은 자바스크립트가 함수 호출을 기록하기 위해 사용하는 우물 형태의 데이터 구조이다.항상 맨 위에 놓인 함수를 우선으로 실행된다. 이런 식으로 자바스크립트 엔진은 가장 위에 쌓여있는 context와 관련 있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장한다.
스코프 체인, 변수 은닉화
스코프 체인
지역 변수를 어떤 객체의 프로퍼티로 생각한다면, 자바스크립트의 모든 코드는 스코프 체인을 갖고 있다. 스코프 체인은 해당 코드의 유효 범위(in scope) 안에 있는 변수를 정의하는 객체의 체인, 리스트다.자바스크립트가 변수 값을 얻으려고 할 때 스코프 체인에서 변수를 찾는다. 첫 번째 객체에서 해당 변수를 찾고, 없으면 그 다음 객체에서 해당 변수를 찾고, 여기도 없으면 그 다음 객체에서 찾는 식이다. 리스트의 끝까지 탐색했는데도 그 변수가 없다면 reference error가 발생하는 것이다.
최상위 자바스크립트 코드(어떠한 함수에도 속하지 않는 코드)의 스코프 체인에는 하나의 객체만 있고, 그것이 전역 객체이다. 중첩되지 않은 함수의 스코프 체인은 2개의 객체로 이루어진다. 하나는 함수의 매개변수와 지역 변수를 정의하는 객체고, 다른 하나는 전역 객체다.
변수 은닉화
객체의 속성과 행위를 하나로 묶고
실제 구현 내용 일부를 외부에 감추어 은닉한다.
즉 변수는 선언했지만 외부에서 찾을 수 없기 때문에, 순회를 할 수 없게 만들 수 있다.
실습과제
콘솔에 찍힐 b값 예상하기, 어떤 console.log에 찍혔는지 왜 그런지 설명
주석을 풀었을 때 오류가 나는 이유 그리고 오류 수정해보기
let b = 1; function hi () { const a = 1; let b = 100; b++; console.log(a,b); //2번 } //console.log(a); console.log(b); // 1번 hi(); console.log(b); // 3번
1,2,3번 순서대로 b값이 찍히고 값은 각각 1,101,1이다.
이유는 우선 1번에 도달해서 변수 b를 찾게 되면 선언된 b가 있기 때문에 b값인 1이 찍힌다.
2번째는 함수 내에 b를 선언했고 그 값을 ++ 했기 때문에 101인데, 밖에(outer)있는 변수보다 함수 내에 있는 변수를 먼저 찾기 때문에 해당 값인 101이 출력된다.
3번째는 함수 내에 b가 101이 있지만, let은 함수 밖에 참조를 할 수 없기 때문에 선택 할 수없다. 즉 let b=1;을 사용한다.주석을 풀었을 때 실행해보면 a변수가 선언되지 않았다 error가 나온다. 그 이유는 const는 블록 스코프 즉 함수 내에서만 참조가 가능하기 때문에 함수 밖에 있는 console.log(a)에 값을 참조할 수 없는 것이다.
a를 사용하기 위해서는 함수 밖에서
const a = 1
을 선언하면 된다.