객체지향 프로그래밍

자바스크립트는 여러 가지 특성으로 객체지향 언어의 특징을 구현해낼 수 있습니다. 초창기 더글라스 크락포드가 소개한 상속과 캡슐화 등으로 자바스크립트가 객체지향 프로그래밍이 가능함이 알려졌고, 그 후 많은 개발자가 자바스크립트로 객체지향적인 구현 방법을 고민하기 시작했습니다. 이번 장에서는 다음에 나오는 객체지향 언어의 특성을 자바스크립트로 구현하는 방법을 살펴보겠습니다.

  • 클래스, 생성자, 메서드
  • 상속
  • 캡슐화

먼저 이번 장을 읽기 전에 객체지향 언어로서 클래스 기반의 언어와 프로토타입 기반의 언어를 간단하게나마 구분해서 알아야 할 필요가 있습니다. 클래스 기반의 언어는 클래스로 객체의 기본적인 형태와 기능을 정의하고, 생성자로 인스턴스를 만들어서 사용할 수 있습니다. 클래스에 정의된 메서드로 여러가지 기능을 수행할 수 있습니다. 우리에게 널리 알려진 Java, C++과 같은 언어가 이에 해당합니다. 이런 유형의 언어는 모든 인스턴스가 클래스에 정의된 대로 같은 구조이고 보통 런타임에 바꿀 수 없습니다. 반면에 프로토타입 기반의 언어는 객체의 자료구조, 메서드 등을 동적으로 바꿀수 있습니다. 이는 마치 정적 타입의 언어와 동적 타입의 언어의 차이와 거의 비슷하게 보입니다. 마찬가지로 장단점도 명확합니다. 정확성, 안전성, 에측성 등의 관점에서 클래스 기반 언어는 프로토타입 기반의 언어보다 좀 더 나은 결과를 보장합니다. 하지만 프로토타입 기반의 언어는 동적으로 자유롭게 객체의 구조와 동작 방식을 바꿀 수 있다는 장점이 있습니다. 자바스크립트는 프로토타입 기반의 언어입니다. 따라서 이번 장을 이해하려면 저번 포스팅을 했던 프로토타입을 잘 이해해야 합니다.

프로토타입은 자바스크립트로 객체지향적으로 구현하는 필수 요소이므로 확실히 이해하는게 좋습니다.

클래스, 생성자, 메서드

C++이나 Java와 같은 경우 class라는 키워드를 제공하여 프로그래머는 클래스를 만들 수 있습니다. 클래스와 같은 이름의 메서드로 생성자를 구현해냅니다. 하지만 자바스크립트에서는 이러한 개념이 없습니다. 게속해서 강조했듯이 자바스크립트는 거의 모든것이 객체이고, 특히 함수 객체로 많은 것을 구현해냅니다. 클래스, 생성자, 메서드 모두 함수로 구현이 가능합니다. 구체적으로 살펴보기에 다시한번 자바스크립트의 프로토타입과 new 연산자를 살펴보겠습니다.

function Person(arg) {

    this.name = arg;

    this.getName = function() {
        return this.name;
    }

    this.setName = function(value) {
        this.name = value;
    }
}

var me = new Person("zzoon");
console.log(me.getName()); // 출력값: zzoon

me.setName("junyoung");
console.log(me.getName()); // 출력값: junyoung

위 예제에서 new 키워드로 새로운 객체 me를 만들었음을 주목합시다.

var me = new Person("zzoon");

이 형태는 기존 객체지향 프로그래밍 언어에서 한 클래스의 인스턴스를 생성하는 코드와 매우 유사합니다. 함수 Person이 클래스이자 생성자의 역할을 합니다. 자바스크릡트에서 클래스 기반의 객체지향 프로그래밍은 기본적인 형태가 이와 같습니다. 클래스 및 생성자의 역할을 하는 함수가 있고, 사용자는 new 키워드로 인스턴스를 생성하여 사용할 수 있습니다. 예제에서 생성된 me는 Person의 인스턴스로서 name 변수가 있고, getName()과 setName() 함수가 있습니다.

하지만 이 예제는 문제가 많습니다. 정확히는 이 예제의 Person 함수의 구현이 바람직하지 못합니다. 이 Person을 생성자로 하여 여러 개의 객체를 생성한다고 가정해보겠습니다.

var me = new Person("me");
var you = new Person("you");
var him = new Person("him");

이와 같이 객체를 생성하여 사용하면 겉으로는 별 문제 없이 작동하는 것을 볼 수 있습니다. 하지만 각 객체는 자기 영역에서 공통으로 사용할 수 있는 setName() 함수와 getName() 함수를 따로 생성하고 있습니다. 이는 불필요하게 중보고디는 영역을 메모리에 올리고 사용함을 의미하고 자원 낭비를 가져옵니다. 이를 그림으로 표현하면 아래와 같습니다.

Untitled Diagram

따라서 앞의 문제를 해결하려면 다른 방식의 접근이 필요한데, 여기서 활용할수 있는 자바스크립트의 특성이 함수 객체의 프로토타입입니다. 다음 코드를 살펴보겠습니다.

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

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

Person.prototype.setName = function(value) {
    this.name = value;
}

var me = new Person("me");
var you = new Person("you");
console.log(me.getName());  // 출력값: me
console.log(you.getName()); // 출력값: junyounyoug

위 예제 코드에서는 Person 함수 객체의 prototype 프로퍼티에 getName()과 setNmae() 함수를 정의하였습니다. 이 Person으로 객체를 생성한다면 각 객체는 각자 따로 함수 객체를 생성할 필요없이 getName()과 setNmae() 함수를 프로토타입 체인으로 접근할 수 있습니다. 이를 그림으로 표현하면 다음과 같습니다.

Untitled Diagram (1)

이와 같이 자바스크립트에서 클래스 안의 메서드를 정의할 때는 프로토타입 객체에 정의한 후, new로 생성한 객체에서 접근할 수 있게 하는 것이 좋습니다. 더글라스 크락포드는 다음과 같은 함수를 제시하면서 메서드를 정의하는 방법을 소개합니다.

Function.prototype.method = function(name, func) {
    if(!this.prototype[name]) {
        this.prototype[name] = func;
    }
}

이 함수를 활용하면 예제는 다음과 같은 형태가 됩니다.

Function.prototype.method = function(name, func) {
    this.prototype[name] = func;
}

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

Person.method("setName", function(value)) {
    this.name = value;
}

Person.method("getName", function() {
    return this.name;
});


var me = new Person("me");
var you = new Person("you");
console.log(me.getName());  
console.log(you.getName()); 

더글라스 크락포드는 함수를 생성자로 사용하여 프로그래밍하는 것을 추천하지 않습니다. 그 이유는 생성된 함수는 new로 호출될 수 있을 뿐만 아니라, 직접 호출도 가능하기 때문입니다. 여기서 문제는 new로 호출될 때와 직접 호출될 때의 this에 바인딩되는 객체가 달라진다는 것입니다. 크락포드는 이러한 문제 때문에, 일단 생성자로 사용되는 함수는 첫 글자를 대문자로 표기할 것을 권고하고 있습니다.

상속

자바스크립트는 클래스를 기반으로 하는 전통적인 상속을 지원하지 않습니다. 하지만 자바스크립트 특성 중 객체 프로토타입 체인을 이용하여 상속을 구현해낼 수 있습니다. 이러한 상속 구현 방식은 크게 두 가지로 구분할 수 있는데, 하나는 클래스 기반 전통적인 상속 방식을 흉내내는 것이고, 다른 하나는 클래스 개념 없이 객체의 프로토타입으로 상속을 구현하는 방식입니다. 이를 프로토타입을 이용한 상속이라고 합니다. 자바나 C++에 익숙한 개발자는 전통적인 상속의 형태가 익숙할 것입니다. 클래스와 생성자 등의 개념이 들어가 있기 때문입니다. 하지만 프로토타입을 이용한 상속은 객체 리터럴을 중심으로 철저히 프로토타입을 이용하여 상속을 구현해낸다, 이 책에서는 프로토타입을 이용한 상속을 먼저 소개하고, 클래스 기반의 상속을 소개하겠습니다.

프로토타입을 이용한 상속

다음 예제를 살펴보겠습니다.

function create_object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

이 코드는 더글라스 크락포드가 자바스크립트 객체를 상속하는 방법으로 오래 전에 소개한 코드입니다. 조금 과장해서 말하면 이 세줄의 코드를 이해하면 자바스크립트에서 프로토타입 기반의 상속을 다 배운 것이나 다름없다고 합니다?... 전 아직도 모르겠습니다

이 세줄의 코드는 언뜻 보면 쉬워 보이지만 이해하기 쉬운 코드는 아닙니다.

다음 그림을 보겠습니다.

Untitled Diagram

create_object() 함수는 인자로 들어온 객체를 부모로 하는 자식 객체를 생성하여 반환합니다. 그림을 보면 새로운 빈 함수 객체 F를 만들고, F.prototype 프로퍼티에 인자로 들어온 객체를 참조합니다. 함수 객체 F를 생성자로 하는 새로운 객체를 만들어 반환합니다. 이렇게 반환된 객체는 부모 객체의 프로퍼티에 접근할 수 있고, 자신만의 프로퍼티를 만들수도 있습니다. 이렇게 프로토타입의 특성을 활용하여 상속을 구현하는 것이 프로토타입 기반의 상속입니다. 참고로 앞에서 소개한 Object() 함수는 ECMAScript 5에서 Object.create() 함수로 제공되므로, 따로 구현할 필요는 없습니다. 이 함수의 코드로 프로토타입 기반 상속의 이해를 돕고자 사용한 것입니다.

다음 예제는 앞에서 소개한 create_object() 함수를 이용하여 상속을 구현한 예제입니다.

var person = {
    name: "junyoung",
    getName: function() {
        return this.name;
    },
    setName: function (arg) {
        this.name = arg;
    }
};

function create_object(o) {

    function F() {};
    F.prototype = o;
    return new F();
}


var student = create_object(person);

student.setName("junyoung");
console.log(student.getName()); // 출력값: me

