자바스크립트를 배워보자 21일차 - this

0

Javascript

목록 보기
21/30

this

1. this 키워드

객체는 상태를 나타내는 프로퍼티와 동작을 나타내는 메서드로 구성된다.( 이 둘을 합쳐서 간단하게 프로퍼티라고도 한다.)

메서드는 자신이 속한 객체의 상태, 즉 프로퍼티를 참조하고 변경해야할 때가 있다. 이 때 메서드가 자신이 속한 객체의 프로퍼티를 참조하려면 먼저 자신이 속한 객체를 가리키는 식별자를 참조할 수 있어야 한다.

1.1 객체리터럴 방식으로 생성한 객체

const circle = {
    radius : 5,
    getDiameter(){
        return 2 * circle.radius
    }
}

console.log(circle.getDiameter()) // 10

다음과 같이 circle 객체를 객체리터럴 방식으로 생성하고 getDiameter 메서드를 호출하면 정확한 답이 나오게 된다.

이는, 객체 리터럴 방식으로 생성한 객체의 경우 내부 메서드에서 자신이 속한 객체를 가리키는 식별자를 재귀적으로 참조하기 때문이다.

즉, getDiameter 메서드에 있는 circle은 재귀적으로 자신이 속한 객체가 무엇인지 확인하여 찾은 값이 된다.

그러나 재귀적으로 호출하여 객체를 찾는 방법은 좋은 방법이 아니다.

또한, 객체 리터럴로 생성한 객체이기 때문에 객체의 이름이 명시된다는 특징이 있는데, 만약 생성자 함수로 생성한 객체인 경우는 어떻게 될까??

즉, 객체 리터럴로 생성하면 circle 식별자에 생성된 객체가 할당되고 난 다음이기 때문에 getDiameter의 circle을 찾을 수 있다는 것이다.

그런데 생성자 함수를 사용하면 어떨까??

1.2 생성자 방식으로 생성한 객체

function Circle(radius){
    ?.radius = radius
}

Circle.prototype.getDiameter = function(){
    return 2 * ?.radius
}

const circle1 = new Circle(2)
const circle2 = new Circle(2)

생성자 함수 Circle은 자신을 호출하여 생성할 객체의 식별자를 모르는 상태이다.

즉, circle1 인지 circle2 인지를 모르는 상태라는 것이다.

그렇기 때문에 식별자를 생성자 함수에 넣어줄 수 없다. 어떤 객체(인스턴스)에 값을 넣어주고 변경해주어야 할 지 모르기 때문이다.

이를 위해 자바스크립트는 this라는 특수한 식별자를 사용한다.

  • this

    this는 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수(self referencing variable)이다. this를 통해 자신이 속한 객체 또는 자신이 생성할 인스턴스의 프로퍼티나 메서드를 참조할 수 있다.

this는 자바스크립트 엔진에 의해 암묵적으로 생성되며 코드 어디서든 참조가능하다.

함수를 호출하면 arguments 객체와 this가 암묵적으로 함수 내부에 전달된다. 함수 내부에서 arguments객체를 지역 변수처럼 사용할 수 있는 것처럼 this역시도 마찬가지이다.

this가 가리키는 값, 즉 this 바인딩은 함수 호출 방식에 의해 동적으로 결정된다.

이를 이용하여 위의 예제들을 바꾸어보자

const circle = {
    radius : 5,
    getDiameter(){
        return 2 * this.radius
    }
}

console.log(circle.getDiameter()) // 10

다음과 같이 객체 리터럴에도 적용이 된다. 호출하는 객체, 즉 circle 자체를 가리킨다.

function Circle(radius){
    this.radius = radius
}

Circle.prototype.getDiameter = function(){
    return 2 * this.radius
}

const circle = new Circle(5)
console.log(circle.getDiameter()) // 10

생성자 함수 내부의 this는 생성자 함수가 생성할 인스턴스를 가리킨다.

여기서 중요한 것은 c++,java와 같은 클래스 기반의 언어는 this가 항상 클래스가 생성하는 인스턴스를 가리킨다는 특징이 있다.

