자바스크립트 핵심 문제

younoah·2021년 8월 16일
2

[My 자바스크립트]

목록 보기
7/17

이 글은 로토(roto)님의 강의자료를 정리한 내용입니다.

생성자 함수와 this 문제

아래 코드를 실행하면 무엇이 출력되나요?

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

const timmy = Cat('timmy', 3);
console.log(timmy.name);

➡️ 오류가 발생한다.

Cat 함수를 실행하면 Cat 함수의 this 는 window가 된다. (브라우저에서 실행했다는 가정하에)

또한 Cat 함수는 리턴값이 없기 때문에 timmy 의 값은 undefined 가 되고 timmy.name 을 참조할 때 오류가 발생한다.

console.log(this.name); // timmy
console.log(this.age); // 3

Cat 함수를 실행한 뒤 this.namethis.age 를 각각 콘솔로 찍어보면 timmy , 3 을 출력하는데

Cat 함수에서 window의 name과 age 프로퍼티에 각각 값을 할당했기 때문이다.

위 코드가 의도대로 올바르게 동작하기 위해서는

생성자 함수인 Cat 함수 앞에 new 키워드를 넣어줘야 한다.

new 키워드를 넣어서 생성자 함수를 실행하면 Cat 생성자 함수의 this 는 새로 생긴 객체를 가리킨다. 즉 timmy 를 가리킨다.

아래는 의도한 대로 올바르게 작성한 코드이다.

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

  this.printCatInfo = () => {
    console.log(`${this.name}${this.age}살 고양이 입니다.`);
  };
}

const timmy = new Cat('timmy', 3);
const jimmy = new Cat('jimmy', 5);

timmy.printCatInfo(); // timmy은 3살 고양이 입니다.
jimmy.printCatInfo(); // jimmy은 5살 고양이 입니다.

즉시실행함수 문제

아래 코드를 실행하면 무엇이 출력되나요?

(function (name) {
  console.log(`hello ${name}`);
})('yoonho');

➡️ hello yoonho 가 출력된다.

이런 형태로 사용하는 함수를 즉시실행함수(IIFE)라고한다.

변수나, 함수를 선언할 때 해당 변수나, 함수는 선언한 곳을 감싸는 함수에 묶이게 된다.

이렇게 사용하는 이유는 상위 컨텍스트 혹은 전역변수에 변수나 함수를 선언하는것을 피하기 위함이다. 즉 전역의 오염을 막아서 변수, 함수의 중복을 막는등의 효과를 얻을 수 있다.

즉시실행함수(IIFE) 활용 예제

const logger = (function () {
  // logCount는 밖에서 접근할 수 없다. 일종의 private 효과
  let logCount = 0;

  function log(message) {
    console.log(message);
    logCount++;
  }

  function getLogCount() {
    return logCount;
  }

  return { // log, getLogCount만 logger를 통해서 사용이 가능하다.
    log: log,
    getLogCount: getLogCount,
  };
})();

logger.log('hello my name is yoonho'); //hello my name is yoonho
logger.log('have a good time'); // have a good time
console.log(logger.getLogCount()); // 2
console.log(logger.logCount); // undefined

이렇게 함수안에 사용할 변수와 함수를 정의하고 바로 실행을 하여 logger 라는 변수에 할당을 해주면

log , getLogCount 함수는 전역이 아닌 logger 를 통해서만 실행할 수 있게되고 logCount 는 접근할 수 없다.

log , getLogCount 함수는 즉시실행함수(IIFE)logCount 를 참조하는 클로저이기도 하다.

this 바인딩 문제1

아래 코드를 실행하면 무엇이 출력되나요?

const webDevCourse = {
  name: 'Web Dev Course',
  goal: '자생력있는 개발자',
  members: {
    yoonho: {
      memberName: 'yoonho',
      print: function () {
        console.log(`in ${this.name}, hello my name is ${this.memberName}!`);
      },
    },
  },
};

