this: 함수와 객체(method)를 나누는 유일한 기능

01. 상황에 따라 달라지는 this

this는 기본적으로 실행 컨텍스트가 생성될 때 결정 됨(thisBinding) → 실행 컨택스트는 함수를 호출할 때 생성 됨.

따라서, this는 함수를 호출할 때 결정됨

3-1-1. 전역 공간에서의 this

브라우져 환경 → this === window

Node.js 환경 → this === global

전역 변수는 전역객체의 Property로 할당됨. 변수이자 객체의 property

자바스크립트의 모든 변수는 사실 특정 객체(LexicalEnvironment)의 Property로 동작함

따라서 전역 변수가 전역 객체의 proeperty로 할당되는 이유는 전역 컨텍스트의 경우 L.E가 전역 객체를 그대로 참조하기 때문

var a=1;
console.log(a) //1 - 스코프 체인에서 a를 검색하다가 전역 스코프의 L.E에서 a를 찾았기 때문
console.log(window.a) //1
console.log(this.a) //1

3-1-2 메서드로서 호출할 때 그 메서드 내부에서의 this

함수 vs. 메서드

함수: 그 자체로 독립적인 기능 수행

메서드: 자신을 호출한 대상 객체에 관한 동작을 수행

obj.method(2) //점 표기법
obj['method'](2) //대괄호 표기법

어떤 함수를 호출 할때 그 함수 이름 앞에 객체가 명시되어 있는 경우 메서드로 호출한 것

매서드 내부에서의 this

obj.methodA() // this -> obj
obj.inner.methodB() // this -> obj.inner

3-1-3 함수로써 호출할 때 그 함수 내부에서의 this

함수 내부에서의 this

this 가 지정되지 않음. 따라서 전역 객체

메서드 내부함수에서의 this

var obj1 ={
  outer:function(){
    console.log(this)//obj1
    var innerFunc=function(){
      console.log(this)//window
    }
    innerFunc();
   
    var obj2={
      innerMethod:innerFunc
    }
    obj2.innerMethod();//obj2.innerMethod
  }
}
obj1.outer();

주의 해서 보아야 할 점은 "obj2.innerMethod()" 가 호출 될 때의 this 는 obj2 가 아닌 obj2.innerMethod 라는 부분이다. 따라서 해당 this 에서는 innerMethod 객체가 this 이다.

메서드 내부 함수에서의 this를 우회하는 방법

var obj1 ={
  outer:function(){
    console.log(this)//obj1
    var innerFunc1=function(){
      console.log(this)//window
    }
    innerFunc1();

		var self = this;
    var innerFunc2=function(){
      console.log(self)//outer
    }
    innerFunc2();
  }
}
obj1.outer();

self 변수를 이용해 this를 우회

this를 바인딩하지 않는 함수

함수 내부에서 this 가 전역 객체를 바라보는 문제를 보완하고자 화살표 함수(arrow function)를 도입

화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체를 제거하여 상위 스코프의 this 를 바라보게 함.

var obj1 ={
  outer:function(){
    console.log(this) // obj1
    var innerFunc1=()=>{
      console.log(this) // obj1
    }
    innerFunc1();
  }
}
obj1.outer();

3-1-4 콜백 함수 호출 시 그 함수 내부에서의 this

콜백 함수: 함수 A의 제어권을 함수 B 에게 넘겨주는 경우, 함수 A 가 콜백 함수

콜백 함수도 기본적으로 this는 전역 객체를 바라봄. 하지만 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우 해당 대상을 참조함

//(1) setTimeout
setTimeout(function(){
  console.log(this); // 전역 객체
},300);

//(2) forEach
[1,2,3,4,5].forEach(function(x){
  console.log(this,x) // 전역 객체
})

//(3) addEventListener
document.body.innerHTML+='<button id="a">click</button>'

document.body.querySelector('#a').addEventListener('click', function(e){
  console.log(this,e) // #a 객체가 this
})

//콜백함수의 this는 항상 전역 또는 .앞에 객체를 바라보지는 않는다

