스코프란?
바스크립트는 var 문법을 통해 변수를 선언할 수 있습니다. 또한 es2015부터 let과 const라는 문법으로도 선언할 수 있게 되었습니다.
모질라 문서에 따르면 var은 함수 스코프(function-level scoped), let과 const는 블록 스코프(block-level scoped)를 갖는다고 합니다.
스코프의 범위
전역스코프?
- 변수가 함수나 블록에 속해 있지 않음
- 함수 내부에 속해 있더라도 var로 지정자를 설정하지 않음
- const,let 등으로 선언을 했는데 그 어떤 블록 안에도 들어가있지 않음
지역스코프?
- 변수가 함수에 속해 있고 var로 선언되어 있음
- 변수가 블록에 속해 있고 const나 let으로 선언되어 있다.
스코프 레벨
함수 스코프?
함수 내부에서 (var)로 변수를 선언했을 때 함수 내부에서만 이 변수에 접근 가능
function scopeTest() {
var a = 0;
if (true) {
var b = 0;
for (var c = 0; c < 5; c + +) {
console.log("c=" + c);
}
console.log("c=" + c);
}
console.log("b=" + b);
}
scopeTest();
//실행결과
/*
c = 0
c = 1
c = 2
c = 3
c = 4
c = 5
b = 0
*/
위의 코드는 JavaScript의 유효범위 단위가 블록 단위가 아닌 함수 단위로 정의된다는 것을 설명하기 위한 예제 코드입니다.
다른 프로그래밍 언어들은 유효범위의 단위가 블록 단위이기 때문에 위의 코드와 같은 if문, for문 등 구문들이 사용되었을 때 중괄호 밖의 범위에서는 그 안의 변수를 사용할 수 없습니다.
하지만 JavaScript의 유효범위는 함수 단위이기 때문에 예제코드의 변수 a,b,c모두 같은 유효범위를 갖습니다.
그 결과, 실행화면을 보면 알 수 있듯이 구문 밖에서 그 변수를 참조합니다.
1 2 3 4 5 6 7 8 9 10 11 |
var scope = 10; function scopeExam(){ var scope = 20; var scope = 30; console.log("scope = " +scope); } scopeExam(); //실행결과 /* scope =20 */ |
cs |
JavaScript는 다른 프로그래밍 언어와는 달리 변수명이 중복되어도 에러가 나지 않습니다.
단, 같은 변수명이 여러 개 있는 변수를 참조할 때 가장 가까운 범위의 변수를 참조합니다.
위의 코드 실행화면을 보면 함수 내에서 scope를 호출했을 때 전역 변수 scope를 참조하는 것이 아니라
같은 함수 내에 있는 지역변수 scope를 참조합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function scopeExam(){ scope = 20; console.log("scope = " +scope); }
function scopeExam2(){ console.log("scope = " + scope); } scopeExam(); scopeExam2(); //실행결과 /* scope=20 scope=20 */ |
cs |
다른 프로그래밍 언어의 경우 변수를 선언할 때 int나 char와 같은 변수 형을 썼지만, JavaScript는 var 키워드를 사용합니다.
또, 다른 프로그래밍 언어의 경우 변수를 선언할 때 변수형을 쓰지 않을 경우 에러가 나지만 JavaScript는 var 키워드가 생략이 가능합니다.
단, var 키워드를 빼먹고 변수를 선언할 경우 전역 변수로 선언됩니다.
위 코드의 실행 결과를 보면 scope라는 변수가 함수 scopeExam 안에서 변수 선언이 이루어졌지만,
var 키워드가 생략된 상태로 선언되어 함수 scopeExam2에서 호출을 했을 때도 참조합니다.
블록스코프?
괄호({})안에 const 나 let키워드로 선언했을 때, 괄호 안에서만 변수 접근 가능
1 2 3 4 5 6 7 8 9 10 |
function blockScope() { if (true) { let a ="hello"; } console.log(a); }
blockScope(); //a is not defined
|
cs |
스코프 규칙
렉시컬 스코프 규칙?
렉시컬 스코프에서는 소스코드가 작성된 그 문맥에서 결정된다. 현대 프로그래밍에서 대부분의 언어들은 렉시컬 스코프 규칙을 따름
블록스코프 규칙?
동적 스코프는 프로그램의 런타임 도중의 실행 컨텍스트나 호출 컨텍스트에 의해 결정
호이스팅
호이스팅은 var을 통해 정의된 변수의 선언문을 유효 범위의 최상단으로 끌어올리는 행위===>'선언과 할당의 분리'
Before
1 2 3 4 5 6 7 8 9 10 11 12 |
function hoistingExam(){ console.log("value=" +value); var value =10; console.log("value=" +value); } hoistingExam();
//실행결과 /* value= undefined value= 10 */ |
cs |
After
1 2 3 4 5 6 7 8 9 10 11 12 |
function hoistingExam(){ var value; console.log("value=" +value); value =10; console.log("value=" +value); } hoistingExam(); //실행결과 /* value= undefined value= 10 */ |
cs |
위의 코드는 호이스팅을 설명하기 위한 간단한 예제입니다.첫번째 코드를 보시게 되면 함수 hoistingExam안에서 변수 value의 호출이 두 번 일어납니다.
한 번은 변수 선언문 전에 또 한 번은 변수 선언 후에 호출이 되는데, 다른 프로그래밍 언어의 경우에는 선언문 전에 호출됐을 때 에러가 납니다.
하지만 JavaScript의 경우 호이스팅이 됨으로써 오른쪽 코드와 같은 구동이 이루어집니다.
즉, 변수 선언문이 유효범위 안의 제일 상단부로 끌어올려 지게 되고, 선언문이 있던 자리에서 초기화가 이루어지는 결과를 갖는 것입니다.
그 실행결과 첫 번째 호출에서는 초기화가 되지 않은 undefined가, 두 번째 호출에서는 초기화된 값이 나옵니다.
1 2 3 4 5 6 7 8 9 10 11 12 |
var value =30; function hoistingExam(){ console.log("value=" +value); var value =10; console.log("value=" +value); } hoistingExam(); //실행결과 /* value= undefined value= 10 */ |
cs |
그렇다면 위와 같은 코드에서는 어떤 결과가 나올까요?
다른 프로그래밍 언어에 익숙한 개발자 분들은 변수 value의 첫 호출에서 전역변수가 참조된다고 생각하실 수 있습니다.
하지만 JavaScript의 호이스팅으로 인해서 선언 부가 함수 hoistingExam의 최 상단에서 끌어올려 짐으로써 전역변수가 아닌 지역변수를 참조합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// 함수 선언문 hoistingExam(); function hoistingExam(){ var hoisting_val =10; console.log("hoisting_val =" +hoisting_val); } //실행결과 /* hoisting_val =10 */ //함수 표현식 hoistingExam2(); var hoistingExam2 = function(){ var hoisting_val =10; console.log("hoisting_val =" +hoisting_val); } //실행결과 /* hoistingExam2 of object is not a function */ //Function 생성자 hoistingExam3(); var hoistingExam3 = new Function("","return console.log('Ya-ho!!');"); //실행결과 /* hoistingExam3 of object is not a function */ |
cs |
위의 코드와 실행결과를 보시면 함수 선언문 방식만 호이스팅이 제대로 이루어집니다.
이 결과를 보고 왜 함수 선언문 방식만 호이스팅이 되고 함수 표현 식과 Function생성자를 통해 함수를 정의하는 방법은 호이스팅이 되지 않는지 궁금해하시는 분들도 계실 것 같은데요.
그 이유는 함수 표현 식과 Function생성자를 통한 함수 정의 방법은 변수에 함수를 초기화시키기 때문에 선언문이 호이스팅이 되어 상단으로 올려진다 하더라도 함수가 아닌 변수로써 인지되기 때문입니다.
위의 코드에서 함수실행 구문이 아닌 변수를 호출하게 되면 변수의 호이스팅과 같은 undefined란 결과가 나옵니다.
중첩 스코프(Nested Scope)와 스코프 체인
Scope 내에서 별도의 Scope 를 정의한 경우 바깥쪽 Scope 에서 안쪽 Scope 에 접근이 불가능하지만 안쪽 Scope 에서는 바깥쪽 Scope 의 변수에 접근이 가능하다.
1 2 3 4 5 6 7 8 9 10 11 12 |
var a = "A"; function f() { var b = "B"; console.log('[g::c]',c);//c is not defined function g() { var c = "C"; console.log(a + b + c); } g(); } f(); //ABC
|
cs |
자바스크립트 엔진은 앞 코드의 함수 g 안에 문장인 console.log(a+b+c)에서 변수 a,b,c의 식별자는 어떻게 찾아낼까요? 스코프 체인으로!!!!
스코프 체인이란?
범위 연쇄 라고 언어적으로 해석 할 수 있습니다. 한국말로 해석된 말을 의미 유추해보면 어떤 범위안에 범위안에 범위에 연쇄적으로 사슬과 같이 올라간다 라고 유추 할 수 있을거 같습니다.
한마디로 scope chain 은 Identifiers(식별자)를 찾는 일련의 과정을 말합니다.
자바스크립트는 실행시점에 Excecution Context(EC)가 생성이되고, 그 시점에 온갖 잡다한 행위들이 이뤄집니다.(예를들어 Hoisting)
1 2 3 4 5 6 7 8 9 10 11 |
ExecutionContext = {//실행 문맥==>실행 가능한 코드가 실제로 실행되고 관리되는 영역으로 실행에 필요한 모든 정보를 컴포넌트 여러 개가 나누어 관리 하도록 만들었습니다. LexicalEnvironment: {//렉시컬 환경 컴포넌트=>[ 함수,블록의 유효범위 안에 있는 식별자와 그 결과값]등 자바스크립트엔진이 자바스크립트 코드를 실행하기 위해 유효 범위 안에 있는 식별자와 그 값을 키와 값의 쌍으로 바인드 해서 기록 EnvironmentRecord: {//환경 레코드=>유효범위 안에 포함된 식별자를 기록하고 실행하는 영역 DeclarativeEnvionmentRecord: {}, //선언적 환경 레코드==>실제로 함수와 변수,catch문의 식별자와 실행결과가 저장되는 영역 ObjectEnvornmentRecord: {} //객체 환경 레코드==>실행문맥 외부에 별도로 저장된 객체의 참조에서 데이터를 읽거나 쓴다. }, OuterLexicalEnvironment_Reference: {} //외부 렉시컬 환경 참조 => 함수를 둘러싸고 있는 코드가 속한 렉시컬 환경 컴포넌트의 참조가 저장, 중첩된 함수 안에서 바깥 코드에 정의된 변수를 읽거나 써야할때 사용 }, VariableEnvironment: {}, //변수 환경 컴포넌트=>렉시컬 환경 컴포넌트와 동일한 타입 ThisBinding: null //this 바인딩 컴포넌트 =>그 함수를 호출한 객체의 참조가 저장되는 곳 }; |
cs |
이제 자바스크립트의 함수 실행과정을 좀 더 자세히 들여다 봅시다!
함수를 호출하면 현재 실행중인 코드의 작업을 일시적으로 멈추고 실행 문맥영역을 생성합니다.
그리고 프로그램의 실행 흐름이 그 실행 문맥으로 이동합니다.
다음으로 그함수의 실행 문맥이 호출 스택에 푸쉬되고 실행 문맥 안에 렉시컬 환경컴포넌트를 생성합니다.
이 렉시컬 환경 컴포넌트는 환경 레코드를 가지고 있으며, 환경 레코드 안에 그함수 안에서 선언된 중첩 함수와 변수를 기록합니다.
즉,함수 안팎의 환경을 기록합니다. 이 환경 레코드는 사용자가 읽거나 쓸수 없으며 다음과 같은 정보를 기록하는 용도로 사용됩니다.
- 함수인자
- 함수 안에서 선언된 중첩 함수의 참조
- 함수안에서 var로 선언한 지역 변수
- arguments(인수값 전체목록,length,callee)
함수가 실행될 때 그 함수의 선언적 환경 레코드에는 이에 대응하는 인수의 값이 설정되며, 대응하는 인수가 없으면 undefined가 설정됩니다.
함수 선언문으로 생성한 함수 안의 지역 변수에는 함수 선언문으로 생성한 함수 객체의 참조가 설정됩니다. var로 선언한 지역 변수에는 undefined가 설정됩니다.
함수의 실행 문맥,렉시컬 환경,환경 레코드가 생성되면 실행 문맥에 있는 디스 바인딩 컴포넌트에 그 함수를 호출한 객체의 참조를 저장하며,이것으로 this의 값을 결정합니다.
이 this는 동적이며 함수를 호출하는 상황에 때라 가리키는 객체가 봐뀝니다.
환경레코드와 this값이 결정되면 함수 안의 코드가 순서대로 실행됩니다. 함수가 실행되는 시점에는 지역변수 또는 함수 선언문으로 선언한 함수 이름이 함수를 평가하는 시점에 선언적 환경 레코드에 기록된 상태입니다.
따라서 변수 또는 함수 선언문이 함수 안에 어느 부분에 위치하더라도 함수 전체에서 사용할 수 있습니다. 이것이 변수 선언문과 함수 선언문이 함수의 첫머리로 끌어올려지는 이유입니다.
함수가 종료되어 제어권이 호출한 코드로 돌아가면 일반적으로 실행 문맥과 함께 그 안에 있는 렉시컬 환경 컴포넌트가 메모리에서 지워집니다. 하지만 그 함수의 바깥에 위치한 함수의 참조가 환경 레코드에 유지되는 경우에는 렉시컬 환경 컴포넌트가 지워지지 않습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var a = "A";
function f() {
var b = "B";
function g() { var c = "C"; console.log(a + b + c); }
g(); }
f(); //ABC |
cs |
용어 소개
- 속박변수:일반적으로 함수의 인수와 지역변수 (ex:c)
- 자유변수:그 외 변수(ex:a,b)
- 닫힌함수: 소박변수만 포함하는 함수(ex:f)
- 열린함수:자유변수를 가지고 있는 함수(ex:g)
————————————————————————————————————————————————————————————————————————————————————————
속박변수 c
변수 c는 함수 g 안에서 선언된 속박 변수이므로 함수 g의 환경 레코드(선언적 환경 레코드)안에서 찾을 수있습니다.
————————————————————————————————————————————————————————————————————————————————————————
자유변수 b
변수 b는 함수 g의 바깥에서 선언된 자유 변수입니다. 변수 b는 함수 g가 속한 실행 문맥의 환경레코드 안에서 찾으로 수가 없습니다.
그래서 실행 문맥속에 있는 외부 렉시컬 환경참조를 따라 함수 g를 호출한 함수인 f가 속한 실행 문맥의 환경 레코드를 검색합니다.
변수 b는 함수 f안에 선언되어 있으므로 함수 f의 환경 레코드안에서 찾을 수 있습니다.
*함수 f가 호출되면 함수 f의 환경 레코드에 변수 b가 프로퍼티로 추가됩니다. 그후에 함수 g의 선언문이 평가되어 환경 레코드가 생성됩니다.
이떄 함수 g의 함수 객체가 함수 f의 렉시컬 환경을 참조합니다. 이 참조로 함수 g 안에서 변수 b를 사용할 수 있게 됩니다.
이러한 과졍을 거쳐 함수 g를 실행하는 시점에서는 변수 b의 위치를 외부 렉시컬 환경 참조를 따라 검색할 수 있는 상태가 됩니다.
————————————————————————————————————————————————————————————————————————————————————————
자유변수 a
변수 a는 함수 g 바깥에서 선언된 자유변수 입니다. 이 경우에는 변수 a를 함수 g의 환경 레코드에서 찾을 수 없고 실행문맥속에 있는 외부 렉시컬 환경 참조를 따라 g를 호출한 함수인 f의 환경레코드 내부를 탐색합니다.
하지만 이 안에서도 찾을 수 없습니다. 그러면 외부 렉시컬 환경 참조를 따라 한단계를 더 거슬러 올라가 함수 f의 전역 실행문맥 속에 있는 환경 레코드를 검색합니다. a는 var로 선언되어 있으므로 여기서 찾을 수있습니다.
————————————————————————————————————————————————————————————————————————————————————————
이처럼 식별자 결정은 현재의 유효 범위 안에 없는 식별자를 찾을때 바깥쪽 범위로 호출자의 렉시컬환경에 속한 외부 렉시컬 환경의 참조를 따라 찾아가는 방식을 취합니다. 이러한 논리적인 연결고리를 스코프 체인이라고 부릅니다.
가비지 콜렉션
참조카운터 방식=>순환참조가 발생시 메모리 누수 발생
1 2 3 4 5 6 |
var p ={x:0,y:0}; var q ={x:0,y:1}; p.next =q; q.next =p; p =null; q =null; |
cs |
마크 앤 스윕 방식
전역 객체가 참조할 수 없는 객체를 검색하고 해당객체가 있다면 필요 없는 객체라고 판단하여 메모리에서 해제==>순환참조에 의한 메모리 누수 방지
클로저(Closure)
클로저는 JavaScript의 유효범위 체인을 이용하여 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 방법입니다.
외부 함수가 종료되더라도 내부함수가 실행되는 상태면 내부함수에서 참조하는 외부함수는 닫히지 못하고 내부함수에 의해서 닫히게 되어 클로저라 불리 웁니다.
따라서 클로저란 외부에서 내부 변수에 접근할 수 있도록 하는 함수입니다.
내부 변수는 하나의 클로저에만 종속될 필요는 없으며 외부 함수가 실행 될 때마다 새로운 유효범위 체인과 새로운 내부 변수를 생성합니다.
또, 클로저가 참조하는 내부 변수는 실제 내부 변수의 복사본이 아닌 그 내부 변수를 직접 참조합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
function outerFunc(){ var a = 0; return { innerFunc1 : function(){ a + =1; console.log("a :" +a); }, innerFunc2 : function(){ a + =2; console.log("a :" +a); } }; } var out = outerFunc(); out.innerFunc1(); out.innerFunc2(); out.innerFunc2(); out.innerFunc1();
//실행결과 /* a = 1 a = 3 a = 5 a = 6 */ |
cs |
위의 코드는 클로저의 예제 코드이며 그 중 좌측 코드는 서로 다른 클로저가 같은 내부 변수를 참조하고 있다는 것을 보여주고 있습니다.
서로 다른 클로저 innerFunc1과 innerFunc2가 내부 변수 a를 참조하고 a의 값을 바꿔주고 있습니다.
실행 결과를 보면 내부 변수 a의 메모리를 같이 공유한다는 것을 알 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
function outerFunc(){ var a = 0; return { innerFunc1 : function(){ a + =1; console.log("a :" +a); }, innerFunc2 : function(){ a + =2; console.log("a :" +a); } }; } var out = outerFunc(); var out2 = outerFunc(); out.innerFunc1(); out.innerFunc2(); out2.innerFunc1(); out2.innerFunc2(); //실행결과 /* a = 1 a = 3 a = 1 a = 3 */ |
cs |
위의 코드는 같은 함수를 쓰지만 서로 다른 객체로 내부 변수를 참조하는 모습입니다.
외부 함수가 여러 번 실행되면서 서로 다른 객체가 생성되고 객체가 생성될 때 마다 서로 다른 내부 변수가 생성되어
보기엔 같은 내부 변수 a로 보이지만 서로 다른 내부 변수를 참조합니다.
클로저의 사용이유
클로저를 사용하게 되면 전역변수의 오,남용이 없는 깔끔한 스크립트를 작성 할 수 있습니다.
같은 변수를 사용하고자 할 때 전역 변수가 아닌 클로저를 통해 같은 내부 변수를 참조하게 되면 전역변수의 오남용을 줄일 수 있습니다.
또한, 클로저는 JavaScript에 적합한 방식의 스크립트를 구성하고 다양한 JavaScript의 디자인 패턴을 적용할 수 있습니다.
마지막으로 함수 내부의 함수를 이용해 함수 내부변수 또는 함수에 접근 함으로써 JavaScript에 없는 class의 역할을 대신해 비공개 속성/함수, 공개 속성/함수에 접근을 함으로 class를 구현하는 근거 입니다.
클로저 사용시 주의할 점
클로저를 사용할 때 주의해야 할 점이 여럿 있습니다. 제가 알려드리고 싶은 주의 점은 다음과 같습니다.
for 문 클로저는 상위 함수의 변수를 참조할 때 자신의 생성될 때가 아닌 내부 변수의 최종 값을 참조합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<body > <script > window.onload = function(){ var list = document.getElementsByTagName("button");
for(var i =0, length = list.length; i <length; i + +){ list[i].onclick=function(){ console.log(this.innerHTML +"은" +(i +1) +"번째 버튼입니다"); } } } < /script > <button >1번째 버튼 < /button > <button >2번째 버튼 < /button > <button >3번째 버튼 < /button > < /body > //실행결과 /* 1번째 버튼은 4번째 버튼입니다 2번째 버튼은 4번째 버튼입니다 3번째 버튼은 4번째 버튼입니다 */ |
cs |
위의 코드는 각각의 버튼에 이벤트를 걸어 클릭된 버튼이 몇 번째 버튼인지를 알기 위한 예제 입니다.
하지만, 실행 결과 값은 바라던 결과가 나오지 않습니다.
위의 클로저인 클릭 이벤트가 참조 하는 변수 i의 값이 버튼이 클릭될 때의 값이 아닌 for 구문을 다 돌고 난후 i의 값 4를 참조하기 때문에 모두 4라는 결과가 나옵니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<body > <script > window.onload = function(){ var list = document.getElementsByTagName("button");
var gate = function(i){ list[i].onclick=function(){ console.log(this.innerHTML +"은" +(i +1) +"번째 버튼입니다"); } } for(var i =0, length = list.length; i <length; i + +){ gate(i); } } < /script > <button >1번째 버튼 < /button > <button >2번째 버튼 < /button > <button >3번째 버튼 < /button > < /body > //실행결과 /* 1번째 버튼은 1번째 버튼입니다 2번째 버튼은 2번째 버튼입니다 3번째 버튼은 3번째 버튼입니다 */ |
cs |
위의 예제 코드를 통해 중첩 된 클로저를 사용하는 것 만으로 위와 같은 문제를 피하여 원하는 값이 나옵니다
캡슐화
슐화란 간단하게 말하면 객체의 자료화 행위를 하나로 묶고,실제 구현 내용을 외부에 감추는 겁니다.
즉, 외부에서 볼 때는 실제 하는 것이 아닌 추상화 되어 보이게 하는 것으로 정보은닉에 쓰입니다.
JavaScript는 이와 같은 캡슐화를 클로저를 사용하여 구현합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
function Gugudan(dan){ var maxDan =3; this.calculate = function(){ for(var i =1; i < =maxDan; i + +){ console.log(dan +"*" +i +"=" +dan *i); } } this.setMaxDan = function(reset){ maxDan = reset; } } var dan5 = new Gugudan(5); dan5.calculate();
dan5.maxDan =2; dan5.calculate();
dan5.setMaxDan(2) dan5.calculate(); //실행결과 /* 5*1=5 5*2=10 5*3=15
5*1=5 5*2=10 5*3=15
5*1=5 5*2=10 */ |
cs |