[코어 자바스크립트] -This

이예슬·2022년 12월 31일
0

함수와 객체(메서드)의 구분이 느슨한 자바스크립트에서 this는 실질적으로 이 둘을 구분하는 거의 유일한 기능이다.

상황에 따라 달라지는 this

자바스크립트에서 this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정된다. 즉 this는 함수를 호출할 때 결정된다.

전역공간에서의 this

전역 공간에서 전역 컨텍스트를 생성하는 주체가 전역 객체이므로 this는 전역 객체를 가르킨다. 전역 객체는 자바스크립트 런타임 환경에 따라 달라지며 브라우저 환경에서 전역객체는 window이고 Node.js 환경에서는 global이다.

전역 공간에서 var로 변수를 선언하는 대신 window의 프로퍼티에 직접 할당하더라도 결과적으로 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

window.b = 2; 
delete window.b // true
console.log(b, window.b, this.b) // Uncaught ReferenceError: b is not defined

전역변수를 선언하면 자바스크립트 엔진이 이를 자동으로 전역객체의 프로퍼티로 할당하면서 추가적으로 해당 프로퍼티의 configurable 속성(변경 및 삭제 가능성)을 false로 정의한다.

메서드로서 호출할 때 그 메서드 내부에서의 this

함수를 실행하는 일반적인 방법으로는 함수로서 호출하는 경우와 메서드로서 호출하는 경우가 있다. 이 둘의 유일한 차이는 독립성이다. 함수는 객체의 메소드로 호출할 때만 메서드로 동작한다.

var func = function(x){
	console.log(this, x);
};
func(1) // 1

var obj = {
	method: func
};
obj.method(2) //2

1번과 2번은 모두 같은 func 함수를 실행하지만 1번은 함수로서 호출되었고 2번은 객체의 메서드로서 호출되었다. 즉 원래의 함수는 그대로인데 이를 변수에 담아 호출한 경우와 obj 객체의 프로퍼티에 할당해서 호출한 경우에 this가 달라진다.

함수로서 호출과 메서드로서 호출은 함수 앞에 점(.)의 유무로 간단하게 구분할 수 있다.

함수를 호출할 때 그 함수 이름 앞에 객체가 명시되어 있는 경우에는 메서드로 호출한 것이고 그렇지 않은 모든 경우에는 함수로 호출한 것이다.

메서드 내부에서의 this

this에는 호출한 주체에 대한 정보가 담긴다. 어떤 함수를 메서드로서 호출하는 경우 호출 주제는 바로 함수명 앞의 객체이다. 점 표기법의 경우 마지막 점 앞에 명시된 객체가 곧 this가 된다.

var obj = {
	methodA : function(){console.log(this)}
	inner: {
		methodB : function(){console.log(this)
	}
};

obj.methodA()
obj.inner.methodB()

함수로서 호출할 때 그 함수 내부에서의 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를 바인딩하지 않는 함수

ES6에서는 함수 내부에서 this가 전역 객체를 바라보는 문제를 보완하고자 this를 바인딩하지 않는 화살표 함수를 새로 도입했다. 화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 빠지게 되어 상위 스코프의 this를 그대로 활용할 수 있다.

var obj1 = {
	outer: function(){
		console.log(this)
		var innerFunc = () => {
			console.log(this)
		} 
		innerFunc()
	}
}

콜백 함수 호출 시 그 함수 내부에서의 this

함수A의 제어권을 다른 함수 B에게 넘겨주는 경우 함수 A를 콜백함수라고 한다.

콜백함수도 함수이므로 기본적으로 this가 전역 객체를 참조하지만 제어권을 받은 함수에서 콜백함수에 별도로 this가 될 대상을 지정하는 경우 그 대상을 참조하게 된다.

setTimeout(function(){console.log("setTimeout 안에서의 this",this)}, 300 )

let arr = [1, 2, 3, 4]

arr.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)
	})

생성자 함수 내부에서의 this

생성자 함수는 어떤 공통된 성질을 지니는 객체들을 생성하는 데 사용하는 함수이다. 객체지향 언어에서는 생성자를 클래스 클래스를 통해 만든 객체를 인스턴스라고 한다.

프로그래밍적으로 생성자는 구체적인 인스턴스를 만들기 위한 일종의 틀이다. 이 틀에는 해당 클래스의 공통 속성들이 미리 준비돼 있고 여기에 구체적인 인스턴스의 개성을 더해 개별 인스턴스를 만들 수 있다. new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작한다. 그리고 어떤 함수가 생성자 함수로서 호출된 경우 내부에서 this는 곧 새로 만들 구체적인 인스턴스 자신이 된다.

var Cat = function(name, age){
	this.bark = '야옹'
	this.name = name
	this.age = age
} 

var nabi = new Cat('나비', 5)
console.log(nabi)

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

call 메서드

call 메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령이다. 이때 call 메서드의 첫 번쨰 인자를 this로 바인딩하고 이후의 인자들을 호출할 함수의 매개변수로 한다. call 메서드를 이용하면 임의의 객체를 this로 지정할 수 있다.

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) 

이는 메서드로 호출했을 경우에도 동일하게 동작한다.

apply 메서드

apply 메서드는 call 메서드와 기능적으로 완전히 동일하나 두 번쨰 인자로 배열을 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 점에서 차이가 있다.

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

func.apply({x: 1}, [4, 5, 6])

call, apply 메서드의 활용

