JavaScript

함수와 프로토타입 체이닝

준준영 2020. 2. 2. 23:29

함수와 프로토타입 체이닝

자바스크립트에서 가장 중요한 개념은 1순위는 당연히 함수입니다. 마치 C 프로그래밍을 배울 때 포인터를 얼마나 제대로 이해하느냐에 따라 고급 C 개발자로 발돋움 할 수 있듯이 자바스크립트에서 이러한 함수를 얼마나 제대로 이해하고 활용하느냐에 따라서 고급 자바스크립트 개발자로 거듭날 수 있습니다.

자바스크립트에서의 함수는 언뜻 보면 언어와 마찬가지로 기능을 제공합니다. 즉, 특정 기능을 제공하는 코드를 작성해서 함수를 정의하고, 이를 호출해서 결과값을 얻는 것처럼 말입니다. 하지만 이러한 기능 외에도 자바스크립트의 함수는 모듈화 처리나 클로저, 객체 생성 등 자바스크립트의 근간이 되는 많은 기능을 제공하고 있습니다.

  • 함수 생성
  • 함수 객체
  • 다양한 함수 형태
  • 함수 호출 this
  • 프로토타입과 프로토타입 체이닝

함수 정의

자바스크립트에서 함수를 생성하는 방법은 3가지가 있습니다. 이들 방식 모두 같은 함수를 생성하지만, 각각의 방식에 따라 함수 동작이 미묘하게 차이가 납니다.

  • 함수 선언문
  • 함수 표현식
  • Function() 생성자 함수

이번 절에서는 두 개의 인자를 받아 그합을 구하는 add() 함수를 3가지 방식으로 정의해보고, 이를 통해 각 방식의 차이점을 살펴보겠습니다. 먼저 그 전에 함수 선언문과 함수 표현식에서 함수를 생성하는 함수 리터럴의 개념부터 알아보겠습니다.

함수 리터럴

자바스크립트에서도 함수는 일반 객체처럼 값으로 취급됩니다. 때문에 객체 리터럴 방식으로 일반 객체를 생성할 수 있는 것처럼, 자바스크립트에서는 함수 리터럴을 이용해 함수를 생성항 수 있습니다. 실제로 함수 선언문이나 함수 표현식 방법 모두 이런 함수 리터럴 방식으로 함수를 생성합니다. 다음 예제는 함수 리터럴로 두 개의 인자를 더하는 add() 함수를 정의한 것입니다.

function add(x, y){
    return x + y;
}

함수 리터럴은 위의 코드와 같이 크게 네 부분으로 구성됩니다.

  1. function 키워드: 자바스크립트 함수 리터럴은 function 키워드로 시작합니다.
  2. 함수명: 함수명은 함수 몸체의 내부 코드에서 자신을 재귀적으로 호출하거나 또는 자바스크립트 디버거가 해당 함수를 구분하는 식별자로 사용합니다. 여기서 주목할 점은 함수명은 선택 사항이라는 것입니다. 이것은 기존 C나 자바에 익숙한 개발자들에겐 생소할 수도 있습니다. 자바스크립트에서 함수명이 없는 함수를 익명 함수라 합니다.
  3. 매개변수 리스트 : 매개변수는 C언어와 같은 기존 언어의 함수 매개변수 형태와 거의 비슷하지만, 매개변수 타입을 기술하지 않는다는 차이가 있습니다.
  4. 함수 몸체 : 실제 함수가 호출됐을 때 실행되는 코드 부분입니다.

함수 선언문 방식으로 함수 생성하기

함수 선언문 방식은 위에서 설명한 함수 리터럴 형태와 같습니다. 여기서 주의할 점은 함수 선언문 방식으로 정의된 함수의 경우는 반드시 함수명이 정의되어 있어야 한다는 것입니다. 일반적으로 함수 선언문 방식은 C/C++에서 사용하는 함수 정의방법과 유사하지만, function이라는 키워드를 명시적으로 사용하고 리턴값과 매개변수로 넘기는 값에 변수 타입(int, char등)을 기술하지 않는다는 점에서 차이가 있습니다.

아래 예제코드는 함수 선언문 형태로 add()함수를 구현한 코드입니다. 함수명 add가 있고, 또한 이 함수명으로 함수를 호출하고 있습니다.

// 함수 선언문 방식
function(x, y){
    return x + y;
}

console.log(add(3,4)); // 출력값 : 7

함수 표현식 방식으로 함수 생성하기

자바스크립트에서는 함수도 하나의 값처럼 취급됩니다. 그래서 자바스크립트에서 함수를 일급 객체라고도 합니다. 따라서 함수도 숫자나 문자열처럼 변수에 할당하는 것이 가능합니다.

이런 방식으로 함수 리터럴로 하나의 함수를 만들고, 여기서 생성된 함수를 변수에 할당하는 함수를 생성하는 것을 함수 표현식이라고 말합니다.

아래 예제는 add()함수를 함수 표현식 형태로 생성한 것입니다. 함수 리터럴로 두 값을 덯하는 함수를 생성한 다음, 이를 add 변수에 저장한 것입니다. 여기서 함수 리터럴로 생성한 함수는 함수명이 없으므로 익명 함수입니다.

// 함수 표현식 방식

var add = function(x, y){
    return x + y;
}

var plus = add;

console.log(add(3,4)); // 출력값 : 7
console.log(plus(5,6)); // 출력값 : 11

에제 코드에서 알 수 있듯이 함수 표현식은 함수 선언문 문법과 거의 유사합니다. 유일한 차이점은 함수 표현식 방법에서는 함수 이름이 선택 사항이며, 보통 사용하지 않습니다.

  1. add 변수는 함수 리터럴로 생성한 함수를 참조하는 변수이지, 예제처럼 함수 이름이 아니라는 것에 주의해야합니다. add와 같이 함수가 할당된 변수를 함수 변수라고 부릅니다.

  2. 함수 변수 add는 함수의 참조값을 가지므로 다른 변수 plus에도 그 값을 그대로 할당할 수가 있습니다.

  3. 함수 표현식으로 생성된 함수를 호출하려면 함수 변수를 사용해야 합니다.

  4. plus 또한 add 함수 변수와 같은 함수를 참조하는 함수 변수이므로 plus(5,6)과 같은 형태로 함수를 호출하는 것이 가능합니다.

add와 plus 함수 변수는 두 개의 인자를 더하는 동일한 익명 함수를 참조합니다.

스크린샷 2020-01-14 오후 11 12 11

위의 예제에서는 함수 변수 add가 실제로 참조하는 두 수를 더하는 함수의 이름이 없습니다. 앞서 설명했듯이 이렇게 이름이 없는 함수 형태를 자바스크립트에서는 익명 함수라고 부릅니다.
즉, 앞 예제는 인자로 넘겨진 두 수를 더하는 익명 함수를 만들고 이를 add 변수에 할당한 것입니다. 이것이 바로 익명 함수를 이용한 함수 표현식 방법입니다. 이러한 익명 함수의 호출은 앞 에제와 같이 함수 변수에 함수 호출 연산자인 ()를 붙여서 기술하는 것으로 가능합니다.

참고로 함수 이름이 포함된 함수 표현식을 기명 함수 표현식이라고 합니다. 이러한 기명 함수 표현식을 사용할 경우 주의해야 할 점이 있습니다.

var add = function sum(xm y){
    return x + y;
}

console.log(add(3,4)); // 출력값 : 7
console.log(sum(3,4)); // 출력값 : Uncaught RefereceError: sum is not defined 에러 발생

sum() 함수를 정의하고, 이 함수를 add 함수 변수에 할당했습니다. 예제에서 특이한 점은 add() 함수를 호출는 결과값이 성공적으로 리턴된 반면에, sum() 함수 호출의 경우 에러가 발생한다는 것입니다. 이것은 함수 표현식에서 사용된 함수 이름이 외부 코드에서 접근 불가능 하기 때문입니다.

실제로 함수 표현식에 사용된 함수 이름은 정의된 함수 내부에서 해당 함수를 재귀적으로 호출하거나, 디버거 등에서 함수를 구분할 때 사용됩니다. 따라서 함수 이름으로 사용된 sum으로 함수 외부에서 해당 함수를 호출할 때 sum() 함수가 정의되어 있지 않다는 에러가 발생합니다.

그렇다면 함수 선언문으로 정의할 때 add() 함수는 어떻게 함수 이름으로 함수 외부에서 호출이 가능할까요? 함수 선언문 형식으로 정의된 add() 함수는 자바스크립트 엔진에 의해 다음과 같은 함수 표현식 상태로 변경되기 때문입니다.

var add = function(x, y){
    return x + y;
}

함수 이름과 함수 변수 이름이 add로 같으므로, 함수 이름으로 함수가 호출되는 것처럼 보이지만 실제로 add 함수 변수로 함수 외부에서 호출이 가능하게 된 것입니다.
이를 그림으로 표현하면 다음과 같습니다.

스크린샷 2020-01-14 오후 11 24 24

앞서 설명했듯이 함수 표현식에서는 함수 이름이 선택 사항이지만, 이러한 함수 이름을 이용하면 함수 코드 내부에서 함수 이름으로 함수의 재귀적인 호출 처리가 가능합니다.

var factorialVar = function factorial(n) {

    if (n <= 1){
        return 1;
    }
    return n * factorial(n-1);
}

console.log(factorialVar(3)); // 출력값 : 6
console.log(factorial(3)); // Uncaught ReferenceError: factorial is not defined
  1. 함수 외부에서는 함수 변수 factorialVar로 함수를 호출하였으며, 함수 내부에서 이뤄지는 재귀 호출은 factorial() 함수 이름으로 처리한다는 것을 알 수 있습니다.

  2. 앞서 설명한 것과 마찬가지로 함수명 factorial()으로 함수 외부에서 해당 함수를 호출하지 못해 에러가 발생합니다.

Funtion() 생성자 함수를 통한 함수 생성하기

이 정의 뒷부분에 설명하겠지만, 자바스크립트의 함수도 Function()이라는 기본 내장 생성자 함수로부터 생성된 객체라고 볼 수 있습니다. 앞에서 설명한 함수 선언문이나 함수 표현식 방식도 Function() 생성자 함수가 아닌 함수 리터럴 방식으로 함수를 생성하지만, 결국엔 이 또한 내부적으로 Function() 생성자 함수로 함수를 생성하는 문법은 다음과 같습니다.

new Function(arg1, arg2, ..., argN, functionBody)
  • arg1, arg2, argN - 함수의 매개변수
  • functionBody - 함수가 호출될 때 실행할 코드를 포함한 문자열

이 방식을 사용해서 작성한 add 함수는 다음과 같습니다.

var add = new Function('x', 'y', 'return x + y');
console.log(add(3.4)); // 출력값 : 7

