자바나 코틀린같은 다른 언어를 사용하다보면 this context를 사용해본 경험이 있을 것이다. 자바스크립트에서도 this context가 존재하나 다른 언어와 다른점이 있어서 포스팅을 한다.
this context
자바스크립트에는 선언할 때 결정되는 부분이 있고 호출할 때 결정되는 부분이 있다. 자바스크립트에서 this는 함수를 호출할 때 결정되며 함수를 호출한 객체가 this로 지정한다. 즉 this는 함수의 실행 context를 의미한다.
다음의 코드를 보자
const person={
name:'Mike',
age:'10',
getName: function(){
console.log(this);
return this.name;
},
}
person.getName(); // 1번
setTimeout(person.getName,1000); // 2번
1번 방식과 2번 방식은 모두 같은 객체에서 같은 함수를 호출하는 코드이다. 그러나 이 두개의 this값은 다르다. 1번 방식에서 getName이 가지는 this는 person객체 자신을 가지지만 2번 방식의 경우 크롬 브라우저에서 실행할 경우 window객체가 반환된다. 1번 방식은 호출한 객체가 person객체여서 this가 person으로 잡히고 2번 방식은 함수를 바로 실행시키지 않고 1초 후에 window객체가 함수를 실행시키기 때문에 window객체가 this로 잡힌다.
전역객체
같은 자바스크립트라 하더라도 수행되는 부분이 어디인지에 따라 this의 기본 객체는 다르게 잡힌다. 필자는 브라우저(크롬)과 nodejs에서 this전역 객체에 대해 서술한다. 같은 V8엔진 기반이지만 전역객체가 다르다니 신기하기도 하다.
1. 브라우저
브라우저 전역객체는 window객체가 잡힌다.
console.log(this===window);// true
2. node
nodejs는 브라우저의 window객체와 같은 global이란 전역 객체가 있다. 이 객체는 global
키워드를 통해 호출할 수 있다.
그러나 브라우저와 동일하게 nodejs를 js파일로 실행시키 전역에서 this 객체를 출력하면 빈 객체가 나온다.
//test.js
console.log(this);
그런데 REPL로 같은 코드를 수행하면 global객체가 찍혀서 나온다.
nodejs의 전역객체는 global이라는데 .js파일을 이용하여 실행하면 빈객체가 나오고 REPL로 수행하면 global객체가 출력된다. 이유는 파일로 실행하면 this가 module.exports로 잡히기 때문이다. 실제로 module.exports의 축약형인 exports에 값을 추가하면 this전역객체에 추가된다. 다음의 코드를 보면 알 수 있다.
console.log(this);
console.log(this===module.exports);
console.log(this===exports);
exports.val='1234';
this.func1=()=>{console.log("test fuction");}
console.log(this);
console.log(module.exports);
위의 코드의 실행결과는 다음과 같다.
앞의 ===
연산자로 this===module.exports===exports
관계를 가지는 것을 알 수 있고 exports에 추가가하거나 전역 상태에서 this 객체에 내용을 추가할
this객체에 추가된 내용과 module.exports에 추가된 내용이 모두 같다는 것을 확인할 수 있다.
global이 nodejs의 전역객체라는데 global이 잡히는 경우는 없는것인가 라고 물어보면 잡히는 경우가 있다. 그것은 바로 함수에서 사용했을 때이다.
function outer(){
console.log(this===global); // true
function inner1(){
console.log(this===global); // true
function inner2(){
console.log(this===global); // true
}
inner2();
}
inner1();
}
outer();
위의 코드의 결과는 모두 true가 나온다.
this가 햇갈리는 경우
this가 여러운 경우는 실행하는 곳에 따라 값이 바뀌기 때문이다. 예상치도 못한 경우에 this가 바뀌는 경우가 있는데 이러한 경우들에 대해 설명을 하겠다.
1. 내부함수
다음의 코드를 보자
const obj={
value1:10,
value2:20,
outer:function(){
console.log(this.value1+this.value2); // 30
function inner(){
console.log(this===window);// true
console.log(this.value1+this.value2); // NaN
}
inner();
}
}
obj.outer();
obj객체가 sum을 호출하였으니 sum에서 호출한 inner객체도 같은 obj객체가 this일 줄 알았지만 확인해보니 전역 객체가 나왔다 이것은 왜 그럴까?
이러한 결과가 나온 이유는 아쉽게도 설계단계에서의 결함이라고 한다. 이 결함으로 인해 내부함수는 선언 위치에 관계없이 전역 객체를 가진다. 이 문제를 해결하는 방법은 뒤에서 나올 명시적 바인딩이나 arrow function을 이용하여 전역 객체를 가지지 않도록 만들 수 있다.
const obj={
value1:10,
value2:20,
outer:function(){
console.log(this.value1+this.value2); // 30
const inner1=()=>{
console.log(this===window);// false
console.log(this.value1+this.value2); // 30
}
inner1();
const inner2=function(){
console.log(this===window);// true
console.log(this.value1+this.value2); // NaN
}
inner2();
}
}
obj.outer();
arrow function형태로 작성하니 내부함수인 inner1이 외부함수인 outer과 같은 this객체를 가지는 것을 볼 수 있다.
2. new operator사용
new operator은 새로운 객체를 만들어주는 연산자로서 문법은 다음과 같다.
new constructor[([arguments])]
- constructor: 생성될 객체를 정의해주는 함수
- arguments: constructor에 필요한 인자
var obj1;
function Obj(number1, number2){
this.number1=number1;
this.number2=number2;
console.log(this); // Obj {number1: 10, number2: 20}
obj1=this;
}
const myObj=new Obj(10,20);
console.log(obj1===myObj); // true
console.log(myObj.number1); // 10
new키워드를 통해 함수를 호출할 시 함수를 기반으로 새로운 객체로 반환해준다. 이때 반환해주는 객체는 this이며 이를 통해 new 키워드를 사용할 시 this가 새롭게 생긴다는 것을 알 수 있다.
3. arrow function
arrow function을 단순히 익명함수를 용이하게 작성하는 문법아리고만 알고있는 사람들이 있다. 그러나 자바스크립트는 아니다. arrow function으로 선언한 익명함수와 function으로 선언한 익명함수는 this부분에서 달라진다.
const person={
name:'Mike',
age:'10',
getName: function(){
return this.name;
},
getAge:()=>{
return this.age;
}
}
console.log(person.getName());
console.log(person.getAge());
위의 코드에서 person객체에서 선언된 getName이라는 함수는 function키워드로 익명함수를 작성하였고 getAge함수는 arrow function으로 익명함수를 작성하였다. 두개가 같은 익명함수임에도 불구하고 getName의 this는 person객체의 값인 name을 제대로 가져오지만 getAge는 person객체의 값인 age를 가져오지 못한다.
그럼 getAge와 getName에서 사용되는 this객체가 서로 다른 context를 가리킨다는 것을 알 수 있다. 이를 출력하면 다음과 같다.
//getName()
{
name: 'Mike',
age: '10',
getName: [Function: getName],
getAge: [Function: getAge]
}
//getAge
Window{~
브라우저 콘솔에서 실행했을 때 getAge함수는 전역 객체인window객체가 출력되고 getName은 person객체를 출력해준다.
이러한 결과가 나오는 이유는 arrow function을 이용하여 함수를 만들 시 함수가 만들어졌던 당시에 this가 결정이 되기 때문이다. 즉 getAge가 선언이 될 때는 person객체가 만들어지고 있는 중이기 때문에 전역 객체인 window로 this가 결정되었다고 볼 수 있다.
this 바인딩(binding)
this의 어려운 점은 this값이 고정적이지 않고 실행하는 장소마다 변할 수 있다는 것이 코딩하는데 어려움을 준다. 이러한 문제를 해결하기 위해 this값이 변하지 않도록 특정 객체를 this로 고정하는 것을 this바인딩이라고 한다. 영단어의 bind의 의미인 '누군가의 손이나 발을 움직이지 못하도록 묶는다'는 것을 생각하면 쉽게 이해할 수 있다.
this context는 기본적으로 전역 객체로 잡힌다. 그래서 다음과 같은 코드도 작성할 수 있다.(default binding)
this.name='Mike';
function getName(){
return this.name;
}
console.log('name: ',getName()); // name: Mike
브라우저에서 위의 코드를 실행시키면 this가 브라우저 전역 객체인 window로 잡혀서 해당 코드가 동작한다.
바인딩 함수(call, apply, bind)
자바스크립트에서는 바인딩을 수행할 수 있는 3가지 함수가 있다. 이 함수를 이용하여 바인딩을 수행하는 것을 명시적 바인딩이라고 한다. 각 함수의 기능은 다음과 같다.
Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])
함수를 호출하되 this객체를 thisArg값으로 바인딩하고 함수 실행한다. 이때 함수 실행을 위한 필수 인자는 thisArg객체 이후로 차례대로 작성한다.
function calculator(opend1,opend2) {
if(this.operator==='+'){
return opend1+opend2;
}else{
return null;
}
}
console.log(calculator(10,20)); //null
console.log(calculator.call({operator:'+'},10,20)); // 30
Function.prototype.apply(thisArg, [argsArray])
함수를 호출하되 함수 내부의 this객체를 thisArg로 바인딩하고 함수 실행 시 필요한 인자들을 배열 형태로 넘겨준다.
function calculator(opend1,opend2) {
if(this.operator==='+'){
return opend1+opend2;
}else{
return null;
}
}
console.log(calculator(10,20)); // null
console.log(calculator.apply({operator:'+'},[10,20])); // 30
Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])
함수를 호출하지 않고 함수 내부의 this context를 바인드를 수행하고 이를 새로운 함수로 반환한다. 이때 새로 반환된 함수를 bound function이라고 불린다.
function calculator(opend1,opend2) {
if(this.operator==='+'){
return opend1+opend2;
}else{
return null;
}
}
const bindFunc=calculator.bind({operator:'+'});
console.log(bindFunc(10,20));
console.log(calculator(10,20));
Arrow function도 명시적 바인딩이 가능할까?
arrow function은 함수를 선언할 때 함수의 선언하는 부분에서 this가 정해진다면 선언 후에 명시적 바인딩으로 this를 바인딩 함수로 바꿀 수 있는지를 보기 위해 다음의 코드를 실행해보자.
const person={
name:'Mike',
age:'10',
getName: function(){
return this.name;
},
getAge:()=>{
return this.age;
}
}
console.log(person.getAge()); // undefined
console.log(person.getAge.call(person)); // undefined
console.log(person.getAge.apply(person)); // undefined
console.log(person.getAge.bind(person)()); // undefined
위의 코드를 실행하면 바인딩 함수 사용 여부에 관계없이 모두 undefine
를 출력한다. 즉 this 가 바뀌지 않았다. 이를 통해 arrow function으로 선언 시 this가 불변으로 고정되어서 바인딩이 불가능함을 알 수 있다.
참고
Function.prototype.call() - JavaScript | MDN
Function.prototype.apply() - JavaScript | MDN
'javascript' 카테고리의 다른 글
javascript 문자열 치환방법 (0) | 2021.10.21 |
---|