[책스터디] You Don't Know JS Yet - 02

jjee·2025년 10월 16일

스터디

목록 보기
3/4
post-thumbnail

You Don't Know JS Yet - 02

벌써 두번째 스터디!
이번에는 3.3 this 이해하기 부분을 맡게 되었다.

3.3 this 이해하기

this란 무엇인가

JavaScript에서의 this란 무엇인가에 대해 알아보자.

this의 오해

  1. 함수에서 this가 가리키는 것이 자기 자신이다.
  2. this는 메서드가 속한 인스턴스를 참조한다.

⇒ 이는 JS가 아닌 다른 언어에서의 this 작동 방식 때문에 생긴 오해이다.

대표적으로 JAVA에서 this는 인스턴스 자신(self)을 가리키는 참조변수이다.
주로 매개변수와 객체 자신이 가지고 있는 맴버변수명이 같은 경우, 이를 구분하기 위해 사용된다.

스코프와 실행 컨텍스트

JS의 this에 대해 이해하려면 우선 스코프와 실행 컨텍스트를 알아봐야 한다.

  1. 스코프 (정적)
    JS의 함수는 정의되는 시점에 클로저를 통해서 함수를 에워싸는 스코프에 바인딩된다.
    즉, 스코프는 변수 사용 범위로, 함수가 바인딩 되는 시점에 해당 스코프에서 사용할 수 있는 한정된 변수 집합이 결정된다.
  2. 실행 컨텍스트 (동적)
    함수를 호출하는 방식에 따라 실행 컨텍스트가 달라진다.
    실행 컨텍스트는 함수가 실행되는 동안 함수에서 사용할 수 있는 환경 정보를 모아놓은 객체라고 볼 수 있다.

스코프도 객체?
스코프도 객체라 생각할 수 있다.
다만 스코프에서 객체는 JS 엔진 내부에 숨겨져있고 함수 하나당 동일하며,
프로퍼티의 경우 함수 내부에서 사용할 수 있는 식별자(변수) 형태를 띈다는 점에서 차이가 있다.

그렇다면 this는?

함수를 호출할 때마다 결정되는 동적인 특성으로, 실행 컨텍스트에 바인딩된다.

스코프가 지정된 함수는 다른 스코프를 참조할 수 없고 변수를 지정할 수도 없지만
this를 사용하면 동적으로 컨텍스트를 지정할 수 있고 다른 객체에도 해당 함수를 재사용할 수 있어 특정한 작업 환경에서 아주 유용하다.

함수 호출 방식

함수 호출 방식은 크게 4가지가 있다.

var hello = function () {
  console.dir(this);
};

1. 함수 호출

hello(); // window
// window.hello();

2. 메서드 호출

var obj = { hello: hello };
obj.hello(); // obj

3. 생성자 함수 호출

var instance = new hello(); // instance

4. apply/call/bind 호출

var me = { name: 'jin' };
hello.call(me);   // me
hello.apply(me);  // me
hello.bind(me)(); // me

함수 호출에 따른 this 바인딩

위에서 알아본 함수 호출 방법에 따른 각각의 this 바인딩에 대해 알아보자.

1. 함수 호출

1.1 전역 일반 함수

function classroom(teacher) {
	return function study() {
		console.log(`${ teacher } 선생님이 ${ this.topic }을/를 공부하라고 했습니다.`);
	}
}

var assignment = classroom("카일");

assignment();
// undefined의 카일 선생님 등장.
// 카일 선생님이 undefined을/를 공부하라고 했습니다.

아무런 실행 컨텍스트를 제공하지 않고 일반 함수와 같이 호출하면 기본적으로 this전역객체(Global Object)에 바인딩 된다.
(Browser에서는 window 객체, Server에서는 global 객체)

전역 함수는 물론이고, 전역 함수 내에 있는 내부함수
this는 내부함수를 감싸는 외부함수가 아닌 전역 객체에 바인딩된다.

즉, 위 예시에서는 전역(window)에 topic이라는 변수가 없고, 전역 객체에 topic이라는 프로퍼티 역시 없으므로 this.topicundefined가 된다.

