본문 바로가기

자스핑

[week3] this is ..

 

안녕하세요 ❕ 물결웹팟 OB 박채연입니다 😋 

 

이번주 자스핑에서 다룰 주제는 자바스크립트의 this / callback / closure 인데요,

그 중에서도 저는 정말 생소했던 개념인 this, closure 에 대해 정리해봤습니다! 

정말 어렵고 헷갈리는 개념들이라 자스에 정말 deep dive 할 수 있었던 시간이었습니다 .. 💨

 


🍀 This

this는 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수입니다.

자바스크립트의 경우, 다른 언어와 조금 다르게 동작한다고 하는데요!

자바스크립트에선, 함수가 호출될 때 this가 결정되기 때문입니다.

 

함수를 호출하는 방식에 따라 this가 가리키는 대상이 달라진다는 것은,

함수를 어떤 식으로 호출했느냐에 따라 어떻게 가리킬 대상을 정하는지 알아야 한다는 거겠죠 !

 

1️⃣ 전역 공간에서의 this

전역 공간에서 this는 window / global과 같은 전역 객체를 가리킵니다.

 

2️⃣ 함수 호출 시 this

함수를 호출하는 순간, 이 함수를 실행하는 주체는 전역에 있는데요,

따라서 함수 호출 시 this도 전역 객체를 가리킵니다.

function a() {
	console.log(this);
}

a(); // 호출한 대상이 전역


fucntion b() {
	function c() {
		console.log(this);
	}
	c(); 
}

b(); // 얘도 전역객체

 

a()의 경우, 호출한 대상이 전역이란 걸 바로 이해할 수 있지만,

c()의 경우, 함수 b 안에서 실행되었기 때문에 전역 공간이 아닌 것처럼 보이지만,

c()도 전역 객체를 가리킨다고 합니다.

 

이게 ..

좋게 말하면 자바스크립트 고유의 특성이고, 나쁘게 말하면 버그라고 불리는 이유 중 하나더라구요?

그래서 ECMAScript6에서 this 바인딩을 하지 않는 arrow function 즉, 화살표 함수가 나타났다고 합니다!

화살표 함수는 따로 this 바인딩을 하지 않고, 바로 위 컨텍스트에 있는 this를 그대로 가져다 쓰기 때문에

저런 오류같은 상황을 만들이 내지 않는다고 합니다 🤔

 

 

3️⃣ 메서드 호출 시 this

메서드로 호출하는 경우, 호출한 대상 객체가 this에 해당합니다.

var d = {
	e: function() {
		function f() {
			console.log(this);
		}
		f(); // 함수로서 호출 > 전역객체
	}
}
d.e(); // 메서드로서 호출 > d

 

 

메서드 내부 함수에서 함수를 그냥 호출하는 경우,

2️⃣ 함수 호출 시의 this 처럼 전역을 가리키는 상황이 발생하기도 합니다.

아래 코드에서 c() 함수를 호출하면, 함수가 전역공간을 this로 삼기 때문에 this.a가 10이 되는거죠!

var a = 10;
var obj = {
	a: 20,
	b: function() {
		console.log(this.a); // 20
		
		function c() {
			console.log(this.a); // 10 전역변수가 전역객체의 프로퍼티로 동작
		}
		c();
	}
}

obj.b();

 

이를 우회하기 위해 this를 내부 함수보다 상위에 다른 변수(self)에 담아,

메서드로 호출하는 형태(self.a)로 구현하기도 합니다.

var a = 10;
var obj = {
	a: 20,
	b: function() {
		var self = this; // 내부 함수보다 상위에서 self에 this 담기
		console.log(this.a); // 20
		
		function c() {
			console.log(self.a); // 메서드로 호출 
		}
		c();
	}
}

obj.b();

 

내부함수는 자신의 렉시컬 환경에서 self를 찾고,

해당 환경에 self가 없으니 상위 스코프로 올라가, 외부에 있는 self를 찾겠죠?
이 self엔 앞서 들어온 this가 들어있고, 이 this는 obj.b를 호출할 때의 this니까 obj를 가리키게 됩니다.

 

이런 우회법도 많이 쓰이지만,

최근 자바스크립트에선 화살표 함수를 사용하는 방식으로 더 간단하게 해결할 수 있습니다.

var a = 10;
var obj = {
	a: 20,
	b: function() {
		console.log(this.a); // 20
		
		const c = () => {
			console.log(this.a); // 20
			// 화살표 함수는 this를 바인딩하지 않으니까 상위 this를 그대로 씀
			// es5에서도 call/apply를 이용해 처리 가능
		}
		c();
	}
}

