일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- type assertion
- 암묵적 타입 변환
- Custom Hook
- docker
- prettier
- 프로젝트 구조
- 주니어개발자
- Microtask Queue
- 명시적 타입 변환
- Render Queue
- linux 배포판
- TypeScript
- 타입 단언
- JavaScript
- webpack
- Headless 컴포넌트
- next.js
- CS
- prettier-plugin-tailwindcss
- Sparkplug
- VirtualDom
- task queue
- 좋은 PR
- Dockerfile
- 프로세스
- Pull Request
- Compound Component
- Event Loop
- AJIT
- react
- Today
- Total
너굴 개발 일지
[리뷰] 코어 자바스크립트 3장 - this 본문
이 글은 코어 자바스크립트 책을 읽으며 정리한 글입니다.
3장. this
3-1. 상황에 따라 달라지는 this
전역 공간에서의 this
전역 공간에서의 this는 전역 객체를 의미합니다. 개념상 전역 컨텍스트를 생성하는 주체는 전역 객체이기 때문입니다.
브라우저 환경에서 전역 객체는 window, Node.js 환경에서 전역 객체는 global입니다.
전역 변수, 전역 객체 예시 (1)
var a = 1;
console.log(a); // 1
console.log(window.a); // 1
console.log(this.a); // 1
전역 변수 선언 및 할당시, window.a와 this.a 모두 1이 출력됩니다. 이는 JS의 모든 변수는 실행 컨텍스트의 LexicalEnnvironment(이하 L.E)의 프로퍼티로서 동작하기 때문입니다. 실행 컨텍스트는 변수를 수집해 L.E의 프로퍼티로 저장하고 이후 어떤 변수 호출시 L.E를 조회해 일치하는 프로퍼티가 있을 경우 값을 반환합니다. 전역 컨텍스트의 경우 L.E는 전역 객체를 그대로 참조합니다.
따라서 전역 변수 선언시, JS 엔진은 이를 전역 객체의 프로퍼티로 할당합니다.
전역 변수, 전역 객체 예시 (2)
var a = 1;
delete window.a; // false
console.log(a, window.a, this.a); // 1 1 1
var b = 2;
delete b; // false
console.log(b, window.b, this.b); // 2 2 2
window.c = 3;
delete window.c; // true
console.log(c, window.c, this.c); // Uncaught ReferenceError: c is not defined
window.d = 3;
delete d; // true
console.log(d, window.d, this.d); // Uncaught ReferenceError: d is not defined
처음부터 전역 객체의 프로퍼티로 할당한 경우, 삭제가 가능하지만 전역 변수로 선언시 삭제가 불가능합니다.
전역 변수 선언시, JS엔진이 이를 자동으로 전역 객체의 프로퍼티로 할당하면서 configurable(변경 및 삭제 가능성) 속성을 false로 정의하기 때문입니다. (사용자가 의도치 않게 삭제하는 것을 방지하기 위한 나름의 방어 전략입니다.)
메소드로서 호출할 때 그 메소드 내부에서의 this
함수 실행 방법으로 (1) 함수로서 호출, (2) 메소드로서 호출하는 방법이 있는데 이 둘의 차이점은 독립성에 있습니다.
함수로서 호출, 메소드로서 호출 예시
var func = function (x) {
console.log(this, x);
};
func(1); // Window {...} 1
var obj = {
method: func,
};
obj.method(2); // {method: f} 2
익명 함수는 그대로인데 이를 변수에 담아 호출한 경우와 obj 객체의 프로퍼티에 할당해 호출한 경우 this가 달라집니다.
함수로서의 호출, 메소드로서의 호출을 구분하는 방법은 함수 앞에 점(.)이 있는지 여부로만으로도 간단하게 구분할 수 있습니다. (대괄호 표기법도 마찬가지입니다.)
어떤 함수를 메소드로서 호출하는 경우, 호출 주체는 바로 함수명(프로퍼티명) 앞의 객체로 점 표기법의 경우, 마지막 점 앞에 명시된 객체가 곧 this입니다.
함수로서 호출할 때 그 함수 내부에서의 this
함수 내부에서의 this
어떤 함수를 함수로서 호출할 경우 this가 지정되지 않습니다. 함수로서 호출하는 것은 호출 주체(객체지향 언어에서의 객체)를 명시하지 않고 개발자가 코드에 직접 관여해 실행한 것이기 때문입니다.
this가 지정되지 않은 경우, this는 전역 객체를 가리키게 되기에 함수에서의 this는 전역 객체를 가리키게 됩니다. (명백한 설계 상의 오류라고 볼 수 있습니다.)
메서드의 내부 함수에서의 this
내부 함수에서의 this
var obj1 = {
outer: function () {
console.log(this); // (1)
var innerFunc = function () {
console.log(this); // (2) (3)
};
innerFunc();
var obj2 = {
innerMethod: innerFunc,
};
obj2.innerMethod();
},
};
obj1.outer();
위 코드 실행시 (1): obj1, (2): 전역객체(Window), (3): obj2가 출력됩니다.
7번째 줄에서는 outer 메서드 내부에 있는 함수(innerFunc)를 함수로서 호출하는 반면, 12번째 줄에 있는 같은 함수를 메서드로서 호출했습니다. 이처럼 같은 함수여도 바인딩되는 this의 대상이 달라질 수 있습니다.
즉, this 바인딩은 함수를 실행하는 당시의 주변 환경(메서드 내부인지, 함수 내부인지)는 중요하지 않고, 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지의 여부가 관건입니다.
메서드 내부 함수에서의 this를 우회하는 방법
(1) 변수 활용
var obj1 = {
outer: function () {
console.log(this); // (1) {outer: f}
var innerFunc = function () {
console.log(this); // (2) Window {...}
};
innerFunc();
var self = this;
var innerFunc2 = function () {
console.log(self); // (3) { outer: f }
};
innerFunc2();
},
};
obj1.outer();
메소드 내부 함수에서의 this를 우회하는 첫번째 방법은 변수를 활용하는 것입니다. 상위 스코프의 this를 변수에 저장해 내부 함수에서 활용할 수 있습니다.
(2) this를 바인딩하지 않는 함수
var obj1 = {
outer: function () {
console.log(this); // (1) {outer: f}
var innerFunc = () => {
console.log(this); // (1) {outer: f}
};
innerFunc();
},
};
obj1.outer();
ES6에서는 this를 바인딩하지 않는 화살표 함수를 도입했습니다. 화살표 함수는 실행 컨텍스트 생성시, this 바인딩 과정 자체가 빠지게 되어, this에 접근시 스코프체인상 가장 가까운 this인 상위 스코프의 this를 그대로 활용할 수 있습니다.
콜백 함수 호출 시 그 함수 내부에서의 this
setTimeout(function () {
console.log(this);
}, 300) // (1)
[(1, 2, 3, 4, 5)].forEach(function (x) {
console.log(this, x); // (2)
});
document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector("#a").addEventListener("click", function (e) {
console.log(this, e); // (3)
});
콜백 함수도 기본적으로 함수기에 this는 전역 객체를 참조하지만 제어권을 받은 함수에서 콜백함수에 별도로 this가 될 대상을 지정하면, 그 대상을 참조합니다.
setTimeout, forEach 함수의 경우, 콜백 함수 호출시 대상이 될 this를 지정하기 않기에, 결과적으로 this는 전역 객체가 됩니다. 하지만 addEventListener 함수의 경우 콜백 함수 호출시 자신의 this를 상속하도록 정의되어 있기에, 메서드 명의 점(.) 앞부분이 this가 됩니다.
이처럼 콜백 함수의 제어권을 가지는 함수(메서드)가 콜백 함수에서의 this를 무엇으로 할지를 결정하며, 특별히 정의하지 않은 경우에는 기본적으로 함수와 마찬가지로 전역객체를 바라봅니다.
생성자 함수 내부에서의 this
생성자 함수는 어떤 공통된 성질을 지니는 객체들을 생성하는 데 사용하는 함수입니다.
JS에서는 함수에 생성자로서의 역할을 함께 부여했습니다. 생성자 함수를 new 명령어와 함께 호출하면 우선 생성자의 prototype 프로퍼티를 참조하는 __proto__라는 프로퍼티가 있는 객체(인스턴스)를 만들고, 미리 준비된 공통 속성 및 개성을 해당 객체(this)에 부여합니다.
3-2. 명시적으로 this를 바인딩하는 방법
call 메서드
Function.prototype.call(thisarg [, arg1[, arg2[, ...]]])
call 메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령입니다. 이때 call 메서드의 첫 번째 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 합니다.
apply 메서드
Function.prototype.apply(thisarg [, argsArray])
apply 메서드는 call 메서드와 기능적으로 완전히 동일하지만, 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 점에서만 차이가 있습니다.
call / apply 메서드의 활용
유사배열객체에 배열 메서드를 적용
var obj = {
0: "a",
1: "b",
2: "c",
length: 3,
};
Array.prototype.push.call(obj, "d");
console.log(obj); // { '0': 'a', '1': 'b', '2': 'c', '3': 'd', length: 4 }
var arr = Array.prototype.slice.call(obj); // [ 'a', 'b', 'c', 'd' ]
console.log(arr);
객체에는 배열 메서드를 직접 적용할 수 없지만, 키가 0 또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티의 값이 0 또는 양의 정수인 객체, 즉 배열의 구조와 유사한 객체의 경우(유사배열객체) call/apply 메서드를 이용해 배열 메서드를 차용할 수 있습니다.
그밖에도, 배열처럼 인덱스와 length 프로퍼티를 지니는 문자열에 대해서도 마찬가지입니다. 단, 문자열의 경우 length 프로퍼티가 읽기 전용이기에 원본 문자열에 변경을 가하는 메서드(push, pop, shift 등)는 에러를 던지며, concat처럼 대상이 반드시 배열이어야 하는 경우 제대로 된 결과를 얻을 수 없습니다.
사실 call/apply를 이용해 형변환하는 것은 'this를 원하는 값으로 지정해서 호출한다'라는 본래의 메서드의 의도와는 다소 동떨어진 활용법입니다. slice 메서드는 단지 배열 형태로 복사하기 위해 차용됐기 때문입니다. 이에 ES6에서는 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 Array.from 메서드를 새로 도입했습니다.
bind 메서드
Function.prototype.bind(thisarg [, arg1[, arg2[, ...]]])
ES5에서 추가된 기능으로, call 메서드와 비슷하지만 즉시 호출하지 않으며, this 및 인수들을 바탕으로 새로운 함수를 반환하기만 합니다.
즉, (1) 함수에 this를 미리 적용, (2) 부분 함수를 구현하는 2가지의 목적을 가지고 있습니다.
(부분 함수 : n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가, 나중에 (n-m)개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수)
var func = function (a, b, c, d) {
console.log(this, a, b, c, d);
};
func(1, 2, 3, 4); // Window{...} 1 2 3 4
var bindFunc1 = func.bind({ x: 1 });
bindFunc1(5, 6, 7, 8); // {x: 1} 5 6 7 8
var bindFunc2 = func.bind({ x: 1 }, 4, 5);
bindFunc2(6, 7); // {x: 1} 4 5 6 7
bindFunc2(8, 9); // {x: 1} 4 5 8 9
name 프로퍼티
var func = function (a, b, c, d) {
console.log(this, a, b, c, d);
};
var bindFunc = func.bind({ x: 1 }, 4, 5);
console.log(func.name); // func
console.log(bindFunc.name); // bound func
bind 메서드 적용시, 새로 만든 함수의 name 프로퍼티에 bind의 수동태인 bound라는 접두어가 붙게 됩니다.
함수명이 x인 원본 함수에 bind 메소드를 적용한 새로운 함수라는 의미로, call/apply보다 추적하기 쉽습니다.
상위 컨텍스트의 this를 내부함수에 전달
var obj1 = {
outer: function () {
console.log(this);
var innerFunc = function () {
console.log(this);
};
innerFunc.call(this);
},
};
obj1.outer();
var obj1 = {
outer: function () {
console.log(this);
var innerFunc = function () {
console.log(this);
}.bind(this);
innerFunc();
},
};
obj1.outer();
상위 컨텍스트의 this를 콜백함수에 전달
var obj = {
logThis: function () {
console.log(this);
},
logThisLater1: function () {
setTimeout(this.logThis, 500);
},
logThisLater2: function () {
setTimeout(this.logThis.bind(this), 500);
},
};
obj.logThisLater1(); // Window {...}
obj.logThisLater2(); // obj {logThis: f..}
별도의 인자로 this를 받는 경우(콜백 함수 내에서의 this)
var report = {
sum: 0,
count: 0,
add: function () {
var args = Array.prototype.slice.call(arguments);
args.forEach(function (entry) {
this.sum += entry;
++this.count;
}, this);
},
average: function () {
return this.sum / this.count;
},
};
report.add(60, 85, 95);
console.log(report.sum, report.count, report.average()); // 240 3 80
// 콜백 함수와 함께 thisArg를 인자로 받는 메서드
Array.prototype.forEach(callback[, thisArg])
Array.prototype.map(callback[, thisArg])
Array.prototype.filter(callback[, thisArg])
Array.prototype.some(callback[, thisArg])
Array.prototype.every(callback[, thisArg])
Array.prototype.find(callback[, thisArg])
콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체(thisArg)를 인자로 지정할 수 있습니다.
이러한 메서드의 thisArg 값을 지정하면 콜백 함수 내부에서 this 값을 원하는 대로 변경할 수 있습니다. 이런 형태는 배열 메서드에 많이 포진돼 있으며, ES6에서 등장한 Set, Map 등의 메서드에도 일부 존재합니다.
3-3. 정리
명시적 this 바인딩이 없을 때 성립하는 규칙
- 전역 공간에서의 this는 전역 객체(브라우저에서는 Window, Node.js에서는 global)를 참조
- 어떤 함수를 메소드로서 호출한 경우, this는 메서드 호출 주체(메서드명 앞의 객체)를 참조
- 어떤 함수를 함수로서 호출한 경우, this는 전역 객체를 참조하며 메서드의 내부함수에서도 동일
- 콜백 함수 내부에서의 this는 해당 콜백 함수의 제어권을 넘겨 받은 함수가 정의한 바에 따르며, 정의하지 않은 경우 전역객체를 참조
- 생성자 함수에서의 this는 생성될 인스턴스를 참조
명시적 this 바인딩
- call, apply 메서드는 this를 명시적으로 지정하면서 함수 또는 메서드를 호출
- bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해 새로운 함수 생성
- 요소를 순회하며 콜백 함수를 반복 호출하는 내용의 일부 메서드는 별도의 인자로 this를 받기도 함
Quiz
this란 무엇이며 this가 결정되는 시점은 언제인가요?
this란 자신이 속한 객체를 의미하며 보통 this는 함수를 호출하는 시점에 결정되지만 화살표 함수에서는 this가 lexical this로 결정되며 일부 상황에서는 call, apply, bind 등의 메소드로 명시적으로 this를 지정할 수 있습니다.
화살표 함수에서 this가 어떻게 결정되며, call, apply, bind 메소드를 이용해 this를 명시적으로 지정하려면 어떻게 해야할까요?
화살표 함수는 this가 어디에도 묶이지 않으며, 상위 스코프에서의 this 값을 그대로 사용하기 때문에 this가 어디에서 호출되어도 상관 없습니다. 그리고 call, apply, bind 메서드를 이용해 this를 변경하려 해도 무시됩니다. 다만, bind 메서드를 사용하면 완전히 새로운 함수를 생성한 후 첫번째 인자로 전달한 객체를 이용하여 새로운 함수 내부의 this가 변경됩니다.
위 규칙 중 다수가 적용되면 어떤 것이 우선순위가 될까요?
- new 바인딩 - 생성자 함수가 (미래에) 생성할 인스턴스
- 명시적 바인딩(apply, call, bind) - 해당 메서드에 첫번째 인수로 전달한 객체
- 암시적 바인딩 - 메서드를 호출한 객체
- 기본 바인딩 - 전역 객체
정답은 1 - 2 - 3 - 4 입니다.
관련 글
'독서' 카테고리의 다른 글
[리뷰] 도커 교과서 - 멀티 스테이지 빌드 (1) | 2023.08.07 |
---|---|
[리뷰] 도커 교과서 - 컨테이너, 도커 이미지 (0) | 2023.08.03 |
[리뷰] 코어 자바스크립트 4장 - 콜백 함수 (0) | 2023.06.05 |
[리뷰] 코어 자바스크립트 2장 - 실행 컨텍스트 (0) | 2023.06.03 |
[리뷰] 코어 자바스크립트 1장 - 데이터 타입 (1) | 2023.05.30 |