[코어 자바스크립트] this

변진상·2024년 5월 9일
0

학습 기록

목록 보기
23/31

코어 자바스크립트를 읽고 스터디 세션에서 공유를 위해 정리한 글입니다.

  • 다른 대부분의 객체지향 언어에서 this는 클래스로 생성한 인스턴스 객체를 의미하고 클래스에서만 사용 가능하다. 그러나 JS는 어디서든 사용할 수 있다.
  • 함수와 객체(메서드)의 구분이 느슨한 JS에서 this는 실질적으로 이 둘을 구분하는 거의 유일한 기능이다.
  • this에는 호출한 주체에 대한 정보가 담긴다.

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

  • JS에서 this는 실행 컨텍스트가 생성될 때 함께 결정된다. → 함수를 호출할 때 결정된다.

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

  • 전역 공간에서 this는 전역 객체를 가리킨다. 개념상 전역 컨텍스트를 생성하는 주체가 전역객체이기 때문이다.
    • 브라우저: window, Node.js: global
  • 참고: 전역공간과 변수
    • 전역변수 선언시 JS 엔진은 이를 전역객체의 프로퍼티로도 할당한다.
	var a = 1;
	console.log(a); // 1
	console.log(window.a); // 1
	console.log(this.a); // this === window, 1
  • JS의 모든 변수들은 특정 객체의 프로퍼티로서 동작한다. ⇒ 특정 객체란 실행 컨텍스트의 LexicalEnvironment이다.
  • this.a와 window.a는 전역 객체에 프로퍼티로 할당해 그 값을 보고 있기 때문에 1이 나오는 것을 예상할 수 있다. 그렇다면 a는 왜 1이 나오는 것인가?
    → 현재 활성화 된 실행컨텍스트의 LE를 보고 값이 없으면 스코프 체인을 따라가며 LE를 보게 되는데 결국 스코프 체인의 마지막인 전역 스코프의 LE 즉, 전역객체에서 a프로퍼티를 발견하고 할당된 값을 반환하기 때문이다.
    var a = 1;
    window.b = 2;
    
    console.log(a, this.a, window.a) // 1 1 1
    console.log(b, this.b, window.b) // 2 2 2
    var a = 1;
    delete window.a;
    console.log(a); // 1
    
    var b = 2;
    delete b;
    console.log(b) // 2
    
    window.c = 3;
    delete c;
    console.log(c) // Uncaught RefereceError: c is not defiend
    
    window.d = 3;
    delete window.d;
    console.log(d) // Uncaught RefereceError: d is not defiend
  • JS 엔진이 전역변수를 사용자가 의도치 않게 삭제하는 것을 방지하기 위해 전역변수를 선언하면 자동으로 전역객체의 프로퍼티로 할당하면서 추가적으로 해당 프로퍼티의 configurable 속성(변경 및 삭제 가능성)을 false로 정의한다.
	// 위에서 설명하는 전역변수 할당 시 JS엔진의 동작을 코드로 나타내 보면...
        
	// var a = 1;
	Object.defineProperty(window, "a", { value: "1", configurable: false });
        
	delete window.a;
	console.log(a); // 1

💡 추가: 위 동작에 대한 나의 생각
MDN의 delete 연산자의 정의에 따르면… delete 연산자는 객체의 프로퍼티를 제거하기 위한 연산자이다. 엄밀히 말하면 전역변수는 전역객체의 프로퍼티가 맞지만, JS의 동작을 위해 존재하는 전역객체를 사용자로 부터 은닉하고 전역변수로 기대되는 동작을 보장하기 위해 변수 자체로 추상화 하는 것이라고 생각하면 delete 연산자로 삭제할 수 없음을 이해할 수 있을 것이다.

Delete
The delete operator removes a property from an object. If the property's value is an object and there are no more references to the object, the object held by that property is eventually released automatically.
delete - JavaScript | MDN

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

함수 vs. 메서드

  • 함수와 메서드를 구분할 수 있는 유일한 차이: 독립성
    • 함수: 독립적으로 기능 수행

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

      → JS는 상황별로 this 키워드에 다른 값을 부여함으로써 이를 구현

흔히 메서드를 ‘객체의 프로퍼티에 할당된 함수’로 이해한다. 반은 맞고 반은 틀리다. 어떤 함수를 객체의 프로퍼티에 할당한다고 해서 그 자체로 무조건 메서드가 되는 것이 아니라 객체의 메소드로서 호출할 경우에만 메서드로 동작한다.

var func = function(x){
	console.log(this, x);
}
func(1) // Window { ... } 1
// 함술로서 호출

var obj = {
	method: func
}

obj.method(2) // { method: f } 2
obj['method']
// 메서드로서 호출
// (점 표기법이든 대괄호 표기법이든, 어떤 함수를 호출할 때
// 그 함수 이름 앞에 객체가 명시되어 있는 경우에는 메서드로서 호출.)

메서드 내부에서의 this

  • this에는 호출한 주체에 대한 정보가 담긴다.
  • 메서드로 호출하는 경우 호출 주체: 함수명(프로퍼티 명) 앞의 객체
