V-logue

[항해99] Javascript라는 언어의 특성 본문

항해 99

[항해99] Javascript라는 언어의 특성

보그 2022. 5. 18. 22:18

JavaScript의 자료형과 JavaScript만의 특성

느슨한 타입

자바 스크립트는 느슨한 타입의 언어이다. 느슨한 타입의 언어라면 이해가 잘 안가는 사람이 있을텐데,

예를 들어본다면 강력한 타입의 언어와 비교를 들 수 있다.

var a = 11;
var b = "ELEVEN" // javascript Example

int a = 11;
string b = "ELEVEN" // java Example

위의 예시를 보면, 자바스크립트는 변수를 단순히 타입과 상관없이 var라고 주었지만 자바는 

11은 정수를 표현할 때 사용하는 int를 ELEVEN은 문자를 표현할 때 사용하는 string으로 사용되었다.

 

자바스크립트의 변수는 타입을 가지고 있지만, 내부적으로 정해지는 것일 뿐이다.

 1 + 1 + 1; = // 3 
 1 + 1 + "1"; = // 21
 "1" + 1 + 1; = // 111
 
 //
 
 1 == true; // true
 1 === true; // false
 
 7 == "7" // true
 7 === "7" // false

위 계산식을 살펴보면 숫자 끼리 있을 때는 정상적으로 계산이 되지만, 숫자에 String이 섞이면 String이 섞이고 난 후에는 모두 String으로 변환된다. 또한, 엄격한 의미의 동급을 비교하는 것이 아닌 부분에서는 1 == true;지만

엄격한 의미로 양항을 비교하는 ===을 사용한다면 false가 나온다.

 

이 또한, 자바스크립트가 내부적으로 타입기 관리되기 때문에 타입들이 바뀌는 느슨한 타입의 예라고 할 수 있다.

동적 언어

자바스크립트는 동적 언어이기도 하다.

간단하게 예를 들자면 자바스크립트 없이 html과 css로만 코드를 구성하고 웹페이지를 실행시키면, 내가 사용한 그대로만 페이지를 보여줄 뿐이지 사용자의 요청에 따라 변화하는 유동성을 가지고 있지 않다.

 

반면, 자바스크립트를 사용한다면 사용자가 주는 정보에 따라 유동적으로 웹페이지를 재구성할 수 있다.

동적타입의 언어는 런타임의 변수나 필드 등이 선언될 때가 아닌 변수의 값에 따라 타입이 설정된다.

빠르게 작성할 수 있고 유연하게 사용할 수 있지만 쉽게 사용할 수 있는 만큼 에러에 취약한 면이 있다.

undefined와 null
undefined와 null은 모두 각각의 값이 타입에서 유일하다. undefined는 undefined만 null은 null값만 가지는 것이다. 

undefined은 아무런 값도 할당 받지 못한 상태를 의미한다. 

var // let // const로 선언한 변수는 암묵적으로 undefined로 초기화된다. 따라서, 변수를 선언 한 후 아무런 값도 할당하지 않은 변수를 참조한다면 undefined가 리턴된다.

만약 변수에 현재 값이 없다는 것을 보여주고 싶다면 undefined를 사용하기 보다는 null을 사용하는 것이 좋다

 

null은 비어있는, 존재하지 않는 값을 의미한다.

언어적으로 null은 존재하지 않는 없는 값이라는 것을 보여주고 싶을 때 사용된다.

만약, 변수에 null을 할당한다면 변수가 이전에 참조하던 것을 더이상 참조하지 않겠다는 의미와도 같다.

null값은 함수가 유효한 값을 반환할 수 없을 때나, typeof로 자료형을 확인할 때 객체를 리턴하는데 null이 엄밀히 말하자면 객체이기 때문에 이럴 때 자주 사용된다.

물론 자바스크립트는 대소문자를 구분하기 때문에 Null과 NULL과는 다르다.


JavaScript 객체와 불변성

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

