Chapter 3. this
3.1 상황에 따라 달라지는 this
대부분의 객체지향 언어에서 this는 클래스로 생성한 인스턴스 객체를 의미하기 때문에 클래스에서만 사용할 수 있다. 그러나 자바스크립트에서는 어디든지 사용할 수 있으며, 상황에 따라 this가 바라보는 대상이 달라진다.
this는 자바스크립트에서 함수와 객체를 구분하는 유일한 기능이다.
this는 기본적으로 실행 컨텍스트가 생성될 때(함수가 호출될 때) 함께 결정된다. 함수가 어떤 방식으로 호출되느냐에 따라 this의 값이 달라진다.
3.1.1 전역 공간에서의 this
전역 공간에서 this는 전역 객체를 가리킨다. 전역 컨텍스트를 생성하는 주체가 전역 객체이기 때문이다.
전역 객체는 자바스크립트 런타임 환경에 따라 다른데 브라우저 환경에서는 window이고 Node.js 환경에서는 global이다.
전역 변수를 생성하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다. 변수이면서 객체의 프로퍼티이기도 한 셈이다. 즉, 아래와 같은 코드가 동작하게 된다.
var a = 1;
console.log(a); // 1
console.log(window.a) // 1
console.log(this.a) // 1
쉽게 말하자면 자바스크립트의 모든 변수는 특정 객체(LexicalEnvironment)의 프로퍼티로서 동작한다. 사용자가 var 연산자를 이용해 변수를 선언하더라도 실제 자바스크립트 엔진은 특정 객체의 프로퍼티로 인식한다. 즉, var 연산자를 사용하지 않고 직접 window 프로퍼티에 할당하는 것도 가능하다. 아래의 코드가 그 예시다.
var a = 1;
window.b = 2;
console.log(a, window.a, this.a); // 1 1 1
console.log(b, window.b, this.b); // 2 2 2
delete window.a; // false
console.log(a, window.a, this.a); // 1 1 1
delete window.b; // true
console.log(b, window.b, this.b); // Uncaught ReferenceError : b is not defined
변수를 var 연산자를 이용하여 전역변수로 선언한 경우에는 delete 명령어가 먹히지 않지만, window 객체의 프로퍼티로 할당한 경우에는 delete가 가능한 것을 확인할 수 있다.
이는 사용자가 의도치 않게 변수를 삭제하는 것을 막기 위해, var 연산자를 사용하여 전역변수를 선언할 경우 자바스크립트 엔진이 이를 전역객체의 프로퍼티로 할당하면서 해당 프로퍼티의 configurable 속성(변경 및 삭제 가능성)을 false로 정의하기 때문이다.
또한 window 객체의 프로퍼티로 할당할 경우에는 호이스팅 역시 적용되지 않는다. 만약 아래의 코드를 실행할 경우, 1과 2를 출력하는 것이 아니라 1을 출력하고 Uncaught ReferenceError : b is not defined를 출력한다. 만약 var a = 1을 console.log(b)의 아래로 바꾸면 값은 1이 아니라 undefined를 출력하게 된다.
var a = 1;
console.log(a); // 1
console.log(b); // Uncaught ReferenceError : b is not defined
window.b = 2; // 호이스팅 되지 않음
3.1.2 메서드로서 호출할 때 그 메서드 내부에서의 this
함수와 메서드를 구분하는 데에는 독립성에 있다. 함수는 그 자체로 독립적인 기능을 수행하는 반면, 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행한다. 자바스크립트는 상황별로 this 키워드에 다른 값을 부여하게 함으로써 이를 구현했다.
var func = function (x) {
console.log(this, x);
};
func(1); // Window { ... } 1
var obj = {
method: func
};
obj.method(2); // { method: f } 2
obj['method'](2); // { method : f } 2
위 코드는 함수로서의 호출과 메서드로서의 호출을 나타낸다. 메서드로 호출할 때에는 앞에 객체가 명시되어 있으며, 점(.) 또는 대괄호[ ]로 표기한다.
그러나 중첩된 메서드의 경우, 호출 방식에 따라 this가 가리키는 것이 달라질 수 있다. 아래는 그 예시 코드다.
var obj = {
methodA : function () { console.log(this); },
inner : {
methodB : function () { console.log(this); }
}
};
obj.methodA(); // { methodA : f, inner: { ... } } ( ===obj )
obj['methodA'](); // { methodA : f, inner: { ... } } ( ===obj )
obj.inner.methodB(); // { methodB : f } ( ===obj.inner )
obj.inner['methodB'](); // { methodB : f } ( ===obj.inner )
obj['inner'].methodB(); // { methodB : f } ( ===obj.inner )
obj['inner']['methodB'](); // { methodB : f } ( ===obj.inner )
3.1.3 함수로서 호출할 때 그 함수 내부에서의 this
함수를 함수로서 호출할 때에는 this가 지정되지 않는다. 따라서 함수에서의 this는 전역 객체를 가리킨다. 즉, 객체로부터 호출되지 않는, 전역이든 함수 내부에서든 작성되어 해당 함수 내부에서 호출된 함수는 무조건 this가 전역 컨텍스트를 가리킨다.
var obj = {
outer : function () {
console.log(this); // (1) { outer : f } obj
var innerFunc = function () {
console.log(this);
};
innerFunc(); // (2) Window { ... } Window
var obj2 = {
innerMethod : innerFunc
};
obj2.innerMethod(); //(3) { innerMethod : f } obj2
}
};
obj.outer();
만약 위 코드같은 상황에서 (2)에 Window가 아닌 obj를 호출하게 하고 싶다면, 코드에 아래와 같이 추가하면 된다. 따로 self라는 변수를 선언한 뒤, 해당 변수에 obj의 this를 저장하고 출력하는 것이다.
var obj = {
outer : function () {
console.log(this); // (1) { outer : f } obj
var innerFunc1 = function () {
console.log(this);
};
innerFunc1(); // (2) Window { ... } Window
var self = this;
var innerFunc2 = function () {
console.log(self); // (3) { outer : f } obj
};
}
};
obj.outer();
ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제를 보완하고자, 화살표 함수(arrow function)를 새로 도입했다. 화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 빠지게 되어, 상위 스코프의 this를 그대로 활용할 수 있다. 이 경위 위와 같이 변수를 이용한 우회법은 필요하지 않게 된다.
▼ 화살표 함수
var obj = {
outer : function () {
console.log(this); // (1) { outer : f } obj
var innerFunc = () => {
console.log(this);
};
innerFunc(); // (2) { outer : f } obj
}
};
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)
[ 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)
});
(1) setTimeout 함수는 300ms 만큼 시간 지연을 한 뒤, 함수를 실행하라는 명령이다. 0.3초 후 전역 객체가 출력된다.
(2) forEach 메서드는 배열의 각 요소를 앞에서부터 차례로 하나씩 꺼내어 그 값을 콜백 함수의 첫 번째 인자로 삼아 함수를 실행하라는 명령이다. 전역 객체와 배열의 각 요소가 총 5회 출력된다.
(3) .addEventListener는 지정한 HTML 엘리먼트에 'click' 이벤트가 발생할 때마다 그 이벤트 정보를 콜백 함수의 첫 번째 인자로 삼아 함수를 실행하라는 명령이다. 버튼을 클릭하면 앞서 지정한 엘리먼트와 클릭 이벤트에 관한 정보가 담긴 객체가 출력된다.
(3)의 경우, (1)과 (2)와는 다르게 앞에 점(.)을 통해 this를 넘겨주었다. 즉, "document.body.querySelector('#a')가 this가 된다.
3.1.5 생성자 함수 내부에서의 this
생성자는 객체지향 언어에서는 생성자를 클래스(class), 클래스를 통해 만든 객체를 인스턴스(instance)라고 부른다. 쉽게 말하면 생성자는 구체적인 인스턴스를 만들기 위한 틀이다. 이 틀에는 해당 클래스의 공통 속성들이 미리 준비돼 있고, 여기에 구체적인 인스턴스의 개성을 더해 개별 인스턴스를 만들 수 있다.
자바스크립트는 함수에 생성자로서의 역할을 함께 부여했다. new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작하게 된다. 그리고 어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 된다. (거의 JAVA에서의 클래스 생성과 동일하다!
생성자 함수를 호출하면 우선 생성자의 prototype 프로퍼티를 참조하는 __proto__라는 프로퍼티가 있는 객체(인스턴스)를 만들고, (Chapter 6) 미리 준비된 공통 속성 및 개성을 해당 객체(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(chocho, nabi);
/* 결과
Cat { bark : '야옹', name : '초코', age : 7 }
Cat { bark : '야옹', name : '나비', age : 5 }
*/
3.2 명시적으로 this를 바인딩하는 방법
3.2.1 call 메서드
Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])
call 메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 방법이다. 이때 call 메서드의 첫 번째 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 한다.
함수를 그냥 실행하면 this는 전역 객체를 참조하지만, call 메서드를 이용하면 임의의 객체를 this로 지정할 수 있다.
위에서는 prototype이 들어갔지만, 왜 들어갔는지는 잘 모르겠고... 사용 예시는 아래와 같다.
▼ 함수를 call 메서드로 호출하기
var func = function (a, b, c) {
console.log(this, a, b, c);
}
func(1, 2, 3); // Window {...} 1, 2, 3
func.call({x : 1}, 4, 5, 6); // { x : 1 } 4 5 6
메서드에 대해서도 객체의 메서드를 그냥 호출하면 this는 객체를 참조하지만, call 메서드를 이용하면 임의의 객체를 this로 지정할 수 있다.
▼ 메서드를 call 메서드로 호출하기
var obj = {
a : 1,
method : function (x, y) {
console.log(this.a, x, y);
}
};
obj.method(2, 3); // 1 2 3
obj.method.call({a : 4}, 5, 6); // 4 5 6
3.2.2 apply 메서드
Function.prototype.apply(thisArg[, argsArray])
apply 메서드는 call 메서드와 기능적으로 완전히 동일하다. 다만 차이가 있다면 apply 메서드는 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다.
▼ apply 메서드
// 함수를 apply 메서드로 호출하기
var func = function (a, b, c) {
console.log(this, a, b, c);
};
func.apply({x : 1}, [4, 5, 6]); // { x : 1 } 4 5 6
// 메서드를 apply 메서드로 호출하기
var obj = {
a : 1
method : function () {
console.log(this.a, x, y);
}
};
obj.method.apply({a : 4}, [5, 6]); // 4 5 6
3.2.3 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']
객체에는 배열 메서드(push, slice 등)를 직접 적용할 수 없다. 그러나 키가 0 또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티의 값이 0 또는 양의 정수인 객체, 즉 배열의 구조와 유사한 객체(유사배열객체)의 경우, call 또는 apply 메서드를 이용해 배열 메서드를 사용할 수 있다.
push 메서드는 obj 객체에 프로퍼티 3에 'd'를 추가했고, slice 메서드는 객체를 배열로 전환했다. slice 메서드는 원래 시작 인덱스 값과 마지막 인덱스 값을 받아 시작 값부터 마지막값의 앞부분까지의 배열 요소를 추출하는 메서드인데, 매개변수를 아무 것도 넘기지 않을 경우에는 그냥 원본 배열의 얕은 복사본을 반환한다.
함수 내부에서 접근할 수 있는 arguments 객체도 유사배열객체이므로 위와 같은 접근이 가능하다.
querySelectorAll, getElementsByClassName 등의 Node 선택자로 선택한 결과인 NodeList도 마찬가지다.
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.querySelectAll('div');
var nodeArr = Array.prototype.slice.call(nodeList);
nodeArr.forEach(function (node) {
console.log(node);
});
그 밖에도 유사배열객체에는 call/apply 메서드를 이용해 모든 배열 메서드를 적용할 수 있다. 배열처럼 인덱스와 length 프로퍼티를 지니는 문자열에 대해서도 마찬가지다. 단, 문자열의 경우 length 프로퍼티가 읽기 적용이기 때문에 원본 문자열에 변경을 가하는 메서드(push, pop, shift, unshift, splice 등)는 에러를 던지며, concat처럼 대상이 반드시 배열이어야 하는 경우에는 에러는 나지 않지만 제대로 된 결과를 얻을 수 없다.
var str = 'abc def';
Array.prototype.push.call(str, ', pushed string');
// Error : Cannot assign to read only property 'length' of object [object String]
Array.prototype.concat.call(str, 'string'); // [String {"abc def"}, "string"]
Array.prototype.every.call(str, function(char) { return char != ' '; } // false
Array.prototype.some.call(str, function(char) { return char == ' '; } // true
var newArr = Array.prototype.map.call(str, function(char) { return char + '!'; });
console.log(newArr); // ['a!', 'b!', 'c!', ' !', 'd!', 'e!', 'f!']
var newStr = Array.prototype.reduce.apply(str, [
function(string, char, i) { return string + char + i; },
''
]);
console.log(newStr); //"a0b1c2 3d4e5f6"
call/apply 메서드를 이용해 형변환 하는 것은 'this를 원하는 값으로 지정해서 호출한다'는 본래의 메서드의 의도와는 다소 동떨어진 활용법이라 할 수 있다. slice 메서드는 오직 배열의 형태로 '복사'하기 위해 차용됐을 뿐이니, 코드만 봐서는 어떤 의도인지 파악하기 쉽지 않다. 이에 ES6에서는 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 Array.from 메서드를 새로 도입했다.
▼ Array.from 메서드
var obj = {
0 : 'a',
1 : 'b',
2 : 'c',
length : 3
};
var arr = Array.from(obj);
console.log(arr); // ['a', 'b', 'c']
- 생성자 내부에서 다른 생성자를 호출
생성자 내부에서 다른 생성자와 공통된 내용이 있을 경우, 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, '구골');
- 여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 - apply 활용
여러 개의 인수를 받는 메서드에게 하나의 배열로 인수들을 전달하고 싶을 때 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); // 45 3
// 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에서는 펼치기 연산자(spread operator)를 이용하면 apply를 적용하는 것보다 더 간편하게 작성할 수 있다.
var numbers = [10, 20, 3, 16, 45];
var max = Math.max(...numbers);
var min = Math.min(...numbers);
console.log(max, min); // 45 3
3.2.4 bind 메서드
bind 메서드는 ES5에서 추가된 기능으로, call과 비슷하지만 즉시 호출하지는 않고 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드다. 다시 새로운 함수를 호출할 때 인수를 넘기면, 그 인수들은 기존 bind 메서드를 호출할 때 전달했던 인수들의 뒤에 이어서 등록된다. 즉, bind 메서드는 함수에 this를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 모두 지닌다.
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
bind 메서드를 적용해서 새로 만든 함수는 한 가지 독특한 성질이 있는데, 바로 name 프로퍼티에 동사 bind의 수동태인 'bound'라는 접두어가 붙는다. 어떤 함수의 name 프로퍼티가 'bound xxx'라면 이는 곧 함수명이 xxx인 원본 함수에 bind 메서드를 적용한 새로운 함수라는 의미가 되므로 기존의 call이나 apply보다 코드를 추적하기에 더 수월해진 면이 있다.
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
또한 앞서 소개한 것 외에도 상위 컨텍스트의 this를 전달할 때에도 call, apply, bind 메서드를 사용해 손쉽게 전달할 수 있다.
// call 메서드를 사용
var obj = {
outer : function () {
console.log(this);
var innerFunc = function () {
console.log(this);
};
innerFunc.call(this);
}
};
obj.outer();
// bind 메서드를 사용
var obj = {
outer : function () {
console.log(this);
var innerFunc = function () {
console.log(this);
}.bind(this);
innerFunc();
}
};
obj.outer();
또한 콜백 함수를 인자로 받는 함수나 메서드 중에서 기본적으로 콜백 함수 내에서의 this에 관여하는 함수 또는 메서드에 대해서도 bind 메서드를 이용하면 this 값을 사용자의 입맛에 맞게 바꿀 수 있다.
var obj = {
logThis : function () {
console.log(this);
},
logThisLater1 : function () {
setTimeout(this.logThis, 500);
},
logThisLater2 : function () {
setTimeout(this.logThis.bind(this), 1000);
}
};
obj.logThisLater1(); // Window { ... }
obj.logThisLater2(); // obj { logThis : f, ... }
3.2.5 화살표 함수의 예외사항
ES6에 새롭게 도입된 화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외됐다. 즉, 이 함수 내부에는 this가 아예 없으며, 접근하고자 하면 스코프체인 상 가장 가까운 this에 접근하게 된다.
var obj = {
outer : function () {
console.log(this); // obj { outer : f, ... }
var innerFunc = () => {
console.log(this); // obj { outer : f, ... }
};
innerFunc();
}
};
obj.outer();
3.2.6 별도의 인자로 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);
},
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])
Array.prototype.findIndex(callback[, thisArg])
Array.prototype.flatMap(callback[, thisArg])
Array.prototype.from(callback[, thisArg])
Set.prototype.forEach(callback[, thisArg])
Map.prototype.forEach(callback[, thisArg])
'Javascript' 카테고리의 다른 글
인프런 리액트 강의 - Javascript 요약 ① (1) | 2023.12.07 |
---|---|
Chapter 5 요약 (1) | 2022.09.21 |
Chapter 4 요약 (0) | 2022.09.15 |
Chapter 2 요약 (0) | 2022.09.06 |
Chapter 1 요약 (0) | 2022.09.04 |