이 글은 로토(roto)님의 강의자료를 정리한 내용입니다.
아래 코드를 실행하면 무엇이 출력되나요?
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.name
과 this.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
를 참조하는 클로저이기도 하다.
아래 코드를 실행하면 무엇이 출력되나요?
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
함수가 가리키는 this
는 yoonho
가 된다.
따라서 this.memberName
은 yoonho.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
함수의 this
는 thisTest
가 된다.thisTest.testInTest.whoAmI();
에서 whoAmI
함수의 this
는 testInTest
가 된다.다시 원래 문제에서 의도대로 올바르게 코드를 짠다면 아래와 같다.
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
으로 정확하게 어떤 객체인지를 명시한다.
다음 코드를 실행하면 오류가 발생한다.
오류가 발생하는 원인은 무엇이고 어떻게 해결할 수 있을까요?
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.members
의 this
는 전역객체
를 가리킨다.
내부함수는 일반 함수, 메소드, 콜백함수 어디에서 선언되었든 관게없이 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
를 찾는다.
이 때 상위 스코프는 RockBand
의 this.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
, 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
키워드로 선언된 변수를 전역 변수로 사용하는 경우, let
전역 변수는 전역 객체의 프로퍼티가 아니다. let
전역 변수는 보이지 않는 개념적인 블록 내에 존재하게 된다.호이스팅이란 함수가 실행 가능한 코드가 실행되기 전에 필요한 변수들을 모두 한번씩 읽어서 미리 초기화 하는 작업이다.
선언한 것들을 마치 최상단에 끌어 올려 놓는것과 같아서 호이스팅이라고 한다.
var
, let
, const
, function
, class
등 모든 선언 키워드들은 호이스팅이 된다.
쉽게 말해 일단 실행에 필요한 재료들을 한번 싹 읽어 오는것이다. 사용할수 있냐 없냐는 그 다음 문제이다.
실행 가능한 코드 안에서
var
는 자동으로 undefined
로 초기화 된다. 따라서 초기값이 있든 없든 선언전에 사용이 가능한다. 다만 초기값이 없다면 초기화 이전까지는 undefined
라는 값으로 할당이 되어 있는것이다.
let
, const
는 var
처럼 자동으로 undefined
초기값을 할당하지 않는다. 따라서 선언부 이전까지는 사용이 불가능하다. 이것이 바로 TDZ(Temporal Dead Zone)이다.
간단하게 말해서 외부 환경을 참조해서 사용하는 것을 말한다.
자세한 내용은 이전에 작성한 실행컨텍스트를 통해 호이스팅, 스코프체인, 클로저 이해하기 글을 참고하자.