var obj = {
	methodA: function() {
		console.log(this);
	},
	
	inner: {
		methodB: function() {
			console.log(this);
		}
	}
}

obj.methodA();
obj['methodA'](); // { methodA: f, inner: {...} } (=== obj)

obj.inner.methodB();
obj['inner'].methodB(); // { methodB: f } (=== obj.inner)

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

함수 내부에서의 this

  • 실행 컨텍스트를 활성화할 당시에 this가 지정되지 않은 경우 this는 전역객체를 바라본다고 했다. (더글라스 크락포드는 이를 명백한 설계상의 오류라고 지적했다고 한다.)

메서드 내부함수에서의 this

  • JS 초심자들이 this에 대해 자주 혼란을 느낀다. 앞서 소개한 ‘설계상의 오류’로 인해 실제 동작과 다르게 예측된다.
  • this는 직관적인 느낌 그대로 코드의 실행을 예측하면 예상과 다른 결과가 나온다.

→ 우리는 이미 함수로써 호출한 함수의 this가 무엇을 가리키는지 알고 있다.

내부 함수 역시 함수로서 호출했는지 메서드로서 호출했는지 파악하면 this의 값을 맞출 수 있다.

아래 코드의 결과를 예상해보자.

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

    innerFunc(); // **함수** | 메서드 호출!

    var obj2 = {
      innerMethod: innerFunc
    };

    obj2.innerMethod(); // 함수 | **메서드** 호출!
  }
};

obj.outer(); // 함수 | **메서드** 호출!

this 바인딩에 관해서는 함수를 실행하는 당시의 주변환경(메서드 내외부인지 등)은 중요하지 않고, 오직 해당 함수가 메서드로써, 함수로써 호출 되는지 여부(== 함수 호출 구문 앞 저 혹은 대괄호 표기가 있는지)가 관건이다.

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

  • ES5 기준으로 내부함수에 this를 상속할 방법이 없다. → 우회법 존재. 변수를 활용하는 방법: 변수를 검색하면 가장 가까운 스코프의 LE를 찾고 없으면 상위 스코프를 탐색하듯이, this 역시 현재 컨텍스트에 바인딩된 대상이 없으면 직전 컨텍스트의 this를 바라보도록 함.
    var obj = {
      outer: function () {
        console.log(this); // (1) { outer: f } === obj
        var innerFunc1 = function () {
          console.log(this);  // (2) Window { ... }
        }
        innerFunc1();
    
        var self = this;
        var innerFunc2 = function () {
          console.log(self); // (3) { outer: f }
        };
        innerFunc2();
      }
    };
    
    obj.outer();

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

  • ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제를 보완하고자, 화살표 함수 도입
  • this를 바인딩하는 과정이 빠지게 되어 상위 스코프의 this를 그대로 활용할 수 있다.
var obj = {
  outer: function () {
    console.log(this); // (1) { outer: f }

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

    innerFunc();
  }
}

obj.outer();

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

  • 함수 A의 제어권을 다른 함수(또는 메서드) B에게 넘겨주는 경우 함수 A를 콜백 함수라고 한다.
  • 이때 콜백 함수의 this는 콜백함수를 제어하는 함수인 **함수 B**의 내부 구현에 따라 값이 결정된다.
  • 기본적으로 this 전역객체 참조, 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정할 경우 그 대상을 참조하게 된다.
setTimeout(function () {
  console.log(this); // Window { ... }
}, 300);

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

document.body.innerHTML += "<button id='btn'>클릭</button>";
**document.body.querySelector('#btn')**.addEventListener('click', function (e) { 
  console.log(this, e); // 버튼 엘리먼트 객체
})

결과
결과

addEventListener는 내부적으로 콜백함수의 this를 지정하도록 정의되어있다.

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

  • 프로그래밍적으로 ‘생성자’는 구체적인 인스턴스를 만들기 위한 틀.
  • JS에서는 함수에 생성자로서의 역할을 함께 부여했다. (new 명령어 + 함수 ⇒ 생성자로서 동작)
  • 이 경우 this는 곧 새로 만들 구체적인 인스턴스 자신
var Cat = function (name, age) {
  this.bark = '야옹';
  this.name = name;
  this.age = age;
}

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

console.log(choco, nabi);

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

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

call, apply, bind

3-2-1 call 메서드

Function.prototype.call(**thisArg**[, arg1[, arg2[, ...]]])
  • 메서드를 호출 주체인 함수를 즉시 실행하도록 하는 명령어
  • 첫 번째 인자를 this 바인딩 한다. 이후 인자는 호출 함수의 매개변수로 한다. → 함수를 그냥 선택할 경우 this는 전역객체를 참조하지만 call 메서드를 이용하면 임의 객체를 this로 지정가능
var func = function (a, b, c) {
  console.log(this, a, b, c);
}

func(1, 2, 3); // Window{...} 1 2 3
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: 111 }, 4, 5); // 111 4 5

3-2-2 apply 메서드

