[책요약] 코어 자바스크립트 3장 this
2021-02-03
3장 this
- 대부분의 객체지향 언어에서 this
- 클래스로 생성한 인스턴스 객체
- 혼란의 여지가 없음
- 자바스크립트의 this
- 어디서든 사용할 수 있음
- 정확한 작동 방식을 이해하지 못하면 문제 해결에 어려움
3.1 상황에 따라 달라지는 this
- 기본적으로 실행 컨텍스트가 생성될 때 함께 결정됨
- 실행 컨텍스트는 함수를 호출할 때 생성
- 함수를 호출할 때 결정
3.1.1 전역 공간에서의 this
- 전역 공간에서의 this는 전역 객체
- 브라우저에서는 window, node.js에서는 global
console.log(this);
console.log(global);
console.log(this === global);
- 전역 공간에서만 발생하는 특이한 성질 하나가 있다
- 전역변수를 선언하면 자바스크립트 엔진은 이를 전역 객체의 프로퍼티로도 할당
var a = 1;
console.log(a);
console.log(global.a);
console.log(this.a);
- 자바스크립트의 모든 변수는 실은 특정 객체의 프로퍼티로서 동작
- a를 직접 호출할 때에 1이 나오는 까닭은?
- 스코프 체인에서 a를 검색하다가 가장 마지막에 도달하는 전역 스코프(전역 객체)에서 해당 프로퍼티 a를 발견
- 단순히 (global. or window.)이 생략된 것이라고 여겨도 무방
- 전역 공간에서는 var로 선언하는 대신 window의 프로퍼티에 직접 할당하더라도 똑같이 동작할 것일까??
- 대부분의 경우에는 이 말이 맞다
var a = 1;
global.b = 2;
console.log(a, global.a, this.a);
console.log(b, global.b, this.b);
global.a = 3;
b = 4;
console.log(a, global.a, this.a);
console.log(b, global.b, this.b);
- 전역변수 선언과 전역객체의 프로퍼티 할당 사이에 전혀 다른 경우도 있다.
- ‘삭제’ 명령
var a = 1;
delete global.a;
console.log(a, global.a, this.a);
var b = 2;
delete b;
console.log(b, global.b, this.b);
global.c = 3;
delete global.c;
console.log(c, global.c, this.c);
global.d = 4;
delete d;
console.log(d, global.d, this.d);
- 처음부터 전역객체의 프로퍼티로 할당한 경우에는 삭제가 되지만 전역변수로 선언한 경우에는 삭제가 되지 않는다.
- 전역변수를 선언하면 자바스크립트 엔진이 이를 자동으로 전역객체의 프로퍼티로 할당하면서 추가적으로 해당 프로퍼티의 configurable 속성(변경 및 삭제 가능성)을 false로 정의
3.1.2 메서드로서 호출할 때 그 메서드 내부에서의 this
함수 vs 메서드
- 어떤 함수를 실행하는 방법
- 함수로서 호출
- 메서드로서 호출
- 둘을 구분하는 유일한 차이
- 독립성
- 함수는 그 자체로 독립적인 기능을 수행
- 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행
- 자바스크립트는 상황별로 this 키워드에 다른 값을 부여하게 함으로써 구현
- 자바스크립트 초심자분들은 흔히 메서드를 ‘~객체의 프로퍼티에 할당된 함수~’로 이해 (▵)
- 그 자체로서 무조건 메서드가 되는 것이 아니라 객체의 메서드로서 호출한 경우에만 메서드로 동작
var func = function (x) {
console.log(this, x);
};
func(1);
var obj = {
method: func,
};
obj.method(2);
- ‘함수로서 호출’, ‘메서드로서 호출’ 구분
- 함수 앞에 점(.)이 있으면 메서드로서 호출
- (대괄호 표기법에 따른 경우에도 메서드로서 호출)
var obj = {
method: function (x) {
console.log(this, x);
},
};
obj.method(1);
obj["method"](2);
메서드 내부에서의 this
- this에는 호출한 주체에 대한 정보가 담김
- 어떤 함수를 메서드로서 호출하는 경우
- 호출 주체는 함수명(프로퍼티명) 앞의 객체
- 점 표기법의 경우 마지막 점 앞에 명시된 객체가 곧 this
var obj = {
methodA: function () {
console.log(this);
},
inner: {
methodB: function () {
console.log(this);
},
},
};
obj.methodA();
obj["methodA"]();
obj.inner.methodB();
obj.inner["methodB"]();
obj["inner"].methodB();
obj["inner"]["methodB"]();
3.1.3 함수로서 호출할 때 그 함수 내부에서의 this
함수 내부에서의 this
- 어떤 함수를 함수로서 호출할 경우에는 this가 지정되지 않음
- 실행 컨텍스트를 활성화할 당시에 this가 지정되지 않은 경우 this는 전역 객체를 바라봄
- 함수에서의 this는 전역 객체
매서드의 내부함수에서의 this
- 메서드 내부에서 정의하고 실행한 함수에서의 this는 초심자들이 가장 혼란을 느끼는 지점 중 하나
- 실제 동작과 다르게 예측
var obj1 = {
outer: function () {
console.log(this);
var innerFunc = function () {
console.log(this);
};
innerFunc();
var obj2 = {
innerMethod: innerFunc,
};
obj2.innerMethod();
},
};
obj1.outer();
- this 바인딩에 관해서는 함수를 실행하는 당시의 주변 환경(메서드 내부인지, 함수 내부인지 등)은 중요하지 않고
- 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 없는지가 관건
메서드의 내부 함수에서의 this를 우회하는 방법
- 호출 주체가 없을 때는 자동으로 전역객체를 바인딩하지 않고 호출 당시 주변 환경의 this를 그대로 상속받아 사용할 수 있다면 좋겠다
- 훨씬 자연스럽고 스코프체인과의 일관성을 지키는 설득력 있는 방식
- ES5까지는 자체적으로 내부함수에 this를 상속할 방법이 없었다.
- 변수를 활용해서 우회
var obj = {
outer: function () {
console.log(this);
var innerFunc1 = function () {
console.log(this);
};
innerFunc1();
var self = this;
var innerFunc2 = function () {
console.log(self);
};
innerFunc2();
},
};
obj.outer();
this를 바인딩하지 않는 함수
- this를 바인딩하지 않는 화살표 함수arrow function를 새로 도입
- 상위 스코프의 this를 그대로 활용
var obj = {
outer: function () {
console.log(this);
var innerFunc = () => {
console.log(this);
};
innerFunc();
},
};
obj.outer();
- 그 밖에 call, apply 등 메서드 활용해 함수를 호출할 때 명시적으로 this를 지정하는 방법(뒤에서 설명)
3.1.4 콜백 함수 호출 시 그 함수 내부에서의 this
- 함수 A의 제어권을 다른 함수(또는 메서드) B에게 넘겨주는 경우 함수 A를 콜백 함수라고 한다.
- 함수 A는 함수 B의 내부 로직에 따라 실행되며,
- this 역시 함수 B 내부로직에서 정한 규칙에 따라 값이 결정
- 콜백 함수도 함수이기 때문에 기본적으로 this는 전역객체 참조
- 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우 그 대상을 참조
setTimeout(function () {
console.log(this);
}, 300);
[1, 2, 3, 4, 5].forEach(function (x) {
console.log(this, x);
});
document.body.innerHTML += '<button id="a">클릭<button>';
document.body.querySelector("#a").addEventListener("click", function (e) {
console.log(this, e);
});
3.1.5 생성자 함수 내부에서의 this
- 생성자 함수
- 어떤 공통된 성질을 지니는 객체들을 생성하는 데 사용하는 함수
- new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작
- 어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신
var Cat = function (name, age) {
this.bark = "야옹";
this.name = name;
this.age = age;
};
var choco = new Cat("초코", 7);
var nabi = new Cat("나비", 5);
console.log(choco, nabi);
3.2 명시적으로 this를 바인딩하는 방법
3.2.1 call 메서드
Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])
- 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령
- 첫 번째 인자를 this로 바인딩
- 그 이후의 인자들, 호출할 함수의 매개변수
var func = function (a, b, c) {
console.log(this, a, b, c);
};
func(1, 2, 3);
func.call({ x: 1 }, 4, 5, 6);
var obj = {
a: 1,
method: function (x, y) {
console.log(this.a, x, y);
},
};
obj.method(2, 3);
obj.method.call({ a: 4 }, 5, 6);
3.2.2 apply 메서드
Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])
- call 메서드와 기능적으로 완전히 동일
- 두 번째 인자를 배열로 받아 매개변수로 지정
- call -> comma, apply -> array
var func = function (a, b, c) {
console.log(this, a, b, c);
};
func.apply({ x: 1 }, [4, 5, 6]);
var obj = {
a: 1,
method: function (x, y) {
console.log(this.a, x, y);
},
};
obj.method.apply({ a: 4 }, [5, 6]);
3.2.3 call / apply 메서드의 활용
유사배열객체array-like obejct에 배열 메서드를 적용
var obj = {
0: "a",
1: "b",
2: "c",
length: 3,
};
Array.prototype.push.call(obj, "d");
console.log(obj);
var arr = Array.prototype.slice.call(obj);
console.log(arr);
- 객체에는 배열 메서드를 직접 적용할 수 없다.
- 그러나 키가 0 또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티의 값이 0 또는 양의 정수인 객체
- 배열의 구조와 유사한 객채의 경우(유사배열객체)
- call 또는 apply 메서드를 이용해 배열 메서드를 차용할 수 있다.
function a() {
var argv = Array.prototype.slice.call(arguments);
argv.forEach(function (arg) {
console.log(arg);
});
}
a(1, 2, 3);
document.body.innerHTML = "<div>a</div><div>b</div><div>c</div>";
var nodeList = document.querySelectorAll("div");
var nodeArr = Array.prototype.slice.call(nodeList);
nodeArr.forEach(function (node) {
console.log(node);
});
var str = "abc def";
Array.prototype.push.call(str, ", pushed string");
Array.prototype.concat.call(str, "sring");
Array.prototype.every.call(str, function (char) {
return char !== " ";
});
Array.prototype.some.call(str, function (char) {
return char === " ";
});
var newArr = Array.prototype.map.call(str, function (char) {
return char + "!";
});
console.log(newArr);
var newStr = Array.prototype.reduce.apply(str, [
function (string, char, i) {
return string + char + i;
},
"",
]);
console.log(newStr);
- call/apply를 이용해 형변환하는 것은 본래 메서드 의도와는 다소 동떨어진 활용법
- ES6에서는 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 Array.from 메서드를 새로 도입
var obj = {
0: "a",
1: "b",
2: "c",
length: 3,
};
var arr = Array.from(obj);
console.log(arr);
생성자 내부에서 다른 생성자를 호출
- 생성자 내부에 다른 생성자와 공통된 내용이 있을 경우 call 또는 apply를 이용해 다른 생성자를 호출하면 간단하게 반복을 줄일 수 있다.
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", "구글");
console.log(by);
여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 - apply 활용
var numbers = [10, 20, 3, 16, 45];
var max = (min = numbers[0]);
numbers.forEach(function (number) {
if (number > max) {
max = number;
}
if (number < min) {
min = number;
}
});
console.log(max, min);
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);
var numbers = [10, 20, 3, 16, 45];
var max = Math.max(...numbers);
var min = Math.min(...numbers);
console.log(max, min);
3.2.4 bind 메서드
Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])
- ES5에 추가된 기능
- call과 비슷하지만 즉시 호출하지는 않고, 넘겨받은 this및 인수들을 바탕으로 새로운 함수를 반환
var func = function (a, b, c, d) {
console.log(this, a, b, c, d);
};
func(1, 2, 3, 4);
var bindFunc1 = func.bind({ x: 1 });
bindFunc1(5, 6, 7, 8);
var bindFunc2 = func.bind({ x: 1 }, 4, 5);
bindFunc2(6, 7);
bindFunc2(8, 9);
name 프로퍼티
- bind 메서드를 적용해서 새로 만든 함수는 name프로퍼티에 동사 bind의 수동태인 ‘bound’라는 접두어 붙는다.
- 어떤 함수의 name 프로퍼티가 ‘bound xxx’라면 함수명이 xxx인 원본 함수에 bind를 적용한 새로운 함수라는 의미
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);
console.log(bindFunc.name);
상위 컨텍스트의 this를 내부함수나 콜백 함수에 전달하기
- 3.1.3에서 self 등의 변수를 활용한 우회법을 소개
- call, apply 또는 bind 메서드를 이용해서 깔끔하게 처리
var obj = {
outer: function () {
console.log(this);
var innerFunc = function () {
console.log(this);
};
innerFunc.call(this);
},
};
obj.outer();
var obj = {
outer: function () {
console.log(this);
var innerFunc = function () {
console.log(this);
}.bind(this);
innerFunc();
},
};
obj.outer();
var obj = {
logThis: function () {
console.log(this);
},
logThisLater1: function () {
setTimeout(this.logThis, 500);
},
logThisLater2: function () {
setTimeout(this.logThis.bind(this), 1000);
},
};
obj.logThisLater1();
obj.logThisLater2();
3.2.5 화살표 함수의 예외사항
- 화살표 함수는 실행 컨텍스트 생성시 this를 바인딩하는 과정이 제외
- 스코프체인상 가장 가까운 this에 접근
var obj = {
outer: function () {
console.log(this);
var innerFunc = () => {
console.log(this);
};
innerFunc();
},
};
obj.outer();
3.2.6 별도의 인자로 this를 받는 경우(콜백 함수 내에서의 this)
- 콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체를 인자로 지정할 수 있는 경우가 있다.
- forEach, Set, Map 등
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());
3.3 정리
- 다음 규칙은 명시적 this 바인딩이 없는 한 늘 성립한다
- 전역공간에서의 this는 전역객체(브라우저에서는 window, Node.js에서는 global)를 참조합니다.
- 어떤 함수를 메서드로서 호출한 경우 this는 메서드 호출 주체(메서드명 앞의 객체)를 참조합니다.
- 어떤 함수를 함수로서 호출한 경우 this는 전역객체를 참조합니다. 메서드의 내부함수에서도 같습니다.
- 콜백 함수 내부에서의 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역객체를 참조합니다.
- 생성자 함수에서의 this는 생성될 인스턴스를 참조합니다.
- 명시적 this 바인딩
- call, apply 메서드는 this를 명시적으로 지정하면서 함수 또는 메서드를 호출합니다.
- bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만듭니다.
- 요소를 순회하면서 콜백 함수를 반복 호출하는 내용의 일부 메서드는 별도의 인자로 this를 받기도 합니다.
Comments