this와 클로저

김영준·2023년 6월 14일
0

TIL

목록 보기
5/91
post-thumbnail

this란?

함수를 실행할 때 할당해 주는 것으로 상황에 따라 가끔은 전역 객체, 가끔은 instance를 가리킵니다.

instance: 클래스의 정의를 통해 만들어진 객체를 의미

예제를 통해서 살펴보자. 아래 코드는 오류가 발생한다.

function Cat(name, age){
    this.name = name;
    this.age = age;
}

const tabby1 = Cat('nana', 7); // undefined
console.log(tabby1.name) // error

오류가 발생하는 이유

  1. 위 코드에서 this는 window 객체를 가리킨다.
  2. 함수에 return 문이 없다.

이 코드를 해결하려면?

function Cat(name, age){
    this.name = name;
    this.age = age;
}

const tabby1 = new Cat('nana', 7); // 새로 생긴 객체 정보가 들어감
console.log(tabby1.name) // nana

new 키워드를 붙이면 의도한 대로 실행이 된다.

new 키워드를 붙이게 되면 this는 window가 아니라 새로 생긴 객체를 가리킨다.

또한 따로 return 문을 넣지 않아도 tabby1에는 this가 들어가게 된다.


스크립트 내에 자바스크립트를 바로 작성하면 전역 범위를 침범하고 있다는 사실을 알고 있는가?

즉시 실행 함수 안에 코드를 작성하면 window(전역)를 침범하지 않는다.

코드가 함수 내부 범위에서 동작하기 때문이다.

(function (name) {
  console.log(`hello ${name}`);
})("roto");

아래 예시는 외부에서 logger라는 객체를 통해 함수에 있는 변수와 메서드에 접근할 수 있다.

하지만 logger에 할당을 하지 않은 logCount에는 접근이 불가능하다.

이러한 방식은 의도치 않은 전역 침범을 막아주고 보호해 주는 효과가 있다.

const logger = (function () {
  let logCount = 0;
  function log(message) {
    console.log(message);
    logCount = logCount + 1;
  }
  function getLogCount() {
    return logCount;
  }
  return {
    log: log,
    getLogCount: getLogCount,
  };
})();

logger.log("Hello"); // Hello
logger.log("Hi"); // Hi
console.log(logger.getLogCount()); // 2
console.log(logger.logCount); // undefined

this에 대해서 더 살펴보자.

var idiots = {
  name: "idiots",
  genre: "punk rock",
  members: {
    roto: {
      memberName: "roto",
      play: function () {
        console.log(`band ${this.name} ${this.memberName} play start`);
      },
    },
  },
};

idiots.members.roto.play(); // band undefined roto play start

위 예제에서는 name은 찾지 못하고 memberName은 찾을 수 있었다. 이유가 무엇일까?

현재 console.log 창에서 가리키고 있는 this는 roto 객체를 가리키고 있다.

따라서 roto 안에 정의된 memberName은 찾을 수 있지만, roto 밖에 정의된 name은 찾을 수 없는 것이다.

해결 방법

var idiots = {
  name: "idiots",
  genre: "punk rock",
  members: {
    roto: {
      memberName: "roto",
      play: function () {
        console.log(`band ${idiots.name} ${this.memberName} play start`);
      },
    },
  },
};

idiots.members.roto.play(); // band idiots roto play start

this.name을 부모 객체인 idiots의 name으로 변경하면 된다.


또 다른 예제를 살펴보자

function RockBand(members) {
  this.members = members;
  this.perform = function () {
    setTimeout(function () {
      this.members.forEach(function (member) {
        member.perform();
      });
    }, 1000);
  };
}

var theOralCigarettes = new RockBand([
  {
    name: "takuya",
    perform: function () {
      console.log("Sing: a e u i a e u i");
    },
  },
]);

theOralCigarettes.perform();

위 코드는 에러가 발생한다. 이유가 무엇일까?

setTimeout 함수 내부에 새로운 함수를 새로 정의해 주었기 때문에 this.members는 밖에 있는 members를 가리키지 못한다.

따라서 members는 undefined 기 때문에 forEach를 실행하지 못한다.

해결 방법 1

Arrow funtion을 사용하는 방법

Arrow function은 Arrow function 자체 스코프를 가지지 않고 상위에 있는 함수 스코프를 찾아간다.

function RockBand(members) {
  this.members = members;
  this.perform = function () {
    setTimeout(() => {
      this.members.forEach(function (member) {
        member.perform();
      });
    }, 1000);
  };
}

var theOralCigarettes = new RockBand([
  {
    name: "takuya",
    perform: function () {
      console.log("Sing: a e u i a e u i");
    },
  },
]);

theOralCigarettes.perform();

해결 방법 2

bind 함수를 사용하는 방법

bind 함수는 새로운 함수를 생성한다. 이는 함수를 실행하기 위해 () 소괄호를 사용하는 것과는 차이점이 있다.

받게 되는 첫 인자의 값은 this이고 이어지는 인자들은 바인드 된 함수의 인수에 제공된다.