Function.prototype.apply(**thisArg**[, argsArray])
  • call 함수와 기능적으로 완벽히 동일
  • 차이: 두번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정
obj.method.apply({ a: 111 }, [222, 333]); // 111 222 333

참고: 3-2-3 call / apply 메서드 활용

var obb = {
  0: "1",
  1: "2",
  2: "3",
  length: 3,
};

Array.prototype.push.call(obb, "33");

console.log(obb); // { 0: '1', 1: '2', 2: '3', 3: '33', length: 4 }

var arr = Array.prototype.slice.call(obb);

console.log(obb); // [ 'a', 'b', 'c', 'd']
function a() {
  var argv = Array.prototype.slice.call(**arguments**);
  // 유사배열객체인 arguments를 배열로 전환해 forEach 메서드 사용.
  
  argv.forEach((ele) => {
    console.log(ele);
  });
}

a(1, 2, 3, 4)

The arguments object - JavaScript | MDN

  • 문자열의 경우 인덱스와 length 프로퍼티에서도 마찬가지. 다만, length 프로퍼티가 문자열의 경우 읽기 전용이기 때문에 변경을 가하는 배열 메서드(push, pop, shift, unshift, slice)는 에러를 던진다.

Array.From

slice를 배열을 복사하기 위한 방법으로 사용하는 것은 경험을 통해 숨은 뜻을 알고 있는 사람이 아닌한 의도파악이 어렵다.
→ ES6에서는 유사 배열객체 또는 순회 가능한 종류의 데이터 타입을 배열로 전환하는 Array.from 메서드 도입

var arr = Array.from(obj);
console.log(arr); // => Array

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

function Person(name, gender) {
  this.name = name;
  this.gender = gender;
}

function Student(name, gender, school) {
  Person.call(this, name, gender);
  // Person의 this를 Student의 인스턴스를 바꿔 상속 구현
  this.school = school;
}

var jin = new Student("otter", 13, "river");

console.log(jin); // { name: 'otter', gender: 13, school: 'river' }

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

var numbers = [1, 2, 3, 4, 5];

var max = Math.max.apply(null, numbers);

console.log(max) // 5

여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 - ES6 spread operator 사용

var numbers = [1, 2, 3, 4, 5];

var max = Math.max(...numbers);

console.log(max) // 5

3-2-4 bind 메서드

ES5에서 추가된 기능으로 call과 비슷하지만 즉시 호출하는 것이 아니라 새로운 함수를 반환한다.

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

sum(1, 2, 3, 4); // Window{ ... } 10

var sumWithTen = sum.bind({ten:10}, 10);

sumWithTen(1, 2, 3); // [ { ten: 10 }, 16 ] at this index.js:26:3

name 프로퍼티

  • bind 함수를 적용해 새로운 함수를 만들면 한가지 독특한 성질이 있다.
  • name 프로퍼티에 bound라는 접두어가 붙는다.(ex. bound xxx → 함수명이 xxx인 함수에 bind 메서드를 이용해 만든 새로운 함수) call이나 apply에 비해 추적하기가 쉽다.
console.log(sum.name, sumWithTen.name); // [ 'sum', 'bound sum' ]

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

var obj = {
	outer: function() {
		var innerFunc = function() {
			console.log(this) // 기본은 전역객체, call을 이용해 실행해 obj를 가리킨다.
		}
		
		innerFunc.call(this);
	}
}

obj.outer();
var obj = {
  logThis: function () {
    console.log(this);
  },

  logThisLater: function () {
    setTimeout(this.logThis.bind(this), 300);
  },
};

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

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

(+ this에는 argument property, prototype에 대한 정보가 없다.)

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

    var innerFunc1 = function () {
      console.log(this);
    };

    innerFunc(); // obj { outer }
    innerFunc1(); // Window { ... }

  },
};

obj.outer();

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

  • 콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체를 인자로 지정할 수 있는 경우가 있다.
  • 배열 메서드에 많이 포진 돼 있고, Set, Map 등의 메서드에도 일부 존재한다.
Array.prototype.forEach(callback[, thisArg]);

3. 정리

  • 묵시적 this 바인딩 규칙
    • 전역 공간의 this ⇒ 전역객체(브라우저 window, Node.js global)
    • 메서드로서 호출한 함수의 this ⇒ 메서드 호출 주체(메서드명 앞의 객체)
    • 함수로서 호출한 함수의 this ⇒ 전역객체(메서드의 내부함수도 마찬가지)
    • 콜백 함수 내부의 this ⇒ 콜백함수의 제어권을 가진 함수가 정의한 바에 따름, 그 외에는 전역객체
    • 생성자 함수에서의 this ⇒ 생성될 인스턴스
  • 명시적 this 바인딩 규칙
    • call, apply 메서드는 명시적으로 this를 지정해 함수, 메서드 호출
    • bind 메서드는 새로운 함수를 만든다.
    • 요소 순회하며 콜백 함수를 호출하는 일부 메서드는 thisArg를 받기도 한다.
profile
자신을 개발하는 개발자!

0개의 댓글