하지만 일반적으로 Function() 생성자 함수를 사용한 함수 생성 방법은 자주 사용되지 않으므로 본서에서는 더 이상 다루지 않겠습니다. 이러한 문법은 실제 사용하기보다는 다른 사람이 작성한 소스를 분석할 때 나오는 경우가 있으므로 상식 수준으로 알아두도록 하겠습니다.

함수 호이스팅

지금까지 자바스크립트에서 함수를 생성하는 3가지 방법을 살펴봤습니다. 코드는 약간씩 다르지만 서로 모두 같은 기능의 함수를 생성함을 확인할 수 있습니다. 하지만 이들 사이에는 동작 방식의 약간 차이가 있습니다. 그중의 하나가 바로 함수 호이스팅 입니다.

자바스크립트 Guru로 알려진 더글라스 크락포드는 함수 생성에 있어서 그의 저서 더글라스 크락포드의 자바스크립트 핵심 가이드에서 함수 표현식만을 사용할 것을 권하고 있습니다. 그 이유 중의 하나가 바로 함수 호이스팅 때문입니다. 다음 예제를 살펴보겠습니다.

add(2,3); // 출력값 : 5


function add(x, y){
    return x + y;
}

add(3,4); // 출력값 : 7 

위의 add() 함수 호출 부분은 함수가 정의되지 않았음에도 정의된 add()함수를 호출하는 것이 가능합니다. 이것은 함수가 자신이 위치한 코드에 상관없이 함수 선언문 형태로 정의한 함수의 유효범위는 코드의 맨 처음부터 시작한다는 것을 확인할 수 있습니다. 이것을 함수 호이팅이라고 부릅니다.

더글라스 크락포드는 이러한 함수 호이스팅은 함수를 사용하기 전에 반드시 선언해야한다는 규칙을 무시하므로 코드의 구조를 엉성하게 만들 수도 있다고 지적하였습니다. 함수 표현식을 권장하고 있습니다. 아래 예제를 보면 함수 표현식 형태로 add() 함수를 정의하면 실행결과는 다르게 됩니다.

add(2,3); // uncaught type error

// 함수 표현식 형태로 add() 함수 정의
var add = function(x, y){
    return x + y;
} 

add(3, 4); // 7

add() 함수는 함수 표현식 형태로 정의되어 있어 호이스팅이 일어나지 않습니다. 따라서 맨 아래와 같이 함수가 생성된 이후에 호출이 가능합니다. 맨 위에서 add() 함수를 호출한 시점에서 아직 add() 함수가 생성되기 전이므로 uncaught type error 에러가 발생합니다.

이러한 함수 호이스팅이 발생하는 원인은 자바스크립트의 변수 생성과 초기화의 작업이 분리돼서 진행되기 때문입니다.

함수 객체: 함수도 객체다

자바스크립트에서는 함수도 객체입니다. 즉, 함수의 기본 기능인 코드 실행뿐만 아니라, 함수 자체가 일반 객체처럼 프로퍼티들을 가질 수 있다는 것입니다. 아래 예제 코드를 보겠습니다.

// 함수 선언 방식으로 add() 함수 정의
function add(x, y){
    return x + y;
}

// add() 함수 객체에 result, status 프로퍼티 추가
add.result = add(3, 2);
add.status = 'OK';

console.log(add.result); // 출력값: 5
console.log(add.status); // 출력값: 'OK'

위의 코드는 add() 함수가 일반 객체처럼 result, status 프로퍼티를 추가하는게 가능하다는 것을 보여줍니다.

add() 함수를 생성할 때 함수 코드는 함수 객체의 [[Code]] 내부 프로퍼티에 자동으로 저장됩니다. add() 함수 객체의 status 프로퍼티도 일반 객체에서의 접근 방식처럼 add.status를 이용해 접근 가능합니다.

변수나 프로퍼티의 값으로 할당

함수는 숫자나 문자열처럼 변수나 프로퍼티의 값으로 할당될 수 있습니다. 아래 예제를 살펴보겠습니다.

// 변수에 함수 할당
var foo = 100;
var bar = function () {return 100; };
console.log(bar()); // 출력값: 100

// 프로퍼티에 함수 할당
var obj= {};
obj.baz = function () {return 200; };
console.log(obj.baz()); // 출력값: 200

함수 인자로 전달

함수는 다른 함수의 인자로도 전달이 가능합니다. 다음 예제를 보겠습니다. foo()는 함수 표현식 방법으로 생성한 함수로서, 인자로 받은 func 함수를 내부에서 함수 호출 연산자를() 붙여 호출하는 기능을 합니다.

// 함수 표현식으로 foo() 함수 생성
var foo = function(func){
    func();
};

// foo() 함수 실행
foo(function(){
    console.log('Function can be used as the argument.');
});

foo() 함수를 호출할 때, 함수 리터럴 방식으로 생성한 익명 함수 func 인자로 넘겼습니다. 따라서 foo() 함수 내부에서는 func 매개변수로 인자에 넘겨진 함수를 호출할 수 있습니다. 출력결과를 보면 알 수 있듯이 인자로 넘긴 익명 함수가 foo() 함수 내부에서 제대로 호출된 것을 알 수 있습니다.

리턴값으로 활용

함수는 다른 함수의 리턴값으로도 활용할 수 있습니다. 다음 예제에서 foo() 함수는 console.log()를 이용해 출력하는 간단한 익명 함수를 리턴하는 역할을 합니다. 이것이 가능한 이유 또한 함수 자체가 값으로 취급되기 때문입니다.

var foo = function(){
    return function(){
        console.log('this function is the return value.')
    };
};

var bar = foo();
bar();
// 출력결과: this function is the return value.

foo() 함수가 호출되면, 리턴값으로 전달되는 함수가 bar 변수에 저장됩니다.
() 함수 호출 연산자를 이용해 bar()로 리턴된 함수를 실행하는 것이 가능합니다.

함수 객체의 기본 프로퍼티

앞에서 강조했듯이 자바스크립트에서는 함수 역시 객체입니다. 이것은 함수 역시 일반적인 객체의 기능에 추가로 호출됐을 때 정의된 코드를 실행하는 기능을 가지고 있다는 것입니다. 또한, 일반 객체와는 다르게 추가로 함수 객체만의 표준 프로퍼티가 정의되어 있습니다.

참고로 모든 함수들의 부모 객체는 Function Prototype 객체 입니다.
그런데 ECMAScript 명세서에는 Function Prototype은 함수라고 정의되어 있습니다. 그렇다면 이러한 규칙에 의해 Function Prototype은 함수 객체도 결국 함수이므로 Function Prototype 객체, 즉, 자기 자신을 부모가 갖는 것일까요? ECMAScript 명세서에는 예외적으로 Function Prototype 함수 객체의 부모는 자바스크립트의 모든 객체의 조상격인 Object.Prototype 객체라고 설명하고 있습니다. 때문에 Function Prototype 객체의 proto 프로퍼티는 Object.Prototype 객체를 가르키고 있는 것입니다.

length 프로퍼티

함수 객체의 length 프로퍼티는 앞서 설명했듯이 ECMAScript에서 정한 모든 함수가 가져야 하는 표준 프로퍼티로서, 함수가 정상적으로 실행될 대 기대되는 인자의 개수를 나타냅니다. 아래 코드를 보면 함수 객체의 length 프로퍼티를 이해할 수 있을 것입니다.

예제 코드

스크린샷 2020-02-02 오후 8 15 31

위의 코드를 보면 인자 개수가 서로 다른 함수들로 구성되어 있습니다. 출력값을 살펴보면 함수 객체의 length 프로퍼티는 함수를 작성할 때 인자 개수를 나타내고 있음을 확인할 수 있습니다.

prtotype 프로퍼티

모든 함수는 객체로서 prototype 프로퍼티를 가지고 있습니다. 여기서 주의할 것은 함수 객체의 prototype 프로퍼티는 앞서 설명한 모든 객체의 부모를 나타내는 내부 프로퍼티인 [[Prototype]]과 혼동하지 말아야 합니다.

prototype 프로퍼티와 [[Prototype]] 프로퍼티 모두 프로토타입 객체를 가르킨다는 점에서 공통점이 있지만, 관점에서 차이가 있습니다. 모든 객체에 있는 내부 프로퍼티인 [[Prototype]]는 객체 입장에서 자신의 부모 역할을 하는 프로토타입 객체를 가르키는 반면에, 함수 객체가 가지고 있는 prototype 프로퍼티는 이 함수가 생성자로 사용될 때 이 함수를 통해 생성된 객체의 부모 역할을 하는 프로토타입 객체를 가르킵니다.

prototype 프로퍼티는 함수가 생성될 때 만들어지며, 다음 그림과 같이 단지 constructor 프로퍼티 하나만 있는 객체를 가르킵니다. 그리고 prototype 프로퍼티가 가르키는 프로토타입 객체의 유일한 constructor 프로퍼티는 자신과 연결된 함수를 가르킵니다. 즉 , 자바스크립트에서는 함수를 생성할 때, 함수 자신과 연결된 프로토타입 객체를 동시에 생성하며, 이 둘은 아래 그림처럼 각각 prototype과 constructor라는 프로퍼티로 서로를 참조하게 됩니다.

Untitled Diagram (1)

프로토타입 객체 네이밍은 함수의 prototype 프로퍼티가 가르키는 프로토타입 객체는 일반적으로 따로 네이밍하지 않고, 자신과 연결된 함수의 prtotype 프로퍼티값을 그대로 이용합니다. 가령 add() 함수의 프로토타입 객체는 add.prototype이 됩니다.

// MyFunction() 함수 정의
function myFunction(){
    return true;
}

console.dir(myFunction.prtotype);
console.dir(myFunction.prtotype.constructor);

실행 결과

스크린샷 2020-02-02 오후 8 34 22

실행결과를 보면 알 수 있듯이 myFunction.prototype 객체는 constructor와 proto 라는 두 개의 프로퍼티가 있습니다. 이 객체는 myFunction() 함수의 프로토타입 객체이므로 constructor 프로퍼티가 있음을 확인할 수 있습니다.

또한, 프로토타입 객체 역시 자바스크립트 객체이므로 예외 없이 자신의 부모 역할을 하는 proto 프로퍼티가 있습니다.

myFunction.prototype.constructor의 값을 출력함으로써 프로토타입 객체와 매핑된 함수를 알아볼 수 있습니다. 결과값을 보면 myFunction() 함수를 가르키고 있습니다.

함수의 다양한 형태

콜백 함수

