본문 바로가기

항해99_10기/105일의 TIL & WIL

[20221118] JavaScript 특성과 자료구조

JavaScript의 특성


자바스크립트는 느슨한 타입(loose typed)의 동적 언어이다. 

JS에서 변수는 어떤 특정 타입과 연결되지 않으며, 모든 타입의 값으로 할당 (및 재할당) 가능하다.

예를 들면, 상수 변수인 const도 값 자체는 재할당이 불가능하지만, 형변환이 가능하다. 

const a = 1;
console.log(a, typeof a); // 1 number
b = "string" + a;
console.log(b, typeof b); // string1 string

 

JavaScript의 이러한 특성은 C나 JAVA와 같은 언와는 달리, 변수를 선언할 때부터 타입을 할당하지 않아도 되어 편하다는 장점이 존재한다. 

하지만, 변수의 타입이 언어 엔진에 의해 자동 지정되기 때문에, 코드 작성자의 의도와는 다른 타입이 할당될 수도 있고, 실수로 같은 변수명에 다른 자료가 담길 수도 있는 치명적인 결함이 있다. 숫자 타입을 예상한 변수가 실제로는 문자타입이어서 코득 작동하지 않는다거나 하는 등의 문제는 실제로 빈번하게 일어난다.

 

이런 특성을 보완하기 위해 자바스크립트를 사용할 때는 

  • 변수를 꼭 필요한 경우에 한해 제한적으로 사용하여 오류가 발생할 확률을 줄이기 (변수의 숫자가 늘어날 수록 오류 확률도 커지기 때문에...)
  • 변수의 유효 스코프를 최대한 좁게 만들어 오류 발생 확률을 줄이기
  • 전역변수의 사용 최소화 => 전역에는 변수대신 상수를 사용
  • 변수의 이름을 지을 때 목적이나 의미를 파악할 수 있도록 네이밍
  • 강제로 타입 캐스팅 또는 타입을 검사하는 별도의 모듈 사용

하는 등의 추가 노력이 필요하다.

 

요즘 현업에서는 위험요소를 줄이기 위해 JS를 대신해 오류확률을 줄여줄 더욱 엄격한 TypeScript를 많이 사용하는 추세다.

타입 스크립트란?
언어는 크게 정적 타입과 동적 타입 언어로 구분할 수 있습니다. 타입 스크립트는 자바스크립트에 타입을 부여한 정적 타입 언어입니다. 만약 타입 스크립트를 브라우저에서 실행하려면 파일을 변환하는 트랜스 파일 과정을 거쳐서 사용합니다. 공식적으로는 트랜스 파일이 아닌 컴파일된다고 표현합니다. 컴파일의 경우 한 언어로 작성된 소스 코드를 다른 언어로 변환하는 것을 뜻하는 반면, 트랜스 파일의 경우 한 언어로 작성된 소스 코드를 비슷한 수준의 다른 언어로 변환한다는 차이가 있습니다. 예를 들어 Java를 컴파일하면 bytecode 코드가 출력되지만, C++를 트랜스 파일 하면 C가 출력되며 Typescript를 트랜스 파일 하면 Javascript가 출력됩니다. 하지만 공식적으로 컴파일된다고 표현하기 때문에 컴파일이란 용어를 사용하겠습니다.
이런 정적 타입 언어는 런타임 이전에 타입이 올바른지에 대한 검사를 시행하며, 동적 타입 언어는 런타임에 프로그램의 타입이 올바른지에 대한 검사를 실행합니다. 만약 래퍼런스 오류를 유발하는 코드가 존재한다면 정적 언어는 컴파일하는 과정에서 오류를 출력하는 반면 동적 언어는 해당 구문이 실행되는 시점에서 오류를 출력합니다.
- https://overcome-the-limits.tistory.com/357

 

 

JavaScript의 자료구조


 

JS에서 타입은 원시값(premitives) 과 객체로 나눠지며, 객체를 제외한 모든 타입은 불변 값을 정의한다.

 

원시값 

객체를 제외한 모든 타입은 불변 값(변경할 수 없는 값)을 정의하며, 이런한 타입을 '원시 값'이라고 함
JavaScript에서 원시 값(primitive, 또는 원시 자료형)이란  객체가 아니면서 메서드도 가지지 않는 데이터를 의미한다. 그렇지만, null undefined제외하고, 모든 원시 값은 원시 값을 래핑한 객체를 갖는다.

