어제의 나보다 성장한 오늘의 나

[자바스크립트] 호출패턴과 this 바인딩 본문

공부/JavaScript && jquery

[자바스크립트] 호출패턴과 this 바인딩

NineOne 2021. 4. 15. 15:18

1. 객체의 메서드 호출할 때 this 바인딩

  • 객체의 프로퍼티가 함수일 경우, 이 함수를 메서드라고 부른다. 
  • 이러한 메서드를 호출할 때 메서드 내부 코드에서 사용된 this는 해당 메서드를 호출한 객체로 바인딩된다.
        // 객체의 메서드에서의 this는 자신을 호출한 객체에 바인딩된다
        var myObject = {
            name:'foo',
            
            sayName:function(){
                console.log(this.name);
            },
        };
        
        var otherObject = {
            name:'bar'
        };
        
        otherObject.sayName = myObject.sayName;
        
        myObject.sayName(); // foo 
        otherObject.sayName(); // bar
  • myObject 객체와 ohterObject 객체는 name 프로퍼티와 sayName() 메서드가 있다.
  • sayName() 메서드는 this.name 값을 출력하는 간단한 함수로서, myObject와 otherObject객체로부터 각각 호출된다.
  • 이때 앞서 설명했던 대로 sayName() 메서드에서 사용된 this는 자신을 호출한 객체에 바인딩된다.

2. 함수를 호출할 때 this 바인딩

자바스크립트에서는 함수를 호출하면, 해당 함수 내부 코드에서 사용된 this는 전역 객체에 바인딩된다.

브라우저에서 자바스크립트를 실행하는 경우 전역 객체는 window 객체가 된다.

var foo = "오늘보다 성장한 오늘의 나"; // 전역변수 선언
console.log(foo);                   // 출력 : 오늘보다 성장한 오늘의 나
console.log(window.foo);            // 출력 : 오늘보다 성장한 오늘의 나

이제 함수를 호출할 때 this 바인딩이 어떻게 되는지 살펴보자.

        var test = 'This is test';
        console.log(window.test); // This is test
        
        // sayFoo() 함수 호출시 this는 전역 객체에 바인딩된다.
        var sayFoo = function(){
            console.log(this.test); // This is test
        };
        
        sayFoo();
  • test라는 전역 변수는 전역 객체 window의 프로퍼티로 접근 가능하다.
  • sayFoo() 함수를 보면 단순히 this.test를 출력하는 함수이다. 함수를 호출할 때 this는 전역 객체에 바인딩된다고 했는데, sayFoo() 함수가 호출된 시점에서 this는 전역 객체인 window에 바인딩된다. 
  • 때문에 this.test는 window.test를 의미한다. 결국 "this is test" 출력

하지만!!! 이러한 함수 호출에서의 this 바인딩 특성은 내부 함수를 호출했을 경우에도 그대로 적용된다.

  • 내부 함수에서 this를 이용할 때는 주의해야 한다.
	// 전역 변수 value 정의
        var value = 100;

        // myObject 객체 생성
        var myObject = {
            value : 1,
            func1 : function () {
                this.value +=1 ;  
                console.log('func1() called, this.value : '+this.value);
                
                // 내부 함수의this는 전역 객체 (window)에 바인딩 된다.
                // func2() 내부함수
                func2 = function () {
                    this.value +=1;
                    console.log('func2() called. this.value : ' + this.value);
                    
                    // func3 내부 함수
                    func3 = function() {
                        this.value += 1;
                        console.log('func3() called. this.value : '+this.value);
                    }
                    func3(); // func3 내부 함수 호출
                }
                func2(); // func2 내부 함수 호출
            }
        };
        myObject.func1(); // func1 내부 함수 호출

  • 이렇게 실행결과가 출력된 이유는 자바스크립트에서는 내부 함수 호출 패턴을 정의해 놓지 않기 때문이다.
  • 내부 함수도 결국 함수 이므로 이를 호출할 때는 함수 호출로 취급된다.
  • 따라서 함수 호출 패턴 규칙에 따라 내부 함수의 this는 전역 객체에 바인딩된다.

 

