처음부터 차근차근

[코어 자바스크립트] This 본문

Language/JavaScript

[코어 자바스크립트] This

HangJu_95 2023. 6. 10. 17:21
728x90

다른 객체지향 언어에서의 this : 클래스로 생성한 인스턴스 객체를 의미한다.

Javascript에서의 this : 어디에서든지 사용가능하다.

This : '이것' 이란 뜻.

  • this는 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수(self-reference variable)이다.
  • this를 통해 자신이 속한 객체 또는 자신이 생성할 인스턴스의 프로퍼티나 메서드를 참조할 수 있다.
  • this는 자바스크립트 엔진에 의해 암묵적으로 생성된다.
  • this는 코드 어디서든 참조할 수 있다.
  • 자바스크립트의 this는 기본적으로 실행 컨텍스트가 생성될 때 결정된다. 즉, 함수를 호출할 때 결정된다.
  • 하지만 this는 객체의 프로퍼티나 메서드를 참조하기 위한 자기 참조 변수이므로
    일반적으로 객체의 메서드 내부 또는 생성자 함수 내부에서만 의미가 있다.
  • 함수를 호출하면 인자와 this가 암묵적으로 함수 내부에 전달된다. (전역 변수로 할당)
  • 함수 내부에서 인자를 지역 변수처럼 사용할 수 있는 것처럼, this도 지역 변수처럼 사용할 수 있다.
  • 단, this가 가리키는 값, 즉 this 바인딩은 함수 호출 방식에 의해 동적으로 결정된다.
  • 크게 전역에서 사용할 때와 함수안에서 사용할 때로 나눌 수 있다.

여기서 Binding이란??

  • 식별자와 값을 연결하는 과정을 말한다.
  • 변수선언은 변수 이름과 확보된 메모리 공간의 주소를 바인딩하는 것이다.
  • 즉, this 바인딩은 this(키워드로 분류되지만 식별자의 역할을 한다.)와 this가 가리킬 객체를 바인딩하는 것이다.

전역 공간에서의 this

전역 공간에서의 this : 전역 객체를 가리킨다.

전역 컨텍스트를 생성하는 주체가 전역 객체이기 때문

런타임 환경에 따라 this는 window(브라우저 환경) or global(node 환경)를 각각 지칭

 

전역변수를 선언하면 자바스크립트 엔진은 이를 전역 객체의 프로퍼티로도 할당한다.

var a = 1;
console.log(a)        // 1
console.log(window.a) // 1
console.log(this.a).  // 1

이렇게 출력되는 이유는, 자바스크립트의 모든 변수는 실은 특정 객체의 프로퍼티로써 동작하기 때문이다.

여기서 특정 객체란, LexicalEnvironment이다. 실행 컨텍스트는 변수를 수집해서 LexicalEnvironment의 프로퍼티로 저장한다.

즉, 정확히 표현하자면 전역변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다.

 

그렇다면 왜 a를 직접 호출했을 때도 1이 나올까??

변수 a에 접근하고자 하면 스코프 체인에서 a를 검색하다가 가장 마지막에 도달하는 전역 스코프의 렉시컬 환경, 즉 전역객체에서 해당 프로퍼티 a를 발견해서 그 값을 반환하기 때문!

 

특이점으로는, var로 변수를 선언하면 delete 연산자는 적용되지 않는다. (window.a로 프로퍼티를 입력하면 이는 삭제가 가능)

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

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

함수 : 독립성을 가지며, 호출 주체가 없다

메서드 : 종속성을 가지고 있으며, 실행 주체가 있어야 한다.

 

구체적으로, 함수는 전역에 선언된 일반 함수 / 객체 안에 메소드로 크게 구분할 수 있다.

=> 객체안에 프로퍼티로 선언된 함수를 전역에 선언된 함수와 구분하기 위해 메소드라고 한다.

this의 할당