그러나 자바스크립트의 this는 함수가 호출되는 방식에 따라 this에 바인딩된 값, 즉 this 바인딩이 동적으로 결정된다.

또한 strict mode 역시 this 바인딩에 영향을 준다.

console.log(this)// window

function square(number){
    console.log(this) // window
    return number * number 
}

console.log(square(2)) // 4

const person = {
    name: 'lee',
    getName(){
        console.log(this) // {name: "lee", getName: ƒ}
        return this.name
    }
}

console.log(person.getName()) // lee

function Person(name){
    this.name = name;
    console.log(this) // Person {name: "lee"}
}

const me = new Person("lee")

크롬의 개발자 도구 console에서 다음의 코드를 넣어서 실행해보면 다양한 this가 나오는 것을 확인할 수 있다.

특히, 함수 선언식으로 만들 함수에서 this는 전역 객체를 호출하고, 생성자 함수의 this는 생성자 함수가 만들 인스턴 this를 일컫는 것이다.

하지만, this는 객체의 프로퍼티나 메서드를 참조하기 위한 자기 참조 변수이므로 일반적으로 객체의 메서드 내부 또는 생성자 함수 내부에서만 의미가 있다.

따라서, strict mode가 적용된 일반 함수 내부 this에는 undefined가 바인딩된다. 일반 함수 내부에서 this를 사용할 필요가 없기 때문이다.

2. 함수 호출 방식과 this 바인딩

this 바인딩(this에 바인딩될 값)은 함수 호출 방식, 즉 함수가 어떻게 호출되었는지에 따라 동적으로 결정된다.

  • 참고
    함수의 상위 스코프를 결정하는 방식인 렉시컬 스코프는 함수 정의가 평가되어 객체가 생성되는 시점에 상위 스코프를 결정한다. 하지만 this 바인딩은 함수 호출 시점에 결정된다.

즉, 스코프는 함수를 정의할 때 결정되지만, this는 함수를 호출할 때 결정된다는 것이다.

주의할 것은 동일한 함수도 다양한 방식으로 호출할 수 있다는 것이다.

함수를 호출하는 방식은 다음과 같이 다양하다.

  1. 일반 함수 호출
  2. 메서드 호출
  3. 생성자 함수 호출
  4. Function.prototype.apply/call/bind 메서드에 의한 간접 호출
const foo = function(){
    console.dir(this)
}
//1. 일반함수 호출 -> 전역 객체를 가리킨다.
foo() // Window

//2. 메서드 호출 -> 메서드를 호출한 객체 obj를 가리킨다.
const obj = { 'name' : "hello", foo }
obj.foo() // obj

//3. 생성자 함수 호출 -> foo 함수를 new 연산자와 함께 생성자 함수로 호출
new foo() // foo {}

//4. Function.prototype.call/apply/bind 메서드에 의한 간접 호출
// 함수 내부의 this는 인수에 의해 결정된다.
const bar = {name : 'bar'}

foo.call(bar) // bar
foo.apply(bar) // bar 
foo.bind(bar)() // bar

위의 1,2,3,4 예제를 정리하면 위와 같다.

위 예제처럼 전역 함수는 물론이고, 중첩 함수를 일반 함수로 호출하면 함수 내부의 this에는 전역 객체가 바인딩된다.

다만 this는 객체의 프로퍼티나 메서드를 참조하기 위한 자기 참조 변수이므로 객체를 생성하지 않는 일반 함수에서 this는 의미가 없다. 따라서, 다음의 에처럼 strict mode가 적용된 일반 함수 내부의 this에는 undefined 가 바인딩된다.

즉 위의 예제에서 use strict를 맨 위에 써놓고, 1번 경우에 해당하는 일반 함수 호출을 보면 this에는 undefined가 나올 것이다.

2.1 중첩된 함수에서의 this

위에서도 말했듯이 중첩된 함수도 일반 함수로 호출되면 바인딩된 객체가 없기 때문에 전역 객체를 this로 갖는다.

크롬 브라우저의 다음의 코드를 입력해보자

var value = 1