기본형 데이터(Primative Type)는 객체가 아닌 데이터 유형을 말한다.

5가지 값을 가지는데ㅡ ES6에 새로 하나가 추가되서 6가지 값을 가지게 됐다.

  • Number
  • String
  • Boolean
  • Symbol(ES6에 추가, 객체 속성을 만드는 데이터 타입)
  • null
  • undefined

기본형 데이터는 있는 그대로의 값을 할당한다.

고정된 크기로 저장되며 원시 데이터 그대의 값을 보관하기 때문에 불변적이다.

기본적으로 같은 데이터는 하나의 메모리를 사용한다.

 

참조형 데이터(Reference Type)는 변수에 할당할때 값이 아닌 데이터의 주소를 저장한다.

  • object
  • array
  • function RegExp
  • Map
  • else

참조형 데이터는 기본형의 집합이다. 참조형 데이터는 값이 지정된 주소값을 할당한다.

불변성

Immutable type

불변성이란 객체가 생성된 이후로 그 상태를 변경할 수 없는 것을 말한다.

자바스크립트의 기본형 데이터들은 기본적으로 불변성을 유지한다.

let name = 'Vogue'
let blog = name;
name = Vlogue;

console.log(blog); // Vogue
console.log(name); // Vlogue

첫 let name = 'Vogue'이며 다음에 재할당된 name의 값은 Vlogue이다.

console.log(name)으로 찍어보면 출력되는 값은 Vlogue인데,

Vogue라는 String값이 메모리에 한 번 생성된 이후로는 변하지 않는 것을 알 수 있다.

물론 let은 재할당이 가능하고 name = Vlogue는 Vlogue라는 String이 다시 메모리에 생성된 것이기 때문에

console.log(blog)라고 찍으면 Vogue가 출력된다.

 

Mutable type

위에 나열한 immutable type을 제외하고 모든 값은 객체(Object)타입이며 변할 수 있는 값이다.

 다음 예시를 살펴보자,

let a = {
	name : 'Vlogue',
    blog : 'tistory'
}

let b = a;

a.blog = 'naver';

console.log(a.blog); // naver

let a라는 값에 name과 blog값이 새로 생성되고, let b에선 그런 a의 주소를 가리킨다.

 

a.blog는 변수 a의 객체인 name에 naver라는 String을 다시 할당했기 때문에,

 

console.log(a.blog) 가 tistory가 아닌 naver가 출력된다. 이해가 잘 되지 않을 수도 있기 때문에 배열로 한번 더 보자면,

let a = ['Vlogue'];
let b = a;

a.push('tistory');

console.log(a); // ['Vlogue','tistory']
console.log(b); // ['Vlogue','tistory']

let a는 ['Vlogue']라는 배열을 참조하고, let b는 그런 배열을 참조하고 있는 a를 다시 참조한다.

여기서 a.push를 통해 배열을 참조하고 있는 변수 a에 tistory라는 값을 추가하고,

콘솔을 찍어보면 위와 같이 나올 것이다.

 

let b는 let a와 같은 말이기 때문에 콘솔에 b를 찍어보면 a와 같은 값이 나올 것이다.

얕은복사와 깊은복사

얕은 복사는 객체의 참조 값(주소 값)을 복사하고, 깊은 복사는 객체의 실제 값을 복사한다.

 

자바스크립트에는 원시값과 참조값 두 가지 데이터가 존재한다.

 

여기서 원시값은 기본 자료형을 의미한다. 위에서 설명한 것처럼 변수에 원시값을 저장하면

변수의 독립적인 메모리 공간에 실제 데이터 값이 저장된다.

할당된 변수를 조작하려고 한다면 실제 값이 조작된다.

 

한편, 참조값은 여러 자료값이 존재한다. 변수에 객체를 저장하면 독립적인 메모리 공간에 값을 저장하고, 

변수에 저장된 공간의 참조(위치 값)을 저장하게 된다. 

따라서 할당된 변수를 조작하는 것은 사실상 참조를 조작하는 것이다.

 

