자바스크립트의 this는 파이썬의 self와 비슷하다. 그렇지만 미묘하게 다르게 작동하는 부분이 있는 것 같아서 자바스크립트의 this에 대해서 공부해보려 한다.
class Test:
def __init__(self,_num):
self.__num = _num
@property
def num(self):
return self.__num
def increaseInner(self):
self.__num += 1
return self.__num
test = Test(1)
increaseGlobal = test.increaseInner
increaseGlobal()
print(test.num) # 2
increaseGlobal()
print(test.num) # 3
increaseGlobal()
print(test.num) # 4
파이썬의 self는 위와 같이 동작했다. global 변수인 increaseGlobal로 Test 클래스의 객체인 test의 메서드를 받는다고 하더라도 해당 메서드에서 self는 여전히 test 객체를 의미하고, 따라서 test객체의 num이 변경되는 것을 알 수 있다. 그러나, 자바스크립트의 경우 가끔 다르게 행동하는 것을 알 수 있었다.
const test = {
num: 1,
increaseInner() {
return ++this.num;
},
};
const increaseGlobal = test.increaseInner;
increaseGlobal();
console.log(test.num); // 1
increaseGlobal();
console.log(test.num); // 1
increaseGlobal();
console.log(test.num); // 1
test.increaseInner();
console.log(test.num); // 2
test.increaseInner();
console.log(test.num); // 3
test.increaseInner();
console.log(test.num); // 4
test객체의 increaseInner 함수를 외부 변수인 increaseGlobal로 받고나서 실행했을 때에는 test객체의 num 프로퍼티가 예상했던 것처럼 변경되지 않았다. 여기서 의문이 생겨서 increaseInner 함수에서 this가 무엇인지 알기 위해 console.log 함수를 이용해서 알아보았다.
// increaseGlobal()의 this
<ref *1> Object [global] {
global: [Circular *1],
queueMicrotask: [Function: queueMicrotask],
clearImmediate: [Function: clearImmediate],
setImmediate: [Function: setImmediate] {
[Symbol(nodejs.util.promisify.custom)]: [Getter]
},
structuredClone: [Function: structuredClone],
clearInterval: [Function: clearInterval],
clearTimeout: [Function: clearTimeout],
setInterval: [Function: setInterval],
setTimeout: [Function: setTimeout] {
[Symbol(nodejs.util.promisify.custom)]: [Getter]
},
performance: Performance {
nodeTiming: PerformanceNodeTiming {
name: 'node',
entryType: 'node',
startTime: 0,
duration: 103.18854200094938,
nodeStart: 19.362916998565197,
v8Start: 28.167625002563,
bootstrapComplete: 93.71975000202656,
environment: 61.38929200172424,
loopStart: -1,
loopExit: -1,
idleTime: 0
},
timeOrigin: 1654499959665.99
},
fetch: [AsyncFunction: fetch],
num: NaN
}
// test.increaseInner()의 this
{ num: 1, increaseInner: [Function: increaseInner] }
test.increaseInner()로 메서드를 호출하였을 때에는 기대했던 것처럼 test객체를 의미하고 있었지만, 외부 변수인 increaseGlobal()로 메서드를 호출하였을 때에는 this는 전역객체를 의미하고 있었다. 전역 객체에는 num이라는 프로퍼티가 없으니 최초에 undefined였고, 나중에 연산이 가해지면서 undefined + 1 연산이 수행되어 NaN이 대입된 것으로 보였는데, 왜 이런 차이가 발생하는지 의문이 생겼다.
class Test {
constructor(num) {
this.num = num;
}
increaseInner() {
console.log(this)
return ++this.num;
}
}
const classTest = new Test(1);
classTest.increaseInner(); // Test { num: 1 }
console.log(classTest.num); // 2
classTest.increaseInner(); // Test { num: 2 }
console.log(classTest.num); // 3
classTest.increaseInner(); // Test { num: 3 }
console.log(classTest.num); // 4
const classIncreaseGlobal = classTest.increaseInner;
classIncreaseGlobal(); // undefined
// TypeError: Cannot read properties of undefined (reading 'num')
console.log(classTest.num);
classIncreaseGlobal();
console.log(classTest.num);
classIncreaseGlobal();
console.log(classTest.num);
생성자를 통해 객체를 만들어서 해당 객체의 메서드를 받아올 경우, this가 undefined를 의미하게 된다. 이 경우에서는 마찬가지로 전역 객체를 통해 호출함에도 불구하고 undefied가 출력된다. this는 undefined를 의미하게 되었고, undefined의 num이라는 property를 참조하려 하였기 때문에 오류가 난다. 이 부분에 대해서는 왜 자바스크립트 엔진이 이런식으로 작동하는 지에 대해서 더 공부가 필요할 것 같다. classIncreaseGlobal을 출력해볼 경우, [Function: increaseInner]이라고 함수가 정상적으로 받아진 것은 알 수 있었다.
그러나, 정적 메서드를 활용해 정적 프로퍼티를 제어하는 경우에는 정확하게 의도한 바대로 진행되는 것을 알 수 있었다. 그러나, 여기서도 this가 의미하는 것은 변했는데, 외부 변수로 받아서 this에 대해서 알아볼 경우에는 undefined가 나오고, class를 통해서 메서드에 접근할 때에만 정상적으로 해당 class를 의미하는 것을 알 수 있었다.
class TestStatic {
static num = 1
static increaseInner() {
console.log(this)
return ++TestStatic.num;
}
}
const staticIncreaseGlobal = TestStatic.increaseInner
staticIncreaseGlobal(); // undefined
console.log(TestStatic.num); // 2
staticIncreaseGlobal(); // undefined
console.log(TestStatic.num); // 3
staticIncreaseGlobal(); // undefined
console.log(TestStatic.num); // 4
TestStatic.increaseInner(); // [class TestStatic] { num: 4 }
console.log(TestStatic.num); 5
TestStatic.increaseInner(); // [class TestStatic] { num: 5 }
console.log(TestStatic.num); 6
TestStatic.increaseInner(); // [class TestStatic] { num: 6 }
console.log(TestStatic.num); 7
결국 돌고 돌아서 자바스크립트 딥다이브 책을 참고하게 되었는데, 책에서 말하는 this는 다음과 같았다.
this란 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가르키는 자기 참조 변수이다. this를 통해 자신이 속한 객체 또는 자신이 생성할 인스턴스의 프로퍼티나 메서드를 참조할 수 있다. 함수를 호출하게 되면 arguments 객체와 this가 자바스크립트 엔진에 의해 암묵적으로 함수 내부로 전달되고 사용할 수 있게 되지만, this가 가르키는 값, 즉 this 바인딩은 함수 호출 방식에 의해 동적으로 결정된다.
const foo = function () {
console.log(this);
};
foo();
/*
<ref *1> Object [global] {
global: [Circular *1],
queueMicrotask: [Function: queueMicrotask],
clearImmediate: [Function: clearImmediate],
setImmediate: [Function: setImmediate] {
[Symbol(nodejs.util.promisify.custom)]: [Getter]
},
structuredClone: [Function: structuredClone],
clearInterval: [Function: clearInterval],
clearTimeout: [Function: clearTimeout],
setInterval: [Function: setInterval],
setTimeout: [Function: setTimeout] {
[Symbol(nodejs.util.promisify.custom)]: [Getter]
},
performance: Performance {
nodeTiming: PerformanceNodeTiming {
name: 'node',
entryType: 'node',
startTime: 0,
duration: 98.11158300191164,
nodeStart: 20.139915995299816,
v8Start: 29.74704100191593,
bootstrapComplete: 90.46666599810123,
environment: 58.58533299714327,
loopStart: -1,
loopExit: -1,
idleTime: 0
},
timeOrigin: 1654502283818.563
},
fetch: [AsyncFunction: fetch]
}
*/
const obj = { foo };
obj.foo();
// { foo: [Function: foo] }
new foo();
// foo {}
const bar = { name: "bar" };
foo.call(bar); // { name: 'bar' } (bar이다.)
foo.apply(bar); // { name: 'bar' } (bar이다.)
foo.bind(bar)(); // { name: 'bar' } (bar이다.)
함수를 호출하는 방식은 굉장히 많다.
1. 일반 함수 호출
2. 메서드 호출
3. 생성자 함수 호출
4. Function.prototype.call / apply / bind에 의한 호출
우선, 일반 함수 호출에 의해 함수가 호출되었을 때에는 this는 전역 객체를 의미한다. 그러나, 'use strict' 키워드와 함께 쓰여진 함수에 대해서는 undefined에 바인딩되며, 이는 메서드 안에 일반 함수를 선언하고 일반 함수를 그 안에서 호출할 때에도 마찬가지로 전역 객체 / undefined가 바인딩된다. 이는 콜백함수로써 함수가 호출되는 경우에도 마찬가지인데, 즉 일반 함수를 호출할 때에는 this는 전역 객체에 바인딩된다는 것을 알 수 있다. 화살표 함수의 경우에는 조금 다른데, 화살표 함수는 this에 바인딩되지 않고 상위 스코프의 this에 바인딩되게 된다. 화살표 함수와 일반 함수가 완벽하게 일치하지 않는다는 것이다.
메서드 호출에 의해서 함수가 호출되었을 때에는 메서드를 호출한 객체를 의미하게 된다. 글에서 처음에 test 객체를 선언하고, test 객체를 통해 메서드를 호출하였을 때와 다른 전역 변수로 메서드를 받아서 전역 변수를 통해 메서드를 호출한 경우, 전자는 test 객체를 통해 메서드를 호출하였으므로 test 객체를, 후자의 경우 전역 변수를 통해 호출하였으므로 전역 객체를 의미하게 되는 것이다.
생성자 함수를 호출하였을 때에는 만들어지는 객체를 의미하게 된다. 단, 여기서 객체는 순차적으로 만들어지기 때문에 객체에 프로퍼티를 추가하는 중간에 this를 출력하게 되면, 출력 이후에 할당될 프로퍼티에 대한 정보는 담기지 않는다. 따라서, 만들고 있는 객체에 바인딩된다고 보는 것이 더 적당할 것 같다.
마지막으로 Function.prototype.call / apply / bind의 경우, 함수를 호출할 때 this로 바인딩 될 것에 대한 인자를 넘겨주게 된다. 위의 예시의 경우에는 bar를 this로 바인딩하도록 인자를 넘겨줬기 때문에, bar가 출력되는 것을 알 수 있다.
'배운 것' 카테고리의 다른 글
DAY01 - 학습정리 (0) | 2022.07.18 |
---|---|
미확인 도착지 (0) | 2022.06.08 |
타입스크립트 데코레이터 (0) | 2022.06.01 |
징검다리 건너기 (0) | 2022.05.30 |
프로그래머스 지형 이동 (0) | 2022.05.27 |