반응형
자바스크립트의 변수
변수는 프로그래밍언어를 배울때 가장먼저 접하는 개념입니다. 언어가 어떤 형태의 데이터를 선언하고 다룰것인지를 결정하는 부분이라 실제 언어의 특성들을 이해하기 전에 가장 먼저 접할수 밖에 없습니다.
보통은 언어는 특정 데이터 타입을 가지고 있습니다. 예를들면 int, string, date등 해당 데이터타입으로 데이터를 설정하는 방법이 있지만, 자바스크립트의 경우에는 느슨한 데이터 타입(loosley data type)이기에 모든 데이터 타입을 var 로 지정을 할수 있습니다.
문자열, 숫자, 객체, 심지어 함수까지도 변수로 지정하고 있습니다. 그래서 자바스크립트의 변수를 이야기 하다보면 자바스크립트의 거의 모든 부분을 다루게 됩니다.
이처럼 변수가 자바스크립트에서 단순히 값을 설정하고 사용하는 개념에 그치지 않고 참조변수, 스코프, 클로저, 컨택스트, this등 자바스크립트를 제대로 이해하기 위한 핵심적인 개념을 담고 있기에 반드시 이해하고 넘어가야 하는 부분입니다.
위에서 나열한 내용을 제외하고는 프로토타입 정도가 어려운 개념으로 남을듯합니다.
그럼 변수와 관련된 내용을 하나하나 알아가보도록 하겠습니다.
[변수 선언]
자바스크립트에서 변수는 선언하는 위치를 반드시 신경을 써줘야 합니다. 그 변수의 위치에 따라 변수가 의미하는 바가 달라지기때문입니다.
[전역변수 or 지역변수]
변수를 처음 접하면 가장 먼저 접하게 되는 부분이 바로 전역변수냐 지역변수냐의 문제입니다. 즉, 코드의 어떤 부분에서든 접근하게 해줄것이냐 아니면 필요한 부분에서만 사용할것인지를 결정하는 부분입니다.
프로그램 상에서 변수의 활용범위는 전역변수 > 지역변수 이지만 변수의 영향력은 전역변수 < 지역변수 입니다. 전역변수보다 지역변수가 우선순위를 가지게 됩니다.
지역변수 함수내부에서 선언된 변수로 함수 내부에서만 사용하겠다는 의미의 변수입니다. 단순히 함수 내부에서만 사용하는 변수이다 보니 프로그램의 전체 로직에 영향을 주지 않습니다. 해당 함수의 로직에만 영향을 줍니다.
물론 전역으로 선언된 변수에 함수의 리턴방식으로 지역 변수를 전역 변수로 사용하게 한다든지, 또는 setTimeout 내부에 선언된 함수는 전역에서 실행되는등 의도치 않게 또는 의도적으로 지역변수가 전역화 되는 경우도 있습니다.
보통은 전역변수를 만들때는 함수내부가 아닌 전역에서 var를 통한 변수를 선언하여 전역변수를 정의 하지만, 대부분 프로그래머의 실수로 인해 var 를 사용하지 않고 그냥 변수를 선언해 변수가 전역화가 되는 경우도 있습니다.
var를 사용하지 않고 선언한 변수가 왜 전역변수가 되는지는 아래의 변수 생성과정을 통해 더욱 자세히 설명하겠습니다.
var globalValue = “i’m global”;
function func(){
globalValueTwo = “i’m global. too”;
}
변수의 선언에 있어서 유념해야 하는 부분은 '변수의 선언 = 메모리' 라는 개념입니다. 그냥 전역변수를 무분별하게 선언하고 사용하면 결국 메모리 누수가 발생하게 되어 어플리케이션의 성능에 문제가 될수 있습니다. 물론 브라우저들의 성능이 무척이나 좋아지고 브라우저가 스스로 쓰지 않는 변수들을 이래저래 잘 정리를 해주지만 그래도 쓸데없는 변수의 선언은 피하는것이 좋습니다.
그리고 무엇보다 중요한 부분은 전역번수를 함수를 여기저기서 접근하다 보면 의도치 않게 값이 변경이 되어 원하는 결과를 얻지 못하는 경우가 있습니다.
항상 전역스코프는 깔끔하게 유지해두는것이 좋습니다.
이런 문제를 해결하기 위해서 ES6부터는 const, let등 추가적으로 변수를 선언하는 방법들이 소개되고 있지만 아무튼 전역변수의 사용은 항상 조심해야 하는 부분입니다.
이런 문제를 해결하기 위해서 ES6부터는 const, let등 추가적으로 변수를 선언하는 방법들이 소개되고 있지만 아무튼 전역변수의 사용은 항상 조심해야 하는 부분입니다.
[유효범위(scope)]
변수가 전역과 지역이라는 특정범위에서 유효한 영향 가진다는 것을 알게 되었습니다. 앞으로 변수의 유효한 범위를 스코프라고 하겠습니다. 그럼 스코프에 대한 이야기로 넘어가보겠습니다.
스코프의 핵심은 함수단위의 유효범위 입니다.
이 한가지 사실이 모든 것을 설명합니다. 즉, 해당 함수에 정의된 함수는 해당 함수내에서만 유효한 값을 행사한다는 의미입니다.
그런데 아래와 같은 코드가 헷갈리는 부분입니다.
function callYOU(){
var myname = “james”;
callAdam();
}
function callAdam(){
return myname;
}
callYOU();
어떤 결과가 리턴이 될까요? james 라는 결과를 예상할수 있지만 이 경우에는 에러가 발생합니다.
우리가 기대할때는 callAdam을 호출하는 시점에 분명 myname변수가 함수내에 선언이 되어있고 그 다음에 호출하는 callAdam이 myname 변수에 접근을 할수 있을 것이라고 기대를 합니다.
그러나 에러가 발생합니다.
이유는 변수는 함수가 호출하는 시점이 아닌 함수가 정의 되는 시점에 생성이 되기 때문입니다. callAdam이 정의 되는 시점에 myname이라는 변수는 callYOU 함수 안에서만 존재합니다. 당연히 지역변수입니다. 즉, 외부 함수에서는 접근이 안된다는 말입니다.
지금 당장 함수의 정의 시점에 따라 접근가능한 변수가 달라진다는 말에 의아해하실수도 있는데 앞으로 다룰 실행컨택스트의 변수객체와 this의 개념을 이해하시면 자연스럽게 이해가 되실내용입니다. 지금은 유효범위는 함수단위로 정해진다라고만 이해하시고 넘어가겠습니다.
[호이스팅]
자바스크립트에는 호이스팅이라는 개념이 있습니다. 이는 변수의 선언과 변수의 할당을 구분하겠다는 개념입니다. 즉, 함수내의 선언된 어떤 위치의 변수든 함수의 최상단으로 끌어올려집니다. 그리고는 undefined로 임의의 값을 할당합니다. 그리고 나중에 함수가 실행이 되면서 그 변수에 해당하는 값을 할당합니다.
예를들어 보겠습니다.
function hoistingTest(){
console.log(’greeting : ’ + hi);
var hi = “hello”;
console.log(’greeting : ’ + hi);
}
hoistingTest()
위 코드는 호이스팅을 설명하는 일반적인 코드입니다. 위의 코드는 분명히 에러가 나야 하는 상황입니다. hi라는 변수가 선언이 되지도 않았는데 hi변수를 이미 호출해서 사용했습니다. 그런데 에러는 나지 않습니다.
즉 변수의 선언이 먼저되어 있음을 알수 있습니다. 할당은 그 이후에 해당 코드가 실행이 되면서 이뤄졌습니다.
이 개념이 바로 자바스크립트에서의 호이스팅의 개념입니다.
[실행 컨택스트]
이제 변수는 물론 자바스크립트를 이해하는 핵심이 되는 실행컨택스트에 대한 내용입니다.
자바스크립트에서의 코드는 크게 세가지로 분류가 됩니다. global 코드, function 코드, eval 코드. 그리고 이 모든 코드는 실행컨택스트에 들어와 실행이 됩니다. 즉 자바스크립트에서 실행되는 모든 것들을 관리하는 부분이 바로 실행컨택스트부분입니다.
글로벌 컨택스트의 경우에는 오직 하나만 존재하는 반면에 function, eval 컨택스트는 하나의 프로그램 내에서 여러개가 존재할수 있습니다.
만약에 함수가 하나 호출이 되면 실행의 흐름이 함수 컨택스트로 들어가게되고 eval이 실행이 되면 이도 eval 컨택스트로 들어가서 실행이 되게 됩니다. 기본적으로 하나의 함수는 무한대의 컨택트스를 생성합니다. 아래의 예를 보겠습니다.
function foo(bar){}
foo(10)
foo(20)
foo(30)
함수가 각기 다른 인자를 가지고 세번 호출이 되었습니다. 이경우에는 서로다른 실행컨택스트가 생성이 됩니다. 즉 동일한 함수의 실행이라도 이미존재하는 컨택스트를 재활용하는 것이 아닌 새로운 컨택스트를 계속 생성하게 됩니다.
그럼 실행 컨택스트의 내부가 어떤 식으로 동작을 하는지 알아 보겠습니다.
실행컨택스트는 실행컨택스트의 스택이라는 영역에 저장이 됩니다. 실행컨택스트의 스택은 나중에 들어온 컨택스트가 스택의 최상단에 위치하게 됩니다. 즉 현재 실행이 되고 있는 컨택스트를 의미하고 이를 callee라고 부릅니다. 실행 컨택스트 내에서 다른 컨택스트를 실행시키기 위해서 필요한게 caller이고 caller를 통해 다른 컨택스트를 실행하게 됩니다.
예를들면 다음과 같습니다.
caller가 어떠한 context를 실행하게 되면 기존에 caller가 동작시키고 있던 실행의 흐름이 새롭게 동작하는 context, 즉 callee로 변경이 됩니다. 이때 callee는 실행컨택스트의 최상단에 위치하게 되고 Active Context라는 이름을 가지게 됩니다.
그리고 이 callee, 즉 active context의 동작이 끝이 나면 실행흐름을 다시 caller에게 전달하여 다른 context쪽으로 실행의 흐름을 변경하는 작업이 반복되게 됩니다. 그리고 동작을 마친 예전callee는 간단하게 return을 하거나 exception과 함께 exit을 하게 됩니다.
프로그램이 일단 실행이 되면 모두 글로벌 실행 컨택스트(global exceution context)로 들어오게 됩니다. 그리고 이 글로벌 컨택스트는 실행 컨택스트 스택의 제일 아래에 존재하며 이는 최초로 생성된 컨택스트를 의미하기도 합니다.
그후에 글로벌 코드들이 몇몇 초기화 작업을 진행하며 필요한 객체나 함수들을 생성을 합니다. 이렇게 글로벌 컨택스트가 실행이되는 동안 새로운 함수들이 생성이되고 실행이 되면서 스택내로 들어와 글로벌 컨택스트 위로 차곡차곡 쌓이게 됩니다. 일단 초기화가 끝이 나면 runtime 시스템은 사용자의 클릭이나 글로벌하게 발생하는 함수 실행같은 이벤트가 발생하기를 기다립니다.
그리고 이벤트가 발생이 하게 되면서 실행컨택스트 내의 컨택스트들이 하나하나동작을 하게 됩니다.
위와 같은 일련의 과정을 그림으로 나타내면 아래와 같습니다.
이와 같은 형태가 EXMAScript, 즉 우리가 사용하는 자바스크립트에서 코드의 실행을 관리하는 형태입니다.
[실행컨택스트]
실행 컨택스트의 동작방식을 이해했으니 내부 구조에 대해 알아보겠습니다.
실행컨택스트는 일단 크게 세가지의 형태로 필요한 정보를 담고 있습니다. 아래는 실행컨택스트의 구조입니다.
위와 같은 형태로 실행 컨택스트는 정보를 담고 있습니다. 그럼 세가지 요소를 하나하나 살펴보겠습니다.
[변수 객체(Variable object)]
변수객체는 실행컨택스트와 연관된 데이터의 정보를 담고 있는 객체로 대부분 변수나 함수를 선언하면 이곳에 저장이 됩니다. 쉽게 표현을 하면 var를 통해 변수를 선언하면 이곳에 프로퍼티로 저장이 된다는 말입니다. 명심할부분은 var를 통한 선언입니다.
실행컨택스트 = {
변수 객체 : {}
}
이러한 형태로 이해를 하면 됩니다. 그래서 우리가 변수나 함수를 생성을 한다는 의미는 변수객체에 프로퍼티와 값을 추가 한다는 의미와 동일합니다. 예를들면
var global = "hi";
function func(){
var local = "yo"
}
(function addMe(){})()
func()
라고 생성을 하게 되면 이는 두개의 변수 객체를 생성하게 됩니다. 기본적으로 생성이 되는 변수 객체와 함수호출에 따른 func 실행객체내의 변수 객체입니다.
전역 변수객체(글로벌) = {
global : "hi",
func : <reference to function>
}
func함수 변수객체 = {
local: "yo"
}
여기서 주의 깊게 봐야하는 부분이 addMe 함수입니다. 이런 function expression 형태로 정의 된 함수는 변수객체에 따로 저장이 되지 않습니다. addMe 함수에 우리가 접근하려고 시도하면 ReferenceError를 전달하게 됩니다.
그럼 전역컨택스트의 전역변수 객체와 함수 컨택스트의 변수 객체는 어떤 차이를 가지고 있을까요. 좀더 알아 보겠습니다.
전역변수객체의 경우 앞에서도 이야기 했지만 일단 프로그램이 시작이 되면 초기 세팅을 시작합니다. 우리가 사용하는 Math, String 또는 window 객체에 대한 내용들을 미리 지정해두고 있습니다. 그리고 이는 프로그램의 어떠한 곳에서도 접근이 가능합니다.
이 전역객체의 경우 이름을 통해 접근하는 것이 가능하기때문에 바로 해당 프로퍼티의 이름으로 바로 접근을 하는 것이고 전역컨택스트 상에서는 this나 window 같은 프로퍼티를 통해서 접근하는 것도 가능합니다.
반대로 함수 객체의 경우에는 직접적으로 변수객체에 접근하는 것이 불가능합니다. 그래서 그 변수객체의 내용을 그대로 가지고있는 Activation Object라는 것이 존재하여 이를 통해 우리는 변수들을 사용하게 됩니다. 일단 함수가 caller에 의해서 실행이 되면 activation object라고 불리는 특별한 객체가 생성이 되는데 이는 함수내에서 사용하는 일반적인 파라메터에 대한 정의는 물론이고 argument라는 특별한 객체또한 생성하게 됩니다.
그리고 이렇게 생성된 Activation Object를 우리는 함수의 변수 객체로 사용을 하게 됩니다.
function foo(x,y){
var z = 30;
function bar() {}
(function baz(){});
}
foo(10,20)
foo(10,20)
위코드를 보시면 우리가 이미 전역 변수 객체를 다룰때 사용한 코드와 동일한 코드이지만 모든 코드를 foo라는 함수로 한번 감싼형태입니다. foo(10,20) 함수가 실행이 되면서 다음과 같은 형태의 Activation Object를 가지게 됩니다.
Activation Object가 가지는 프로포티는
- 함수 내부에 정의 한 변수
- callee : 현재 실행되고있는 함수에 대한 참조
- length : 전달된 인자 갯수
- properties-indexes
- argument : 우리가 정의한 파라멘터를 인덱스와 함께 배열형태로 정의하고 있습니다.
그럼 여기서 위에서 간단하게 설명을 했던 변수의 호이스팅의 개념이 조금더 자세한 메커니즘과 함께 설명을 해보겠습니다.
우리는 실행컨택스트의 코드가 어떤식으로 동작을 하고있는지 알고 있습니다. 간단하게 설명을 하면 컨택스트 스택에 컨택스트들이 차곡차곡 쌓이게 되고 이를 caller가 호출하면서 프로그램의 흐름을 관리를 하게됩니다.
caller에 의해 호출은 되지 않지만 실행 컨택스트로 컨택스트가 추가가 되면 변수 객체에는 각 종 변수와 함수들이 프로퍼티로 정의 가되고 이때 모든 값은 undefined형태로 존재하게 됩니다.
변수객체 = {
변수 : undefined,
함수 : undefined,
...
}
이때를 우리는 실행컨택스트의 진입 단계로 봅니다. 그리고 이때를 변수들이 호이스팅이 된 상태라고 보고있습니다. 즉, 이미 해당 컨택스트의 코드가 실행이 되기 전에 변수객체와 실행객체의 프로퍼티는 undefined 또는 값으로 이미 생성이 된 상태입니다.
그리고 코드의 실행이 시작되면서 변수객체와 실행객체의 프로퍼티에 값들이 설정이 되기 시작합니다. 이게 앞서 호이스팅에서 이야기한 변수의 선언과 할당을 자바스크립트의 메커니증으로 설명을 한것입니다.
그럼 여기서 한가지 더!
자바스크립트를 다루는 개발자들이 면접등을 볼때 많이 받는 질문중에 하나인 undefined와 not defined의 차이에 대해서 설명을 해보겠습니다.
console.log(A) // undefined
console.log(B) // error
B = "not defined";
var A = "undefined"
과연 어떤 차이가 있는 것일까요? 우선 해당 컨택스트에 집입을 했습니다. 이때 변수객체는 다음과 같습니다.
변수객체 = {
A : undefined
}
엥?? 왜 B가 변수 객체에 존재하지 않는것일까요?? 앞에서 다룬 한가지 중요한 사실! 자바스크립트에서의 모든 변수는 var를 통해서만 생성이 된다는 말입니다. 즉 var를 통해서 선언된 변수만이 변수 객체에 추가가 될수있는 자격이 있습니다. 그럼 B는요??
B의 경우에는 변수객체가 아닌 글로벌 객체의 변수가 아닌 객체의 프로퍼티로 추가가 되어버립니다. 우리에게 변수는 실행객체속의 프로퍼티가 아니라 변수객체 내에 선언된 프로퍼티라는 사실을 명심해야 합니다.
결국 위 코드는 에러가 나고 코드를 수정해보면 다음과 같습니다.
console.log(A)
B = "not defined";
console.log(B)
var A = "undefined"
이렇게 코드를 작성하면 "not defined" 라는 결과를 나타내게 되고 이는 실제로 코드가 실행이 되는 단계에서 전역객체의 프로퍼티 접근 개념으로 값을 불러오게 되는것입니다.
그런데 여기서 문득 한가지 의문점이 생겼습니다. 변수 객체는 해당 함수의 스코프 상에서 존재를 하고 글로벌 변수객체를 제외하고는 독립적인 존재입니다. 즉 , 우리가 앞에서 배웠던 스코프의 개념입니다.
그럼 지역변수가 어떻게 전역변수의 값을 가지러 가는 것일까요? 즉 이런 형태일것입니다.
(function global(){
var globalValue = "i'm global value";
(function innerFunc1(){
var innerValue = "i'm inner value";
console.log(globalValue )
(function innerFunc1(){
console.log(globalValue )
console.log(innerValue )
})()
})()
})
위 코드의 결과는 예상처럼 "i'm global value" / "i'm global value" "i'm inner value" 입니다. 예전에는 당연하다고 느낀 위의 코드가 컨택스트와 실행 객체의 개념을 알고 나서는 이상해 보여야합니다. 독립적인 컨택스트가 어떻게 자신의 컨택스트를 벗어난 변수객체의 변수에 접근을 하는지에 대한 궁금증말입니다.
[스코프 체인(Scope chain)]
이 개념은 자바스크립트의 중요한 내용중의 하나인 프로토타입 체인과 매우 유사한 개념입니다. 프로토타입에 대한 내용은 [프로토타입 이해하기] 를 통해 확인하시면 됩니다.
간단히 설명을 하면 현재 스코프(컨택스트) 상에 변수가 존재하지 않는다면 부모 변수 객체로 이동을 해서 찾고 이를 계속해서 반복해 보통은 최상위 객체인 window 객체(전역변수객체)까지 접근해 변수를 찾는다는 내용입니다.
코드를 보겠습니다.
var x = 10;
(function foo(){
var y = 20;
(function bar(){
var z = 30;
console.log(x + y +z);
})()
})();
위와 같은 코드는 바로 실행이 됩니다. 우리가 이코드에서 원하는 결과는 x + y +z 가 모두 더해진 값을 확인하게 되는 것입니다. bar() 함수가 만든 스코프상에는 z 라는 변수만 존재하고 y와 x 값을 찾기위해서 계속 부모로 이동하면서 변수를 찾게 됩니다. 여기서 x,y를 우리는 free valiables 로 표현할수 있습니다. 즉 bar() 입장에서는 부담없이 접근 가능한 변수입니다.
물론 최상단에 도착했을때 변수가 존재하지 않는다면 우리가 자주보는 undefinded 를 만나게 될것입니다. 그럼 위 함수를 변수 객체형태로 표현을 해보겠습니다.
물론 최상단에 도착했을때 변수가 존재하지 않는다면 우리가 자주보는 undefinded 를 만나게 될것입니다. 그럼 위 함수를 변수 객체형태로 표현을 해보겠습니다.
함수의 실행객체는 __parent__ 라는 파라메터를 가지고 있습니다. 그리고 이 파라메터는 부모 객체를 참조하게 됩니다. 이는 프로토타입의 __proto__가 상위 포로토타입 객체를 참조하면서 부모의 성질을 그대로 이어받는것과 매우 유사한 개념입니다.
컨택스트는 해당 컨택스트가 끝이 나면 destroy 상태가 됩니다. 즉 제거가 됩니다. 담고있던 정보와 함께 말이지요..
그런데 아래와 같이 컨택스트정보가 스택에 저장에 되어있다고 가정해 보겠습니다.
[ EC2 ]
[ EC1 ] [ EC1 ]
[ Global Context] [ Global Context]
위와 같은 구조, 즉 EC2가 EC1의 내부 함수 이고 순서상 EC2가 먼저 실행이 완료되어 삭제가 됩니다. 그리고 EC1차례가 되었는데 내부 함수인 EC2를 리턴해야 한다면 이게 논리적으로 존재하지 않는 대상을 리턴해야되는 어처구니 없는 일이벌어집니다. 더구나 이 이후에 다른 컨택스트가 EC2를 활성화 시켜야 하는 경우도 있게됩니다. 존재하지 않는 컨택스트에 접근을 하고 활성화를 시키는 것이 말이 안되는 거죠..
결국 이미 없어진 객체를 계속적으로 사용하게 되는 오류가 발생하게 됩니다.
그래서 등장하게 되는 개념이 클로저(closure) 입니다.
[클로저(closure)]
자바스크립트에서 함수는 일급객체(first-class object)입니다. 즉, 함수를 다른 함수의 인자로 전달이 가능하다는 말입니다. 이렇게 전달되는 인자이름을 funargs 라고 부르는데 fuctional arguments의 줄임말입니다. 아무튼 funargs를 인자로 전달받는 함수를 higher-order function이라고 부릅니다. 또 함수가 다른 함수를 리턴하는 경우에는 이름 function valued function 이라고 부릅니다.
그런데 funargs와 function values를 사용하기에는 두가지 문제가 있고 이를 Funarg problem 이라고 부릅니다. 즉 함수가 다른 함수를 사용하게 되면서 생기는 문제, 혹은 다른 컨택스트의 정보를 사용하려고 시도하면서 생기는 문제를 말하는 것이고 이를 해결하기 위한 개념이 자바스크립트의 클로저(closure)입니다.
함수를 인자로 그러니 다른 함수를 사용하게 되면서 생기는 문제는 크게 두가지가 있습니다.
첫번째 문제는 “upward funargs problem”입니다.
이 문제는 함수가 다른 함수를 리턴하고 이미 선언된 변수를 사용하면서 발생합니다. 즉 변수를 찾기 위해서 이미 사라진 변수를 찾으로 부모 컨택스트로 접근을 하면서 발생하게 됩니다.
이를 해결하기 위해서 각 함수가 생성이 되는 순간, 즉 컨택스트가 생성이 되는 순간에 [[Scope]] 프로퍼티를 사용해서 부모의 정보를 저장해 둡니다.
그리고는 새로운 실행객체정보와 부모의 정보를 담은 [[Scope]] 정보를 조합해서 새로운 스코프체인 정보를 생성하게 됩니다.
이 문제는 함수가 다른 함수를 리턴하고 이미 선언된 변수를 사용하면서 발생합니다. 즉 변수를 찾기 위해서 이미 사라진 변수를 찾으로 부모 컨택스트로 접근을 하면서 발생하게 됩니다.
이를 해결하기 위해서 각 함수가 생성이 되는 순간, 즉 컨택스트가 생성이 되는 순간에 [[Scope]] 프로퍼티를 사용해서 부모의 정보를 저장해 둡니다.
그리고는 새로운 실행객체정보와 부모의 정보를 담은 [[Scope]] 정보를 조합해서 새로운 스코프체인 정보를 생성하게 됩니다.
Scope Chain = Activation object + [[Scope]]
이렇게 부모 컨택스트가 제거가 된 이후에도 상위 변수들을 참조할수 있는 방법을 마련해 두었습니다. 그럼 간단한 코드를 확인해 보겠습니다.
function foo(){
var x = 10;
return function bar(){
c onsole.log(x)
}
}
var returnFunction = foo();
var x =20;
returnFunction();
-> 10
위의 코드를 보면 foo의 내부 함수인 bar()는 애초에 참조하고 있던 부모 변수x값을 그대로 유지하고 있음을 알수 있습니다. 이와 같은 스코프를 static 또는 lexical 스코프라고 부릅니다.
두번째 문제는 “downward funargs problem” 입니다.
이경우에는 첫번째와 다르게 부모 컨택스트는 존재합니다. 하지만 내부 함수가 어떤 변수를 사용해야 하는지 애매 모호한 경우가 있습니다. 정적으로 함수가 생성이 될때 만들어진 컨택스트의 변수 정보를 사용할것인지 아니면 실행시점에 동적으로 생성된 컨택스트의 변수 정보를 사용할것인지 하나를 선택해야 하는 상황이 발생합니다.
자바스크립트는 이 경우에는 무조건 정적으로 즉, 함수가 생성되는 시점에 저장한 정보를 사용하는것으로 정해두었습니다.
예를들어 보겠습니다.
예를들어 보겠습니다.
var x = 10;
function foo(){
function foo(){
console.log(x)
}
(function(funArg){
(function(funArg){
var x = 20;
funArg();
})(foo);
이경우에 funArg() 함수는 어떤 x를 사용해야하는지 애매모호할수 있으나 함수가 실행되는 시점이 아닌 생성이 되는 시점에 참조하는 x값을 그대로 사용하게 됩니다. 결과는 10이 될것입니다.
자바스크립트는 [[Scope]]라는 프로퍼티를 통해 클로저를 완벽하게 지원을 하고 있고 정적 스코프가 자바스크립트에서 클로저를 사용하는데 필수적인 요소라는 것입니다.
결국 클로저는 코드블럭과 statically / lexically 저장하고 있는 부모 스코프의 모든 정보의 조합의 개념입니다. 그렇기 때문에 함수는 자연스럽게 내부에서 부모의 변수들을 자연스럽게 접근을 할수 있습니다.
그리고 이는 이론적으로 봤을때는 자바스크립트의 모든 함수는 생성이 되는 순간 [[Scope]] 프로퍼티를 가지고 있기 때문에 함수는 곧 클로저와 같다고 생각해도 됩니다.
자 그럼 또다른 상황에 대해서 이야기 해보도록 하겠습니다.
이런경우는 어떨까요? 하나의 부모 스코프를 가지고 여러 자식들이 같이 참조하는 경우입니다. 예를들면
이런경우는 어떨까요? 하나의 부모 스코프를 가지고 여러 자식들이 같이 참조하는 경우입니다. 예를들면
function baz(){
var x = 1;
return {
foo : function foo(){ return ++x},
bar : function bar(){return --x}
}
}
var closers = baz();
console.log(closers.foo() , closers.bar())
var x = 1;
return {
foo : function foo(){ return ++x},
bar : function bar(){return --x}
}
}
var closers = baz();
console.log(closers.foo() , closers.bar())
이 결과는 2와 1을 내놓습니다. 그럼 위 함수를 그림으로 표현해보겠습니다.
하나의 클로저가 다른 클로저의 변수에 영향을 주게됩니다.
또는 중복적으로 함수를 반복문을 통해 생성하는 경우에도 위와 같은 혼란을 줄수있습니다. 아래의 코드를 우선 보겠습니다.
var data = [];
for(var k=0 ; k < 3; k++){
data[k] = function (){
alert(k);
}
}
data[0]();
data[1]();
data[2]();
이세번의 호출은 어떤 값을 리턴할까요? 잠깐 생각해보겠습니다...
....
..아마 0, 1, 2???
답은 3, 3, 3입니다. 왜그럴까요? 분명 매번 돌때마다 그 값을 배열에 저장했고 당연히 0,1,2 가 나올거라고 생각했는데..
....
..아마 0, 1, 2???
답은 3, 3, 3입니다. 왜그럴까요? 분명 매번 돌때마다 그 값을 배열에 저장했고 당연히 0,1,2 가 나올거라고 생각했는데..
이런일이 벌어진 이유는 반복문을 돌면서 각기다른 스코프가 생성이 되어야하는데 이경우에는 전역스코프상의 k를 공유하면서 k값만 계속 변화가 일어나게 됩니다. 결국 공통으로 k를 바라보고 있어서 이런 일이 벌어지게 되었습니다.
우리가 원하는 0,1,2를 가지기 위해서는 각기다른 [[Scope]]를 가지게끔 해줘야합니다. 해결책은 의외로 간단합니다. 일반적으로 익명함수로 한번 감싸주고 인자로 필요한 값을 전달하는 방법으로 해결합니다.
var data = [];
for(var k=0 ; k < 3; k++){
data[k] = (function(){
return function (){
alert(k);
};
})(k);
}
그리고는
data[0]();
data[1]();
data[2]();
이렇게 호출을 하면 원했던 0,1,2를 리턴하게 됩니다.
추가적으로 문득 위의 코드가 뭔가 깨림직하지 않으신가요? 함수안에 또 함수를 추가한다는 말인데.. 딱 봐도 처리속도나 쓸데 없이 메모리를 더 사용하게 될것같다는 생각이 드는건 왜일까요? 물론 위의 상황은 함수로 감싸는 방법말고는 딱히 방법이 없어보이기는 합니다만..
뭐 예를 들면 이런 코드입니다.
function Employee(name, phone, salary){
this.name = name;
this.phone = phone;
this.salary = salary;
this.getSalaryInfo = function(){
return this.name + “’s salary : ” + this.salary;
}
function Employee(name, phone, salary){
this.name = name;
this.phone = phone;
this.salary = salary;
this.getSalaryInfo = function(){
return this.name + “’s salary : ” + this.salary;
}
}
Employee이 생성자를 정의 했습니다. 그리고 생성자 안에 메서드를 정의 했습니다. 그런데 이 경우에는 생성자가 호출이 될떄마다 매번 메서드가 새롭게 할당이 됩니다. 메서드의 할당은 메모리에 실행객체의 생성, 컨택스트의 생성등 메모리의 낭비를 가져오게 됩니다.
무분별한 변수의 선언이나 함수의 선인을 피하는 것이 좋습니다. 그래서 자바스크립트의 프로토타입의 개념을 사용한다면 매번 새롭게 객체가 생성될때마다 메서드정의가 일어나지 않으니 이 특징을 잘 활용해서 코드를 작성하면 효율적인 프로그램이 작성이 될것입니다.
이제 마지막으로 실행컨텍스트 마지막 정보인 this에 대해서 알아보겠습니다.
[this]
this는 실행객체와 연관된 특별한 객체이므로 이름을 컨택스트 객체라고 부를수도 있습니다.
어떤 객체든 컨택스트를 값으로 this를 사용합니다. 여기서 한번더 실행컨택스트와 this 값 사이의 관계에 대한 오해에 대해서 명확하게 하고 넘어가겠습니다.
종종 this는 변수객체의 프로퍼티로 잘못 설명이 됩니다. 한번더 명확하게 하자면 this는 실행 컨택스트의 프로퍼티입니다.
this가 중요한 이유는 this는 일반 변수와는 다르게 어떤 중간 매개 없이 , 스코프 체인 같은 중간 과정이 없이 바로 실행컨택스트에 접근을 합니다. this의 값은 오직 컨택스트에 들어오는 순간 딱 한번 정의 가 됩니다.
그래서 프로그램이 실행되는 도중에 this에 새로운 값을 부여하는것이 불가능합니다. 다시한번 말씀드리지만 일반 변수처럼 변수객체에 위치하지 않기 때문에 일반변수로 생각하면 안됩니다.
var x =10;
console.log(
x,
this.x,
window.x
)
글로벌 컨택스트에서 this는 글로벌 객체 그 자체를 나타냅니다. 즉, 여기서의 this는 변수객체와 같음을 의미합니다.
글로벌 컨택스트 상에서의 this는 그다지 문제가 되지 않습니다.
대신 함수코드 내의 this가 이제부터 새로운 문제로 떠오르게 됩니다. 그럼 간단한 예제를 가지고 변하지 않을것이라고 생각한 this가 바뀌는 상황을 보여드리겠습니다.
var you = {name : "james"}
var me = {
name : "alen",
callme : function(){
alert(this.name);
}
}
me.callme(); // allen
you.callme = me.callme;
you.callme(); // james
분명 me의 callme에서의 this는 당연히 me를 가리키고 있었습니다. 그런데 me의 callme를 그대로 you의 callme로 정의 했더니 callme의 this가 me가 아닌 you를 가리키게 되었습니다. 분명 this는 한번 정의가 되면 바뀌지 않는 값이라고 했는데 어떻게 된일일까요??
이부분을 이해하기 위해서는 한가지 사실을 명심해야하는데 함수에서의 this는 해당 컨택스트의 코드를 활성화 시킨 caller에 의해서 제공이 됩니다. 즉 실행 컨택스트 스택에 있던 컨택스트가 caller에 의해서 호출이 되는 시점에 this가 결정이 되는데 이말은 함수를 호출하는 형태에 의해서 this가 결정이 된다는 말로 이해를 해도됩니다.
호출하는 표현식에 따라 this 값에 영향을 끼치게 됨을 이해하기 위해서 우리가 알아야하는 한가지 개념이 있는데 바로 레퍼런스 타입(Reference type) 입니다.
레퍼런스 타입은 base와 property name이라는 두가지로 구성이 되어있는 객체입니다. 예를들면
var foo = 10;
var fooReference = {
base : global,
propertyName : 'foo'
}
function bar() {}
var barReference = {
base : global,
propertyName : 'bar'
}
대충 레퍼런스 타입객체가 담고있는 정보에 대한 감을 잡으셨을것입니다.
var foo = {
bar: function () {
alert(this);
alert(this === foo);
}
bar: function () {
alert(this);
alert(this === foo);
}
};
위의 경우 우리가 bar 메서드를 호출하기 위해서는 dot notation이나 [""] 표현법을 사용할수 있습니다. 이때의 레퍼런스 타입의 형태는 다음과 같습니다.
foo.bar();
foo["bar"]();
var fooBarReference = {
base: foo,
propertyName: 'bar'
base: foo,
propertyName: 'bar'
};
즉, bar라는 메서드의 기본이 되어주는 것이 foo임을 알수 있습니다. 함수의 호출괄호의 왼편에 레퍼런스 타입의 값이 존재한다면 this는 레퍼런스 타입의 base의 값을 가지게 됩니다. 이를 제외한 모든 경우에는 this가 null 값을 가지는데 null이 아닌 전역객체로 대체되어 값을 가지게 됩니다.
다시 한번 정리를 하면 내가 호출하는 함수가 어떠한 객체를 기반으로 동작을 한다면, 즉 base가 해당 객체를 값으로 가진다면 this는 그 객체를 값으로 가지게 되며 이 경우를 제외한 나머지 모든 경우에는 전역객체를 값으로 가지게 됩니다.
function foo() {
return this;
}
return this;
}
foo(); // global
var fooReference = {
base: global,
propertyName: 'foo'
base: global,
propertyName: 'foo'
};
var foo = {
bar: function () {
return this;
}
};
return this;
}
};
foo.bar(); // foo
var fooBarReference = {
base: foo,
propertyName: 'bar'
propertyName: 'bar'
};
var test = foo.bar;
test(); // global
var testReference = {
base: global,
propertyName: 'test'
propertyName: 'test'
};
jQuery를 사용할때 $.each() 같은 함수를 사용할때 왜 $.를 앞에 붙여서 사용하는지 이제는 알것같습니다.
이제 함수를 호출하는 방식에 따라 다른 this를 가지는 이유와 짜여진 코드에서 this가 무엇을 의미하는지를 구분할수 있게 되었을거라 믿습니다. 이렇게 this의 값이 변화무쌍하다 보니 jQuery같은 라이브러리에서는 this를 $(this)로 바꿔 무조건 해당 함수내부에서의 this를 함수객체 자체로 바라보게 만들어서 코딩을 하기도 합니다.
엄청나게 긴글이 되어버렸습니다. 저도 원문인 http://dmitrysoshnikov.com/ecmascript/javascript-the-core/ 를 보면서 몰랐던 내용들을 공부하고 찾아보면서 쓰다보니 어쩔수 없이 길어졌습니다. 그래도 자바스크립트를 제대로 이해하기위해서는 반드시 한번쯤은 읽어보시는걸 추천드립니다. 원문을 읽으셔도 물론 훌륭합니다.
최근 자바스크립트관련된 기술들이 엄청나게 쏟아져나오고 있습니다. 그 기술들을 사용을 하는데 있어서 왜 그런식으로 사용을 하게 하며 그 뒷단에서 자바스크립트 그 자체가 하는 역활이 무엇인지를 제대로 이해하기 위해서는 테크닉적인 정보가가 아닌 자바스크립트의 가장 기본적이고 핵심적인 내용이 무엇보다 중요합니다. 항상 기본에 충실해야 흔들리지 않는법이겠죠?? 아님 뭐 별수 없지만요..
항상 피드백은 대환영입니다.
참고
출처: https://yubylab.tistory.com/entry/자바스크립트-변수로-자바스크립트-이해하기 [Yuby's Lab.]
반응형
'프로그래밍 > JavaScript' 카테고리의 다른 글
JavaScript - ECMA6 문법 (구조분해,애로우함수 등) (0) | 2019.04.30 |
---|---|
javascript - foreach 문 , for in 문 , for of문 (1) | 2019.03.03 |
javascript - 콜백함수 확장판 (콜백함수 이해하기) (0) | 2019.03.03 |
javascript - 콜백함수,함수의 선언,클로저 (0) | 2019.03.03 |
자바스크립트 기본 (0) | 2019.02.27 |