(1), (2) 처럼 setTimeout, forEach 함수는 내부적으로 콜백 함수를 호출할 때 this를 따로 바인딩 하지 않는다

(3) 처럼 addEventListener 함수는 내부적으로 콜백 함수를 호출할 때 this를 호출 객체를 바인딩 한다.

따라서 콜백함수의 this는 항상 전역 또는 .앞에 객체를 바라보지는 않는다

3-1-5 생성자 함수 내부에서의 this

new 명령어를 함께 사용하면 함수가 생성자로서 동작함. 따라서 인스턴스 내부에서의 this는 인스턴스 자기자신.

생성자의 prototype 프로퍼티를 참조하는 "proto" 라는 property가 있는 인스턴스를 만들고 미리 준비된 공통 속성을 this에 부여함

function Animal(){
/**/
	this.legNo = 4
}
var cat = new Animal();

02 명시적으로 this 를 바인딩하는 방법

명시적으로 앞서 설명한 규칙을 깨고 this 를 바인딩 할 수 있다.

3-2-1 call 메서드

//Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])

var func = function(a, b, c){
	console.log(this, a, b, c)
}
func(1, 2, 3) // 전역 객체
func.call({x:1}, 4, 5, 6) // {x:1}이라는 무명 객체

call 메서드 첫 번째 인자를 this로 바인딩

3-2-2 apply 메서드

//Function.prototype.apply(thisArg[, argsArray])

var func = function(a, b, c){
	console.log(this, a, b, c)
}
func.apply({x:1}, [4, 5, 6]) // {x:1}이라는 무명 객체

call 메서드와 기능적으로 동일.

apply 메서드는 두번째 인자를 배열로 받아 그 배열의 요소를 함수의 매개변수로 지정.

3-2-3 call/apply 메서드의 활용

유사배열객체에 배열 메서드 적용

function a(){
  var argv = Array.prototype.slice.call(arguments);
  console.log(argv) //argv 배열

  argv.forEach(function(arg){
    console.log(arg)
  })
}
a(1,2,3)

document.body.innerHTML="<div>a0</div><div>a1</div><div>a2</div><div>a3</div>"

var nodeList = document.querySelectorAll('div')
console.log(nodeList) //nodeList 유사 배열 객체

var nodeArr = Array.prototype.slice.call(nodeList)
console.log(nodeArr) // nodeArr 배열

nodeArr.forEach(function(node){
  console.log(node)
})

유사 배열 객체: 키가 0 또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티 값이 0 또는 양의 정수인 객체

String 객체는 length가 readonly 프로퍼티이기 때문에, 원본 문자열에 변경을 가하는 메서드는 에러를 던짐

var obj={
	0:'a',
	1:'b',
	2:'c',
	length: 3
}
var arr= Array.from(obj)

Array.from: ES6에서 유사배열 객체에 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('John', 'F', 'SNU');
var jn = new Employee('David', 'M', 'Kakao');

console.log(by, jn)

위와 같이 Person 생성자를 공통화해서 사용할 수 있음

여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 - apply 활용

// ES5 
var numbers = [10, 20, 2, 14, 22];
var min = Math.min.apply(null, numbers)
var max = Math.max.apply(null, numbers)
console.log('min:', min, ' max:', max)

// ES6 spread 연산자 활용
var _min = Math.min(...numbers)
var _max = Math.max(...numbers)
console.log('_min:', _min, ' _max:', _max)

3-2-4 bind 메서드

//Function.prototype.bind(thisArg[, arg1[,arg2[, ...]]]);

var func = function(a, b, c, d){
	console.log(this, a, b, c, d)
}
func(1, 2, 3, 4) // 전역 객체

// (1) bind 메서드는 함수 호출시가 아닌, 할당 시 this를 미리 적용할 수 있음
var bindFunc1 = func.bind({x:1});
bindFunc1(5, 6, 7, 8); // {x:1} 5 6 7 8

// (2) 부분 적용 함수 구현
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