const obj = {
    value : 100,
    foo() {
        console.log(this) // {value: 100, foo: ƒ}
        console.log(this.value) // 100

        function bar(){
            console.log(this) // Window {0: global, window: Window, self: Window, document: document, name: "", location: Location, …}
            console.log(this.value) // 1
        }

        bar()
    }
}
obj.foo()

이와 같이 obj.foo()는 호출될 때 obj와 연결이 된다. 따라서 this가 obj로 바인딩되는 것이다.

그러나 bar()은 중첩 함수라 할 지라도 foo() 함수 내부에서 일반 함수로 호출된다. 즉, 바인딩된 obj가 없기 때문에 일반 함수로 호출되는 것이다.

이렇게 되면 bar()에는 묶인 this가 없기 때문에 전역 변수 window가 this로 나오게 된다.

2.2 콜백 함수 호출 시의 this 바인딩

콜백 함수라도 일반 함수로 호출되면 this에는 전역 객체가 바인딩 된다.

var value = 1

const obj = {
    value : 100,
    foo(){
        console.log("this")
        setTimeout(function(){
            console.log(this) // Window {0: global, window: Window, self: Window, document: document, name: "", location: Location, …}
            console.log(this.value) // 1
        }, 100);
    }
}

obj.foo()

이래나 저래나 모든 함수들은 일반 함수로 호출되면 this가 전역 객체로 바인딩되는 것을 확인할 수 있다.

이는 프로그래밍을 어렵게 만들고, 실제로 js로 개발한 사람 중에 이걸로 고생안했다고 하는 사람은 거의 없을 것이다.

그래서 이 문제를 해결해보고 가도록 하자

var value = 1

const obj = {
    value : 100,
    foo() {
        const that = this
        console.log(this) // { value: 100, foo: [Function: foo] }
        console.log(this.value) // 100
    
        function bar(){
            console.log(that) // { value: 100, foo: [Function: foo] }
            console.log(that.value) // 100
        }

        bar()
    }
}
obj.foo()

콜백 함수도 마찬가지로 다음과 같은 해결방법을 이용하면 된다.

foo를 호출하기 위해 obj.foo()를 불렀고, foo에 바인딩된 this는 obj가 된다.

이를 foo 함수의 지역 변수로 저장하고 있고, 중첩 함수는 함수 스코프 체인에 의해 자신이 that을 가지고 있지 않아 상위로 올라간다.

위에서 that이 this를 가지므로 this와 같은 역할로 사용이 가능한 것이다.

위 방법 이외에도 자바스크립트는 this를 명시적으로 바인딩할 수 있는 Function.prototype.apply, Function.prototype.call , Function.prototype.bind 메서드를 제공한다.

이를 이용하여 callback함수에 this를 바인딩해보자

var value = 1

const obj = {
    value : 100,
    foo() {
        setTimeout(function(){
            console.log(this.value) // 100
        }.bind(this), 100)
    }
}
obj.foo()

바로 모든 함수들은 Function.prototype 을 갖고 있으므로 이를 이용하여 bind를 사용하여 this를 바인딩하면 된다.

즉, foo 함수가 가진 this를 바인딩 시켜주는 것이다.

더불어 화살표 함수는 this를 바인딩하지 않는데, bind 함수를 통해 바인딩시켜줄 수 있다.

화살표 함수에 대해서는 뒤에 더 알아보도록 하자

2.3 메서드 호출

메서드 내부의 this에는 메서드를 호출한 객체, 즉 메서드를 호출할 때 메서드 이름 앞의 마침표(.) 연산자 앞에 기술한 객체가 바인딩된다. 주의할 것은 메서드 내부의 this는 메서드를 소유한 객체가 아닌 메서드를 호출한 객체에 바인딩된다는 것이다.

즉, 간단하게 말하면 언제나 this는 호출 시점으로 정해지는 것이지, 소유의 관점이 아니라는 것이다. 렉시컬 스코프의 결정 방식과 헷갈리지 말라는 것이다.

const person = {
    name: 'lee',
    getName(){
        return this.name
    }
}

console.log(person.getName()) // lee