// CASE1 : 함수
// 호출 주체를 명시할 수 없기 때문에 this는 전역 객체를 의미해요.
var func = function (x) {
	console.log(this, x);
};
func(1); // Window { ... } 1
// CASE2 : 메서드
// 호출 주체를 명시할 수 있기 때문에 this는 해당 객체(obj)를 의미해요.
// obj는 곧 { method: f }를 의미하죠?
var obj = {
	method: func,
};
obj.method(2); // { method: f } 2

함수로써의 호출과 메서드로써의 호출 기준 : 함수 앞에 객체가 명시돼 있는 경우에는 메서드이다.(없으면 그냥 일반 함수 호출)

var obj = {
	method: function (x) { console.log(this, x) }
};
obj.method(1); // { method: f } 1
obj['method'](2); // { method: f } 2

메서드 내부에서의 this 

 this에는 호출한 주체에 대한 정보가 담긴다.

어떤 함수를 메서드로서 호출하는 경우 호출 주제는 바로 함수명 앞의 객체가 된다.

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

obj.methodA();             // this === obj
obj['methodA']();          // this === obj

obj.inner.methodB();       // this === obj.inner
obj.inner['methodB']();    // this === obj.inner
obj['inner'].methodB();    // this === obj.inner
obj['inner']['methodB'](); // this === obj.inner

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

1) 함수 내부에서의 this

어떤 함수를 함수로서 호출할 경우, this는 지정되지 않는다

(호출 주체를 명시하지 않고 개발자가 코드에 직접 관여해서 실행한 것이기 때문에)

실행컨텍스트를 활성화할 당시 this가 지정되지 않은 경우, this는 전역 객체를 의미

→ 함수로서 '독립적으로' 호출할 때는 this는 항상 전역객체를 가리킨다.

 

2) 매서드의 내부함수에서의 this

매서드의 내부라고 해도, 함수로서 호출한다면 this는 전역 객체를 의미

var obj1 = {
	outer: function() {
		console.log(this); // (1)
		var innerFunc = function() {
			console.log(this); // (2), (3)
		}
		innerFunc();

		var obj2 = {
			innerMethod: innerFunc
		};
		obj2.innerMethod();
	}
};
obj1.outer();

위 코드를 실행 결과, (1) = obj1, (2) = 전역객체, (3) = obj2 가 나온다.

(1), (3)은 객체의 메소드로써 호출되었지만, (2)는 함수 자체로써 호출됨.

 

※ thisbinding 관해서는 함수를 실행하는 당시의 주변 환경(메서드 내부인지, 함수 내부인지)는 중요하지 않고, 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지가 관건

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

1) 변수를 활용하는 방법

내부 스코프에 이미 존재하는 this를 별도의 변수(ex : self)에 할당하는 방법

var obj1 = {
	outer: function() {
		console.log(this); // (1) outer

		// AS-IS
		var innerFunc1 = function() {
			console.log(this); // (2) 전역객체
		}
		innerFunc1();

		// TO-BE
		var self = this;
		var innerFunc2 = function() {
			console.log(self); // (3) outer
		};
		innerFunc2();
	}
};

// 메서드 호출 부분
obj1.outer();

2) 화살표 함수(=this를 바인딩하지 않는 함수)

ES6에서 처음 도입된 화살표 함수는, 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 없다.

(따라서 this는 이전의 값 - 상위값- 이 유지)

(ES6에서 함수 내부에서 this가 전역객체를 바라보는 문제 때문에 화살표 함수를 도입)

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

obj.outer();

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

※ Callback 함수 : 어떠한 함수, 메서드의 인자(매개변수)로 넘겨주는 함수

콜백함수 내부의 this는 해당 콜백함수를 넘겨받은 함수(메서드)가 정한 규칙에 따라 값이 결정

콜백 함수도 함수기 때문에 this 전역 객체를 참조하지만, 콜백함수를 넘겨 받은 함수에서 콜백 함수에 별도로 this를 지정한 경우에는 예외적으로 그 대상을 참조하게 되어 있다.

// 별도 지정 없음 : 전역객체
setTimeout(function () { console.log(this) }, 300);

// 별도 지정 없음 : 전역객체
[1, 2, 3, 4, 5].forEach(function(x) {
	console.log(this, x);
});