Person 객체를 상속하여 Student 객체를 만들었습니다. 프로토타입 기반 상속의 특징이 보이나요? 클래스에 해당하는 생성자 함수를 만들지도 않았고, 그 클래스의 인스턴스를 따로 생성하지도 않았습니다. 단지 부모 객체에 해당하는 person 객체와 이 객체를 프로토타입 체인으로 참조할수 있는 자식 객체 Student를 만들어서 사용하였습니다. 이와 같은 방식으로 상속의 개념을 구현하였습니다.
그림을 표현하면 다음과 같습니다.

Untitled Diagram (1)

지금까지는 부모 객체의 메서드를 그대로 상속받아 사용하는 방법을 살펴보았습니다.
여기에서 자식은 자신의 메서드를 재정의 혹은 추가로 기능을 확장시킬수 있어야 합니다.

student.setAge = function(age) { ... }
student.getAge = function() { .... }

단순히 앞과 같이 그 기능을 확장시킬 수는 있습니다. 하지만 이렇게 구현하면 코드가 지저분해지기 십상입니다. 보다 깔끔한 방법을 생각해봅시다. 자바스크립트에서는 범용적으로 extend()라는 이름의 함수로 객체에 자신이 원하는 객체 혹은 함수를 추가시킵니다. 여기서는 가장 유명한 자바스크립트 라이브러리 중 하나인 jQuery의 extend() 함수를 살펴보고 이를 활용하는 방법을 생각해봅시다. jQuery의 1.0의 extend 함수는 다음과 같이 구현되었습니다.

jQuery.extend = jQuery.fn.extend = function(obj, prop) {
    if(!prop) { prop = obj; obj = this; }
    for(var i in prop) obj[i] = prop[i];
    return obj;
};

이 코드를 분석해봅시다.

jQuery.extend = jQuery.fn.extend = ...

jQuery.fn은 jQuery.prototype이다. 따라서 앞 코드가 의마하는 바는 jQuery 함수 객체와 jQuery 함수 객체의 인스턴스 모두 extend 함수가 있겠다는 말입니다. 즉, jQuery.extend()로 호출할 수도 있고, var elem = new jQuery(...); elem.extend(); 형태로도 호출할 수 있음을 뜻합니다.

extend() 함수 추가 예제

var person = {

    name: "junyoung",
    getName: function() {
        return this.name;
    },

    setName: function(arg) {
        this.name = arg;
    }
};


function create_object(o) {
    function F() {};
    F.prototype = o;
    return new F();
}

function extend(obj, prop) {
    if(!prop) { prop = obj; ojb = this; }
    for(var i in prop) obj[i] = prop[i];
    return obj;
}

var student = create.object(person);

var added = {
    setAge: function(age) {
        this.age = age;
    },
    getAge: function() {
        return this.age;
    }
};

extend(student, added);

student.setAge(25);
console.log(student.getAge());

Untitled Diagram

위 예제에서는 얕은 복사를 사용하는 extend() 함수를 사용하여 student 객체를 확장시켰습니다. extend() 함수는 사용자에게 유연하게 기능을 확장을 할 수 있게 하는 주요 함수일 뿐만 아니라, 상속에서도 자식 클래스를 확장할 때 유용하게 사용되므로 반드시 기억하라. 객체지향 프로그래밍 응용 예제에서는 프로토타입 체인과 extend() 함수를 이용하여 사용자가 상속을 직관적으로 쉽게 구현할 수 있게 도와주는 함수를 소개합니다.

클래스 기반의 상속

클래스 기반의 상속을 소개할 차레입니다. 사실 클래스 기반의 상속이라고는 하나, 원리는 6.2.1 프로토타입을 이용한 상속에서 소개한 내용과 거의 같습니다. 앞 절처럼 함수의 프로토타입을 적절히 엮어서 상속을 구현해냅니다. 다만 앞 절에서는 객체 리터럴로 생성된 객체의 상속을 소개했지만, 여기서는 클래스의 역할을 하는 함수로 상속을 구현했습니다.

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

Person.prototype.setName = function(value) {
    this.name = value;
};

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

function Student(arg) {

}

var you = new Person("iamhjoo");
Student.prototype = you;

var me = new Student("zzoon");
me.setName("zzoon");
console.log(me.getName());

앞 예제에서 Student 함수 객체를 만들어서, 이 함수 객체의 프로토타입으로 하여금 Person 함수 객체의 인스턴스를 참조하게 만들었습니다. 이렇게 하면 Student 함수 객체로 생성된 객체 me의 [[Prototype]] 링크가 생성자의 프로토타입 프로퍼티 Student.prototype인 you를 가리키고, new Person()으로 만들어진 객체의 [[Prototype]] 링크는 Person.prototype을 가르키는 프로토타입 체인이 형성됩니다. 따라서 객체 me는 Person.prototype 프로퍼티에 접근할 수 있고, setName()과 getName()을 호출할 수 있습니다.

하지만, 여기에 함정 카드가 있습니다. 먼저 me 인스턴스를 생성할 때 부모 클래스인 Person의 생성자를 호출하지 않습니다.

var me = new Student("zzoon");

이 코드로 me 인스턴스를 생성할 때 "zzoon"을 인자로 넘겼으나, 이를 반영하는 코드는 어디에도 없습니다. 결국 생성된 me 객체는 빈 객체입니다. setName() 메소드가 호출되고 나서야 me 객체에 name 프로퍼티가 만들어집니다. 이렇게 부모의 생성자가 호출되지 않으면, 인스턴스의 초기화가 제대로 이루어지지 않아 문제가 발생할 수 있습니다. 이를 해결하려면 Student 함수에 다음 코드를 추가하여 부모 클래스의 생성자를 호출해야 합니다.

function Student(arg) {
    Person.apply(this, arguments);
}

Student 함수 안에서 새롭게 생성된 객체를 apply 함수의 첫 번째 인자로 넘겨 Person 함수를 실행시킵니다. 이런 방식으로 자식 클래스의 인스턴스에 대해서도 부모 클래스의 생성자를 실행시킬 수 있습니다. 클래스 간의 상속에서 하위 클래스의 인스턴스를 생성할 때, 부모 클래스의 생성자를 호출해야 하는데, 이 경우에 필요한 방식입니다.

여기서 조금만 더 발전시켜 봅시다. 현재는 자식 클래스의 객체가 부모 클래스의 객체를 프로토타입체인으로 직접 접근합니다. 하지만 부모 클래스의 인스턴스와 자식 클래스의 인스턴스는 서로 독립적인 필요가 있습니다. 다음 그림을 보면 이해가 쉬울 것 입니다.

Untitled Diagram (2)

위 그림에서 자식 클래스의 prototpye이 부모 클래스의 인스턴스를 참조합니다.
이 구조는 자식 클래스의 prototype에 메소드를 추가할 때 문제가 됩니다. 이는 부모 클래스의 인스턴스인 you와 자식 클래스의 인스턴스인 me가 독립적이어야 함을 의미합니다.

두 클래스 사이에 중개자를 하나 만들어보겠습니다.

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

Function.prototype.method = function(name, func) {
    this.prototype[name] = func;
}

Person.method("setName", function(value) {
    this.name = value;
});

Person.method("getName", function(value) {
    return this.name;
});

function Student(arg) {

}

function F() {};
F.prototype = new F();
Student.prototype = new F();
Student.prototype.constructor = Student;

Student.super = Person.prototype;

var me = new Student();
me.setName("zzoon");
console.log(me.getName());

위 예제의 프로토타입 체인 형성 과정은 프로토타입을 이용한 상속의 상속 방식과 매우 유사합니다. 어차피 함수의 프로토타입을 이용한 것이니 비슷할 수 밖에 없습니다. 여기에서도 빈 함수 F()를 생성하고, 이 F()의 인스턴스를 Person.prototpye과 Student 사이에 두었습니다. 그리고 이 인스턴스를 Student.prototype에 참조되게 합니다. 다음 그림으로 이해해봅시다.

Untitled Diagram

그림을 보면 자식 클래스의 prtotype 객체는 빈 객체 입니다. 따라서 이 곳에, 자식 클래스의 확장된 메소드와 데이터가 들어갈 수 있습니다.

각 클래스의 객체인 me와 you가 아무런 관계가 없이 독립적입니다.
그리고 보는 바와 같이 빈 함수 객체를 중간에 두어 Person의 인스턴스와 Student의 인스턴스를 서로 독립적으로 만들었습니다. 이제 Person 함수 객체에서 this에 바인딩되는 것이 Student의 인스턴스가 접근할 수 없습니다. 이 상속이 앞서 소개된 상속보다 좀 더 나은 코드입니다.

캡슐화

캡슐화는 객체지향 프로그래밍에서 상당히 중요한 부분을 담당합니다. 캡슐화란 기본적으로 관련된 여러 가지 정보를 하나의 틀 안에 담는 것을 의미합니다. 이를 응용하면 맴버 변수와 메서드가 서로 관련된 정보가 되고 클래스가 이것을 담는 하나의 큰 틀이라고 할 수 있습니다. 여기에서 중요한 것은 정보의 공개 여부입니다. 정보 은닉의 개념이 바로 이 부분을 담당합니다. C++이나 Java에서는 public, private 맴버를 선언함으로써 해당 정보를 외부로 노출시킬지 여부를 결정합니다. 하지만 자바스크립트는 이러한 키워드 자체를 지원하지 않습니다. 그렇다고 해서 자바스크립트에서 정보 은닉이 불가능한 것은 아닙니다. 다음 예제를 살펴보겠습니다.

var Person = function(arg) {
    var name = arg ? arg : "zzoon";

    this.getName = function() {
        return name;
    }
    this.setName = function(arg) {
        name = arg;
    }
};

var me = new Person();
console.log(me.getName());
me.setName("iamhjoo");
console.log(me.getName());
console.log(me.name) // 출력값 undefined

위 예제에서 private 맴버로 name을 선언하고, public 메서드로 getName()과 setName()을 선언하였습니다. 앞서 배운것 처럼 this 객체의 프로퍼티로 선언하면 외부에서 new 키워드로 생성한 객체로 접근할 수 있습니다. 하지만 var로 선언된 맴버들은 외부에서는 접근이 불가능합니다. 그리고 public 메서드가 클로저 역할을 하면서 private 맴버인 name에 접근할 수 있습니다. 이것이 자바스크립트에서 할 수 있는 기본적인 정보 은닉 방법입니다.