webDevCourse.members.yoonho.print();

➡️ in undefined, hello my name is yoonho! 가 출력된다.

함수레벨 스코프와 관련된 문제이다.

play 함수가 가리키는 thisyoonho가 된다.

따라서 this.memberNameyoonho.this.memberName 가 되어 올바르게 yoonho 라는 값을 갖게 되지만

yoonho라는 객체 내부에는 name 이 업식 때문에 this.name이 라는 값이 존재하지 않아 undefined가 출력된다.

this바인딩 예제

const thisTest = {
  whoAmI: function () {
    console.log(this);
  },

  testInTest: {
    whoAmI: function () {
      console.log(this);
    },
  },
};

thisTest.whoAmI();
// {
//   whoAmI: [Function: whoAmI],
//   testInTest: { whoAmI: [Function: whoAmI] }
// }

thisTest.testInTest.whoAmI();
// { whoAmI: [Function: whoAmI] }
  • thisTest.whoAmI(); 에서 whoAmI 함수의 thisthisTest 가 된다.
  • thisTest.testInTest.whoAmI(); 에서 whoAmI 함수의 thistestInTest 가 된다.

다시 원래 문제에서 의도대로 올바르게 코드를 짠다면 아래와 같다.

const webDevCourse = {
  name: 'Web Dev Course',
  goal: '자생력있는 개발자',
  members: {
    yoonho: {
      memberName: 'yoonho',
      print: function () {
        console.log(`in ${webDevCourse.name}, hello my name is ${this.memberName}!`);
      },
    },
  },
};

webDevCourse.members.yoonho.print();
// in Web Dev Course, hello my name is yoonho!

this.name 이 아닌 webDevCourse.name 으로 정확하게 어떤 객체인지를 명시한다.

this 바인딩 문제2

다음 코드를 실행하면 오류가 발생한다.

오류가 발생하는 원인은 무엇이고 어떻게 해결할 수 있을까요?

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('a e u i a e u i');
    },
  },
]);

theOralCigarettes.perform();

➡️ TypeError: Cannot read property 'forEach' of undefined

setTimeout 함수의 콜백함수 내부에서 this.membersthis전역객체를 가리킨다.

내부함수는 일반 함수, 메소드, 콜백함수 어디에서 선언되었든 관게없이 this는 전역객체를 바인딩한다.

이문제를 해결하려면 RockBand 생성자 함수의 this.members 를 올바르게 참조하도록 해야한다.

해결 방법은 크게 2가지가 있다.

해결 1. 화살표 함수 사용하기

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('a e u i a e u i');
    },
  },
]);

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

화살표 함수는 자기 자신만의 스코프를 만들지 않는다. 즉, 자기 자신만의 this는 존재하지 않는다.

따라서 화살표 함수 내부에서 this 를 사용하게 되면 스코프체인을 통해 상위 스코프에서 this 를 찾는다.

이 때 상위 스코프는 RockBandthis.perform 메서드이다.

this.perform 메서드의 this 를 따른다.

해결 2. bind 메서드 사용하기

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('a e u i a e u i');
    },
  },
]);

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

bind 메서드는 첫번째 인자로 this 값을 지정하고 함수를 반환한다.

해결 3. 클로저 사용하기

function RockBand(members) {
  const 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('a e u i a e u i');
    },
  },
]);

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

setTimeout 함수 내의 that 이 스코프 체인을 통해 외부 스코프에 존재하는 that 을 참조하게 된다.

클로저 문제

다음 코드를 실행하면 숫자가 0부터 4까지 출력이 되지 않고 undefined가 다섯번 출력이 된다.

그 이유는 무엇일까?

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

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

// [5] numbers undefined turn!
// [5] numbers undefined turn!
// [5] numbers undefined turn!
// [5] numbers undefined turn!
// [5] numbers undefined turn!

var로 선언된 변수 i 는 함수레벨 스코프이므로 전역변수가 된다.