자바스크립트 함수 표현식에서 함수 이름은 꼭 붙이지 않아도 되는 선택 사항입니다. 즉, 함수의 이름을 지정하지 않아도 함수가 정의되며 이러한 함수가 익명 함수라고 합니다. 이러한 익명 함수의 대표적인 용도가 콜백함수입니다. 콜백 함수는 코드를 통해 명시적으로 호출하는 함수가 아니라, 개발자는 단지 함수를 등록하기만 하고, 어떤 이벤트가 발생했거나 특정 시점에 도달했을 때 시스템에서 호출되는 함수를 말합니다. 또한, 특정 함수의 인자로 넘겨서, 코드 내부에서 호출되는 함수 또한 콜백 함수가 될 수 있습니다.
대표적인 콜백 함수의 사용 예가 자바스크립트에서의 이벤트 핸들러 처리입니다. 웹 페이지가 로드되거나 키보드가 입력되는 등의 DOM 이벤트가 발생할 경우, 브라우저는 정으된 DOM 이벤트에 해당하는 이벤트 핸들러를 실행시킵니다. 만약 이러한 이벤트 핸들러에 콜백 함수가 등록했디먄. 콜백 함수는 이벤트가 발생할 때마다 브라우저에 의해 실행되게 됩니다.

Untitled Diagram

다음 코드를 살펴보겠습니다. 웹 페이지가 로드됐을 때 경고창을 띄워 주는 간단한 예제입니다. window.onload는 이벤트 핸들러로서, 웹 페이지의 로딩이 끝나는 시점에 load 이벤트가 발생하면 실행 됩니다. 예제에서 window.onload 이벤트 핸들러를 익명 함수로 연결했습니다. 따라서 익명 함수가 콜백 함수로 등록된 것입니다.

<!DOCTYPE html>
<html><body>
    <script>
        // 페이지 로드 시 호출될 콜백 함수
        window.onload = function() {
            alert('This is the callback function.');
        };
    </script>
</body></html>

즉시 실행 함수

함수를 정의함과 동시에 바로 실행하는 함수를 즉시 실행 함수라고 합니다. 이 함수도 익명 함수를 응용한 형태입니다. 우선 다음 예제 코드를 살펴보겠습니다. 익명 함수를 정의함과 동시에 결과가 출력됩니다.

(function(name){
    console.log('this is the immediate function -> ' + name);
})('foo');

// 출력결과: this is the immediate function -> foo

즉시 실행 함수를 만드는 방법은 간단합니다. 우선 함수 리터럴을 괄호()로 둘러쌉니다. 이때 함수 이름이 있든 없든 상관없습니다. 앞 예제에서는 function(name) { ... }부분을 괄호로 감쌌습니다. 그런 다음 함수가 바로 호출될 수 있게 () 괄호 쌍을 추가합니다. 이때 괄호 안에 값을 추가해 즉시 실행 함수의 인자로 넘길 수 가 있습니다. 예제의 경우는 ('foo')로 즉시 실행 함수를 호출했으며, 이때 'foo'를 인자로 넘겼습니다. 이 값은 앞 예제 즉시 실행 함수의 name 매개변수로 넘겨지게 됩니다.

이렇게 함수가 선언되자마자 실행되게 만든 즉시 실행 함수의 경우, 같은 함수를 다시 호출 할 수 없습니다. 따라서 즉시 실행 함수의 이러한 특징을 이용한다면 최초 한 번의 실행만을 필요로 하는 초기화 코드 부분 등에서 사용할 수 있습니다.

즉시 실행 함수의 또 다른 용도는 바로 jQuery와 같은 자바스크립트 라이브러리나 프레임워크 소스들에서 사용된다는 것입니다. jQuery의 최신 소스 코드를 살펴보면 소스의 시작부분과 끝 부분이 다음 예제와 같이 즉시 실행 함수 형태로 구성되어 있음을 확인 할 수 있습니다. 즉, jQuery 소스 코드 전체가 즉시 실행 함수로 감싸져 있습니다. 이렇게 jQuery에서 즉시 실행 함수를 사용하는 이유는 자바스크립트의 변수 유효 범위 특성 때문입니다. 자바스크립트에서는 함수 유효 범위를 지원합니다. 기본적으로 자바스크립트는 변수를 선언할 경우 프로그램 전체에서 접근할 수 있는 전역 유효 범위를 가지게 됩니다. 그러나 함수 내부에서 정의된 매개변수와 변수들은 함수 코드 내부에서만 유효할 뿐 함수 밖에서는 유효하지 않습니다.(여기서는 변수들은 var 문을 사용해서 정의해야 합니다. 그렇지 않으면 함수 내의 변수라도 전역 유효 범위를 갖게 됩니다.) 이것은 달리 말하면 함수 외부의 코드에서 함수 내부의 변수를 엑세스하는게 불가능 하다는 것입니다.

따라서 라이브러릐 코드를 이렇게 즉시 실행 함수 내부에 정의해두게 되면, 라이브러리 내의 변수들은 함수 외부에서 접근할 수 없습니다. 따라서 이렇게 즉시 실행 함수 내에 라이브러리 코드를 추가하면 전역 네임스페이스를 더럽히지 않으므로, 이후 다른 자바스크립트 라이브러리들이 동시에 로드가 되더라도 라이브러리 간 변수 이름 충돌 같은 문제를 방지할 수 있습니다.

내부 함수

자바스크립트에서는 함수 코드 내부에서도 다시 함수 정의가 가능합니다. 이렇게 함수 내부에 정의된 함수를 내부 함수라고 부릅니다. 내부 함수는 자바스크립트의 기능을 보다 강력하게 해주는 클로저를 생성하거나 부모 함수 코드에서 외부에서 접근을 막고 독립적인 헬퍼 함수를 구현하는 용도 등으로 사용합니다.

아래 간단한 내부 함수 예제를 살펴보겠습니다.

// parent() 함수 정의
function parent(){
    var a = 100;
    var b = 200;

    // child() 내부 함수 정의
    function child(){
        var b = 300;

        console.log(a);
        console.log(b);
    }
    child();
}
parent();
child();

실행 결과

스크린샷 2020-02-02 오후 9 35 32

앞 코드의 구조는 단순합니다. 우선 parent() 함수 내부에 child() 내부 함수를 정의했습니다. 그리고 아래 부분에서 parent() 함수와 child() 함수를 차례대로 호출했습니다.

| 내부 함수에서는 자신을 둘러싼 부모 함수의 변수에 접근이 가능합니다. |

  1. child() 내부 함수에 변수 a가 선언되지 않았음에도 코드에서 child() 함수가 호출됐을 때 값이 100이 출력됩니다. 이것은 parent() 함수의 변수 a 값에 접근하여 출력했기 때문입니다.

  2. 반면에 변수 b는 child() 함수에 선언이 되어 있으므로 parent() 함수의 b변수가 아닌 child() 함수의 변수 b 값이 바로 출력됐습니다.

이것이 가능한 이유는 자바스크립트의 스코프 체이닝 때문입니다. 이와 관련해서 정확한 내용은 나중에 포스팅하겠습니다. 여기서는 내부 함수는 자신을 둘러싼 외부 함수의 변수에 접근 가능하다는 정도만 기억하도록 합시다.

| 내부 함수는 일반적으로 자신이 정의된 부모 함수 내부에서만 호출이 가능합니다. |

앞 예제에서는 parent() 함수 외부에서 child() 함수 호출을 시도하지만, 함수가 정의되어 있지 않다는 에러를 발생합니다. 이것은 자바스크립트 스코핑 때문입니다. 즉, 함수 내부에 선언된 변수는 함수 외부에서 접근이 불가능 합니다. 이 규칙은 내부 함수도 그대로 적용됩니다. 때문에 부모 함수 외부에서 내부함수를 호출하는 것이 불가능 합니다. 반면에 부모 함수 안에서는 child() 내부 함수를 호출하는 것이 가능합니다. 내부 함수를 호출하는 부분과 내부 함수가 정의된 부분이 모두 부모 함수 내부에 있기 때문입니다.

Untitled Diagram (1)

위 그림에서 함수를 둘러싸고 있는 박스 부분이 바로 함수 스코프를 의미합니다. 기본적으로 함수 스코프 밖에서는 함수 스코프 안에 선언된 모든 변수나 함수에 접근이 불가능 합니다. 그러므로 예제에서는 parent() 함수 외부에서 parent() 함수 스코프 안에 있는 child() 함수를 호출하려 했으므로 에러가 발생한 것입니다.

또한, 자바스크립트 스코프 체이닝 때문에, 함수 내부에서는 함수 밖에서 선언된 변수나 함수의 접근이 가능합니다. 이러한 이유로 앞 예제처럼 child() 함수 내부에서 자신의 함수 스코프 밖에 있는 변수 a에 접근이 가능합니다.

하지만 함수 외부에서도 특정 함수 스코프 안에 선언된 내부 함수를 호출할 수 있습니다. 가령 부모 함수에서 내부 함수를 외부로 리턴하면, 부모 함수 밖에서도 내부 함수를 호출하는 것이 가능합니다. 아래 예제를 살펴보겠습니다.

// 함수 스코브 외부에서 내부 함수 호출하는 예제 코드
function parent(){
    var a = 100;

    // child() 내부 함수
    var child = function() {
        console.log(a);
    }

    // child() 함수 반환
    return child;
}

var inner = parent();
inner();

// 출력결과: 100

위 예제는 parent() 함수를 호출하고, 이 결과로 반환된 inner() 함수를 호출하는 간단한 예제입니다.

  1. 위 예제에서는 내부 함수를 함수 표현식 형식으로 정의하고, child 함수 변수에 저장했습니다. 그리고 parent() 함수의 리턴값으로 내부 함수의 참조값을 가진 child 함수 변수를 리턴했습니다.

  2. parent() 함수가 호출되면 inner 변수에 child 함수 변수 값이 리턴됩니다. child 함수 변수는 내부 함수의 참조 값이 있으므로, 아래 그림처럼 inner 변수도 child() 내부 함수를 참조합니다.

Untitled Diagram

  1. 때문에 inner 변수에 함수 호출 연산자 ()를 붙여 함수 호출 구문을 만들면, parent() 함수 스코프 밖에서도 내부 함수 child()가 호출됩니다. 호출하는 내부 함수에는 a 변수가 정의되어 있지 않아, 스코프 체이닝으로 부모 함수에 a 변수가 정의되어 있는지 확인하게 되고, a가 정의되어 있으면 그 값이 그대로 출력됩니다.

이와 같이 실행이 끝난 parent()와 같은 부모 함수 스코프의 변수를 참조하는 inner()와 같은 함수를 클로저라고 합니다.

함수를 리턴하는 함수

자바스크립트에서는 함수도 일급 객체이므로 일반 값처럼 함수 자체를 리턴할 수도 있습니다. 이러한 특징은 다양한 활용이 가능해집니다. 함수를 호출함과 동시에 다른 함수로 바꾸거나, 자기 자신을 재정의하는 함수를 구현할 수도 있습니다. 이러한 함수 유형 또한 자바스크립트의 언어적인 유연성을 보여주는 좋은 활용 예입니다.

