스코프에 관해서
# 스코프란 무엇일까?
{} //블록레벨 스코프?
function fuc(){} //함수레벨 스코프?
스코프를 대충 중괄호다 라고 생각하는 사람들도 있을거다.
스코프에 대해서 어떤것인지 정확히 생각해보자.
# 스코프란 식별자가 유효한 범위다.
// --우주어딘가(전역)
{ // --집밖
var dog = "무찌";
{ // -- 우리집
{ // -- 무찌전용침대 다이소5천원짜리
}
}
}
얜왜 테두리 검은색이냐
// --우주어딘가(전역)
{ // --집밖
let dog = "무찌";
{ // -- 우리집
{ // -- 무찌전용침대 다이소5천원짜리
}
}
}
요렇게 var이나 let으로 선언한 dog이라는 변수에 값으로 무찌가 바인딩 되어있다.
그리고 블록문을 중첩해서 써놨다. 대충 이름붙여서 우주/집밖/우리집/침대로 이름을 붙여봤다.
그럼 우리 귀여운 무찌는 어딜 싸돌아다니고 있을까?
위 코드에서 console.log(dog)으로 식별자 dog를 참조할때 식별자를 찾는 범위를 스코프라고 하면된다.
즉 블록문은 스코프를 만드는 하나의 문법일뿐 스코프는 따로 존재하는 규칙?같은거다.
암튼 위 코드를 그림으로 그려보자면
집밖에서 무찌가 dog라는 식별자에 무찌가 할당되어있다.
그럼 스코프는 어떻게될까?
- var로 선언되었을때
그림에서 빨간박스가 스코프라고 보면된다.
저 범위내에서 console.log(dog)로 참조할때 에러없이 동작하는 범위다.
사실 뒤에 저 블록문 범위그림자체를 없애고싶었지만 헷갈릴까봐 같이그렸다
https://jacobowl.tistory.com/158
전위연산자 & 후위연산자 (+엘리스 토끼굴에 빠진 var)
사실 이건 중요한건 아닌데 모던 js 읽을때 헷갈렸던 부분이다. # 후위연산자 let num = 1; console.log(num++, " #A"); // 1 #A console.log(num, " #B"); // 2 #B 일단 num에 1을 할당하고 여기에 후위연산자를 붙여서
jacobowl.tistory.com
이전에 이 포스팅에서 var로 선언한 변수는 블록레벨스코프를 무시한다고 하였다.
- let으로 선언되었을떄
반면에 let으로 선언한 변수는 스코프가 이렇게 정해진다.
console.log(dog)으로 dog라는 식별자를 탐색할때 저 빨간박스 범위외에서 참조를 하려하면 참조에러가 뜬다.
# 스코프는 식별자를 검색하는 규칙이다.
identifier resolution이라고 한단다.
즉 변수든 함수든 선언/정의할때 어느 위치에 있느냐에 따라서 정해지는 이름공간(네임스페이스)이다.
그럼 저 console.log로 참조를 하려는 코드를 작성했을때 어느 스코프에 있느냐에 따라서 어떻게 작동할까?
{
var dog = "똥개";
{
console.log(dog); //똥개 출력
{
var dog = "무찌";
} //무찌전용침대
} //집
} //집밖
이 코드를 그림으로 표현해보면
이렇게 생겼는데 console.log로 출력해보면 '똥개'를 출력한다.
언뜻보면 스코프체인을 잘 따라서 출력한것같지만 이건 틀렸다.
var로 선언한 변수는 블록레벨스코프를 무시한다 했으니 저 배경들을 지워보겠다.
이제 조금 이해가 될 수있다. var로 선언한 dog식별자가 참조될수있는 범위는 전체다.
즉 저 코드들은 위에서 아래로 순차대로 그냥 실행되어 재할당으로 dog = '무찌'코드가 실행되기전에 참조를 했으니 똥개 가져온거다.
콘솔로그를 위치바꿔보면 이렇다.
{
var dog = "똥개";
{
{
var dog = "무찌";
} //무찌전용침대
console.log(dog); //무찌 출력
} //집
} //집밖
이걸 그림으로 표현해보면
그냥 스코프내에 저 코드들이 있다고 생각하고 순차대로 실행해버리는거다.
var dog = '무찌'는 그냥 재할당문으로 실행이 된거다.
그럼 let으로 선언한 변수는 어떻게 될까?
# let은 블록레벨 스코프도 지원한다.
{
var dog = "똥개";
{
console.log(dog); //똥개 출력
{
var dog = "무찌";
} //무찌전용침대
} //집
} //집밖
위 코드는 그림으로 이렇게 생겼다.
console.log(dog)로 dog식별자를 참조하는코드는 파란색 무찌전용침대 스코프 밖에있다.
때문에 console.log(dog)로 똥개를 출력하는거다.
{
let dog = "똥개";
{
{
let dog = "무찌";
} //무찌전용침대
console.log(dog); //똥개 출력
} //집
} //집밖
그렇다면 이렇게 아까 var처럼 콘솔로 참조하는 코드를 위치 바꾸면 어떻게 될까?
코드가 저렇게 아래에 갔다고 하더라도 dog='무찌'로 선언되어있는 무찌전용침대 스코프 밖에서 참조하려고 한다.
따라서 dog = '무찌'는 참조할수 없고 범위에 있는 '똥개'를 출력하게된다.
이는 스코프체인을 통해서 탐색을 했기때문이다.
# 스코프 범위가 중복되는경우는? (스코프체인)
{
let dog = "똥개";
{
let dog = "무찌";
{
console.log(dog); //요건 뭘까?
} //무찌전용침대
} //집
} //집밖
위 코드를 스코프 그림으로 표현하면 이렇다.
console.log(dog)라는 코드를 무찌전용침대 블록문 내에 적어서 dog를 참조하려고한다.
하지만 어라? 이곳에서는 dog='똥개'랑 dog='무찌'가 둘다있는 스코프다.
그럼 이 console.log(dog)코드는 어떤 dog를 참조할까?
여기서 스코프체인이라는 개념을 알아야한다.
# 스코프체인의 동작규칙
글자가 ㅈㄹ 작아서 좀 사진을 키웠다.
console.log(dog)라는 코드는 일단 무찌침대블록스코프안에 있다.
근데 여기에선 dog변수를 선언하지 않았다.
그러면 자신의 스코프를 감싸고있는 다음 외부 스코프로 나가서 탐색을 시작한다.
그리고 내집 스코프에서 dog식별자가 선언되어있는지 찾는다. 여기에선 let dog = '무찌'로 dog변수가 선언되어있다.
때문에 더이상 외부스코프로 나가지 않고 dog = '무찌'를 참조하고 식별자검색(identifier resolution)을 종료한다.
# 그럼 이 경우는 어떻게 될까?
let dog = "똥개";
{
{
console.log(dog); //요건 뭘까?
{
let dog = "무찌";
} //무찌전용침대
} //집
} //집밖
//우주 어딘가..
위 코드는 내집 중괄호에서 dog식별자를 참조하고 우주어딘가 블록문에서 let dog = '똥개'를, 무찌전용침대에서 let dog = '무찌'를 선언 하였다. 이를 스코프그림으로 표현하면 이렇다.
내집에서 console.log(dog)로 식별자 dog를 참조하려하기때문에 애초에 무찌스코프는 접근조차 안한다.
그리고 이 코드에서 식별자검색을 하는 순서를 그림으로 표현하면 아래와 같다.
먼저 참조하려는 코드가있는 내집 스코프를 탐색한다, 그리고 찾지못하였기때문에 집밖으로 나가서 또 찾는다.
그 이후에 또 못찾았으니 우주어딘가(전역)로 나가서 찾는다.
스코프체인은 절대 하위스코프(그림에서 무찌전용침대)로는 검색하지 않는다. 스코프체인은 무조껀 상위스코프로 이동하면서 이뤄진다.
# 식별자는 스코프내에서 유일해야한다.
식별자가 스코프내에서 유일해야하다는 개념을 예시로 들어보자.
{
let dog = "똥개";
{
{
let dog = "무찌";
let dog = "도리"; // syntax에러!!
} //무찌전용침대
} //집
} //집밖
//우주 어딘가..
위 코드는 무찌도리침대 블록문에서 dog식별자를 두번 선언해줬다.
이는 구문 에러를 내뿜는다.
{
let dog = "똥개";
{
let dog = "무찌";
let dog = "도리"; // syntax에러!!
{
let dog = "무찌";
} //무찌전용침대
} //집
} //집밖
//우주 어딘가..
이렇게 코드를 써봐도 내집 블록문안/무찌전용침대블록문밖에서 dog변수를 두번 선언했을때도 구문에러를 내뿜는다.
사실 deep dive에서 논리적인 오류를 만들어낸것 같은데.. 스코프를 식별자가 존재하는 범위라고 하는데 한 스코프내에서 여러개의 식별자가 존재하지 말아야한다는걸 정확히는 다음 챕터에서 설명하듯이 표현해야할것같다.
# 스코프의 정확한 그림
{
let dog = "똥개";
{
{
let dog = "무찌";
} //무찌전용침대
} //집
} //집밖
//우주 어딘가..
위 코드를 우리가 지금까지 이해한 스코프그림으로 다시 나타내보면..
지금까지 이렇다고 표현했었다.. 하지만 이 그림은 위에서도 썼듯이 deep dive의 논리적 오류같다.
왜냐하면 하나의 식별자는 하나의 스코프에서 유일해야한다고 하는데 저 그림에선 dog = '무찌라는 식별자가 무찌전용침대스코프내에서 dog = '똥개'라는 식별자랑 동시에 존재하기떄문이다..
스코프내에선 식별자가 유일해야 한다라는 개념으로 스코프를 다시 그려보면
이렇게 되야할것같다. 첫번째 그림에서의 집밖스코프에서 무찌전용침대 스코프의 여집합을 똥개를 참조할수있는 스코프라고 본것이다.
이그림으로 다시보면 스코프체인개념과 스코프라는 개념도 정확해진다.
암튼 무찌는 자기가 침대에 있을때 도리가와서 앵기면 짜증오지게낸다..
다만 지가 침대에없을떄는 도리가 침대에 있어도 신경안쓴다.
근데 이건 침대에만 국한되는거라 스코프개념 떠올릴때 무찌를 떠올리지말길..
암튼 이러한것때문에 스코프는 네임스페이스라고 하기도 한단다.
# 호이스팅은 스코프단위로 동작한다.
중복함수를 써보자
function outfuc() {
var value = "바보";
function insidefuc() {
console.log(value);
var value = "멍청이";
}
insidefuc(); //요기서 출력이 뭘까?
}
outfuc();
var로 선언한 변수는 함수레벨스코프를 갖는다고 했다.
코드처럼 중복함수를 써봤는데 insidefuc에서 value를 참조하는 코드가 실행되면 var value = '멍청이'로 변수를 선언하기 이전에 콘솔을 쳤고 이미 외부스코프에 var value = '바보'로 식별자 선언할당이 끝났기때문에 출력값은 '바보'가 뜰거라고 생각한다.
하지만 출력결과값은?
undefined가 뜬다
이는 스코프 단위로 호이스팅이 일어나기 때문이다.
function outfuc() {
var value = "바보";
function insidefuc() {
console.log(value);
}
insidefuc(); //'바보'를 출력함
}
outfuc();
만약 var value = '멍청이'라는 코드가 없다면 스코프체인이 일어나서 상위스코프에서 var value = '바보'로 선언할당한 value값을 검색해서 가져와줘야할것이다.
let value = "바보";
{
console.log(value);
let value = "멍청이";
}
블록레벨 스코프를 지원하는 let으로도 이렇게 호이스팅 범위를 만들 수 있다.
상위스코프의 바보라는 value를 참조하지않고 본인 스코프의 멍청이라는 value를 참조하려는데 호이스팅이 일어난다.
# 함수몸체내의 참조는 스코프를 어떻게 결정할까? (렉시컬스코프)
let value = "바보";
function fuc() {
let value = "멍청이";
밸류참조해줘();
}
function 밸류참조해줘() {
console.log(value);
}
fuc();
저 코드를 잘 보면 전역에서 value를 '바보'로 선언/할당해줬고 fuc함수내에서 value를 '멍청이'로 선언/할당했다.
fuc내에선 밸류참조해줘 함수가 호출되는데 과연 이 함수몸체의 console.log는 어떤 value를 참조할까?
결과는 전역변수value의 값인 '바보'를 참조한다.
만약 호출시점에서 참조값을 찾는다면 fuc함수내의 value = '멍청이'를 참조할텐데 바보를 가져온다.
함수몸체내에서 참조는 함수정의가 어디에 있냐에따라서 참조할 스코프를 결정한단다. 함수내에서 상위스코프는 코드상에서 자신의 정의된곳 위치기준으로 결정된단다. 이걸 렉시컬스코프라고 한다.
# 내가 생각했던 오류
나는 함수정의시점에 함수객체가 메모리에 할당되면서 여기엔 함수몸체의 정보도 정해져있으니 이 시점에 몸체내에서 일어나는 참조도 전부 정해지지 않을까?라고 생각했었다. 이 뇌피셜? 논리로 생각한다면 저 렉시컬스코프 예제코드도 설명이 잘된다.
하지만 이건 틀렸다.
https://jacobowl.tistory.com/175
선언문에서 let var const를 빼고 선언한다면?
# 일반적인 선언문 let value = "바보"; console.log(value); 일반적인 선언문은 이렇게 생겼다. 그런데 자바스크립트는 ㅈ같이도 아래와 같은 코드도 동작한다. # let var const없이 선언한 변수 value = "바보";
jacobowl.tistory.com
이전 포스팅에서
function fuc() {
value = "바보";
}
fuc();
console.log(value); //value값 잘 참조해준다.
이 코드를 보면 fuc이라는 함수객체가 이미 메모리에 값으로 저장되고 함수몸체내의 참조코드가 모두 참조할 식별자들을 결정을 해놨다면, fuc함수내에 value라는 식별자의 참조는 없다.
그러나 실제로는 런타임 시점에서 함수호출과 함께 value='바보'라는 재할당문을 실행하려 스코프를 탐색하고 최종적으로 못찾았기때문에 value = '바보'라고 메모리에 값으로 저장을 하게된다.
쉽게 생각해서 선언문으로 정의한 함수는 함수객체가 메모리에 런타임이전에 저장되지만 함수몸체내의 식별자들의 참조할 메모리주소들은 다 정해진게 아니란거다. 상위스코프는 결정된걸지 몰라도 함수호출이 되고나서 함수몸체내의 식별자들 참조는 스코프를 이동해가며 값들을 찾는다는거다. 틀릴수도 있다. 하지만 함수몸체내의 식별자들의 메모리값들이 모두 런타임이전에 함수객체에 저장되지 않는건 확실하다.
이것도 실행컨텍스트를 정확히 알아야 알 수 있을것같다.
# 헷갈리는점..
{
{
var value = "멍청이";
}
let value = "바보";
} // 얘는 에러뜸 변수중복선언.
얘는 변수중복선언이라고 구문오류띄워준다.
{
var value = "멍청이";
{
let value = "바보";
}
} // 얘는 에러안뜸
얘는 에러가 안뜬다.
사실 코드순서상으로 생각하면 뭐 쉽게 예상되는 오류지만.. 스코프생성시점과 같이 생각하면 좀 헷갈린다..
var로 선언한 변수가 처음부터 블록문쌩까고 전역스코프로 결정이된다고하면 첫번째 오류생기는 코드는 전역스코프에 멍청이가 생기고 지역스코프로 let으로 선언한 바보가 생긴다.
그래서 스코프가 겹치는거라고 보고 이걸 에러생기는 이유라고 생각한다면 두번째코드도 let선언 상위스코프에 var로 선언한 value가 있어서 에러가 떠야한다. 응? 근데 안뜬다.
또 그래서 첫번쨰 코드에서 선언할때 var로 선언한 변수가 해당 스코프범위에 let으로 선언한 변수가 있어서 그렇다고 하면 var선언코드가 스코프생성할때 스코프체인으로 검색해서 오류띄운다보게되고 var도 스코프를 신경안쓰는척? 신경쓰는거다 싯팔
이건 실행컨텍스트를 배우고 나서 다시 생각해보자.
# 이상한점
console.log(fuc); //undefined뜸
fuc(); // undefined를 호출하려고 해서 타입에러가 뜸.
{
function fuc() {
console.log("돼지");
}
}
console.log(fuc); //함수 객체 잘 가져옴
fuc() // 함수호출 잘됨.
블록문 안에 함수 fuc을 함수선언문으로 정의해줬다.
함수선언문으로 정의한 함수는 블록레벨스코프를 무시한다고 공부했다.
그래서 아래 콘솔로 fuc을 참조한코드랑 fuc()으로 함수호출한 코드는 잘 작동된다.
그런데 문제는 이거다.
함수선언문으로 정의한 함수는 함수호이스팅이 일어나서 코드순서상 정의이전에 참조,호출 할 수 있다.
첫번째, 함수선언문으로 정의한 함수는 블록스코프를 무시하고, 두번째 선언문으로 정의한 함수는 호이스팅이 일어난다
이 두가지 사실로 위에 콘솔로 함수객체 참조한것과 함수호출은 정상적으로 작동해야 한다고 생각했다.
그러나 코드를 돌려봤을때 var로 변수선언한것처럼 undefined가 된다. 함수객체가 안들어가있음 ㅡㅡ
호이스팅을 반은 하고 반은 안한다.
뭐 하긴 함수를 블록안에서 정의하는경우도 거의 없기도하고.. 더군다나 난 지금까지 함수선언문으로 함수를 정의한적도 없다.. 근데 궁금하다
+ 이번포스팅은 좀 길다 ㅡㅡ
사실 스코프가 그닥 어려운개념은 아니었다.. 그냥 너무 잡생각이 많아서 포스팅이 길어진것같다. 암튼 재미는있었다.