도도의 IT이야기
OOP Mixin과 클로져를 이용한 private 속성 본문
Mixin 함수
우선 지금까지 inheritance를 이용해 속성들을 상속받는 방법을 알아봤습니다.
function Machine(){}
Machine.prototype = {
material : 'matal',
fuel : 'gasoline',
engineOn : function(){return "출발";}
}
function Car(brand, color){
this.brand = brand;
this.color = color;
}
Car.prototype = Object.create(Machine.prototype);
Car.prototype.wheels = 4;
Car.prototype.constructor = Car;
function Scooter(brand, color){
this.brand = brand;
this.color = color;
}
Scooter.prototype = Object.create(Machine.prototype);
Scooter.prototype.wheels = 4;
Scooter.prototype.constructor = Scooter;
let car1 = new Car('Honda','blue');
let scooter1 = new Scooter('Kia','red');
이렇게 Car과 Scooter 생성자를 만들었는데
Car과 Scooter은 서로 연관이 있는 생성자이기 때문에 prototype객체에 공통되는 속성들이 많습니다. 그러기에 상위 생성자인 superclass Machine을 만들어 Machine.prototype에 공통되는 속성들을 포함했습니다. 다음에 Car.prototype과 Scooter.prototype을 Machine의 인스턴스로 만들어 그들이 Machine.prototype의 있는 속성들을 상속하게 했습니다.
하지만 서로 연관되어 있지 않지만 공통된 속성이 한두개 존재한다면?
function Bird(name){
this.name=name;
}
Bird.prototype.fly = function(){return 'flying';};
function Plane(number){
this.number=number;
}
Plane.prototype.fly = function(){return 'flying';};
let bird1 = new Bird('cutie');
let plane1 = new Plane(707);
이런 식으로 새와 비행기 객체를 만드는 생성자를 만들어 보았습니다. 새와 비행기는 관계가 없지만 둘다 날 수 있다는 하나의 공통점을 가지고있습니다.
이렇게 하나의 공통점만 공유하고 있는 객체들이 있다면 inheritance를 사용하기 위해 또다른 생성자를 만드는 일은 불필요합니다.
우리는 이때 Mixin이라는 함수를 사용할 수 있습니다. Mixin은 어떠한 객체를 인자로 호출하면 파리미터로 받은 객체에 어떠한 속성을 추가하는 역활을 합니다.
위 예제에선 Bird.prototype과 Plane.prototype이 공유하는 메소드를 가지고 있죠? 그럼 이들을 파라미터로 받아 그들에게 fly메소드를 추가하는 함수를 만들어 봅시다.
function Bird(name){
this.name=name;
}
function Plane(number){
this.number=number;
}
function flyMixin(obj){
obj.fly = function(){return 'flying';};
}
flyMixin(Bird.prototype);
flyMixin(Plane.prototype);
let bird1 = new Bird('cutie');
let plane1 = new Plane(707);
이런 식으로 Mixin을 만들어 사용할 수 있습니다.
물론 prototype객체가 아닌 일반 객체에도 사용할 수 있습니다.
let fish = {
name:'Nemo',
color:'Orange',
isHappy: true,
}
let submarine = {
material:'metal',
lightOn:true,
navigation:'Sonar'
}
function swimMixin(obj){
obj.swim = function(){return 'swimming!';};
}
swimMixin(fish);
swimMixin(submarine);
클로져
하지만 클로저를 알아보기 전에 스코프를 알아봅시다.
스코프/범위
스코프란 어떠한 변수가 사용될 수 있는 범위를 가르킵니다. 즉 { 부터 } 까지를 스코프라고 부름
스코프는 함수 또는 block statement(for, if ,while, 등등)으로 만들어집니다. 함수가 만든 스코프는 function scope. block이 만든 스코프는 block scope이다.
var은 function scope를 가지고 있고 function { 부터 }까지에서 사용될 수 있음
let과 const는 block scope를 가지고 있습니다. block { 부터 } 까지 에서 사용될수 있음
예를 들어보죠
function func(){
var a = 5;
console.log(a)//5
}
func();
console.log(a);//reference error (선언되지도 않았다는 뜻)
var 키워드는 function scope로 변수를 선언합니다.
그 말은 var키워드로 선언된 변수는 선언된 함수안에서만 사용 가능 하다는 의미입니다.
if(true){
var a = 5;
let b = 6;
const c = 7;
}
console.log(a)//5
console.log(b)//reference error
console.log(c)//reference error
let과 const는 block scope를 가지고 있기 때문에 if라는 block 밖에서는 사용이 불가능합니다.
즉, 어떠한 변수는 자신의 스코프 밖에서 사용될수 없습니다.
스코프안에 스코프 (네스팅)
이제 스코프안에 스코프를 만들어 봅시다
function outer(){
var a = 5;
function inner(){}
}
function outer 안에 function inner을 선언했습니다.
헷갈릴 수 있으니 inner함수를 그냥 평범한 변수 라고 생각해봅시다
함수 inner 또한 function outer안에 생성된 변수이기 때문에 function outer 안에서만 사용이 가능합니다.
간단하게 말하면 이렇게 말할 수 있습니다
function outer(){
var a = 5;
var inner = function(){};//일반 function 선언은 var 변수로 선언된거랑 마찬가지이다.
//그렇기에 자신이 속해 있는 함수안에서만 사용이 가능하다
}
하지만 일반 함수 선언과 var 을 사용한 function expression의 다른점은. 일반 함수 선언은 hoisting 때문에 스코프안 어디서나 사용할 수 있는 반면 var을 사용한 function expression은 initialize가 된 후 사용가능하다.
function outer(){
var a = 5;
console.log(b)//reference error
function inner(){
var b = 10;
console.log(b)//10
}
}
만들어진 inner 함수 또한 자기만의 스코프를 가집니다. 그렇기에 inner함수 안에서 선언된 변수들은 inner안에서만 사용 가능합니다.
다시 한번 말하자면 변수가 생성된 스코프 밖에서는 그 변수를 사용할 수 없다.
그럼 function inner 안에서 function outer의 스코프에 있는 변수들을 사용할 수 있을까요?
function outer(){
var a = 5;
function inner(){
var b = 10;
console.log(a)//5
}
이렇게 보이듯이 inner 안에서 outer scope의 변수들을 사용할 수 있습니다.
즉, outer scope에서 선언된 변수들은 inner scope에서 사용가능하다.
이 두개를 기억합시다
1. inner scope에서 선언된 변수들은 outer scope에서 사용 불가능
2. outer scope에서 선언된 변수들은 inner scope에서 사용가능.
그런 inner scope에서 어떻게 바깥 변수들을 사용할수 있는걸까요?
렉시컬 스코핑 lexical scoping
바로 렉시컬 스코핑이라는 자바스크립트의 방식 때문입니다.
자바스크립트는 코드를 실행하기 전에 렉싱 타임이라는 것을 갖습니다.
이 시간동안 자바스크립트는 이런 일을 합니다.
1. inner 스코프를 nesting하고 있는 outer 스코프를 찾습니다.
2. inner 스코프가 outer스코프의 변수들을 사용할 수 있게합니다.
그리고 inner 스코프가 사용할 수 있는 변수의 스코프를 렉시컬 스코프라고 합니다
function a(){
function b(){
function c(){
}
}
}
그렇기에 function c의 렉시컬 스코프는 자기 자신을 포함한 function c, function b, function a, 그리고 global window 가 되겠죠.
이 코드를 봅시다
function outer(){
let a = 8;
function inner(){
console.log(a); //8
}
}
inner 함수는 자기자신을 포함한 모든 outer 함수들의 변수들을 사용할 수 있습니다.
그럼 렉시컬 스코핑이 이 코드에서 어떤 방식으로 작용하는지 보죠
1. outer 함수가 선언됬고 안에는 a가 선언됬군
2. outer 함수 안에 inner함수가 선언됬군
3. inner 안에 선언 키워드(var, let , const)를 사용하지 않은 변수가 있네?
4. 우선 inner안을 탐색한다 하지만 a의 선언은 아무데도 없었다
5. 다음 outer을 탐색한다 a의 선언이 있다. inner의 a의 값을 outer a의 값으로 하자
하지만 이런 상황이 있다고 해보자
function outer(){
let a = 8;
function inner(){
let a = 1;
console.log(a);
}
}
여기서 렉싱 타임 도중에 a의 선언이 inner안에서 발견 됬기 때문에 a의 값을 1로 한다.
그럼 a의 선언이 어디에도 발견되지 않으면 어떻게 될까요? a는 최상위 스코프인 global까지 올라갑니다. 거기에도 a의 선언이 발견되지 않자 a는 그냥 글로벌 스코프에서 주저 앉습니다. 그렇게 글로벌 스코프를 가지게 되는 것
Closure 클로저
이제 우리는 렉시컬 스코핑을 이용해서 밖에있는 변수들을 안에서 사용할수 있다는 점을 압니다.
function outer(){
let a = 10;
function inner(){
console.log(a);//10
}
inner():
}
outer();
이것은 우리가 지금까지 배운점입니다.
하지만 함수의 호출이 밖에서 일어나면 어떻게 될까요?
function outer(){
let a = 10;
function inner(){
console.log(a);//10
}
return inner;
}
let X = outer();
X();
outer 함수는 inner함수를 리턴합니다 그리고 inner함수는 X라는 변수에 담깁니다.
이제 X라는 변수를 통해서 inner함수를 바깥에서 호출하면 어떻게 될까요?
결과는 같습니다
여기서 조금 어려워 집니다.
우선 위의 코드를 다시 차근차근 짚어봅시다
1. outer()함수가 호출되고 inner함수가 리턴된다.
2. 리턴된 inner함수가 X라는 변수에 담긴다.
여기 까지를 한번 코드에 쳐보죠.
function outer(){
let a = 10;
function inner(){
console.log(a);//10
}
return inner;
}
let X = function inner(){//리턴된 inner함수가 X에 담김
console.log(a);
}
X();
3. outer함수는 inner을 return을 하는 자신의 목적을 완수했기 때문에 데이터에서 잠시 사라집니다
let X = function inner(){//리턴된 inner함수가 X에 담김
console.log(a);
}
X();
이런 식으로 사라지지만 아예 사라진건 아닙니다! 호출되면 다시 나타날 것입니다. 더 설명하자면 함수는 자신이 호출될때 데이터에서 불려와서 실행되고 안에있는 로직을 행하고 마지막으로 return값을 반환한 다음에 다시 호출될때까지 사라집니다.
4. 이제 X에 담긴 inner함수를 호출합시다.
그래서 inner함수를 호출하니... 응? a의 값을 console.log해야하는데 a의 선언이 없습니다 일단 inner함수안에도 없고 global에도 없습니다. 그러면 reference error가 뜰까요? 아닙니다.
우리는 여기서 CLOSURE 클로저라는 컨셉을 이해해야합니다.
3번으로 돌아갑시다. 우리는 outer함수가 자신의 목적을 다했기에 사라진다고 했습니다.
function outer(){
let a = 10;
function inner(){
console.log(a);//<=== 여기서 inner 함수가 outer함수의 a값을 사용한다는 것을 기억함
}
return inner;
}
let X = outer();
X();
여기서 inner 함수가 return이 되고 outer함수가 사라지기 전에! 마법같은 일이 일어납니다.
inner함수는 자신의 렉시컬 스코프에 있는 outer 함수의 a가 필요한것을 압니다.
그러므로 inner함수가 return될때 a를 납치(capture)합니다. / 또는 자신의 렉시컬 스코프 체인을 유지합니다.
그러고 나서야 outer은 사라집니다.
그리고 이런결과가 나옵니다.
let X = function inner(){//inner 함수가 return되기 전에 outer의 a를 납치했다는 것을 기억해야함
console.log(a);
}
X();
아까와 같은 결과지만 우리는 한가지 사실을 압니다 inner함수가 return될때 자신이 필요한 a를 납치(capture)했다는 점. 그럼 납치한 a는 어디있을까요? 우리눈에는 안보이지만 이런일이 뒤에서 일어납니다
let a = 10;
{//우리 눈에는 안보임
let X = function inner(){
console.log(a);
}
}//우리 눈에는 안보임
X();
이렇게 우리가 납치한 a는 X의 렉시컬 스코프에 아직도 존재하게 됩니다.
여기서 자신이 밖에 있어도 자신의 렉시컬 스코프에 접근할 수 있는 함수를 closure라고 합니다.
약간 헷갈리니 그림으로 표현해보죠.
1.
2.
3.
결론: 어떠한 함수/메소드가 리턴되어도 자신의 렉시컬 스코프에 존재하는 변수들을 데리고 와서 사용가능
이제 inner함수가 console.log만 하는게 아니라 capture한 변수를 변경하는 코드를 만들어봅시다.
function outer(){
let a = 0;
function inner(){
let b = 0;
console.log(a);
console.log(b);
a++;
b++;
}
return inner;
}
let X = outer();
let Y = outer();
여기서 우리는 a와b의 값을 increment하는 inner 함수를 만들어봤습니다.
let X =outer()부터 봅시다
outer()은 호출되고 안에있는 코드가 실행됩니다. 하나하나 봅시다
1. a가 선언되고 0으로 initialize된다
2. inner function이 선언된다
3. inner function이 return된다
4. inner가 리턴될떄 자신이 필요한 a의 initialization을 가지고온다(그리고 상위 스코프에 위치시킨다). 그리고 자기 역활이 끝난 outer은 사라진다. (나는 가지고 온다는 표현을 했지만 정확히 말하면 inner가 자신의 lexical scope chain을 outer 함수와 유지하는 것이다 근데 가져온다는 비유가 더 쉽게 그려져서 이렇게 해봄)
5. Y에도 같은 일을한다
{//나는 이 imaginary wrapper을 mini global이라고 부른다
let a = 0;//inner가 return될때 자기가 필요한것을 데리고온다
let X = function inner(){
let b = 0;
console.log(a);
console.log(b);
a++;
b++;
};
}
{
let a = 0;//inner가 return될때 자기가 필요한것을 데리고온다
let Y = function inner(){
let b = 0;
console.log(a);
console.log(b);
a++;
b++;
};
}
여기서 중요한 포인트!!!
보다시피 outer가 실행될때마다 새로운 카피의 a를 데리고온다. 그래서 X에서 뭘하든 Y의 값에는 변함이 없다.
이제 X를 호출해보자
X(); a:0, b:0
X(); a:1, b:0
X(); a:2, b:0
Y(); a:0, b:0
X를 호출하면 우선 b가 0으로 initialize가 된다 그래서 X를 몇번 호출하든 b의 값은 그대로.
하지만 a는 우리가 상위 wrapper에 두었기 때문에 마치 글로벌 변수 처럼 a값은 계속 변한다.
앞서 말했듯 outer가 실행될때마다 새로운 inner을 리턴받고 그에따른 새로운 카피의 a를 데려온다. 그래서 X에 한 모든 a의 변화들이 Y에는 미치지않는다.
이제 factory function으로 예를들어보자
function carFactory(brand){
return {
brand
}
}
let car1 = carFactory('kia');
//결과
car1 = {brand:'kia'}
이 factory함수는 괜찮지만 문제가 있다. 아무나 car1.brand="benz"라고 해서 수정이 가능하기 때문이다. 그래서 global인 brand를 private하게 바꿔야한다. 이제 우리가 배운 클로저를 써먹을 수 있다.
function carFactory(brand){
return {
get_prop:function(){
return brand;
}
set_prop:function(_brand){
brand = _brand;
}
}
}
let car1= carFactory('bmw');
이렇게 우리는 리턴하는 객체안에 brand : brand 이런식으로 brand속성을 주지 않았다. 만약 이렇게 했다면 밖에서 모든 사람들이 obj.brand = "blahblah" 이렇게 바꿀 수 있기 때문. 그렇기에 우리는 brand를 건드리지 않을거다. 그대신 brand를 return하는 메소드를 만들것이다. 그리고 set_prop이라는 특별한 메소드를 이용해야만이 brand를 바꿀 수 있게했다.
한번 마지막 줄을 실행하면 어떻게 되는지 같이 차근차근 보자
1. carFactory가 호출되고 인자를 var brand = "bmw" 이런식으로 받는다.
2. 그리고 object literal을 리턴한다.
3. 여기서 앞에도 말했듯이 리턴하는 객체의 메소드(내부함수)가 외부 함수의 brand를 이용하고 있기에 리턴시, brand 변수를 enclose해서 같이 리턴한다.
4. 이제 get_prop을 하면 같이 enclose되있는 brand를 리턴하고.
5. set_prop도 마찬가지로 같이 enclose되있는 brand를 초기화한다.
근데 알다시피 factory function은 new를 사용하지않고 객체를 직접 리턴하기에 prototype을 사용할수 없다 그렇기에 좀더 실용적인 constructor을 이용하여 예를 들어보자.
function Car(color,brand){
let maker = "sean";
this.color=color;
this.brand=brand;
this.madeby=function(){return maker};
}
let car1=new Car('red','benz');
이렇게 constructor안에 로컬 변수를 만들어 그것을 리턴하는 메소드를 추가할 수 있다.
'IT > 자바스크립트' 카테고리의 다른 글
[jQuery] jquery filter (0) | 2020.05.11 |
---|---|
[jQuery] jquery와 javascript의 차이점 (0) | 2020.05.11 |
prototypical inheritance (0) | 2020.05.10 |
[자바스크립트 기본]객체,생성자,프로토타입,프로토타입 체인 (1) | 2020.05.08 |
Object.prototype.toString() 메서드 (0) | 2020.05.07 |