자기 자신을 가리키는 값 this
This Binding
자바스크립트의 함수는 자기 자신을 가리키는 특별한 값인 this를 사용할 수 있으며, 이를 자기 참조 변수(Self-referencing variable)라 부릅니다. this가 가리키는 값은 함수를 호출하는 방식에 따라 동적으로 결정되는데, 이렇게 this의 값이 정해지는 것을 this 바인딩이라 합니다.
호출 방식에 따라 this가 어떻게 달라지는지 확인해 보도록 합시다.
일반 함수 호출
전역 실행 컨텍스트에서의 this는 항상 전역 객체를 가리킵니다. 실행 컨텍스트는 자바스크립트 코드 실행을 위한 정보가 담긴 메모리이며, 함수가 호출될 때마다 해당 함수를 실행하기 위한 정보를 담은 컨텍스트가 생성됩니다. 자바스크립트의 전역 객체는 실행 환경에 따라 달라지며 웹 브라우저에서는 window를 가리키고, Node 환경에서는 global을 가리킵니다. 본문에서는 브라우저를 기준으로 작성했습니다.
function foo() {
console.log(this); // Window {window: Window, ...}
console.log(this === window); // true
}
foo();
foo 함수를 호출하면 this가 전역 객체를 가리키고 있는 것을 확인할 수 있습니다. 하지만 this는 foo 함수를 window.foo()의 방식이 아닌 직접 호출했기 때문에, 어느 컨텍스트에 소속된 함수인지 알 수 없어 undefined가 되어야 합니다. 그러나 자바스크립트는 this가 바인딩되지 않은 경우에는 모두 전역 객체를 가리키도록 하는 규칙을 따르기 때문에 위와 같은 결과를 보이게 됩니다.
만약 undefined 값을 확인하고 싶다면, 엄격 모드(Strict Mode)를 적용해 주세요.
객체 메서드 호출
자바스크립트에서는 객체의 프로퍼티로 존재하는 함수를 메서드라 부릅니다.
const obj = {
name: '제리',
greeting() {
return `제 이름은 ${this.name}입니다.`;
},
};
// 제 이름은 제리입니다.
console.log(obj.greeting());
메서드는 일반 함수를 호출하는 방식과 달리 [obj.method()] 형태로 호출하기 때문에, 호출되는 함수가 소속된 컨텍스트의 정보를 알 수 있습니다. 따라서 this는 호출한 객체에 대한 정보를 담게 됩니다. 하지만 객체의 메서드라 할 지라도 일반 함수를 호출할 때와 동일하게 사용하면, 앞서 언급한 규칙에 따라 전역 객체를 가리키게 됩니다.
생성자를 통한 객체 생성
new 키워드를 사용하여 함수를 호출하면 생성자로 동작합니다.
function Person(name, age, job) {
// this = {}
this.name = name;
this.age = age;
this.job = job;
// return this;
}
const me = new Person('NoHack', 29, 'Developer');
// Person { name: 'NoHack', age: 29, job: 'Developer' }
console.log(me);
new와 함께 함수가 호출되면 보이지는 않지만 암묵적으로 this에 빈 객체가 할당되며, 함수 내부의 코드에 의해 this가 채워집니다. 위 코드에서 주석 처리된 부분은 생성자로 동작했을 때 자동으로 수행되는 부분입니다. 위 코드의 결과는 new와 함께 호출해야만 동일한 값을 확인할 수 있는데, 만약 일반 함수로 호출하게 되면 전역 객체에 새로운 프로퍼티를 추가하는 형태가 됩니다.
Binding Methods
this 바인딩은 함수를 호출하는 방식에 따라 결정됐습니다. 그렇다면 함수 호출 방식에 따라서가 아닌, this에 원하는 객체를 직접 바인딩하면서 호출하는 방법은 없을까요? 이런 경우를 위해 자바스크립트에는 call, apply, bind라는 메서드가 존재하며, 인자로 받은 객체를 함수의 this로 바인딩하면서 호출합니다. 사용 방법은 정말 간단합니다. 😆
call / apply
두 메소드는 인자로 받은 객체를 함수에 바인딩한 후 호출합니다.
const obj = { name: '제리' };
function greeting(age, address) {
console.log(`제 이름은 ${this.name}입니다.`);
console.log(`나이는 ${age}살이고, ${address}에 살고 있습니다.`);
}
// 제 이름은 제리입니다.
// 나이는 20살이고, 서울에 살고 있습니다.
greeting.call(obj, 20, '서울');
greeting.apply(obj, [20, '서울']);
두 메소드 모두 첫 번째 인자로 바인딩하고자 하는 객체를 받으며, 두 번째 인자부터는 함수를 호출할 때 인자로 넘길 값을 입력받습니다. 두 메서드는 동일하게 동작하며, 인자를 배열로 넘기느냐 아니냐의 차이만 있습니다.
bind
bind는 위 메소드들과 동일하게 함수에 객체를 바인딩하지만, 이후 함수를 호출하는 것이 아닌 새로운 this가 바인딩 된 함수를 반환합니다. 반환된 함수는 this 정보가 고정되기 때문에, 이후에는 변경할 수 없습니다.
const obj = { name: '제리' };
const obj2 = { name: '톰' };
function greeting(age, address) {
console.log(`제 이름은 ${this.name}입니다.`);
console.log(`나이는 ${age}살이고, ${address}에 살고 있습니다.`);
}
// 제 이름은 톰입니다.
// 나이는 25살이고, 인천에 살고 있습니다.
const bound = greeting.bind(obj2);
bound(25, '인천');
// 제 이름은 톰입니다.
// 나이는 20살이고, 서울에 살고 있습니다.
bound.call(obj, 20, '서울');
코드의 결과를 보면 bind 함수로 반환받은 함수(bound)는 call과 apply 메서드로 바인딩 정보를 변경하려 해도 불가능한 것을 확인할 수 있습니다. bind 메서드는 함수가 어디서 호출되든 관계없이 this를 고정하고 싶을 때 유용합니다.
Lexical This
일반 함수와 다르게 동작하는 화살표 함수
일반 함수와 달리 화살표 함수는 아래의 특징을 갖습니다.
- 호출에 의해 this가 결정되지 않습니다.
- 생성자 함수로 호출할 수 없습니다.
- arguments 객체를 갖지 않습니다.
- call, apply, bind 메서드로 this를 변경할 수 없습니다.
본문에서는 네 가지의 특징 중 첫 번째에 대해서만 알아보려 합니다. 화살표 함수는 기본적으로 this를 갖고 있지 않은데요. 그렇기 때문에 해당 함수를 둘러싸고 있는 상위 스코프를 가리키게 됩니다. 이렇게 상위 스코프를 거슬러 올라가며 변수를 탐색하는 규칙을 자바스크립트에서는 렉시컬 스코프(Lexical Scope)라 말하며, this가 이를 참조하기 때문에 렉시컬 this라 합니다.
그럼 화살표 함수를 호출하면 일반 함수와 어떻게 다른지 살펴봅시다.
const obj = {
name: '제리',
foo: function () {
console.log(this.name);
},
bar: () => {
console.log(this.name);
},
};
obj.foo(); // 제리
obj.bar(); // undefined
foo 함수는 메서드 형태로 호출했기 때문에 객체 내부의 프로퍼티를 올바르게 참조하지만, 화살표 함수는 함수를 둘러싸고 있는 렉시컬 스코프를 참조하기 때문에 undefined를 출력합니다. bar 함수의 상위 렉시컬 스코프는 전역이기 때문에 위와 같은 결과를 얻게 된 것입니다.
이런 특성으로 인해 화살표 함수는 고차 함수를 사용할 때 더욱 간결한 코드로 작성할 수 있습니다.
function Prefixer(prefixer) {
this.prefixer = prefixer;
}
Prefixer.prototype.add = function (arr) {
return arr.map((item) => {
return `${this.prefixer}${item}`;
});
};
const prefixer = new Prefixer('on');
// [ 'onClick', 'onDrag' ]
console.log(prefixer.add(['Click', 'Drag']));
위 코드는 배열을 인자로 add 메서드를 호출하면, 각 요소의 이름 앞에 접두사를 붙인 다음 새로운 배열로 반환합니다. 고차 함수는 함수를 매개변수로 받는 함수인데, 요소를 순회할 때마다 매개변수로 들어온 함수를 일반 함수처럼 호출합니다. 그렇기 때문에 일반 함수 표현식을 사용해 인자로 보냈다면 전역 객체를 참조하여 문제가 발생했을 것입니다. 하지만 화살표 함수를 사용했기 때문에 함수가 정의된 영역(둘러싸고 있는 렉시컬 스코프) 안에서 값을 참조하게 됩니다.
이처럼 화살표 함수는 간결하게 사용할 수 있고, this가 항상 렉시컬 스코프에 의해 결정되기 때문에 간편합니다. 하지만 this 바인딩이 어떻게 되는지의 차이를 모른다면 사용에 주의가 필요합니다. 대표적인 케이스로 DOM에 이벤트를 등록할 때가 있네요. 😵💫
button.addEventListener('click', function(e){
console.log(this); // <button>버튼</button>
console.log(this === e.currentTarget); // true
});
button.addEventListener('click', (e) => {
console.log(this); // window
console.log(this === e.currentTarget); // false
});
References
'🌈 기술스택 > JavaScript' 카테고리의 다른 글
연속으로 발생하는 이벤트를 제어하는 방법 (0) | 2021.10.07 |
---|---|
코드 실행에 필요한 정보를 담은 실행 컨텍스트 (0) | 2021.10.05 |
자바스크립트 코드의 유효 범위 Scope (0) | 2021.10.03 |
얕은 복사 & 깊은 복사 (0) | 2021.10.03 |
유사 배열 객체 만들기 (0) | 2021.10.02 |
댓글
이 글 공유하기
다른 글
-
연속으로 발생하는 이벤트를 제어하는 방법
연속으로 발생하는 이벤트를 제어하는 방법
2021.10.07 -
코드 실행에 필요한 정보를 담은 실행 컨텍스트
코드 실행에 필요한 정보를 담은 실행 컨텍스트
2021.10.05 -
자바스크립트 코드의 유효 범위 Scope
자바스크립트 코드의 유효 범위 Scope
2021.10.03 -
얕은 복사 & 깊은 복사
얕은 복사 & 깊은 복사
2021.10.03