// 자신을 재정의하는 함수 예제 코드
// self() 함수
var self = function() {
    console.log('a');
    return function() {
        console.log('b');
    }
}
self = self(); // a
self(); //b
  1. 처음 self() 함수가 호출됐을 때는 'a'가 출력됩니다. 그리고 다시 self 함수 변수에 self() 함수 호출 리턴값으로 내보낸 함수가 저장됩니다.

  2. 두번째로 self() 함수가 호출됐을 때는 'b'가 출력됩니다. 즉, 1에서 self() 함수 호출 후에, self 함수 변수가 가리키는 함수가 원래 함수에서 리턴 받은 새로운 함수로 변경됐기 때문입니다.

Untitled Diagram (2)

함수 호출과 this

함수의 기본적인 기능은 당연히 함수를 호출하여 코드를 실행하는 것입니다. 하지만 자바스크립트 언어 자체가 C/C++ 같은 엄격한 문법 체크를 하지 않는 자유로운 특성의 언어이므로 함수 호출 또한 다른 언어와는 달리 자유롭습니다.

arguments 객체

C와 같은 엄격한 언어와 달리, 자바스크립트에서는 함수를 호출할 때 함수 형식에 맞춰 인자를 넘기지 않더라도 에러가 발생하지 않습니다. 다음 간단한 예제를 살펴보겠습니다. func()는 arg1, arg2 두개의 인자를 전달받아 콘솔에 출력하는 간단한 기능의 함수입니다.

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

앞 예제에서는 func() 함수를 인자 개수를 달리해서 호출했습니다. C언어의 경우 2개의 인자를 넘기지 않고 호출했을 경우 바로 에러를 내면서 프로그램이 종료하지만, 자바스크립트는 인자를 어떻게 넘기더라도 함수를 호출할 때 에러가 발생하지 않습니다.

앞 예제에서 func(), func(1) 호출처럼 정의된 함수의 인자보다 적게 함수를 호출했을 경우, 넘겨지지 않는 인자에 undefined 값이 할당됩니다. 이와 반대로 인자 개수보다 많게 함수를 호출했을 경우는 에러가 발생하지 않고, 초과된 인수는 무시됩니다.

자바스크립트의 이러한 특성 때문에 함수 코드를 작성할 때, 런타임 시에 호출된 인자의 개수를 확인하고 이에 따라 동작을 다르게 해줘야 할 경우가 있습니다. 이를 가능케 하는게 바로 arguments 객체입니다. 자바스크립트에서는 함수를 호출할 때 인수들과 함께 암묵적으로 arguments 객체가 함수 내부로 전달되기 때문입니다. arguments 객체는 함수를 호출할 때 넘긴 인자들이 배열 형태로 저장된 객체를 의미합니다. 특이한 점은 이 객체는 실제 배열이 아닌 유사 배열 객체라는 점입니다. 유사 배열 객체에 대한 내용은 나중에 다루도록 하겠습니다.

arguments 객체의 예제를 보겠습니다.

// add() 함수
function add(a, b){
    // arguments 객체 출력
    console.log(arguments);
    reuturn a+b;
}

console.log(add(1));        // 출력값: NaN
console.log(add(1, 2));     // 출력값: 3
console.log(add(1, 2, 3));  // 출력값: 3

위 예제는 두 수의 합을 구하는 add() 함수를 정의하고, 인자 개수를 달리하며 add() 함수를 호출한 예제 코드입니다. add() 함수 내부에서 arguments 객체를 출력하는데, 함수 인자가 달라질 때 이 객체의 값이 아래 그림처럼 달라지게 됩니다.

스크린샷 2020-02-02 오후 11 18 56

그림을 보면 console.log(add(1))를 호출했을 때 arguments 0번 인덱스에 인자가 1인 값이 배열 형태로 저장되는 것을 알 수가 있습니다. length 프로퍼티는 호출 시에 넘긴 인자 개수를 의미합니다.

callee 프로퍼티는 현재 실행 중인 함수의 참조값(예제에서는 add() 함수)를 의미합니다.

앞서 애기했듯이 arguments는 객체이지 배열이 아닙니다. 즉, length 프로퍼티가 있으므로 배열과 유사하게 동작하지만, 배열은 아니므로 배열 메서드를 사용할 경우 에러가 발생한다는 것에 주의해야 합니다. 물론 유사 배열 객체에서 배열 메서드를 사용하는 방법이 있습니다. 이 부분에 대해서는 call과 apply 메서드를 이용한 명시적인 this 바인딩에 대해서 알아야 합니다.

arguments객체는 매개 변수 개수가 정확하게 정해지지 않는 함수를 구현하거나, 전달된 인자의 개수에 따라 서로 다른 처리를 해줘야 하는 함수를 개발하는데 유용하게 사용할 수 있습니다.

function sum(){
    var result = 0;


    for(i=0; i < arguments.length; i++){
        result += arguments[i];
    }
    return result;
}

console.log(sum(1,2,3));                // 출력값: 6
console.log(sum(1,2,3,4,5,6,7,8,9));    // 출력값: 45

예제의 sum() 함수는 호출된 인자 개수에 상관없이 이들 각각의 값을 모두 더해 리턴하는 함수입니다. arguments 객체를 사용할 경우 함수가 호출될 당시의 인자들에 배열 형태로 접근할 수 있으므로 이러한 구현이 가능한 것입니다.

호출 패턴과 this 바인딩

자바스크립트에서 함수를 호출할 때 기존 매개변수로 전달되는 인자값에 더해, 앞서 설명한 arguments 객체 및 this 인자가 함수 내부로 암묵적으로 전달됩니다. 여기서 특히, this 인자는 고급 자바스크립트 개발자로 거듭나려면 필수적으로 이해해야 하는 핵심 개념입니다. this가 이해하기 어려운 이유는 자바스크립트의 여러가지 함수가 호출되는 방식에 따라 this가 다른 객체를 참조하기 때문입니다. 따라서 함수 호출 패턴에 따라 this가 어떤 객체에 바인딩 되는지에 대해서 알아보도록 하겠습니다.

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

객체의 프로퍼티가 함수일 경우, 이 함수를 메서드라고 부릅니다. 이러한 메서드를 호출할 때, 메서드 내부 코드에서 사용된 this는 해당 메서드를 호출한 객체로 바인딩 됩니다.

var myObject = {
    name: 'foo',
    sayName: function(){
        console.log(this.name);
    }
};

// otherObject 객체 생성
var otherObject = {
    name: 'bar'
};

otherObject.sayName = myObject.sayName;
myObject.sayName();
otherObject.sayName();

실행 결과

스크린샷 2020-02-04 오전 12 12 22

Untitled Diagram

myObject 객체와 otherObject 객체는 name 프로퍼티와 sayName() 메서드가 있습니다. sayName() 메서드는 this.name 값을 출력하는 간단한 함수로서, myObject와 otherObject 객체로부터 각각 호출됩니다. 이때 앞에서 설명한 대로 sayName() 메서드에 사용된 this는 자신을 호출한 객체에 바인딩 됩니다.

따라서 위의 예제에서 sayName() 메서드는 myObject 객체에서 호출됐으므로, 이 메서드에서 사용된 this는 myObject 객체를 가르킵니다. 그러므로 this.name은 foo가 출력된 것입니다. 이와 동일하게 otherObject 객체에서 sayName() 메서드를 호출할 때 this는 otherObject 객체와 바인딩 되기 때문에 this.name은 bar가 출력된 것입니다.

함수를 호출할 때 this 바인딩

자바스크립트에서 함수를 호출하면, 해당 함수 내부 코드에서 사용된 this는 전역 객체에 바인딩 됩니다. 브라우저에서 자바스크립트를 실행하는 경우 전역 객체는 window 객체가 됩니다.

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

자바스크립트의 모든 전역 변수는 실제로는 이러한 전역 객체의 프로퍼티들입니다. 다음 에제를 살펴보겠습니다. 전역 변수 foo를 정의하고 출력하는 예제코드입니다. window.foo를 출력해도 같은 결과를 얻을 수 있습니다.

// 전역 객체와 전역 변수의 관계를 보여주는 예제

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

console.log(foo);         // (출력값) I'm foo
console.log(window.foo);  // (출력값) I'm foo

따라서 전역 변수는 전역 객체(window)의 프로퍼티로도 접근할 수가 있습니다.

var test = "This is test";
console.log(test);

var sayFoo = function() { 
    console.log(this.test);
};

sayFoo();
// (출력값) This is test
// (출력값) This is test
  1. 우선 test라는 전역 변수를 선언했습니다. 위에서 설명했듯이 자바스크립트의 전역 변수는 전역 객체 window의 프로퍼티로 접근이 가능하다고 했습니다.

  2. 이제 sayFoo() 함수를 살펴보겠습니다. 단순히 this.test를 출력하는 함수입니다.
    자바스크립트에서는 함수를 호출할 때 this는 전역 객체에 바인딩 된다고 했으므로, sayFoo() 메소드가 호출된 시점에는 this는 전역 객체인 window에 바인딩 됩니다. 때문에 this.test는 window.test를 의미하므로, 결국 This is test 값이 출력되는 것입니다.

하지만 이러한 함수 호출에서의 this 바인딩 특성은 내부 함수를 호출했을 경우에도 그대로 적용되므로, 내부 함수에서 this를 이용할 때는 주의해야합니다.

내부함수의 this 바인딩 동작을 보여주는 예제 코드
var value = 100;

//myObject 객체 생성
var myObject = {
    value: 1,
    func1: function () {
        this.value += 1;
        console.log('func1() called. this.value : ' + this.value);

        // 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();
        }
        func2();
    }
};

myObject.func1();

func2()와 func3() 내부 함수가 두 개가 있어서 코드의 동작을 이해하는데 약간 어려울 수도 있지만, 함수 호출을 따라가다 보면 그리 복잡하진 않습니다. 이 예제의 함수 호출 순서에 대해서 알아보면 우선, func1() 메서드가 호출되고, 이어서 func2() 내부 함수와 func3() 내부 함수가 차례대로 호출됩니다.

  1. func1()은 myObject의 메서드입니다. 따라서 앞에 설명했듯이 메서드로 객체.메서드로 호출할 때는 메서드 코드 내에서 사용된 this는 자신을 호출한 객체를 가리키므로, func1()에서 사용된 this는 이 메서드를 호출한 객체 myObject를 가르킵니다.

  2. func2()는 func1()을 부모 함수로 하여, func2() 내부 함수의 this는 당연히 부모 함수의 this와 같은 객체인 myObject를 가르킨다고 생각하는게 자연스러워 보입니다.

  3. func3() 함수의 this도 마찬가지로, 자신의 부모 함수인 func2()의 this와 같이 myObject 객체를 가르킨다고 생각할 수 있습니다.