1.2 메소드 내부함수

var value = 1;

var obj = {
  value: 100,
  
  // 메소드
  foo: function() { 
    console.log("foo's this: ",  this);  // obj
    console.log("foo's this.value: ",  this.value); // 100
    
    // 메소드 내부함수 bar
    function bar() { 
      console.log("bar's this: ",  this); // window
      console.log("bar's this.value: ", this.value); // 1
    }
    bar();
  }
};

obj.foo();

메소드의 내부함수this전역객체에 바인딩된다.

1.3 콜백 함수

var value = 1;

var obj = {
  value: 100,
  foo: function() { // 콜백함수
    setTimeout(function() {
      console.log("callback's this: ",  this);  // window
      console.log("callback's this.value: ",  this.value); // 1
    }, 100);
  }
};

obj.foo();

콜백 함수의 경우에도 this전역객체에 바인딩된다.

💡내부함수의 this는 무조건 전역객체!
내부함수는 일반 함수, 메소드, 콜백함수 등 어디에서 선언되었든 관계없이 내부함수 자체가 독립적으로 호출되기 때문에 this는 전역객체를 바인딩한다.
더글라스 크락포드는 “이것은 설계 단계의 결함으로 메소드가 내부함수를 사용하여 자신의 작업을 돕게 할 수 없다는 것을 의미한다” 라고 말했다.

내부 함수의 this 전역객체 참조 회피 방법

var value = 1;

var obj = {
  value: 100,
  foo: function() {
    var that = this;  // Workaround : this === obj

    console.log("foo's this: ",  this);  // obj
    console.log("foo's this.value: ",  this.value); // 100.
    
    // 내부함수 bar
    function bar() {
      console.log("bar's this: ",  this); // window
      console.log("bar's this.value: ", this.value); // 1

      console.log("bar's that: ",  that); // obj
      console.log("bar's that.value: ", that.value); // 100
    }
    bar();
  }
};

obj.foo();

메소드의 this 는 해당 객체 자신을 가르킨다.
이 값을 변수(that 또는 self)에 담아 내부 함수에서 참조하여 사용할 수 있다.

최신 문법 화살표 함수

var obj = {
  value: 100,
  foo: function() {
    const bar = () => {
      console.log("bar's this:", this);         // obj
      console.log("bar's this.value:", this.value); // 100
    };
    bar();
  }
};
obj.foo();

ES6부터는 화살표 함수this상위 스코프의 this 를 유지하기 때문에 위의 that = this 를 사용하지 않아도 된다.

2. 메서드 호출

2.1 객체의 프로퍼티인 함수 메소드

function classroom(teacher) {
	return function study() {
		console.log(`${ teacher } 선생님이 ${ this.topic }을/를 공부하라고 했습니다.`);
	}
}

var assignment = classroom("카일");

var homework = {
	topic: "JS",
	assignment: assignment
};
homework.assignment();
// 카일 선생님이 JS을/를 공부하라고 했습니다.

함수가 객체의 프로퍼티 값이면 메소드로 호출된다.
이때 메소드 내부의 this는 해당 메소드를 소유한 객체, 즉 해당 메소드를 호출한 객체에 바인딩된다.

assignment 함수의 복사본을 homework 객체의 프로퍼티로 설정하고 homework.assignment()를 호출하면 this가 homework 객체가 된다.

2.2 프로토타입 객체 메소드

function Classroom(teacher) {
  this.teacher = teacher;
}

Classroom.prototype.study = function() {
  console.log(`${ this.teacher } 선생님이 공부하라고 했습니다.`);
}

var assignment = new Classroom('카일');
assignment.study(); // 카일 선생님이 공부하라고 했습니다.

Classroom.prototype.study = '보라';
Classroom.prototype.study(); // 보라 선생님이 공부하라고 했습니다.

assignment.study(); // 카일 선생님이 공부하라고 했습니다.

프로토타입 객체도 메소드를 가질 수 있다.
프로토타입 객체 메소드 내부에서 사용된 this도 일반 메소드 방식과 마찬가지로 해당 메소드를 호출한 객체에 바인딩된다.