String, Number, BigInt, Boolean, Symbol은 모두 각각의 원시값을 위한 객체이다. 
래퍼 객체의 valueOf() 메서드는 원시 값을 반환한다.
  • Boolean : 논리 요소를 나타내며, truefalse 두 가지의 값을 가짐
  • Null : 컴퓨터 과학에서 null 값은 일반적으로 존재하지 않거나 유효하지 않은 object 또는 주소를 의도적으로 가리키는 참조를 나타냄.  null은 동작이 원시적으로 보이기 때문에 원시값 중 하나로 표시됨
"특정 경우에, null 은 처음 봤던것 만큼 "원시적"이지 않다. 모든 객체는 null 값으로 부터 파생되며 따라서 typeof 연산자는 아래의 코드에서 object를 반환한다."
- MDN 공식문서
typeof null === 'object' // true

typeof(null) // 'object' => null은 존재하지 않거나 유효하지 않은 object 또는 주소를 의도적으로 가리키는 참조를 나타내기 때문에...!
  • Undefined : 선언한 후 값을 할당하지 않은 변수 혹은 값이 주어지지 않은 인수에 자동으로 할당
    • 호이스팅 된 var 변수는 변수의 선언만을 호이스팅하기 때문에, 초기화 되기 전까지는 undefined를 반환
// +Infinity 범위 내 가장 큰 숫자와 작은 수 확인
Number.MAX_SAFE_INTEGER // 9007199254740991 = 2^53 => Number의 안전 한계
Number.MIN_SAFE_INTEGER // -9007199254740991

// -0과 +0의 차이
+0 === -0 //true
5/+0 // Infinity
5/-0 // -Infinity
  • BigInt : BigInt 타입은 임의 정밀도로 정수를 나타낼 수 있는 JavaScript 숫자 원시 값
    • Number의 안전 한계를 넘어서는 큰 수를 저장하고 연산할 수 있으며, 정수 끝에 n을 추가하거나 생성자를 호출해 생성 할 수 있음
    • 수학 연산자를 사용해 Number와 유사하게 계산이 가능하지만, 엄격하게 같지는 않음
    • 불리언 변환(논리 연산자 사용 등)이 발생하는 곳에서는 Number처럼 사용이 가능
  • String : String은 16비트 부호 없는 정수 값 "요소"로 구성된 집합이며, 문자열을 생성한 후 바꾸는 것은 불가능
    • substring()이나 concat() 등을 사용해 새로운 문자열은 생성 가능
"문자열의 타입화"를 조심하라!

문자열을 사용해 복잡한 데이터를 표현하는 것이 매력적으로 보일지도 모르고, 단기적으로는 다음과 같은 장점이 있습니다.

1. 연결 연산자를 통해 복잡한 문자열을 쉽게 만들 수 있습니다.
2. 문자열은 디버깅이 쉽습니다. (출력 내용이 항상 문자열의 값과 동일)
3. 문자열은 많은 API(입력 칸 (en-US), 로컬 스토리지 값, responseText와 함께 사용하는 XMLHttpRequest 등등)의 공통 분모입니다.

규칙만 잘 정한다면 어떤 자료구조라도 문자열로 표현할 수 있습니다. 그러나 그게 좋은 방법이 되는 것은 아닙니다. 예컨대, 구분자를 사용하면 (물론 JavaScript 배열이 더 적합하겠지만) 문자열로 리스트를 흉내낼 수도 있을 것입니다. 그러나 구분자를 리스트의 요소로 사용하는 순간 리스트가 망가지고 맙니다. 이제 구분자를 구분하기 위해 이스케이프 문자를 선택하고, 등등... 이 모든 것이 각자의 규칙을 필요로 하고 불필요한 유지보수 부담을 낳습니다.
문자열은 텍스트 데이터에만 사용하세요. 복잡한 데이터를 표현해야 할 땐 문자열을 파싱해서 적합한 추상화로 덮으세요.
- MDN 공식문서
  • Symbol : 고유하고 변경 불가능한 원시값으로, 객체의 속성 키로 사용이 가능
    • symbol 타입은 잘 사용되지 않지만, 다시 깊에 파 봐야 할 내용인것 같다. (MDN 링크)

 

 

객체