function RockBand(members) {
  this.members = members;
  this.perform = function () {
    setTimeout(
      function () {
        this.members.forEach(function (member) {
          member.perform();
        });
      }.bind(this),
      1000
    );
  };
}

var theOralCigarettes = new RockBand([
  {
    name: "takuya",
    perform: function () {
      console.log("Sing: a e u i a e u i");
    },
  },
]);

theOralCigarettes.perform(); // Sing: a e u i a e u i

해결 방법 3

클로저를 사용하는 방법

클로저란?

함수가 선언된 환경의 스코프를 기억하여 함수가 스코프 밖에서 실행될 때에도 기억한 스코프에 접근할 수 있게 만드는 문법이다.

외부 this를 that 변수에 할당하고 setTimeout 함수 내부에서 할당한 변수 that을 사용하는 방법이 있다.

function RockBand(members) {
  let that = this;
  this.members = members;
  this.perform = function () {
    setTimeout(function () {
      that.members.forEach(function (member) {
        member.perform();
      });
    }, 1000);
  };
}

var theOralCigarettes = new RockBand([
  {
    name: "takuya",
    perform: function () {
      console.log("Sing: a e u i a e u i");
    },
  },
]);

theOralCigarettes.perform(); // Sing: a e u i a e u i

클로저에 대해서 더 알아보자

const numbers = [0, 1, 2, 3, 4];

for (var i = 0; i < numbers.length; i++) {
  setTimeout(function () {
    console.log(`[${i}] number ${numbers[i]} turn!`);
  }, i * 1000);
}

위 코드는 "[5] number undefined turn!" 가 5번 출력된다. 왜 0부터 출력이 안되고 처음부터 5가 출력이 될까?

그 이유는 setTimeout 내부의 i는 함수 외부인 for 문의 i를 가리키고, setTimeout 함수가 실행되는 시점에는 이미 for 문을 다 반복했기 때문에 5가 출력이 된다.

또한 numbers[5]도 존재하지 않으니 undefined가 출력된다.

해결 방법 1

IIFE(즉시 실행 함수 표현)을 사용

i가 0, 1, 2, 3, 4일 때를 각각의 function scope로 가두어서 처리한다. 따라서 매 루프마다 새로운 function scope를 만든다.

이렇게 되면 setTimeout 실행 시점에 참고하는 index는 IIFE에서 인자로 넘긴 i의 값을 쓰기 때문에 문제가 해결된다.

const numbers = [0, 1, 2, 3, 4];

for (var i = 0; i < numbers.length; i++) {
  (function (index) {
    setTimeout(function () {
      console.log(`[${index}] number ${numbers[i]} turn!`);
    }, i * 1000);
  })(i);
}

해결 방법 2

var 대신 let 사용

var는 함수 레벨 스코프이고 let은 블록 레벨 스코프이다.

let은 for 문 내부에서만 유효한 변수이다.

따라서 setTimeout 내에서 let i가 0, 1, 2, 3, 4일 때 각각 참조되기 때문에 정상적으로 동작한다.

const numbers = [0, 1, 2, 3, 4];

for (let i = 0; i < numbers.length; i++) {
  setTimeout(function () {
    console.log(`[${i}] number ${numbers[i]} turn!`);
  }, i * 1000);
}

해결 방법 3

for 대신 forEach 사용

forEach로 numbers를 순회하면서 각각 function을 만들기 때문에 i의 값이 고유해진다.

numbers.forEach((number, i) => {
  setTimeout(function () {
    console.log(`[${i}] number ${numbers[i]} turn!`);
  }, i * 1000);
});

호이스팅에 관한 오해

우리는 var로 선언한 변수가 호이스팅 된다는 사실을 알고 있다.

function tdzTest() {
  console.log(name); // undefined
  var name = "roto";
}

tdzTest();

그럼 let과 const로 선언한 변수는 호이스팅이 될까?

그렇다.

let과 const도 호이스팅이 되지만 선언 단계와 초기화 단계가 동시에 진행되는 var와 달리 선언 단계와 초기화 단계가 분리되어서 진행이 된다.

따라서 undefined로 초기화가 되지 않고 이를 참조하려면 참조 에러가 발생한다.

function tdzTest() {
  console.log(name);
  const name = "roto";
}

tdzTest();

우리는 이것을 보고 호이스팅이 되지 않는다라고 오해할 수도 있다.

하지만 우리는 이 글을 보고 난 후 let과 const가 호이스팅이 된다는 사실을 인지하면 된다!!


개발자로서 클로저를 이해하는 것은 굉장히 중요하다.

클로저를 사용하는 예를 살펴보자.

클로저를 이용한 private 효과

function Counter() {
  let count = 0;

  function increase() {
    count++;
  }
  function printCount() {
    console.log(`count: ${count}`);
  }
  return {
    increase,
    printCount,
  };
}

const counter = Counter();

counter.increase();
counter.increase();
counter.increase();
counter.increase();
counter.printCount(); // 4

console.log(counter.count); // undefined count는 외부에서 접근 불가

외부에서 건드리면 안 되는 중요한 값이 있을 때 클로저를 사용하여 보호할 수 있다.

profile
꾸준히 성장하는 개발자 블로그

0개의 댓글