4. 함수와 프로토타입 체이닝 #4 함수 호출과 this

2021. 11. 4. 12:10JavaScript/basic

이전글

https://yonghwankim-dev.tistory.com/70?category=962746 

 

4. 함수와 프로토타입 체이닝 #3 함수의 다양한 형태

본 글은 인사이드 자바스크립트 도서의 내용을 복습하기 위해 작성된 글입니다. 4.3 함수의 다양한 형태 콜백 함수(CallBack Function) 즉시 실행 함수(Immediate Function) 내부 함수(Inner Function) 함수를 리.

yonghwankim-dev.tistory.com

 

본 글은 INSIDE JAVASCRIPT 도서의 내용을 복습하기 위해 작성된 글입니다.

 

요약 정리

1. arguments 객체

  • arguments 객체는 함수를 호출할 때 넘긴 인자들이 배열 형태로 저장된 객체. 단, arguments 객체는 유사 배열 객체
  • argumetns 객체 구성
    • 함수를 호출할 때 넘겨진 인자 (배열 형태) : 인덱스는 0번째 부터 시작
    • length 프로퍼티 : 호출할 때 넘겨진 인자의 개수
    • callee 프로퍼티 : 현재 실행중인 함수의 참조값

2. 호출패턴과 this 바인딩

  1. 객체의 메서드 호출할 때 this 바인딩
    • 메서드 내부 코드에서 사용된 this는 해당 메서드를 호출한 객체로 바인딩
  2. 함수를 호출할 때 this 바인딩
    • 함수 내부 코드에서 사용된 this는 전역 객체(window)에 바인딩
  3. 생성자 함수를 호출할 때 this 바인딩
    • this는 생성자 함수가 생성하는 빈 객체에 바인딩
  4. call과 apply 메서드를 이용한 명시적인 this 바인딩
    • this는 call과 apply 메서드가 매개변수로 설정한 객체에 바인딩

3. 함수 리턴

  1. 규칙1) 일반 함수나 메서드는 리턴값을 지정하지 않을 경우, undefined 값이 리턴
  2. 규칙2) 생성자 함수에서 리턴값을 지정하지 않을 경우 생성된 객체가 리턴
    • 생성자 함수에서 명시적으로 객체를 리턴했을 경우 => 명시적으로 지정된 객체나 배열 리턴
    • 생성자 함수에서 명시적으로 기본 타입(boolean, number, string) 값을 리턴했을 경우 => this로 바인딩된 객체가 리턴

 

1. arguments 객체

자바스크립트에서는 함수를 호출 시 함수 형식에 맞춰서 매개변수를 맞추지 않아도 에러가 발생하지 않는다.

 

함수 형식에 맞춰 인자를 넘기지 않더라도 함수 호출이 가능함을 나타냅니다. 함수 형식보다 매개변수를 적게 호출하면 undefined 값이 할당되고, 넘게 호출하면 무시됩니다.

function func(arg1, arg2){
    console.log(arg1,arg2);
}

func();         // undefined undefined
func(1);        // 1 undefined
func(1,2);      // 1 2
func(1,2,3);    // 1 2

 

arguments 객체의 필요성

자바스크립트 함수의 매개변수를 맞추지 않아도 된다는 점때문에 런타임시에 호출된 인자의 갯수를 확인하고 이에 따라 동작을 다르게 주어야 하는 경우가 필요합니다. 이러한 경우에 arguments 객체가 활용됩니다.

 

arguments 객체란 무엇인가?

arguments 객체는 함수를 호출할 때 넘긴 인자들이 배열 형태로 저장된 객체입니다. 단, arguments 객체는 유사 배열 객체이기 때문에 배열 메서드를 사용할 수 없습니다.

 

arguments 객체 확인

function add(a,b){
    // arguments 객체 출력
    console.dir(arguments);
    return a+b;
}

console.log(add(1));        // NaN
console.log(add(1,2));      // 3
console.log(add(1,2,3));    // 3

arguments 객체 구성

  • 함수를 호출할 때 넘겨진 인자(배열 형태) : 함수를 호출할 때 첫번째 인자는 0번 인덱스, 두번째 인자는 1번 인덱스, ...
  • length 프로퍼티 : 호출할 때 넘겨진 인자의 개수를 의미
  • callee 프로퍼티 : 현재 실행 중인 함수의 참조값(위의 예제에서는 add() 함수)

