-
[JS/this] 자바스크립트, this의 4가지 역할Frontend 2019. 5. 30. 22:33
Javascript, This.
자바스크립트에는 this라는 키워드가 있다. this는 문맥에 따라서 다양한 값을 가지는 데, this가 쓰이는 함수를 어떤 방식으로 실행하느냐에 따라서 그 역할이 구별된다. this의 값들은 크게 4가지 정도로 나눌 수 있다. 즉, this를 이용하는 함수를 4가지 방식 중에서 어떤 방식으로 실행하느냐에 따라 this의 값이 결정된다는 뜻이다. 이러한 특성 때문에 this가 무엇을 지칭하는지 알기 위해서 우리는 this가 사용된 함수가 어디서 어떻게 실행되었는지를 찾아야만 한다.
1. 일반 함수 실행 방식 (Regular Function Call)
첫 번째로, 일반 함수 실행 방식으로 함수를 실행했을 때 this의 값은 Global Object를 가리킨다. 즉, 브라우저 상에서는 window 객체를 말한다. 일반 함수 실행 방식이란 아래 예제 코드처럼 우리가 함수를 선언한 후, 실행할 때 흔히 사용하는 방식을 말한다.
function foo () { console.log(this); } foo();
위 코드에서 우리는 foo라는 함수를 선언하고 foo();라고 실행하였다. 여기서 foo(); 이런 식으로 함수를 호출하는 방식을 일반 함수 실행 방식이라고 한다. 이 때, foo 함수 안에 있는 this는 글로벌 객체, 브라우저 상에서는 window객체를 가리킨다.
var name = 'Julia'; function foo () { console.log(this.name); // 'Julia' } foo();
위 코드에서 우리는 전역 변수로 name이란 변수를 만들고 'Julia'라는 값을 할당하였다. 이 변수는 전역 변수이기 때문에 전역 객체인 window에 속성으로 추가된다. 즉, 우리가 var name = 'Julia';라는 코드를 쓰면 window객체에 name이라는 key와 'Julia'라는 value가 추가된다.
그리고 우리는 foo라는 함수를 선언하였고 일반 함수 실행 방식으로 실행하였다. 이 때의 this는 window객체를 가리키므로 위 코드의 console.log(this.name);는 console.log(window.name);이라고 한 것과 동일하다. 그러므로 위 코드를 실행시키면 console창에는 'Julia'가 출력된다.
var age = 100; function foo () { var age = 99; bar(age) } function bar () { console.log(this.age); } foo();
위 코드를 보자. 이 코드에서 foo 함수 안에 bar 함수가 실행되고 있는 데, bar 함수는 this.age를 콘솔창에 출력한다. 이 때, bar함수를 잘 보면 foo 함수 내부에서 일반 함수 실행 방식으로 실행되고 있다. 그러므로 우리가 bar함수에 매개변수로 무엇을 넘겨주었든, bar 함수 내부의 this.age는 window.age를 가리키고, 이는 전역 변수로 선언된 age 변수의 값을 말한다. 그러므로 위 코드를 실행하면 99가 아닌 100이 출력된다.
* Strict mode에서 일반 함수 실행 방식 (Regular Function Call in Strict Mode)
Strict mode란 단어 그대로 엄격한 형식이라는 뜻이다. 즉, Strict mode에서 실행되는 모든 코드들에 좀 더 엄격한 규칙들을 적용하는 것을 말한다. 이러한 엄격한 방식을 적용하는 이유에는 여러 가지가 있겠지만 가장 크게는 비엄격 모드(Sloppy mode)에서 자주 일어나는 다양한 실수들을 방지하여 각종 에러들을 감소시키는 데 있다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Strict_mode
strict mode를 사용하려면 스크립트 코드 맨 상단에 'use strict'; 혹은 "use strict";라는 구문을 추가하면 된다.
'use strict'; var name = 'Julia'; function foo () { console.log(this.name); // error } foo();
보통 일반 함수 실행 방식에서 this는 window 객체를 가리킨다고 했다. 그러나 strict mode에서 this는 무조건 undefined이다. 그렇기 때문에 위 코드에서 this.name을 출력하면, foo함수가 아무리 일반 함수 실행 방식으로 실행되었다고 하더라도 strict mode이기 때문에 this는 undefined가 된다. undefined에는 어떠한 속성도 없으므로 this.name은 실행할 수 없어 에러가 출력되게 된다. 보통 코드를 작성할 때 this가 실수로 window 객체로 인식되어 예상치 못한 에러가 발생하는 경우가 많은 데, strict mode는 그러한 버그의 발생을 애초에 방지해준다.
2. 도트 표기법 (Dot Notation)
Dot Notation이란 우리가 Object를 만들고 그 Object의 key와 value를 부여한 후 도트(.)로 값에 접근하는 방식을 말한다. 아래 예제를 보면 이해가 쉽다.
var age = 100; var ken = { age: 35, foo: function () { console.log(this.age); // 35 } } ken.foo();
우리는 ken이라는 변수에 Object를 만들었다. 그리고 foo라는 key에 this.age를 출력하도록 함수를 만들었다. 그리고 우리는 ken.foo();라고 함수를 실행하고 있는 데, 이렇게 도트를 사용하여 객체 속성의 값에 접근하는 방식을 Dot Notation이라고 한다.
그리고 이렇게 Dot Notation으로 함수가 실행되면, this는 그 도트 앞에 써있는 객체 자체를 가리킨다. 즉, 위 코드에서 this.age의 this는 ken을 가리킨다. 그러므로 this.age는 ken.age와 같기 때문에 35가 출력된다.
function foo() { console.log(this.age); } var age = 100; var ken = { age: 36, foo: foo } var wan = { age: 32, foo: foo } ken.foo(); // 36 wan.foo(); // 32 var fn = ken.foo; fn(); // 100
위의 흥미로운 예제를 살펴보자. foo라는 함수에는 this.age를 출력하는 실행문이 들어있다. 그리고 전역 변수 age가 선언되어 100이라는 값이 할당되었다. 그리고 ken이라는 객체가 선언되어, age와 foo라는 key를 부여하였는 데, 여기서 foo의 value는 함수 foo의 이름이다. wan이란 객체도 마찬가지다.
이 상황에서 우리가 ken.foo();라고 함수를 실행하면 이 때의 foo 함수는 Dot Notation 방식으로 실행되었기 때문에, this는 ken 객체 자체를 가리키게 되고 36이란 값이 출력된다. wan.foo();도 마찬가지이다. 그러나 아래 fn변수를 보자. fn이란 변수에 우리는 ken.foo라는 값을 입력하고 fn();라고 실행하였다. 이는 즉, 함수를 Dot notation이 아니고 일반 실행 함수 방식으로 실행한 것이다. 그러므로 이 때의 this는 Global Object를 가리키게 되고 전역 변수 age의 값인 100이 출력된다.
3. 명백한 바인딩 (Explicit Binding) / call, bind, apply
명백한 바인딩, 즉 this의 역할을 우리가 직접 명확하게 지정해준다는 뜻이다. 이는 function.prototype.call, function.prototype.bind, function.prototype.apply과 같은 메소드를 사용하여 할 수 있다.
var age = 100; function foo() { console.log(this.age); } var ken = { age: 35, log: foo } foo.call(ken, 1, 2, 3);
위 코드에서 우리는 foo 함수에 call 메소드를 사용하여 실행하였는 데, 인자로 각각 ken, 1, 2, 3을 주었다. 이 인자들 중에서 가장 첫 번째로 쓴 ken이 바로 this의 값으로 지정된다. 1, 2, 3은 this의 값과는 상관없이 순서대로 foo 함수가 된다. 그러므로 위 코드에서 this.age는 ken.age가 되어 35가 출력된다.
var age = 100; function foo() { console.log(this.age); } var ken = { age: 35, log: foo } foo.apply(ken, [1, 2, 3, 4, 5]);
위 코드에서 apply또한 같은 역할을 한다. apply는 this의 값을 지정해주는 인자 외에도 배열을 인자로 넣을 수 있는 데, 이 배열의 값이 순차적으로 foo 함수의 인자가 된다.
4. new 키워드를 사용한 함수 실행
우리는 함수를 그냥 foo()와 같은 형태로 실행할 수도 있지만 new 키워드를 사용해서 생성자 함수로 만들어 사용할 수도 있다. 이 경우에 this는 빈 객체가 된다.
function Person () { console.log(this); } new Person();
즉 위 코드에서 우리는 Person이라는 함수를 선언하였고, new Person(); 즉, new 키워드를 사용하여 Person 함수를 생성자 함수로 사용하였다. 이 때, this는 빈 객체를 가리키며 위의 생성자 함수는 this라는 빈 객체를 return한다. (심지어 return문이 없더라도 말이다. 이는 생성자 함수의 특징 중 하나이다.)
function Foo () { console.log(this.age); // undefined this.age = 100; // 빈 객체에 속성 추가 console.log(this.age); // 100 } new Foo();
위 코드에서 Foo 함수가 new 키워드와 함께 생성자 함수로 사용되는 즉시, 함수 내부의 this는 빈 객체가 되며, this.age = 100; 이라는 코드를 통해, 그 빈 객체에 age라는 속성을 추가하고 100이란 값을 할당하게 된다. 그러므로 4번째 줄에서 this.age는 100을 출력한다. 그리고 Foo 함수는 { age: 100 }이라는 객체를 리턴한다.
function Person () { this.name = 'ken'; console.log(this); } var ken = new Person(); console.log(ken);
위 코드를 보자. Person이라는 함수가 있고, this.name = 'ken';이라는 구문이 있다. 만약에 위 코드가 일반 함수 실행 방식으로 실행되었다면, this는 window를 가리키게 될 것이고 window 객체에 name이란 속성과 'ken'이란 값이 추가되었을 것이다. 그리고 위 코드는 return문이 없기 때문에 어떠한 값도 리턴하지 않으므로 ken이란 변수엔 어떤 것도 할당되지 않아 console창에는 undefined가 출력될 것이다.
그러나 위 함수는 new Person(); 즉, 생성자 함수로 실행되었다. 그러므로 this는 빈 객체를 생성하여 name이란 속성과 'ken'이란 값을 할당할 것이고 return문이 없음에도 불구하고 그 객체가 리턴된다. 그러므로 ken이란 변수에는 { name: "ken" } 이라는 객체가 할당되어 console.log에는 그 객체가 return된다.
function foo () { this.age = 100; return 3; } var a = new foo(); console.log(a);
생성자 함수는 특이하게도 return문이 있음에도 불구하고 그 return문을 무시하고 this 객체를 return하는 특징이 있다. 즉, 위의 코드에서 일반적인 상식으로는 3이 리턴되어야 하지만 생성자 함수로 사용되었기 때문에 { age: 100 }이 return된다.
function foo () { this.age = 100; return { haha: 23232 }; } var a = new foo(); console.log(a);
그러나 생성자 함수도 리턴되는 대상이 객체라면 this객체 대신에 해당 객체가 리턴된다. 즉, 위에서 a는 { age: 100 }이 아닌 { haha: 23232 }가 된다.
[Quiz] 아래 코드에서 'I can do my work! I am smart!'라는 alert가 출력이 될 것인가?
function programmer() { this.isSmart = false; this.upgrade = function (version) { this.isSmart = !!version; work(); } } function work() { it (this.isSmart) { window.alert('I can do my work! I am smart!'); } } var programmer = new programmer(); programmer.upgrade(1.1);
나는 처음에는 이 퀴즈의 함정에 걸려들었다. 그러니까 처음엔 이렇게 생각했다. programmer 변수에 programmer라는 생성자 함수를 할당하였으므로 생성자 함수의 this객체가 리턴될 것이다. 그 객체의 upgrade라는 함수를 호출하였고 1.1이란 변수를 넣었으므로 4번 째 구문이 실행된다. 그리고 1.1이란 숫자값은 truthy한 값이므로 앞에 !!을 붙여도 true이고 this.isSmart의 값은 true가 된다. (여기까지는 맞았다.)
그리고 work()함수가 실행되는 데, work함수에서 this.isSmart는 true이므로 alert가 실행된다...고 생각했다. 그런데 생각해보니 work 함수는 일반 함수 실행 방식으로 실행되었다! 그러니까 work 함수 안에서 this는 window객체를 말한다. 우리는 어디에서도 전역 변수로 isSmart라는 값을 주지 않았으므로 this.isSmart는 undefined이다. 그러므로 위 구문에서 alert()는 실행되지 않는다... 그렇다... isSmart... 스마트하지 못하다. 흑흑...ㅠㅠ...ㅋㅋㅋㅋㅋ 앞으로는 헷갈리지 않는 것으로...ㅋㅋㅋ
반응형'Frontend' 카테고리의 다른 글
[JS] 자바스크립트, 생성자함수(Constructor function)와 프로토타입 체인(Prototype chain) (1) 2019.05.31 [JS/Slider] 바닐라 자바스크립트로 무한 루프 슬라이드(Carousel) 구현하기 (20) 2019.05.31 [JS/ES2015] 자바스크립트 ES2015(ES6) 문법 정리(1) - let / const (0) 2019.05.17 [JS/디버깅] 개발자도구로 자바스크립트 디버깅하기(debugger) (1) 2019.05.14 [JS/호이스팅] Interpreted Language인 자바스크립트의 호이스팅(Hoisting)에 대하여 (5) 2019.05.03 COMMENT