원시값을 복사할 때 그 값은 독립적인 메모리 공간에 할당하기 때문에, 복사를 하고 값을 변경시켜도

원시값이 저장된 변수에는 영향이 없다. 이같이 실제 값을 복사하는 것을 깊은 복사라고 한다.

let a = Vlogue;
let b = a;

b = logue;

console.log(a); // Vlouge
console.log(b); // logue
console.log(a === b); // false

위의 예시처럼 기존의 a값은 변하지 않는다.

b = logue로 재할당 해줬기 때문에 a와 b는 모두 독립적으로 존재하는

메모리에 값 자체를 할당한 것이라고 볼 수 있다.

그런 의미로 a === b가 같지 않게 됐기 때문에 false가 나오는 것이다.

 

한편, 얕은 복사는 참조(주소)값의 복사를 나타낸다.

let data = { name : "Kim" };
let newName = data;

newName.name = "Park";

console.log(data); // { name : "Park" }
console.log(date === newName) // true

얕은 복사는 결국 데이터가 새로 생성되는 것이 아닌, 변경된 데이터를 참조 값인 메모리 주소에 전달하여

두개의 변수가 한 데이터를 공유하고 있는 것이다.


호이스팅과 TDZ

호이스팅

호이스팅은 간단하게 말하자면, 함수의 선언부가 유효한 범위의 최상단까지 끌어올려지는 것을 말한다.

자바스크립트의 코드는 위에서 아래로 순차적으로 읽힌다. 호이스팅은 2가지가 있는데,

 

먼저 변수 호이스팅

function blog(){
  var name;
  console.log(name);  // undefined // 단순히 변수를 선언
  name = "Vlogue";
  console.log(name); // Vlogue // 선언된 변수를 초기화
}

으로 실행시키면 undefined와 Vlogue가 나오게 된다.  위 코드는 다른 언어(C, Java, etc..)라면 참조(Reference)에러가 

났어야 하지만 자바스크립트에서는 위에 설명한 것 처럼 해석한다.

자바스크립트는 함수가 실행되기전에 변수 및 초기화된 대상들을 모두 함수 유효범위 최상단으로 끌어올리게 되는데,

이렇게 끌어올려진 변수 및 초기화된 대상들은 함수내에서 기억되고 있다가 실행된다.

물론 실제로 끌어올려지는 것은 아니며, 스크립트 내부적으로 구조를 빌드하고 문법을 검사하는 parser가 끌어 올려서

처리하는 것이다.

 

함수 호이스팅은 함수 선언문 방식일 때만 호이스팅이 된다.

함수 표현식이나 new function( )과 (let, const)같은 단순 변수 선언 방식으로는

함수 정의시 호이스팅되지 않는다.

function blog(){
  var name;
  console.log(name);  // undefined // 단순히 변수를 선언
  name = "Vlogue";
  console.log(name); // Vlogue // 선언된 변수를 초기화
}  // 함수 선언문이기 때문에 Hoisting 가능 

let blog = function (){
  console.log(name); 
  let name = "Vlogue";
  console.log(name); 
} // 함수 표현식이기 때문에 Hoisting 불가능(에러가 발생한다)

한편 함수 선언식보다는 표현식 사용을 권장하고 있는데, 그 이유는

console.log(add(2, 3));
function add(x, y) {
return x + y;
}
console.log(add(3, 4));
// >> 5
// >> 7

위와 같이 함수 외부에 위치한 console.log(add(2, 3)); 마저도 함수내부의 식이 호이스팅 되서 위치에 상관없이

유효 범위가 코드의 처음부터 인것을 알 수 있다. 이는 코드를 엉성하게 만들 수 있기 때문에

console.log(add(2, 3)); // error
// 함수 표현식
var add = function (x, y) {
return x + y;
}
console.log(add(3, 4)); // 7

아래와 같은 함수 표현식으로 사용할 것을 권장하는 것이다.

 