arguments 객체는 유사 배열 객체이므로 배열 메서드를 사용할 수 없습니다.

 

2. 호출패턴과 this 바인딩

자바스크립트에서 함수를 호출할 때 기존 매개변수로 전달되는 인자값에 더해, 위에서 소개한 arguments 객체this 인자가 함수 내부로 암묵적으로 전달됩니다. 

 

this 인자는 자바스크립트의 여러 가지 함수의 호출되는 방식(호출패턴)에 따라 this가 다른 객체를 참조(this 바인딩)합니다.

 

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

메서드란 객체의 프로퍼티가 함수일 경우를 의미합니다. 메서드 내부 코드에서 사용된 this는 해당 메서르를 호출한 객체로 바인딩됩니다.

 

메서드 호출 시 this 바인딩

let myObject = {
    name : "foo",
    sayName : function(){
        // myObject 객체가 호출시 myObject.name으로 치환
        console.log(this.name);
    }
};

// otherObject 객체 생성
let otherObject = {
    name : "bar"
};

// otherObject.sayName() 메서드
otherObject.sayName = myObject.sayName;

// sayName() 메서드 호출
myObject.sayName();     // Expected Output : foo
otherObject.sayName();  // Expected Output : bar

위 코드의 동작 과정을 그림으로 표현하면 아래와 같습니다.

 

2.2 함수를 호출할 때 this 바인딩

자바스크립트에서는 함수(메서드가 아닌)를 호출하면, 해당 함수 내부 코드에서 사용된 this는 전역 객체(window)에 바인딩됩니다.

 

전역 객체란 무엇인가? (브라우저, Node.js)

브라우저 환경에서 자바스크립트를 실행하는 경우, 전역 객체는 window 객체가 됩니다. Node.js와 같은 자바스크립트 언어를 통한 런타임 환경에서의 전역 객체는 global 객체가 됩니다. Node.js는 브라우저 기반 프로그래밍을 넘어 서버 기반 프로그래밍 영역까지 개발이 가능하게끔 해주는 플랫폼입니다.

 

전역 객체와 전역 변수의 관계

var foo = "I'm foo";    // 전역 변수 선언

console.log(foo);           // Expected Output : I'm foo
console.log(window.foo);    // Expected Output : I'm foo

위 코드의 결과로 인하여 모든 전역 변수는 전역 객체의 프로퍼티임을 알 수 있습니다.

 

함수를 호출할 때 this 바인딩 결과

var test = "This is test";
console.log(window.test);   // Expected Output : This is test

var sayFoo = function(){
    console.log(this.test); // sayFoo() 함수 호출시 this는 전역 객체(window)에 바인딩된다.
};

sayFoo();   // Expected Output : This is test

위 코드의 결과로 인하여 sayFoo 함수 내부의 this.test는 window.test로 치환되어 'This is test'를 출력함을 알 수 있습니다. 따라서 함수 내부에서의 this는 전역 객체(window)에 바인딩 됨을 알 수 있습니다. 하지만 이러한 함수 호출에서의 this 바인딩 특성은 내부 함수(Inner Function)를 호출했을 경우에 문제가 발생합니다.

 

내부 함수의 this 바인딩 동작 결과

var value = 100; // 전역 변수 value 정의

var myObject = {
    value : 1,
    func1 : function(){
        this.value += 1;    // 메서드로 호출하였기 때문에 자신을 호출한 객체인 myObject으로 대응된다.
        console.log("func1() called. this.value : " + this.value);      // 2

        // func2() 내부 함수
        func2 = function(){
            this.value += 1;    // 내부 함수이기 때문에 전역 객체(window)로 바인딩된다.
            console.log("func2() called. this.value : " + this.value);  // 101

            // func3() 내부 함수
            func3 = function(){
                this.value += 1;    // 내부 함수이기 때문에 전역 객체(window)로 바인됭된다.
                console.log("func3() called. this.value : " + this.value);  // 102
            }
            func3();
        }
        func2();
    }
}