위 예제에서 getName 메서드는 person 객체의 메서드로 정의되었다. 메서드는 프로퍼티에 바인딩된 함수다. 즉, person 객체의 getName 프로퍼티가 가리키는 함수 객체는 person 객체에 포함된 것이 아니라, 독립적으로 존재하는 별도의 객체다.

getName 프로퍼티가 함수 객체를 가리키고 있을 뿐이다.


java나 c++에서는 getName 메서드가 클래스에 소속되기 때문에 따로 분리하여 생각할 수 없다.

그러나, 자바스크립트에서는 함수도 하나의 객체이므로 따로 빼내어 사용할 수 있다.

따라서 getName 프로퍼티가 가리키는 함수 객체, 즉 getName 메서드는 다른 객체의 프로퍼티에 할당하는 것으로 다른 객체의 메서드가 될 수도 있고 일반 변수에 할당하여 일반 함수로 호출될 수도 있다.

name = "hello"

const person = {
    name: 'lee',
    getName(){
        return this.name
    }
}

const anotherPerson = {
    name : 'kim'
}

anotherPerson.getName = person.getName

console.log(anotherPerson.getName()) // kim

const getName = person.getName

console.log(getName()) // hello

즉 포인터로 생각하면 쉽다. getName() 함수 객체를 getName이라는 프로퍼티로 가리키고 있을 뿐이다.

따라서, 위와 같이 따로 getName()함수를 떼어서 호출이 가능한 것이다.

마지막 getName()은 어떠한 객체와도 바인딩되지 않고 일반함수로 불리었기 때문에 전역 객체에 바인딩된다.

맨위에 name은 전역 객체의 프로퍼티이므로, getName() 메서드의 this.name은 'hello' 가 나온다.

다음의 그림처럼, this는 각 객체에 존재하는 것이 아니라, 함수 객체에 존재하는 것이고, 이 this를 호출 시점에 결정하는 것이다.

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

function Person(name){
    this.name = name
}

Person.prototype.getName = function(){
    return this.name
}

const me = new Person('lee')

console.log(me.getName()) // lee

console.log(Person.prototype.getName()) // undefined

Person.prototype.name = "kim"

console.log(Person.prototype.getName()) // kim

위의 코드를 시각화하면 다음과 같다.

결국 이렇게까지해서 말하고 싶은게 뭐냐고한다면, this는 자바나 c++처럼 클래스가 가지는 것이 아니라, 함수가 가지는 것이므로 호출되는 타이밍에 바인딩 된다는 것이다.

2.4 생성자 함수 호출

생성자 함수 내부의 this에는 생성자 함수가 미래에 생성할 인스턴스가 바인딩된다.

function Circle(radius){
    this.radius =radius
    this.getDiameter = function(){
        return 2 * this.radius
    }
}

const circle1 = new Circle(5)
const circle2 = new Circle(10)

console.log(circle1.getDiameter()) // 10
console.log(circle2.getDiameter()) // 20

이전에도 살펴보았듯이 생성자 함수는 일반함수로도 호출이 가능하다. 이 떄에는 this가 전역 객체에 바인딩된다는 것을 명심하자

2.5 Function.prototype.apply / call / bind 메서드에 의한 간접 호출

apply, call , bind 메서드는 Function.prototype의 메서드이다. 즉, 이들 메서드는 모든 함수가 상속받아 사용할 수 있다.

2.5.1 Function.prototype.apply와 call

apply와 call은 본질적인 기능은 함수를 호출하는 것이다. 단지, 호출할 때 인수로 this로 받을 object를 받을 수 있기 때문에 this 바인딩이 가능하다.

function getThisBinding(){
    return this
}

const thisArg = { a: 1}

console.log(getThisBinding()) // window 또는 gloabl

console.log(getThisBinding.apply(thisArg)) // { a: 1 }
console.log(getThisBinding.call(thisArg)) // { a: 1 }

즉, call, apply는 첫번째 인수로 전달한 특정 객체를 호출한 함수의 this에 바인딩한다.

call와 apply는 두 번째에 인자를 넣는 방식만 다를 뿐, 동일하게 동작한다.

function getThisBinding(){
    console.log(arguments)
    return this
}

