전역 공간에서 this
배경지식으로, 자바스크립트의 모든 변수는 특정 객체(실행컨텍스트의 LexicalEnvironment)의 프로퍼티로서 동작합니다. 실행컨텍스트는 변수를 수집해 LE의 프로퍼티로 저장합니다. 이후 어떤 변수를 호출하면 (스코프 체인에서 이를 검색) LE를 조회해서 일치하는 프로퍼티가 있을 경우 그 값을 반환합니다.
처음부터 전역 객체의 프로퍼티로 할당한 경우, 삭제가 가능합니다. 하지만 전역변수로 선언한 경우 삭제가 되지 않습니다. 즉, 전역변수로 선언하면 자바스크립트 엔진이 이를 자동으로 전역 객체의 프로퍼티로 할당하면서 추가적으로, 해당 프로퍼티의 configurable 속성(변경 및 삭제 가능성)을 false로 정의합니다.
메서드를 호출할 때 메서드 내부에서 this
'메서드' 용어 정의
메서드를 '객체의 프로퍼티에 할당된 함수'로 이해하곤 합니다. 반은 맞고 반은 틀립니다. 어떤 함수를 객체의 프로퍼티에 할당한다고 해서 그 자체로서 무조건 메서드가 되는 것이 아니라 객체의 메서드로서 호출할 경우에만 메서드로 동작하고, 그렇지 않으면 함수로 동작합니다(76p)
var func = function (x) {
console.log(this, x);
};
func(1);
// 결과
// Window {window: …} 1
var obj = {
method: func
};
obj.method(2);
// 결과
// {method: ƒ} 2
점 표기법이나 대괄호 표기법으로 함수를 호출할 때 그 함수 이름 앞에 객체가 명시돼 있는 경우, 메서드로 호출한 것입니다. 그 이외에는 함수로 호출한 것입니다.
메서드 내부에서 this
- this에는 호출한 주체에 대한 정보가 담깁니다. 어떤 함수를 메서드로서 호출하는 경우 호출 주체는 함수(객체의 프로퍼티)명 앞의 객체입니다.
함수를 호출할 때 그 함수 내부에서 this
함수 내부에서 this
어떤 함수를 함수로서 호출할 경우, this가 지정되지 않습니다. 함수로서 호출하는 것은 호출 주체를 명시하지 않고 개발자가 코드에 직접 관여해 실행한 것이기 때문에 호출 주체의 정보를 알 수가 없습니다. 따라서 함수에서의 this는 전역 객체를 가리킵니다.
더글라스 크락포트는 이를 명백한 설계상의 오류라고 지적하는데 그 이유는 다음과 같습니다.
var obj1 = {
outer: function () {
console.log(this === obj1); // (1)
var innerFunc = function () {
console.log(this === obj2);
};
innerFunc(); // (2)
// 메서드 안에 있어도, 함수로서 호출할 때 this는 global
var obj2 = {
innerMethod: innerFunc,
};
obj2.innerMethod(); // (3)
},
};
obj1.outer();
// 결과
// (1) -> true
// (2) -> false
// (3) -> true
// 첫번째 console.log -> method이고 이름은 outer
// 세번째 console.log -> 전역 객체
// 세번째 console.log -> method이고 이름은 innerMethod
this 바인딩에 관해 함수를 실행하는 당시 주변 환경(메서드 내부인지 함수 내부인지 등)은 중요하지 않고, 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 여부가 관건입니다.
메서드의 내부 함수에서 this를 우회하는 방법 (ES5까지)
var obj = {
outer: function () {
console.log(this); // (1)
var innerFunc1 = function () {
console.log(this); // (2)
};
innerFunc1();
var self = this;
var innerFunc2 = function () {
console.log(self); // (3)
};
innerFunc2();
},
};
obj.outer();
// 결과
// (1) -> obj
// (2) -> global
// (3) -> obj
this를 바인딩하지 않는 함수 (ES6)
ES6에서는 함수 내부에서 this가 전역 객체를 바라보는 문제를 보완하고자, this를 바인딩하지 않는 화살표 함수를 새로 도입했습니다.
화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 생략되어, 상위 스코프의 this를 그대로 활용할 수 있습니다.
var obj = {
outer: function () {
console.log(this); // (1)
var innerFunc = () => {
console.log(this); // (2)
};
innerFunc();
},
};
obj.outer();
// 결과
// (1) -> obj
// (2) -> obj
콜백 함수 호출 시 그 함수 내부에서 this
콜백 함수는 자신(함수)의 제어권을 다른 함수(또는 메서드)에게 넘겨주는 함수를 말합니다. 이 때 콜백 함수는 '다른 함수'의 내부 로직에 따라 실행되며, this 역시 '다른 함수' 내부 로직에서 정한 규칙에 따라 값이 결정됩니다.
콜백 함수도 함수이기 때문에, 기본적으로 this가 전역 객체를 참조하지만, 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우, 그 대상을 참조합니다.
setTimeout(function () {
console.log(this);
}, 300); // (1) -> 0.3초 뒤 전역 객체 출력
[1, 2, 3, 4, 5].forEach(function (x) {
// (2) -> 전역 객체와 배열의 각 요소가 총 5회 출력
console.log(this, x);
});
document.body.innerHTML += "<button id='a'>클릭</button>";
document.body.querySelector("#a").addEventListener("click", function (event) {
// (3) -> 버튼 엘리먼트와 이벤트 객체가 출력
console.log(this, event);
});
addEventListener 메서드는 콜백 함수를 호출할 때 자신의 this를 상속하도록 정의되어 있습니다. 이 때 자신의 this는 점 표기법 앞의 엘리먼트입니다.
'콜백 함수에서 this는 무조건 무엇이다' 라고 정의할 수 없습니다. 콜백 함수의 제어권을 가지는 함수(메서드)가 콜백 함수의 this가 무엇이 될지 결정합니다. 특별한 경우를 제외하고, 기본적으로 함수와 마찬가지로 전역 객체를 가리킵니다.
생성자 함수 내부에서 this
생성자 함수는 어떤 공통된 성질을 지니는 객체들을 생성하는 데 사용하는 함수입니다.
프로그래밍적으로 '생성자'는 구체적인 인스턴스를 만들기 위한 일종의 틀입니다. 이 틀에는 해당 클래스의 공통 속성들이 미리 준비되어 있고, 여기에 구체적인 인스턴스의 개성을 더해 개별 인스턴스를 만들 수 습니다.
자바스크립트 함수는 생성자 역할도 할 수 있습니다. new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작합니다. 그리고 어떤 함수가 생성자 함수로서 호출된 경우 내부에서 this는 곧 새로 만들 구체적인 인스턴스 자신이 됩니다. 인스턴스가 만들어지는 과정은 다음과 같습니다.
- 생성자 함수를 호출(new 명령어와 함께 함수를 호출)
- 생성자의
prototype
프로퍼티를 참조하는__proto__
라는 프로퍼티가 있는 객체(인스턴스)를 만들고, 미리 준비된 공통 속성 및 개성을 해당 객체(this)에 부여합니다. - 인스턴스 완성!
var Cat = function (name, age) {
// new 명령어와 함께 Cat 함수가 호출된 경우,
// this는 새로 만들어질 인스턴스 그 자신이 된다.
this.bark = "Meow";
this.name = name;
this.age = age;
};
var choco = new Cat("초코", 7);
var nabi = new Cat("나비", 5);
console.log(choco);
console.log(nabi);
/* 실행 결과
Cat { bark: 'Meow', name: '초코', age: 7 }
Cat { bark: 'Meow', name: '나비', age: 5 }
*/
명시적으로 this를 바인딩하는 방법
앞 절의 규칙에 부합하지 않게 this가 바인딩됐다면, 명시적으로 this를 바인딩한 것이라 추측할 수 있습니다.
call 메서드
call() 메소드는 주어진 this 값 및 각각 전달된 인수와 함께 함수를 호출합니다.
call 메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령입니다. 이때 call 메서드의 첫 번째 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 합니다. 함수를 그냥 실행하면 this는 전역 객체를 참조하지만 call 메서드를 이용하면 임의의 객체를 this로 지정할 수 있습니다.
var func = function (a, b, c) {
console.log(this, a, b, c);
};
func(1, 2, 3); // (1)
func.call({ x: 1 }, 4, 5, 6); // (2)
/* 출력 결과
(1) -> Window{...} 1 2 3
(2) -> { x: 1 } 4 5 6
*/
apply 메서드
apply() 메서드는 주어진 this 값과 배열 (또는 유사 배열 객체) 로 제공되는 arguments 로 함수를 호출합니다.
apply 메서드는 call 메서드와 기능 면에서 완전히 동일합니다. call 메서드는 첫 번째 인자를 제외한 나머지 모든 인자들을 호출할 함수의 매개변수로 지정합니다. 하지만 apply 메서드는 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 것이 차이점입니다.
call, apply 메서드의 활용
유사배열객체(array-like object)에 배열 메서드 적용
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);
console.log(arr); // [ 'a', 'b', 'c', 'd' ]
객체에는 배열 메서드를 직접 적용할 수 없습니다. 하지만 키가 (배열의 인덱스처럼) 0또는 양의 정수인 프로퍼티가 존재하고 length(마치 배열의 길이) 프로퍼티 값이 0 또는 양의 정수인 객체일 때 call, apply 메서드를 적용할 수 있습니다. 다시 말해, 배열의 구조와 유사한 객체(유사 배열 객체)의 경우, call 또는 apply 메서드를 이용해 배열 메서드를 차용할 수 있습니다.
call/apply를 이용해 형변환하는 것은 'this를 원하는 값으로 지정해서 호출한다'라는 본래의 메서드의 의도와는 다소 동떨어진 활용법이라 할 수 있습니다. slice 메서드는 오직 배열 형태로 '복사'하기 위해 차용됐을 뿐이니, 보통 사람들은 코드만 봐서는 어떤 의도인지 파악하기 어렵습니다. 이에 ES6에서는 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 Array.from 메서드를 새로 도입했습니다.
var obj = {
0: "a",
1: "b",
2: "c",
length: 3,
};
var arr = Array.from(obj);
console.log(arr);
생성자 내부에서 다른 생성자 호출
생성자 내부에 다른 생성자와 공통된 내용이 있을 경우 call 또는 apply를 이용해 다른 생성자를 호출하면 간단히 반복을 줄일 수 있습니다.
다음 예제에서 Student, Employee 생성자 함수 내부에서 Person 생성자 함수를 호출해 인스턴스의 속성을 정의하도록 구현했습니다.
function Person(name, gender) {
this.name = name;
this.gender = gender;
}
function Student(name, gender, school) {
Person.call(this, name, gender);
this.school = school;
}
function Employee(name, gender, company) {
Person.apply(this, [name, gender]);
this.company = company;
}
var by = new Student("보영", "female", "단국대");
var jn = new Employee("재난", "male", "구골");
여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 apply 활용
예를 들어, 배열에서 최대, 최솟값을 구해야 할 경우 apply
를 사용하지 않는다면 모든 배열의 요소를 조회하여 값을 비교해야 합니다. 하지만 Math.max
와 Math.min
메서드에 apply
를 적용하면 코드가 훨씬 간결해집니다.
var numbers = [10, 20, 3, 16, 45];
var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);
console.log(max, min);
// 출력 결과 -> 45 3
ES6에서 펼치기 연산자를 활용하면 apply
를 사용하지 않고 더 간편하게 작성할 수 있습니다.
const numbers = [10, 20, 3, 16, 45];
const max = Math.max(...numbers);
const min = Math.min(...numbers);
console.log(max, min);
// 출력 결과 -> 45 3
bind 메서드
bind() 메소드가 호출되면 새로운 함수를 생성합니다. 받게되는 첫 인자의 value로는 this 키워드를 설정하고, 이어지는 인자들은 바인드된 함수의 인수에 제공됩니다.
bind() 함수는 새로운 바인딩한 함수를 만듭니다. 바인딩한 함수는 원본 함수 객체를 감싸는 함수로, ECMAScript 2015에서 말하는 특이 함수 객체exotic function object입니다. 바인딩한 함수를 호출하면 일반적으로 래핑된 함수가 호출 됩니다.
- bind 메서드는 ES5에서 추가된 기능입니다. call과 비슷하지만 즉시 호출하지는 않고 넘겨 받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드입니다. 다시 새로운 함수를 호출할 때 인수를 넘기면 그 인수들은 기존 bind 메서드를 함수에 this를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 가집니다.
var func = function (a, b, c, d) {
console.log(this, a, b, c, d);
};
func(1, 2, 3, 4); // (1)
var bindFunc1 = func.bind({ x: 1 });
bindFunc1(5, 6, 7, 8); // (2)
var bindFunc2 = func.bind({ x: 1 }, 4, 5);
bindFunc2(6, 7); // (3)
bindFunc2(8, 9); // (4)
/* 출력 결과
(1) -> Window{...} 1 2 3 4
(2) -> { x: 1 } 5 6 7 8
(3) -> { x: 1 } 4 5 6 7
(4) -> { x: 1 } 4 5 8 9
*매개변수가 주어지지 않으면 undefined가 출력된다.
*/
name 프로퍼티
bind 메서드를 적용해 새로 만든 함수는 특이하게도 name 프로퍼티에 동사 bind의 수동태인 'bound'라는 접두어가 붙습니다. 따라서 함수의 name 프로퍼티 앞에 bound가 붙어있다면, 이는 원본 함수에 bind 메서드를 적용해 생성된 새로운 함수라는 것을 알 수 있습니다.
call, apply 또는 bind 메서드를 이용해 상위 컨텍스트의 this를 내부 함수나 콜백 함수에 전달하기
var obj1 = {
outer: function () {
console.log("1", this);
var innerFunc = function () {
console.log(this);
};
innerFunc.call(this);
},
};
obj1.outer();
var obj2 = {
outer: function () {
console.log("2", this);
var innerFunc = function () {
console.log(this);
}.bind(this);
innerFunc();
},
};
obj2.outer();
var obj3 = {
logThis: function () {
console.log(this);
},
logThisLater1: function () {
setTimeout(this.logThis, 500);
},
logThisLater2: function () {
setTimeout(this.logThis.bind(this), 1000);
},
};
obj3.logThisLater1();
obj3.logThisLater2();
/* 출력 결과
1 { outer: [Function: outer] }
{ outer: [Function: outer] }
2 { outer: [Function: outer] }
{ outer: [Function: outer] }
Timeout { ... } // ***책에서는 Window{...}가 출력된다고 쓰여있다.
{
logThis: [Function: logThis],
logThisLater1: [Function: logThisLater1],
logThisLater2: [Function: logThisLater2]
} // === obj3
*/
화살표 함수의 예외 사항
ES6에 새로 도입된 화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 생략됐습니다. 즉 이 함수 내부에 this 자체가 없으며, 접근하고자 하면 스코프체인상 가장 가까운 this에 접근하게 됩니다.
별도의 인자로 this를 받는 경우(콜백 함수 내에서 this)
콜백 함수를 인자로 받는 메서드 중 일부에서, 추가로 this로 지정할 객체(thisArg)를 인자로 지정할 수 있는 경우가 있습니다. 이러한 메서드의 thisArg 값을 지정하면 콜백 함수 내부에서 this 값을 원하는 대로 변경할 수 있습니다. 이런 형태는 여러 내부 요소에 같은 동작을 반복 수행해야 하는 배열 메서드에 많이 있습니다. 같은 이유로 ES6에서 새로 등장한 Set, Map 등의 메서드의 일부에도 존재합니다.
그 중 대표적인 배열 메서드인 forEach의 예제를 살펴보겠습니다.
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);
// forEach의 콜백 함수의 this는 이 두 번째 인자인 this가 바인딩된다.
// 이 this는 add 메서드의 this로, report 객체를 가리킨다.
},
average: function () {
return this.sum / this.count;
},
};
report.add(60, 85, 95);
console.log(report.sum, report.count, report.average());
// 출력 결과 -> 240 3 80
정리
명시적 this 바인딩이 없는 한 늘 성립하는 this 바인딩 규칙
- 전역공간에서의 this는 전역 객체(브라우저에서는 window, node.js에서는 global)를 참조합니다.
- 어떤 함수를 메서드로 호출한 경우 this는 메서드 호출 주체(메서드 이름 바로 앞 객체)를 참조합니다.
- 어떤 함수를 함수로서 호출한 경우 this는 전역 객체를 참조합니다. 메서드의 내부 함수라 할지라도 같은 원리를 따릅니다.
- 콜백 함수 내부에서 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 this 바인딩 규칙을 정의한 바에 따릅니다. 따로 정의하지 않은 경우는 전역 객체를 참조합니다.
- 생성자 함수에서 this는 앞으로 생성될 인스턴스를 참조합니다.
명시적 this 바인딩
- call, apply 메서드는 this를 명시적으로 지정하며 함수 또는 메서드를 호출합니다.
- bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만듭니다.
- 요소를 순회하며 콜백 함수를 반복 호출하는 내용의 일부 메서드는 별도의 인자로 this를 받는 경우도 있습니다.
참고 및 출처
- 책 <코어자바스크립트> 3장 this
- https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/delete
- https://poiemaweb.com/es6-block-scope
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#properties
- https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/call
- https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/apply
- https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
'프로그래밍-학습기록 > Javascript' 카테고리의 다른 글
클로저는 글로 적기 글렀어. (0) | 2021.02.15 |
---|---|
자바스크립트 콜백 함수 이해하기 (0) | 2021.02.13 |
자바스크립트에서 실행 컨텍스트(execution context)가 도대체 뭐하는 애에요 (0) | 2021.02.10 |
자바스크립트 데이터 타입(기본형, 참조형) 이해하기 (0) | 2021.02.09 |
javascript 논리연산자(&&, ||) 우선순위 vs. 단락 평가(short circuit) (0) | 2021.01.29 |