오늘은 자바스크립트에서 우리가 모르고 쓰면 예상하지 않은 결과가 나오는 사례들, 그리고 그런 사례들이 왜 나오는지에 대하여 공부했다. 사례들을 살펴보니, 자바스크립트 함수의 호출 과정을 이해하고 있다면, 그리고 스코프에 대해 이해하고 있다면 충분히 예방할 수 있는 사례들임을 깨달았다. 따라서 이미 1일차에 정리했던 내용들을 복습하면서 추가적으로
this
바인딩과 자바스크립트의 특성에 따라 여러 예상하지 못한 결과들의 사례와 이를 예방하기 위한 방법들에 대하여 정리했다.
이미 1일차에 정리를 했었지만, 아무리 강조해도 지나치지 않기에 한 번 더 정리하기로 한다.
함수의 호출 과정은 크게 다음 세 가지 과정으로 나눌 수 있다.
1. PrepareForOrdinaryCall
2. OrdinaryCallBindingThis
3. OrdinaryCallEvaluateBody
PrepareForOrdinaryCall
함수가 호출된 뒤, 함수를 실행할 수 있는 환경을 만들고 초기화 하는 단계다. 정확히 말하면 실행 컨텍스트(Execution Context
)가 생성되는 단계이다. 이 과정에서는 함수 내부의 식별자 정보들을 바인딩하고 정의한다. (정확히는 Execution Context
내부의 Lexical Environment
내부의 Environment Record
에 저장된다)
OrdinaryCallBindingThis
this
를 바인딩한다. this
는 함수 호출 시점에 바인딩하여 결정된다. 함수 호출 주체가 누군지에 따라, this
가 결정된다는 것이다. 이는 다른 언어의 this
의 의미와는 다르고, 다른 언어와 가장 큰 차이점 중 하나이다. 이러한 자바스크립트 언어 특성 상 다양한 난감한 사례들이 많이 발생하기도 한다.
OrdinaryCallEvaluateBody
함수가 평가되고 값을 반환한다.
자바스크립트의 This
는 함수 호출 시점에 결정되기 때문에, 함수 호출의 주체가 무엇인지에 따라 달라진다. 이러한 자바스크립트 특성상 여러 재미있는 재미라기보다는 짜증나는 현상들이 발생하기도 한다. 아래 사례들을 살펴보면서 This
가 어떤 특성을 갖고 있는지 살펴보자.
var company = {
name: "MS",
category: "IT",
members: {
person: {
memberName: "person",
work: function () {
console.log(`company ${this.name} ${this.memberName} go to work`);
},
},
},
};
company.members.person.work(); // ??
이 코드를 출력하면 어떤 결과가 나올까?
답은 company undefined person go to work
가 출력된다. 이유를 알기 위해 함수의 호출 주체를 먼저 살펴보면, work
함수는 members
의 person
객체에 의하여 호출되었다. 따라서 여기서 this
는 person
이 된다. 그런데 person
객체 내부에는 name
키가 없으므로 undefined
가 출력된다.
객체 이름을 명시한다.
var company = {
name: "MS",
category: "IT",
members: {
person: {
memberName: "person",
work: function () {
console.log(`company ${company.name} ${this.memberName} go to work`);
},
},
},
};
company.members.person.work(); // ok
// JS의 생성자 함수는 대문자로 시작한다.
function Person(name, age) {
this.name = name;
this.age = age;
this.printInfo = function () {
console.log(this.name, this.age);
};
}
const hommer = Person("hommer", 30);
hommer.printInfo(); // ?
다음 함수를 실행하게 되면, 에러가 발생한다. TypeError: Cannot read printInfo of undefined
에러가 발생하게 되는데, 자세히 살펴보면 우리가 생성자 함수를 이용하여 새로운 객체를 정의할 때, new
키워드를 사용하지 않았다. 일반 함수에서 this
키워드는 global
객체를 나타낸다. new
를 이용하여 객체를 정의하면, 그제서야 this
를 우리가 정의한 새로운 객체에 바인딩함을 알 수 있다. 따라서 오류를 해결하기 위해서는 다음과 같이 작성해야 한다.
// ...
const hommer = new Person("hommer", 30); // ok
function Group(group) {
this.group = group;
this.doSomething = function () {
setTimeout(function () {
this.group.forEach(function (member) {
member.doSomething();
});
}, 1000);
};
}
var group1 = new Group([
{
name: "group1",
doSomething: function () {
console.log("do something");
},
},
]);
group1.doSomething();
다음 호출 과정 역시 에러가 발생한다. 그러나 우리는 함수의 흐름만 쫓아갈 수 있으면 어렵지 않다.
doSomething
메소드 내부에 setTimeout
과 콜백 함수가 존재한다. 콜백 함수 내부에 this.group
을 호출하고 있다. setTimeout
내부의 콜백 함수의 this
는 Group
을 가리키고 있지 않다. this
는 함수 스코프를 가지기 때문에, this
는 global
객체를 가리킨다.
그런데 global
객체에 group
을 정의하지 않았으므로, undefined
에러가 발생하는 것이다.
이를 해결하기 위한 방법은 크게 세 가지가 존재한다.
1. This
를 바인딩하지 않는 화살표 함수를 사용한다.
화살표함수는 this
를 바인딩하지 않고, 무조건 상위 스코프의 this
를 따른다. setTimeout
콜백 함수를 화살표 함수로 작성하면, this
는 상위 스코프의 this
를 따르게 되고, 상위 scope
의 this
는 Group
을 정의한 객체를 가리킬 것이므로, 함수를 실행하는데 문제가 생기지 않는다.
function Group(group) {
this.group = group;
this.doSomething = function () {
// 이런식으로
setTimeout(() => {
this.group.forEach(function (member) {
member.doSomething();
});
}, 1000);
};
}
.bind()
함수로 this 바인딩하기.bind()
함수는 호출 주체를 묶어준다. 따라서 .bind(this)
를 통해 this
를 지정하면, 콜백 함수 내부에서 this
를 호출해도 아무런 문제가 생기지 않는다.function Group(group) {
this.group = group;
this.doSomething = function () {
// 이런식으로
setTimeout(function() {
this.group.forEach(function (member) {
member.doSomething();
}.bind(this));
}, 1000);
};
}
lexical environment
의 조합이다. 함수가 호출될 때 자신의 주변 환경을 기억하고, 내부 함수에서 외부 스코프의 변수에 접근할 수 있는 함수를 뜻한다. 함수를 클로저로 만들기 위해서는function Group(group) {
var that = this;
this.group = group;
this.doSomething = function () {
// 이런식으로
setTimeout(function() {
that.forEach(function (member) {
member.doSomething();
}.bind(this));
}, 1000);
};
}
이런식으로 사용할 수 있다.
몇 가지 사례들을 가지고 실수하기 쉬운 자바스크립트에서 this 호출 과정에 대하여 살펴보았다. 상황에 따라 다르긴 하지만, 기본적으로 다음만 명심하면 될 것 같기도 하다.
1. 일반 function
호출 시, 기본적으로 this
는 window(global)
을 가리킨다. 생성자 함수를 통해 new
예약어를 이용하여 객체를 생성하면, 객체를 this
에 바인딩한다.
2. 화살표 함수는 this
를 바인딩하지 않는다. 무조건 상위 스코프의 this
를 따라간다. 따라서 함수의 call
, bind
, apply
함수를 이용하여 this
를 바인딩할 수도 없다.
원래 this
와 클로저를 함께 정리하려고 했는데, 아직 클로저에 대하여 공부가 덜 되었고, this
에 대해 정리하는 양이 많아지면서 다음에 따로 정리하기로 했다.
그리고 TIL을 위한 TIL이 아닌, 꼭 내가 정리하면서 얻어가는 TIL이 되었으면 좋겠다.
함수의 호출과정에 대해 배우고 갑니다. 좋은 글 감사드려요 🙏