딥다이브 스터디 22,24(this, 클로저)

김영현·2023년 10월 24일
0

대망의 this!!!!!!!!!!!!!!!!!!
내가 너무 알고싶었던 this!!!!!!!!!!!!!!!!
객체지향의 최고존엄 this!!!!!!!!!!!!!!!!!!!!!!!!!!!
를 알아보자

this

메서드는 자신이 속한 객체를 참조할 수 있어야 한다
=> 아하! 자신이 속한 객체참조가 this로군

참고로 객체 리터럴{}방식으로 선언하면, 재귀적으로 참조가능

const obj = {
	name:"kim",
  	getName(){
    	return obj.name;
    }
}
obj.getName() // "kim"

하지만 일반적이지 않고 바람직하지 않다.
짜식아 객체는 생성자 함수클래스를 이용하여 인스턴스를 생성 후 사용해야지!

function obj(name){
	????.name = name;
}
obj.prototype.getName = function(){
	return ????.name
}
//여기까지는 생성자 함수 자신이 생성할 인스턴스를 가리키는 식별자를 알 수 없다.
//obj.xxx로 접근이 불가능하단 소리다.

const instance = new obj("kim"); //이때 instance.xxx로 접근 가능하다.

이처럼 생성자함수 내부에서 생성될 인스턴스의 참조가 필요함.

함수를 호출하면, 암묵적으로 arguments객체와 this암묵적으로 함수내부에 전달됨
this바인딩함수 호출 방법에 의해 동적으로 결정된다!!!!
=> 스코프의 결정방식과 정확히 반대다!. 스코프는 정의 위치에 따라 결정된다

this객체의 프로퍼티나 메서드를 참조하기위한 것이라, 나머지 함수는 의미없음.
=> 그래서 엄격모드에선 undefined가 바인딩 된다.

함수 호출 방식과 this 바인딩

총 4가지다.

  • 일반호출: 의미없다. 전역객체가 바인딩됨. 메서드 내부에서정의한 함수도 그냥호출하면 전역객체바인딩
var value = 1;
const obj = {
	value: 100,
  	foo(){
    	console.log(this.value) //100
    }
  	setTimeout(function(){
    	console.log(this.value) //1
    })
}

이처럼 콜백함수 내부에 일반함수를 호출해도 전역객체가 바인딩됨.
해결법은 this를 밖에서 변수에 할당 후 참조 및 call, apply, bind메서드 활용, 화살표 함수를 사용하면 됨.

  	setTimeout(function(){
    	console.log(this.value) //100
    }.bind(this)).
    //or
     setTimeout(()=>console.log(this.value)) //100.
	//화살표 함수 내부의 this는 상위 스코프의 this임
  • 메서드 호출: 호출할때 마침표앞에 기술된 객체가 바인딩.
    obj.methodobj가 바인딩.
    주의할 점은 소유객체가 아니라, 호출 객체가 바인딩.
    프로토 타입 메서드호출 객체에 바인딩.
  • 생성자함수 호출: 미래에 생성할 인스턴스에 바인딩.
  • apply, call, bind: 예제로 후술.
//apply와 call은 함수를 호출하면서 this를 넘긴다
function getThisBinding() {
  	console.log(arguments);
  	return this;
}
const thisArg = { a: 1 };

console.log(getThisBinding.apply(thisArg, [1,2,3]));
console.log(getThisBinding.call(thisArg, 1, 2, 3));
//Arguments(3) [1, 2, 3, callee: f, Symbol(Symbol.iterator):f]
//결과는 둘 다똑같다...차이는 인수를 '배열'이냐 '쉼표구분'이냐다.

보통 유사배열객체배열 메소드를 사용하는 경우에 쓴다.

function convertArgToArray(){
	console.log(arguments);
  	const arr = Array.prototype.slice.call(arguments);
  	console.log(arr);
  	return arr;
}

convertArgToArray(1,2,3) // [1,2,3]

bindthis바인딩이 교체된 새로운 함수를 반환한다.


클로저

A clouser is the combination of a function and the lexical environment within which that function was declared

뭐라카노?
하지만 23챕터(실행컨텍스트)를 배웠다면...
=> 함수와 함수가 선언된 렉시컬 환경의 조합. 아하! 외부환경을 참조하고있어서 내부 함수외부 함수보다 오래 살아있는 거구나 라며 단박에 이해할수 있다.

실행컨텍스트를 잘 몰랐을때의 글

흠! 그때도 나쁘지 않게 이해하고 있었군.
그래도 이번 챕터자체가 클로저인 만큼, 보다 자세히 알아보자
단, 클로저 자체보다는 활용에 집중하겠다.

  • 외부함수의 참조는 내부함수[[Environment]]슬롯에 저장된다
    => Outer Lexical Environment Reference(정의당시 외부스코프)를 저기에 저장하는군
  • 클로저에 의해 참조되는 상위스코프의 변수를 자유 변수(free variable)이라 함.
    => free variable에 closure(묶여있는) function!

클로저의 활용

상태를 안전하게 변경하고 유지하기위해사용된다.
뭔말이더냐?
아마 전역적으로 사용하지 못하게, 막아버리는듯한데...예제로 알아보자.

let num = 0;

const increase = () => {
	return ++num;
}
increase(); //1
increase(); //2
increase(); //3