결국 코드의 가독성과 유지보수성을 위해 호이스팅이 일어나지 않도록 코드를 작성해야 한다.

가급적 코드 상단부에 함수와 변수를 선언한다면 호이스팅으로 인한 스코프 현상을 최대한 방지할 수 있다.

스코프(Scope)

단순하게 스코프(Scope)를 한글로 번역하면 '범위'이다.

자바스크립트에서 스코프는 '변수에 접근할 수 있는 범위'를 의미한다.

자바스크립트 스코프에는 2가지 Type이 존재하는데, 하나는 전역(global)과 지역(local)이다.

 

전역스코프는 말그대로 전역에 접근할 수 있는 범위를 의미하고,

지역스코프는 해당 범위에만 접근할 수 있고 범위를 벗어난 부분에 대해서는 접근할 수 없다.

 

자바스크립트에서는 함수를 선언할때마다 새로운 스코프를 생성하게되는데, 함수 내부에서 선언한 변수는

함수 내부적으로만 실행되고, 이를 함수 스코프라고 하고 함수 내부에서만 실행된 변수는 지역스코프가 된다.

var a = 1;  // 전역 스코프

function plus() {   // { } 괄호안 지역스코프
	var a = 2;
    console.log(a);
}
plus(); /// 2

console.log(a); /// 1
TDZ(Temperal Dead Zone)

TDZ는 일시적인 사각지대, 죽은 지역을 말한다.

자바스크립트적으로는 스코프의 시작지점과 초기화 시작 지점까지의 구간을 TDZ라고 한다.

console.log(name); // TDZ
const name = 'Jihun’; // 함수 선언 및 할당
console.log(name); //사용 가능

TDZ를 얘기하기 위해서는 변수를 꼭 얘기해야 하는데, 변수는 선언, 초기화, 할당 단계를 거쳐서 생성된다.

  • 선언단계: 변수를 실행 컨텍스트의 변수 객체에 등록하는 단계를 의미합니다.                                               이 변수 객체는 스코프가 참조하는 대상이 된다.
  • 초기화 단계: 실행 컨텍스트에 존재 하는 변수 객체에 선언 단계의 변수를 위한 메모리를 만드는 단계 입니다.      이 단계에서 할당된 메모리에는 undefined로 초기화 됩니다.
  • 할당 단계사용자가 undefined로 초기화된 메모리의 다른 값을 할당하는 단계 입니다. 

사실 아무렇지 않게 사용했던 var / let / const는 사실 저런 엄청난 단계들을 거치고 있던 것이다.

 

먼저 var 변수부터 보자면, var는 변수 선언전에 선언단계와 초기화 단계를 동시에 진행한다. 

그렇기 때문에, 자바스크립트에서 실행 컨텍스트 변수 객체를 등록하고 메모리를 undefined로 만들어 버린다.

변수를 선언하기도 전에 호출을 해도 undefined로 호출이 되는 호이스팅이 발생하는 것이다.

 

let 변수는 var와는 다르게 선언단계와 초기화 단계가 나뉘어진다.

그렇기 때문에 컨텍스트에 변수를 등록했다고 해도 메모리가 할당되지 않아,

접근할 수 없어 참조에러(ReferenceError)가 발생한다. (호이스팅이 안되는 것이 아니다. 메모리가 할당되지 않은 것이다.)

 

위에서 설명한 것처럼 var와 마찬가지로 let또한 일단 실행 컨텍스트에 변수가 등록은 되기때문에 호이스팅 자체는

실행되고 있는 것이다. 할당하기 전에는 사용할 수 없어도...

 

const 변수는 선언 + 초기화 + 할당을 동시에 진행하지 않는다면 에러가 발생한다.

let name;
name = 'Jihun';

var age;
age = 30;

# Uncaught SyntaxError: Missing initializer in const declaration
const gender; // 선언만하고 할당은 하지 않았기 때문에 에러발생
gender = 'male';
실행 컨텍스트와 콜스텍