setTimeout 함수의 콜백함수들이 실행될 시점에는 변수 i 는 for루프가 끝난 상태이고 값이 5로 되어있기 때문이다.

해결 1. IIFE

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

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

// [0] numbers 1 turn!
// [1] numbers 2 turn!
// [2] numbers 3 turn!
// [3] numbers 4 turn!
// [4] numbers 5 turn!

매 반복문마다 즉시실행함수를 활용하여 함수 스코프로 i를 전달하여 그 때 당시의 i값을 기억하게 한다.

즉, i가 0일 때, 1일 때, 2일 때, 3일 때, 4일 때를 각각 함수 스코프로 가두게 되면 setTimeout 함수의 콜백함수들이 실행 시점의 index 값을 사용하기 때문에 인자로 넘긴 i값을 사용하게 되기 때문이다.

해결 2. var대신 let 사용하기

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

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

// [0] numbers 1 turn!
// [1] numbers 2 turn!
// [2] numbers 3 turn!
// [3] numbers 4 turn!
// [4] numbers 5 turn!

let은 블록레벨 스코프이다. 따라서 매 반복문 마다 i값을 블록 스코프로 갖고 있고 setTimeout 의 콜백함수가 해당 블록스코프에서의 i값을 참조하게 된다.

해결 3. for 대신 forEach 사용하기

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

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

// [0] numbers 1 turn!
// [1] numbers 2 turn!
// [2] numbers 3 turn!
// [3] numbers 4 turn!
// [4] numbers 5 turn!

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

var, let, cont의 차이는 무엇일까요?

  • var 는 함수 레벨 스코프이다.
  • let , const 는 블록 레벨 스코프이다.

예제를 한번 보자.

{
  var a = 111;
  let b = 222;
}

console.log(a); // 111
console.log(b); // ReferenceError: b is not defined

함수 레벨 스코프인 var 로 선언한 변수 a 는 블록 내부에 선언을 해도 블록의 영향을 받지않고 전역에서 참조가 가능하다.

하지만 블록 레벨 스코프인 let 으로 선언한 변수 b 는 블록 내부에 선언했다면 외부에서 참조가 불가능하다.

🤔 전역객체에서 var 와 let

  • var 키워드로 선언된 변수를 전역 변수로 사용하면 전역 객체의 프로퍼티가 된다.
  • let 키워드로 선언된 변수를 전역 변수로 사용하는 경우, let 전역 변수는 전역 객체의 프로퍼티가 아니다. let 전역 변수는 보이지 않는 개념적인 블록 내에 존재하게 된다.

호이스팅

호이스팅이란 함수가 실행 가능한 코드가 실행되기 전에 필요한 변수들을 모두 한번씩 읽어서 미리 초기화 하는 작업이다.

선언한 것들을 마치 최상단에 끌어 올려 놓는것과 같아서 호이스팅이라고 한다.

var, let, const, function, class 등 모든 선언 키워드들은 호이스팅이 된다.

쉽게 말해 일단 실행에 필요한 재료들을 한번 싹 읽어 오는것이다. 사용할수 있냐 없냐는 그 다음 문제이다.

실행 가능한 코드 안에서

var 는 자동으로 undefined 로 초기화 된다. 따라서 초기값이 있든 없든 선언전에 사용이 가능한다. 다만 초기값이 없다면 초기화 이전까지는 undefined 라는 값으로 할당이 되어 있는것이다.

let , constvar 처럼 자동으로 undefined초기값을 할당하지 않는다. 따라서 선언부 이전까지는 사용이 불가능하다. 이것이 바로 TDZ(Temporal Dead Zone)이다.

클로저는 무엇인가요?

간단하게 말해서 외부 환경을 참조해서 사용하는 것을 말한다.

자세한 내용은 이전에 작성한 실행컨텍스트를 통해 호이스팅, 스코프체인, 클로저 이해하기 글을 참고하자.

profile
console.log(noah(🍕 , 🍺)); // true

0개의 댓글