오늘을 자바스크립트의 데이터의 타입(원시타입,참조타입)과 스코프 그리고 클로저에 대해 공부했다.
자바스크립트의 데이터 타입은 크게 두가지로 분류할 수 있다.
원시타입의 데이터들은 모두 하나의 데이터만을 담고있다.
원시타입의 변수들은 메모리의 공간에 변수를 할당하고 그 안에 하나의 데이터를 저장한다.
그에 비해 참조타입의 변수는 메모리의 공간에 변수를 할당하고 그 안에 실제로 참조타입의 데이터가 저장되어 있는 주소를 저장한다.
let a=1;
let b=a;
console.log(a,b); // 1 1을 출력
b=b+1;
console.log(a,b); // 1 2를 출력
위의 코드에서 b에 a를 할당할 때에 메모리에 변수 b의 공간을 만들고 그 안에 a의 값 1을 저장했다.
a와 b는 각각의 공간에 1이라는 값을 저장했고 b에 1을 더한 값을 재할당해도 a의 값이 변하지 않는 것을 확인할 수 있다.
let a=[1,2,3];
let b=a;
console.log(a); // [1,2,3]
console.log(b); // [1,2,3]
b.push(4);
console.log(a); // [1,2,3,4]
console.log(b); // [1,2,3,4]
일단 참조타입은 원시타입을 제외한 모든 값들이므로 배열은 참조타입이다.
위의 코드에서 b에 a를 할당할 때에는 메모리에 변수 b의 공간을 만들고 그 안에 a의 값(실제 배열 [1,2,3]이 저장된 메모리의 주소값)을 저장했다.
따라서 a와 b에 저장된 주소값은 동일하게 [1,2,3]이 저장된 공간을 가르킨다.
그래서 b.push(4)
로 b가 가르키는 [1,2,3]에 4를 추가하면 a가 가르키는 배열도 변하는 것을 확인할 수 있다.
참조타입과 원시타입을 공부하며 예전에 c언어를 잠깐 공부했을때 봤던 포인터라는 개념이 굉장히 도움이 되었다.
값을 저장하는 개념과 실제 값이 저장된 주소값을 저장하는 개념이 프로그래밍언어에서 중요한 개념인 것 같다.
scope는 범위라는 의미를 갖는 영단어이다.
자바스크립트에서도 스코프는 범위라는 의미를 갖는다
자세하게 표현을 하면 변수접근규칙에 따른 유효범위라는 의미를 갖는다.
변수접근규칙은 다음과 같다.
스코프를 공부하면서 변수를 선언할때의 키워드에 대해서도 알아보았다.
자바스크립트에서 변수를 선언할 때에는 var,let,const 세가지의 키워드를 사용한다.
let과 const는 ECMA Script6부터 도입되었도 이전까지는 var키워드 만을 사용했다고 한다.
const는 전에 배운것 처럼 재할당이 불가능한 상수를 선언할때 사용된다.
그렇다면 var과 let은 어떠한 차이가 있을까?
var키워드로 선언된 변수는 함수레벨스코프를 갖는다. 이에반해 let키워드로 선언된 변수는 블록레벨스코프를 갖는다.
블록레벨스코프와 함수레벨스코프
블록레벨스코프는 코드블록{...} 내에서 유효한 스코프를 의미한다.
블록내부에서 선언된 변수는 블록외부에서 유효하지않다.
함수레벨스코프는 함수코드블록 내에서 유효한 스코프를 의미한다.
함수블록내부에서 선언된 변수는 함수블록외부에서 유효하지않다.//let사용 for(let i=0;i<4;i++){ console.log(i); } console.log(i) //Error //var사용 for(var i=0;i<4;i++){ console.log(i); } console.log(i) //4
var키워드는 동일한 이름을 갖는 변수를 중복해서 선언할수 있지만 let키워드는 동일한 이름의 변수를 중복해서 선언하면 문법에러가 발생한다.
호이스팅이 이루어질때도 약간의 차이점이 있다.
호이스팅
호이스팅이란 자바스크립트가 실행될 때 모든 선언문(변수,함수,클래스등 선언)들을 해당 스코프의 맨 처음으로 옮긴 것처럼 동작하는 특성을 말한다.
var키워드로 선언된 변수는 호이스팅으로 인해 스코프의 선두에서 변수생성 3단계(선언,초기화,할당)중 선언과 초기화가 이루어진다.
초기화의 단계까지 이루어졌기 때문에 실제 코드에서 변수를 선언하기 전에 호출해도 에러가 없으며 undefined라는 값을 갖는다.
let키워드로 선언된 변수는 호이스팅으로 인해 스코프의 선두에서 선언단계만 이루어진다.
아직 초기화가 되지않아 값을 갖고있지 않기 때문에 실제 코드에서 변수를 선언하기 전에 호출하게 되면 에러가 발생한다.
스코프체이닝: 현재 스코프에서 선언되지 않은 변수의 값을 찾기위해 순차적으로 바깥스코프로 접근하여 찾는 과정이다.
클로저에 대한 MDN의 정의는 다음과 같다.
클로저는 함수와 그 함수가 선언되었을 때의 어휘적 환경(Lexical environment)과의 조합이다.
//함수선언
function outerFunc(){
let x=10;
function innerFunc(){
console.log(x);
}
return innerFunc;
//outerFunc함수의 리턴값은 outerFunc안에서 선언된함수 innerFunc이다.
}
let inner = outerFunc();
inner();
// 콘솔에 10이 출력된다.
위의 코드를 실행해보면 콘솔에 외부함수의 변수(x=10
)이 출력된다.
이처럼 내부함수가 자신을 포함하는 외부함수보다 더 오래 유지되는 경우 외부함수밖에서 내부함수가 호출되더라도 외부함수의 지역변수에 접근할수 있는 이러한 함수를 클로저라고 부른다.
MDN에서 말하는 함수는 외부함수에 의해 반환된 내부함수(innerFunc)를 의미하는 것이고 그 함수가 선언되었을 때의 어휘적 환경이란 외부함수(outerFunc)의 스코프를 의미한다.
쉽게말해 inner라는 변수가 innerFunc함수객체 뿐만 아니라 innerFunc가 선언되었을 때의 outerFunc의 스코프까지 담고있기 때문에 외부함수에서 선언된 변수 x를 사용할수 있는 것이다.
클로저에 의해 참조되는 외부함수의 변수(위 코드의 경우 x)를 자유변수라고 부르며 클로저는 자유변수에 함수가 닫혀있다(closed)라는 의미로 자유변수에 엮여있는 함수라는 뜻이다.
클로저는 함수 자체만이 아닌 그 함수가 선언되었을 때의 환경까지 기억해야 하므로 메모리의 관점에서는 손해를 볼 수 있다.
하지만 선언되었을 때의 환경을 기억하는 클로저의 특성을 이용하여 객체지향의 캡슐화를 구현할 수 있다.
캡슐화란?
캡슐화란 객체지향 프로그래밍에서 객체의 속성(데이터,변수)과 행위(메서드)를 하나로 묶고, 실제 구현 내용을 감추어 은닉한다.
관련성이 많은 속성과 행위를 묶음으로써, 결합도가 낮아지고 재사용이 용이해진다.
실제 구현 내용을 감추어 은닉함으로써 고려되지 않은 영향(side-effect)들을 최소화한다.
대표적인 객체지향언어 JAVA의 경우에는 접근제어자를 이용해 외부에서 내부의 데이터에 접근하지 못하도록 한다.
자바스크립트의 클로저도 외부함수의 환경은 기억하고 있지만 직접 그 환경을 호출할수는 없다는 특성을 이용해 외부에서의 접근을 막을 수 있다.
class User{
private String name;
private int age;
public String getName(){
return name;
}
public int getAge(){
return age;
}
public void setName(String name){
this.name=name;
}
public void setAge(int age){
this.age=age;
}
}
public class main
{
public static void main(String[] args)
{
User one=new User();
one.setName("홍길동");
one.setAge(20);
System.out.println(one.getName()); //홍길동
System.out.println(one.getAge()); //20
//System.out.println(one.name); Error
//System.out.println(one.age); Error
}
}
위의 JAVA코드는 User라는 클래스를 선언할때 클래스의 모든 멤버변수들의 접근제어자를 private으로 설정했다.
멤버변수들을 호출하거나(read),쓰기위한(write) 메서드들(getName,setName,getAge,setAge)의 접근제어자는 public으로 설정했다.
private 접근제어자는 외부에서 접근이 불가능하기 때문에 직접 one.name,one.age로 변수를 호출할 수 없다.
public 접근제어자는 외부에서 접근이 가능하기에 변수를 호출하기 위해 public 메서드 getName,getAge를 사용해야한다.
이 코드를 클로저로 바꿔본다면 다음과 같다.
function userMaker(){
let name;
let age;
function getName(){
return name;
}
function setName(_name){
name=_name;
}
function getAge(){
return age;
}
function setAge(_age){
age=_age;
}
return {getName,setName,getAge,setAge};
}
let temp=userMaker();
temp.setName('홍길동');
temp.setAge(20);
console.log(`${temp.name}의 나이는 ${temp.age}입니다.`);
//undefined의 나이는 undefined입니다.
console.log(`${temp.getName()}의 나이는 ${temp.getAge()}입니다.`)
//홍길동의 나이는 20입니다.
외부에서는 name과 age에 접근할수 없다.(undefined)
하지만 userMaker가 리턴한 함수들을 이용해 name과 age를 수정하거나 호출할 수 있다.