자바스크립트는 코드를 실행하기 위해 필요한 여러가지 정보가 필요한데,

실행 컨텍스트란 코드가 실행되기 위해 필요한 정보들을 가진 범위를 추상화하기 위해 객체 형태로 나타낸 것을 말한다.

 

자바스크립트에서 실행 컨텍스트를 만들 수 있는 방법은 다음과 같다.

  • 전역 코드 : 전역 영역에 존재하는 코드
  • Eval 코드 : eval 함수로 실행되는 코드
  • 함수 코드 : 함수 내에 존재하는 코드
  • (ES6부터는) 블록문

실행 컨텍스트는 논리적 스택 구조를 가진다. 실행되는 순서대로 콜 스택(call stack)에 쌓였다가,

가장 위에 쌓여있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 동일한 환경과 순서를 보장한다.

처음 자바스크립트가 실행되는 순간 전역 코드가 콜 스택에 쌓인다. 최상단의 코드는 

코드 내부의 별도 실행 명령 없이도 자연스럽게 실행되기 때문에 JS가 실행되는 순간 전역 코드가 활성화된다.

전역 컨텍스트는 프로그램이 종료될 때까지 유지된다.

호이스트의 개념에 대해서 알고 있다면 순차적으로 최상단에 위치한 것부터 실행된다는 것의 의미를

알 수 있다.

 

전역 컨텍스트가 실행되고 나서, 함수가 호출되고 새로운 실행 컨텍스트가 실행되면 전역 컨텍스트위에 쌓이고

새로운 컨텍스트가 실행되면 그 역시 스택의 최상단에 쌓이게 된다.이런식으로 쌓인 컨텍스트들은 가장 최상단에 쌓인 실행 컨텍스트 함수가 종료되면 콜 스택에서 제거된다.

 

실행 컨텍스트 안에는 다음과 같은 정보가 담긴다.

  • VariableEnvironment : 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보. 선언 시점의 LexicalEnvironment의 스냅샷으로, 변경사항은 반영되지 않음
    ◦ environmentRecord
    ◦ outer-EnvironmentReference
  • LexicalEnvironment : 처음에는 VariableEnvironment와 같지만 변경사항이 실시간으로 반영됨
    ◦ environmentRecord
    ◦ outer-EnvironmentReference

    • VariableEnvironment(V.E)와 LexicalEnvironment(L.E)는 실행 컨텍스트에서 변수의 참조들을 기억하는 환경이다.이 두 환경의 내부는 다시 EnvironmentRecord  OuterEnvironmentReference 로 구성되어 있다.
      • EnvironmentRecord : 컨텍스트와 관련된 코드의 식별자 정보들이 저장됨
      • OuterEnvironmentReference : 호출된 함수가 선언될 당시의 Lexical Environment를 참조하는 포인터로, 스코프 체인을 가능하게 함
  • ThisBinding : this 식별자가 바라보고 있는 대상 객체
스코프 체인

자바스크립트는 변수의 유효 범위를 검색할 때 안에서부터 바깥으로 찾아나가는데, 이것을 스코프 체인(scope chain)이라고 한다.

 

은닉화

은닉화는 아래 예제를 보고 얘기를 하자면,

(function () {
  var a = 'a';
})();

console.log(a);
ReferenceError: a is not defined

위의 함수를 실행시키면 아래 처럼 에러가 나오게 된다.

이러한 방식과 같이 직접적으로 변경되면 안 되는 변수에 대한 접근을 막는 것을 은닉화라고 한다.

 

클로저를 통한 은닉화

 

자바스크립트에서 일반적인 객체지향 프로그래밍 방식으로 prototype를 사용하는데 객체 안에서 사용할 속성을 생성할 때 this 키워드를 사용하게 된다.

하지만 이러한 Prototype을 통한 객체를 만들 때의 주요한 문제가 하나 있다.

바로 Private variables에 대한 접근 권한 문제입니다. 아래 코드를 예시로,

function Hello(name) {
  this._name = name;
}

