Javascript의 scope와 Closure

haileyself·2019년 12월 26일
0

Javascript의 scope란?

스코프란 현재 접근할 수 있는 변수들의 범위를 뜻한다, 현재 위치에서 볼 수 있는 변수들의 범위.
어떠한 변수가 스코프 안에 선언되었으면 해당 스코프 안에서는 변수에 접근해서 읽거나, 쓸 수 있고 스코프 밖에서는 해당 변수에 접근할 수 없다.

변수는 전역 또는 코드 블록(if, for, while, try/catch 등)이나 함수 내에 선언하며 코드 블록이나 함수는 중첩될 수 있음.

var x = 'global';

function foo() {
var x = 'function scope';
console.log(x);
}

foo();  // function scope

console.log(x); // global

해당 식을 보면 서로 x라는 이름으로 값을 할당한 변수이나, foo라는 함수 안에서 선언된 변수 x는 함수 foo 내부에서만 참조할 수 있고,
함수 외부에서는 참조할 수 없다. 이런 규칙이 스코프다!

scope의 생성

자바스크립트는 다른 언어와는 달리 일반적인 블록 스코프를 따르지 않음.
자바스크립트의 스코프는 특정 구문이 실행될 때 생성하여, 스코프 체인을 생성하게 됨.
scope를 생성하는 구문은 다음과 같다.

  • function
  • with
  • catch
    자바스크립트에서 이들의 사용법은 각각 다르지만, 중요한 것은 이런 구문들이 사용될 때만 스코프가 생성되고
    다른 프로그래밍 언어처럼 {} 를 이용해 블록을 생성한다고 해서 스코프가 생성되는 것이 아니라는 점이다.

1. function 구문의 스코프 생성

function foo() {
 var b = "Can you acess me?";
}
console.log(typeof b === "undeifined");
// false
// function구문을 통해서 스코프가 생성됨, foo 함수 외부에서 내부 선언된 변수에 접근할 수 없다.

2. catch 구문의 스코프 생성

with와 cath 구문도 스코프를 생성하기는 하지만, function과는 다소 다른 동작을 보여줌
이 두 구문은 괄호 안에 인자로 받는 변수들만 새로운 내부 스코프에 포함되어 그다음으로 오는 블록 안에서만 접근가능.

try {
  throw new exception("fake exception")
} catch (err) {
  var test = "can you see me";
  console.log(err instanceof ReferenceError === true);
}
console.log(test === "can you see me");
console.log(typeof err === "undefined");
//출력 :true / true / true 
// catch 구문 내부의 파라미터로 넘겨지는 err 변수는, catch 블록 내부에서 접근 가능하나,
// catch 함수 외부에서는 접근 불가능
// test 변수는 catch 블록 외부에서도 접근 가능. 

3. with구문의 스코프 생성

위의 catch 구문과 같이, 괄호 안에 인자로 받는 변수들만 새로운 내부 스코프에 포함되어, 그 다음으로 오는 블록 안에서만 접근 가능

with ({inScope: "You can't see me"}) {
  var notInScope = "but you can see me";
  console.log(inScope === "You can't see me");
}
console.log(typeof inScope === "undefined");
console.log(notInScope === "but you can see me");
// 출력값: true / true / true

with 구문도 파라미터로 받은 변수만 스코프 내부에서 접근할 수 있다.
with 구문은 eval구문과 함께 자바스크립트 개발자 사이에서 사용하지 말아야 할 구문 중 하나....
더 심오하게는 들어가지 않기로 한다...

Lexical Scope 렉시컬 스코프

렉시컬 스코프는 함수를 어디서 호출하는지가 아니라 어디서 선언하였는지에 따라 결정된다.
자바스크립트는 렉시컬 스코프를 따르므로 함수를 선언한 시점에 상위 스코프가 결정된다.
함수를 어디에서 호출하였는지는 스코프 결정에 아무런 의미를 주지 않는다.

var x = 1;

function foo() {
  var x = 10;
  bar();
}

function bar() {
  console.log(x);
}

foo(); 
bar();
//출력값: 1  / 1

위의 bar는 어떤 함수 내부에 선언되지않고, 전역에 선언되었다.
따라서 함수 bar의 상위 스코프는 전역 스코프이고, 위 예제는 전역변수 x의 값 1을 두번 출력한다.

클로저(Closure)란?

클로저는 특정 함수가 참조한 변수들이 선언된 렉시컬 스코프는 계속 유지되는데, 그 함수와 스코프를 묶어서 클로저라고 함.
클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경과의 조합이다.

function outerFunc() {
  var x = 10;
  var innerFunc = function () { console.log(x); };
  innerFunc();
}

outerFunc(); // 10

함수 outerFunc 내에서 innerFunc이 선언되고 호출됨. 이 때, 내부함수 innerFunc는 자신을 포함하고 있는 외부함수 outerFunc의 변수 x에 접근할 수 있음.
innerFunc가 outerFunc의 내부에 선언되었기 때문!
innerFunc의 상위스코프가 함수 outerFunc임!

클로저를 이해하기 위해서는 실행 컨텍스트를 알아야 한다.(이건 블로그 정리는 나중에..)
실행컨텍스트의 관점에서 살펴보면,
내부함수 innerFunc가 호출되면 자신의 실행 컨텍스트가 실행 컨텍스트 스택에 쌓이고, 변수객체와 스코프 체인 그리고 this에 바인딩할 객체가 결정된다.
실행컨텍스트의 스코프 체인을 자바스크립트 엔진이 검색했기때문에
innerFunc이 외부함수 outerFunc에서 변수에 접근할 수 있는 것!
(innerFunc 자신의 스코프에서 변수 검색했으나 검색실패해서 innerFun함수를 포함하는 외부함수의 스코프에서 변수 x 검색 성공!)


