-
자바스크립트(JS)의 thisCS 2024. 8. 8. 22:58
자바스크립트의 this 키워드는 다른 언어와 다르게 함수나 메서드가 호출되는 방식에 따라 그 값이 결정되기에, 자바스크립트에서는 가장 기본적인 개념 중 하나이면서도 가장 혼란스럽고 중요한 개념 중 하나이다.
이 this 키워드를 주목해야하는 이유는 this의 값은 함수가 어떻게 호출되었는지에 따라 동적으로 변하고, 또한 엄격 모드와 비엄격 모드에서도 일부 차이를 가지고 있기 때문이다.
this의 경우 실행 중에는 할당으로 설정할 수 없는데, ES5는 함수를 어떻게 호출하는지에 관계없이 this 값을 설정할 수 있는 bind 메서드를 도입했고, ES2015는 스스로 this 바인딩을 제공하지 않는 화살표 함수를 도입했다.
이 this 키워드의 다양한 사례와 함께 동작원리를 이해해보도록 하자.
자바스크립트의 함수는 호출될 때 매개변수로 전달되는 인자값 이외에 arguments 객체와 this를 암묵적으로 전달 받는다.
자바스크립트에서 사용되는 this와 유사한 키워드를 우리는 JAVA에서 찾아볼 수 있다.
하지만, 겉으로 보기엔 비슷해도 속에 담긴 개념은 다르다.
❓ JAVA에서 this ❓
JAVA에서 this는 인스턴스 자기 자신을 가리키는 참조변수이다.
즉, this가 객체 자신에 대한 참조 값을 가지고 있다는 뜻이다.
주로 매개변수와 객체 자신이 가지고 있는 멤버변수명이 같을 경우 이를 구분하기 위해서 사용한다.
public Class Cat { private String name; public Person(String name) { this.name = name; } }
위 JAVA 코드의 생성자 함수 내에 this.name은 멤버변수를 의미한다.
name은 생성자 함수가 전달받은 매개변수를 의미하는데, 이 둘을 구분하기 위하여 this를 주로 사용한다.
하지만 자바스크립트의 경우 JAVA와 같이 this에 바인딩되는 객체는 한 가지가 아닌, 해당 함수 호출 방식에 따라 this에 바인딩되는 객체가 달라진다.
✅ 단독으로 사용하는 this ✅
일반적으로 그냥 this를 사용하는 경우 global object를 가리킨다.
브라우저에서 호출하는 경우에는 [object Window]를 가리킨다.
브라우저에서 호출하는 경우 'use strict'; var a = this; console.log(a); // Window
실행 컨텍스트(global, fuction, eval)의 프로퍼티는 비엄격 모드에서는 항상 객체를 참조한다.
반대로 엄격 모드에서는 어떠한 값이든 될 수 있다.
✅ 전역 문맥 ✅
전역 실행 맥락에서 this는 엄격 모드와 비엄격 모드에 관계없이 전역 객체를 참조한다.
// 웹 브라우저에서는 window 객체가 전역 객체 console.log(this === window); // true a = 3; console.log(window.a); // 3 this.b = "alswlfjddl"; console.log(window.b); // "alswlfjddl" console.log(b); // "alswlfjddl"
전역에서 console.log(this === window);를 실행했을 때, 여기서 this는 전역인 window를 가리키기에 true가 출력된다.
✅ 함수 호출 방식에 따른 this 바인딩 ✅
1️⃣ 함수 호출
함수 내에 this는 함수의 주인에게 바인딩이 된다.
여기서 주인이란 그 함수를 품고 있는 대상이다.
function func() { return this; } console.log(func()); //Window
여기서 func 함수를 품고 있는 대상은 전역이다. 즉, Window인 것이다.
var num = 0; function addNum () { this.num = 10; num++; console.log(num) // 11 console.log(window.num) //11 console.log(num === window.num) //true } addNum();
이 코드를 보면 전역으로 num이라는 변수가 선언되어 있고, 아래로 addNum 함수가 정의되어 있다.
그리고 맨 마지막엔 addNum 함수를 호출하고 있는 것이 보인다.
코드가 실행되면 addNum 함수가 실행되어 함수 내부로 들어가게 된다.
addNum 함수 내의 this.num = 10;에서 가리키는 건 전역에 선언된 num을 가르킨다.
왜냐, 위에서 함수 내에 this는 함수를 품고 있는 대상(주인)에게 바인딩이 된다고 했다.
addNum 함수는 전역에 품어져있기에 주인은 Window, 따라서 this는 window 객체를 가리킨다.
그리하여 함수 내부의 num은 전역 변수를 가리키게 된다.
전역 객체는 모든 객체의 유일한 최상위 객체를 의미하며 일반적으로 browser-side에서는 window, sever-side인 Node.js에서는 global 객체를 의미하게 된다.
하지만 엄격 모드에서는 다르다.
function func2() { "use strict"; // 엄격 모드 참고 return this; } func2() === undefined; // true
엄격 모드에서 this 값은 함수 내의 this에 default 바인딩이 없기에 this는 undefined로 남아있다.
따라서 this.num을 호출하면 undefined.num을 호출하는 것과 마찬가지임으로 에러가 발생한다.
2️⃣ 메서드 호출
일반 함수가 아닌 메서드라면, 메서드 호출시 메서드 내부 코드에서 사용된 this는 해당 메서드를 호출한 객체로 바인딩이 된다.
var minji = { firstName: ‘Minji’, lastName: ‘Kim’, fullName: function() { return this.firstName + ‘ ‘ + this.lastName; }, }; minji.fullName(); // "Minji Kim”
위 코드에서는 minji 라는 객체 안에 fullName 메서드를 호출하게 된다.
이 fullName 메서드를 품고 있는 객체의 자체가 this에 바인딩 되는 것이다.
즉, 여기서 this는 minji 라는 객체를 바인딩하고 있어, this.firstName은 minji 라는 객체 안의 firstName 값을 가리키고, this.lastName은 minji 라는 객체 안에 lastName 값을 가리키는 것이다.
var num = 0; function showNum() { console.log(this.num); } showNum(); // 0 var obj = { num: 100, func: showNum, } obj.func(); // 100
함수 호출과 메서드 호출을 비교하기 위해 이 코드를 살펴보도록 하자.
처음 showNum();은 함수 호출이기에, 해당 함수를 품고있는 전역인 window가 this에 바인딩된다.
그리하여, 이 showNum 함수가 호출되었을 때는, window.num인 전역 변수를 가리켜, 0이 출력된다.
그 다음 obj.func();은 메서드 호출임을 이제는 알 수 있다.
그렇기에 해당 func 메서드를 품고 있는 obj 객체가 this에 바인딩된다.
따라서 func 메서드가 호출되면서 showNum 함수가 호출되고 이때는 this.num은 obj.num을 가리키는 것이기에 100이 출력된다.
3️⃣ 이벤트 핸들러 안에 쓴 this (DOM 이벤트 처리기)
이벤트 핸들러에서 this는 이벤트를 받은 HTML 요소를 가리킨다.
var btn = document.querySelector('#btn'); btn.addEventListener(‘click’, function() { console.log(this); //#btn });
var btn = document.querySelector('#btn');은 #btn와 일치하는 문서 내 첫 번째 Element를 반환하고, 일치하는 요소가 없다면 null 반환해준다.
이를 클릭하게 되면 addEventListenver의 두 번째 매개변수의 함수가 실행된다.
이 두 번째 매개변수를 이벤트 핸들러라고 할 수 있다.
이때, 이 이벤트 핸들러에서의 this는 해당 이벤트 핸들러를 받는 HTML 요소를 가리키게 된다.
현재 이 이벤트 핸들러를 받는 요소는 #btn이기에 #btn이 출력된다.
4️⃣ 생성자 함수 호출
생성자 함수로 호출할 경우, 생성자 함수가 생성하는 객체로 this가 바인딩된다.
function Person(name) { this.name = name; } var Kim = new Person(‘Kim’); var lee = new Person(‘lee’); console.log(kim.name); // Kim console.log(lee.name); // lee
이는 JAVA와 동일하게 생성하는 객체로 this가 바인딩되는 것이다.
var name = ‘window’; function Person(name) { this.name = name; } var Kim = Person(‘Kim’); console.log(window.name); // Kim
여기서 주의할 점은 위 코드처럼 new 키워드를 빼먹는다면 일반 함수 호출과 같아지기에, 이 경우에는 this에 window가 바인딩이 된다.
5️⃣ 명시적 바인딩을 한 this
명시적 바인딩은 짝을 연결해주는 것이다.
apply(), call() 메서드는 function object에 기본적으로 정의된 메서드로, 인자를 this로 만들어주는 기능을 한다.
function who() { console.log(this); } who(); // window var obj = { x: 123, }; who.call(obj); // {x:123}
위 코드를 보면 첫 번째 who();는 일반 함수 호출이다. 이럴 경우에는 this가 window를 바인딩한다는 걸 이제는 바로 알 것이다.
그 후 who.call(obj);가 중요하다. call()은 짝을 연결해 주는 것이다. 누구로 연결을 하냐. 인자로 들어온 obj를 this에 바인딩 해주는 것이다. 따라서 who.call(obj);에서 this는 obj 자체를 가리키기에 obj 객체를 출력한다.
apply()에서는 매개변수로 받은 첫 번째 함수 내부에서 사용되는 this에 바인딩이 된다.
두 번째 값인 배열은 자신을 호출한 함수의 인자로 사용한다.
function Character (name, level) { this.name = name; this.level = level; } function Player (name, level, job) { this.name = name; this.level = level; this.job = job; }
위 코드와 같이 두 생성자가 있을 때, 둘은 this.job = job; 외에 동일하다.
이럴 경우 apply()를 사용할 수 있다.
function Character (name, level) { this.name = name; this.level = level; } function Player (name, level, job) { Character.apply(this, [name, level]); this.job = job; } var me = new Player('minji', 100, 'Magician');
그럼 call()과 apply()의 차이는 무엇일까?
그것은 바로 call()은 인수 목록을 받고, apply()는 인수 배열을 받는다는 것이다.
function Character (name, level) { this.name = name; this.level = level; } function Player (name, level, job) { Character.call(this, name, level); this.job = job; } var me = {}; Player.call(me, 'minji', 100, 'Magician');
여기서 주의해야할 점은 둘 다 함수를 호출한다는 것이다.
이 두 함수는 보통 유사배열 객체에서 배열 메서드를 쓰고자 할 때 사용하게 된다.
예를 들어, arguments 객체는 함수에 전달된 인수 배열 형태로 보여주지만, 배열 메서드를 쓸 수는 없다.
이럴 때 apply()와 call()을 사용해, 해당 값을 가져와서 사용이 가능하다.
function func(a, b, c) { console.log(arguments); arguments.push('hi!'); //ERROR! (arguments.push is not a function); }
function func(a, b, c) { var args = Array.prototype.slice.apply(arguments); args.push('hi!'); console.dir(args); } func(1, 2, 3); // [ 1, 2, 3, 'hi!']
var list = { 0: 'Kim', 1: 'Lee', 2: 'Park', length: 3, }; Array.prototype.push.call(list, 'Choi'); console.log(list);
여기에 추가로 bind() 메서드를 알고 가야한다.
bind 메서드는 자바스크립트에서 this의 값을 영구적으로 특정 객체로 고정시키기 위해 사용되는데, bind를 호출하면 원래 함수와 동일한 기능을 수행하지만, this가 고정된 새로운 함수를 반환합니다.
var person = { name: 'ming', greet: function() { console.log('Hello, ' + this.name); } }; var anotherPerson = { name: 'Minji' }; // greet 메서드를 anotherPerson 객체의 this로 고정 var greetAnotherPerson = person.greet.bind(anotherPerson); greetAnotherPerson(); // "Hello, Minji"
위 코드를 보면 person 객체에는 name 속성과 greet 메서드가 있다.
greet 메서드는 this.name을 사용해 person 객체의 name 값을 참조한다.
greet 메서드를 anotherPerson 객체의 this로 고정시키기 위해 bind를 사용한다.
이때 greet.bind(anotherPerson)는 this가 anotherPerson 객체로 고정된 새로운 함수를 반환한다.
greetAnotherPerson()을 호출하면, this는 anotherPerson 객체로 고정되었기 때문에 "Hello, Minji"가 출력된다.
💡 bind와 call, apply 비교 💡
- bind: 함수의 this 값을 고정시키지만, 새로운 함수를 반환하며 즉시 호출하지 않는다.
- call: 함수를 즉시 호출하며, this 값을 첫 번째 인자로 전달한다.
- apply: call과 동일하지만, 인자를 배열로 전달한다.
❗️ 이처럼 bind는 this를 고정시키고 이후에 여러 번 호출할 수 있는 새로운 함수를 생성할 때 유용하고, call과 apply는 함수를 즉시 호출해야 할 때 사용된다.6️⃣ 화살표 함수 this
전역 컨텍스트에서 실행되더라도 this를 새로 정의하지 않고, 바로 바깥 함수나 클래스의 this를 사용한다.
위 코드를 보면 내부 함수에서 this가 전역 객체를 가리키는 바람에 의도와는 다른 결과가 나왔다.
화살표 함수 사용 하지만 이렇게 화살표 함수를 사용하면 this를 새로 정의하지 않고, 바로 바깥 함수나 클래스의 this를 사용하기에 제대로 값이 출력되는 것을 확인할 수 있다.
이것이 가능한 이유는 화살표 함수의 this는 함수가 정의된 컨텍스트를 그대로 가져오기 때문이다. 따라서 일반 함수와 달리 this가 호출 시점의 컨텍스트에 의해 결정되지 않으며, 부모 스코프의 this를 사용하기에 내용이 제대로 출력되는 것이다.
💡 React와 Vue.js에서의 this 💡
React의 this
React 클래스 컴포넌트에서 this는 컴포넌트 인스턴스를 가리킨다. 클래스 컴포넌트의 메서드를 사용할 때 this 바인딩에 주의해야 하며, 이를 해결하기 위해 메서드를 바인딩하거나 화살표 함수를 사용할 수 있다.
class MyComponent extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; this.handleClick = this.handleClick.bind(this); } handleClick() { this.setState({ count: this.state.count + 1 }); } render() { return <button onClick={this.handleClick}>Click me</button>; } }
위 코드에서 this.handleClick을 바인딩하지 않으면, handleClick 메서드에서 this는 undefined가 된다.
이를 방지하려면 bind를 사용하거나 화살표 함수로 정의해야 한다.
// handleClick을 화살표 함수로 구현 handleClick = () => { this.setState({ count: this.state.count + 1 }); };
Vue.js의 this
Vue.js에서 this는 컴포넌트 인스턴스를 참조하며, data, methods, computed, watch 등의 속성에 접근할 때 사용한다.
new Vue({ el: '#app', data: { message: 'Hello Vue!' }, methods: { greet() { console.log(this.message); // 'Hello Vue!' } } });
Vue 컴포넌트에서 주의할 점은 콜백 함수 내에서의 this 바인딩이다. 콜백 내에서 this는 Vue 인스턴스가 아닌 다른 컨텍스트를 가리킬 수 있으므로, this를 고정하기 위해 bind나 화살표 함수를 사용할 수 있다.
// 화살표 함수로 구현 methods: { delayedGreet() { setTimeout(() => { console.log(this.message); // 'Hello Vue!' }, 1000); } }
⭐️ 결론 ⭐️
JavaScript의 this 키워드는 매우 중요한 개념 중 하나인 만큼 매우 복잡하고 혼란을 불러온다. 함수나 메서드가 호출되는 방식에 따라 동적으로 결정되고, 엄격 모드의 사용 여부에 따라 동작 방식이 달라질 수 있기에, this의 동작을 정확히 이해하는 것이 무엇보다도 중요하다.
함수 호출, 메서드 호출, 생성자 호출, 그리고 call, apply, bind 같은 명시적 바인딩 메서드들은 각각 this에 다른 영향을 미친다. 특히, 화살표 함수는 부모 스코프의 this를 상속하는 독특한 특성을 지니고 있어, 복잡한 함수 내부 구조에서도 예측 가능한 this 바인딩을 가능하게 한다.
매우 복잡하지만, 한 번 이해하면 JavaScript 개발에 도움이 될 너무나도 중요한 개념이다.
'CS' 카테고리의 다른 글
자바스크립트(JS)의 prototype (0) 2024.08.09 자바스크립트(JS)의 Closure (0) 2024.08.09 자바스크립트(JS)의 동작원리 (0) 2024.08.08 throttle과 debounce (0) 2024.08.02 Javascript 이벤트 전파와 이벤트 위임 (0) 2024.08.02