myObject.func1();

위 코드는 복잡하지만 큰 흐름에서 보면 func1()->func2()->func3()을 호출하는 것을 알 수 있습니다. 그리고 위 코드를 따라 갔을 때 원래 의도한 내부 함수의 this 바인딩은 아래 그림과 같을 것입니다.

위의 그림과 같은 의도로 예상한 출력 결과는 다음과 같이 생각할 것입니다.

func1() called - this.value : 2
func2() called - this.value : 3
func3() called - this.value : 4

하지만 위 코드의 결과는 예상과는 다르게 아래와 같이 출력됩니다.

func1() called - this.value : 2
func2() called - this.value : 101
func3() called - this.value : 102

 

위와 같이 출력되는 이유는 자바스크립트에서는 내부 함수 호출 패턴을 정의해 놓지 않기 때문입니다. 내부 함수도 결국 함수이므로 이를 호출할 때는 함수 호출로 취급됩니다. 따라서 함수 호출 패턴 규칙에 따라 내부 함수의 this는 전역 객체(window)에 바인딩 됩니다. 그러면 실제 내부 함수의 this 바인딩은 아래 그림과 같습니다.

위와 같은 내부 함수가 this를 참조하는 문제를 해결하기 위해서는 부모 함수(위 예제의 경우 func1() 메서드)의 this를 내부 함수가 접근하게 하도록 해야 합니다. 보통 관례상 this 값을 저장하는 변수의 이름을 that이라고 정의합니다. 부모 함수 func1() 메서드 내부에서 변수 that이 myObject 객체를 가리키는 this를 저장한다면 func2(), func3() 내부 함수는 that 변수를 사용하여 myObject 객체를 가리키게 할 수 있을 것입니다.

 

내부 함수의 this 바인딩 문제를 해결

// 내부 함수의 this 바인딩 문제를 해결한 예제 코드
var value2 = 100;

var myObject2 = {
    value2 : 1,
    func1 : function(){
        var that = this;    // myObject 객체를 가리킨다.

        that.value2 +=1;
        console.log("func1() called. this.value : " + that.value2);      // Expected Output : 2

        // func2() 내부 함수
        func2 = function(){
            that.value2 +=1;
            console.log("func2() called. this.value : " + that.value2);  // Expected Output : 3

            // func3() 내부 함수
            func3 = function(){
                that.value2 +=1;
                console.log("func3() called. this.value : " + that.value2);  // Expected Output : 4
            }
            func3();
        }
        func2();
    }
}

myObject2.func1();
func1() called - this.value : 2
func2() called - this.value : 3
func3() called - this.value : 4

위 코드를 실행하면 처음 의도했던 출력 결과를 볼 수 있습니다. 위 코드를 그림으로 표현하면 아래와 같습니다.

 

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

생성자 함수를 호출하면 매개변수로 받은 값들을 저장하기 위해서 빈 객체(Empty Object)를 생성합니다. 생성자 함수를 호출할 때 this는 빈 객체(Empty Object)를 가리킵니다. 이후 생성자 함수 내에서 this를 활용하여 빈 객체는 프로퍼티를 동적으로 생성하고 값을 저장합니다. 생성자 함수의 대표적인 특징은 return문이 없습니다. 따라서 생성자 함수를 종료하면 일반적으로 return문이 없는 경우 this로 바인딩된 새로 생성한 객체가 리턴됩니다.

 

생성자 함수란 무엇인가?

생성자 함수는 자바스크립트에서 객체를 생성하는 역할을 수행합니다. 생성자 함수의 형식은 정해져 있지 않지만 기존 함수에 new 연산자를 붙여서 호출하면 해당 함수는 생성자 함수로 동작합니다. 하지만 일반 함수에 new를 붙여 호출하면 원치 않는 생성자 함수처럼 동작하는 문제가 발생할 수 있습니다. 따라서 대부분의 자바스크립트 가이드에서는 특정 함수가 생성자 함수로 정의되어 있음을 알려고 함수 이름의 첫 문자를 대문자로 쓰기를 권하고 있습니다.

 

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

  1. 빈 객체 생성 및 this 바인딩
    • this는 빈 객체를 가리킴
  2. this를 통한 프로퍼티 생성
  3. 생성된 객체 리턴
    • return 값이 명시적으로 지정된 다른 객체를 가리키는 경우를 제외하면 this가 가리키는 객체(빈 객체)를 반환합니다.
    • 생성자 함수에서 return 값은 일반적으로 없습니다. 생성자 함수가 종료되면 this가 가리키는 객체를 반환합니다.