따라서 앞의 가정대로 코드가 동작한다면, func1(), func2(), func3() 함수의 this가 모두 같은 myObject 가르키기 때문에, 출력 결과는 아래와 같을거 같지만...

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

하지만 실행 결과는 생각과는 다르게 아래와 같습니다.

실행 결과

image

이렇게 실행결과과 예측했던 것과 다르게 출력된 이유는 자바스크립트에서는 내부 함수 호출 패턴을 정의해놓치 않기 때문입니다. 내부 함수도 결국 함수이므로 이를 호출할 때는 함수 호출로 취급됩니다. 따라서 함수 호출 패턴 규칙에 따라 내부 함수의 this는 전역 객체(window)에 바인딩 됩니다. 때문에 2,3에서 this.value 값에 1을 더한 것은 결국 원래 의도했던 것과 다르게 window.value 값에 1을 더한 결과가 나옵니다.

이를 그림으로 나타낸다면 다음과 같습니다.

Untitled Diagram

이렇게 내부 함수가 this를 참조하는 자바스크립트의 한계를 극복하려면 부모 함수의 this를 내부 함수가 접근 가능한 다른 변수에 저장하는 방법이 사용됩니다. 보통 관례상 this 값을 저장하는 변수의 이름을 that이라고 짓습니다. 이렇게 되면 내부 함수에서는 that 변수로 부모 함수의 this가 가리키는 객체에 접근할 수 있습니다. 다음 코드는 이러한 방식을 이용해서 코드를 약간 수정한 것입니다. 결과과 원래 의도한 방향으로 제대로 출력된 것을 확인할 수 있습니다.

// 내부 함수 this 바인딩

var value = 100;

var myObject = {
     value: 1,
     func1: function () {
        var that = this;

        this.value += 1;
        console.log('func1() called. this.value : ' + this.value);

        // func2() 내부 함수
        func2 = function () {
            that.value += 1;
            console.log('func2() called. this.value : ' + that.value);

            //func3() 내부 함수
            func3 = function () {
                that.value += 1;
                console.log('func3() called. this.value : ' + that.value);
            }

            func3();
        }
        func2();
    }
};

myObject.func1(); // func1 메서드 호출

실행 결과

image

변수 that을 통해 내부 함수의 this 바인딩 한계 극복하기

Untitled Diagram (1)

자바스크립트에서는 이와 같은 this 바인딩 한계를 극복하려고, this 바인딩을 명시적으로 할 수 있도록 call과 apply 메서드를 제공하는데, 이것도 나중에 포스팅 하도록 하겠습니다.

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

위에서 설명했듯이 자바스크립트 객체를 생성하는 방법은 크게 객체 리터럴 방식이나 생성자 함수를 이용하는 두가지 방법이 있습니다. 이번 절에는 생성자 함수를 이용한 객체 생성 방법을 살펴보겠습니다.

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

자바스크립트에서는 이러한 생성자 함수를 호출할 때, 생성자 함수 코드 내부에서 this는 앞서 알아본 메서드 함수 호출 방식에서의 this 바인딩과는 다르게 동작합니다. 이를 정확히 이해하려면 생성자 함수가 호출됐을 때 동작하는 방식을 살펴봐야 합니다.

  1. 생성자 함수 코드가 실행되기 전 빈 객체가 생성됩니다. 바로 이 객체가 생성자

함수가 새로 생성하는 객체이며, 이 객체는 this로 바인딩 됩니다. 따라서 이후 생성자 함수의 코드 내부에서 사용된 this는 이 빈 객체를 가르킵니다.
하지만 여기서 생성된 객체는 엄밀히 말하면 빈 객체는 아닙니다. 앞서 설명했듯이 자바스크립트의 모든 객체는 자신의 부모인 프로토타입 객체와 연결되어 있으며, 이를 통해 부모 객체의 프로퍼티나 메소드를 마치 자신의 것처럼 사용할 수가 있기 때문입니다.
이렇게 생성자 함수가 생성한 객체는 자신을 생성한 생성자 함수의 prototype 프로퍼티가 가르키는 객체를 자신의 프로토타입 객체로 설정합니다. (이것은 자바스크립트의 규칙이니 잘 기억해야 합니다.)

  1. this를 통한 프로퍼티 생성

이후에는 함수 코드 내부에서 this를 사용해서, 앞에서 생성된 빈 객체에 동적으로 프로퍼티나 메소드를 생성할 수 있습니다.

  1. 생성한 객체 리턴

리턴문이 동작하는 방식은 경우에 따라 다르므로 주의해야 합니다. 우선 가장 일반적인 경우로 특별하게 리턴문이 없을 경우, this로 바인딩된 새로 생성한 객체가 리턴됩니다.
이것은 명시적으로 this를 리턴해도 결과는 같습니다.하지만 리턴값이 새로 생성한 객체가 아닌 다른 객체를 반환하는 경우는 생성자 함수를 호출했다고 하더라도 this가 아닌 해당 객체가 리턴됩니다.

생성자 함수 동작 방식

var Person = fucntion(name) {
    // 함수 코드 실행 전
    this.name = name;
    // 함수 리턴
}

var foo = new Person('foo');
console.log(foo.name);

위의 예제를 보면 Person이라는 생성자 함수를 정의하고, 이를 통해 foo 객체를 만드는 예제코드입니다. Person 함수를 new로 호출하면, Person은 생성자 함수로 동작합니다.

  1. Person() 함수가 생성자로 호출되면, 함수 코드가 실행되기 전에 빈 객체가 생성됩니다. 여기서 생성된 빈 객체는 Person() 생성자 함수의 prototype 프로퍼티가 가르키는 객체(Person.prototype 객체)를 [[Prototype]] 링크로 연결해서 자신의 프로토타입을 설정합니다. 그리고 이렇게 생성된 객체는 생성자 함수 코드에서 사용되는 this로 바인딩 됩니다.

  2. this가 가르키는 빈 객체에 name이라는 동적 프로퍼티를 생성했습니다.

  3. 리턴값이 특별히 없으므로 this로 바인딩한 객체가 생성자 함수의 리턴값으로 반환돼서, foo 변수에 저장됩니다.

생성자 함수 동작 방식

Untitled Diagram (2)

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

이제 자바스크립트에서 객체를 생성하는 두 가지 방법인 객체 리터럴 방식과 생성자 함수를 이용해서 객체를 생성하는 방식을 모두 알아봤습니다. 그렇다면 이 두가지 방법의 차이점은 무엇일까요?

이를 위해 간단한 예제를 살펴보겠습니다. 예제에는 자바스크립트 두 가지 객체 생성 방식으로 객체를 만들고, 이를 출력하는 간단한 예제입니다.

우선 가장 쉽게 생각할 수 있는 차이는 foo 객체와 같이 리터럴 방식으로 생성된 객체는 같은 형태의 객체를 재생성할 수 없다는 점입니다. 이에 반해 예제와 같이 Person() 생성자 함수를 사용해서 객체를 생성한다면, 생성자 함수를 호출할 때 다른 인자를 넘김으로써 같은 형태의 서로 다른 객체 bar와 baz를 생성할 수 있습니다.

객체 생성 두 가지 방법(객체 리터럴 vs 생성자 함수)

// 객체 리터럴 방식으로 foo 객체 생성
var foo = {
    name: 'foo',
    age: 35,
    gender: 'man'
};

console.dir(foo);

// 생성자 함수(첫 머리글자가 대문자일 경우 자바스크립트 스타일을 따른다.)
function Person(name, age, gender, position) {
    this.name = name;
    this.age = age;
    this.gender = gender;
}

// Person 생성자 함수를 이용해 bar 객체, baz 객체 생성
var bar = new Person('bar', 35, 'woman');
console.dir(bar);

var baz = new Person('baz', 25, 'woman');
console.dir(baz);

실행 결과

image

출력결과를 살펴보면 알겠지만, 객체 리터럴 방식과 생성자 함수 방식의 차이가 프로토타입 객체에 있음을 알 수 있습니다. 실행 결과에서는 _proto_ 프로퍼티가 모두 Object로 나와있지만... 실제로 객체 리터럴 방식의 경우는 자신의 프로토타입 객체가 Object(Object.prototype)이고, 생성자 함수 방식의 경우는 Person(실제로는 Person.prototype)으로 서로 다릅니다.

이렇게 차이가 발생하는 이유는 자바크스크립트 객체 생성 규칙 때문입니다. 자바스크립트 객체는 자신을 생성한 생성자 함수의 prototype 프로퍼티가 가르키는 객체를 자신의 프로토타입 객체로 설정합니다. 객체 리터럴 방식에서 객체 생성자 함수는 Object()이며,생성자 함수 방식의 경우는 생성자 함수 자체, 예제에서는 Person()이므로 두 가지 방식이 다른 프로토타입 객체가 있는 것입니다. 이와 관련된 자세한 내용은 프로토타입 체이닝에서 포스팅 하겠습니다.

| 생성자 함수를 new를 붙이지 않고 호출한 경우 |

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

new를 붙이지 않고 생성자 함수 호출 시의 오류

var qux = Person('qux', 20, 'man');
console.log(qux);           //출력값: undefined

console.log(window.name);   //출력값: qux
console.log(window.age);    //출력값: 20
console.log(window.gender); //출력값: man

위 예제코드에서 생성자 함수 Person()을 new 없이 일반 함수 형태로 호출할 경우, this는 함수 호출이므로 전역 객체인 window 객체로 바인딩 됩니다. 따라서 이 코드는 Person 객체를 생성해서 이를 qux 변수에 저장하려는 원래 의도와는 다르게 this가 바인딩된 window 객체에 동적으로 name, age, gender 프로퍼티가 생성됩니다. 따라서 window.name 값이 qux로 출력된 것입니다.

Person() 함수는 리턴값이 특별히 없습니다. 생성자 함수는 별도의 리턴값이 정해져 있지 않는 경우 새로 생성된 객체가 리턴되지만, 일반 함수를 호출할 때는 undefined가 리턴됩니다. 따라서 undefined 값이 출력된 것입니다.

다시 한번 강조하지만 일반 함수와 생성자 함수의 구분이 별도로 없으므로, 일반적으로 생성자 함수로 사용할 함수는 첫 글자를 대문자로 표기하는 네이밍 규칙을 권장합니다. 그러나 이러한 규칙을 사용하더라도 결국 new를 사용해서 호출하지 않을 경우 코드의 에러가 발생할 수 있으므로, 더글라스 크락포드와 같은 자바스크립트 빡고수들은 객체를 생성하는 다음과 같은 별도의 코드 패턴을 사용하기도 합니다.

< 강제로 인스턴스 생성하기 >

