새오의 개발 기록

코어 자바스크립트: 3. this 본문

Javascript/코어 자바스크립트

코어 자바스크립트: 3. this

새오: 2023. 2. 7. 16:52

 

 

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

 

  • 다른 대부분의 객체지향 언어에서 this는 클래스로 생성한 인스턴스 객체를 의미하지만 자바스크립트에서 this는 상황별로 달라짐.
  • 자바스크립트에서 this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정
  • 실행 컨텍스트는 함수를 호출할 때 생성되므로, this는 함수를 호출할 때 결정된다고 할 수 있음
  • 즉 함수를 어떤 방식으로 호출하느냐에 따라 값이 달라짐

 

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

 

  • 전역공간에서 this는 전역 객체를 가리킴
    • 개념상 전역 컨텍스트를 생성하는 주체가 바로 전역 객체이기 때문
    • 전역 객체는 자바스크립트 런타임 환경에 따라 다른 이름과 정보를 가지고 있는데 브라우저 환경에서 전역객체는 window이고 Node.js 환경에서는 global임

 


 

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

 

// 전역변수와 전역객체

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

 

  • 전역공간에서 선언한 변수 a에 1을 할당했을 뿐인데 window.a와 this.a 모두 1이 출력되는데 자바스크립트의 모든 변수는 특정 객체의 프로퍼티로서 동작하기 때문임
    • 여기서 특정 객체란 실행 컨텍스트의 LexicalEnvironment
    • 실행 컨텍스트는 변수를 수집해서 L.E의 프로퍼티로 저장함
  • 즉, 전역변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다.
  • window.a나 this.a 뿐만 아니라 a를 직접 호출할 때도 1이 나오는 까닭은 변수 a에 접근하고자 할 때 스코프 체인에서 a를 검색하다가 가장 마지막에 도달하는 전역 스코프의 L.E, 즉 전역객체에서 해당 프로퍼티 a를 발견해서 그 값을 반환하기 때문
  • 전역 공간에서는 var로 변수를 선언하는 대신 window의 프로퍼티에 직접 할당하더라도 결과적으로 var로 선언한 것과 똑같이 동작할 것이라는 예상 가능함

 


 

  • 삭제의 경우는 다름
var a = 1;
delete window.a; // false
console.log(a, window.a, this.a); // 1 1 1
var b = 2;
delete b; // false
console.log(b, window.b, this.b); // 2 2 2
var c = 3;
delete window.c; // true
console.log(c, window.c, this.c); // uncaught ReferenceError: c is not defined
var d = 4;
delete d; // true
console.log(d, window.d, this.d); // uncaught ReferenceError: d is not defined

 

  • 처음부터 전역객체의 프로퍼티로 할당한 경우에는 삭제가 되는 반면 전역변수로 선언한 경우에는 삭제가 되지 않음
  • 사용자가 의도치 않게 삭제하는 것을 방지하는 차원에서 마련한 방어 전략으로 전역변수를 선언하면 자바스크립트 엔진이 이를 자동으로 전역객체의 프로퍼티로 할당하면서 추가적으로 해당 프로퍼티의 configurable 속성(변경 및 삭제 가능성)을 false로 정의하는 것
  • var로 선언한 전역변수와 전역객체의 프로퍼티는 호이스팅 여부 및 configurable 여부에서 차이를 보임

 

 

 

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

 

함수 vs 메서드

 

  • 함수를 실행하는 방법
    1. 함수로서 호출
    2. 메서드로서 호출

 

  • 함수: 그 자체로 독립적인 기능을 수행
  • 메서드: 자신을 호출한 대상 객체에 관한 동작 수행
  • 어떤 함수를 객체의 프로퍼티에 할당한다고 해서 그 자체로서 무조건 메서드가 되는 것이 아니라 객체의 메서드로서 호출할 경우에만 메서드로 동작하고, 그렇지 않으면 함수로 동작함 
  • 함수 앞에 점(.) 여부 또는 대괄호 표기법으로 '함수로서 호출'과 '메서드로서 호출' 구분

메서드 내부에서의 this

 

  • this에는 호출한 주체에 대한 정보가 담김
  • 어떤 함수를 메서드로서 호출하는 경우 호출 주체는 함수명(프로퍼티명) 앞의 객체
    • ex) obj.method();라면 호출 주체는 obj

 

 

 

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

 

 

함수 내부에서의 this

  • 어떤 함수를 함수로서 호출할 경우에는 this가 지정되지 않으며 실행 컨텍스트를 활성화할 당시에 this가 지정되지 않은 경우 this는 전역 객체를 가리킴

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

 

  • 내부함수에서 이를 함수로서 호출했는지 메서드로 호출했는지만 파악하면 this의 값 알 수 있음
  • 함수를 실행하는 주변 환경(메서드 내부인지, 함수 내부인지 등)은 중요하지 않고, 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 없는지로 판단하면 됨

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

  • 매서드 내부 함수에서 this 구분 명확하지만 this의 직관적인 의미와는 차이 생기기 때문에 호출 주체가 없을 때 자동으로 전역객체를 바인딩하지 않고 호출 당시 주변 환경의 this를 그대로 상속받아 사용하고 싶음 -> 변수 활용하는 방법

 

var obj = {
	outer: function () {
    	console.log(this);
        var innerFunc1 = function () {
        	console.log(this);
        };
        innerFunc1();
        
        var self = this;
        var innerFunc2 = function () {
        	console.log(self);
        };
        innerFunc2();
    }
}
obj.outer();

 

  • innerFunc1 내부에서 this는 전역객체를 가리킴 
  • outer 스코프에서 self라는 변수에 this를 저장한 상태에서 호출한 innerFunc2의 경우 self에 obj가 출력됨
  • 변수명은 꼭 self는 아니어도 됨
  • 상위 스코프의 this를 저장해서 내부함수에서 활용하려는 수단임