Classroom.prototype에서 호출한 study()Classroom.prototype을,
assignment에서 호출한 study()는 new 연산자를 이용해 생성한 assignment를 바인딩 하여 this를 출력하기 때문에
각각 this.teacher 값이 다른 것을 확인할 수 있다.

3. 생성자 함수 호출

// 생성자 함수
function Person(name) {
  this.name = name;
}

var me = new Person('Lee');
console.log(me); // Person : {name: "Lee"}

// new 연산자와 함께 생성자 함수를 호출하지 않으면 생성자 함수로 동작하지 않는다.
var you = Person('Kim');
console.log(you); // undefined

자바스크립트의 생성자 함수는 말 그대로 객체를 생성하는 역할을 한다.
new 연산자와 함께 생성자 함수를 호출하면 객체를 생성하여 그 객체에 this를 바인딩 한다.

new 연산자를 이용해 객체를 호출하면?

1. 빈 객체 생성 및 this 바인딩
생성자 함수의 코드가 실행되기 전 빈 객체를 만들고, 생성자 함수 내에서 사용되는 this는 이 빈 객체를 가리킨다.
그리고 생성된 빈 객체는 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정한다.

2. this를 통한 프로퍼티 생성
생성된 빈 객체에 this를 사용하여 동적으로 프로퍼티나 메소드를 생성할 수 있다.
this는 새로 생성된 객체를 가리키므로 this를 통해 생성한 프로퍼티와 메소드는 새로 생성된 객체에 추가된다.

3. 생성된 객체 반환

  • 반환문이 없는 경우:
    this에 바인딩된 새로 생성한 객체가 반환된다.
    명시적으로 this를 반환하여도 결과는 같다.
  • 반환문이 this가 아닌 다른 객체를 명시적으로 반환하는 경우:
    this가 아닌 해당 객체가 반환된다.
    이때 this를 반환하지 않은 함수는 생성자 함수로서의 역할을 수행하지 못한다.
    따라서 생성자 함수는 반환문을 명시적으로 사용하지 않는다.
function Person(name) {
  // 생성자 함수 코드 실행 전 -------- 1
  this.name = name;  // --------- 2
  // 생성된 함수 반환 -------------- 3
}

var me = new Person('Lee');
console.log(me.name); // Lee

생성자 함수 new
자바와 같은 객체지향 언어의 생성자 함수와는 다르게 그 형식이 정해져 있는 것이 아니라
기존 함수에 new 연산자를 붙여서 호출하면 해당 함수는 생성자 함수로 동작한다.
즉, 생성자 함수가 아닌 일반 함수에 new 연산자를 붙여 호출하면 생성자 함수처럼 동작할 수 있기 때문에
생성자 함수명은 첫문자를 대문자로 기술하여 혼란을 방지하려는 노력을 한다.

