본문 바로가기

Programming/Javascript

8. Javascript 함수 호출 (5) - call, apply 메서드와 this 바인딩

javascript image logo

 

지금까지 우리는 Javascript 함수 호출(즉, 함수를 실행하는 것을 의미합니다) 과정에서 다양한 this 바인딩 규칙을 살펴보았습니다. 현재까지 메서드, 함수, new 연산자를 통한 함수 호출 과정에서 this 바인딩이 일어나는 것들은 규칙에 의한 묵시적(implicit)인 바인딩이었습니다. 

 

하지만 지금부터는 어떤 객체를 this에 명시적(explicit)으로 바인딩 하는 방법을 살펴보고자 합니다. this 바인딩을 명시적으로 진행하기 위해서는 call과 apply 메서드를 사용해야 합니다. 이 중apply 메서드를 우선 살펴보도록 하겠습니다. apply 메서드는 기본적으로 모든 함수의 [[Prototype]] 객체인 Function.prototype에 정의된 함수이기 때문에, 모든 함수가 호출할 수 있습니다. 

 

function.apply(thisArg, argArray);

 

기본적인 apply 메서드의 사용방법은 위와 같습니다. 우선, apply(와 call) 메서드의 기본 동작은 function으로 표시된 "해당 함수를 호출한다(실행한다)"입니다. 기본적인 함수를 실행하되, apply를 통해 실행하게 될 경우에는 일종의 옵션이 붙는다고 생각해주시면 됩니다! 간단하죠? 그럼, 어떤 옵션이 붙는 것인지를 살펴봐야 하겠습니다.

 

우선 첫 번째 파라미터인 thisArg를 보겠습니다. 여기에는 객체를 인자로 전달합니다. 어떤 객체일요? function이라는 함수의 선언문 내부에 사용된 this 키워드에 바인딩되는 객체를 의미합니다. 두 번째 파라미터인 argArray를 볼까요? 여기에는 배열을 인자로 전달합니다. 이 배열의 용도는 무엇일까요? 역시 function이라는 함수를 호출(실행)할 때 전달할 인자입니다. 

 

그럼 앞서 설명했었던 생성자 함수를 예로 들어 apply 사용 예시를 살펴보도록 하겠습니다. 

 

// 생성자 함수 Rapper 선언
function Rapper(name, age, lable){
    this.name = name;
    this.age = age;
    this.lable = lable;
}

// new 연산자를 사용해 객체 생성
var loco = new Rapper('LOCO', 33, 'AOMG');
console.dir(loco);

// apply 메서드를 사용해 객체 생성
var lilboi = {};

Rapper.apply(lilboi, ['LIL BOI', 35, 'HALF-TIME']);
console.dir(lilboi);

// 실행결과
// Rapper { name: 'LOCO', age: 33, lable: 'AOMG' }
// { name: 'LIL BOI', age: 35, lable: 'HALF-TIME' }

 

위 실행결과를 살펴봅시다. loco라는 객체와 lilboi라는 객체를 생성했는데요, loco는 익히 알려진 기본적인 new 연산자를 통해 Rapper 생성자 함수를 사용했습니다. 각각의 프로퍼티 name, age, lable가 적용되었죠. 여기에는 Rapper 생성자 함수의 this 바인딩이 정상적으로 진행되었습니다. 

 

하지만 lilboi 변수는 어떨까요? 우선 빈 객체 변수 lilboi를 먼저 선언하고, Rapper를 apply메서드를 통해 실행했습니다. 여기서 첫 번째 파라미터에 인자 값으로 lilboi를 전달했습니다. 여기서 명시적으로 apply를 통해  실행하는 Rapper라는 함수(생성자고 뭐고 상관없습니다) 안에 this가 있으면 여기에는 무조건 lilboi 객체를 바인딩하라고 명시하였습니다. 그리고, 파라미터로 전달할 인자 값을 배열 형태로 지정하였죠. 

 

이 경우 결국 해당 객체에 new 연산자 사용 시와 동일하게 프로퍼티 name, age, lable에 우리가 전달한 인자 값들이 적용되었습니다. 결과적으로는 (거의) 동일한 결과를 가져왔습니다. 

 

하지만 여기서 loco와 lilboi는 중요한 차이점을 하나 갖게 되는데요, loco는 new 키워드를 통해 생성자 함수에서 만들어진 객체이므로 [[Prototype]]으로 Rapper.prototype을 갖게됩니다. 하지만 lilboi 객체는 new를 사용하지 않고 함수를 실행한 경우이므로 [[Prototype]]은 Object.prototype이 됩니다.

 

