1) JavaScript의 자료형
프로그래밍에서 프로그램이 처리할 수 있는 모든 것을 자료라고 부르며 자료 형태에 따라 숫자 자료형, 문자열 자료형, 불 자료형으로 나뉨.
JavaScript는 느슨한 타입(loosely typed)의 동적(dynamic) 언어다. JavaScript의 변수는 어떤 특정 타입과 연결되지 않으며, 모든 타입의 값으로 할당 (및 재할당) 가능.
let foo = 42 // foo가 숫자
foo = 'bar' // foo가 이제 문자열
foo = true // foo가 이제 불리언
1. 문자 자료형
'this is "string"'
이스케이프 문자(\)를 사용하면 따옴표를 문자 그대로 사용 가능.
->"this is\"string\""
이스케이프 문자의 특수기능들
\n : 줄바꿈
\t : 탭
\\ : 역슬래시(\)그 자체를 의미
문자열 연결 연산자 : 문자열 + 문자열
'가나다'+'라마'+'바사'+'아자차카타파하'
"가나다라마바사아자차카타파하"
2. 숫자 자료형
숫자 연산자 : + 더하기, - 빼기, * 곱하기, / 나누기
자바스크립트는 숫자 자료형을 연산할 때 연산자 우선순위를 고려함.
예) 5+3*2 곱셈의 우선순위가 덧셈보다 높으므로 곱셈 먼저 계산
나머지 연산자 % 좌변을 우변으로 나눈 나머지를 출력하는 연산자.
> 10 % 2 == 0 >10 % 3 == 1
3. 불(불린) 자료형
참과 거짓 값을 표현할 때 사용. true, false.
비교 연산자를 사용해도 불을 만들 수 있음.
== | 왼쪽 피연산자와 오른쪽 피연산자의 값이 같으면 참을 반환함. |
=== | 왼쪽 피연산자와 오른쪽 피연산자의 값이 같고, 같은 타입이면 참을 반환함. |
!= | 왼쪽 피연산자와 오른쪽 피연산자의 값이 같지 않으면 참을 반환함. |
!== | 왼쪽 피연산자와 오른쪽 피연산자의 값이 같지 않거나, 타입이 다르면 참을 반환함. |
> | 왼쪽 피연산자의 값이 오른쪽 피연산자의 값보다 크면 참을 반환함. |
>= | 왼쪽 피연산자의 값이 오른쪽 피연산자의 값보다 크거나 같으면 참을 반환함. |
< | 왼쪽 피연산자의 값이 오른쪽 피연산자의 값보다 작으면 참을 반환함. |
<= | 왼쪽 피연산자의 값이 오른쪽 피연산자의 값보다 작거나 같으면 참을 반환함. |
자바스크립트에서 비교 연산자는 피연산자의 타입에 따라 두 가지 기준으로 비교를 진행.
1. 피연산자가 둘 다 숫자면, 해당 숫자를 서로 비교.
2. 피연산자가 둘 다 문자열이면, 문자열의 첫 번째 문자부터 알파벳 순서대로 비교. 사전 앞에 있는 알파벳일수록 값이 작음.
ex) 가방 > 하마
2) 느슨한 타입(loosely typed)의 동적(dynamic) 언어의 문제점은 무엇이고 보완할 수 있는 방법은?
실행 도중에 변수에 예상치 못한 타입이 들어와 타입에러가 발생할 수 있음 동적타입 언어는 런타임 시 확인할 수 밖에 없기 때문에, 코드가 길고 복잡해질 경우 타입 에러를 찾기가 어렵다. 이러한 불편함을 해소하기 위해 TypeScipt나 Flow 등을 사용할 수 있다.
3) Undefined 와 null 의 차이점
undefined은 변수를 선언하고 값을 할당하지 않은 상태, null은 변수를 선언하고 빈 값을 할당한 상태(빈 객체)이다. 즉, undefined는 자료형이 없는 상태이다.
따라서 typeof를 통해 자료형을 확인해보면 null은 object로, undefined는 undefined가 출력되는 것을 확인할 수 있다.
4) 자바스크립트에서의 변수 선언 & 형 변환
자바스크립트는 타입이 매우 유연한 언어이다. 때문에 때로는 자바스크립트 엔진이 필요에 따라 ‘암시적변환’ 을 혹은 개발자의 의도에 따라 ‘명시적변환’ 을 실행한다.
암시적변환
암시적 변환이란 자바스크립트 엔진이 필요에 따라 자동으로 데이터타입을 변환시키는 것이다.
산술 연산자
더하기(+) 연산자는 숫자보다 문자열이 우선시 되기때문에, 숫자형이 문자형을 만나면 문자형으로 변환하여 연산된다. (문자 > 숫자)
// 더하기(+)number + number // number
number + string // string
string + string // string
string + boolean // string
number + boolean // number
50 + 50; //100
100 + “점”; //”100점”
“100” + “점”; //”100점”
“10” + false; //”100"
99 + true; //100
다른 연산자(-,*,/,%)는 숫자형이 문자형보다 우선시되기 때문에 더하기와 같은 문자형으로의 변환이 일어나지 않는다. (문자 < 숫자)
//다른 연산자(-,*,/,%)string * number // number
string * string // number
number * number // number
string * boolean //number
number * boolean //number
“2” * false; //0
2 * true; //2
동치 비교
느슨한 동치 연산자(==)
두 피연산자의 자료형을 일치시킨 후, 비교를 수행하는 연산자를 느슨한 동치 연산자라고 한다.
1 == 1 // true
"1" == 1 // true
1 == '1' // true
0 == false // true
0 == null // false
0 == undefined // false
null == undefined // true
위의 예시와 같이 느슨한 동치 연산자는 자료형을 일치 시킨 후 비교를 합니다.
엄격한 동치 연산자(===)
자료형 변환 없이 두 피연산자가 엄격히 같은지 판별하는 연산자를 엄격한 동치 연산자라고 한다.
3 === 3 // true
3 === '3' // false
위의 예시와 같이 엄격한 동치 연산자는 느슨한 연산자와 다르게 자료형 또한 같아야 한다.
명시적변환
명시적변환이란 개발자가 의도를 가지고 데이터타입을 변환시키는 것이다.
타입을 변경하는 기본적인 방법은 `Object(), Number(), String(), Boolean()` 와 같은 함수를 이용하는데 new 연산자가 없다면 사용한 함수는 타입을 변환하는 함수로써 사용된다.
var trans = 100; //NumberObject(trans); //100
console.log(typeof trans); //NumberString(trans); //”100"
console.log(typeof trans); //StringBoolean(trans); //true
console.log(typeof trans); //Bolean
A Type → Number Type
다른 자료형을 숫자타입으로 변형하는 방법은 아래와 같다.
Number()
Number()는 정수형과 실수형의 숫자로 변환한다.
Number(“12345”); //12345
Number(“2”*2); //4
parseInt()
parseInt()는 정수형의 숫자로 변환한다. 만약 문자열이 `숫자 0` 으로 시작하면 8진수로 인식하고(구형브라우저 O, 신형브라우저X), `0x, 0X` 로 시작한다면 해당 문자열을 16진수 숫자로 인식한다. 또한 앞부분 빈 공백을 두고 나오는 문자는 모두 무시되어 NaN을 반환한다.
parseInt(“27”) //27
parseInt(0033); //27
parseInt(0x1b); //27
parseInt(“ 2”); //2
parseInt(“ 2ㅎ”); //2
parseInt(“ ㅎ2”); //NaN
parseFloat()
parseFloat()는 부동 소수점의 숫자로 변환한다. parseInt()와는 달리 parseFloat()는 항상 10진수를 사용하며 parseFloat() 또한 앞부분 빈 공백을 두고 나오는 문자는 모두 무시되어 NaN을 반환한다.
parseFloat(“!123”); //NaN
parseFloat(“123.123456”); //123.123456
parseInt(“123.123456”); //123
parseFloat(“ 123.123456”); //123.123456
parseFloat(“ a123.123456”); //NaN
A Type → String Type
다른 자료형을 문자타입으로 변형하는 방법은 아래와 같다.
String()
String(123); //”123"
String(123.456); //”123.456"
toString()
toString()는 인자로 기수를 선택할 수 있다. 인자를 전달하지 않으면 10진수로 변환한다.
var trans = 100;trans.toString(); //”100"
trans.toString(2); //”1100100"
trans.toString(8); //”144"var boolT = true;
var boolF = false;boolT.toString(); //”true”
boolF.toString(); //”false”
toFixed()
toFixed()의 인자를 넣으면 인자값만큼 반올림하여 소수점을 표현하며 소수점을 넘치는 값이 인자로 들어오면 `0`으로 길이를 맞춘 문자열을 반환한다.
var trans = 123.456789;
var roundOff = 99.987654;trans.toFixed(); //”123"
trans.toFixed(0); //”123"
trans.toFixed(2); //”123.46"
trans.toFixed(8); //”123.45678900"roundOff.toFixed(2); //”99.99"
roundOff.toFixed(0); //”100"
A Type → Boolean Type
다른 자료형을 불린타입으로 변형하는 방법은 아래와 같다.
Boolean()
Boolean(100); //true
Boolean(“1”); //true
Boolean(true); //true
Boolean(Object); //true
Boolean([]); //true
Boolean(0); //false
Boolean(NaN); //false
Boolean(null); //false
Boolean(undefined); //false
Boolean( ); //false
5) JavaScript 객체와 불변성이란?
Immutability(변경불가성)는 객체가 생성된 이후 그 상태를 변경할 수 없는 디자인 패턴을 의미한다.
Immutability은 함수형 프로그래밍의 핵심 원리이다.
불변 객체를 사용하면 복제나 비교를 위한 조작을 단순화 할 수 있고 성능 개선에도 도움이 된다.
하지만 객체가 변경 가능한 데이터를 많이 가지고 있는 경우 오히려 부적절한 경우가 있다.
ES6에서는 불변 데이터 패턴(immutable data pattern)을 쉽게 구현할 수 있는 새로운 기능이 추가되었다.
6) 기본형 타입(Primitive Type) 데이터
기본형 타입의 종류에는 숫자, 문자열, 불리언, null, undefined, symbol.
일반적으로 기본형은 '할당이나 연산시 데이터가 복제'된다.
기본형 타입의 메모리 저장 방식
메모리 할당 시 두 영역을 사용한다고 생각하면 쉽다.
- 식별자가 할당되는 변수 영역
- 데이터 값이 담기는 데이터 영역
변수 영역에는 식별자와 데이터 영역의 주솟값으로 이루어져있음.
예시1) 변수 선언
var name = '홍길동';
변수 영역(1002)에 식별자(변수명)를 name으로 할당. 데이터 영역(5000)에 문자열 '홍길동' 데이터를 할당했음.
식별자 name의 변수 영역에 데이터 영역의 주솟값을 값에 연결. 사용자가 식별자 name을 호출했을때,
해당 변수 영역 값에 연결된 주솟값(5000)에 담긴 데이터가 반환.
예시2) 데이터 재할당
var name = '홍길동';
name = '고길동';
새로운 데이터이므로 비어있는 데이터 영역(5001)에 문자열 '고길동'을 할당.
식별자 name에 변수 영역에 해당하는 데이터 영역의 주솟값(@5000 -> @5001)으로 변경.
예시3) 변수에 변수를 대입
var name = '홍길동';
var name2 = name;
비어있는 변수 영역(1003)에 식별자 name2를 할당하고 name2의 데이터 주소에 name의 주솟값(5000)을 가져와 할당.
var name = '홍길동';
var name2 = name;
name2 = '고길동';
새로운 문자열 '고길동'을 비어있는 데이터 영역(5001)에 할당하고 식별자 name2의 주솟값을 변경해줌.
7) 참조형 타입(Reference Type)
참조형 타입의 종류는 객체, 배열, 함수, 날짜, 정규표현식, Map, WeakMap, Set, WeakSet이 있다.
참조형 타입의 메모리 저장 방식
예시1) 변수 선언
var person = {
name : '홍길동',
age : 123
};
기본형타입과 마찬가지로 변수 영역, 데이터 영역이 존재한다.
식별자 person 변수 영역에 데이터 영역 주솟값을 연결.
하지만 참조형타입은 영역이 하나 더 존재. '객체의 변수(프로퍼티) 영역'이다.
데이터 영역 주솟값에 데이터가 바로 할당되는 것이 아니라 객체의 프로퍼티 영역의 주솟 값(7000~7001)이 연결된다.
그리고 해당 객체의 프로퍼티 영역에 데이터 영역의 주솟값을 연결해준다.(name-5002 / age-5003)
위에서 데이터 영역은 불변하다고 말했던 것처럼 참조형 데이터도 데이터 영역은 불변하다고 할 수 있다.
하지만 기본형 타입은 불변성을 띄고 참조형 타입은 불변하지 않다(가변성)고들 말한다고 하는데, 그 이유는 다음과 같다
예시2) 참조형타입은 가변성을 띄는가
var person = {
name : '홍길동',
age : 123
};
person.name = '고길동';
식별자 person의 변수 영역의 값과 데이터 영역(5000)의 값이 변경되지 않았다. 하지만 객체의 프로퍼티 영역의 데이터 주솟값이 변경된 것이 확인 되는데, 이같은 부분을 보고 가변성을 띈다고 한다.
간단히 말해 person의 데이터 영역은 불변성을 띄지만, person의 객체의 프로퍼티 영역은 가변성을 띈다고 할 수 있다.
참조형 타입
var person = {
name : '홍길동'
};
var person2 = person;
식별자 person의 값을 person2에 대입하게되는 경우에 위와 같이 같은 데이터 영역의 주솟값을 가지게 됨.
이 부분은 기본형 타입과 참조형 타입 둘 다 동일.
두 타입의 차이는 데이터 영역, 객체의 프로퍼티 영역에서 확인할 수 있다.
아래와 같이 식별자 person2의 name 프로퍼티의 데이터를 수정하게 될 경우 생기는 변화에 대해 확인해보겠다.
var person = {
name : '홍길동'
};
var person2 = person;
person2.name = '고길동';
변수 영역의 주솟값이나 데이터 영역의 주솟값은 변경되지 않는다.
name 프로퍼티만 변경된 것이므로 해당 객체의 프로퍼티 영역에서 데이터 주솟값을 변경한다. 즉, 5002에서 5003으로 변경.
이때 해당 객체의 프로퍼티 영역의 주솟값만 변경하므로 변수 영역, 데이터 영역을 참조하고 똑같이 참조하고 있던 person역시 영향을 받는다.
var person = {
name : '홍길동'
};
var person2 = person;
person2.name = '고길동';
console.log(person.name); // '고길동'
8) 불변 객체를 만드는 간단한 방법
참조형 데이터는 기본형 데이터와 마찬가지로 데이터 자체를 변경하려고 한다면 데이터는 변하지 않는다. 하지만 참조형 데이터가 가변적이다라고 말하는 것은, 내부 프로퍼티를 변경할 때를 말한다.
만약 객체를 복사해서, 내부 프로퍼티를 변경하고 싶을 때, 복사한 객체를 변경하더라도, 원본 객체가 변하지 않아야 하는 경우가 생길 것이다. 이런 경우에 '불변 객체'가 필요하다. 불변 객체를 만들기 위해서는 다양한 방법을 활용할 수 있다.
내부 프로퍼티를 변경할 필요가 있을 때마다 매번 새로운 객체를 만들어 재할당하기로 규칙을 정하거나 자동으로 새로운 객체를 만드는 도구를 활용한다면 불변성을 확보할 수 있다. 혹은 불변성을 확보할 필요가 있을 경우에는 불변 객체로 취급하고, 그렇지 않은 경우에는 기존 방식대로 사용하는 식으로 상황에 따라 대처해도 된다. 그렇다면 불변 객체를 어떻게 만들 수 있는지 살펴보겠다.
let user = { name: "const", gender: "male" }; function changeName(user, newName) { let newUser = user; newUser.name = newName; return newUser; } let user2 = changeName(user, "epitone"); if (user !== user2) { console.log("유저 정보가 변경되었습니다."); } console.log(user.name, user2.name); // epitone epitone console.log(user === user2); // true
불변 객체를 만들기 전, 객체의 가변성으로 인해 어떤 문제가 나타날 수 있을지 알아보겠다. 첫 번째 줄에서 user 객체를 생성하고 user 객체의 name 프로퍼티를 epitone으로 바꿔주는 함수를 호출해서, 그 결과를 user2 변수에 할당했다. 이때 user, user2 변수 모두 name 프로퍼티가 'epitone'으로 출력되는 것을 볼 수 있다. 마지막 줄에서는 user와 user2가 서로 동일하다고 나온다. 만약 user2와 user가 프로퍼티가 바뀌더라도, 다른 객체가 되려면 어떻게 해야 할까?
let user = { name: "const", gender: "male" }; function changeName(user, newName) { return { name: newName, gender: user.gender }; } let user2 = changeName(user, "epitone"); if (user !== user2) { console.log("유저 정보가 변경되었습니다."); } console.log(user.name, user2.name); // const epitone console.log(user === user2); // false
changeName 함수가 정말 새로운 객체를 반환하도록 수정했다. 이렇게 된다면, user와 user2는 서로 다른 객체이므로 안전하게 변경 전과 후를 비교할 수 있다. 하지만 문제점이 있다. changeName 함수는 새로운 객체를 만들면서 변경할 필요가 없는 기존 객체의 프로퍼티(gender)를 하드코딩으로 입력했다. 지금은 gender 프로퍼티가 하나 있어서 쉬웠을 수 있지만, 만약 프로퍼티가 많은 객체였다면, 하드코딩의 양이 더욱 많아질 것이다. 이런 방식보다는 대상 객체의 프로퍼티 개수와 상관없이 모든 프로퍼티를 복사하는 함수를 만드는 편이 더 좋을 것입니다.
let user = { name: "const", gender: "male" }; function copyObject(target) { let result = {}; for(let prop in target) { result[prop] = target[prop]; } return result; } let user2 = copyObject(user); user2.name = 'epitone'; if (user !== user2) { console.log("유저 정보가 변경되었습니다."); } console.log(user.name, user2.name); // const epitone console.log(user === user2); // false
위에서 copyObject 함수를 만들었다. copyObject 함수는 for in 문법을 이용해 result 객체에 target 객체의 프로퍼티들을 복사하는 함수다. copyObject 함수를 활용해서 간단하게 객체를 복사하고 내용을 수정하는 데 성공했다. copyObject 함수는 프로토타입 체이닝 상의 모든 프로퍼티를 복사하는 점, getter/setter는 복사하지 않는 점, 얕은 복사만을 수행한다는 점에서 아쉽지만, 문제를 모두 보완하려면 함수가 무거워질 수밖에 없지만, user 객체에 대해서는 문제가 되지 않으므로 일단 진행해보겠다. copyObject 함수를 활용해서 객체를 만들었을 때, 가장 아쉬운 점은 이 함수는 '얕은 복사만을 수행한다'는 점이다. 그렇다면, 얕은 복사는 무엇이고, 깊은 복사는 또 무엇일까?
9) 얕은 복사 (shallow copy)
얕은 복사는 참조형 데이터가 저장된 프로퍼티를 복사할 때 그 주솟값만 복사하는 방법이다. 위에서 copyObject 함수는 얕은 복사만 수행했다. copyObject는 해당 프로퍼티에 대해 원본과 사본이 모두 동일한 참조형 데이터의 주소를 가리키게 된다.
얕은 복사에 대해 예를 들어 살펴보겠다.
const obj = { vaule: 1 } const newObj = obj; newObj.vaule = 2; console.log(obj.vaule); // 2 console.log(obj === newObj); // true
obj 변수에 object를 할당하고, newObj 변수에 obj 변수의 값을 할당했다. 그리고 newObj 프로퍼티인 value 값을 2로 설정하고, obj.value를 콘솔에 출력하면, 2로 변경된 것을 볼 수 있다. 왜냐면, 얕은 복사 때문에, 사본의 데이터를 변경하더라도, 동일한 참조형 데이터 주소를 가리키고 있기에, 원본의 데이터도 변경되는 것이다. 그렇다면, 깊은 복사를 사용하려면 어떻게 해야 할까?
10) 깊은 복사 (deep copy)
깊은 복사는 내부의 모든 값들을 하나하나 찾아서 전부 복사하는 방법이다. 깊은 복사에 대해 예를 들어 살펴보겠다.
let a = 1;
let b = a;
b = 2;
console.log(a); // 1
console.log(b); // 2
console.log(a === b); // false
만약 변수 a의 값으로 1을 할당하고, 변수 b에 a를 할당했다. 그리고 변수 b에 2를 재할당하고, a와 b를 출력해보면, a는 1, b는 2가 출력된다. 자바스크립트에서 원시 타입은 깊은 복사가 진행된다. 그렇다면, 원시 타입이 아닌, 객체에서 깊은 복사는 어떻게 이뤄지는지 알아보겠다. 객체의 깊은 복사에는 다양한 방법이 있다.
Object.assign()
Object.assign(생성할 객체, 복사할 객체) 메서드는 첫 번째 인수로 빈 객체를 넣어주며, 두 번째 인수로 할당할 객체를 넣으면 된다.
const obj = { a: 1 };
const newObj = Object.assign({}, obj);
newObj.a = 2;
console.log(obj); // { a: 1 }
console.log(obj === newObj); // false
새로운 newObj 객체를 Object.assign() 메서드를 사용해서 생성했고, newObj.a 값을 변경해도 기존의 obj는 변하지 않았다. 객체 간의 비교를 해도, 서로 참조값이 다르기 때문에 false가 나온다. 하지만 Object.assign에서의 문제는 2차원 객체의 경우 깊은 복사가 이뤄지지 않는다는 점이다.
const obj = {
a: 1,
b: {
c: 2,
},
};
const newObj = Object.assign({}, obj);
newObj.b.c = 3;
console.log(obj); // { a: 1, b: { c: 3 } }
console.log(obj.b.c === newObj.b.c); // true
만약 obj 변수에 b 객체가 있다고 가정했을 때, 2차원 객체를 newObj에 복사하고, newObj.b.c의 값을 변경했다. 그리고 obj 변수를 출력해보면, c의 값이 3이 된 것을 확인할 수 있다. 중복 객체의 경우 Object.assign() 메서드는 중복 객체를 깊은 복사를 하지 않는다는 한계가 있다. 이 문제는 전개 연산자(Spread Operator)를 활용할 경우에도 발생한다.
11) 호이스팅
함수 안에 있는 선언들을 모두 끌어 올려서 해당 함수 유효 스코프의 최상단에 선언 하는 것을 말한다. (var 선언문이나 function 선언문 등을 해당 스코프의 선두로 옮긴 것처럼 동작하는 특성)
호이스팅의 대상
var 변수 선언과 함수선언문에서만 호이스팅이 일어난다.
var 변수/함수의 선언만 위로 끌어 올려지며, 할당은 끌어 올려지지 않는다. let/const 변수 선언과 함수표현식에서는 호이스팅이 발생하지 않는다. var , let , const 키워드로 선언된 변수는 선언 부분만 끌어올려진다고 생각할 수 있다. 따라서 var 의 경우에는 변수 선언전에도 참조할 수 있지만 할당을 하지 않았기에 undefined 이다.
let/ const 가 호이스팅이 발생하지 않는 이유
식별자가 호이스팅 후 실제 코드에서 선언되기 전까지 TDZ(Temporal Dead Zone/일시적 사각지대)에 있기 때문에 let, const 선언코드가 있는 곳 이전에는 해당 변수에 참조할 수 없게 된다.
12) Temporal Dead Zone(TDZ)
참조 오류가 나는 구간인 스코프 시작지점부터 초기화 지점까지의 구간.
초기화가 되기 전까지는 TDZ라는 곳에 머물러서 초기화(혹은 할당)이 될 때까지 잠시 '죽어있는 상태'이기 때문에 선언 전에 참조가 불가능한 것. 선언 전에 변수를 사용하는 것을 비허용하는 개념상의 공간이다.
console.log(a); // undefined
f1(); // undefined
console.log(f2) // undefined
f2(); // TypeError: f2 is not a function
function f1(){
console.log(b);
var b = 5;
}
var f2 = function () {
console.log(c);
var c = 7;
}
var a = 10;
코드 해석
var a; //초기화 된 전역변수
function f1(){
var b; //초기화 된 지역변수
console.log(b);
b = 5;
}
var f2;
console.log(a);
f1();
console.log(f2);
f2();
f2 = function f2() {
var c; //초기화된 지역변수
console.log(c);
c = 7;
}
a = 10;
- 지역변수 : Block{}안에서 선언된 변수. Block 안에서만 쓸 수 있음.
- 전역변수 : Block{}밖에서 선언을 한 어디서든 쓰일 수 있는 변수
호이스팅 시 function 안에서 쓴 것 빼고는 전부 전역변수라고 생각하면 된다!
참고로 const , let 말고도 class 구문과 클래스의 constructor() 내부의 super(), 기본 함수 매개변수도 TDZ 제한이 있다. 반면 var , function 구문은 TDZ에 영향을 받지 않으며 현재 스코프에서 호이스팅이 된다.
13) 스코프(Scope) : 변수나 함수에 접근할 수 있는 위치를 말한다.
function foo() {
var x;
}
위의 예시에서 x의 스코프는 함수 foo() 이다.
어휘적 스코프
정적 스코프(Static scope), 렉시컬 스코프(Lexical scope)라고도 불리며, 프로그램을 실행하지 않고 소스 코드에 존재하는대로 해석한 스코프이다.
변수 스코프
어휘적(정적)으로 지정되며, 즉 프로그램의 정적 구조를 보면 변수의 스코프를 판단할 수 있고 함수를 어디서 호출했는지 등에 영향을 받지 않는다.
또한 변수 스코프는 함수이며, 함수만이 새 스코프를 도입할 수 있다.
중첩 스코프
스코프가 변수의 스코프 안에 중첩되어 있으면 그 변수는 해당 스코프 전체에서 접근할 수 있다.
ex)
function foo(arg){
function bar(){ //중첩 스코프 bar()
console.log(`Hi, ${arg}`);
}
bar();
}
foo("jacob"); //Hi, jacob
Shadowing
: 내부 스코프에서 외부 스코프에 있는 변수와 이름이 같은 변수를 선언하면, 내부 스코프와 그 안에 중첩된 모든 스코프는 외부 스코프의 이름이 같은 변수에 접근할 수 있다.
내부 변수가 바뀌어도 외부 변수는 바뀌지 않으며, 내부 스코프에서 빠져나가면 다시 접근할 수 있다.
ex)
var x = "global";
function f(){
var x = "local";
console.log(x); // local
}
f(); // local
console.log(x); // global
javascript 코드를 보면 다음과 같은 경우를 보았을 것이다.
function getName() {
console.log('name');
}
var name = function() {
console.log('name');
};
javascript 에서 함수를 변수에 담을 수 있다. 이렇게 사용하는 것을 함수 표현식 이라고 한다.
그리고 function getName() 과 같이 함수를 선언하는 것을 함수 선언문이라고 한다.
14) 함수 선언식과 표현식의 차이점
함수 선언식은 호이스팅에 영향을 받지만, 함수 표현식은 호이스팅에 영향을 받지 않는다.
함수 선언식은 코드를 구현한 위치와 관계없이 자바스크립트의 특징인 호이스팅에 따라 브라우저가 자바스크립트를 해석할 때 맨 위로 끌어 올려진다.
예를 들어, 아래의 코드를 실행할 때
// 실행 전
logMessage();
sumNumbers();
function logMessage() {
return 'worked';
}
var sumNumbers = function () {
return 10 + 20;
};
호이스팅에 의해 자바스크립트 해석기는 코드를 아래와 같이 인식한다.
// 실행 시
function logMessage() {
return 'worked';
}
var sumNumbers;
logMessage(); // 'worked'
sumNumbers(); // Uncaught TypeError: sumNumbers is not a function
sumNumbers = function () {
return 10 + 20;
};
위 코드 결과는 아래와 같다.
함수 표현식 sumNumbers 에서 var 도 호이스팅이 적용되어 위치가 상단으로 끌어올려졌다.
var sumNumbers;
logMessage();
sumNumbers();
하지만 실제 sumNumbers 에 할당될 function 로직은 호출된 이후에 선언되므로, sumNumbers 는 함수로 인식하지 않고 변수로 인식한다.
호이스팅을 제대로 모르더라도 함수와 변수를 가급적 코드 상단부에서 선언하면, 호이스팅으로 인한 스코프 꼬임 현상은 방지할 수 있다.
함수 표현식의 장점
‘함수 표현식이 호이스팅에 영향을 받지 않는다’ 는 특징 이외에도 함수 선언식보다 유용하게 쓰이는 경우는 다음과 같다.
- 클로져로 사용
- 콜백으로 사용 (다른 함수의 인자로 넘길 수 있음)
함수 표현식으로 클로져 생성하기
클로져는 함수를 실행하기 전에 해당 함수에 변수를 넘기고 싶을 때 사용된다. 더 쉽게 이해하기 위해 아래 예제를 살펴보자.
function tabsHandler(index) {
return function tabClickEvent(event) {
// 바깥 함수인 tabsHandler() 의 index 인자를 여기서 접근할 수 있다.
console.log(index); // 탭을 클릭할 때 마다 해당 탭의 index 값을 표시
};
}
var tabs = document.querySelectorAll('.tab');
var i;
for (i = 0; i < tabs.length; i += 1) {
tabs[i].onclick = tabsHandler(i);
}
위 예제는 모든 .tab 요소에 클릭 이벤트를 추가하는 예제다. 주목할 점은 클로져를 이용해 tabClickEvent() 에서 바깥 함수 tabsHandler() 의 인자 값 index 를 접근했다는 점이다.
function tabsHandler(index) {
return function tabClickEvent(event) {
console.log(index);
};
}
for 반복문의 실행이 끝난 후, 사용자가 tab 을 클릭했을 때 tabClickEvent() 가 실행된다. 만약 클로져를 쓰지 않았다면 모든 tab 의 index 값이 for 반복문의 마지막 값인 tabs.length 와 같다.
for (i = 0; i < tabs.length; i += 1) {
tabs[i].onclick = tabsHandler(i);
}
클로져를 쓰지 않은 예제를 보자.
var tabs = document.querySelectorAll('.tab');
var i;
for (i = 0; i < tabs.length; i += 1) {
tabs[i].onclick = function (event) {
console.log(i); // 어느 탭을 클릭해도 항상 tabs.length (i 의 최종 값) 이 출력
};
}
위 소스는 탭이 3개라고 했을 때, 어느 탭을 클릭해도 i 는 for 반복문의 최종 값인 3이 찍힌다.
문제점을 더 파악하기 쉽게 for 문 안의 function() 을 밖으로 꺼내서 선언해보면
var tabs = document.querySelectorAll('.tab');
var i;
var logIndex = function (event) {
console.log(i); // 3
};
for (i = 0; i < tabs.length; i += 1) {
tabs[i].onclick = logIndex;
}
logIndex 가 실행되는 시점은 이미 for 문의 실행이 모두 끝난 시점이다. 따라서, 어느 탭을 눌러도 for 문의 최종 값인 3이 찍힌다.
이 문제점을 해결하기 위해 클로져를 적용하면
function tabsHandler(index) {
return function tabClickEvent(event) {
// 바깥 함수인 tabsHandler 의 index 인자를 여기서 접근할 수 있다.
console.log(index); // 탭을 클릭할 때 마다 해당 탭의 index 값을 표시
};
}
var tabs = document.querySelectorAll('.tab');
var i;
for (i = 0; i < tabs.length; i += 1) {
tabs[i].onclick = tabsHandler(i);
}
for 반복문이 수행될 때 각 i 값을 tabsHandler() 에 넘기고, 클로져인 tabClickEvent() 에서 tabsHandler() 의 인자 값 index 를 접근할 수 있게 된다. 따라서, 우리가 원하는 각 탭의 index 를 접근할 수 있다.
함수 표현식을 다른 함수의 인자 값으로 넘기기
함수 표현식은 일반적으로 임시 변수에 저장하여 사용한다.
// doSth 이라는 임시 변수를 사용
var doSth = function () {
// ...
};
함수 표현식을 임시 변수에 넣지 않고도 아래와 같이 콜백함수로 사용할 수 있다.
$(document).ready(function () {
console.log('An anonymous function'); // 'An anonymous function'
});
jQuery 를 사용할 때 많이 보던 문법으로 위와 아래의 코드 결과는 같다.
var logMessage = function () {
console.log('An anonymous function');
};
$(document).ready(logMessage); // 'An anonymous function'
자바스크립트 내장 API 인 forEach() 를 사용할 때도 콜백함수를 사용할 수 있다.
var arr = ["a", "b", "c"];
arr.forEach(function () {
// ...
});
15) Execution Context(실행 컨텍스트)란?
자바스크립트 엔진이 코드를 실행하기 위해선 코드에 대한 정보들이 필요합니다. 코드에 선언된 변수와 함수, 스코프, this, arguments 등을 묶어, 코드가 실행되는 위치를 설명한다는 뜻의 Execution Context라고 부릅니다. 자바스크립트 엔진은 Execution Context를 객체로 관리하며 코드를 Execution Context 내에서 실행합니다.
Execution Context의 종류
실행 컨텍스트의 종류는 아래의 세 가지로 나누어집니다.
1. Global Execution Context
코드를 실행하며 단 한 개만 정의되는 전역 Context입니다. global object를 생성하며 this 값에 global object를 참조합니다. 전역 실행 컨텍스트는 Call Stack에 가장 먼저 추가되며 앱이 종료 될 때 삭제됩니다.
2. Functional Execution Context
함수가 실행 될 때 마다 정의되는 Context입니다. 전역 실행 컨텍스트가 단 한 번만 정의되는 것과 달리, 함수 실행 컨텍스트는 매 실행시마다 정의되며 함수 실행이 종료(return)되면 Call Stack에서 제거됩니다.
3. Eval Context
eval 함수로 실행한 코드의 Context입니다. 보안상 취약한 점이 있어 비권장 함수이기 때문에 이 글에서는 다루지 않습니다.
Execution Context의 관리: CallStack
js 엔진은 생성된 Context를 관리하는 목적의 Call Stack(호출스택)을 갖고 있습니다. JS는 단일 스레드 형식이기 때문에 런타임에 단 하나의 Call Stack이 존재합니다. js 엔진은 전역 범위의 코드를 실행하며 Global Execution Context를 생성해 stack에 push합니다. 그리고 함수가 실행 또는 종료 될 때마다 Global Execution Context의 위로 Functional Execution Context stack을 push(추가), pop(제거)합니다.
Call Stack은 최대 stack 사이즈가 정해져있습니다. Call Stack에 쌓인 Context Stack이 최대치를 넘게 될 경우 ‘RangeError: Maximum call stack size exceeded’라는 에러가 발생합니다. 이 에러는 Stack Overflow라고 부르기도 합니다.
- 코드의 전역 범위가 실행되며 Global Execution Context를 push합니다.
- fn1이 실행됩니다.
- fn1의 Functional Execution Context가 Call Stack에 push됩니다.
- fn2이 실행됩니다.
- fn2의 Functional Execution Context가 Call Stack에 push됩니다.
- console.log가 실행됩니다.
- console.log의 Functional Execution Context가 Call Stack에 push 됩니다.
- console.log의 실행이 완료되며 console.log의 Functional Execution Context가 pop됩니다.
- fn2의 실행이 완료되며 fn2의 Functional Execution Context가 pop됩니다.
- fn1의 실행이 완료되며 fn1의 Functional Execution Context가 pop됩니다.
- 앱 종료 시 Global Execution Context가 pop됩니다.
16) 스코프 체인
함수는 전역에서 정의할 수도 있고 함수 몸체 내부에서도 할 수 있다. 함수 몸체 내부에서 함수가 정의된 것을 '함수의 중첩', 중첩 함수를 포함하는 함수를 '외부 함수'라고 한다.
함수는 중첩될 수 있으므로 함수의 지역 스코프도 중첩될 수 있는데, 이는 스코프가 함수의 중첩에 의해 계층적 구조를 갖는다는 것을 의미한다. 이때 외부 함수의 지역 스코프를 중첩 함수의 상위 스코프라고 지칭한다.
var x = "global x";
var y = "global y";
function outer (){
var z = "outer's local z";
console.log(x); //global x
console.log(y); //global y
console.log(z); //outer's local z
function inner (){
var x = "inner's local x";
console.log(x); //inner's local x
console.log(y); //global y
console.log(z); //outer's local z
}
inner();
}
outer();
console.log(x); //global x
console.log(z); //ReferenceError: z is not defined
위의 예제 코드는 outer 함수가 만든 지역 스코프 내에 inner 함수가 만든 지역 스코프가 있으므로 outer 스코프가 inner 스코프의 상위 스코프이다. 그리고 outer 함수의 지역 스코프의 상위 스코프는 전역 스코프이다.
이처럼 모든 스코프는 하나의 계층적 구조로 연결되며, 모든 지역 스코프의 최상위 스코프는 전역 스코프이다.
스코프 체인에 의한 변수 검색
변수를 참조할 때 자바스크립트 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작하여 참조할 변수가 존재하지 않는다면 상위 스코프의 방향으로 이동하며 선언된 변수를 검색한다. 이를 통해 상위 스코프에서 선언한 변수를 하위 스코프에서 참조할 수 있다. 반대로 하위 스코프에서 유효한 변수는 상위 스코프에서 참조할 수 없다.
17) 캡슐화와 정보 은닉
캡슐화는 객체의 상태를 나타내는 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것을 말한다. 캡슐화는 객체의 특징 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 정보 은닉이라 한다.
정보 은닉은 적절치 못한 접근으로부터 객체의 상태가 변경되는 것을 방지해 정보를 보호한다.
대부분의 객체지향 프로그래밍 언어는 클래스를 정의하고 그 클래스를 구성하는 멤버에 public, private, protected 같은 접근 제한자를 선언하여 공개 범위를 한정할 수 있다. 하지만 자바스크립트는 접근 제한자를 제공하지 않는다.
실습 과제
let b = 1;
let a = 2;
function hi() {
const a = 1;
let b = 100;
b + +;
console.log(a, b); // function hi() 실행시 1, 101로 찍힘.
}
console.log(a); // 앞에 주석이 달려있으면 콘솔 안찍힘.a 값도 없어서 위에 let a = 2;
console.log(b); // 맨 위에 b = 1; 이 찍힘.
hi(); // funtion hi()를 실행해줘
console.log(b); // 맨 위에 b = 1;이 찍힘.
Reference
- https://velog.io/@filoscoder/-%EC%99%80-%EC%9D%98-%EC%B0%A8%EC%9D%B4-oak1091tes#-%EC%99%80-%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90
https://developer.mozilla.org/ko/docs/Web/JavaScript/Equality_comparisons_and_sameness
https://joshua1988.github.io/web-development/javascript/function-expressions-vs-declarations/
'Sparta x 이노베이션 캠프 > JavaScript' 카테고리의 다른 글
TIL) 논리 연산자 || OR (0) | 2022.08.30 |
---|---|
TIL : JavaScript 대괄호, 중괄호, 소괄호 사용 정리 (0) | 2022.08.25 |
TIL : JavaScript filter() (0) | 2022.08.20 |
TIL: JavaScript 구조분해할당 (1) | 2022.08.20 |
WIL : JavaScript의 ES란?, ES5/ES6 문법 차이 - 2022.08.15 (0) | 2022.08.15 |