객체 리터럴 방식과 생성자 함수 방식의 차이 객체 리터럴 방식과 생성자 함수 방식의 차이는 [프로토타입 객체([[Prototype]])](https://poiemaweb.com/js-prototype#4-prototype-chain)에 있다.
// 객체 리터럴 방식
var foo = {
  name: 'foo',
  gender: 'male'
}

console.dir(foo);

객체 리터럴 방식의 경우, 생성된 객체의 프로토타입 객체는 Object.prototype이다.

// 생성자 함수 방식
function Person(name, gender) {
  this.name = name;
  this.gender = gender;
}

var me  = new Person('Lee', 'male');
console.dir(me);

var you = new Person('Kim', 'female');
console.dir(you);

생성자 함수 방식의 경우, 생성된 객체의 프로토타입 객체는 Person.prototype이다.

생성자 함수에 new 연산자를 붙이지 않고 호출할 경우 JS에서는 일반함수와 생성자 함수에 특별한 형식적 차이는 없으며 **함수에 new 연산자를 붙여서 호출**하면 해당 함수는 **생성자 함수**로 동작한다.

그러나 객체 생성 목적으로 작성한 생성자 함수를 new 없이 호출하거나 일반함수에 new를 붙여 호출하면 오류가 발생할 수 있다.

일반함수와 생성자 함수의 호출 시 this 바인딩 방식

  • 일반 함수를 호출 ⇒ this는 전역객체에 바인딩
  • new 연산자와 함께 생성자 함수를 호출 ⇒ this는 생성자 함수가 암묵적으로 생성한 빈 객체에 바인딩
function Person(name) {
  // new없이 호출하는 경우, 전역객체에 name 프로퍼티를 추가
  this.name = name;
};

// 일반 함수로서 호출되었기 때문에 객체를 암묵적으로 생성하여 반환하지 않는다.
// 일반 함수의 this는 전역객체를 가리킨다.
var me = Person('Lee');

console.log(me); // undefined
console.log(window.name); // Lee

생성자 함수를 new 없이 호출한 경우

함수 Person 내부의 this는 전역객체를 가리키므로 name은 전역변수(window)에 바인딩된다.

또한 new와 함께 생성자 함수를 호출하는 경우에 암묵적으로 반환하던 this도 반환하지 않으며, 반환문이 없으므로 undefined를 반환하게 된다.

JS는 일반함수와 생성자 함수에 특별한 형식적 차이는 없기 때문에 일반적으로 생성자 함수명은 첫문자를 대문자로 기술하여 구분한다.

그럼에도 불구하고 생길 수 있는 위험성을 회피하기 위해 다음과 같은 패턴(Scope-Safe Constructor)을 사용한다.

// Scope-Safe Constructor Pattern
function A(arg) {
  // 생성자 함수가 new 연산자와 함께 호출되면 함수의 선두에서 빈객체를 생성하고 this에 바인딩한다.

  /*
  this가 호출된 함수(arguments.callee, 본 예제의 경우 A)의 인스턴스가 아니면 new 연산자를 사용하지 않은 것이므로 이 경우 new와 함께 생성자 함수를 호출하여 인스턴스를 반환한다.
  arguments.callee는 호출된 함수의 이름을 나타낸다. 이 예제의 경우 A로 표기하여도 문제없이 동작하지만 특정함수의 이름과 의존성을 없애기 위해서 arguments.callee를 사용하는 것이 좋다.
  */
  if (!(this instanceof arguments.callee)) {
    return new arguments.callee(arg);
  }

  // 프로퍼티 생성과 값의 할당
  this.value = arg ? arg : 0;
}

var a = new A(100);
var b = A(10);

console.log(a.value); // 100
console.log(b.value); // 10

참고로 대부분의 빌트인 생성자(Object, Regex, Array 등)는 new 연산자와 함께 호출되었는지를 확인한 후 적절한 값을 반환한다.

callee는 arguments 객체의 프로퍼티로서 함수 바디 내에서 현재 실행 중인 함수를 참조할 때 사용한다.
다시 말해, 함수 바디 내에서 현재 실행 중인 함수의 이름을 반환한다.

4. apply/call/bind 호출

this에 바인딩될 객체는 함수 호출 패턴에 의해 결정되는 자바스크립트 엔진의 암묵적 this 바인딩 이외에
this를 특정 객체에 명시적으로 바인딩하는 방법으로 아래의 메소드가 제공된다.

  • Function.prototype.apply
  • Function.prototype.call
  • Function.prototype.bind (ES5에 추가)

위 메소드는 모든 함수 객체의 프로토타입 객체인 Function.prototype 객체의 메소드이다.

4.1 apply

function classroom(teacher) {
	return function study() {
		console.log(`${ teacher } 선생님이 ${ this.topic }을/를 공부하라고 했습니다.`);
	}
}

var assignment = classroom("카일");

var otherHomework = {
	topic: "React"
};
// apply 메소드는 생성자함수 classroom을 호출한다. 이때 this에 객체 otherHomework를 바인딩한다.
homework.apply(otherHomework);
// 카일 선생님이 React을/를 공부하라고 했습니다.

apply() 메소드의 첫번째 매개변수에 객체**this에 바인딩 되며,
두번째 매개변수에 argument의 배열을 전달하면
프로퍼티에 매개변수에 할당된 인수**를 할당한다.

위 예시에서는 this에 바인딩된 otherHomework 객체는 topic 프로퍼티를 가지고 있기 때문에, this로 값을 가져올 수 있게된다.

기억하자!!
apply() 메소드를 호출하는 주체는 함수이며
apply() 메소드는 this를 특정 객체에 바인딩할 뿐 본질적인 기능은 함수 호출이다

apply() 메소드의 대표적인 용도
apply() 메소드의 대표적인 용도는 arguments 객체와 같은 유사 배열 객체에 배열 메소드를 사용하는 경우이다.
arguments 객체는 배열이 아니기 때문에 slice() 같은 배열의 메소드를 사용할 수 없으나 apply() 메소드를 이용하면 가능하다.

function convertArgsToArray() {
  console.log(arguments);
  // arguments 객체를 배열로 변환
  // slice: 배열의 특정 부분에 대한 복사본을 생성한다.
  var arr = Array.prototype.slice.apply(arguments); // arguments.slice
  // var arr = [].slice.apply(arguments);
  console.log(arr);
  return arr;
}
convertArgsToArray(1, 2, 3);

Array.prototype.slice.apply(arguments)는 “Array.prototype.slice() 메소드를 호출하라. 단 this는 arguments 객체로 바인딩하라”는 의미가 된다.
결국 Array.prototype.slice() 메소드를 arguments 객체 자신의 메소드인 것처럼 arguments.slice()와 같은 형태로 호출하라는 것이다.

4.2 call

function classroom(teacher) {
	return function study() {
		console.log(`${ teacher } 선생님이 ${ this.topic }을/를 공부하라고 했습니다.`);
	}
}

var assignment = classroom("카일");

var otherHomework = {
	topic: "React"
};
homework.call(otherHomework);
// 카일 선생님이 React을/를 공부하라고 했습니다.

함수를 호출할 때 this가 참조하는 객체를 결정하는 메서드인 call()을 사용해 assignment 함수를 실행하면
this.topic 프로퍼티는 otherHomeworktopic‘React’를 참조한다.

call(), apply()
call() 메소드의 경우, apply()와 기능은 같지만 apply()의 두번째 인자에서 배열 형태로 넘긴 것을 각각 하나의 인자로 넘긴다.

Person.apply(foo, [1, 2, 3]); // 두번째 인자로 배열
Person.call(foo, 1, 2, 3); // 두번째 인자부터 각각 하나의 인자로 넘김

apply()call() 메소드는 콜백 함수의 this를 위해서 사용되기도 한다.

4.3 bind

function classroom(teacher) {
	return function study() {
		console.log(`${ teacher } 선생님이 ${ this.topic }을/를 공부하라고 했습니다.`);
	}
}

var assignment = classroom("카일");

var otherHomework = {
	topic: "React"
};
homework.bind(otherHomework)();
// 카일 선생님이 React을/를 공부하라고 했습니다.

Function.prototype.bind는 함수에 인자로 전달한 this가 바인딩된 새로운 함수를 리턴한다.
bind()apply()call() 메소드와는 다르게 함수를 호출하지 않는다.
그렇기 때문에 함수를 명시적으로 호출(())해야 한다.

Function.prototype.bind

정리

1. 함수 호출

실행 컨텍스트를 제공하지 않은 일반적인 함수 호출내부함수this전역객체 (window)가 바인딩 된다.

2. 메소드 호출

객체의 프로퍼티 또는 프로토타입 객체의 메소드인 경우
메소드 내부의 this해당 메소드를 호출한 객체에 바인딩 된다.

3. 생성자 함수

new 연산자와 함께 생성자 함수를 호출하면
객체를 생성하여 그 객체에 this를 직접 바인딩한다.

4. apply/call/bind 호출

매개변수로 전달된 객체가 this에 바인딩 된다.
bind는 명시적으로 호출(())해줘야 한다.


위에서 보았듯이 JavaScript의 this함수가 호출되는 방식에 따라 달라진다.
함수 내에서 this 를 사용하는 경우, this의 참조를 위해 실행 컨텍스트를 필수로 파악해야 한다.

발표ppt

profile
내가 나에게 알려주는 개발 공부

0개의 댓글