var Person = function(name){
    // 함수 코드 실행 전
    this.name = name;
    // 함수 리턴
};

// foo 객체 생성
var foo = new Person('foo');
console.log(foo.name);  // Expected Output : foo

위 코드의 수행을 그림으로 표현하면 아래와 같습니다.

 

객체 리터럴 방식과 생성자 함수를 통한 객체 생성 방식의 차이

// 객체 리터럴 방식
var foo = {
	name : 'foo'
};

// 생성자 함수 방식
var bar = new Person('bar');

두 방식의 대표적인 차이점은 foo 객체와 같이 객체 리터럴 방식으로 생성된 객체는 같은 형태의 객체를 재생성할 수 없다는 점입니다. 이에 반해 Person() 생성자 함수를 사용해서 객체를 생성한다면, 생성자 함수를 호출할 때 다른 인자를 넘김으로써 같은 형태의 서로 다른 객체를 생성할 수 있다는 점입니다.

 

그리고 또 다른 차이점은 객체 리터럴 방식과 생성자 함수 방식으로 생성한 객체는 프로토타입(__proto__ 프로퍼티)이 다른 것을 알 수 있습니다. 객체 리터럴 방식의 경우, 자신의 프로토타입 객체가 Object이고 생성자 함수 방식의 경우는 Person인 것을 알 수 있습니다.

 

생성자 함수의 new를 붙이지 않고 호출하는 경우 어떻게 되는가?

객체 생성을 목적으로 작성한 생성자 함수를 new 없이 호출하거나 일반 함수를 new를 붙여서 호출할 경우 코드에서 오류가 발생할 수 있습니다. 그 이유는 일반 함수 호출과 생성자 함수를 호출할 때 this 바인딩 방식이 다르기 때문입니다.

일반 함수 호출의 경우 this가 window 전역 객체에 바인딩 되는 반면, 생성자 함수 호출의 경우 this는 새로 생성되는 빈 객체에 바인딩되기 때문입니다.

 

Person 생성자 함수를 new를 붙이지 않고 호출했을때 결과

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

var qux = Person('qux',20,'man');
console.log(qux);           // Expected Output : undefined

console.log(window.name);   // Expedted Output : qux
console.log(window.age);    // Expedted Output : 20
console.log(window.gender); // Expedted Output : man

위 코드의 실행결과를 보면 Person 생성자 함수를 new없이 호출한 결과 this가 빈 객체에 바인딩되지 않고 window 전역 객체에 바인딩 된 것을 확인할 수 있습니다.

 

강제로 인스턴스 생성하기

위와 같이 생성자 함수 이름의 첫글자를 대문자로 표기해도 불구하고 new를 붙이지 않음으로써 문제가 발생할 수 있습니다. 따라서 이러한 문제를 해결하기 위해서 아래와 같은 패턴을 사용합니다.

function A(arg){
    if(!(this instanceof A)){
        return new A(arg);
    }
    this.value = arg ? arg : 0;
}

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

console.log(a.value);           // Expected Output : 100
console.log(b.value);           // Expected Output : 10
console.log(globalThis.value);  // Expected Output : undefined

만약 위와 같이 A(10)을 호출하면 this는 window를 가리키기 때문에 new를 강제로 붙여서 객체를 생성해서 반환합니다. 하지만 위의 패턴의 문제는 특정 함수 이름을 표기해야 되는 문제가 있습니다. 이를 해결하기 위해서 아래와 같이 변형할 수 있습니다.

function B(arg){
    if(!(this instanceof arguments.callee)){
        return new arguments.callee(arg);
    }
    this.value = arg ? arg : 0;
}

위와같이 arguments 객체의 callee를 활용하면 호출한 함수 이름을 추출하여 검사할 수 있습니다.

 