var Person = function(arg) {
    var name = arg ? arg : "zzoon";

    return {
        getName: function() {
            return name;
        },
        setName: function(arg) {
            name = arg;
        }
    }
}

var me = new Person();
console.log(me.getName());

위 예제는 Person 함수를 호출하여 객체를 반환받습니다. 이 객체에 Person 함수의 private 맴버에 접근할 수 있는 메서드들이 담겨있다. 사용자는 반환받는 객체로 메서드를 호출할 수 있고, private 맴버에 접근할 수 있습니다. 이렇게 메서드가 담겨있는 객체를 반환하는 함수는 여러 유명 자바스크립트 라이브러리에서 쉽게 볼 수 있는 구조입니다. 다만 한 가지 주의할 점이 있습니다. 접근하는 private 맴버가 객체나 배열이면 얕은 복사로 참조만을 반환하므로 사용자가 이후 이를 쉽게 변경할 수 있습니다. 다음 예제는 이러한 문제를 잘보여줍니다.

var ArrCreate = function(arg) {
    var arr = [1, 2, 3];

    return {
        getArr: function() {
            return arr;
        }
    };
}

var obj = new ArrCreate();
var arr = obj.getArr();
arr.push(5);
console.log(obj.getArr()); // 출력값 : [1, 2, 3, 5]

이와 같은 문제가 있으므로 프로그래머는 객체를 반환하는 경우 신중해야 합니다. 보통의 경우, 객체를 반환하지 않고 객체의 주요 정보를 새로운 객체에 담아서 반환하는 방법을 많이 사용합니다. 하지만 꼭 객체가 반환되어야 하는 경우에는 깊은 복사로 복사본을 만들어서 반환하는 방법을 사용하는 것이 좋습니다.

다시 예제로 돌아가보겠습니다. 이 예제에서 사용자가 반환받는 객체는 Person 함수 객체의 프로토타입에는 접근할 수 없다는 단점이 있습니다. 이는 Person을 부모로 하는 프로토타입을 이용한 상속을 구현하기가 용이하지 않다는 것을 의미합니다. 이를 보완하려면 객체를 반환하는 것이 아닌, 함수를 반환하는 것이 좋습니다.

var Person = function(arg) {
    var name = arg ? arg : "zzoon";

    var Func = function() {}

    Func.prototype = {

        getName: function() {
            return name;
        },

        setName: function(arg) {
            name = arg;
        }
    };

    return Func;
}();

var me = new Person();
console.log(me.getName());

클로저를 활용하여 name에 접근할 수 없게 했습니다. 즉시 실행 함수에서 반환되는 Func이 클로저가 되고 이 함수가 참조하는 name 프로퍼티가 자유 변수가 됩니다. 따라서 사용자는 name에 대한 접근이 불가능합니다.

이와 같이 자바스크립트에서 캡슐화를 구현하는 방법 역시 다양합니다. 실제로 여기에 소개된 패턴은 많은 자바스크립트 라이브러리에서 사용되고 있으므로, 이들을 잘 분석하고 장단점을 잘 구분할 수 있다면 본인이 작성하는 코드를 보다 더 효율적으로 만들수 있습니다. 실제로 위 예제와 같은 패턴을 모듈 패턴이라고 하는데 꽤 유용한 패턴입니다.

객체지향 프로그래밍 응용 예제

클래스의 기능을 가진 subClass 함수

프로토타입을 이용한 상속과 클래스 기반의 상속에서 소개한 내용을 바탕으로 기존 클래스와 같은 기능을 하는 자바스크립트 함수를 만들어 보겠습니다. 이 함수에서는 앞서 소개한 다음 세 가지를 활용해서 구현합니다. 함수의 이름은 subClass로 하겠습니다.

  • 함수의 프로토타입 체인
  • extend 함수
  • 인스턴스를 생성할 때 생성자 호출

subClass 함수 구조

subClass는 상속받은 클래스에 넣을 변수 및 메서드가 담긴 객체를 인자로 받아 부모 함수를 상속받는 자식 클래스를 만듭니다. 여기서 부모 함수 subClass() 함수를 호출할 때 this 객체를 읨합니다.
예를들면 다음과 같습니다.

var subClass = subClass(obj);
var subClass = SuperClass.subClass(obj);

이처럼 SuperClass를 상속받는 subClass를 만들고자 할 때, SuperClass.subClass()의 형식으로 호출하게 구현합니다. 참고로 최상위 클래스인 SuperClass는 자바스크립트의 Function 함수를 상속받게 합니다.

함수 SubClass 구조는 아래와 같이 구성됩니다.

function subClass(obj) {

    /* (1) 자식 클래스 (함수 객체) 생성 */
    /* (2) 생성자 호출 */
    /* (3) 프로토타입 체인을 활용한 상속 구현 */
    /* (4) obj를 통해 들어온 변수 및 메서드를 자식 클래스에 추가 */
    /* (5) 자식 함수 객체 변환 */
}

자식 클래스 생성 및 상속

function subClass(obj){    
    ........

    var parent = this;
    var F = function() {};

    var child = function() {

    };

    F.prototype = parent.prototype;
    child.prototype = new F();
    child.prototype.constructor = child;
    child.parent = parent.prototype;
    child.parent_constructor = parent;

    .........
    return child;
}

자식 클래스는 child라는 이름의 함수 객체를 생성함으로써 만들어졌습니다. 부모 클래스를 가르키는 parent는 this를 그대로 참조합니다. 그리고 프로토타입 체인 구성은 클래스 기반 상속에서 설명된 방식을 그대로 사용하였습니다.

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

실행 컨텍스트와 클로저

이번 장에서는 자바스크립트가 실행될 때 생성되는 하나의 실행 단위인 실행 컨텍스트, 변수의 유효 범위, 그리고 클로저에 대해서 포스팅하겠습니다. 이는 자바스크립트에서 매우 중요한 개념입니다. 이들을 정확히 이해하지 못한다면 다른 사람이 작성한 자바스크립트 코드를 이해하기 어렵고, 활용하기 불편할뿐만 아니라 디버깅도 굉장히 어렵습니다.

여기서 살펴볼 내용은 다음과 같습니다.

  1. 실행 컨텍스트의 개념
  2. 활성 객체와 변수 객체
  3. 스코프 체인
  4. 클로저

실행 컨텍스트의 개념

기존언어를 경험한 독자라면, 콜 스택을 들어보았을 것입니다. 이는 함수를 호출할 때 해당 함수의 호출 정보(C 언어를 예로 들면, 함수의 호출 정보 등으로 함수 내 지역 변수 혹은 인자값 등)가 차곡차곡 쌓여있는 스택을 의미합니다. 가령 C언어의 경우는 함수가 호출될 때마다 해당 함수의 호출 정보가 기존 함수의 호출 정보 위에 스택 형태로 하나씩 쌓입니다. 따라서 C 개발자는 이러한 콜 스택의 호출 정보 등으로 코드의 실행 과정을 추적하여 디버깅과 같은 작업을 수행합니다. 이는 보통의 고급 프로그래밍 언어에서 흔하게 사용하는 방식이고, 자바스크립트 역시 이 범주를 크게 벗어나지 않습니다.

실행 컨텍스트는 실행 가능한 코드가 실행되기 위해 필요한 환경이라고 말할 수 있겠습니다.

실행 컨텍스트는 앞에서 설명한 콜 스택에 들어가는 실행 정보 하나와 비슷합니다. ECMAScript에서는 실행 컨텍스트를 실행 가능한 코드를 형상화하고 구분하는 추상적인 개념으로 기술합니다. 이를 앞서 설명한 콜스택과 연관하여 정의하면, 실행 가능한 자바스크립트 코드 블록이 실행되는 환경이라고 할 수 있고, 이 컨텍스트 안에 실행에 필요한 여러가지 정보를 담고 있습니다. 여기서 말하는 실행 가능한 코드 블록은 대부분의 경우 함수가 됩니다. ECMAScript에는 실행 컨텍스트가 형성되는 경우를 크게 세 가지로 규정하고 있는데 전역 콜, eval() 함수로 실행되는 코드, 함수 안의 코드를 실행한 경우입니다. 대부분 프로그래머는 함수로 실행 컨텍스트를 만듭니다. 그리고 이 코드 블록 안에 변수 및 객체, 실행 가능한 코드가 들어있습니다. 이 코드가 실행되면 실행 컨텍스트가 생성되고, 실행 컨텍스트는 스택 안에 차곡차곡 쌓이고, 제일 위에 위치하는 실행 컨텍스트가 현재 실행되고 있는 컨텍스트입니다. ECMAScript에서는 실행 컨텍스트의 생성을 다음처럼 설명합니다.
현재 실행되는 컨텍스트에서 이 컨텍스트와 관련 없는 실행 코드가 실행되면, 새로운 컨텍스트가 생성되어 스택에 들어가고 그 제어권이 그 컨텍스트로 이동합니다.

역시.. 무슨 말인지 이해가 안갔습니다. 예제로 알아보겠습니다.

console.log("This is global context");

function ExContext1() {
    console.log("This is ExContext1");
};

function ExContext2() {
    ExContext1();
    console.log("This is ExContext2");
};

ExContext2();

Untitled Diagram (1)

위의 그림과 같이 전역 실행 컨텍스트가 가장 먼저 실행됩니다. 여기서 전역 실행 컨텍스트는 사실 별 의미가 없습니다. 가장 먼저 실행되는 실행 컨텍스트일 뿐입니다. 다른 실행 컨텍스트와 다른 점은 이후에 설명하겠습니다. 이 과정에서 새로운 함수 호출이 발생하면 새로운 컨텍스트가 만들어지고 실행되며, 종료되면 반환됩니다. 이와 같은 과정이 반복된 후, 전역 실행 컨텍스트의 실행이 완료되면 모든 실행이 끝납니다.

실행 컨텍스트 생성 과정