obj.b();

 

 

 

4️⃣ callback 호출 시 this

기본적으론 함수 내부에서와 동일합니다.

var callback = function() {
	console.dir(this);
};
var obj = {
	a: 1,
	b: function(cb) {
		cb(); // 함수로 호출했기 때문에 전역객체가 나옴
	}
};

obj.b(callback)

 

위 코드에선, cb() 를 함수로 호출했기 때문에 this는 전역 객체를 가리키고

 

var callback = function() { 
	console.dir(this);
};
var obj = {
	a: 1,
	b: function(cb) {
		cb.call(this); // obj
	}
};

obj.b(callback)

 

이 코드에선, 메서드로 호출했기 때문에 obj를 가리키게 됩니다.

 

이처럼, 콜백함수의 경우, this가 무엇을 가리키는지 콜백함수 자체가 결정하는 것이 아닙니다.

콜백함수를 넘겨받는 대상이 어떤 식으로 처리하는지에 따라 달라지게 됩니다 !

 

5️⃣ 생성자 함수 호출 시 this

new 연산자는 생성자 함수의 내용을 바탕으로 인스턴스 객체를 만드는 명령입니다.

따라서, 새로 만들 인스턴스 객체 그 자체가 곧 this가 되는 거죠!

 

function Person(n, a) {
	this.name = n;
	this.age = a;
}
var roy = Person('재남', 30); // 함수로서 호출 > 전역객체로 할당이 됨
console.log(window.name, window.age);
function Person(n, a) {
	this.name = n;
	this.age = a;
}
var roy = new Person('재남', 30); 
// 새로 생성될 Person의 인스턴스 객체 자신(roy)이 곧 this가 됨
console.log(roy);

 

 


 

🍀 Closure

클로저란, 함수와 그 함수가 선언된 렉시컬 환경과의 조합을 의미합니다.

즉, 함수가 선언될 당시에 주변 환경에 함께 갇히는 것을 말하는데요!

함수가 속한 렉시컬 스코프를 기억하여, 함수가 렉시컬 스코프 밖에서 실행될 때도

해당 스코프에 접근할 수 있게 해주는 기능이라고도 할 수 있습니다!

 

 

1️⃣ [[Environment]]

자바스크립트 엔진은 함수를 어디에 정의했는지에 따라 상위 스코프를 결정하는데요! ( = 렉시컬 스코프)

렉시컬 스코프가 가능하려면 함수는 자신이 정의된 환경, 즉 상위 스코프를 기억해야 합니다.

이를 위해 함수는 자신의 내부 슬롯에 자신이 정의된 환경을 저장하는데,

이때 사용되는 게 바로 [[Environment]] 입니다.

 

 

2️⃣ 클로저에 대해 알아보자

const x = 1;

function outer() {
	const x = 10;
	const inner = function() {
		console.log(x);
	};
	return inner
}

const innerFunc = outer();
innnerFunc();

 

위 코드를 가지고 클로저 개념에 대해 알아봅시다.

 

우선, outer 함수를 호출하면 outer 함수는 중첩함수 inner를 반환하고 생명주기가 마감됩니다.

그러면 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거가 되겠죠?

즉, outer 함수의 지역변수 x와 변수 값 10을 저장하고 있던 outer 함수의 실행 컨텍스트가 제거되니까,

지역변수 x도 생명주기가 마감됩니다.

 

그치만, 위 코드의 실행 결과인 innerFunc()은 outer 함수의 지역변수 x 값인 10에 해당합니다.

외부 함수가 중첩 함수보다 더 오래 유지되는 경우,

중첩 함수는 이미 생명 주기가 종료된 외부 함수의 변수를 참조할 수 있기 때문인데요 !

이때, 여기에서 사용되는 중첩함수가 바로 클로저 입니다 💨

 

자바스크립트의 모든 함수는 자신의 상위 스코프를 기억하고 있으니까,

함수가 어디에서 호출되든 상관없이 자신의 상위 스코프의 식별자를 참조할 수 있고,

식별자에 바인딩된 값을 변경할 수도 있는거죠!

 

 

3️⃣ 왜 이게 가능한거지?

outer 함수의 렉시컬 환경은 inner 함수의 [[Environment]] 내부 슬롯에 의해 참조되고 있고,

inner 함수는 전역 변수 innerFunc에 의해 참조되고 있으므로, 가비지 컬렉션의 대상이 되지 않기 때문입니다.

가비지 컬렉터는 누군가가 참조하고 있는 메모리 공간을 함부로 해제하지 않거든요 !-!

 

