[엘리스 sw 엔지니어 트랙] 13일차 컨텍스트,스코프,this,클로저,고차원함수

오경찬·2022년 4월 27일
1

수업 13일차

오늘은 그렇게 어렵고 코딩의 꽃이라는 클로저를 배우는 날이다. 클로저 배우기전에 컨텍스트랑 스코프개념도 배우는데 헷갈리기 시작했다. 이해 안되더라도 많이 보다보면 이해 되겠지 하고 봐야겠다.

이론

  • 스코프 : 참조할수 있는 유효범위를 의미
    Lexical Scope : 함수 호출이 아닌 함수 선언한 곳을 기준으로 스코프를 결정
    실행 컨텍스트 : 실행 가능한 코드가 실행되기 위해 필요한 환경, 실행시 생성되고 종료시 소멸됨
    컨텍스트 종류 : 1. 전역실행 2. 함수 실행 3. Eval 실행
    스택구조 : 마지막에 들어온게 처음으로 나가는 방식
    큐 구조 : 먼저 들어온것이 처음으로 나가는 방식
    호이스팅 : 코드 실행전, 변수/함수 선언이 해당 스코프의 최상단으로 끌어올려진 것 같은 현상
    this가 가리키는것 : 1. Global Context(window), 2. Function Context
    클로저 : 자신이 선언될 당시의 환경을 기억하는 함수(메모리에 올라감)
    스코프 체인 : 함수선언때 생성됨
    Lexucal Environment : 렉시컬 스코프를 가져올수있는 환경
    고차원 함수 : 함수를 인자로 받거나 리턴하는 함수
    map : 배열 중 각각 하나씩 따로 처리하고 다시 합칠수 있는 함수,원본값을 건드리지 않음
    forEach : for문을 함수 형태로 사용할 수 있음, map과 달리는 반환값이 없음
    filter : 각 요소를 돌명서 조건값이 true인 값만 새배열에 담아서 반환해줌
    reduce : 각 요소를 돌며 누적으로 수행하는 함수 (누적값, 현재값, 인덱스, 원본배열)

지역 스코프

지역이란 함수 몸체 내부를 말한다. 지역은 지역 스코프(local scope)를 만든다. 지역에 변수를 선언하면 지역 스코프를 갖는 지역 변수(local variable)가 된다. 지역 변수는 자신이 선언된 지역과 하위 지역(중첩 함수)에서만 참조할 수 있다. 다시 말해, 지역 변수는 자신의 지역 스코프와 하위 지역 스코프에서 유효하다. 자신이 선언된 지역의 상위 스코프에서는 자신의 변수를 참조할 수 없다. 즉, 외부 함수에서는 내부 함수에서 선언된 변수를 참조할 수 없다.(let과 const키워드를 사용한 변수는 해당하지 않는다)

스코프 체인

수는 전역에서 정의할 수도 있고 함수 몸체 내부에서 정의할 수도 있다. 함수 몸체 내부에서 함수가 정의된 것을 ‘함수의 중첩’이라 한다. 그리고 함수 몸체 내부에서 정의한 함수를 ‘중첩 함수(nested function)’, 중첩 함수를 포함하는 함수를 ‘외부 함수(outer function)’라고 한다.
중첩 함수의 지역 스코프는 중첩 함수를 포함하는 외부 함수의 지역 스코프와 계층적 구조를 갖는다. 이때 외부 함수의 지역 스코프를 중첩 함수의 상위 스코프라 한다.
식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것을 스코프 체인이라고 한다.

함수 레벨 스코프

var 키워드로 선언된 변수는 오로지 함수의 코드 블록(함수 몸체)만을 지역 스코프로 인정한다. 이러한 특성을 함수 레벨 스코프(function level scope)라 한다.

코드var x = 1;

if (true) {
  // var 키워드로 선언된 변수는 함수의 코드 블록(함수 몸체)만을 지역 스코프로 인정한다.
  // 함수 밖에서 var 키워드로 선언된 변수는 코드 블록 내에서 선언되었다 할지라도 모두 전역 변수다.
  // 따라서 x는 전역 변수다. 이미 선언된 전역 변수 x가 있으므로 x 변수는 중복 선언된다.
  // 이는 의도치 않게 변수 값이 변경되는 부작용을 발생시킨다.
  var x = 10;
}

console.log(x); // 10를 입력하세요

if문 안에 선언한 변수 x는 if문의 블록 영역을 지역 스코프로 인정하지 않기 때문에 전역변수로서 선언되어진다. 같은 스코프 안에 같은 이름의 변수를 선언하면 두 번째 선언에서는 var가 없는 것으로 동작해서 재할당이 되어지는 것 처럼 진행된다. 따라서 나중에 할당한 10이 x의 값이 된다.

렉시컬 스코프

자바스크립트는 렉시컬 스코프를 따르므로 함수를 어디서 호출했는지가 아니라 함수를 어디서 정의했는지에 따라 상위 스코프를 결정한다. 함수가 호출된 위치는 상위 스코프 결정에 어떠한 영향도 주지 않는다. 즉, 함수의 상위 스코프는 언제나 자신이 정의된 스코프다.
이처럼 함수의 상위 스코프는 함수 정의가 실행될 때 정적으로 결정된다. 함수 정의(함수 선언문 또는 함수 표현식)가 실행되어 생성된 함수 객체는 이렇게 결정된 상위 스코프를 기억한다. 함수가 호출될 때마다 함수의 상위 스코프를 참조할 필요가 있기 때문이다.

var x = 1;

function foo() {
  var x = 10;
  bar();
}

function bar() {
  console.log(x);
}

foo(); // 1
bar(); // 1

This

전역공간