그렇다면? 내부 함수가 this를 참조하는 자바스크립트의 한계를 극복하려면?

 

  • 부모 함수의 this를 내부 함수가 접근 가능한 다른 변수에 저장하는 방법이 사용된다.
  • 보통 관례상 this 값을 저장하는 변수의 이름을 that이라고 짓는다.
  • 이렇게 되면 내부 함수에서는 that 변수로 부모 함수의 this가 가리키는 객체에 접근할 수 있다.
	// func2()와 func3()내부 함수는 자신을 둘러싼 부모 함수인 func1()의 변수에 접근 가능하므로, 
        // func2()와 func3()도 that변수로 func1()의 this바인딩 된 객체인 myObject에 접근 가능하게 된다.
        var value = 100;
        
        var myObject = {
            value:1,
            func1:function(){
                var that = this;
                this.value += 1;
                console.log('func1() called. this.value: '+this.value);
                func2 = function(){
                    that.value += 1;
                    console.log('func2() called this.value: '+this.value);
                    func3 = function(){
                        that.value += 1;
                        console.log('func3() called this.value: '+that.value);
                    }
                    func3();
                }
                func2();
            }
        }
        myObject.func1();
  • 부모 함수인 func1()의 this 값을 that 변수에 저장했다.
  • 앞서 내부 함수의 특징에서 설명했듯이 func2()와 func3() 내부 함수는 자신을 둘러싼 부모 함수인 func1()의 변수에 접근 가능하다
  • func2()와 func3()도 that 변수로 func1()의 this가 바인딩된 객체인 myObject에 접근 가능하게 된다

3. 생성자 함수를 호출할 때 this 바인딩

객체를 생성하는 방법은 크게 객체 리터럴 방식이나 생성자 함수를 이용하는 두 가지 방법이다.

이번에는 생성자 함수를 이용한 객체 생성 방법을 알아보자!

  • 자바스크립트의 생성자 함수는 말 그대로 자바스크립트의 객체를 생성하는 역할을 한다.
  • 자바와 같은 객체지향 언어에서의 생성자 함수의 형식과는 다르게 그 형식이 정해져 잇는 것이 아니라, 기존 함수에 new 연산자를 붙여서 호출하면 해당 함수는 생성자 함수로 동작한다.
  • 이는 반대로 생각하면 일반 함수에 new를 붙여 호출하면 원치 않는 생성자 함수처럼 동작할 수 있다.
  • 따라서 대부분의 자바스크립트 스타일 가이드에서는 특정 함수가 생성자 함수로 정의되어 있음을 알리려고 함수 이름의 첫 문자를 대문자로 쓰기를 권하고 있다.

 

이러한 생성자 함수를 호출할 때, 생성자 함수 코드 내부에서 this는 메서드와 함수 호출 방식에서의 this 바인딩과 다르게 동작한다. 이를 정확히 이해하려면 생성자 함수가 호출됐을 때 동작하는 방식을 살펴보자!

생성자 함수가 동작하는 방식

  1. 빈 객체 생성 및 this 바인딩
    • 생성자 함수 코드가 실행되기 전빈 객체가 생성된다. 바로 이 객체가 생성자 함수가 새로 생성하는 객체이며, 이 객체는 this로 바인딩된다.
    • 따라서 이후 생성자 함수의 코드 내부에서 사용된 this는 이 빈 객체를 가리킨다.
    • 하지만 여기서 생성된 객체는 엄밀히 말하면 빈 객체가 아니다. 자바스크립트의 모든 객체는 자신의 부모인 프로토타입 객체와 연결되어 있으며, 이를 통해 부모 객체의 프로퍼티나 메서드를 마치 자신의 것처럼 사용할 수 있기 때문이다.
    • 중요! 이렇게 생성자 함수가 생성한 객체는 자신을 생성한 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정한다. (이는 자바스크립트의 규칙이니 잘 기억하자!)
  2. this를 통한 프로퍼티 생성
    • 함수 코드 내부에서 this를 사용해서, 앞에서 생성된 빈 객체에 동적으로 프로퍼티나 메서드를 생성할 수 있다.
  3. 생성된 객체 리턴
    • 리턴 문이 동작하는 방식은 경우에 따라 다르므로 주의해야 한다.
    • 우선 가장 일반적인 경우로 특별하게 리턴 문이 없을 경우! this로 바인딩된 새로 생성한 객체가 리턴된다.
    • 이것은 명시적으로 this를 리턴해도 결과는 같다. (주의! - 생성자 함수가 아닌 일반 함수를 호출할 때 리턴 값이 명시되어 있지 않으면 undefined가 리턴된다.)

위의 내용을 코드를 통해 알아보자!

        // Person() 생성자 함수
        var Person = function(name){
            // 함수 코드 실행 전
            this.name=name;
            // 함수 리턴
        };
        
        // foo 객체 생성
        var foo = new Person('foo');
        console.log(foo.name); // 출력값 : foo

위에 처럼 new로 호출하면, Person()은 생성자 함수로 동작한다.

  1. Person() 함수가 생성자로 호출되면, 함수 코드가 실행되기 전에 빈 객체가 생성된다.
  2. 여기서 생성된 빈 객체는 Person() 생성자 함수의 prototype 프로퍼티가 가리키는 객체(Person.prototype 객체)를 __proto__ 링크로 연결해서 자신의 프로토타입으로 설정한다.
  3. 그리고 이렇게 생성된 객체는 생성자 함수 코드에서 사용되는 this로 바인딩된다.
  4. this가 가리키는 빈 객체에 name이라는 동적 프로퍼티를 생성했다.
  5. 리턴 값이 특별히 없으므로 this로 바인딩한 객체가 생성자 함수의 리턴 값으로 반환돼서, foo 변수에 저장된다.

