[코어 자바스크립트] 03 this

임승민·2022년 12월 16일
0
post-thumbnail

객체지향 언어에서의 this는 클래스로 생성한 인스턴스 객체로 클래스에서만 사용이 가능하다.

하지만 JS에선 어디서든 사용이 가능하나 상황에 따라 this가 바라보는 대상이 다르다. 그래서 내가 예상한 대로 대상을 바라보게 하기 위해 정확한 작동 방식을 이해해야 한다.

01 상황에 따라 달리지는 this

this는 실행 컨텍스트가 생성될 때 즉, 함수를 호출할 때 결정된다. (함수 호출 → 실행 컨텍스트 생성)

따라서 함수 호출 방식에 따라 값이 달라진다.

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

전역 컨텍스트를 생성하는 주체가 전역객체여서 전역 공간에서 this는 전역객체를 가리킨다.
브라우저 환경의 전역객체: window, node.js ****환경의 전역객체: global

전역 공간 특징

  • JS 변수는 LexicalEnvironment의 프로퍼티로서 동작한다.
    따라서 var 연산자로 변수를 선언하면 JS는 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의 차이가 있다.

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

함수, 메서드

함수를 실행하는 일반적인 방법: 함수로서 호출, 메소드로서 호출

둘의 차이는 독립성이다.
함수:독립적인 기능 수행, 메서드: 자신을 호출한 대상 객체에 관한 동작 수행

메서드는 객체의 프로퍼티에 할당된 함수이기도 하지만 객체의 메서드로서 호출할 경우에만 메서드로 동작하며 아닐 경우엔 함수로 동작한다.

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

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)

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

함수 내부에서의 this

  • 함수로서 함수를 호출하면 this가 지정되지 않는다.
    • this에는 호출한 주체의 정보가 담기는데, 함수로서 호출은 주체를 명시하지 않고 코드에 직접 관여해서 실행하는 것이기 때문에 호출 주체를 알 수 없다.
  • 실행 컨텍스트 활성화 시 this가 지정되지 않았다면 전역객체를 바라본다. 그래서 함수로서 호출하면 this는 전역객체를 바라본다.

메서드의 내부함수에서의 this

this 바인딩 핵심

함수 실행 시의 주변환경(메서드 내부/함수 내부)❌

함수 호출 구문 앞에 ./[] 유무 ✅ 

메서드의 내부 함수에서의 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, _ 도 사용은 함*)

this를 바인딩하지 않는 함수

ES6에서 this가 전역객체를 바라보는 문제를 해결하기 위해 화살표 함수를 도입했다.

화살표 함수는 실행 컨텍스트 생성 시 this 바인딩 과정이 없어서 상위 스코프의 this를 활용할 수 있다.

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

콜백함수 관해선 다음장에서 자세히 설명 예정

콜백함수: A함수의 제어권을 B함수에게 넘겨준 경우의 A함수

  • 제어권을 가진 함수의 내부로직에 따라 실행되며, 콜백함수의 this도 정해진다.
  • 콜백함수도 함수라 전역객체를 참조하지만, 제어권을 가진 함수가 콜백함수의 this를 지정할 수 있다.
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를 정의해서 함수 호출 시 엘리먼트, 클릭 이벤트 정보 객체가 반환된다.

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

생성자 함수: 공통된 성질을 지닌 객체를 생성하는 함수

(객체지향 언어에서의) 생성자: 클래스, 클래스를 통해 만든 객체: 인스턴스

생성자는 구체적인 인스턴스를 만드는 “틀”
틀에 해당 클래스의 공통 속성들이 준비되어있고 개성을 더해 개별 인스턴스를 만든다.

ex) 사람을 만드는 인간 클래스

개성⬇️노란머리 🟡
class(공통속성)🔁직립보행, 언어구사, 도구사용 등 👤
인스턴스노란 머리 사람 🧑🏼

사람들은 인간 클래스에 속한 인스턴스들이다. 따라서 공통점도 가지고 있지만 개성도 있을 수 있다.

new 명령어와 같이 함수 호출 ⇒ 함수가 생성자로서 동작

  • 생성자 함수로 함수가 호출된 경우, 내부 this는 만들어질 인스턴스 자신이다.
  • 생성자 함수 호출 시 생성자의 prototype 프로퍼티를 참조하는 proto라는 프로퍼티가 있는 객체를 만들고(06장에서 설명 예정) 미리 준비된 공통 속성 및 개성을 해당 객체(this)에 부여한다. 이로써 구체적인 인스턴스가 만들어진다.
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는 만들어질 인스턴스 자신이다.

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

this에 별도의 대상을 바인딩하는 방법이 있다.

3-2-1 call 메서드

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로 지정한다.

3-2-2 apply 메서드

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

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

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

객체엔 배열 메서드 적용불가

유사배열객체는 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 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', '떡잎대');

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

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 이하 환경에선 다른 방법이 없어 실무에서 많이 사용된다.

3-2-4 bind 메서드

Function.prototpye.bind(thisArg[, arg[, arg[, ...]]])
  1. bind메서드는 call과 비슷하나 this, 인자로 새로운 함수를 반환한다.
  2. 새로운 함수를 호출해 인자를 넘기면 bind메서드 호출 시 전달했던 인자 뒤에 이어서 등록된다.

즉, 함수에 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

name 프로퍼티

bind메서드를 적용해 만든 함수는 name프로퍼티에 bound가 붙는다.

따라서 함수의 name프로퍼티가 bound func이면 func이란 원본 함수에 bind메서드를 적용한 함수라는 걸 알 수 있다.

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

이전에 메서드 내부 함수에 메서드의 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();

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

화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩 하지 않는다.

함수 내부에는 this가 없고 접근한다면 스코프체인상 가까운 this에 접근한다.

//화살표 함수
var obj = {
	outer: function(){
		console.log(this);
		var innerFunc = () => {
			console.log(this);
		};
		innerFunc()
	}
};

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

콜백함수를 인자로 받는 메서드 중 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])

0개의 댓글