(코딩앙마) 자바스크립트는 어휘적 환경(Lexical Environment)을 갖는다. 문서가 실행되면, 함수선언문/var변수 등이 호이스팅 되며 먼저 읽어지고, 그 후에 순차적으로 문서가 읽어지며 실행되는 것이 자바스크립트의 어휘적 환경의 특징이다. Closure란 함수와 어휘적 환경의 조합으로, 함수가 생성될 당시의 외부 변수를 기억하고, 생성 이후에도 계속해서 접근이 가능한 기능를 말한다.
모질라, 클로저 설명
뭔가 찜찜하다. 모질라의 설명을 추가로 살펴보자. 클로저란 함수와 함수가 선언된 어휘적 환경의 조합을 뜻한다.
function init() {
var name = "Mozilla"; // name은 init에 의해 생성된 지역 변수이다.
function displayName() { // displayName() 은 내부 함수이며, 클로저다.
alert(name); // 부모 함수에서 선언된 변수를 사용한다.
}
displayName();
}
init();
init()를 선언하면, 이에 따라 function init() 실행된다. 해당함수는 1) var name 2) function displayName() 를 각각 실행한다. 이때 function displayName() 은 부모함수인 init()에 종속되어 있다. 그러기에 부모함수 밖에서는 사용될 수 없다.
function displayName()의 작동과정을 보면 변수 name을 받아서 경고창에 띄어주게 되는데, function displayName() 자체는 내부에 지역변수를 가지고 있지 않는다. 그렇다면, name은 어디서 가져올 수 있을까? 바로 부모함수인 init()에서 선언된 변수에 접근한 것이다. 해당 변수를 가져온 function displayName()은 정상적으로 변수name에 대한 경고창을 띄울 것이다.
이 과정에서 "어휘적 범위 지정(lexical scoping)"의 한 예를 살펴볼 수 있는데, 어휘적 범위 지정란 그 과정에서 변수의 사용 과정에 있어서, 그 변수가 소스코드 내 어디에 선언되어 있는지 고려하는 과정을 의미한다. function displayName()의 변수 name에서 보았듯이, 내부에서 찾을 수 없는 변수를 부모함수 내에 선언된 변수 name에 도달하여 변수를 찾아서 실행하는 과정을 실례로 들 수 있다.
변수를 선언했던 var와 let과 const에 대한 개념이 있었다. 먼저 var는 전역스코프로 한 번 선언되면 이를 제 사용할 수 있었다. 그러나 var라 할지라도 함수에서 선언되었다면, 해당 함수에서만 접근이 가능하도록 제한되었다.(함수스코프) 반면에 let과 const는 {블록} 내부에서 선언되었다면, 해단 {블록} 밖으로 나갈 수 없었기에 블록 스코프란 이름을 지니게 되었다.
if (Math.random() > 0.5) {
var x = 1;
} else {
var x = 2;
}
console.log(x);
변수var는 그것이 함수 밖에 있느냐(전역스코프), 내에 있느냐(함수스코프)에 따라서 스코프의 범위가 달라진다. 그러나 위의 코드에서 선언된 var는 조건문 내에 있기에 전역스코프에 해당된다. x는 1인가 2인가. 나중에 선언된 var x = 2에 따라 출력될 것이다. 그런데 이러한 선언은 추후에 혼란을 일으킨다. let과 const의 등장과 이를 통한 변수 선언은 중괄호에서 선언한 변수를 제어할 수 있는 기능을 갖추게 되었다.
if (Math.random() > 0.5) {
const x = 1;
} else {
const x = 2;
}
console.log(x);
그 결과 var로 선언된 코드는 실행은 되지만, const로 선언된 코드는 "ReferenceError: x is not defined"와 같은 문구를 전달받는다. 이와 같은 에러는 "시간상 사각지대(TDZ, Temporal Dead Zone)"라는 용어로 부른다. 즉 {블록}스코프 내에 있는 변수에 console.log(x)의 x가 도달할 수 없기 때문이다.
여기서 Clusure가 등장한다. Clusure란 이 모든 스코프의 변수를 저장하고, 사용할 수 있는 개념을 말한다.
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
myFunc();를 입력하면, 순차적으로 진행될 것인데, 1) makeFunc()가 실행될 것이고 2) makeFunc()는 첫째 var name = "Mozilla"를 선언함과 둘째 return displayName를 실행시킬 것이다. 3) return displayName는 function displayName()를 실행시켜 alert(name)을 실행한다.
이 모든 과정이 myFunc()으로 실행된다. 그러나 이를 이해하기 전에 var myFunc = makeFunc()를 기억해야 한다. function makeFunc() - return의 값이 var myFunc에 대입되어 있다는 사실이다. 이는 위에서 선언했던 init()함수와 다르다.
function makeAdder(x) {
var y = 1;
return function(z) {
y = 100;
return x + y + z;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
//클로저에 x와 y의 환경이 저장됨
console.log(add5(2)); // 107 (x:5 + y:100 + z:2)
console.log(add10(2)); // 112 (x:10 + y:100 + z:2)
//함수 실행 시 클로저에 저장된 x, y값에 접근하여 값을 계산
모질라의 두번째 예시이다. console.log(add5(2));를 먼저 살펴보자.
부모함수의 변수(함수스코프)에 변수가 저장되었고 이것이 저장되었다가(클로저), 이를 자녀함수의 변수에서 가져와서(클로저) 결과를 도출하였다. 이를 코드적 이해에서 살펴보자.
makeAdder(인자)는 function makeAdder(매개변수)를 일으킨다. 그 결과 x와 y의 변수에 대한 대입이 이뤄진다. 그리고 해당 대입은 add5/add10에 전달된다. 이때 add5/add10를 클로저라고 부른다. 그런데 코드를 보면 부모함수에 아직 실행되지 못한 부분이 있다. 만약 add5/add10(인자)를 주지 않으면 다음과 같이 콘솔에 기록된다.
function(z) {y = 100; return x + y + z;} 이 실행되지 못하여 그대로 return 되었다. 그런데 보면 매개변수 z의 값이 없을 뿐 x와 y의 값은 주어졌다. 즉 자녀함수가 부모함수로부터 가져온 값을 가지고 있다는 이야기 이다. 단지 매개변수z에 대한 인자가 주어지지 않은 것뿐이다. 즉 클로저로 x와 y의 전달되었다는 것이 "어휘적 범위 지정(lexical scoping)"로 클로저를 설명하고자 했던 부분인 것이다.
모질라의 설명은 클로저의 실용적인 사용에 대해서 다음과 같이 말한다.
클로저는 어떤 데이터(어휘적 환경)와 그 데이터를 조작하는 함수를 연관시켜주기 때문에 유용하다. 이것은 객체가 어떤 데이터와(그 객체의 속성) 하나 혹은 그 이상의 메소드들을 연관시킨다는 점에서 객체지향 프로그래밍과 분명히 같은 맥락에 있다. 결론적으로 오직 하나의 메소드를 가지고 있는 객체를 일반적으로 사용하는 모든 곳에 클로저를 사용할 수 있다.
글자 크기를 클로저를 사용하여 적용해보자.
적용된 사례, 모질라
위의 사이트로 접속해서 12/14/15를 각각 클릭해보자. 이런 식으로 클로저는 사용된다.
먼저 javascript
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
size12는 함수 makeSizer(12)의 값을 저장하고 있고, size14는 함수 makeSizer(14)의 값을 저장하고 있고, size16는 함수 makeSizer(16)의 값을 저장하고 있다. 즉 3개의 클로저가 생성된 것이다.
각각의 클로저들은 document.getElementById().onclick에 따라 html에서 size-12/14/16을 id값으로 가지고 있는 버튼들이 실행될 때, 클로저 size12/14/16을 각각 가져갈 것이다.
javascript가 적용될 HTMl
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>
버튼(코드에서는 a태그)이 실행될 때마다, 저장된 클로저가 실행될 것인데, 자녀함수의 내용을 보면, body 태그의 스타일로 폰트사이즈에 해단 클로저의 px을 적용하는 것이었다. 즉 웹에서 a태그를 클릭하면, css에 가변적인 효과를 줄 수 있는 것이다.
a태그 href='#'이란
html에 설정된 특정한 id의 위치로 이동할 수 있는 것을 말한다. 만약 href="#some-id" 로 기록되었다면, 클릭하는 순간 해당 html의 id="some-id"의 위치로 스크롤 되는 것이다. 그러나 href='#'와 같이 빈값이면, 해당 페이지의 최상위 위치로 스크롤이 이동된다.
적용될 CSS
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 12px;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
이에 따라 자녀함수에서 설정한 선택자(body)태그의 속성값이 변경되는 것을 확인할 수 있다. 여기서 em단위는 px에 대한 상대적 크기이다. 1.5배, 1.2배이다. 모질라는 이어서 "클로저를 이용해서 프라이빗 메소드 흉내내기", "클로저 스코프 체인", "루프에서 클로저 생성하기" 등을 다루는데, 코린이로써 클로저에 대한 이해를 목표로 하였고, 이를 달성했기에 여기서 마치고자 한다. 자세한 공부는 심화과정에서 살펴볼 예정이다. 여기서 Edwin은 Html의 문법을 하나 의도치 않게 배운 것에 만족한다.(a태그 href='#')
setTimeout은 함수에 인자를 줄 때, 함수명과 시간을 전달한다. 이때 시간은 1000 = 1s 에 해당된다.
function fn() {
console.log(3);
}
setTimeout(fn, 3000);
이렇게도 동일하게 작성이 가능하다.
setTimeout(function() {
console.log(3);
}, 3000);
만약 매개변수로 전달해줄 인자가 존재한다면, 시간 뒤에 인자를 기록해도 된다. setTimeout(fn, 3000, "인자")
const tId = setInterval(() => console.log(3), 2000)
작동방식은 setTimeout과 동일하지만, 반복해서 실행된다는 점이 다르다. 중간에 중지하고 싶다면, clearTimeout(tId)를 선언하면 된다.
let num = 0;
function showTime() {
console.log(`접속하신지 ${num++}초가 지났습니다.`)
}
setInterval(showTime, 1000);
const tId = setTimeout(() => alert('안녕하세요.'), 3000)
setTimeout은 함수표현식으로 위와 같이 기록할 수도 있다. 그런데 clearTimeout은 위와 같이 선언된 함수표현식의 실행을 무마시키다. 함수표현식이 선언되는 사이에 먼저 clearTimeout이 실행되기 때문이다.
const tId = setTimeout(() => alert('안녕하세요.'), 3000)
clearTimeout(tId);
let num = 0;
function showTime() {
console.log(`접속하신지 ${num++}초가 지났습니다.`)
if(num > 5) {
clearInterval(tId)
}
}
const tId = setInterval(showTime, 1000);
함수표현식 tId는 기록함과 동시에 실행된다. 그러나 showTime() 안을 보면, 조건문으로 num>5라면 작동을 중지한다고 기록하였더니, 5초까지 실행되고 난 후에 함수가 종료된 것을 볼 수 있다. 이런 식으로 함수를 시간의 개념에서 사용할 수 있다. 기억할 것은 1s는 1000이다.
지연시간은 0이라고 기록해도 바로 작동하지 않는다. 이는 웹 브라우저의 특징과 Javascript의 진행 때문이다.
위의 사진에서도 확인할 수 있듯이 1이 먼저 콘솔에 기록되고, 2가 나중에 기록된다. 그리고 웹 브라우저는 기본적으로 4ms 정도 로딩시간이 존재한다.
const Adwin = {
name : "Adwin",
};
const Bdwin = {
name : "Bdwin",
};
function showThisName() {
console.log(this.name);
};
showThisName()
showThisName.call(Adwin)
함수를 호줄 할 때 매개변수를 주지 않으면, 함수에 따라 this는 해당 문서의 최상위 인 웹브라우저를 호출하여 그 이름을 적어줄 것이다. 그래서 이미지에서는 해당 이미지의 웹브라우저인 'CodePen'이 기록되었다. 반면 매개변수로 Adwin을 준 두번째 함수는 this의 값이 Adwin에 접근하여, 그 객체의 property의 name에 해당되는 값을 호출한 것을 볼 수 있다.
const Adwin = {
name : "Adwin",
};
function update(birthYear, occupation) {
this.birthYear = birthYear;
this.occupation = occupation
};
console.log(Adwin)
update.call(Adwin, 1990, 'singer');
console.log(Adwin)
함수명.call(매개변수)는 이렇게도 활용된다. 기존의 객체에 자료를 업데이트 하고자 할 때, 직접 객체에 추가하는 방법도 있지만, 반복적으로 작업할 일이라면, 함수를 만들어 관리하는 것이 보다 유용할 것이다. 객체 새로운 property를 추가하는 방법은 점표기법과 대괄호 표기법이 있었다. 함수를 사용하지 않는다면 아래의 방법이 있었다.
const Bdwin = {
name : "Bdwin",
};
Bdwin.birthYear = 1990;
Bdwin.occupation = 'singer';
이러한 작업이 반복된다면, 함수를 사용하는 것이 훨씬 유용할 것이다.
개념은 call 메소드와 동일하다. 차이가 발생되는 것은 call은 매개변수를 직접 받는 것에 비해서, apply는 배열로 정보를 받아서 처리한다는 점이다.
update.call(Adwin, 1990, 'singer');
update.apply(Bdwin, [1990, 'singer']);
const num = [1,2,-1, -130, 1002, 123];
console.log(Math.max(num)); // NaN, 배열을 풀어서 접근할 수 없기에 읽지 못한다.
console.log(Math.max(...num)) ; // 반면에 spread로는 접근할 수 있기에, 1002를 호출한다.
위의 방법을 call과 apply의 this를 활용하여 풀어볼 수도 있다. 이때, apply는 배열을 받지만, call은 배열을 받지 못한다는 점을 기억하자.
const num = [1,2,-1, -130, 1002, 123];
console.log(Math.max.call(this, ...num));
console.log(Math.max.apply(this, num));
여기서 사용된 spread(...변수)는 문자열/숫자열/배열/객체에 있는 내용을 하나씩 가져온다는 것을 지칭하는 문법임으로 배열을 풀어서 가져올 것이다. 그 가운데, 가장 큰 수를 찾아내는 것이었다.
bind 메소드도 이전의 용례 쓰임은 동일하지만, 동작 방식에 차이가 있다.
const Adwin = {
name : "Adwin",
};
function update(birthYear, occupation) {
this.birthYear = birthYear;
this.occupation = occupation;
};
const updateAdwin = update.bind(Adwin);
updateAdwin(1990, "police");
console.log(Adwin);
조금더 bind 메소드에 대해서 살펴보자.
const students = {
name : "Adwin",
showName() {
console.log(`hello, ${this.name}`);
},
};
let fn = students.showName;
let boundFn = fn.bind(students);
students.showName();
fn();
boundFn();
fn.call(students);
fn.apply(students);
const students = {
name : "Adwin",
showName() {
console.log(`hello, ${this.name}`);
},
};
const teacher = {
name : "John",
};
let fn = students.showName;
let boundfn = fn.bind(students);
fn.bind(students);
boundfn();
그런데 fn.bind(students)를 단독으로 호출했을 때는 작동하지 않았다. 변수 안에 담으면 작동하고, 변수밖에서는 왜 작동하지 않는지 이유를 모르겠다.
모질라, bind 정리
bind메소드는 새로운 함수를 생성한다. 이때 기록하는 인자의 값을 this로 할당하고, 이어지는 인자들은 함수의 인수로 제공한다.
구문 : func.bind(thisArg[, arg1[, arg2[, ...]]])
모질라의 설명에 따르면, 호출 방법과 관계없이 특정 this값으로 호출되는 함수를 만드는 것이다. 코린이들이 기대하는 바는 객체로부터 메소드를 추출한 뒤 그 함수를 호출할 때, 원본 객체가 그 함수의 this로 사용될 것이라는 기대이다. 그러나 특별한 조치가 없으면, 대부분의 경우 원본 객체는 손실된다. 원본 객체가 바인딩 되는 함수를 생성하려면, 이러한 문제를 bind나 call, apply를 통해서 접근해야 한다. 이때 call과 apply는 바로 사용되지만, bind는 대입연산자를 통해서 연결을 해주어야 사용이 가능한 것 같다.
this.x = 9;
var module = {
x: 81,
getX: function() { return this.x; }
};
module.getX(); // 81
var retrieveX = module.getX;
retrieveX();
위의 코드에서 보면 var retrieveX를 통해서 module.getX() 메소드를 별도의 변수에 저장했다. 이것이 위에서 기록한 "객체로부터 메소드를 추출한 뒤 그 함수를 호출할 때" 이다. retrieveX(); 를 통해서 호출하면 그 값은 this.x = 9;로 설정한 가장 this의 어휘적 환경을 찾아갈 것이다. 즉 객체로부터 메소드가 분리된 순간, {블록}스코프에 접근하지 못하는 것이다.
var boundGetX = retrieveX.bind(module);
boundGetX(); // 81
직전의 코드에 이를 덧붙이는 방법으로 bind() 바인드메소드를 설정해 줄 수 있다. 인자로 변수 module을 받음으로 this를 할당하겠다는 것이다. 코린이로는 여기까지 밖에 이해를 못하겠다. 이어지는 개념은 아직 많이 어렵다. 이유식 먹어야 되는 단계인데, 딱딱한 바게뜨가 왔으니,, 씹지를 못하겠다. pass.
author. EDWIN
date. 23/02/01