전역공간에서 this는 window(global) 객체

// 전역공간
console.log(this); // Window {parent: Window ... }

함수 내부

함수내부에서 this는 window(global) 객체

함수를 전역객체의 메소드라 생각하면 코드상 this를 파악하기 용이하다. (아래 메소드 호출 시 this 참고)

// 함수내부
function foo() {
  console.log(this);
}
foo(); // Window {parent: Window ... }
// 함수의 함수내부
function foo() {
  function bar() {
    console.log(this);
  }
  bar();
}
foo(); // Window {parent: Window ... }
// 메소드의 함수내부
const obj = {
  a: function() {
    function b() {
      console.log(this);
    }
    b();
  }
}
obj.a(); // Window {parent: Window ... }

메소드 호출

메소드 호출시 this는 메소드 호출 주제

// 메소드 호출
const obj = {
  a: function() {
    console.log(this);
  }
}
obj.a(); // {fn: ƒ}
// 메소드 호출
const obj = {
  a: {
    b: function() {
      console.log(this);
    }
  }
}
obj.a.b(); // {b: ƒ}

화살표 함수에는 함수 이름, this, arguments가 없다. 따라서 예제 코드를 화살표 함수로 바꾸면 스코프 체인에 따라 global 객체가 this 바인딩 된다.

콜백함수

콜백 함수에서 this는 기본적으로 함수내부에서와 동일하다.

하지만 언급했던 것 처럼 상황에 따라 this 값이 달라지는데 가장 흔한 경우가 콜백 함수안에 쓰인 this 이다.

1. call, apply, bind 메서드를 통해 명시적인 this 바인딩 한 경우와 그렇지 않은 경우

// this 바인딩
const callback = function() {
  console.log(this);
}
const obj = {
  a: 1,
  b: function(cb) {
    cb();
    // cb.call(this); // {a: 1, b: ƒ}
  }
}
obj.b(callback.bind(obj)); // {a: 1, b: ƒ}
setTimeout(callback.bind(obj), 100); // {a: 1, b: ƒ}
// this 바인딩 하지 않은 경우
const callback = function() {
  console.log(this);
}
const obj = {
  a: 1,
  b: function(cb) {
    cb();
  }
}
obj.b(callback); // Window {parent: Window ... }
setTimeout(callback, 100); // Window {parent: Window ... }

2. 이벤트 리스너
addEventListener 콜백 함수의 this는 내부 규칙으로 인해 event 객체의 currentTarget으로 바인딩 된다.

// 이벤트 핸들러
document.querySelector('body').addEventListener('click', function() {
	console.log(this); // <body>...<body>
});
// 하지만 this를 명시적으로 대체한 경우
const obj = { a: 'a' };

document.querySelector('body').addEventListener('click', function() {
	console.log(this); // {a: "a"}
}.bind(obj));

클로저

클로저 ( closure ) 는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.
내부함수가 정의될 떄 외부함수의 환경을 기억하고있는 내부함수를 말한다. 즉, 자신이 생성될 때의 환경을 기억하는 함수
함수 정의가 평가되는시점 ( 실행 x ) 에 함수가 상위스코프의 렉시컬환경을 참조를 하게 되는 것이다.

const makeCounter = () => {
  let count = 0; // 독립변수, 자유변수

  return () => {
    return count += 1;
  }
}

const counter = makeCounter();

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

익명함수가 선언될 때 익명함수의 내부슬롯에 makeCounter의 렉시컬환경을 참조한다.

  • const counter = makeCounter(); 에서 makeCounter 함수가 호출될 떄 makeCounter 함수의 컨텍스트가 생성된다.
  • makeCounter함수의 컨텍스트안에 변수 count가 기록되고 익명함수 () => {} 가 기록, 정의된다.
  • (익명)함수가 정의될 때 (익명)함수는 자신의 내부슬롯 [[Environment]] 에 상위스코프의 렉시컬환경이 담긴다.
  • (익명)함수가 정의된 상위스코프 makeCounter()의 렉시컬환경을 (익명)함수의 내부슬롯에 담아놨기때문에, 계속 참조를 안끊기고 하므로 가비지컬렉션의 대상이 되지않아 참조가 계속 살아있다.
  • 이렇게 함수객체는 정의되었을 때 자신의 내부슬롯에 그 상위 스코프의 렉시컬환경참조가 담기는것이다.

    여기서 let count = 0; 은 독립변수 또는 자유변수 라고하는데,
    이것이 바로 클로저를 쓰는 이유가 된다.
    변수를 외부에서 접근할수없게 하기위해서, 변수를 은닉하고, 독립적으로 쓰고싶을 때 클로저패턴을 쓴다. ( 모듈화를 위함이라고도 한다. )

    클로저를 사용하는 이유

    1. 전역변수 사용억제

    전역변수가 많으면 어디에서든 접근이 가능하므로 최대한 전역변수를 줄여서 코딩을 해야한다.
    하지만 프로그램을 구현하다보면 이 함수 하나에서만 사용하는데 전역변수가 필요한 순간이 오는데, 이 때 클로저를 사용하면 된다.

    2. 현재상태를 기억하고 변경된 최신상태를 유지

    상태 변경이나 가변 데이터를 피하고 오류를 피하는 안정성을 증가 시킬수 있다.

    3. 변수를 외부에서 접근할 수 없게 하기 위해서 = 정보은닉

    클로저 단점

    스코프가 종료 된 후에도 스코프 밖에서 언제든지 호출 될 수 있도록 메모리에 계속해서 저장하고 있기 때문에 메모리 사용량이 늘어난다.

    클로저 해제는 null 을 대입한다고 한다.

    profile
    코린이 입니당 :)

    0개의 댓글