이 장에서는 실행 컨텍스트 생성 과정을 설명하고, 활성화 객체와 변수 객체, 스코프 체인에 대한 중요 개념을 살펴보겠습니다.

  • 활성화 객체와 변수 객체
  • 스코프 체인

다음 예제를 실행하면 실행 컨텍스트가 어떻게 만들어질지 생각해봅시다.

function execute(param1, param2) {
    var a = 1, b = 2;

    function func() {
        return a + b; 
    }
    return param1 + param2 + func();
}

execute(3, 4);

자바스크립트 함수를 실행하여 실행 컨텍스트가 생성되면 자바스크립트 엔진은 다음과 같은 일을 정해진 순서대로 실행합니다. 예제에서 execute() 함수를 실행한 경우를 예로 설명하겠습니다.

활성화 객체 생성

실행 컨텍스트가 생성되면 자바스크립트 엔진은 해당 컨텍스트에서 실행에 필요한 여러가지 정보를 담은 객체를 생성하는데, 이를 활성 객체라고 합니다. 이 객체에 앞으로 사용하게 될 매개변수나 사용자가 정의한 변수 및 객체를 저장하고, 새로 만들어진 컨텍스트로 접근 가능하게 되어 있습니다. 이는 엔진 내부에서 접근할 수 있다는 것이지 사용자가 접근할 수 있다는 것은 아닙니다.

argument 객체 생성

다음 단계에서는 arguments 객체를 생성합니다. 이 객체에 대해서는 이전에 포스팅 했던 함수 프로토타입 체이닝 부분에서 설명하였습니다. 앞서 만들어진 활성 객체는 param1과 param2가 들어왔을 경우의 활성 객체의 상태를 표현합니다.

[그림 - 활성 객체]

Untitled Diagram

스코프 정보 생성

현재 컨텍스트의 유효 범위를 나타내는 스코프 정보를 생성합니다. 이 스코프 정보는 현재 실행 중인 실행 컨텍스트 안에서 연결 리스트와 유사한 형식으로 만들어집니다. 현재 컨텍스트에 특정 변수에 접근해야 할 경우, 이 리스트를 활용합니다. 이 리스트로 현재 컨텍스트의 변수뿐 아니라, 상위 실행 컨텍스트의 변수도 접근이 가능합니다. 이 리스트에서 찾지 못한 변수는 결국 정의되지 않는 변수에 접근하는 것으로 판단하여 에러를 검출합니다. 이 리스트를 스코프 체인이라고 하는데, [[scope]] 프로퍼티로 참조합니다. 이것이 왜 리스트의 형태를 띄고 있고, 이 체인이 어떻게 형성되며, 리스트의 구성 요소가 무엇인지는 이후에 좀 더 자세히 살펴보겠습니다. 여기서는 현재 생성된 활성 객체가 스코프 체인의 제일 앞에 추가되며, execute() 함수의 인자나 지역 변수 등에 접근할 수 있다는 것만 알고 넘어갑시다.

Untitled Diagram (1)

변수 생성

현재 실행 컨텍스트 내부에서 사용되는 지역 변수의 생성이 이루어집니다. ECMAScript에서는 생성되는 변수를 저장하는 변수 객체를 언급하는데, 실제적으로 앞서 생성된 활성 객체가 변수 객체로 사용됩니다. 우리가 자바스크립트 관련 문서를 읽다 보면 어떤 곳에서는 활성 객체, 어떤 곳에서는 변수 객체라고 사용되어 혼란스러운 경우가 있는데 두 객체가 같은 객체이므로, 혼동하는 일이 없기를 바랍니다.

변수 객체 안에서 호출된 함수 인자는 각각의 프로퍼티가 만들어지고 그 값이 할당됩니다. 만약 값이 넘겨지지 않았다면 undefined가 할당됩니다. 위의 예제에서 execute() 함수 안에 정의된 변수 a, b와 함수 func가 생성됩니다. 여기서 주의할 점은 이 과정에서 변수나 내부 함수를 단지 메모리에 생성하고, 초기화는 각 변수나 함수에 해당하는 표현식이 실행되기 전까지는 이루어지지 않는다는 점입니다. 따라서 변수 a와 b에는 먼저 undefined가 할당됩니다. 표현식의 실행은 변수 객체 생성이 다 이루어진 후 시작됩니다.

this 바인딩

마지막 단계에서 this 키워드를 사용하는 값이 할당됩니다. 이 값에 어떤 객체가 들어가는지는 이전 포스팅에서 살펴보았으니, 넘어가겠습니다. 여기서 this가 참조하는 객체가 없으면 전역 객체를 참조합니다.

Untitled Diagram

코드 실행

이렇게 하나의 실행 컨텍스트가 생성되고, 변수 객체가 만들어진 후에, 코드에 있는 여러 가지 표현식 실행이 이루어집니다. 이렇게 실행되면서 변수의 초기화 및 연산, 또 다른 함수 실행등이 이루어집니다. 위 그림에서 undefined가 할당된 변수 a와 b에도 이 과정에서 1, 2의 값이 할당됩니다.

참고로 전역 실행 컨텍스트는 일반적인 실행 컨텍스트와는 약간 다른데, arguments 객체가 없으며, 전역 객체 하나만을 포함하는 스코프 체인이 있습니다. ECMAScript에서 언급된 바에 의하면 실행 컨텍스트가 형성되는 세 가지 중 하나로서 전역 코드가 있는데, 이 전역코드가 실행될 때 생성되는 컨텍스트가 전역 실행 컨텍스트입니다. 전역 실행 컨텍스트는 변수를 초기화하고 이것의 내부 함수는 일반적인 탑 레벨의 함수로 선언됩니다. 그리고 전역 실행 컨텍스트의 변수 객체가 전역 객체로 사용됩니다. 즉, 전역 실행 컨텍스트에서는 변수 객체가 곧 전역 객체 입니다. 따라서 전역적으로 선언된 함수와 변수가 전역 객체의 프로퍼티가 됩니다. 전역 실행 컨텍스트 역시, this를 전역 객체의 참조로 사용합니다.

참조 브라우저에서는 최상위 코드가 곧 전역 코드이지만, Node.js에서는 다릅니다.

var a = 10;
b = 15;
console.log(window.a); //출력값 10
console.log(window.b); //출력값 15

브라우저에서 위 코드는 잘 실행됩니다. var a로 정의한 변수가 전역 객체인 window의 한 프로퍼티로 들어갔습니다. 하지만 Node.js에서는 다릅니다.

var a = 10;
b = 15;

console.log(window.a); //출력값 undefined
console.log(window.b); //출력값 15

Node.js에서는 최상위 코드가 브라우저와는 달리 전역 코드가 아닙니다. 따라서 var a로 정의된 변수가 전역 객체에 들어가지 않습니다. Node.js에서는 일반적으로 자바스크립트 파일, 이를테면 filename.js가 하나의 모듈로 동작하고 이 파일의 최상위에 변수를 선언해도 그 모듈의 지역변수가 됩니다. 하지만 var를 사용하지 않을 경우 전역 객체인 global에 들어가고, 이는 전역 객체를 오염시키는 원인이 되므로 주의해야 합니다.

스코프 체인

자바스크립 코드를 이해하려면 스코프 체인의 이해는 필수적입니다. 이를 알아야, 변수에 대한 인식 메커니즘을 알 수 있고, 현재 사용되는 변수가 어디에 선언된 변수인지 정확히 알 수 있기 때문입니다. 앞서 이전에 설명했던 프로토타입 체인과 거의 비슷한 메커니즘이므로 이해하는데 어렵지는 않을 것입니다.

자바스크립트도 다른 언어와 마찬가지로 스코프, 즉 유효 범위가 있습니다. 이 유효 범위 안에서 변수와 함수가 존재합니다. C 코드를 예를 들면, {,}로 묶여 있는 범위 안에 선언된 변수는 블록이 끝나는 순간 사라지므로 밖에서는 접근할 수 없습니다. 특히, 함수의 {,}뿐만 아니라 if, for문의 {,}로 한 블록으로 묶여, 그 안에서 선언된 변수가 밖에서는 접근이 불가능합니다. 하지만 자바스크립트에서는 함수 내의 {,} 블록은, 이를테면 for() {}, if{}와 같은 구문은 유효 범위가 없습니다. 오직 함수만이 유효 범위의 한 단위가 됩니다. 이 유효 범위를 나타내는 스코프가 [[scope]] 프로퍼티로 각 함수 객체 내에서 연결 리스트 형식으로 관리되는데, 이를 스코프 체인이라고 합니다.
스코프 체인은 각 실행 컨텍스트의 변수 객체가 구성 요소인 리스트와 같습니다.
스코프 체인은 의미 그대로 각각의 스코프가 어떻게 연결되었는지 보여주는 것이 스코프
체인입니다.

각각의 함수는 [[scope]] 프로퍼티로 자신이 생성된 실행 컨텍스트의 스코프 체인을 참조합니다. 함수가 실행되는 순간 실행 컨텍스트가 만들어지고, 이 실행 컨텍스트는 실행된 함수의 [[scope]] 프로퍼티를 기반으로 새로운 스코프 체인을 만듭니다. 예제로 살펴보겠습니다.

var var1 = 1;
var var2 = 2;
console.log(var1); // 출력 값 1
console.log(var2); // 출력 값 2

위 예제는 전역 코드입니다. 함수가 선언되지 않아 함수 호출이 없고, 실행 가능한 코드들만 나열되어 있습니다. 이 자바스크립트 코드를 실행하면, 먼저 전역 실행 컨텍스트가 생성되고, 변수 객체가 만들어 집니다. 이 변수 객체의 스코프 체인은 어떻게 될까요? 현재 전역 실행 컨텍스트 단 하나만 실행되고 있어 참조할 상위 실행 컨텍스트가 없습니다. 자신이 최상위에 위치하는 변수 객체인 것입니다. 따라서, 이 변수 객체의 스코프 체인은 자기 자신만을 가집니다. 다시 말해서, 변수 객체의 [[scope]]는 변수 객체 자신을 가르킵니다. 그 후, var1, va2 변수들이 생성되고 변수 객체에 의해 참조됩니다. 실행 컨텍스트 생성과정에서 언급한 대로 이 변수 객체가 곧 전역 객체가 됩니다.