call과 비슷하지만 즉시 호출하지않고, 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드

두가지 목적:

  1. bind 메서드는 함수 호출시가 아닌, 할당 시 this를 미리 적용할 수 있음
  2. 부분 적용 함수 구현 → 디폴트 매개 변수를 bind시 결정할 수 있음.

name 프로퍼티

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

bind된 함수는 name 프로퍼티 앞에 "bound"라는 단어가 붙는다

상위 컨텍스트의 this를 내부함수나 콜백 함수에 전달하기

var obj ={
  logThis: function(){
    console.log(this);
  },
  logThisLater1: function(){
    setTimeout(this.logThis, 500)
  },
  logThisLater2: function(){
    setTimeout(this.logThis.bind(this), 1000)
  }
}
obj.logThisLater1() // (1) 전역 객체
obj.logThisLater2() // (2) obj

<중요!>

(1)에서 logThisLater1를 obj의 메서드로서 호출했으니, logThisLater1 함수 내에서의 this는 obj 임. 하지만 setTimeout 함수의 첫 번째 인자로 logThis라는 함수를 콜백 함수로써 전달했기 때문에, setTimeout 내에서 logThis 콜백함수 내의 this는 전역 객체가 된다. (함수로써 호출될 경우 this는 전역 객체)

(2)에서도 마찬가지로 obj의 메서드로써 logThisLater2를 호출했으니, logThisLater2 함수 내에서의 this는 obj임. 추가로 setTimeout 함수를 호출할때 첫 번째 인자로 logThis라는 함수에 this를 바인딩하여 콜백 함수로 전달했음. 따라서 setTimeout 내에서 logThis 콜백함수 내의 this는 obj 객체가 된다.

3-2-5 화살표 함수의 예외사항

var obj1 ={
  outer:function(){
    console.log(this) // obj1
    var innerFunc1=()=>{
      console.log(this) // obj1
    }
    innerFunc1();
  }
}
obj1.outer();

위에서 설명 했듯 화살표 함수를 사용하면 call/apply/bind 함수를 적용할 필요 없이 스코프 체인상 가장 가까운 this에 접근함

3-2-6 별도의 인자로 this를 받는 경우(콜백 함수 내에서의 this)

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) // (1) this 전달
  },
  average: function(){
    return this.sum/this.count;
  }
}
report.add(60, 70, 80); // (2)
console.log(report.sum, report.count, report.average())

(2) 에서 add 함수를 report 객체의 메서드로써 호출 했기 때문에, add 함수 내 this 는 전역 객체가 아닌 report 객체를 바라본다. 기본적으로 forEach 함수에 첫 번째 인자로 전달되는 콜백 함수 내 this는 전역 객체를 바라보나,두 번째 인자로 this 를 넘겨주면, 해당 this를 콜백 함수에서 참조하게 된다.

03 정리

아래 룰은 명시적 this 바인딩이 없는 경우 성립

  • 전역 공간

    : this === 전역객체

  • 함수를 어떤 객체의 메서드로 호출한 경우

    : this === 호출 주체 객체

  • 함수를 함수로서 호출한 경우

    : this === 전역객체

  • 콜백 함수 내부에서의 this

    : 해당 콜백함수의 제어권을 받은 함수가 정의한 바에 따름, 기본적으로 this === 전역객체

  • 생성자 함수

    : this === 생성될 인스턴스

명시적 this 바인딩

  • call, apply 메서드는 this를 명시적으로 지정하면서 함수 또는 메서드를 호출함

    func.call({x:1}, 4, 5, 6) 
    func.apply({x:1}, [4, 5, 6]) 
  • bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만듬

    var bindFunc = func.bind({x:1}, 4, 5);
  • 요소를 순회하면서 콜백 함수를 반복 호출하는 내용의 일부 메서드(배열 객체 메서드) 는 별도의 this를 인자로 받음

    args.forEach(function(entry){
          this.sum+=entry;
          ++this.count;
        }, this) // (1) this 전달
profile
Frontend engineer

0개의 댓글