객체 리터럴 방식과 생성자 함수를 이용해서 객체를 생성하는 방식을 모두 알아봤다! 

그럼 두 가지 방법의 차이점은 무엇일까?

	var foo = {
            name:'foo',
            age:35,
            gender:'man'
        };
        
        console.dir(foo);
        
        // 생성자 함수
        function Person(name,age,gender){
            this.name=name;
            this.age=age;
            this.gender=gender;
        };
        
        // Person 생성자 함수를 이용하여 객체 생성
        var bar = new Person('bar',33,'woman');
        console.dir(bar);
        
        // Person 생성자 함수를 이용하여 객체 생성
        var baz = new Person('baz',25,'woman');
        console.log(baz);

위는 두 가지 객체 생성 방식으로 객체를 만들고 출력하는 예제이다.

  • foo 객체와 같이 객체 리터럴 방식으로 생성된 객체는 같은 형태의 객체를 재생성할 수 없다.
  • Person() 생성자 함수를 사용해서 객체를 생성한다면, 생성자 함수를 호출할 때 다른 인자를 넘김으로써 같은 형태의 서로 다른 객체 bar와 baz를 생성할 수 있다.

그렇다면! console.dir()를 이용한 객체의 출력 결과는 어떨까?

  • 객체 리터럴과 생성자 함수 방식의 차이는 프로토타입 객체(__proto__프로퍼티)에 있다.
  • 객체 리터럴 방식은 Object, 생성자 함수 방식은 Person(실제로는 person.prototype)으로 서로 다르다

이렇게 차이가 발생하는 이유는 자바 스크립트 객체 생성 규칙 때문이다!

  • 객체는 자신을 생성한 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정한다. 

 

이제 생성자 함수를 new를 붙이지 않고 호출할 경우!

  • 일반 함수와 생성자 함수가 별도의 차이가 없다.
  • new를 붙여서 함수를 호출하면 생성자 함수로 동작하는 것이다.
  • 객체 생성을 목적으로 작성한 생성자 함수에 new 없이 호출하거나, 일반 함수를 new를 붙여서 호출할 경우 코드에서 오류가 발생할 수 있다.
  • 그 이유는 일반 함수 호출과 생성자 함수를 호출할 때 this바인딩 방식이 다르기 때문이다.
  • 일반 함수 호출 : this가 window 전역 객체 바인딩
  • 생성자 함수 호출 : this는 새로 생성되는 빈 객체에 바인딩

그렇다면 Person() 생성자 함수를 new 없이 호출을 한다면?

        // new를 붙히지않고 생성자 함수 호출시의 에러
        var qux = Person('qux',20,'man');
        console.log(qux); // undefined
        
        // 생성자 함수
        function Person(name,age,gender){
            this.name=name;
            this.age=age;
            this.gender=gender;
        };
        
        console.log(window.name); // qux
        console.log(window.age); // 20
        console.log(window.gender); // man
  • new 없이 일반 함수 형태로 호출할 경우, this는 함수 호출이므로 전역 객체인 window 객체에 바인딩된다.
  • window 객체에 동적으로 name, age, gender 프로퍼티가 생성된다.
  • 생성자 함수는 별도의 리턴 값이 정해져 있지 않은 경우에 새로 생성된 객체가 리턴된다.
  • 일반 함수를 호출할 때는 undefined가 리턴된다.

일반과 생성자 함수를 구분하기 위해 첫 글자를 대문자로 표기를 해도 사람은 실수를 한다!

그래서 다음과 같은 별도의 코드 패턴을 사용하기도 한다!

	function A(arg){
            if(!(this instanceof A)) return new A(arg);

            this.value = arg ? arg : 0;
        }

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

        console.log(a.value)        // 출력값 100
        console.log(b.value)        // 출력값 100
        console.log(global.value)        // 출력값 undefined
  • this가 A의 인스턴스인지 확인하는 분기문 추가
  • this가 A의 인스턴스가 아니라면, new로 호출된 것이 아님을 의미
  • 어떤 코드에서는 앞과 같이 함수의 이름을 그대로 쓰지 않고 다음과 같이 표현식을 쓰곤 한다.
if(!(this instanceof arguments.callee))
  • arguments.callee가 곧 호출될 함수를 가리킨다.
  • 이와 같이 하면, 특정 함수 이름과 상관없이 이 패턴을 공통으로 사용하는 모듈을 작성할 수 있는 장점이 있다.
  • 대부분의 자바스크립트 라이브러리에 이 패턴이 들어가 있다.

 

 

출처

인사이드 자바스크립트(송형주, 고현준 지음) - 한빛미디어

Comments