객체는 배열 메서드를 직접 적용할 수 없다. 그러나 키가 0 또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티 값이 0 또는 양의 정수인 객체 즉 배열의 구조와 유사한 객체의 경우(유사배열객체) call 또는 apply 메서드를 이용해 배열 메서드를 차용할 수 있다.

var obj1 = {
	0: 'a', 
	1: 'b', 
	2: 'c'
	length: 3
} 

Array.prototype.push.call(obj, 'd') 
console.log(obj)
var newArr = Array.prototype.slice.call(obj)
console.log(newArr)

함수 내부에서 접근할 수 있는 argument 객체도 유사 배열 객체이므로 위의 방법으로 배열로 전환해 활용할 수 있다.

그 밖에도 유사배열객체는 call/apply 메서드를 이용해 모든 배열 매서드를 적용할 수 있다. 하지만 문자열의 경우 length 프로퍼티가 읽기 전용이므로 원본 문자열에 변경을 가하는 메서드는 에러를 던지며 concat처럼 대상이 반드시 배열이어야 하는 경우에는 에러는 나지 않지만 제대로 된 결과를 얻을 수 없다.

var str = 'abc def'

Array.prototype.push.call(str, 'pushed string') 

Array.prototype.concat.call(str, 'string') 

var newArr = Array.prototype.slice.call(obj);

이때 slice 메서드가 배열 메서드이므로 복사본은 배열로 반환된다.

call/apply를 이용해 형변환하는 것은 “this를 원하는 값으로 지정해서 호출한다.”라는 본래의 메서드의 의도와는 다소 동떨어진 활용법이다. 이는 경험을 통해 숨은 뜻을 알고 있지 않는 한 코드만 보아서는 어떤 의도인지 파악하기 어렵다.

이에 ES6에서는 유사배열 객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 Array.from 메서드를 새로 도입했다.

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

생성자 내부에서 다른 생성자를 호출

생성자 내부에 다른 생성자와 공통된 내용이 있을 경우 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 Anna = new Student('Anna', 'female', 'SNU');

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

var numbers = [1, 2, 3, 4, 5, 10];
var max = Math.max.apply(null, numbers);
var min = Math.min.apply(nul, numbers); 

var spreadMax = Math.max(...numbers);
var spreadMin = Math.min(...numbers);

bind 메서드

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

bind 메서드는 ES5에서 추가된 기능으로 call과 비슷하지만 즉시 호출하지는 않고 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드이다. 즉 bind 메서드는 함수에 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) 

name 프로퍼티

bind 메서드를 적용해서 새로 만든 함수는 name 프로퍼티에 bound라는 접두어가 붙는다. 이로 인해 기존의 call이나 apply보다는 코드를 추적하기에 더 수월하다.

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

var obj = {
	outer: function(){
		console.log(this)
		var innerFunc = fucntion() {
			console.log(this)
		} 
		innerFunc.call(this)
	}
}

obj.outer()
var obj = {
	outer: function(){
		console.log(this)
		var innerFunc = fucntion() {
			console.log(this)
		}.bind(this); 
		innerFunc();
	}
}

obj.outer();

화살표 함수의 예외 사항

화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외되었다. 즉 이 함수 내부에는 this가 아예 없으며 접근하고자 하면 스코프체인상 가장 가까운 this에 접근하게 된다.

즉 일반 함수는 실행 시점에서 this가 바인딩된다면 화살표 함수는 선언될 시점에서의 상위 스코프의 this를 참조하게 된다.

var obj = {
	outer: function(){
		console.log(this)
		var innerFunc =() =>  {
			console.log(this)
		};
		innerFunc();
	}
}

obj.outer();

이처럼 화살표 함수를 사용하면 별도의 변수로 this를 우회하거나 call/apply/bind를 적용할 필요가 없다.

왜 없을까?

const person = {
  name: 'lee',
  foo1: function() {
    const foo2 = function() {
      console.log(this.name);
    }
    foo2();
  }
};

person.foo1();	

const person2 = {
  name: 'kim',
  foo1: function() {
    const foo2 = () => {
      console.log(this.name);
    }
    foo2();
  }
};

person2.foo1();	

화살표 함수를 쓰면 안되는 경우

  • 메소드 자신을 호출한 객체가 아닌 함수 선언 시점의 상위 스코프를 가리키게 되므로
  • 생성자 함수 화살표 함수는 생성자 함수로 사용할 수 없다.
  • addEventListener() 의 콜백함수
    const button = document.getElementById('myButton');
    
    button.addEventListener('click', () => {
      console.log(this);	// Window
      this.innerHTML = 'clicked';
    });
    
    button.addEventListener('click', function() {
       console.log(this);	// button 엘리먼트
       this.innerHTML = 'clicked';
    });
    addEventListener() 의 콜백함수에는 this에 해당 이벤트 리스너가 호출된 엘리먼트가 바인딩되도록 정의되어 있다. 이처럼 this가 이미 정해져 있는 콜백함수의 경우 화살표 함수를 사용하면 기존 바인딩이 사라지고 상위 스코프가 바인딩될 수 있다.

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

콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체를 인자로 지정할 수 있는 경우가 있다.

이러한 메서드의 thisArg 값을 지정하면 콜백 함수 내부에서 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);
	},
	average: function(){
		return this.sum/this.count;
		}
}
report.add(10, 20, 30) 
cosole.log(report.sum, report.count, report.average())

콜백 함수 내부에서 this는 forEach 함수의 두 번째 인자로 전달해준 this가 바인딩된다.


<코어 자바스크립트> 정재남, 위키북스(2019)

profile
꾸준히 열심히!

0개의 댓글