// addListener 안에서의 this는 항상 호출한 주체의 element를 return하도록 설계되었음
// 따라서 this는 button을 의미함
document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a').addEventListener('click', function(e) {
	console.log(this, e);
});
  1. setTimeout 함수, forEach 메서드는 콜백 함수를 호출할 때 대상이 될 this를 지정하지 않으므로, this는 곧 window객체
  2. addEventListner 메서드는 콜백 함수 호출 시, 자신의 this를 상속하므로, this는 addEventListner의 앞부분(button 태그)

생성자 함수 내부에서의 this

생성자 : 구체적인 인스턴스를 만들기 위한 일종의 틀

여기서의 this는 새로 만들 구체적인 인스턴스 자신이 된다.

(이는 Class의 constructor와 동일하다)

생성자 함수를 호출하면 우선 생성자의 prototype 프로퍼티를 참조하는 __proto__라는 프로퍼티가 있는 객체(인스턴스)를 만들고, 미리 준비된 공통 속성 및 개성을 해당 객체(this)에 부여합니다.

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

var choco = new Cat('초코', 7); //this : choco
var nabi = new Cat('나비', 5);  //this : nabi

(Python, Java와 같은 인스턴트 툴)

명시적 this 바인딩

 - 자동으로 부여되는 상황별 this의 규칙을 깨고, this에 별도의 값을 저장하는 방법

 

1. Call 메서드

  1. 호출 주체인 함수를 즉시 실행하는 명령어다.
  2. call명령어를 사용하여, 첫 번째 매개변수에 this로 binding할 객체를 넣어주면 명시적으로 binding

call 메서드의 첫 번째 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 한다.

var func = function (a, b, c) {
	console.log(this, a, b, c);
};

// no binding
func(1, 2, 3); // Window{ ... } 1 2 3

// 명시적 binding
// func 안에 this에는 {x: 1}이 binding돼요
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

2. Apply 메서드

call 메서드와 완전 동일하지만, this에 binding할 객체는 똑같이 넣어주고 나머지부분만 배열 형태로 넘겨준다.

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.apply({ a: 4 }, [5, 6]); // 4 5 6

3. call/ apply 메서드 활용

1. 유사배열객체(array-like-objet)에 배열 메서드를 적용

출처 : https://kamang-it.tistory.com/entry/JavaScript15유사배열-객체Arraylike-Objects

//객체에는 배열 메서드를 직접 적용할 수 없어요.
//유사배열객체에는 call 또는 apply 메서드를 이용해 배열 메서드를 차용할 수 있어요.
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' ]

함수 내부에서도 접근할 수 있는 arguments 객체도 유사배열객체이므로, 위의 방법을 사용할 수 있다.

유사배열객체는 call/apply 메서드를 이용해 모든 배열 메서드를 적용할 수 있다.

문자열의 경우 length 프로퍼티가 읽기 전용이기 때문에 원본 문자열에 변경을 가하는 메서드는 에러를 던지며, concat처럼 대상이 반드시 배열이어야 하는 경우 에러가 나지 않지만 제대로 된 결과를 얻을 수 없다.

 

2) Array.from 메서드(ES6)

call/apply를 통해 this.binding을 하는 것이 아니라, 객체 → 배열 로의 형 변환 만을 위해서도 쓸 수 있지만, 원래 의도와는 거리가 먼 방법

→ ES6에서는 Array.from이라는 방법 제시

// 유사배열
var obj = {
	0: 'a',
	1: 'b',
	2: 'c',
	length: 3
};

// 객체 -> 배열
var arr = Array.from(obj);

// 찍어보면 배열이 출력됩니다.
console.log(arr);

3) 생성자 내부에서 다른 생성자를 호출(공통된 내용의 반복 제거)

function Person(name, gender) {
	this.name = name;
	this.gender = gender;
}
function Student(name, gender, school) {
	Person.call(this, name, gender); // 여기서 this는 student 인스턴스!
	this.school = school;
}
function Employee(name, gender, company) {
	Person.apply(this, [name, gender]); // 여기서 this는 employee 인스턴스!
	this.company = company;
}
var kd = new Student('길동', 'male', '서울대');
var ks = new Employee('길순', 'female', '삼성');