컴퓨터 과학에서의 객체란 식별자(identifier)로 참조할 수 있는 메모리 상의 값을 의미
다른 말로, key와 value 사이의 mapping

  • 속성 : 객체는 데이터 속성접근자 속성 두 종류로 이루어진 속성의 컬렉션 이다.
    • 제한적으로 속성을 초기화할 수 있고, 이후 속성을 편집 가능.
    • 속성 값으로 다른 객체를 포함할 수 있으며, 모든 타입을 사용할 수 있음.
    • 속성은 'key' 값으로 식별이 가능하고, 'key' 값은 문자열이나 심볼을 사용 가능
  • 데이터 속성 : 'key'를 'value'와 연결하며 아래와 같은 특성을 가짐

데이터 속성의 특성

  • 접근자 속성 : 접근자 속성은 키를 두 개의 접근자 함수(get, set) 중 하나 이상과 연결하여 값을 가져오거나 저장
    • 접근자 '메서드'가 아니라  접근자 '속성'임을 인지하는 것이 중요

접근자 속성 특성

 

 

  • 함수 : 호출이 가능한 일반 객체 => Js에서 함수를 일급객체라고 부름
    • 일급객체 : 함수에 인자로 함수를 넘기거나, 변수에 대입하기와 같은 연산을 지원하는 등, 함수를 다른 변수와 동일하게 취급
  • 배열 : 배열은 인덱스 컬렉션이라고 표현할 수 있으며, 정수 key를 가진 속성과 length 속성 사이에 특별한 연관을 지어놓은 일반 객체임
    • 형식화 배열이라는 개념도 있으나, 추후 공부해야 할것 같음
  • key 컬렉션 : 객체 참조를 key로 가지는 자료구조로, Set WeakSet은 객체의 집합을 나타내며 Map (en-US) WeakMap은 객체와 값을 연결 (요 개념도 나중에 보기!!)
    • Map : 객체의 key 값에 다양한 자료형을 허용함
    • Set : 중복을 허용하지 않는 value 컬렉션으로 key가 없는 value가 저장됨
let john = { name: "John" };
let pete = { name: "Pete" };
let jane = { name: "Jane" };

let map = new Map(); 
map.set(john, 1);
map.set(pete, 2);
map.set(jane, 3);

console.log(map); 
/* Map(3) {
      { name: 'John' } => 1,
      { name: 'Pete' } => 2,
      { name: 'Jane' } => 3
    } */

let set = new Set();

set.add(john);
set.add(pete);
set.add(jane);

console.log(set);
// Set(3) { { name: 'John' }, { name: 'Pete' }, { name: 'Jane' } }
Map과 Set은 순수한 ECMAScript 5에서도 구현할 수 있습니다. 그러나 객체를 직접 비교(<, "작음" 비교와 같이)할 방법은 없으므로 탐색 성능이 선형(O(n))으로 강제됩니다. 반면 (WeakMap을 포함해) 네이티브 구현의 경우 대략 로그 시간(O(log n))에 가까운 성능을 낼 수 있습니다.

보통 DOM 노드에 데이터를 연결할 땐 해당 객체에 직접 속성을 추가하거나 data-* 특성을 사용하겠지만, 같은 맥락 아래에서라면 이렇게 추가한 데이터를 모든 스크립트에서 다 사용할 수 있다는 문제가 있습니다. Map과 WeakMap을 사용하면 비공개 데이터를 객체에 쉽게 바인딩 할 수 있습니다.
- MDN 공식문서

 

  • 구조화된 자료 : JSON
    • JSON(JavaScript Object Notation)은 경량 데이터 교환 형식으로 범용 데이터 구조를 구성
    • number, boolean, string, null, array(ordered sequences of values), object(string-value mappings)로 구성되며, 이보다 복잡한 함수, 정규 표현식, date 등은 포함하지 않음

 

 

JavaScript 형변환


함수와 연산자에 전달된 값을 적절한 자료형으로 자동 변환되는 과정

 

원시값의 형변환

문자형으로 변환

  • alert 메서드로 받은 값은 자동으로 문자형으로 변환되어 들어옴
  • String() : 전역 객체로, 문자열의 생성자이지만, 함수로 불렸을 때는 형변환을 해줌
  • toString() : Number.prototype의 메서드로 특정한 Number 객체를 나타내는 문자열을 반환
  numObj.toString([radix])
  • radix는 진수를 나타내는 기수의 값으로, 2~36 사이의 정수이며, radix를 지정하지 않았을 때는 기본 10진수로 값을 반환한다.