2.4 call과 apply 메서드를 이용한 명시적인 this 바인딩

지금까지는 자바스크립트에서 객체의 메서드, 함수 호출, 생성자 함수안에서 this는 자동으로 바인딩이 수행되었습니다. 하지만 자바스크립트는 내부적인 this 바인딩 이외에도 this를 특정 객체에 명시적으로 바인딩시키는 방법도 제공하고 있습니다. 그 방법이 Function.prototype 객체의 메서드인 apply()와 call() 메서드입니다. 

 

apply()메서드와 call() 메서드는 this를 특정 객체에게 바인딩시키고 함수를 호출합니다.

 

apply() 메서드 형식

function.apply(thisArg, argArray)

첫번째 인자는 apply() 메서드를 호출한 함수 내부에서 사용한 this에 바인딩할 객체를 가리킵니다. 즉, this는 thisArg 객체를 가리키게 됩니다.

두번째 argArray 인자는 함수를 호출할 때 넘길 인자들의 배열을 가리킵니다.

 

apply() 메서드를 이용한 명시적인 this 바인딩

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

// foo 빈 객체 생성
var foo = {};

// apply() 메서드 호출
Person.apply(foo,['foo',30,'man']);

console.dir(foo);   // Expected Output : Object { age : 30, gender : "man", gender : "man"}

위 예제의 결과를 보면 Person 생성자 함수를 실행하되 this는 foo 객체를 가리키게 합니다. 따라서 생성자 함수 내의 this.name => foo.name 등으로 치환될 수 있습니다.

 

예제로 보는 call() 메서드의 형식

Person.call(foo, 'foo', 30, 'man');

call() 메서드의 경우 apply() 메서드와 기능은 같지만 apply() 메서드의 경우 두번째 인자는 배열 형태로 넘긴 것에 반해 call() 메서드는 각각 하나의 인자로 넘기는 것을 볼수 있습니다. 이는 arguments 객체를 통해서 활용할 수 있습니다.

 

3. 함수 리턴

자바스크립 함수는 항상 리턴값을 반환한다. 특히, return문을 사용하지 않더라도 다음의 규칙으로 항상 리턴값을 전달하게 된다.

 

3.1 규칙1) 일반 함수나 메서드는 리턴값을 지정하지 않을 경우, undefined 값이 리턴된다.

var noReturnFunc = function(){
    console.log("This function has no return statement");
}

var result = noReturnFunc();    // Expected Output : This function has no return statement
console.log(result);            // Expected Output : undefined

 

3.2 규칙2) 생성자 함수에서 리턴값을 지정하지 않을 경우 생성된 객체가 리턴된다.

생성자 함수에서 별도의 리턴값을 지정하지 않을 경우 this로 바인딩된 새로 생성된 객체가 리턴된다. 때문에 일반적으로 생성자 함수에서는 return문을 지정하지 않습니다. 하지만 리턴값을 처리하는 몇가지 예외사항이 존재합니다.

 

생성자 함수에서 명시적으로 객체를 리턴했을 경우

// Person() 생성자 함수
function Person(name, age, gender){
    this.name = name;
    this.age = age;
    this.gender = gender;
    
    // 명시적으로 다른 객체 반환
    return {name : 'bar', age:20, gender:'woman'};
}

var foo = new Person('foo');
console.dir(foo); // Expected Output : Object {age : 20, gender : "woman", name : "bar"}

위 예제의 결과를 보면 생성자 함수에서 명시적으로 객체를 리턴했을 경우 명시적으로 지정된 객체를 반환합니다.

 

생성자 함수에서 명시적으로 기본 타입(불린, 숫자, 문자열) 값을 리턴했을 경우

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

var foo = new Person('foo',30,'man');
console.dir(foo);   // Expected Output : Person {name : "foo", age : 30, gender : "man"}

위 예제의 결과를 확인하면 생성자 함수에서 명시적으로 기본타입 값을 리턴함에도 불구하고 리턴값을 무시하고 this로 바인딩된 객체가 리턴됩니다.

 

References

source code : https://github.com/yonghwankim-dev/javascript_study
INSIDE JAVASCRIPT 한빛미디어, 송형주, 고현준 지음