4) 여러 인수를 묶어 하나의 배열로 전달할 때 apply 사용할 수 있다.

//비효율
var numbers = [10, 20, 3, 16, 45];
var max = min = numbers[0];
numbers.forEach(function(number) {
	// 현재 돌아가는 숫자가 max값 보다 큰 경우
	if (number > max) {
		// max 값을 교체
		max = number;
	}

	// 현재 돌아가는 숫자가 min값 보다 작은 경우
	if (number < min) {
		// min 값을 교체
		min = number;
	}
});

console.log(max, min);

apply를 적용한 경우

//효율
var numbers = [10, 20, 3, 16, 45];
var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);
console.log(max, min);

// 펼치기 연산자(Spread Operation)를 통하면 더 간편하게 해결도 가능해요
const numbers = [10, 20, 3, 16, 45];
const max = Math.max(...numbers);
const min = Math.min(...numbers);
console.log(max min);


4. bind 메서드

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

  1. 함수에 this를 미리 적용
  2. 부분 적용 함수 구현할 때 용이
var func = function (a, b, c, d) {
	console.log(this, a, b, c, d);
};
func(1, 2, 3, 4); // window객체

// 함수에 this 미리 적용
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); // 4와 5를 미리 적용
bindFunc2(6, 7); // { x: 1 } 4 5 6 7
bindFunc2(8, 9); // { x: 1 } 4 5 8 9

- name 프로퍼티

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

var func = function (a, b, c, d) {
	console.log(this, a, b, c, d);
};
var bindFunc = func.bind({ x:1 }, 4, 5);

// func와 bindFunc의 name 프로퍼티의 차이를 살펴보세요!
console.log(func.name); // func
console.log(bindFunc.name); // bound func

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

1) 내부 함수

sefl 등의 변수를 활용한 우회법보다 call, apply, bind를 사용하면 깔끔하게 처리 가능

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

		// call을 이용해서 즉시실행하면서 this를 넘겨주었습니다
		innerFunc.call(this); // obj
	}
};
obj.outer();
var obj = {
	outer: function() {
		console.log(this);
		var innerFunc = function () {
			console.log(this);
		}.bind(this); // innerFunc에 this를 결합한 새로운 함수를 할당
		innerFunc();
	}
};
obj.outer();

2) 콜백함수

var obj = {
	logThis: function () {
		console.log(this);
	},
	logThisLater1: function () {
		// 0.5초를 기다렸다가 출력해요. 정상동작하지 않아요.
		// 콜백함수도 함수이기 때문에 this를 bind해주지 않아서 잃어버렸어요!(유실)
		setTimeout(this.logThis, 500);
	},
	logThisLater2: function () {
		// 1초를 기다렸다가 출력해요. 정상동작해요.
		// 콜백함수에 this를 bind 해주었기 때문이죠.
		setTimeout(this.logThis.bind(this), 1000);
	}
};

obj.logThisLater1(); // Window {..}
obj.logThisLater2(); // obj {logthis: f, ...}

화살표 함수의 예외사항

  1. 화살표 함수는 실행 컨텍스트 생성 시, this를 바인딩하는 과정이 제외된다고 했었죠!
  2. 이 함수 내부에는 this의 할당과정(바인딩 과정)이 아에 없으며, 접근코자 하면 스코프체인상 가장 가까운 this에 접근하게 됨
  3. this우회, call, apply, bind보다 편리한 방법
var obj = {
	outer: function () {
		console.log(this);
		var innerFunc = () => {
			console.log(this);
		};
		innerFunc();
	};
};
obj.outer();

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

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

이러한 메서드의 thisArg값을 지정하면 콜백 함수 내부에서 this값을 원하는 대로 변경할 수 있다.

주로 배열 메서드에 많이 포진되어 있으며, Set과 Map 메서드에도 많이 포진되어 있다.

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(60, 85, 95);
console.log(report.sum, report.count, report.average()); // 240 3 80