코어자바스크립트를 읽으며 공부한 내용입니다. 자세한 내용은 책을 참고!
1) 전역 공간에서 this는 ‘전역 객체'인 window를 의미한다.
2) 참고로, 전역 변수를 선언하면 자바스크립트는 이를 전역객체의 프로퍼티로도 할당한다.
// 아래 실행결과는 모두 1을 콘솔에 출력한다.
var a = 1;
console.log(a);
console.log(window.a);
console.log(this.a);
3) 그러나 삭제명령, 즉 delete 명령의 경우 전역변수로 선언한 것은 삭제되지 않고, 전역객체의 프로퍼티로 할당한 경우(window.c)만 삭제된다.
var a = 1;
delete a;
console.log(a, window.a, this.a); // 1 1 1
window.c = 1;
delete window.c;
console.log(c, window.c, this.c); // Uncaught ReferenceError: c is not defined
☑️ 함수 vs. 메서드
‘독립성'에 의해 구분된다.함수는 독립적으로 사용되는 반면, 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행한다. 그리고 기억할 것은, 어떤 함수를 오브젝트 안의 프로퍼티로 할당한다고 해서 그 자체로서 무조건 메서드가 되는 것이 아니라, 객체의 메서드로 호출할 경우
에만 메서드로 동작하고, 그렇지 않으면 함수로 동작한다.`
☑️ 메서드 내부에서의 this
→ ‘그 함수를 호출한 객체 자신’을 의미함.
☑️ 함수 내부에서의 this
별도로 지정되지 않기 때문에, this는 전역객체인 window
를 의미한다.
☑️ 메서드의 내부함수에서의 this
위와 같다. 메서드 내부이더라도 메서드가 아니라 함수로 호출
되었기 때문에, 별도로 지정되지 않아, 결국에는 this는 전역객체인 window
를 의미한다.
obj.innerMethod(); // this = obj
innerFunc(); // this = window
☑️ 메서드의 내부 함수에서의 this를 우회하는 방법
🤔💭 : 흠.. 함수로 사용하면 this에 대한 구분은 명확히 할 수 있어서 좋긴 한데.. this가 주는 인상이 다 사라져 버리네, 이걸 어쩌지?
“호출 주체가 없을 때에는, 자동으로 전역객체를 바인딩 하지 말고, 호출 당시 주변의 this를 그대로 상속받아 사용할 수 있으면 좋을 텐데... 마치 변수 검색시 가장 가까운 스코프의 L.E를 찾고 만약 없으면 상위 스코프를 선택하듯이 말이야..”
💡ES5까지는 자체적으로 내부함수에 this를 상속할 방법이 없지만, 우회할 방법이 있기는 해!
⇒ ‘변수를 이용'하는 것.
var obj = {
var self = this; // 변수에 객체를 저장
var innerFunc = function(){
console.log(self); // 내부함수에서 사용, this는 obj
}
innerFunc();
}
☑️ this를 바인딩하지 않는 함수(화살표 함수)
💡ES6에서 이런 문제를 해결하고자 도입한 것이 화살표 함수!
화살표 함수는 실행컨텍스트 생성시, this 바인딩 과정 자체가 빠져, 상위 스코프의 this를 그대로 사용할 수 있다.
var obj = {
outer: function(){
console.log(this); // this = outer
var innerFunc = () => { // this = outer!!
console.log(this)
};
innerFunc();
}
};
obj.outer();
→ 콜백 함수도 함수이기에 기본적으로는 this가 전역객체를 참조하지만, 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 된다.
document.body.querySelector("a").addEventListener('click', function(e) {
console.log(this, e) //this는 앞서 지정한 element가 된다.
})
👶 앞서서 메서드로 호출되는 경우에 this는 메서드를 호출한 객체를 의미한다고 했고, 이는 ‘.’점으로 구분된다. 이걸 적용해서 이해해보면 이해하기가 쉬워지는데, .addEventListener가 그 앞의 객체가 부른 메서드라고 생각해 보면 좋을 것 같다.
생성자는 class, 클래스를 통해 만든 객체는 instance라고 부른다.
어떤 함수가 생성자 함수로 호출된 경우, 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 된다.
⇒ 생성자 함수를 호출(new 명령어와 함께 함수를 호출)하면 우선 생성자의 prototype 프로퍼티를 참조하는 proto라는 프로퍼티가 있는 인스턴스를 만들고, 미리 준비된 공통 속성 및 개성을 해당 객체(this)에 부여한다.
var Cat = function(name, age){
this.bark = '야옹'; // this는 새로 만들 구체적인 인스턴스 자신이다!!
this.name = name;
this.age = age;
};
var choco = new Cat('초코', 7); // Cat{bark:야용', name:'초코', age:7}
상황별로 this에 바인딩되는 규칙들이 있는데, 이런걸 깨고 this에 별도의 대상을 바인딩하는 방법도 있다. 만약 어떤 코드를 봤는데, 그 규칙에 부합되지 않는다면, 다음 방법 중 하나를 사용했을 것이라고 추측할 수 있다.
메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령이다.
call 메서드의 첫 번째 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 한다.
함수를 그냥 실행하면 this는 전역객체를 참조하지만, call 메서드를 이용하면 임의의 객체를 this로 지정할 수 있다.
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}
call과 기능적으로 동일하다. 단 한 가지 차이점은, 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
💼 유사배열객체에 배열 메서드를 적용
→ 객체에는 배열 메서드를 직접 적용할 수 없다. 그러나 키가 0 또는 양의 정수인 프로퍼티가 존재하고, length프로퍼티의 값이 0또는 양수의 정수인 객체, 즉 배열의 구조와 유사한 객체의 경우 call/apply 메서드를 이용해 배열 메서드를 차용할 수 있다.
let obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
Array.prototype.push.call(obj, 'd');
console.log(obj);
let arr = Array.prototype.slice.call(obj);
console.log(arr);
[object Object] {
0: "a",
1: "b",
2: "c",
3: "d",
length: 4
}
["a", "b", "c", "d"]
→ 유사배열객체인 arguments, NodeList에도 배열 메서드를 적용 가능하게 된다.
function a(){
const argv = Array.prototype.slice.call(arguments);
argv.forEach(function (arg) {
console.log(arg)
});
}
a(1,2,3); // 1, 2, 3을 출력한다.
→ call/apply를 통해 형변환을 하는 것은 ‘this를 원하는 값으로 지정해서 호출한다'는 본래의 메서드의 의도와는 다소 동떨어진 활용법이다. 의도를 파악하기도 어렵고 말이다.
그래서! ES6에서는 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 Array.from 메서드를 새로 도입하였다.
let obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3};
let 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에 Person으로 만드는 객체를 저장
this.school = school;
}
let test = new Student('minju', 'f', 'Code');
console.log(test);
💼 여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 - apply 활용
→ 여러 개의 인수를 받는 매서드에게 하나의 배열로 인수들을 전달하고 싶을 때,
let numbers = [1,20,40,23,41];
const max = Math.max.apply(null, numbers);
⚡ES6에서는 그냥 펼치기 연산자를 사용하면 된다.
let numbers = [1,20,40,23,41];
const max = Math.max(...numbers);
→ call과 비슷하지만, 즉시 호출하지는 않고 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드. 다시 새로운 함수를 호출할 때 인수를 넘기면, 그 인수들은 기존 bind 메서드를 호출할 때 전달했던 인수들의 뒤에 이어서 등록된다.
즉, bind 메서드는 함수에 this를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 모두 지닌다.
let func = function(a,b,c,d){
console.log(this,a,b,c,d)
};
func(1,2,3,4);
let bindFunc = func.bind({x:1}, 11, 12); // this바인딩, 함수 부분 적용
console.log(bindFunc(2,3)); // 이어서 나온다.
💼 name 프로퍼티
→ bind 메서드를 적용해서 새로 만든 함수는 name 프로퍼티에 ‘bound’라는 접두어가 붙어 apply/call보다 코드를 추적하기에 훨씬 수월하다.
// 위의 예제 코드에서,
console.log(func.name); // "func"
console.log(bindFunc.name); // "bound func"
💼 상위 컨텍스트의 this를 내부함수나 콜백 함수에 전달하기
<내부함수에 전달한 예>
메서드의 내부함수에서 메서드의 this를 그대로 바라보게 하려면, ES6에서 추가된 화살표 함수 말고, self등의 변수를 활용한 우회법이 있었는데, self 변수 활용해서 this를 저장하는 대신, call/apply/bind를 사용하면 훨씬 깔끔한 처리가 가능하다.
let obj = {
outer : function(){
console.log(this);
let innerFunc = function(){
console.log(this);
};
innerFunc.call(this); // innerFunc의 this = outer
}
};
obj.outer();
<콜백 함수에 전달한 예>
콜백 함수를 인자로 받는 함수나 메서드 중에서 기본적으로 콜백 함수 내에서의 this에 관여하는 함수 또는 메서드에 대해서도 bind 메서드를 이용하면 this값을 사용자의 입맛에 맞게 바꿀 수 있다.
let obj = {
logThis : function(){
console.log(this);
},
logThisLater function () {
setTimeout(this.logThis.bind(this), 1000); // this = obj
}
};
화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외되었다.
따라서 this가 아예 없으며, 접근하고자 하는 경우 스코프체인상 가장 가까운 this에 접근하게 된다.
따라서 별도의 변수로 우회하거나, call/apply/bind를 적용할 필요가 없어 더욱 편리하다.
콜백함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체(thisArg)를 인자로 지정할 수 있는 경우가 있다. 이러한 메서드의 thisArg 값을 지정하면, 콜백 함수 배누에서 this값을 원하는 대로 변경할 수 있다. 이런 형태는 배열메서드에 많이 포진돼있다. 대표적인 예인 forEach를 살펴보자면 다음과 같다.
→ 아래 예제는 배열의 각 항목에서, 객체에 속성을 갱신한다.
function Counter() {
this.sum = 0
this.count = 0
}
Counter.prototype.add = function(array) {
array.forEach(function(entry) {
this.sum += entry
++this.count
}, this)
// ^---- 주의
}
const obj = new Counter()
obj.add([2, 5, 9])
obj.count
// 3
obj.sum
// 16