const thisArg = {a : 1}
console.log(getThisBinding.apply(thisArg, [1,2,3])) // [Arguments] { '0': 1, '1': 2, '2': 3 }
// { a: 1 }

console.log(getThisBinding.call(thisArg, 1,2,3)) // [Arguments] { '0': 1, '1': 2, '2': 3 }
// { a: 1 }

apply는 배열로 묶어서 인자를 호출하는 것을 볼 수 있다.

반면 call은 하나하나 인자를 가변 인자로 호출하는 것을 볼 수있다.

어찌됐든 간에 둘 다 this를 바인딩하여 함수를 호출할 수 있다는 특징이 있다.

2.5.2 Function.prototype.bind

apply와 call과는 달리 함수를 호출하지 않고, this로 사용할 객체만 전달한다.

function getThisBinding(){
    return this
}

const thisArg = {a : 1}
console.log(getThisBinding.bind(thisArg)) // [Function: bound getThisBinding]

console.log(getThisBinding.bind(thisArg)()) //{ a: 1 }

bind는 this로 바인딩할 객체를 넣고 반환값으로 바인딩한 함수 객체를 반환한다.

따라서, [Function: bound getThisBinding]가 반환되는 것이다.

그래서 bind된 함수를 바로 실행하기 위해서는 () 을 호출해주면 된다. 해당 예제가 두 번째 console.log 예제이다.

bind 메서드는 메서드의 this와 메서드 내부의 중첩 함수 또는 콜백 함수의 this가 불일치하는 문제를 해결하기 위해 유용하게 사용된다.

const person = {
    name : 'lee',
    foo(callback){
        setTimeout(callback, 100)
    }
}

person.foo(function(){
    console.log(`hi my name is ${this.name}`) // hi my name is undefined
})

아직까지도 어질어질할 텐데, callback 함수는 foo 함수 내부에서 호출되었으니 this가 person 객체가 아닌가?? 할 수도 있을 것이다.

그러나, 아니다.

foo 함수가 호출되는 시점에서는 분명 this가 person이다. 그러나, callback함수를 호출할 때 person.callback()으로 호출했는가??

그건 아니다. 그렇기 때문에 callback 함수는 일반 함수와 마찬가지로 호출된 것이다.

따라서 callback 함수가 호출될 때, this는 global이나 window 로 바인딩된다.

이에 따라, bind로 foo가 호출될 시점의 this를 사용해주면 된다.

const person = {
    name : 'lee',
    foo(callback){
        console.log(this.name) // lee
        setTimeout(callback.bind(this), 100)
    }
}

person.foo(function(){
    console.log(`hi my name is ${this.name}`) // hi my name is lee
})

다시 한 번 말하겠지만 this는 객체의 프로퍼티나 메서드를 이용하기 위해 사용되므로 함수에서만 유효하다.

또한, 함수 객체 자체에서 this를 가지고 있는 것이지 객체에서 this를 가지고 있는 것이 아니다.

따라서, 객체를 정의하는 시점이 아닌, 함수를 호출하는 시점에서 this가 바인딩되는 것이다.

호출되는 시점은 다음과 같다.

  1. 일반 함수 호출 : this는 전역 객체
  2. 메서드 호출 : this는 메서드를 호출한 객체
  3. 생성자 함수 호출 : this는 생성자 함수가 미래에 생성할 인스턴스
  4. call/apply/bind : this는 메서드에 첫번째 인수로 전달한 객체

더 생성자 함수와 call/apply/bind를 사용하여 this를 바인딩하는 것은 헷갈리지 않을 것이다.

그러나 일반 함수 호출과 메서드 호출은 조금 헷갈릴 것이다.

간단하게 obj.someFunction()으로 불렀을 때 someFunction()은 메서드로 불리었으니 obj의 this가 someFunction() 내부에 this로 바인딩된다.

즉, 앞에 ( . ) 오퍼레이터로 함수를 호출했냐 아니냐로 구분하는 것이 좋다.

아무리 중첩 함수나 콜백 함수라 할 지라도 앞에 ( . ) 오퍼레이터 없이 호출했다면 이는 일반 함수 호출이다.

0개의 댓글