toString() 메소드는 메소드의 첫 번째 아규먼트를 파싱하여, 메소드는 특정 기수(radix)를 기준으로 한 진수 값의 문자열을 환원하기 위한 시도를 합니다. 진수를 나타내는 기수 값(radix) 이 10 이상의 값일 때는, 알파벳의 글자는 9보다 큰 수를 나타냅니다. 예를 들면, 16진수(base 16)는, 알파벳 f 까지 사용하여 표현됩니다.
- MDN 공식문서

 

숫자형으로 변환

  • 수학 연산자를 사용한 표현식에서 자동으로 형변환되어 계산함
  • Number(value) : Number는 숫자를 표현하고 다룰 때 사용하는 원시 래퍼 객체이나, String과 비슷하게 함수로 사용하면 문자열이나 다른 값을 숫자형으로 변환함
    • 이때, value를 인수로 변환할 수 없으면 NaN을 리턴 (ex. Number("임의의 문자열 123")은 NaN을 리턴)
전달받은 값 형 변환 후
undefined NaN
null 0
boolean true -> 1 / false -> 0
string 문자열의 처음과 끝 공백 제거 후 남아있는 문자열이 없다면 0, 그렇지 않다면 문자열에서 숫자를 읽습니다. 변환에 실패하면 NaN이 됩니다.
- 모던 자바스크립트 튜토리얼
  • Number 프로토타입의 메서드 
    • Number.parseFloat(value) :  주어진 값을 필요한 경우 문자열로 변환한 후 부동소수점 실수로 파싱해 반환
    • Number.parseInt(value, radix) :  문자열 인자를 파싱하여 특정 진수(수의 진법 체계에서 기준이 되는 값)의 정수를 반환
      • parseInt 메서드의 경우, 위의 toString 메서드와 동일하게, radix를 인자로 받을 수 있다. 이러한 특성 때문에 아래 경우가 발생한다. 진수를 나타내는 기수 값(radix) 이 10 이상의 값일 때는, 알파벳의 글자는 9보다 큰 수를 나타내는 특성 때문에, 아래 코드에서는 인자로 받은 "null"이라는 문자열의 첫 알파뱃인 "n"이 14번째에 위치하므로, 9+14 = 23을 반환하는 것이다. "n" 다음 글자인 "u"의 경우는 21번째에 있기 때문에 해당 메서드는 23만을 반환하고 멈춘 것이다. null의 모든 알파뱃을 숫자로 변환하려면, 9+14+7 = 31을 radix로 넣어주면 된다. (이때, 반환값으로 714695가 찍히는데, 어떻게 계산된 건지는 나중에 풀어보기로...)
console.log(parseInt(null)); //NaN
console.log(parseInt(null,24)); //23
console.log(parseInt(null, 31)); //714695

 

boolean형으로 변환

  • 논리 연산자(&&, ||, ==, ===, !value) 또는 Boolean(value) 생성자를 사용하면 자동으로 값을 불린형으로 변환
    • 숫자 0, 빈 문자열, null, undefined, NaN과 같이 (원시타입 중) 빈 값들은 false, 그 외는 true를 반환 (참고사항! 모든 객체(빈 객체도 포함)는 항상 true를 반환)

 

객체를 원시값으로 변환(이 개념은 아직 잘 이해가 안된다..)

객체를 원시 값으로 변환할 때의 특성

  • 논리 평가시, 모든 객체는 true를 반환하며, 빈 객체도 true를 반환 (= [], {} 모두 true를 반환)
  • 객체끼리 수학 연산을 하거나, 수학 관련 함수를 적용하면 객체는 숫자형으로 변환
  • 객체를 출력하려고 하면 문자형으로 변환
  • 객체를 원시 타입으로 변환할 때, 만약 객체의 값이 여러 종류의 원시 타입으로 이뤄져 있다면, optional hint를 통해 선호 타입을 지정할 수 있음 hint와 관련된 자세한 내용은 ECMAScript 명세서를 참고...
  • 객체를 alert로 찍었을 때와, console.log로 찍었을 때, 서로 다른 값을 나타냄
let user = {name: "John"};

alert(user); // [object Object] -> 실행 후 콘솔에 underined가 찍힘
console.log(toString(user)) // [object undefined]
  • 이런 저런 실험을 해보니, 객체를 원시 값으로 형변환 시키려면, 단일 key의 value를 형변환 시켰을 때, 원하는 결과가 나오는 것이 확인됨(배열은 제외)
let obj1 = { str: "str" };
let obj2 = { str: "str" };
let obj3 = { num: 1 };