function outerFunc() {
  var x = 10;
  var innerFunc = function () { console.log(x); };
  return innerFunc;
}

var inner = outerFunc();
inner(); // 10

함수 outerFunc은 내부함수 innerFunc을 반환하고, 생 마감.
실행 이후, 콜스택에서 제거되어서 변수 x도 더이상 유효하지 않아보이는데, 10이 출력됨
자신을 포함하고 있는 외부함수보다 내부함수(innerFunc)가 더 오래 유지되는 경우, 외부 함수 밖에서 내부함수가 호출되더라도 외부함수의 지역변수에 접근할 수 있는데..이러한 함수를 클로저라 함

클로저는 반환된 내부함수가 자신이 선언됐을 때의 환경인 스코프를 기억하여, 해당 스코프 밖에서 호출되어도 그 환경에 접근할 수 있는 함수로 자신이 생성될 때의 환경을 기억하는 함수!

function retirement(retirementAge) {
	let a = 'years left until retirement.';
	return function(yearOfBirth) {
	let age = 2019 - yearOfBirth;
	console.log(yearOfBirth,'생년월일');
	console.log(age,'나이는');
	console.log(retirementAge - age + a);
    };
}

let retirementUS = retirement(66);
retirementUS(1991); 

출력값

retirement라는 함수는 익명함수를 리턴하고, 사라져야하는 것이 맞으나 여기서 변수 a값도 기억하고,
retirement함수에 인자로 받은 retirementAge의 값도 기억한다.
어떻게?
내부함수가 리턴되면서 클로져가 생성되었다. 클로져는 자신의 lexical scope를 기억하기때문에,
외부함수에 정의한 a 값과 외부함수로 인자로 받은 retirementAge값도 기억하는 것이다.

클로저의 활용

클로저는 자신이 생성됬을 때의 환경(Lexical environment)을 기억해야해서 메모리차원에서 손해처럼 느낄 수 있지만...아주 유용하게 사용 됨
사용되는 경우를 살펴보자

1. 전역변수의 사용억제

클로저를 사용하여 해당 변수값을 기억하게하고, 자신을 참조하는 함수가 소멸될 때까지 유지되게 한다.

function makeCounter(predicate) {
 let counter = 0;
 return function () {
 	counter = predicate(counter);
	return counter;
 };
}

function increase(n) {
	return ++n;
}
 
function decrease(n) {
	return --n;
}

const increaser = makeCounter(increase);
console.log(increaser()); // 1 
console.log(increaser()); // 2
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1 
console.log(decreaser()); // -2

makeCounter를 실행해서 반환된 함수는 자신이 생성됬을 때의 렉시컬 스코프를 기억함.
makeCounter에 속한 counter라는 변수를 기억하기 때문에, 전역변수 counter에 영향을 받지 않음
그래서 처음 increaser를 선언했을 때, makeCounter(increase) 함수가 한번 실행되면서,
counter값을 0으로 반환하고,
그다음부터 기억된 0 값을 가지고 increaser가 실행될 때마다, 기억했던 값에서 증가하는 값을 가지게 됨
decreaser도 마찬가지로, makeCounter(decrease) 가 선언될 때 실행되면서 counter값이 0이되고,
0이된 렉시컬 스코프를 기억하고 그 스코프의 변수값을 기억해서 -1, -2로 console이 나옴

만약, decreaser2 = makeCounter(decrease) 를 하고, decreaser2를 실행하면 -1 값이 뜰 것.

2. 정보의 은닉

function Counter() {
  var counter = 0;
  this.increase = function () {
    return ++counter;
  };
  this.decrease = function () {
    return --counter;
  };
}

const counter = new Counter();

console.log(counter.increase()); // 1
console.log(counter.decrease()); // 0

counter로 새로운 counter의 인스턴스를 생성하면, increase 나 decrease는 method가 되어서,
외부에서도 접근이 가능하다.
그러나, increase와 decrease는 클로저이기때문에, 생성되었을 때 scope를 기억하며
counter의 값을 기억한다.
그러나, 우리는 외부에서 counter값에 접근할 수 없다.이게바로 ! 정보의 은닉화효과다
(사실 잘 이해가 안간다 이부분....)

클로저의 단점

  • 클로저는 메모리를 소비한다.
    이벤트에 대한 콜백 함수 등으로 등록했던 함수들이 메모리에 계속
    남아있게 되면, 해당하는 클로저도 같이 메모리에 계속 남아있기때문에 루프를 돌면서 클로저를 계속 생성하는 설계는 지양해야한다!
  • 스코프 생성과 이후 변수 조회에 따른 퍼포먼스 손해가 있다.
    with구문과 마찬가지로 클로저로 생성한 스코프를 탐색해야 한다는 문제가 있음.

실행컨텍스트 (Execution Context)

scope,hoisting, this , function, closure 등의 동작원리를 담고 있는 자바스크립트의 핵심원리.
실행가능한 코드가 실행되기 위한 필요한 환경.
일반적으로 실행 가능한 코드는 전역 코드와 함수 내 코드이다.
자바스크립트 엔진은 코드를 실행하기 위해서 다음의 정보를 알고 있어야 함.

  • 변수(전역변수, 지역변수, 매개변수, 객체의 프로퍼티), 함수선언, scope, this
    let x = 'xxx';
    function foo () {
    	let y = "yyy";
    function bar() {
    	let z = "zzz";
    	console.log(x + y + z);
    }
    bar();
    }
    foo();
    //출력: xxxyyyzzz

profile
Keep on my way ! :)

0개의 댓글