this를 바인딩하지 않는 함수

 

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

 

 

 

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

 

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

 

setTimeout(function () { console.log(this); }, 300); // (1)

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

document.body.innerHTML += '<button id="a">클릭</button>'; // (3)
document.body.querySelector('#a').addEventListener('click', function(e) {
	console.log(this, e);
    });

 

  • (1)의 setTimeout 함수와 (2)의 forEach 메서드는 그 내부에서 콜백 함수를 호출할 때 대상이 될 this를 지정하지 않음. 따라서 콜백 함수 내부에서의 this는 전역객체를 참조함
  • (3)의 addEventListner 메서드는 콜백 함수를 호출할 때 자신의 this를 상송하도록 정의됨

 

 

 

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

 

  • 어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 됨

 

// 생성자 함수
var Cat = function(name, age) {
	this.bark = '야옹';
    this.name = name;
    this.age = age;
};

var choco = new Cat('초코', 7);
var nabi = new Cat('나비', 5);
console.log(choco, nabi);

/* 결과
Cat { bark: '야옹', name: '초코', age: 7 }
Cat { bark: '야옹', name: '나비', age: 5 }
*/

 

 

 

 

 

 

 

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

 

상황별 기본적으로 this에 바인딩 되는 규칙을 깨고 별도의 대상을 바인딩 하는 방법도 있음

 

 

2-1. call 메서드

 

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

 

  • 첫 번째 인자를 this로 바인딩
  • 이후의 인자들을 호출할 함수의 매개변수로 사용
  • 호출 주체인 함수를 즉시 실행

 

 

2-2. apply 메서드

 

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

 

  • 첫 번째 인자를 this로 바인딩
  • 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정
  • 호출 주체인 함수를 즉시 실행

 

 

2-3. 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 메서드에 매개변수를 아무것도 넘기지 않을 경우에 원본 배열의 얕은 복사본을 반환함
  • call 메서드를 이용해 원본인 유사배열객체의 얕은 복사를 수행한 것이며 slice 메서드가 배열 메서드이기 때문에 복사본은 배열로 반환하게 됨

 


 

 

  • 함수 내부에서 접근할 수 있는 arguments 객체도 유사배열객체이므로 위의 방법으로 배열로 전환해 활용 가능함
  • qureySelectorAll, getElementByClassName 등 Node 선택자로 선택한 결과인 NodeList도 마찬가지임

 

// call/apply 메서드의 활용

function a () {
	var argv = Array.prototype.slice.call(arguments);
    argv.forEach(function (arg) {
    	console.log(arg);
    });
}
a(1, 2, 3);

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);
});

 

  • 그 밖에도 유사배열객체에는 call/apply 메서드를 이용해 모든 배열 메서드를 적용할 수 있으며 배열처럼 인덱스와 length 프로퍼티를 지니는 문자열에 대해서도 마찬가지임
  • 단 문자열의 경우 length 프로퍼티가 읽기 전용이기 때문에 원본 문자열에 변경을 가하는 메서드(push, pop, shift, unshift, splice 등)는 에러를 던지며, concat 처럼 대상이 반드시 배열이어야 하는 경우에는 에러는 나지 않지만 제대로 된 결과를 얻을 수 없음

 

 


 

 

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

 

var obj = {
	0: 'a',
    1: 'b',
    2: 'c',
    length: 3
};
var arr = Array.from(obj);
console.log(arr); // ['a', 'b', 'c']

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

 

  • 생성자 내부에 다른 생성자와 공통된 내용이 있을 경우 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('보영', 'female', '단국대');
var jn = new Employee('재난', 'male', '구골');

여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 - 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); // 45 3

 

 

spread 연산자 이용해서 apply 적용하는 것보다 간결하게 작성 가능함

 

const numbers = [10, 20, 3, 16,45];
const max = Math.max(...numbers);
const mine = Math.min(...numbers);
console.log(max, min); // 45 3

 

 

 

 

2-4. bind 메서드

 

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

 

  • call 과 비슷하지만 즉시 호출하지 않고 넘겨 받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드
  • 함수에 this를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 지님

 

 

name 프로퍼티

 

  • name 프로퍼티에 동사 bind의 수동태인 'bound'라는 접두어가 붙음
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

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

 

  • self 등의 변수를 활용한 우회법 대신 call, apply 또는 bind 메서드를 이용하면 깔끔하게 사용할 수 있음

 

 

 

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

 

  • 화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외되어 함수 내부에 this가 아예 없으며, 접근하고자 하면 스코프체인상 가장 가까운 this에 접근하게 됨
// 내부함수를 화살표 함수로 변경함

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

 

 

 

 

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

 

  • 요소를 순회하면서 콜백 함수를 반복 호출하는 내용의 일부 메서드는 별도의 인자로 this를 받기도 함
  • 이런 형태는 여러 내부 요소에 대해 같은 동작을 반복 수행해야 하는 배열 메서드에 많이 포진돼 있으며, 같은 이유로 ES6에서 새로 등장한 Set, Map 등의 메서드에도 일부 존재

 

// thisArg를 받는 경우 예시 - forEach 메서드

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

 

// 콜백 함수와 함께 thisArg를 인자로 받는 메서드

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(Callback[, thisArg])
Array.prototype.from(arrayLike[Callback[, thisArg])
Set.prototype.forEach(Callback[, thisArg])
Map.prototype.forEach(Callback[, thisArg])