[그림 - 전역 실행 컨텍스트]

Untitled Diagram

함수를 호출한 경우 생성되는 실행 컨텍스트의 스코프 체인

아래 예제에서 함수를 하나 생성해보겠습니다.

var var1 = 1;
var var2 = 2;

function func() {
    var var1 = 10;
    var var2 = 20;
    console.log(var1); // 출력 값 10
    console.log(var2); // 출력 값 20
}
func();

console.log(var1); // 출력 값 1
console.log(var2); // 출력 값 2

이 예제를 실행하면 전역 실행 컨텍스트가 생성되고, func() 함수 객체가 만들어 집니다. 이 함수 객체가 생성될 때, 그 함수 객체의 [[scope]]는 현재 실행되는 컨텍스트의 변수 객체에 있는 [[scope]]를 그대로 가집니다. 따라서, func 함수 객체의 [[scope]]는 전역 변수 객체가 됩니다.

그런 다음에 func() 험수를 실행하였으므로 새로운 컨텍스트가 만들어집니다. 이 컨텍스트를 편의상 func 컨텍스트라고 하겠습니다. func 컨텍스트의 스코프 체인은 실행된 함수의 [[scope]] 프로퍼티를 그대로 복사한 후, 현재 생성된 변수 객체를 복사한 스코프 체인의 맨 앞에 추가합니다. func() 함수 객체의 [[scope]] 프로퍼티가 전역 객체 하나만을 가지고 있었으므로, func 실행 컨텍스트의 스코프 체인은 다음 그림과 같이 [func 변수 객체 - 전역 객체]가 됩니다.

Untitled Diagram (1)

위 그림을 바탕으로 스코프 체인을 간단히 정리하겠습니다.

  • 각 함수 객체는 [[scope]] 프로퍼티로 현재 컨텍스트의 스코프 체인을 참조합니다.
  • 한 함수가 실행되면 새로운 실행 컨텍스트가 만들어지는데, 이 새로운 실행 컨텍스트는 자신이 사용할 스코프 체인을 다음과 같은 방법으로 만듭니다. 현재 실행되는 함수 객체의 [[scope]] 프로퍼티를 복사하고, 새롭게 생성된 변수 객체를 해당 체인의 제일 맨 앞에 추가합니다.
  • 요약하면 스코프 체인은 다음과 같이 표현할 수 있습니다.

스코프 체인 = 현재 실행 컨텍스트의 변수 객체 + 상위 컨텍스트의 스코프 체인

이제, 앞에서 정리한 내용으로 다음 두 예제의 결과를 예상해보겠습니다.

var value = "value1";

function printFunc() {
    var value = "value2";

    function printValue() {
        return value;
    }
    console.log(printValue());
}

printFunc();

앞에 설명을 제대로 이해했다면, 코드가 출력하는 결과값을 쉽게 예측할 수 있습니다. 스코프 체인을 그려보면 다음과 같습니다.

Untitled Diagram

결과 값은 printValue() 함수가 실행될 때 Value 변수를 printFunc 변수 객체에서 찾으므로, 결과값은 value2가 됩니다.

그렇다면 다음 예제도 확인해보겠습니다.

var value = "value1";

function printValue() {
    return value;
}

function printFunc(func) {
    var value = "value2";
    console.log(func());
}

printFunc(printValue);

이 예제는 각 함수 객체가 처음 생성될 당시 실행 컨텍스트가 무엇인지를 생각해야 합니다. 각 함수객체가 처음 생성될 때 [[scope]]는 전역 객체의 [[scope]]를 참조합니다. 따라서 각 함수가 실행될 때 생성되는 실행 컨텍스트의 스코프 체인은 전역 객체와 그 앞에 새롭게 만들어진 변수 객체가 추가됩니다. 그래서 printValue()가 실행될 때의 스코프 체인은 위 예제와는 다르게 만들어집니다. 이것도 그림으로 설명하겠습니다.

Untitled Diagram (1)

printValue 함수는 전역 실행 컨텍스트의 전역 객체에서 함수 객체가 생성되기 때문에 [[scope]] 프로퍼티는 전역 객체를 참조하게 됩니다. 따라서 value 변수를 전역 객체에서 찾으므로 결과 값은 value1이 됩니다.

함수가 정의될 때의 환경을 기준으로 스코프가 설정된다는 것을 꼭 기억해야 합니다.

참고로 크롬 개발자 도구에서 console.dir(함수())를 실행하면 [[scope]] 프로퍼티가 존재합니다. 이것이 바로 스코프 체인입니다. 자기 자신의 스코프를 제외하고, 자신과 가장 가까운 변수 객체의 스코프 순으로 접근하고 있는 것을 확인할 수 있습니다.

image

지금까지 실행 컨텍스트가 만들어지면서 스코프 체인이 어떻게 형성되는지 살펴보았습니다. 이렇게 만들어진 스코프 체인으로 식별자 인식이 이루어집니다. 식별자 인식은 스코프 체인의 첫번째 변수 객체부터 시작합니다. 식별자와 대응되는 이름을 가진 프로퍼티가 있는지를 확인합니다. 함수를 호출할 때 스코프 체인의 가장 앞에 있는 객체가 변수 객체이므로, 이 객체에 있는 공식 인자, 내부 함수, 지역 변수에 대응되는지 먼저 확인합니다. 첫 번째 객체에 대응되는 프로퍼티를 발견하지 못하면, 다음 객체로 이동하여 찾습니다. 이런 식으로 대응되는 이름의 프로퍼티를 찾을 때까지 계속됩니다. 여기서 this는 식별자가 아닌 키워드로 분류되므로, 스코프 체인의 참조 없이 접근할 수 있음을 기억합시다.

클로저

클로저는 자바스크립트의 중요 개념 중 하나로 함수를 일급 객체로 취급하는 함수형 언어에서 사용되는 중요한 특징입니다. 보통 함수가 실행되면 실행 컨텍스트가 생성되고, 다시 반환되면서 함수의 생명주기가 끝나게 됩니다. 하지만 클로저를 사용하면 함수의 생명을 연장 시킬 수 있습니다.

클로저를 간단하게 설명하면 외부함수에서 반환된 내부 함수가 반환 이후에도 생명 주기가 유지되어 외부함수의 변수 객체 안을 탐색 할 수 있는 것을 의미합니다.

아래 예제를 살펴보겠습니다.

function outerFunc() {
    var x = 10;
    var innerFunc = function() { console.log(x); }
    return innerFunc;
}

var inner = outerFunc();
inner();

Untitled Diagram

앞서 배운 그림과 크게 다르지 않습니다. innerFunc()의 [[scope]]은 outerFunc 변수 객체와 전역 객체를 가집니다. 그런데 여기서 잠깐 혼란스러운 부분이 있습니다. 위 예제에서 innerFunc()은 outerFunc()의 실행이 끝난 후 실행됩니다. 그렇다면 outerFunc() 실행 컨텍스트가 사라진 이후에 innerFunc 실행 컨텍스트가 생성되는 것인데, innerFunc()의 스코프 체인은 outerFunc 변수 객체를 여전히 참조할 수 있을까요? 위 예제 결과를 보면 짐작할 수 있지만, outerFunc 실행 컨텍스트는 사라졌지만, outerFunc 변수 객체는 여전히 남아있고, innerFunc의 스코프 체인으로 참조되고 있습니다. 이것이 바로 자바스크립트에서 구현한 클로저라는 개념입니다.

이전 포스팅에서 언급했듯이, 자바스크립트의 함수는 일급 객체로 취급됩니다. 이는 함수를 다른 함수의 인자로 넘길 수 있고, return으로 함수를 통째로 반환할 수도 있음을 의미합니다. 이러한 기능은 앞 예제와 같은 코드가 가능합니다. 여기서 최종 반환되는 함수가 외부 함수의 지역 변수에 접근하고 있다는 것이 중요합니다. 이 지역변수에 접근하려면, 함수가 종료되어 외부 함수의 컨텍스트가 반환되더라도 변수 객체는 반환되는 내부 함수의 스코프 체인에 그대로 남아있어야만 접근할 수 있습니다. 이것이 바로 클로저입니다. 클로저는 이미 많은 곳에서 정의내리고 있지만, 그 정의만 보고는 쉽게 이해하기 어렵습니다. 여기서는 조금 쉽게 풀어서 정의하겠습니다. 이미 생명주기가 끝난 외부함수의 변수를 참조하는 함수를 클로저 함수라고 합니다. 따라서 앞 예제에서 outerFunc에서 선언된 x를 참조하는 innerFunc가 클로저가 됩니다. 그리고 클로저로 참조되는 외부 변수 즉, outerFunc의 x와 같은 변수를 자유 변수라고 합니다. closure라는 이름은 함수가 자유 변수에 대해 닫혀있다는 의미인데, 우리말로 의역하면 자유 변수에 엮여있는 함수라는 표현이 맞을 듯 합니다.

클로저를 이해했다면 다음 예제를 보고 결과값을 예측해봅시다.

function outerFunc(arg1, arg2){
    var local = 8;
    function innerFunc(innerArg){
        console.log((arg1 + arg2)/(innerArg + local));
    }
    return innerFunc;
}

var exam1 = outerFunc(2, 4);
exam(2);

앞 예제에서는 outerFunc() 함수를 호출하고 반환되는 함수 객체인 innerFunc()가 exam1으로 참조됩니다. 이것은 exam1(n)의 형태로 실행될 수 있습니다.

여기서 outerFunc()가 실행되면서 생성되는 변수 객체가 스코프 체인에 들어가게 되고, 이 스코프 체인은 innerFunc의 스코프 체인으로 참조됩니다. 즉, outerFunc() 함수가 종료되었지만, 여전히 내부 함수(innerFunc())의 [[scope]]으로 참조되므로 가비지 컬렉션 대상이 되지 않고, 여전히 접근 가능하게 살아있습니다. 따라서 이후에 exam1(n)을 호출하여도, innerFunc()에서 참조하고자 하는 변수 local에 접근할 수 있습니다. 클로저는 이렇게 만들어집니다. 이 outerFunc 변수 객체의 프로퍼티 값은 여전히 읽기 및 쓰기까지 가능합니다.

