객체지향 언어에서의 this
는 클래스로 생성한 인스턴스 객체로 클래스에서만 사용이 가능하다.
하지만 JS에선 어디서든 사용이 가능하나 상황에 따라 this
가 바라보는 대상이 다르다. 그래서 내가 예상한 대로 대상을 바라보게 하기 위해 정확한 작동 방식을 이해해야 한다.
this는 실행 컨텍스트가 생성될 때 즉, 함수를 호출할 때 결정된다. (함수 호출 → 실행 컨텍스트 생성)
따라서 함수 호출 방식에 따라 값이 달라진다.
전역 컨텍스트를 생성하는 주체가 전역객체여서 전역 공간에서 this는 전역객체를 가리킨다.
브라우저 환경의 전역객체: window
, node.js ****환경의 전역객체: global
LexicalEnvironment
의 프로퍼티로서 동작한다.LexicalEnvironment
의 프로퍼티로 인식한다.LexicalEnvironment
의 프로퍼티로 저장해서 변수 호출 시 LexicalEnvironment
를 조회해 일치하는 프로퍼티를 반환한다.LexicalEnvironment
가 전역객체를 참조하기에 전역변수 선언 시 JS는 전역객체의 프로퍼티로 할당한다.전역 공간에서는 var로 선언하는 것과 전역객체 프로퍼티에 직접 할당하는 것이 같은 역할을 한다.
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
하지만 삭제에 있어선 다른 반응을 보인다.
var a = 1;
delete window.a; // false
console.log(a, window.a, this.a) // 1 1 1
delete a; // false
console.log(a, window.a, this.a) // 1 1 1
전역변수로 선언한 경우 삭제가 안되지만, 전역객체 프로퍼티에 할당한 경우는 삭제가 가능하다.
window.a = 1;
delete window.a; // true
console.log(a, window.a, this.a) // Uncaugth ReferenceError: a is not defined
// ---
window.b = 1;
delete b; // true
console.log(b, window.b, this.b) // Uncaugth ReferenceError: b is not defined
이는 JS엔진이 전역변수 선언 시 자동으로 전역객체의 프로퍼티에 할당하면서 해당 프로퍼티의 configurable(변경 및 삭제 가능성) 속성을 false로 정의하기 때문이다.
var로 선언한 전역변수, 전역객체 프로퍼티는 호이스팅, configurable의 차이가 있다.
함수를 실행하는 일반적인 방법: 함수로서 호출
, 메소드로서 호출
둘의 차이는 독립성이다.
함수:독립적인 기능 수행, 메서드: 자신을 호출한 대상 객체에 관한 동작 수행
메서드는 객체의 프로퍼티에 할당된 함수이기도 하지만 객체의 메서드로서 호출할 경우에만 메서드로 동작하며 아닐 경우엔 함수로 동작한다.
var func = function (x) {
console.log(this, x);
}
func(1) // Window {...} 1
var obj = {
method: func
}
obj.method(2) // {method: f} 2
func변수에 할당한 값과 obj객체의 method 프로퍼티에 할당한 값 모두 익명 함수를 참조하고 있다.
익명 함수는 그대로지만 함수를 변수에 담아 호출한 경우와 객체의 프로퍼티에 할당해 호출한 경우 this가 달라진다.
메서드로서 호출 구분: 함수 앞에 .
,[]
이 있는지, 함수 앞에 객체가 명시 되어있는지
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)
this 바인딩 핵심
함수 실행 시의 주변환경(메서드 내부/함수 내부)❌
함수 호출 구문 앞에 .
/[]
유무 ✅
ES5, 내부함수에 this 상속하는 방법
var obj = {
outer: function () {
console.log(this);
var self = this;
var innerFunc = function () {
console.log(self);
}
innerFunc();// {outer: f}
}
};
obj.outer();// {outer: f}
변수에 this를 할당하면 끝이다. 상위 스코프의 this를 저장해 내부함수에서 사용하는 것이다. 보통 변수명은 self
를 많이 사용한다. (_*this
, that
, _
도 사용은 함*)
ES6에서 this가 전역객체를 바라보는 문제를 해결하기 위해 화살표 함수
를 도입했다.
화살표 함수
는 실행 컨텍스트 생성 시 this 바인딩 과정이 없어서 상위 스코프의 this를 활용할 수 있다.
콜백함수 관해선 다음장에서 자세히 설명 예정
콜백함수: A함수의 제어권을 B함수에게 넘겨준 경우의 A함수
setTimeout(function () {console.log(this)},300); //window {...}
[1,2,3,4,5].forEach(function(x) {
console.log(this,x); //window {...}
});
document.body.innerHTML += '<button id="a">click</button>';
document.body.querySelector('#a')
.addEventListener('click', function (e) {
console.log(this, e) //<button id="a">click</button>, {클릭 이벤트 정보}
});
setTimeout
, forEach
는 내부 함수 호출 시 this의 대상을 정하지 않아 전역객체를 바라본다.
addEventListener
는 메서드 앞에 this를 정의해서 함수 호출 시 엘리먼트
, 클릭 이벤트 정보 객체
가 반환된다.
생성자 함수: 공통된 성질을 지닌 객체를 생성하는 함수
(객체지향 언어에서의) 생성자: 클래스, 클래스를 통해 만든 객체: 인스턴스
생성자는 구체적인 인스턴스를 만드는 “틀”
틀에 해당 클래스의 공통 속성들이 준비되어있고 개성을 더해 개별 인스턴스를 만든다.
ex) 사람을 만드는 인간 클래스
개성⬇️ | 노란머리 🟡 |
---|---|
class(공통속성)🔁 | 직립보행, 언어구사, 도구사용 등 👤 |
인스턴스 | 노란 머리 사람 🧑🏼 |
사람들은 인간 클래스에 속한 인스턴스들이다. 따라서 공통점도 가지고 있지만 개성도 있을 수 있다.
new 명령어와 같이 함수 호출 ⇒ 함수가 생성자로서 동작
var Cat = function (name, age) {
this.bark = '야옹';
this.name = name;
this.age = age;
};
var choco = new Cat('초코',7) //1번
var nabi = new Cat('나비',8) //2번
console.log(choco, nabi)
/* 결과
Cat {bark: '야옹', name: '초코', age:7, [[Prototype]]: Object}
Cat {bark: '야옹', name: '나비', age:8, [[Prototype]]: Object}
*/
new와 함께 함수를 호출해서 변수에 담아준다. 출력하면 각 Cat클래스의 인스턴스가 출력된다.
생성자 함수 내부 this는 만들어질 인스턴스 자신이다.
this에 별도의 대상을 바인딩하는 방법이 있다.
Function.prototype.call(thisArg[, arg1 [, arg2[, ...]]])
call
은 메서드의 호출 주체인 함수를 바로 실행시키는 메서드이다.
첫번째 인자를 this로 바인딩, 이후 인자는 매개변수이다.
var func = function(a,b,c){
console.log(this,a,b,c);
}
func.call({x:1},4,5,6); //{x:1} 4 5 6
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
객체 메서드를 그냥 호출하면 this는 객체를 참조한다. 하지만 call메서드를 사용하면 설정한 객체를 this로 지정한다.
Function.prototype.apply(thisArg[, argsArray])
apply메서드는 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
var obj = {
a:1,
method: function(x,y){
console.log(this.a,x,y);
}
}
obj.method.call({a:4},[5,6]); //4 5 6
객체엔 배열 메서드 적용불가
유사배열객체는 call/ apply를 이용해 배열 메서드를 차용할 수 있다.
유사배열객체: 키가 0 또는 양의 정수인 프로퍼티가 존재하며 length프로퍼티의 값이 0또는 양의 정수인 객체
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']
slice에 아무 인자도 넣지 않으면 얕은 복사가 되고 배열 메서드기 때문에 복사본은 배열이다.
arguments 객체도 유사배열객체라 배열로 전환해서 활용할 수 있고 nodelist도 가능하다.
arguments
객체는 함수에 전달된 인자에 해당하는 Array
형태의 객체이다.function func1(a, b, c) {
console.log(arguments[1]); // 2
console.log(arguments);
/*
0: 1,
1: 2,
2: 3,
callee:ƒ func1(a, b, c)
length: 3
[[Prototype]]: Object
*/
}
func1(1, 2, 3);
//arguments
function a () {
var argv = Array.prototype.slice.call(arguments);
argv.forEach(function(arg){
console.log(arg);
})
}
a(1,2,3) // 1 2 3
//nodelist
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)
/*
<div>a</div>
<div>b</div>
<div>c</div>
*/
})
call
/ apply
를 이용해 형변환하는 것은 this
를 원하는 값으로 설정하는 본래의 의도에 벗어난 활용법이다.
ES6에서 유사배열객체, 순회 가능한 데이터 타입을 배열로 전환하는 Array.from
메서드가 등장했다.
생성자 내부에 다른 생성자와 공톤된 부분이 있다면 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;
}
var jg = new Student('짱구', 'male', '떡잎대');
var numbers = [10,12,4,1,100]
var max = min = numbers[0];
numbers.forEach(function(number){
if(number > max){
max = number;
}
if(number < min){
min = number;
}
});
배열 요소중 최대값, 최소값을 구해야 하는데 apply
를 사용하지 않으면 위처럼 코드가 길어진다.
var numbers = [10,12,4,1,100];
var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);
console.log(max, min); // 100 1
하지만 Math.max
/min
메서드와 apply
메서드를 이용하면 짧고 간단히 구현할 수 있다.
var numbers = [10,12,4,1,100];
var max = Math.max(...numbers);
var min = Math.min(...numbers);
console.log(max, min); // 100 1
사실 이 방법 보단 스프레드 연산자(ES6)를 이용하는게 더 간단하다.
call
/apply
는 원하는 this
를 바인딩하면서 함수, 메소드를 실행하는 좋은 방법이지만 this예측, 코드해석 방해의 단점이 있다. ES5 이하 환경에선 다른 방법이 없어 실무에서 많이 사용된다.
Function.prototpye.bind(thisArg[, arg[, arg[, ...]]])
bind
메서드는 call
과 비슷하나 this
, 인자
로 새로운 함수를 반환한다. 즉, 함수에 this 미리 적용, 부분 적용 함수 구현 2가지 목적을 지닌다.
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프로퍼티에 bound
가 붙는다.
따라서 함수의 name프로퍼티가 bound func
이면 func이란 원본 함수에 bind메서드를 적용한 함수라는 걸 알 수 있다.
이전에 메서드 내부 함수에 메서드의 this를 바라보게 하는 법으로 self변수
를 이용했지만 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를 바인딩 하지 않는다.
함수 내부에는 this가 없고 접근한다면 스코프체인상 가까운 this에 접근한다.
//화살표 함수
var obj = {
outer: function(){
console.log(this);
var innerFunc = () => {
console.log(this);
};
innerFunc()
}
};
콜백함수를 인자로 받는 메서드 중 this를 지정할 수 있는 메서드도 있다.
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(arrayLike[, thisArg])
Set.prototype.from(callback[, thisArg])
Map.prototype.forEach(callback[, thisArg])