JavaScript는 느슨한 타입(loosely typed)의 동적(dynamic) 언어입니다.
JavaScript의 변수는 어떤 특정 타입과 연결되지 않으며,
모든 타입의 값으로 할당 (및 재할당) 가능합니다.
let foo = 42 // foo가 숫자
foo = 'bar' // foo가 이제 문자열
foo = true // foo가 이제 불리언
형변환은 명시적일 수도 있고, 암시적일 수도 있습니다.
명시적 형변환(explicit coercion)은 Number("123") 처럼
프로그래머의 코드에서 암시적으로 자료형을 정해서 변환하는 과정입니다.
암시적 형변환(implicit coercion)은 연산자 사용으로 인해 자연적으로 일어나는 형변환입니다.
대표적인 예시로 ==, +, > 등 연산자의 사용이 있습니다.
예외적으로 ===는 형변환을 야기하지 않습니다.
이 암시적 형변환을 잘 이용하면 더욱 가독성 있는 코드를 작성할 수 있지만
잘못 생각하면 프로그램의 버그가 될 수 있습니다.
JavaScript에서의 형변환은 세 가지가 있습니다.
String으로 형변환
Number로 형변환
Boolean으로 형변환
String으로의 변환은 자연스럽습니다. 출력되는 형태 그대로 변환되기 때문입니다.
String(12345) // "12345"
String(-3.14) // "-3.14"
String(true) // "true"
String(false) // "false"
String(undefined) // "undefined"
String(null) // "null"
String(BigInt(42)) // "42"
JavaScript의 object 형변환
예제
4 + 10 + "string" // "14string"
"string" + 4 + 10 // "string410"
"true" == true // false
undefined == '' // false
8 * null // 0
0 == "\n" // true
!![]+!!{}+!!"false" // 3
~undefined // -1
[2] > "1" // true
"hello" > 3 // false
"hello" < 3 // false
"-1" > "+1" // true
"-1" > +1 // false
"b" + "a" + + "a" + "a" // "baNaNa"
[] + undefined + 1 // undefined1
[2,3,5] == [2,3,5] // false
{}+[]+{}+[1] // "0[object Object]1"
!+[]+[]+![] // "truefalse"
!+[]+![]+[] // "1"
[1] + [2,3] // "12,3"
하나하나 분석해보도록 하겠습니다.
4 + 10 + "string"에선 4 + 10이 먼저 계산되어 14가 되고,
이후 14 + "string"이 되어 "14string"이 됩니다.
"string" + 4 + 10에선 "string" + 4가 먼저 계산되어 "string4"가 되고,
이후 "string4" + 10이 계산되어 "string410"이 됩니다.
+에 String이 들어가면 계속 String이라 보시면 됩니다.
"true" == true에서, == 에 의해 numeric conversion이 일어나
"true"가 NaN이 되고 true는 1이 됩니다. 때문에 전체 식은 false가 됩니다.
undefined == ''에선, ==에 undefined가 있기 때문에
numeric conversion이 일어나지 않습니다. 전체 식은 false가 됩니다.
8 * null에선 null이 0으로 변환되어 전체 식이 0이 됩니다.
0 == "\n"에선 numeric conversion이 일어나 "\n"이 0으로 변환됩니다.
때문에 전체 식은 true가 됩니다.
!![]+!!{}+!!"false"에선 !!~sth~의 ~sth~가 Boolean으로 true로 변환되므로,
두 번 complement를 해 true + true + true가 됩니다.
이후는 numeric conversion이 일어나 3이 됩니다.
~undefined는 undefined가 NaN으로 형변환되고,
비트 연산에서 NaN이 0으로 간주되기 때문 ~NaN은 -1로 계산됩니다.
[2] > "1"의 경우, numeric conversion이 일어나 [2]는
valueOf 메서드에 의해 2로, "1"은 1로 변환되기 때문에 true가 됩니다.
"hello" > 3과 "hello" < 3에서 "hello"는 NaN으로 변환되기 때문에
비교 결과도 둘 다 NaN이 됩니다.
"-1" > "+1"는 conversion이 일어나지 않습니다.
-의 ASCII 코드(45)가 +보다 크므로(43), true가 됩니다.
"-1" > +1는 타입이 일치하지 않으므로 numeric conversion이 일어납니다.
결과는 -1 > 1이 되어 false입니다.
"b" + "a" + + "a" + "a"는 서두의 그림에 있던 예시입니다.
흐름을 표현하면 다음과 같습니다.
>> "b" + "a" + + "a" + "a"
- ("b" + "a") + + "a" + "a"
- ("ba" + (+ "a")) + "a"
- ("ba" + NaN) + "a"
- "baNaN" + "a"
- "baNaNa"
[] + undefined + 1에서 우선 [] + undefined이 계산됩니다. numeric conversion에 의해[].valueOf()가 호출되는데, 이는 자기 자신이므로 원시 타입이 아니어서 numeric conversion이 실패합니다. Number([])이 0임에도 불구하고 []가 object이기 때문에 그렇습니다. 때문에 string conversion이 일어나고 이 땐 "" + "undefined"가 되어 "undefined"가 됩니다. 이후 결과는 당연히 "undefined1"이 됩니다.
[2,3,5] == [2,3,5]는 타입이 같아서 형변환이 일어나지 않고, 둘이 같은 객체가 아니므로 false가 됩니다. 이와 달리 [2,3,5] == "2,3,5"는 string conversion이 일어나므로 true가 됩니다.
{}+[]+{}+[1]는 원 포스트 최상단에 있는 예제인데, 약간의 낚시가 들어가 있습니다. 우선, 첫 중괄호 ({})는 scope로 인식되어 연산에 아무런 영향이 없습니다. 실제 연산은 +[]부터 시작합니다.
>> +[]+{}+[1]
- +''+{}+[1] // numeric conversion에서 []이 toString에 의해 ''로 변환
- +0+{}+[1] // 이후 ''이 (계속된 numeric conversion에 의해) 0으로 변환
- 0+{}+[1] // {}이 toString에 의해 "[object Object]"로 변환
- "0[object Object]" + [1]
- "0[object Object]1"
!+[]+[]+![]도 위랑 비슷합니다. !+[]는 !0이 되므로 true가 되며, true+[]에서 []는 ''으로 변환되어 "true"가 됩니다. 이후 나머지도 String이 되기에 결과적으로 "truefalse"가 됩니다.
!+[]+![]+[]는 비슷하지만 약간 다릅니다. !+[]가 true, ![]는 false이므로 둘이 더해서 1이 되며, 이후 []가 ''으로 변환되어 결과적으로 "1"이 됩니다.
[1] + [2,3]에선 valueOf가 원시 타입을 반환하지 않으므로 toString이 사용되어 "1" + "2,3"이 되기에 "12,3"이 됩니다.
//출처 : https://www.secmem.org/blog/2020/03/19/javascript-type-coercion/
'==' 와 '===' 차이점
☝ '==' 연산자를 이용하여 서로 다른 유형의 두 변수의 [값] 비교
✌ '==='는 엄격한 비교를 하는 것으로 알려져 있다 ([값 & 자료형] -> true).
🧐 #간단한 예제
🔍 숫자와 불리언 비교
✔ 0값은 false와 동일하므로 -> true 출력
0 == false // true
✔ 두 피연산자의 유형이 다르기 때문에 ->false
0 === false // expected output: false
console.log(typeof 0); // expected output: "number"
console.log(typeof false); // expected output: "boolean"
🔍 숫자와 문자열 비교
✔ 자동 유형변화 비교
2 == "2" // expected output: true
✔ 두 피연산자의 유형이 다르기 때문에 ->false
2 === "2" // expected output: false
console.log(typeof 2); // expected output: "number"
console.log(typeof "2"); // expected output: "string"
🤔 #궁굼한 케이스들
🔍 1) null 와 undefined 비교했을 때 어떤 결과가 나올 것인가?
✔ 자동 유형변화 비교
null == undefined // expected output: true
✔ 두 피연산자의 유형이 다르기 때문에 ->false
null == undefined // expected output: false
console.log(typeof null); // expected output: "object"
console.log(typeof undefined); // expected output: "undefined"
🔍 2) '!=' 와 '!==' 비교연산자의 차이는?
예상대로, 유형 변환 비교와 엄격한 비교의 [값]과 [자료형]의
다름을 boolean 형식으로 반환한다.
✔ 값이 다르지 않음으로 (자료형 비교 안함)
2 != "2" // expected output: false
✔ 두 피연산자의 유형이 다른 것이 맞기 때문에 -> true
2 !== "2" // expected output: true
console.log(typeof 2); // expected output: "number"
console.log(typeof "2"); // expected output: "string"
🔍 3) 비교 연산자를 이용하여, 'NaN'을 비교하면?
✔NaN (Not a Number)은, 어떤 것과도 같지 않다는 것을 기억해야 한다.
//출처 : https://velog.io/@filoscoder/-%EC%99%80-%EC%9D%98-%EC%B0%A8%EC%9D%B4-oak1091tes
실행 도중에 변수에 예상치 못한 타입이 들어와 타입에러가 발생할 수 있음
동적타입 언어는 런타임 시 확인할 수 밖에 없기 때문에, 코드가 길고 복잡해질 경우 타입 에러를 찾기가 어려워 집니다.
이러한 불편함을 해소하기 위해 TypeScipt나 Flow 등을 사용할 수 있습니다.
출처: https://devuna.tistory.com/82 [튜나 개발일기]
undefined은 변수를 선언하고 값을 할당하지 않은 상태, null은 변수를 선언하고 빈 값을 할당한 상태(빈 객체)이다. 즉, undefined는 자료형이 없는 상태이다.
따라서 typeof를 통해 자료형을 확인해보면 null은 object로, undefined는 undefined가 출력되는 것을 확인할 수 있다.
typeof null // 'object'
typeof undefined // 'undefined'
null === undefined // false
null == undefined // true
null === null // true
null == null // true
!null // true
isNaN(1 + null) // false
isNaN(1 + undefined) // true
#undefined
undefined는 원시값(Primitive Type)으로, 선언한 후에 값을 할당하지 않은 변수나 값이 주어지지 않은 인수에 자동으로 할당된다. 이 값은 전역 객체의 속성 중 하나로, 전역 스코프에서의 변수이기도 하다. 따라서 undefined 변수의 초기 값은 undefined 원시 값이다.
cf) undefined는 예약어가 아니기 때문에, 전역 범위 외에서 변수 이름으로 사용할 수 있다. 그러나 유지보수와 디버깅에 어려움을 겪을 수 있으므로 피하는 것이 좋다.
아래의 경우에 변수가 undefined를 반환한다.
값을 할당하지 않은 변수
메서드와 선언에서 변수가 할당받지 않은 경우
함수가 값을 return 하지 않았을 때
#null
null은 원시값(Primitive Type) 중 하나로, 어떤 값이 의도적으로 비어있음을 표현한다. undefined는 값이 지정되지 않은 경우를 의미하지만, null의 경우에는 해당 변수가 어떤 객체도 가리키고 있지 않다는 것을 의미한다.
cf) null은 undefined처럼 전역 객체의 속성 중 하나가 아니라 리터럴 값이다.
#알아두면 좋은 것
typeof undefined는 출력하면 undefined이다.
typeof null은 출력하면 object이다. 하지만 이는 여전히 원시 타입(primitive value)로, JavaScript에서는 구현 버그로 간주한다.
undefined == null은 true이다.
자바스크립트의 기본 타입(data type)은 객체(object)입니다.
객체란 이름(name)과 값(value)으로 구성된 프로퍼티(property)의
정렬되지 않은 집합입니다.
프로퍼티의 값으로 함수가 올 수도 있는데, 이러한 프로퍼티를 메소드(method)라고 합니다.
자바스크립트에서는 숫자, 문자열, 불리언, undefined 타입을 제외한 모든 것이 객체입니다.
하지만 숫자, 문자열, 불리언과 같은 원시 타입은 값이 정해진 객체로 취급되어, 객체로서의 특징도 함께 가지게 됩니다.
객체의 프로퍼티 참조
자바스크립트에서 객체의 프로퍼티를 참조하는 방법은 다음과 같습니다.
문법
객체이름.프로퍼티이름
또는
객체이름["프로퍼티이름"]
예제
name: "홍길동", // 이름 프로퍼티를 정의함.
birthday: "030219", // 생년월일 프로퍼티를 정의함.
pId: "1234567", // 개인 id 프로퍼티를 정의함.
fullId: function() { // 생년월일과 개인 id를 합쳐서 주민등록번호를 반환함.
return this.birthday + this.pId;
}
};
person.name // 홍길동
person["name"] // 홍길동
객체의 메소드 참조
자바스크립트에서 객체의 메소드를 참조하는 방법은 다음과 같습니다.
문법
객체이름.메소드이름()
예제
var person = {
name: "홍길동",
birthday: "030219",
pId: "1234567",
fullId: function() {
return this.birthday + this.pId;
}
};
person.fullId() // 0302191234567
person.fullId; // function () { return this.birthday + this.pId; }
메소드를 참조할 때 메소드 이름 뒤에 괄호(())를 붙이지 않으면,
메소드가 아닌 프로퍼티 그 자체를 참조하게 됩니다.
따라서 괄호를 사용하지 않고 프로퍼티 그 자체를 참조하게 되면
해당 메소드의 정의 그 자체가 반환됩니다.
/출처 : http://www.tcpschool.com/javascript/js_object_concept
기본형
객체가 아닌 데이터 유형을 말한다.
Number
String
Boolean
Symbol(ES6에 추가, 객체 속성을 만드는 데이터 타입)
null
undefined
기본형 데이터는 값을 그대로 할당한다.
메모리상에 고정된 크기로 저장되며 원시 데이터 값 자체를 보관하므로, 불변적이다.
기본적으로 같은 데이터는 하나의 메모리를 사용한다.(재사용)
참조형
참조 타입은 변수에 할당할때 값이 아닌 데이터의 주소를 저장한다.
Object
Array
const 로 선언된 변수 배열에 Array.push를 적용할 수 있는 이유는 배열은 참조 타입이기 때문에 데이터의 주소를 대입할 수 있기 때문이다.
Function RegExp
문자열에 나타나는 특정 문자조합과 대응시키기 위해 사용되는 패턴이다.
Map
else..
참조형은 기본형 데이터의 집합이다. 참조형 데이터는 값이 지정된 주소값을 할당한다.
불변 객체
먼저 불변(immutability)이란 뭘까? 단어에서 유추해볼 수 있다시피
'변하지 않는' 뜻이라고 생각하면 되겠다.
그럼 '불변 객체'란? '변하지 않는 객체'
즉 이미 할당된 객체가 변하지 않는다는 뜻을 가지고 있다.
자바스크립트에서 불변 객체를 만들 수 있는 방법은 기본적으로 2가지 인데
const와 Object.freeze()를 사용하는 것이다.
깊은 복사, 얕은 복사
결론부터 말하자면 얕은 복사는 객체의 참조값(주소 값)을 복사하고,
깊은 복사는 객체의 실제 값을 복사합니다.
먼저, 자바스크립트에서 값은 원시값과 참조값 두 가지 데이터 타입의 값이 존재합니다.
원시값은 기본 자료형(단순한 데이터)을 의미합니다.
Number, String, Boolean, Null, Undefined 등이 해당합니다.
변수에 원시값을 저장하면 변수의 메모리 공간에 실제 데이터 값이 저장됩니다.
할당된 변수를 조작하려고 하면 저장된 실제 값이 조작됩니다.
참조값은 여러 자료형으로 구성되는 메모리에 저장된 객체입니다.
Object, Symbol 등이 해당합니다. 변수에 객체를 저장하면
독립적인 메모리 공간에 값을 저장하고, 변수에 저장된 메모리 공간의 참조(위치 값)를
저장하게 됩니다. 그래서 할당된 변수를 조작하는 것은
사실 객체 자체를 조작하는 것이 아닌, 해당 객체의 참조를 조작하는 것입니다.
원시값을 복사할 때 그 값은 또 다른 독립적인 메모리 공간에 할당하기 때문에, 복사를 하고 값을 수정해도 기존 원시값을 저장한 변수에는 영향을 끼치지 않습니다. 이처럼 실제 값을 복사하는 것을 깊은 복사라고 합니다. 하지만 이것은 자료형을 깊은 복사한 것입니다.
const a = 'a';
let b = 'b';
b = 'c';
console.log(a); // 'a';
console.log(b); // 'c';
// 기존 값에 영향을 끼치지 않는다.
참조값을 복사할 때는 변수가 객체의 참조를 가리키고 있기 때문에
복사된 변수 또한 객체가 저장된 메모리 공간의 참조를 가리키고 있습니다.
그래서 복사를 하고 객체를 수정하면 두 변수는 똑같은 참조를 가리키고 있기 때문에
기존 객체를 저장한 변수에 영향을 끼칩니다. 이처럼 객체의 참조값(주소값)을
복사하는 것을 얕은 복사라고 합니다.
const a = {
one: 1,
two: 2,
};
let b = a;
b.one = 3;
console.log(a); // { one: 3, two: 2 } 출력
console.log(b); // { one: 3, two: 2 } 출력
// 기존 값에 영향을 끼친다.
출처: https://bbangson.tistory.com/78 [뺑슨 개발 블로그]
변수, 호이스팅, TDZ(Temporal Dead Zone), 스코프
var name = 'kim';
var name = 'park';
var 는 한번 선언한 변수를 다시 선언할 수 있다. 하지만 let은 안된다.
-스코프는 범위이다.
var type으로 선언한 경우 호이스팅된 후 사용이 가능한데 값은 undefined 이다.
선언은 호이스팅되지만 할당은 호이스팅 안되서 그러는 것이다.
let 과 const도 호이스팅 되는데 실제로 사용할 경우에는 에러가 발생하는데 그 이유는 이는 TDZ 때문이다.
TDZ는 대충 직역하면 일시적으로 죽은 공간인데 이는 초기화가 안된 변수가 있어서 죽은 공간이라고 생각하면 된다.
(호이스팅은 스코프 단위로 일어난다.)
let age = 30;
function showAge(){
console.log(age);
let age = 20;
}
showAge();
위 코드가 그 예시인데 에러가 발생하는 코드이다.
그 이유는 let도 호이스팅이 되서 에러가 발생하는 것이다.
showAge 함수 안에 let age 가 없었다면 에러가 발생하지 않는데 이게 있어서
console.log(age)에서 하단에 선언된 age를 호이스팅되어 사용하게되는데 여기서 문제는
var와 다르게 let은 선언과 초기화가 동시에 이루어지지 않는다.
그래서 선언만 되고 초기화가 안되서 레퍼런스 에러가 발생하는 것이다.
var 1.선언, 초기화 2. 할당
let 1.선언 2.초기화 3.할당
const 1.선언,초기화,할당 (무조건 선언 시 할당해야 된다.)
var : 함수 스코프
let, const : 블록 스코프(함수, if, for, while, try/catch문)
함수 선언식 - Function Declarations
일반적인 프로그래밍 언어에서의 함수 선언과 비슷한 형식이다.
function 함수명() {
구현 로직
}
// 예시
function funcDeclarations() {
return 'A function declaration';
}
funcDeclarations(); // 'A function declaration'
함수 표현식 - Function Expressions
유연한 자바스크립트 언어의 특징을 활용한 선언 방식
var 함수명 = function () {
구현 로직
};
// 예시
var funcExpression = function () {
return 'A function expression';
}
funcExpression(); // 'A function expression'
함수 선언식과 표현식의 차이점
함수 선언식은 호이스팅에 영향을 받지만, 함수 표현식은 호이스팅에 영향을 받지 않는다.
함수 선언식은 코드를 구현한 위치와 관계없이 자바스크립트의 특징인 호이스팅에 따라 브라우저가 자바스크립트를 해석할 때 맨 위로 끌어 올려진다.
Execution Context 는 자바스크립트의 핵심 개념으로,
코드를 실행하기 위해 필요한 환경이다.
더 자세히 말하자면, 실행할 코드에 제공할 환경 정보들을 모아놓은 객체이다.
자바스크립트의 동적 언어로서의 성격을 가장 잘 파악할 수 있는 개념.
모든 코드는 특정한 실행 컨텍스트 안에서 실행된다.
javascript는 어떤 execution context가 활성화되는 시점에
선언된 변수들을 위로 끌어올리고(hoisting), 외부 환경 정보를 구성하고,
this값을 설정하는 등의 동작을 수행하는데, 이로 인해 다른 언어에서는
발생할 수 없는 특이한 현상들이 발생한다.
자바스크립트의 주요한 실행 컨텍스트에는 두 가지가 있다.
Global Execution Context
디폴트 실행 컨텍스트로, 자바스크립트 파일이 엔진에 의해 처음 로드되었을 때
실행되기 시작하는 환경이다.
Fuction Execution Context
우리가 execution context를 따로 구성하는 방법은 함수를 실행하는 것 뿐이다.
함수가 호출되고 실행됨에 따라서 해당 함수 안에서 생성되는 컨텍스트.
각각의 함수는 고유의 실행 컨텍스트를 가진다.
그리고 전역 실행 컨텍스트에 언제나 접근할 수 있다.
스코프는 함수의 중첩에 의해 계층적 구조를 가짐.
⇒ 외부 함수(outer function) / 중첩 함수(nested function)
변수를 참조할 때, 자바스크립트 엔진은 스코프 체인을 통해 변수를 참조하는
코드의 스코프에서 시작하여 상위 스코프로 이동하면서 선언된 변수를 검색함.
여러 스코프에서 동일한 식별자를 선언한 경우,
무조건 스코프 체인 상에서 가장 먼저 검색된 식별자에만 접근 가능
⇒ 변수 은닉화(variable shadowing)
스코프 체인은 outerEnvironmentReference와 밀접한 관계를 가짐.
클로저는 독립적인 (자유) 변수를 가리키는 함수이다.
또는, 클로저 안에 정의된 함수는 만들어진 환경을 ‘기억한다’.
일반적으로 함수 내에서 함수를 정의하고 사용하면 클로저라고 한다.
function getClosure() {
var text = 'variable 1';
return function() {
return text;
};
}
var closure = getClosure();
console.log(closure()); // 'variable 1'
위에서 정의한 getClosure()는 함수를 반환한다.
반환된 함수는 getClosure() 내부에서 선언된 변수 text를 참조하고 있다.
참조된 변수는 함수 실행이 끝났다고 해서 사라지지 않고, variable 1을 반환하고 있다.
getClosure()에서 반환된 함수를 클로저라고 한다.
클로저를 통한 은닉화
JavaScript에서 객체지향 프로그래밍을 말한다면 Prototype을 통해
객체를 다루는 것을 말한다. ES2015에는 클래스도 있다.
Prototype을 통한 객체를 만들 때의 주요한 문제 중 하나는
Private variables에 대한 접근 권한 문제이다.
function Hello(name) {
this._name = name;
}
Hello.prototype.say = function() {
console.log('Hello, ' + this._name);
}
var hello1 = new Hello('김지');
var hello2 = new Hello('삼지');
var hello3 = new Hello('부지');
hello1.say(); // 'Hello, 김지'
hello2.say(); // 'Hello, 삼지'
hello3.say(); // 'Hello, 부지'
hello1._name = 'anonymous';
hello1.say(); // 'Hello, anonymous'
=====================================
function hello(name) {
var _name = name;
return function() {
console.log('Hello, ' + _name);
};
}
var hello1 = hello('김지');
var hello2 = hello('삼지');
var hello3 = hello('부지');
hello1(); // 'Hello, 김지'
hello2(); // 'Hello, 삼지'
hello3(); // 'Hello, 부지'
Hello()로 생성된 객체들은 모두 name이라는 변수를 가지게 된다.
변수명 앞에 underscore()를 포함했기 때문에 일반적인 JavaScript
네이밍 컨벤션을 생각해 봤을때 이 변수는 Private variable으로
쓰고싶다는 의도를 알 수 있다. 하지만 외부에서 쉽게 접근가능한 변수일 뿐이다.
특별히 인터페이스를 제공하는 것이 아니라면, 여기서는 외부에서
_name에 접근할 방법이 전혀 없다. 이렇게 은닉화도 생각보다 쉽게 해결할 수 있다.