이 함수의 문제점은 뭘까?

  1. num은 함수가 호출되기 전까지 변경되지 않아야 한다.
  2. 1번을 위해 카운트 상태는 increase함수만이 변경시킬 수 있어야한다.

이는 오류를 줄이고 통일성을 위한조건이다.
아무것도 지켜지지 않았다.

일단 전역변수를 지역변수로 바꿔본다.

const increase = () => {
	let num = 0;
	return ++num;
}
increase(); //1
increase(); //1
increase(); //1

아주 멍청한 생각이었다.
이전상태를 유지하지 못한다.
그러면, 이전상태도 유지하고 지역변수로 활용해보자

const increase = (function(){
	let num = 0;
	return function(){
    	return ++num;
    }
}());
increase(); //1
increase(); //2
increase(); //3

이게 클로저의 핵심이다.
increase메소드의 상위 스코프는 메서드가 평가되는 시점에 실행중인 실행 컨텍스트(즉시실행 함수)의 렉시컬 환경이다!
=> 지역변수num을 참조할수 있는 이유가 이것 때문!

이를 생성자함수나 함수형 프로그래밍으로 나타낼수도 있다

// ----생성자 함수---- //
const Counter = (function(){
	let num = 0;
  	function Counter(){
    
    }
  	Counter.prototype.increase = function(){
    	return ++num;
    }
  	return Counter;
}())
const counter = new Counter();
counter.increase(); //1
counter.increase(); //2

// ----함수형 ----- //

function makeCounter(aux){
	let count = 0;
  	return function(){
    	count = aux(count);
      	return count;
    }
}
//보조함수를 선언하여 고차함수로 넘겨준다
function increase(n){
	return ++n;
}
function decrease(n){
	return --n;
}
makeCounter(increase()); //1
makeCounter(increase()); //2
makeCounter(decrease()); //-1
//독립된 렉시컬 환경을 갖기에 변수가 공유되지 않음

변수를 공유하려면, 즉시실행함수로 감싸야한다

const counter = (function(){
  
	let count = 0;
  	return function(aux){
    	count = aux(count);
      	return count;
    }
}())
//보조함수를 선언하여 고차함수로 넘겨준다
function increase(n){
	return ++n;
}
function decrease(n){
	return --n;
}
counter(increase); //1
counter(increase); //2
counter(increase); //3
counter(decrease); //2

이는 전의 함수인 makeCounter가 호출될때마다 렉시컬 환경이 새로 생성되었기 때문이다.
위처럼 즉시실행함수로 호출해버리고 끝나면, 렉시컬 환경이그대로다.

캡슐화, 정보은닉

  • 캡슐화(encapsulation): 객체의 상태를 나타내는 프로퍼티와, 프로퍼티를 참조하고 조작할수있는 동작인 메서드를 하나로 묶는것
  • 정보 은닉: 프로퍼티나 메서드중 숨기고싶은거 숨기는거. 적절치 못한 접근 방지 및 상호 의존성(결합도)낮춤.

js는 es10(2019)이후 정보은닉을 위해 private기능을 지원하지만, 이왕 클로저 파트에 왔으니 이전에 쓰던 방법을 알아보자.

const Person = (function(){
	let _age = 0; //private
  
  	function Person(name,age){
    	this.name = name;
      	_age = age;
    }
  
  	Person.prototype.sayHi = function(){
    	console.log(`Hi my name is ${this.name}, I am ${_age}`)
    }
  
  	return Person;
}());

const me = new Person('lee', 20);
me.sayHi() // Hi my name is lee, I am 20.
me.name // lee
me._age // undefined

하지만 문제가있다.
어딘갈 잘 보아라.
즉시실행함수로 감쌌기에, 새로운 인스턴스를 생성하여 age를 넣어주면 _age의 값이 계속 바뀐다(공유)

이전에는 잘 사용하지 못했구나?

클로저를 사용할때 발생하는 실수

클로저를 사용할때라기보단...var를 사용할때 생기는 실수다.
아예 쓰지 않으면 좋겠지만, 일단 알아두기나 하자.

var funcs = [];

for(var i = 0; i<3; i++){
	funcs[i] = function(){ return i; };
}
//블록레벨 스코프가 지원되지 않는 var = i 는 값이 3이되어버렸다. 따라서 3만 출력된다.

for(var j = 0; j<funcs.length; j++){
	console.log(funcs[j]());
}

해결법은 역시 클로저와 즉시실행 함수다

var funcs = [];

for(var i = 0; i<3; i++){
	funcs[i] = (function(id){
    	return function(){
        	return id
        }
    }(i))
}

for(var j = 0; j<funcs.length; j++){
	console.log(funcs[j]());
}

그냥 const,let을 사용하는게 마음 편하다.
let키워드를 사용하면 블록스코프에서 새로운 렉시컬 환경을 생성하기에, 식별자 값 유지가 가능하다.

아니면 함수형 프로그래밍 기법중 하나인 고차함수를 사용하여도 좋다

const funcs = Array.from(new Array(3), (_,i) => () => i);
funcs.forEach(f=>console.log(f())) // 0 1 2

느낀점

this와 클로저에대해 정확히 알게되었다.
또한 객체끼리의 의존성을 낮춰야하는것도 깨달았다.
과제를 하며 컴포넌트끼리 왔다갔다하는 일이 빈번해졌는데...더 의존성을 낮춰봐야겠다.

profile
모르는 것을 모른다고 하기

0개의 댓글