즉, outer 함수의 실행 컨텍스트가 실행 컨텍스트 스택에서는 제거되지만,

outer 함수의 렉시컬 환경까지 소멸되는 건 아닙니다 !

 

 

4️⃣ 헙.. 그러면 자바스크립트의 모든 함수가 클로저인가?

자바스크립트의 모든 함수가 전역을 포함한 상위 스코프를 기억하므로

이론적으론 모든 함수가 클로저인 것처럼 보이지만!

상위 스코프의 어떤 식별자도 참조하지 않는 함수는 클로저로 취급하지 않습니다.

 

실제로 상위 스코프의 어떤 식별자도 참조하지 않는 함수의 경우,

대부분 모던 브라우저는 최적화를 통해 상위 스코프를 기억하지 않습니다!

참조하지도 않는 식별자를 기억하고 있는 건 메모리 낭비잖아요 ~

 

 

5️⃣ 그럼 어떤 함수를 클로저라 칭할까?

✔️ 중첩 함수가 상위 스코프의 식별자를 참조하는 경우
✔️ 중첩 함수가 외부 함수보다 더 오래 유지되는 경우

 

 

6️⃣ 이런 클로저는 언제 쓰일까?

클로저는 상태를 안전하게 변경하고 유지하기 위해 사용합니다!

즉, 상태가 의도치 않게 변경되지 않도록 상태를 은닉하고,

특정 함수에게만 상태 변경을 허용하기 위해 사용합니다.

 

간단한 코드로 클로저의 쓰임을 살펴볼까요?

let num = 0;

const increase = function() {
	return ++num
}

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3

 

해당 코드에선, 카운트 상태가 전역변수를 통해 관리되고 있기 때문에

언제든지 누구나 접근할 수 있고 값이 언제든지 변경될 위험이 있습니다.

 

const increase = function() {
	let num = 0;
	return ++num;
};

console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1

 

이 코드에선, 카운트 상태가 지역변수로 관리되고 있기 때문에 의도치 않은 상태 변경은 방지했지만

increase 함수가 호출될 때마다 지역변수가 다시 선언되고 0으로 초기화 되기 때문에 출력 결과가 언제나 1입니다.

상태가 변경되기 이전 상태를 유지하지 못하고 있죠!

 

const increase = ( function() {
	let num = 0;
	
	return function() {
		return ++num;
	};
}());

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3

 

이럴 때 사용하는 것이 바로 클로저입니다 !

코드가 실행되면, 즉시 실행 함수가 호출되고, 즉시 실행 함수가 반환한 함수가 increase 변수에 할당됩니다.

 

즉시 실행 함수는 호출된 이후 소멸되지만,

즉시 실행 함수가 반환한 클로저는 increase 변수에 할당되어 호출됩니다.

⇒ 카운트 상태를 유지하기 위한 자유 변수 num을 언제 어디서 호출하든지 참조하고 변경할 수 있는거죠.

 

 

좀 더 간단한 예시를 하나 더 살펴볼까요? 💨

 

const fruits = ['apple', 'banana', 'peach'];
const ul = document.createElement('ul');

const fruitBuilder = function (fruit) {
  return function () {
    console.log('your choice is ' + fruit);
  }
}

fruits.forEach(function (fruit) {
  let li = document.createElement('li');
  li.innerText = fruit;
  li.addEventListener('click', fruitBuilder(fruit));
  ul.appendChild(li);
})

document.body.appendChild(ul);

 

콜백함수 내부에서 외부 데이터를 사용하는 예시 코드입니다.

fruitBuilder 함수는 또 다른 익명 함수를 반환하는 형태죠?

 

코드에 대해 상세하게 설명해보자면,

  • forEach문 안에 fruitBuilder 함수를 실행하면서, fruit 값을 인자로 전달하고,
  • 함수의 실행 결과가 담긴 익명함수를 리스너의 콜백함수로 전달합니다.
  • 클릭 이벤트가 발생하면, 함수의 실행 컨텍스트가 열리면서 fruitBuilder의 인자로 넘어온 fruit를 참조하게 됩니다.

⇒ fruitBuilder의 실행 결과로 반환된 함수에 클로저가 존재하는 거죠!

 

 


 

오늘은 자스 개념 중에서도 정말 어려운 개념들을 다뤄봤습니다 😓

사실 자스핑 주차가 넘어갈수록, 어려운 개념 순위가 계속 업데이트 되는 것 같긴 한데 하하

그래도 딥다이브 하면서 조금씩 확장되는 기분이라 좋습니다..

 

너무 열심히 해서 머리에 열나는 것 같아요 =_=

이만.