여기서 한가지 더, apply와 call의 차이는 인자를 넘길 때 형태가 다르다는 점입니다. apply는 해당 함수 호출 시 두 번째 파라미터에 인자들의 배열을 작성하지만 call의 경우 배열 형태가 아니라 인자를 직접 나열하게 됩니다. 

 

Rapper.call(lilboi, 'LIL BOI', 35, 'HALF-TIME');

 

 


 

apply, call로 유사 배열 객체 다루기

우리는 위에서 apply/call 메서드를 통해서 '생성자 객체'를 호출해 사용해 보았습니다. 여기서 생성자 객체가 어떤 결과물을 가져왔는지보다는, 어떤 방식으로 apply/call이 작동했는지가 더 중요하다고 말할 수 있습니다. 그래야 다음 유사 배열 객체를 다루는 방식을 이해할 수 있습니다. 

 

우선, 이번 항목에서의 목표는 유사 배열 객체(array-like objects)에서 배열 표준 메서드를 사용하는 것입니다. 참고로 유사 배열 객체는 여기서 배열 객체는 아니지만, 프로퍼티로 length를 갖고 있는 객체를 의미하는 것으로 정의하겠습니다. 

 

우선 유사 배열 객체에서는 배열 표준 메서드(Array.prototype 메서드)를 사용할 수 없습니다. 기본적으로 배열이 아니기 때문이죠. 굳이 코드로 예를 들어보자면 다음과 같습니다. 

 

var nonArray = {
  length: 3,
  0: 100,
  1: 200,
  2: 300,
};

 

여기서는 유사 배열 객체의 대표적인 예시인 함수의 arguments 프로퍼티를 다루어 볼 예정인데요, 배열 표준 메서드는 slice( )를 사용해보겠습니다. 그렇다면 slice( ) 메서드에 대해서도 살펴봐야겠죠? 기본적으로 slice( )의 정의를 공식 문서에서 찾아보면 다음과 같은 항목이 등장합니다. 

 

The slice() method reads the length property of this. It then reads the integer-keyed properties from start to end and defines them on a newly created array. 
(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice)

 

다행히 우리가 원하는 요소들을 쉽게 확인할 수 있습니다. slice( ) 메서드는 this에 바인딩된 객체의 length 프로퍼티를 읽고 정수 키값에 할당된 프로퍼티를 읽어 들인 다음, 새로운 배열로 리턴한다고 명시되었습니다. 결과물은 생성자 함수를 통해 사용한 것과 완전히 다른 케이스이지만, call/apply 메서드가 동작하는 방식은 똑같습니다. 

 

자, 그럼 이제 유사 배열 객체에서 slice( )를 실행해볼까요? 우선 우리가 함수 아티클에서 배운 'arguments' 객체에 slice( )를 호출해보도록 하겠습니다. arguments 객체는 함수 호출 시 파라미터 값으로 전달받은 인수를 정수 키값으로 프로퍼티를 가지며 그에 따른 length를 가진 대표적인 유사 배열 객체입니다.

 

arguments object
arguments 객체의 구성 형태

 

이제 다음 예제 코드를 확인해 보시기 바랍니다. 함수 myFunc( )를 실행할 때 인자 값을 전달하면, 해당 인자 값들을 별도의 배열로 묶어 출력하도록 선언하고 실행해 본 것입니다. 

 

function myFunc() {
    console.dir(arguments);

    var newArgs = Array.prototype.slice.apply(arguments);
    console.dir(newArgs);
}

myFunc(3, 4, 6, 'a', 'X');

// 출력
// [Arguments] { '0': 3, '1': 4, '2': 6, '3': 'a', '4': 'X' }
// [ 3, 4, 6, 'a', 'X' ]

 

위 예제 코드를 보고 이해가 가지 않을 수 있는 부분들을 위에서 최대한 자세하게 설명했으니, 차분히 읽어보시면 쉽게 이해가 가실 것입니다. 여기서 질문 하나 드리겠습니다. 이 코드에서 등장한 arguments 객체와 newArgs 객체의 [[Prototype]]은 각각 어떻게 될까요? (arguments 객체는 Object.prototype이고, 생성된 newArgs는 Array.prototype이 됩니다)

 

여기서는 apply 메서드를 중심으로 설명했습니다. call 메서드 역시 동작은 동일하니 직접 apply를 call 메서드로 바꾸어 직접 코드를 연습해 보시기 바랍니다.