클로저의 활용

클로저의 개념을 이해했다면, 이제 이 클로저를 어떻게 활용할 것인지 고민해야 합니다. 앞에서 설명했듯이, 클로저는 성능적인 면과 자원적인 면에서 약간 손해를 볼 수 있으므로 무차별적으로 사용해서는 안됩니다. 사실 클로저를 잘 활용하려면 경험이 가장 중요하게 작용합니다. 한마디로... 계속 클로저에 대해서 공부하고 반복적으로 코딩을 하면... 된다는 말인데..아는만큼 보인다는것 같습니다.

특정 함수에 사용자가 정의한 객체의 메서드 연결하기

function HelloFunc(){
    this.greeting = "hello";
}


HelloFunc.prototype.call = function(func) {
    func ? func(this.greeting) : this.func(this.greeting);
}

var userFunc = function(greeting) {
    console.log(greeting);
}

// 생성자 함수를 이용한 객체 생성
var objHello = new HelloFunc();

objHello.func = userFunc;
objHello.call();

함수 HelloFunc는 greeting 변수가 있고, func 프로퍼티로 참조되는 함수를 call() 함수로 호출합니다. 사용자는 func 프로퍼티에 자신의 정의한 함수를 참조시켜 호출할 수 있습니다. 다만, HelloFunc.prototype.call()을 보면 알 수 있듯이 자신의 지역 변수인 greeting만을 인자로 사용자가 정의한 함수에 넘깁니다. 앞 예제에서 userFunc() 함수를 정의하여 objHello.func()에 참조시킨 뒤, HelloFunc()의 지역 변수인 greeting을 화면에 출력시킵니다. 앞 코드를 실행시키면 다음과 같습니다.

출력결과: hello

함수의 캡슐화

다음과 같은 함수를 작서안다고 가정해봅시다.

I am XXX, I live in XXX. I'am XX years old라는 문장을 출력하는데, XX 부분은 사용자에게 인자로 입력 받아 값을 출력하는 함수 입니다.

가장 먼저 생각할 수 있는 것은 앞 문장 템플릿을 전역 변수에 저장하고, 사용자의 입력을 받은 후, 이 전역 변수에 접근하여 완성된 문장을 출력하는 방식으로 함수를 작성하는 것입니다. 이 방식으로 구현하면 코드는 다음과 같습니다.

var buffAr = [
    'I am',
    '',
    '. I live in ',
    '',
    ' I\'am ',
    '',
    ' years old',
];


function getCompletedStr(name, city, age){
    buffAr[1] = name;
    buffAr[3] = city;
    buffAr[5] = age;

    return buffAr.join('');
}

var str = getCompletedStr('junyoung', 'jungja', 29);
console.log(str);

하지만 위의 예제는 단점이 있습니다. 바로 buffAr이라는 배열은 전역 변수로서, 외부에 노출되어 있다는 점입니다.

이는 다른 함수에서 이 배열에 쉽게 접근하여 값을 바꿀수도 있고, 실수로 같은 이름의 변수를 만들어 버그가 생길 수도 있습니다. 이는 특히 다른 코드와의 통합 혹은 이 코드를 라이브러리로 만들어 버그가 생길 수도 있습니다. 실제로 다른 사람이 사용할 라이브러리로 만들려고 할 때, 까다로운 문제를 발생시킬 가능성이 있습니다. 실제로 다른 사람이 사용할 라이브러리를 만들려고 하는 개발자는 이러한 충돌 가능성이을 충분히 대비해서 라이브러리를 작성해야만 합니다. 앞 예제의 경우, 클로저를 활용하여 buffAr을 추가적인 스코프에 넣고 사용하게 하면, 이 문제를 해결할 수 있습니다.

var getCompletedStr = (function() {

    var buffAr = [
        'I am',
        '',
        '. I live in ',
        '',
        ' I\'am ',
        '',
        ' years old',
    ];

    return (function(name, city, age) {
        buffAr[1] = name;
        buffAr[3] = city;
        buffAr[5] = age;

        return buffAr.join('');
    });
})();

var str = getCompletedStr('zzoon', 'seoul', 16);
console.log(str);

예제에서 가장 먼저 주의해서 봐야 할 점은 변수 getCompletedStr에 익명의 함수를 즉시 실행시켜 반환되는 함수를 할당하는 것입니다. 이 반환되는 함수가 클로저가 되고, 이 클로저는 자유 변수 buffAr을 스코프 체인에서 참조할 수 있습니다.

setTimeout()에 지정되는 함수의 사용자 정의

첫 번째로 소개했던 활용 예제와 유사합니다. setTimeout 함수는 웹 브라우저에서 제공하는 함수인데, 첫 번째 인자로 넘겨지는 함수 실행의 스케쥴링을 할 수 있습니다. 두 번째 인자인 밀리 초 단위 숫자만큼의 시간 간격으로 해당 함수를 호출합니다. setTimeout()으로 자신의 코드를 호출하고 싶다면 첫 번째 인자로 해당 함수 객체의 참조를 넘겨주면 되지만, 이것으로 실제 실행될 때 함수에 인자를 줄 수 없습니다. 그렇다면 자신이 정의한 함수에 인자를 넣어줄 수 있게 하려면 어떻게 해야 할까요? 역시 갓 클로저로 해결할 수 있습니다.

아래 예제 코드를 다시 살펴보겠습니다.

function callLater(obj, a, b) {
    return (function() {
        obj["sum"] = a + b;
            console.log(obj["sum"]);
    });
}

var sumObj = {
    sum : 0
}

var func = callLater(sumObj, 1, 2);
setTimeout(func, 500);

위의 예제를 살펴보면 사용자가 정의한 함수 callLater를 setTimeout 함수로 호출하려면, 변수 func에 함수를 반환받아 setTimeout() 함수의 첫 번째 인자로 넣어주면 됩니다. 반환받는 함수는 당연히 클로저고, 사용자가 원하는 인자에 접근할 수 있습니다.

클로저를 사용할 때 주의사항

지금까지 클로저의 개념과 활용을 알아봤습니다. 앞서 언급한 대로 클로저는 자바스크립트의 강력한 기능이지만, 너무 남발하여 사용하면 안됩니다. 여기서는 클로저에서 사용자가 쉽게 간과할 수 있는 사항을 정리했습니다.

클로저의 프로퍼티값이 쓰기 가능하므로 그 값이 여러 번 호출로 항상 변할 수 있음에 유의해야 합니다.

function outerFunc(argNum) {

    var num = argNum;
    return function(x) {
        num += x;
        console.log('num: ' + num);
    }
}

var exam = outerFunc(40);
exam(5);
exam(-10);

예제에서 보는 바와 같이 exam 값을 호출할 때마다, 자유 변수 num 값은 계속해서 변화하니 주의하기 바랍니다.

하나의 클로저가 여러 함수 객체의 스코프 체인에 들어가 있는 경우도 있습니다.

function func() {
    var x = 1;
    return {
        func1: function() { console.log(++x); },
        func2: function() { console.log(-x); }
    };
};

var exam = func();
exam.func1();
exam.func2();

위 예제에서 반환되는 객체에는 두 개의 함수가 정의되어 있는데, 두 함수 모두 자유 변수 x를 참조합니다. 그리고 각각의 함수가 호출될 때마다 x 값이 변화하므로 유의해야 합니다.

루프 안에서 클로저를 활용할 때는 주의합시다.

이번에 소개할 주의사항은 클로저 설명에서 단골손님처럼 등장하는 예제 코드입니다.

function countSeconds(howMany) {
    for(var i = 1; i <= howMany; i++) {
        setTimeout(function() {
            console.log(i);
        }, 1000);
    }
};

countSeconds(3);

위 에제는 1, 2, 3을 1초 간격으로 출력하는 의도로 만든 에제입니다. 하지만 결과는 4가 연속 3번 1초 간격으로 출력됩니다. 클로저를 잘 이해했다면, 이유를 쉽게 이해할 수 있을 것입니다. setTimeout 함수의 인자로 들어가는 함수는 자유 변수 i를 참조합니다. 하지만 이 함수가 실행되는 시점은 countSeconds() 함수의 실행이 종료된 이후이고, i의 값은 이미 4가 된 상태입니다. 그러므로 setTimeout()로 실행되는 함수는 모두 4를 출력하게 됩니다.

이제 원하는 결과를 얻기 위해 이 코드를 수정하겠습니다. 이를 위해서는 루프 i 값 복사본을 함수에 넘겨줍니다. 이를 위해 즉시 실행 함수를 사용했습니다.

function countSeconds(howMany) {
    for(var i =1; i<= howMany; i++) {
        (function (currentI) {
            setTimeout(function() {
                console.log(currentI);
            }, currentI * 1000);
        }(i));
    }
};

countSecond(i);

즉시 실행 함수를 실행시켜 루프 i 값을 currentI에 복사해서 setTimeout() 들어갈 함수에서 사용하면, 원하는 결과를 얻을 수 있습니다.

지금까지 실행 컨텍스트, 스코프 체인 및 클로저를 알아봤습니다. 우리가 자바스크립트 라이브러리를 만들고자 할 때 이에 대한 지식 없이 만든다면 이름 충돌, 성능 저하, 비효율적인 자원활용 등의 문제가 틀림없이 발생할 것이고, 이는 좋은 라이브러리라고 할수 없습니다. 반드시 자식을 갖춘 후 개발합시다.

인사이드 자바스크립트

'JavaScript' 카테고리의 다른 글

객체지향 프로그래밍  (0) 2020.03.22
함수와 프로토타입 체이닝  (0) 2020.02.02
자바스크립트 데이터 타입과 연산자  (0) 2019.12.26

함수와 프로토타입 체이닝

자바스크립트에서 가장 중요한 개념은 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가 출력됩니다.

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

'JavaScript' 카테고리의 다른 글

객체지향 프로그래밍  (0) 2020.03.22
실행 컨텍스트와 클로저 개념 정리  (0) 2020.02.25
자바스크립트 데이터 타입과 연산자  (0) 2019.12.26