앞에서 설명한 위험성을 피하려고 널리 사용되는 패턴이 있습니다. 다음 예제를 보겠습니다.

function A(arg) {
    if (!(this instance of A)))
        return new A(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
console.log(global.value); // 출력값: undefined

함수 A에서는 A가 호출될 때, this가 A의 인스턴스인지를 확인하는 분기문이 추가되었습니다. this가 A의 인스턴스가 아니라면, new로 호출된 것이 아님을 의미하고, 이 경우 new로 A를 호출하여 반환하게 하였습니다. 이렇게 하면 var b = A(10);과 같이 사용자가 사용했다고 하더라도, 전역 객체에 접근하지 않고, 새 인스턴스가 생성되어 b에 반환될 것입니다.

어떤 코드에서는 앞과 같이 함수의 이름을 그대로 쓰지 않고 다음과 같은 표현식을 사용하기도 합니다.

if (!(this isntanceof arguments.callee))

arguments.callee가 곧 호출된 함수를 가르킵니다. 이와 같이 하면, 특정 함수 이름과 상관없이 이 패턴을 공통으로 사용하는 모듈을 작성할 수 있는 장점이 있습니다.

이 패턴으로 함수 사용자가 함수 작성자의 의도와는 다르게 함수를 호출할 때에도 문제가 발생하지 않게 합니다. 그리고 함수 작성자 역시, 사용자가 new로 반환된 인스턴스를 사용하게 될 것을 확신하고 자신의 코드를 작성할 수 있습니다. 이 패턴은 매우 광범위하게 사용됩니다. 대중적으로 사용하는 대부분의 자바스크립트 라이브러리는 이 패턴이 들어가 있음을 확인할 수 있습니다.

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

지금까지 자바스크립트에서 함수 호출이 발생할 때 각각의 상황에 따라 this가 정해진 객체에 자동으로 바인됭된다는 것을 확인했습니다. 자바스크립트는 이러한 내부적인 this 바인딩 이외에도 this를 특정 객체에 명시적으로 바인딩시키는 방법도 제공합니다. 이를 가능케 하는 것이 바로 위의 함수 객체의 기본 프로퍼티에서 간단히 설명한 apply()와 call() 메소드 입니다. 이 메소드들은 모든 함수의 부모 객체인 Funtion.prototype 객체의 메서드이므로, 모든 함수는 다음과 같은 형식으로 apply() 메소드를 호출하는 것이 가능합니다.

function.apply(thisArg, argArray)

call() 메소드와 apply() 메소드와는 기능이 같고 단지 넘겨받는 인자의 형식만 다르므로 이후에 설명하겠습니다.

우선 여기서 기억해야 할 것은 apply() 메서드를 호출하는 주체가 함수고, apply() 메소드도 특정 객체에 바인딩할 뿐 결국 본질적인 기능은 함수 호출이라는 것입니다.
가령, Person()이라는 함수가 있고, Person.apply() 이렇게 호출한다면 이것의 기본적인 기능은 Person() 함수를 호출하는 것입니다.

이제 apply() 메서드에 대해 살펴보면 첫 번째 인자 thisArg는 apply() 메서드를 호출한 함수내부에서 사용한 this에 바인딩할 객체를 가르킵니다. 즉, 첫 번째 인자로 넘긴 객체가 this로 명시적으로 바인딩되는 것입니다.

두 번째 argArray 인자는 함수를 호출할 때 넘길 인자들의 배열을 가르킵니다. apply() 메서드의 기능도 결국 함수를 호출하는 것이므로, 함수에 넘길 인자를 argArray 배열로 넘깁니다. 결국 apply() 메서드를 정리하면, 두 번째 인자인 argArray 배열을 자신을 호출한 함수의 인자로 사용하되, 이함수 내부에 사용된 this는 첫 번째 인자인 thisArg 객체로 바인딩해서 함수를 호출하는 기능을 하는 것입니다.

이것도 마찬가지로 예제로 확인 해보겠습니다.

function Person(name, age, gender) {

    this.name = name;
    this.age = age;
    this.gender = gender;

}

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

Person.apply(foo, ['junyoung', 29, 'man']);
console.dir(foo);

실행 결과

image

  1. foo는 객체 리터럴 방식으로 생성한 빈 객체입니다.
  2. apply() 메서드를 사용해서, Person() 함수를 호출한 코드입니다. 첫 번째 인자로 넘긴 foo가 Person() 함수에서 this로 바인딩 됩니다. 그리고 apply() 메서드의 두 번째 인자로 넘긴 배열 ['junyoung', 29, 'man']은 호출하려는 Person() 함수의 인자 name, age, gender로 각각 전달됩니다.이 코드는 결국 Person('junyoung', 29, 'man') 함수를 호출하면서, this를 foo 객체에 명시적으로 바인딩하는 것을 의미하는 것입니다.

코드 결과를 살펴보면, foo 객체에 제대로 프로퍼티가 생성되어 있음을 확인할 수 있습니다.
call() 메서드의 경우 apply()와 기능은 같지만, apply()의 두 번째 인자에서 배열 형태로 넘긴 것을 각각 하나의 인자로 넘깁니다. 예를들어 위의 apply() 메서드를 사용한 코드 부분을 call() 메서드로 바꾼다면 다음과 같습니다.

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

이러한 apply()나 call() 메서드는 this를 원하는 값으로 명시적으로 매핑해서 특정 함수나 메서드를 호출할 수 있다는 장점이 있습니다. 그리고 이들의 대표적인 용도가 바로 arguments 객체에서 설명한 arguments 객체와 같은 유사 배열 객체에서 배열 메서드를 사용하는 경우입니다. arguments 객체는 실제 배열이 아니므로 pop(), shift() 같은 표준 배열 메서드를 사용할 수 없습니다. 하지만 apply() 메서드를 이용하면 가능합니다.

다음 예제는 myFunction() 함수에 최초로 전달된 인자에서 첫 번쩨 요소를 삭제하고 나머지 인자를 다시 내부 함수 inner()로 전달하는 예제입니다.

apply 메서드를 활용한 arguments 객체의 표준 배열 메서드 slice() 활용 코드

function myFunction() {

    console.log(arguments);

// arguments.shift();   에러 발생

// arguments 객체를 배열로 변환
var args = Array.prototype.slice.apply(arguments);
console.dir(args);
}

myFunction(1, 2, 3);

배열에서는 shift() 메서드를 사용해 첫 번째 원소를 쉽게 삭제할 수 있지만, arguments 객체는 length 프로퍼티만을 가진 유사 배열이므로, 앞 코드에서 주석을 제거하고 arguments.shift()와 같이 표준 배열 메소드를 호출하면 에러가 발생하며 프로그램이 종료합니다.

이러한 경우 apply() 메서드로 arguments 객체에서 마치 배열 메서드가 있는 것처럼 처리할 수 있습니다.

앞에서 살펴본 apply() 메서드의 동작 방식을 떠올리며 코드를 해석하면 다음과 같습니다.
Array.prototype.slice() 메서드를 호출해라. 이때 this는 arguments 객체로 바인딩 해라. 결국 이 말은 arguments 객체가 Array.prototype.slice() 메서드를 마치 자신의 메서드인 양 arguments.slice()와 같은 형태로 메서드를 호출하라는 것입니다.

여기서 Array.protoype은 배열과 객체에서 설명했듯이 모든 배열 객체의 부모 역할을 하는 자바 스크립트 기본 프로토타입 객체로서 slice() 등을 비롯한 push(), pool()과 같은 배열 표준 메서드가 있습니다. slice(start, end) 메서드는 이 메서드를 호출한 배열의 start 인덱스에서 end-1 인덱스까지 복사한 배열을 리턴합니다. end 인자를 지정하지 않을 경우 기본값은 배열의 length 값입니다. slice() 메서드에 아무 인자도 넘기지 않을 경우는 전체 배열이 복사됩니다.

var arrA = [1, 2, 3];
var arrB = arrA.slice(0);    // [1, 2, 3]
var arrC = arrA.slice();     // [1, 2, 3]
var arrD = arrA.slice(1);    // [2, 3]
var arre = arrA.slice(1, 2); // [2]

Array.prototype.slice().apply(arguments)의 결과값은 apply() 메서드의 두 번째 인자로 slice() 메서드를 호출할 때 사용할 인자를 넘기지 않았으므로, arguements 객체로 인자 없이 slice() 메서드를 호출한 형태가 됩니다. 앞서 설명했듯이 slice() 메소드는 인자 없이 호출한 경우, 이 메서드를 호출한 배열을 복사한 새로운 배열을 생성합니다. 따라서 arguments 객체의 모든 요소를 그대로 복사한 배열이 생성되고, 이것은 args 모두 같은 프로퍼티가 있는 것처럼 보이지만 두 객체의 proto 프로퍼티는 다르다는 것을 확인할 수 있습니다. 즉, arguments는 객체이므로 프로토타입이 Object.prototype인 반면에, args는 배열이므로 Array.prototype인 것을 확인할 수 있습니다.

함수 리턴

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

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

//noReturnFunc() 함수
var noReturnFunc () {
    console.log('This function has no statement');
};

var result = noReturnFunc();
console.log(result);
/* 출력값: 
          This function has no return statement
          undefined
*/

앞 예제에서 noReturnFunc() 와 같이 return 문이 없는 함수의 경우, 함수를 호출할 때 undefined 값이 리턴됩니다.

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

생성자 함수에서 별도의 리턴값을 지정하지 않을 경우 this로 바인딩된 새로 생성된 객체가 리턴됩니다. 때문에 생성자 함수에서는 일반적으로 리턴값을 지정하지 않습니다. 이와 관련해서는 위에서 예제를 통해 알아봤습니다.
생성자 함수의 경우는 리턴값을 처리하는 몇가지 예외 상황이 있습니다.
만약 다음 코드와 같이 생성자 함수에서 this로 바인딩되는 생성된 객체가 아닌 다른 객체를 리턴한다면 어떻게 될까요?

// 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', 30, 'man');
console.dir(foo);

실행 결과

image

  1. 생성자 함수의 리턴값을 새로 생성한 객체가 아니라, 객체 리터럴 방식으로 특정 객체로 지정한 경우 new 연산자로 Person() 생성자 함수를 호출해서 새로운 객체를 생성하더라도, 리턴값에서 명시적으로 넘긴 객체나 배열이 리턴됩니다.

  2. Person() 함수에서 return 문이 없었다면 새로 생성되는 foo 객체가 리턴됩니다. 그러나 생성자 함수에서 명시적으로 bar 객체를 리턴값으로 넘기므로, 결국 console.dir(foo) 함수에서 bar 객체가 출력되는 것을 확인할 수 있습니다.

생성자 함수의 리턴값으로 넘긴 값이 객체가 아닌 불린, 숫자, 문자열의 경우에는 이러한 리턴값을 무시하고 this로 바인딩된 객체가 리턴됩니다. 이것도 예제로 작성해보겠습니다.

// 생성자 함수에서 명시적으로 기본 타입(불린, 숫자, 문자열)값을 리턴했을 경우
function Person(name, age, gender){
    this.name = name;
    this.age = age;
    this.gender = gender;

    return 100;
}

var foo = new Person('foo', 30, 'man');
console.dir(foo);

실행 결과

image

프로토타입 체이닝

프로토타입의 두가지 의미

자바스크립트는 기존 C++이나 자바 같은 객체지향 프로그래밍 언어와는 다르게 다른 프로토타입 기반의 객체지향 프로그래밍을 지원합니다. 따라서 자바스크립트의 동작 과정을 제대로 이해하려면 프로토타입의 개념도 잘 이해하고 있어야 합니다. 이번 절에서는 자바스크립트에서 OOP 상속에 근간이 되는 프로토타입과 프로토타입 체이닝에의 기본 개념을 설명할 것입니다.

자바와 같은 객체지향 프로그래밍에서는 클래스를 정의하고 이를 통해 객체를 생성하지만, 자바스크립트에서는 이러한 클래스 개념이 없습니다.
대신에 객체 리터럴이나 앞서 설명했던 생성자 함수로 객체를 생성합니다. 이렇게 생성된 객체의 부모 객체가 바로 프로토콜 객체입니다. 즉, 상속 개념과 마찬가지로 자식 객체는 부모 객체가 가진 프로퍼티 접근이나 메소드를 상속받아 호출하는 것이 가능합니다.

앞서 여러 번 강조했듯이 자바스크립트의 모든 객체는 자신의 부모인 프로토타입 객체를 가리키는 참조 링크 형태의 숨겨진 프로퍼티가 있습니다. ECMAScript에서는 이러한 링크를 암묵적으로 프로토타입 링크라고 부르며 이러한 링크는 모든 객체의 [[Prototype]] 프로퍼티에 저장됩니다. 그리고 이 책에서 이러한 링크를 [[Prototype]] 링크라고 명명합니다.

한가지 더 여기서 주의해야할 점은 앞에 설명한 것처럼 prototype 프로퍼티에서 설명했던 함수 객체의 prototype 프로퍼티와 객체의 숨은 프로퍼티인 [[Prototype]] 링크를 구분해야 한다는 점입니다. 자바스크립트를 잘 이해하지 못하는 저 같은 초급 개발자들이 이 둘을 정확하게 구분하지 못하는 경우가 많습니다. 우선 이둘의 차이점을 알려면 자바스크립의 객체 생성 규칙을 알아야 합니다.

자바스크립트에서 모든 객체는 자신을 생성한 생성자 함수의 prototype 프로퍼티가 가르키는 프로토타입 객체를 자신의 부모 객체로 설정하는 [[Prototype]] 링크로 연결합니다.

이 규칙을 적용해서 아래 에제 코드를 살펴보겠습니다. Person() 생성자 함수를 정의하고, 이를 통해 foo 객체를 생성하는 간단한 코드입니다.

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

var foo = new Person('foo');
console.dir(Person);
console.dir(foo);

Person() 생성자 함수를 이용해서 foo 객체를 생성했습니다.
Person() 생성자 함수는 prototype 프로퍼티로 자신과 링크된 프로토타입 객체를 가르킵니다. 그리고 앞서 설명한 자바스크립트의 객체 생성 규칙에 의하면 Person() 생성자 함수로 생성된 foo 객체는 Person() 함수의 프로토타입 객체를 [[Prototype]] 링크로 연결합니다. 결국 prototype 프로퍼티나 [[Prototype]] 링크는 같은 프로토타입 객체를 가르키고 있습니다.

prototype 프로퍼티는 함수의 입장에서 자신과 링크된 프로토타입 객체를 가르키고 있으며, 이에 반해 [[Prototype]] 링크는 객체의 입장에서 자신의 부모 객체인 프로토타입 객체를 내부의 숨겨진 링크로 가르키고 있습니다.

결국, 자바스크립트에서 객체를 생성하는 건 생성자 함수의 역할이지만, 생성된 객체의 실제 부모 역할을 하는 건 자신이 아닌 생성자의 prototype 프로퍼티가 가르키는 프로토타입 객체입니다.

객체, 생성자 함수, 프로토타입 객체의 관계

Untitled Diagram

실행 결과

image

결과값을 살펴보면, Person() 생성자 함수의 prototype 프로퍼티와 foo 객체의 proto 프로퍼티(이것은 [[Prototype]] 프로퍼티를 의미합니다.)가 같은 프로토타입 객체를 가리키는 것을 알 수 있습니다. 해당 프로토타입 객체는 constructor 프로퍼티가 Person() 생성자 함수를 가르키고 있습니다.

함수 객체의 기본 프로퍼티에서 간단히 설명한 것처럼, proto 프로퍼티는 모든 객체에 존재하는 숨겨진 프로퍼티로 객체 자신의 프로토타입 객체를 가르키는 참조 링크 정보입니다.ECMAScript에서는 이것을 [[Prototype]] 프로퍼티로 정하고, 내부적으로만 사용된다고 명시하고 있지만, 크롬이나 파이어폭스 같은 브라우저에서는 proto 프로퍼티로 명시적으로 제공하고 있습니다. 따라서 proto 프로퍼티나 [[Prototype]] 프로퍼티는 같다고 간주하면 됩니다.

객체 리터럴 방식으로 생성된 객체의 프로토타입 체이닝

자바스크립트에서 객체는 자신의 프로퍼티뿐만이 아니라, 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티 또한 마치 자신의 것처럼 접근하는게 가능합니다. 이것을 가능케 하는게 바로 프로토타입 체이닝입니다.

var myObject = {
    name: 'foo',
    sayName: function () {
        console.log('My Name is ' + this.name);
    }
};

myObject.sayName();
console.log(myObject.hasOwnProperty('name'));
console.log(myObject.hasOwnProperty('nickName'));
myObject.sayNickName();

실행 결과

image

myObject는 name 프로퍼티와 sayName() 메서드를 가진 객체입니다. 출력 결과를 보면 알 수 있듯이 sayName() 메서드의 결과값은 제대로 출력됐지만, sayNickName() 메서드는 myObject의 메서드가 아니므로 에러가 발생합니다.

그런데 myObject 객체에 hasOwnProperty() 메서드가 없음에도 결과가 정상적으로 출력됐습니다. 참고로 hasOwnProperty() 메서드는 이 메서드를 호출한 객체에 인자로 넘긴 문자열 이름의 프로퍼티나 메서드가 있는지 체크하는 자바스크립트 표준 API 함수입니다. 따라서 myObject는 name이라는 프로퍼티가 있으므로 true가 출력됐지만, 에제에서는 nickName 프로퍼티가 없어서 false가 출력됐습니다.

hasOwnProperty() 메서드를 호출할 때는 에러가 발생하지 않았을까요? 우선 이를 이해하려면 객체 리터럴 방식으로 생성한 객체와 프로토타입 체이닝의 개념을 알아야 합니다.

위에서 설명한 객체 생성에서 말했듯이 객체 리터럴로 생성한 객체는 Object()라는 내장 생성자 함수로 생성된 것입니다. Object() 생성자 함수도 함수 객체이므로 prototype이라는 프로퍼티 속성이 있습니다. 따라서 앞서 설명한 자바스크립트의 규칙으로 생성한 객체 리터럴 형태의 myObject는 다음 그림처럼 Object() 함수의 prototype 프로퍼티가 가르키는 Object.prototype 객체를 자신의 프로토타입 객체로 연결합니다.

객체 리터럴 방식에서의 객체와 프로토타입 객체의 관계

Untitled Diagram (1)

이제 프로토타입 체이닝이라는 개념을 살펴보겠습니다. 자바스크립트에서 특정 객체의 프로퍼티나 메서드에 접근하려고 할 때, 해당 객체에 접근하려는 프로퍼티 또는 메서드가 없다면 [[Prototype]] 링크를 따라 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티를 차례대로 검색하는 것을 프로토타입 체이닝이라고 말합니다.

sayName() 메소드는 객체 내에 메서드가 있어 바로 수행됩니다. 반면에 hasOwnProperty() 메서드를 호출하는 동작 과정은 조금 다릅니다. myObject.hasOwnProperty() 메서드를 호출했지만, myObject 객체는 hasOwnProperty() 메서드가 없습니다. 그렇다면 이후에는 myObject 객체 내에 [[Prototype]] 링크를 따라 그것의 부모 역할을 하는 Object.prototype 프로토타입 객체 hasOwnProperty() 메서드가 있는지를 검색합니다. 앞서 설명했듯이 hasOwnProperty() 메서드는 자바스크립트 표준 API로 Object.prototype 객체에 포함되어 있습니다. 따라서 hasOwnProperty() 메서드가 없어서 에러가 나지 않고 정상적으로 코드가 수행됩니다.
참고로 Object.prototype 객체는 자바스크립트 모든 객체의 조상 역할을 하는 객체로서, 자바스크립트 모든 객체가 호출할 수 있는 toString(), hasOwnProperty() 등과 같은 표준 메서드를 제공하고 있습니다.

sayNickName() 메서드를 호출할 때는 myObject 객체에도 없고, Object.prototype 객체에도 없으므로 에러가 발생한 것 입니다.

생성자 함수로 생성된 객체의 프로토타입 체이닝

생성자 함수로 객체를 생성하는 경우는 객체 리터럴 방식과 약간 다른 프로토타입 체이닝이 이뤄집니다. 하지만 두 가지 방식 모두 다음과 같은 기본 원칙을 잘 지키고 있습니다.

자바스크립트에서 모든 객체는 자신을 생성한 생성자 함수의 prototpye 프로퍼티가 가르키는 객체를 자신의 프로토타입 객체(부모 객체)로 취급합니다.

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

var foo = new Person('foo', 30 , 'tennis');

// 프로토타입 체이닝
console.log(foo.hasOwnProperty('name')); // true

// Person.prototype 객체 출력
console.dir(Person.prototype);
  1. foo 객체의 생성자는 Person() 함수입니다. 따라서 자바스크립트 룰에 따르면 foo 객체의 프로토타입 객체는 자신을 생성한 Person 생성자 함수 객체의 prototype 프로퍼티가 가르키는 객체가 됩니다. 즉, 이를 정리하면 foo 객체의 프로토타입 객체는 Person.prototype이 됩니다.

  2. foo.hasOwnProperty() 메서드를 호출했지만, foo 객체는 hasOwnProperty() 메서드가 없어서 프로토타입 체이닝으로 foo 부모 객체인 Person.prototype 객체에서 hasOwnProperty() 메서드를 찾습니다. 그러나 위에서 prototype 프로퍼티에서 알아봤듯이 함수에 연결된 프로토타입 객체는 디폴트로 constructor 프로퍼티만을 가진 객체이므로 hasOwnProperty() 메서드는 존재하지 않습니다.

  3. 다음 그림은 크롬 브루아저에서 Person.prototype 객체를 출력한 것입니다. 설명한 대로 constructor 프로퍼티만 있는 것을 확인할 수 있습니다.

실행 결과

image

하지만 Person.prototype 역시 자바스크립트 객체이므로 앞 절에서 설명했던 것처럼 Object.prototype을 프로토타입 객체로 가집니다. 따라서 프로토타입 체이닝은 Object.prototype 객체로 계속 이어집니다. 그리고 Object.prototype 객체의 hasOwnPrototype 메서드가 실행되므로 에러가 발생하지 않고 true가 출력됩니다.

생성자 함수 방식에서 객체와 프로토타입 객체의 관계

Untitled Diagram (2)

이렇듯 객체 리터럴 방식과 생성자 함수로 객체를 생성하는 방식이 프로토타입 체이닝 방법에서도 차이가 있음을 알 수 있습니다.

프로토타입 체이닝의 종점

자바스크립트에서 Object.prototype 객체는 프로토타입 체이닝의 종점입니다. 앞에서 살펴봤듯이 객체 리터럴 방식이나 생성자 함수를 이용한 방식이나 결국엔 Object.prototype 객체가 가진 프로퍼티와 메서드에 접근하고, 서로 공유가 가능하다는 것을 알 수 있습니다.

Untitled Diagram

때문에 자바스크립트 표준 빌트인 객체인 Object.prototype에는 hasOwnProperty()나 isPrototypeOf() 등과 같이 모든 객체가 호출 가능한 표준 메서드들이 정의되어 있습니다.

기본 데이터 타입 확장

앞서 자바스크립트의 모든 객체가 프로토타입 체이닝으로 Object.prototype에 정의한 메소드를 사용 가능하다는 것을 살펴봤습니다. 즉, Object.prototype에 정의된 메서드들은 자바스크립트의 모든 객체의 표준 메서드라고 볼 수 있습니다. 자바스크립트 모든 객체에서 호출 가능한 hasOwnProperty()나 isPrototypeOf() 등과 같은 표준 메서드들은 Object.prototype에 정의되어 있습니다. 이와 같은 방식으로 자바스크립트의 숫자, 문자열, 배열 등에서 사용되는 표준 메서드들의 경우, 이들의 프로토타입인 Number.prototype, String.prototype, Array.Prototype 등에 정의되어 있습니다. 물론 이러한 기본 내장 프로토타입 객체 또한 Object.prototype을 자신의 프로토타입으로 가지고 있어 프로토타입 체이닝 으로 연결됩니다. ECMAScript 명세서를 보면 자바스크립트의 각 네이티브 객체별로 공통으로 제공해야 하는 메서드들은 각각의 프로토타입 객체 내에 메서드로 정의해야한다고 기술하고 있습니다.

자바스크립트는 Object.prototype, String.prototype 등과 같이 표준 빌트인 프로토타입 객체에도 사용자가 직접 정의한 메서드들을 추가하는 것을 허용합니다.

가령, 다음 예제처럼 String.prototype 객체에 testMethod() 메서드를 추가하면 에 메서드는 일반 문자열 표준 메서드처럼, 모든 문자열에서 접근 가능합니다.

기본 타입에 메서드 추가

String.prototype.testMethod = function () {
  console.log('This is the String.prototype.testMethod()');  
};

var str = "this is test";
str.testMethod();

console.dir(String.prototype);

문자열의 표준 빌트인 프로토타입 객체 String.prototype에 testMethod()를 추가한 것입니다. 이제 이후 문자열에서는 testMethod()를 문자열 API처럼 사용할 수 있습니다.

str 변수에 문자열을 생성한 후 위에서 생성한 testMethod()를 호출했습니다. 때문에 프로토타입 체이닝으로 String.prototype에 정의한 testMethod()가 호출한 것이 가능합니다.

프로토타입도 자바스크립트 객체입니다.

함수가 생성될 때, 자신의 prototype 프로퍼티에 연결되는 프로토타입 객체는 디폴트로 constructor 프로퍼티만을 가진 객체입니다. 당연히 프로토타입 객체 역시 자바스크립트 객체이므로 일반 객체처럼 동적으로 프로퍼티 추가/삭제하는 것이 가능합니다. 그리고 이렇게 변경된 프로퍼티는 실시간으로 프로토타입 체이닝에 반영됩니다.

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

// foo 객체 생성
var foo = new Person('foo');

// foo.sayHello(); 

// 프로토타입 객체에 sayHello() 메서드 정의
Person.prototype.sayHello = function () {
    console.log('Hello');
}

foo.sayHello();  // Hello

실행 결과

스크린샷 2020-02-20 오전 12 34 52

  1. 여기 foo.sayHello() 코드의 주석을 제거하면, foo 객체는 sayHello() 메서드가 정의되어 있지 않아 에러가 발생합니다.

  2. foo 객체의 프로토타입 객체인 Person.prototype 객체에 동적으로 sayHello() 메서드를 추가했습니다.

  3. foo 객체에서 sayHello() 메서드를 호출합니다. 이때 foo 객체는 sayHello() 메서드가 없지만, 프로토타입 체이닝으로 Person.prototype 객체에서 sayHello() 메서드를 검색합니다. 그리고 sayHello() 메서드가 정의되어 있어서 Hello가 정상적으로 출력됩니다.

프로토타입 메서드와 this 바인딩

프로토타입 객체는 메서드를 가질 수 있습니다. 만약 프로토타입 메서드 내부에서 this를 사용한다면 이는 어디에 바인딩 될 까요? 이에 대한 해답은 앞에서 살펴본 객체의 메서드를 호출할 때 this 바인딩에서 설명한 this 바인딩 규칙을 그대로 적용하면 됩니다. 결국, 메서드 호출 패턴에서의 this는 그 메서드를 호출한 객체에 바인딩된다는 것을 기억합시다.

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

// getName() 프로토타입 메서드
Person.prototype.getName = function() {
    return this.name;
};

// foo 객체 생성
var foo = new Person('foo');

console.log(foo.getName());

// Person.prototype 객체에 name 프로퍼티 동적 추가
Person.prototype.name = 'person';

console.log(Person.prototype.getName());
  1. Person.prototype 객체에 getName() 메서드를 작성합니다. 이 메서드 코드 내부에는 this를 포함합니다.

  2. foo 객체에서 getName() 메소드를 호출하면, getName() 메소드는 foo 객체에 존재하지 않기 때문에 프로토타입 체이닝이 발생합니다. foo 객체의 프로토타입 객체인 Person.prototype에서 getName() 메서드가 있으므로, 이 메서드가 호출됩니다. 이때 getName() 메서드를 호출한 객체는 foo이므로, this는 foo 객체에 바인딩 됩니다. 따라서 foo.getName() 결과괎으로 foo가 출력됩니다.

  3. Person.prototype.getName() 메서드와 같이 프로토타입 체이닝이 아니라, 바로 Person.prototype 객체에 접근해서 getName() 메서드를 호출하면 어떻게 될까요? 이때는 getName() 메서드를 호출한 객체가 Person.prototype이므로 this도 여기에 바인딩 됩니다. 그리고 Person.prototype 객체에 name 프로퍼티를 동적으로 추가하고 person을 저장했으므로 this.name은 person이 출력됩니다.

디폴트 프로토타입은 다른 객체로 변경이 가능합니다.

디폴트 프로토타입 객체는 함수가 생성될 때 같이 생성되며, 함수의 prototype 프로퍼티에 연결됩니다. 자바스크립트에서는 이렇게 함수를 생성할 때 해당 함수와 연결되는 디폴트 프로토타입 객체를 다른 일반 객체로 변경하는 것이 가능합니다. 이러한 특징을 이용해서 객체지향의 상속을 구현합니다. 여기서 주의할 점이 있는데 생성자 함수의 프로토타입 객체가 변경되면, 변경된 시점 이후에 생성된 객체들은 변경된 프로토타입 객체로 [[Prototype]] 링크를 연결한다는 점을 기억해야 합니다. 이에 반해 생성자 함수의 프로토타입이 변경되기 이전에 생성된 객체들은 기존의 프로토타입 객체로의 [[Prototype]] 링크를 그대로 유지합니다.

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

console.log(Person.prototype.constructor);

// foo 객체 생성
var foo = new Person('foo');
console.log(foo.country);

// 디폴트 프로토타입 객체 변경
Person.prototype = {
    country: 'korea'
};

console.log(Person.prototype.constructor);

// bar 객체 생성
var bar = new Person('bar');
console.log(foo.country);
console.log(bar.country);
console.log(foo.constructor);
console.log(bar.constructor);

객체의 프로퍼티 읽기나 메서드를 실행할 때만 프로토타입 체이닝이 동작합니다.

객체의 특정 프로퍼티를 읽으려고 할 때, 프로퍼티가 해당 객체에 없는 경우 프로토타입 체이닝이 발생합니다. 반대로 객체에 있는 특정 프로퍼티에 값을 쓰려고 한다면 이때는 프로토타입 체이닝이 일어나지 않습니다. 이는 당연한 애기입니다. 자바슼크립트는 객체에 없는 프로퍼티에 값을 쓰려고 할 경우 동적으로 객체에 프로퍼티를 추가하기 때문입니다.

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

Person.prototype.country = 'Korea';

var foo = new Person('foo');
var bar = new Person('bar');

console.log(foo.country);
console.log(bar.country);
// 프로퍼티에 쓰기를 하기 때문에 프로토타입 체이닝이 발생하지 않고 동적으로 추가됨.
foo.country = "USA";

console.log(foo.country);
console.log(bar.country);

foo와 bar 객체 둘 다 Person.prototype 객체를 프로토타입으로 가집니다.

  1. foo.country에 접근하려고 했을 때 foo 객체는 name 프로퍼티밖에 없으므로 프로퍼티타입 체이닝이 이뤄지면서 foo의 프로토타입 객체인 Person.prototype의 country 프로퍼티값인 'Korea'가 출력됩니다.

  2. 1과 반대로 foo.country 값에 USA라는 값을 저장하면, 프로토타입 체이닝이 동작하는 것이 아니라, foo 객체에 country 프로퍼티값이 동적으로 생성됩니다.

  3. 그러므로 foo.country는 프로토타입 체이닝 없이 바로 USA 값이 출력되는 반면, bar 객체는 프로토타입 체이닝을 거쳐 Korea가 출력됩니다.

참조: 인사이드 자바스크립트