Hello.prototype.say = function() {
  console.log('Hello, ' + this._name);
}

let a = new Hello('영서');
let b = new Hello('아름');

a.say() //Hello, 영서
b.say() //Hello, 아름

a._name = 'anonymous'
a.say() // Hello, anonymous

현재 Hello() 함수를 통해 생성된 객체들은 모두 _name이라는 변수를 가지게 된다.

변수명 앞에 underscore(_)를 포함했기 때문에 일반적인 JavaScript 네이밍 컨벤션을 생각해 봤을때

이 변수는 Private variable으로 쓰고싶다는 의도를 알 수 있다.

하지만 실제로는 여전히 외부에서도 쉽게 접근가능한 것을 확인할 수 있습니다.

이 경우 클로저를 사용하여 외부에서 직접적으로 변수에 접근할 수 있도록 캡슐화(은닉화)할 수 있다.

function hello(name) {
  let _name = name;
  return function () {
    console.log('Hello, ' + _name);
  };
}

let a = new hello('영서');
let b = new hello('아름');

a() //Hello, 영서
b() //Hello, 아름

이렇게 a와 b라는 클로저를 생성함으로써 함수 내부적으로 접근이 가능하도록 만들 수 있다.

이번에는 조금 더 간단한 예제를 살펴보도록 하겠다.

function a(){
  let temp = 'a' 
  
  return temp;
} 

// console.log(temp)  error: temp is not defined
const result = a()
console.log(result); //a

현재 위 함수 내부적으로 선언된 temp에는 직접적으로 접근을 할 수 없다.

함수 a를 실행시켜 그 값을 result라는 변수에 담아 클로저를 생성함으로써 temp의 값에 접근이 가능하다.

이렇게 함수 안에 값을 숨기고 싶은 경우 클로저를 활용해볼 수 있다.

 

실습 과제
let b = 1; // 전역스코프, 스크립트 전역에 영향을 미친다.

function hi () { // 지역스코프, function이라는 함수 안에서 선언된 변수는 
                // 함수 안에서만 유효하다.

const a = 1;

let b = 100;

b++;

console.log(a,b);

}

// console.log(a); 를 실행시키면 ReferenceError: a is not defined이라고 나오는데 이는 함수
// const a =1; 이 function함수 안에서 선언된 변수이기 때문에 지역을 벗어나서 실행되지 않는 것이다.

console.log(b); // 가장 최상단에 위치한 let b =1;이 실행된모습

hi(); // 선언됐던 함수 function hi() { ...}가 hi()로 호출된 모습이다.
//console.log(a,b) = 1, 101 함수 내에서 실행된 콘솔이기 때문에
// 함수 내부적인 a, b값을 따라간다. ++ 는 1을 더해주라는 의미이기 때문에
// 100이 아닌 101이 출력된다.

console.log(b); // 함수 function이 실행됐다고 해도, 얕은 복사로서 데이터를 공유하는게 아니기 때문에
                // 마지막 console.log(b)도 let b = 1;인 가장 최상단에 위치한 함수가 실행된다.
                // 애초에 let b = 100; 이라는 값이 function안에 있었기 때문이다.
                // 밑에서 이와 비슷한 예와 호이스팅을 설명하자면,

// let x = 5;

//console.log(add(2, 3)); => 5  (function add(x, y)와 return x+ y;가 호이스팅되서 함수 밖의
                                // console.log(add(2, 3)))에 까지 영향을 미친 모습이다.
                                // 이런식이면 코드가 엉성해질 수 있기 때문에 유의해야 한다.
//function add(x, y) {
//return x + y;
//}
//console.log(add(3, 4)); => 7

//console.log(x);  => 5 ( 위에서 x + y라고 식을 정해주고 cosole.log(add(3,4))로서 x값에 3이 들어
//                        갔음에도 가장 최상단에 실행된 전역 스코프인 let x =5;가 실행된 모습이다.
//                        위와 같다.)
Comments