자바 스크립트 기초

자바스크립트 기본 타입

자바스크립트에서 기본 타입은 숫자, 문자열, 불린값을 비롯해 null, undefined라는 타입이 존재합니다. 자바스크립트는 느슨한 타입 체크언어 입니다. 따라서 자바스크립트는 변수에 어떤 형태의 데이터를 저장하느냐에 따라 해당 변수의 타입이 결정됩니다.

var intNum = 10;
var floatNum = 0.1;

var singleQuoteStr = 'single quote string';
var doubleQuoteStr = 'double quote string';

var booVar = true;
var emptyVar;
var nullVar = null;

console.log( typeof intNum, typeof floatNum, typeof singleQuoteStr, typeof doubleQuoteStr, typeof booVar, typeof nullVar, typeof emptyVar);

// 출력 값: number number string string boolean object undefined

숫자

C언어의 경우 정수냐 실수냐에 따라 int, long, float, double 등과 같은 다양한 숫자 타입이 존재하지만, 자바스크립트는 하나의 숫자형만 존재합니다. 자바스크립트에서는 모든 숫자를 64비트 부동 소수점 형태로 저장하기 때문입니다. 이는 C언어의 double 타입과 유사합니다. intNum, floatNum 변수 모두 typeof 연산자의 결과값이 number타입임을 확인할 수 있습니다.

자바스크립트에서는 정수형이 따로 없고, 모든 숫자를 실수로 처리하므로 나눗셈 연산을 할 때는 주의해야 합니다. 아래 예제와 같은 연산을 C언어에서 할 경우 5/2는 소수 부분을 버린 2가 출력됩니다. 반면에 자바스크립트에서는 5와 2가 둘 다 정수가 아닌 실수로 취급되므로 소수 부분까지 출력된 2.5가 결과값이 됩니다.

var num = 5 / 2;

console.log(num); //(출력값) 2.5
console.log(Math.floor(num)); //(출력값) 2

문자열