console.log(obj1 + obj2); // [object Object][object Object] => 형 변환 된것인가??
console.log(obj1.str + obj2.str); //strstr => 객체의 value가 원시값으로 할당되었기 때문에...
console.log(obj1 + obj3); // [object Object][object Object]

console.log(typeOf(obj1 + obj2)); // ReferenceError: typeOf is not defined at Object.<anonymous>
//위 에러 메시지를 추론하자면, Object.key가 명시된 value만을 형변환 시킬 수 있는 것으로 파악된다..

console.log(obj1.str + obj3.num); //str1 => hint가 String으로 설정되어 있는 듯

// 객체 안에서 메서드 처리
let obj = {
  str: "str",
  num: 1,
  toString() {
    return this.str;
  },
  Number() {
    return this.num * 2;
  },
};

alert(obj); // str
alert(obj + 10) // str10

//객체의 default hint를 확인해보기 
let obj = {
  str: "str",
  num: 1,
  Number() {
    return this.num * 2;
  },
};

alert(obj+10) // [Object Object]10 => String임이 확인 된 듯...

=> 여러개의 key-value로 이루어진 객체가 어떻게 형변환이 되는건지 아직까지 정확하게 이해는 가지 않는다. 다만, 전체 객체를 원시 타입으로 변환할 때는, 컴퓨터도 그 많은 key와 value를 어떻게 처리해야 하는지 몰라서 [object object]로 반환하는 것 같다는 느낌적인 느낌...ㅎㅎ

=> 고유한 key값을 사용해 해당 value를 형변환하는 것은 결국 원시 값을 다른 원시 타입으로 바꾼다는 의미 아닌가? 싶다...

=> optional hint를 선택하는 리터럴??

 

동등 연산자(==) vs 일치 연산자(===)


일치 연산자는 두 개의 값이 일치하는지 확인하기 위해 사용하는데, 동등 연산자(==)두개의 값만을 평가하고, 일치 연산자(===)는 두 값과 타입의 일치성을 검사하는 동등 연산자의 엄격 버전
이와 같은 맥락에서, 불일치 연산자(!==)부등 연산자 (!=)엄격 버전
일치와 불일치 연산자는 각각 동등과 부등 연산자보다 더욱 안전하게 사용할 수 있음
  

 

동등(==) 연산자의 트릭

  • 동등 == 연산자는 두 개의 피연산자의 타입이 다를 때, 두 값을 모두 숫자형으로 변환하는 특성이 있는데, 이런 특성 때문에, 아래와 같은 트릭 상황이 발생하기도 함

 

console.log([] == ![]); // true

// ![] 표현은 array를 boolean 값으로 변형 
console.log(![]) // false

// 두 값의 타입이 다르므로, == 연산자가 두 값을 모두 숫자로 변형
console.log(Number([])) // 0 []는 빈 배열이므로, 0을 반환
console.log(Number(![])) // 0 ![]는 false이므로, 0을 반환

// 따라서, 첫 줄은 다시
console.log(0 == 0) // true
//을 의미하므로, true를 반환

 

 

Null vs Undefined : 같아 보이지만, 미세하게 다르다!


undefined는 global object의 속성으로, 초기화되지 않은 변수들의 타입으로 지정되며, global scope에서 identifier로 사용이 가능함
null은 undefined와는 달리 identifier로 사용할 수 없으며, 식별이 불가능한(존재하지 않거나 유효하지 않은) objet 주소의 참조값임. 보통 API에서 null은 object의 value가 없을 때 반환됨
console.log(typeof undefined, typeof null); // undefined object

 

두 값의 비교

  • 일치 연산자를 사용해서 두 값을 비교하면, 서로의 타입이 다르므로 false를, 비교 연산자를 사용해 두 값을 비교하면, 각각을 숫자로 변환했을 때, null은 0을, undefined는 NaN을 반환하므로 false를 반환함
console.log(undefined === null); //false

console.log(undefined >= null); //false
console.log(Number(undefined)); //NaN
console.log(Number(null)); //0
console.log(NaN == 0); //false
  • 그런데 특이하게도, 동등 연산자를 사용해 두 값을 비교하면,  true를 반환하는데, 그 이유는 동등 연산자가 두 원시값을 '각별한 커플'처럼 취급하기 때문이라고 함
    • 동등 연산자만의 특별한 취급 규칙은 추후에 더 알아봐야 할듯...
console.log(undefined == null); //true