문자열은 작은 따옴표(')나 큰 따옴표(")로 생성합니다. 따라서 위에서 singleQuoteStr, doubleQuoteStr 변수의 typeof 연산자 결과가 string으로 나옵니다. C언어와 다르게 char 타입과 같이 문자 하나만을 별도로 나타내는 데이터 타입은 존재하지 않습니다. 따라서 한 개의 문자를 나타내려면 위의 코드 singleChar 변수같이 길이가 1인 문자열을 사용해야 합니다.

자바스크립트 문자열 예제

//str 문자열 생성
var str = 'test';
console.log(str[0], str[1], str[2], str[3]); //(출력값) test

//문자열의 첫 글자를 대문자로 변경?
str[0] = 'T';
console.log(str); // 출력값 test

문자열은 문자 배열처럼 인덱스를 이용해서 접근할 수 있습니다. 그리고 가장 주목할 점은 예제에서 str[0]에 'T'를 넣어서 문자여르이 첫 글자를 대문자로 변경했습니다. 물론 에러가 발생하지 않았습니다. 그러나 console.log(str)로 문자열을 출력하면, 우리가 의도했던 출력결과인 'Test'가 아니라 원래의 문자열인 'test'가 출력됩니다. 즉, 자바스크립트에서는 한 번 생성된 문자열은 읽기만 가능하지 수정은 불가능합니다.

불린값

자바스크립트는 true와 false 값을 나타내는 불린 타입들을 가집니다. booVar 변수에 'true'라는 값을 저장했으므로 booVar 변수는 boolean 타입이 출력됩니다.

null과 undefined

이 두 타입은 모두 자바스크립트에서 값이 비어있음을 나타냅니다. 자바스크립트 환경 내에서 기본적으로 값이 할당되지 않은 변수는 undefined 타입이며, undefined 타입의 변수는 변수 자체의 값 또한 undefined입니다.이처럼 자바스크립트에서 undefined는 타입이자, 값을 나타내는 것에 주의합시다. 위의 예제코드에서 emptyVar 변수에는 아무런 값이 할당되지 않으므로 undefined 타입이 출력된 것입니다. 이에 반해 nullVar 변수와 같이 null 타입 변수의 경우는 개발자가 명시적으로 값이 비어있음을 나타내는데 사용합니다.

여기서 또 주의할 점은 null 타입 변수인 nullVar의 typeof 결과가 null이 아니라 object라는 것입니다. 때문에 아래 예제와 같이 자바스크립트에서는 null 타입 변수인지를 확인할 때 typeof 연산자를 사용하면 안 되고, 일치 연산자(===)를 사용해서 변수의 값을 직접 확인해야 합니다.

var nullVar = null;

console.log(typeof nullVar === null); // (출력값) false
console.log(nullVar === null); // (출력값) true

자바스크립트 참조 타입(객체 타입)

자바스크립트에서 숫자, 문자열, 불린값, null, undefined 같은 기본 타입을 제외한 모든 값은 객체입니다. 따라서 배열, 함수, 정규표현식 등도 모두 결국 자바스크립트 객체로 표현됩니다.

자바스크립트에서 객체는 단순히 '이름(key):값(value)' 형태로 프로퍼티들을 저장하는 컨테이너로서, 컴퓨터 과학 분야에서 해시(Hash)라는 자료구조와 상당히 유사합니다. 자바스크립트에서 기본 타입은 하나의 값만을 가지는 데 비해, 참조 타입인 객체는 여러 개의 프로퍼티들을 포함할 수 있으며, 이러한 객체의 프로퍼티는 기본 타입의 값을 포함하거나, 다른 객체를 가리킬 수도 있습니다. 이러한 프로퍼티의 성질에 따라 객체의 프로퍼티는 함수로 포함할 수 있으며, 자바스크립트에서는 이러한 프로퍼티를 메서드라고 부릅니다.

객체 생성

자바스크립트의 객체 개념은 생성 방법이나 상속 방식 등에서 C++이나 자바와 같은 기존 객체지향 언어에서의 객체 개념과는 약간 다릅니다. 자바에서는 클래스를 정의하고, 클래스의 인스턴스를 생성하는 과정에서 객체가 만들어집니다. 이에 비해 자바스크립트에서는 클래스라는 개념이 없고, 객체 리터럴이나 생성자 함수 등 별도의 생성방식이 존재합니다.

자바스크립트에서 객체를 생성하는 방법은 크게 세 가지가 있습니다.

  • Object() 객체 생성자 함수를 이용하는 방식
  • 객체 리터럴을 이용하는 방식
  • 생성자 함수를 이용하는 방식

Object() 생성자 함수 이용

자바 스크립트에서는 객체를 생성할 때, 내장 Object() 생성자 함수를 제공합니다. 다음 예제를 살펴보겠습니다. Object() 생성자 함수를 이용해서 foo라는 빈 객체를 생성한 후, 몇 가지 프로퍼티(name, age, gender)들을 추가한 것입니다.

// Object()를 이용해서 foo 빈 객체 생성
var foo = new Object();

// foo 객체 프로퍼티 생성
foo.name = 'foo';
foo.age = 30;
foo.gender = 'male';

console.log(typeof foo); // (출력값) object
console.log(foo); // (출력값) {name: "foo", age: 30, gender: "male"}

객체 리터럴 방식 이용

리터럴이란 용어의 의미는 표기법이라고 생각하면 됩니다. 따라서 객체 리터럴이란 객체를 생성하는 표기법을 의미합니다. 객체 리터럴 방식은 간단한 표기법만으로도 객체를 생성할 수 있는 자바스크립트의 강력한 문법입니다.

객체 리터럴은 중괄호({})를 이용해서 객체를 생성합니다. {} 안에 아무것도 적지 않는 경우는 빈 객체가 생성되며, 중괄호 안에 "프로퍼티 이름":"프로퍼티 값" 형태로 표기하면, 해당 프로퍼티가 추가된 객체를 생성할 수 있습니다.
여기서 프로퍼티 이름은 문자열이나 숫자가 올 수 있고, 프로퍼티 값으로는 자바스크립트의 값을 나타내는 어떤 표현식도 올 수 있습니다. 이 값이 함수일 경우 이러한 프로퍼티를 메소드라고 부릅니다.

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

console.log(typeof foo) // (출력값) object
console.log(foo) // (출력값) {name: "foo", age: 30, gender: "male"}

생성자 함수 이용

자바스크립트의 경우는 함수를 통해서도 객체를 생성할 수 있습니다. 이렇게 객체를 생성하는 함수를 생성자 함수라고 부릅니다.

객체 프로퍼티 읽기/쓰기/갱신

객체는 새로운 값을 가진 프로퍼티를 생성하고, 생성된 프로퍼티에 접근해서 해당 값을 읽거나 또는 원하는 값으로 프로퍼티의 값을 갱신할 수 있습니다.

객체의 프로퍼티에 접근하는 방법

  • 대괄호([]) 표기법
  • 마침표(.) 표기법

객체 프로퍼티에 접근할 때 대괄호 표기법만 사용해야 하는 경우가 있습니다. 접근하려는 프로퍼티가 표현식이거나 예약어일 경우입니다. 이때는 대괄호 표기법만을 이용해서 접근해야 합니다. 아래 코드에서 접근하고자하는 프로퍼티가 'full-name'입니다. 이 경우는 '-' 연산자가 있는 표현식입니다. 이 경우에는 대괄호 표기법만을 이용해서 ['full-name'] 형태로 프로퍼티에 접근해야 합니다.

// foo 객체에 'full-name' 프로퍼티가 없으므로 동적으로 프로퍼티 생성 후 'foo bar' 문자열이 할당하게 됩니다.
foo['full-name'] = 'foo bar';

참고사항 : NaN(Not a Number) 값은 자바스크립트에서 수치 연산을 해서 정상적인 값을 얻디 못할 때 출력되는 값입니다. 가령, 1 - 'hello'라는 연산의 결과는 NaN입니다. 1이라는 숫자와 문자열 'hello'를 빼는 연산을 수행했기 때문입니다.

console.log(foo.full-name); // (출력값) NaN

위에서 NaN이 출력된 이유는 앞 코드가 foo 객체의 full-name 프로퍼티에 접근하려는 우리의 의도와는 다르게 foo.full(foo 객체의 full 프로퍼티의 저장된 값)과 name이라는 변수의 값을 - 연산자로 계산하는 표현식으로 취급했기 때문입니다.
자바스크립트에서 undefined-undefined 의 연산 결과가 NaN으로 정의됩니다.

for in 문과 객체 프로퍼티 출력

for in 문을 사용하면, 객체에 포함된 모든 프로퍼티에 대해 루프를 수행할 수 있습니다. 다음 예제는 for in 문을 이용해서 foo 객체의 모든 프로퍼티 이름과 프로퍼티 값을 출력한 예제입니다.

// 객체 리터럴을 통한 foo 객체 생성
var foo = {
    name : 'foo',
    age : 30,
    gender : 'male'
};

// for in 문을 이용한 객체 프로퍼티 출력
var prop;
for (prop in foo){
    console.log(prop);
};

// (출력값)
name
age
gender

객체 프로퍼티 삭제

자바스크립트에서는 객체의 프로퍼티를 delete 연산자를 이용해 즉시 삭제할 수 있습니다. 여기서 주의할 점은 delete 연산자는 객체의 프로퍼티를 삭제할 뿐, 객체 자체를 삭제하지는 못한다는 것입니다.

var foo = {
    name : 'foo',
    age : 30,
    gender : 'male'
};

console.log(foo.gender); // (출력값) male
delete foo.gender; // (출력값)true gender 프로퍼티 삭제
console.log(foo.gender); // (출력값) undefined

delete foo; // (출력값) false
console.log(foo.name); // (출력값) foo

참조 타입의 특성

자바스크립트에서는 기본 타입인 숫자, 문자열, 불린값, null, undefined 5가지를 제외한 모든 값은 객체입니다. 배열이나 함수 또한 객체로 취급됩니다. 그리고 이러한 객체는 자바스크립트에서 참조 타입이라고 부릅니다. 이것은 객체의 모든 연산이 실제 값이 아닌 참조값으로 처리되기 때문입니다. 아래 예제를 살펴보겠습니다.

var objA = {
    val : 40 
};
var objB = objA;
console.log(objA); // (출력값) 40
console.log(objB); // (출력값) 40

objB.val = 50;
console.log(objA); // (출력값) 50
console.log(objB); // (출력값) 50
  1. objA 객체를 객체 리터럴 방식으로 생성했습니다. 여기서 objA 변수는 객체 자체를 저장하고 있는 것이 아니라 생성된 객체를 가리키는 참조값을 저장하고 있습니다.

  2. 변수 objB에 objA 값을 할당합니디ㅏ. objA는 1에서 생성된 객체를 가리키는 참조값을 가지고 있으므로 변수 objB에도 이같은 객체의 참조값이 저장됩니다. 즉, 아래 그림과 같이 objA와 objB 변수가 동일한 객체를 가리키는 참조값을 가지게 되는 것입니다. 때문에 a.val과 b.val 값이 40으로 같게 됩니다.

Untitled Diagram

  1. 변수 objB가 가르키는 객체의 val 값을 40에서 50으로 갱신했습니다. 이때 변수 objA도 변수 objB와 동일한 객체를 참조하고 있으므로 a.val 값이 50으로 변경된 것을 확인할 수 있습니다.

결론은 objA 객체는 참조 변수 objA가 가리키고 있는 객체를 나타낸다고 생각하면 됩니다.

객체 비교

동등 연산자(==)를 사용하여 두 객체를 비교할 때도 객체의 프로퍼티 값이 아닌 참조값을 비교한다는 것에 주의해야 합니다.

var a = 100;
var b = 100;

var objA = { val:100};
var objB = { val:100};
var objC = objB;

console.log(a == b); // (출력값) true
console.log(objA == objB); // (출력값) false
console.log(objB == objC);  // (출력값) true

a와 b는 숫자 100을 저장하고 있는 기본 타입의 변수입니다. 기본 타입의 경우 동등 연산자(==)를 이용해서 비교할때 값을 비교합니다. 두 변수 모두 100이라는 동일한 값을 가지고 있으므로 a == b는 true 가 됩니다.

objA와 objB는 다른 객체지만, 같은 형태의 프로퍼티 값을 가지고 있습니다. 하지만 동등 연산자(==)로 두 객체를 비교하면 위의 결과랑 다르게 false가 됩니다. 그 이유는 기본타입의 경우는 값 자체를 비교해서 일치 여부를 판단하지만, 객체와 같은 참조 타입의 경우는 참조값이 같아야 true를 리턴합니다. 따라서 objB와 objC는 같은 객체를 참조하므로 동등 연산자(==) 결과가 true가 되는 것입니다.

참조에 의한 함수 호출 방식

기본 타입과 참조 타입의 경우는 함수 호출 방식도 다릅니다. 기본 타입의 경우는 값에 의한 호출방식(call by value)로 동작합니다 즉, 함수를 호출할 때 인자로 기본 타입의 값을 넘길 경우, 호출된 함수의 매개변수로 복사된 값이 전달됩니다. 때문에 함수 내부에서 매개변수를 이용해 값을 변경해도 , 실제로 호출된 변수의 값이 변경되지는 않습니다.

이에 반해 객체와 같은 참조 타입의 경우 함수를 호출할 때 참조에 의한 호출(call by reference)방식으로 동작합니다. 즉, 함수를 호출할 때 인자로 참조 타입인 객체를 전달할 경우, 객체의 프로퍼티 값이 함수의 매개변수로 복사되지 않고, 인자로 넘긴 객체의 참조값이 그대로 함수 내부로 전달됩니다. 때문에 함수 내부에서 참조값을 이용해서 인자로 넘긴 실제 객체의 값을 변경할 수 있는 것입니다.

var a = 100;
var objA = { value:100 };

function changeArg(num, obj){
    num = 200;
    obj.value = 200;

    console.log(num);
    console.log(obj);
}

changeArg(a, objA);

console.log(a);
console.log(objA);

// 출력 결과
200
{value: 200}
100
{value: 200}

changeArg() 함수를 호출하면서, 인자값으로 기본 타입인 숫자를 가진 변수 a와 참조 타입인 objA를 넘겼습니다. 함수 내부에서 num과 obj를 이용해 인자로 전달된 a와 objA.val의 값을 100에서 200으로 바꿨지만, 함수 호출이 끝난 후에는 참조 타입인 객체의 objA.value 프로퍼티만이 실제 값으로 변해 있다는것을 확인할 수 있었습니다. 이것을 통해서 기본 타입인 변수 a는 값이 변하지 않습니다. 반면에 객체의 경우는 매개변수 obj로 objA가 참조하는 객체의 위치 값이 그대로 전달되므로 실제 객체의 value 프로퍼티 값이 changeArg() 함수 호출 후에도 적용되는 것입니다.

Untitled Diagram (1)

프로토타입

자바스크립트의 모든 객체는 자신의 부모 역할을 하는 객체와 연결되어 있습니다. 그리고 이것은 마치 객체지향의 상속 개념과 같아 부모 객체의 프로퍼티를 마치 자신의 것처럼 쓸 수 있는 것 같은 특징이 있습니다.
자바스크립트에서는 이러한 부모 객체를 프로토타입 객체라고 부릅니다.

// 객체 생성 및 출력
var foo = { 
    name : 'foo',
    age: 30
};

console.log(foo.toString());

console.dir(foo);

위 코드는 단순히 객체 리터럴 방식으로 foo 객체를 생성하고, 이 객체의 toString() 메서드를 출력한 것입니다. 그러나 foo 객체는 toString() 메서드가 없으므로 에러가 발생해야 하지만, 정상적으로 결과가 출력된 것이 확인됩니다. 그 이유는 foo 객체의 프로토타입에 toString() 메서드가 이미 정의되어 있고, foo 객체가 상속처럼 toString() 메서드를 호출했기 때문입니다.

크롬 브라우저에서의 foo 객체의 출력결과

스크린샷 2019-12-26 오후 6 13 49

객체 리터럴에서 생성한 name과 age 프로퍼티 이외에도 foo 객체에 proto 프로퍼티가 있다는 것을 확인할 수 있습니다. 이 프로퍼티가 바로 앞서 설명한 foo 객체의 부모인 프로토타입 객체를 가리킵니다. 이 객체에 toString() 메서드가 정의되어 있다는 것도 확인이 됩니다.

ECMAScript 명세서에는 자바스크립트의 모든 객체는 자신의 프로토타입을 가리키는 [[Prototype]]라는 숨겨진 프로퍼티를 가진다고 설명합니다. 크롬 브라우저에서는 _proto_가 바로 이 숨겨진 [[Prototype]] 프로퍼티를 의미합니다. 즉, foo 객체는 자신의 부모 객체를 _proto_라는 내부 프로퍼티로 연결하고 있는 것입니다.

참고로 객체 리터럴 방식으로 생성된 객체의 경우 Object.prototype 객체가 프로토타입 객체가 된다는 것만 기억합시다. foo 객체의 proto 프로퍼티가 가르키는 객체가 바로 Object.prototype 이며, toString(), valueOf() 등과 같은 모든 객체에서 호출 가능한 자바스크립트 기본 내장 메서드가 포함되어 있습니다. 그 결과 foo 객체는 foo.toString()과 같이 자신의 프로토타입인 Object.prototype 객체에 포함된 다양한 메서드를 마치 자신의 프로토타입인 것처럼 상속받아 사용할 수 있습니다.

foo 객체와 Object.prototype 객체의 관계

Untitled Diagram (2)

객체를 생성할 때 결정된 프로토타입 객체는 임의의 다른 객체로 변경하는 것도 가능합니다.
즉, 부모 객체를 동적으로 바꿀 수도 있는 것입니다. 자바스크립트에서 이러한 특징을 활용해서 객체 상속 등의 기능을 구현합니다.

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

'JavaScript' 카테고리의 다른 글

객체지향 프로그래밍  (0) 2020.03.22
실행 컨텍스트와 클로저 개념 정리  (0) 2020.02.25
함수와 프로토타입 체이닝  (0) 2020.02.02

+ Recent posts