ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 자바스크립트 객체 지향 프로그래밍
    Study/개발 2021. 4. 14. 16:29

    노란책

    이미지 출처

    프론트엔드 개발자를 위한 자바스크립트 프로그래밍 6장 요약

    객체 지향 프로그래밍

    ECMA 에는 클래스 개념이 없다. 객체란 그저 특별한 순서가 없는 값의 배열이다.
    객체: `프로퍼티의 순서 없는 컬렉션이며 각 프로퍼티는 원시값이나 객체, 함수를 포함한다.

    6.1 객체의 이해와 생성

    같은 인터페이스의 객체를 여러개 만들 때, 중복을 줄이기 위해 팩토리패턴을 사용하기 시작했다.

    팩토리 패턴

    특정 객체를 생성하는 과정을 추상화.

    function createPerson(name) {
        var o = new Object();
        o.name = name;
        o.sayName = function(){
            console.log(this.name);
        }
        return o;
    }
    
    var person1 = createPerson('name', 213, 'SWSD');

    문제점: 생성한 객체가 어떤 타입인지 알 수 없다.

    생성자 패턴

    자바스크립트 엔진이 발전하면서 생성자 패턴이 등장했다.
    생성자 함수를 만들어서 위의 팩토리를 대신하게 된다.

    function Person(name) {
        this.name = name;
        this.sayName = function(){
            console.log(this.name);
        }
    }
    
    var person1 = createPerson('name', 213, 'SWSD');

    Personnew와 함께 호출하면 다음과 같은 순서로 동작한다.

    1. 객체를 생성한다.
    2. 생성자의 this에 객체를 할당한다.
    3. 생성자 내부 코드를 실행한다.
    4. 새 객체를 반환한다.

    생성자를 통해 객체를 만들면 constructor 프로퍼티로 생성자를 알 수 있기도 하고, instanceof 연산자를 사용해서 인스턴스 타입도 식별할 수 있다는 점이 팩토리 패턴에 비해 큰 장점이다.

    문제점: 인스턴스마다 메서드가 생성된다.

    해결하기 위해

    function Person(name) {
        this.name = name;
        this.sayName = sayName;
    }
    
    sayName = function(){
        console.log(this.name);
    }

    이 방식으로 Person의 모든 인스턴스가 전역의 sayName이라는 함수를 참조하게 할 수 있지만, 이런 함수가 늘어나면 전역 스코프를 어지럽히게 되므로 문제가 있다.

    프로토타입 패턴

    프로토타입은 객체의 인스턴스가 가져야할 프로퍼티와 메서드를 담고 있는 객체이다. 프로토타입의 프로퍼티와 메서드는 객체 인스턴스 전체에서 공유된다.

    function Person() {
    }
    
    Person.prototype.name = 'jaem';
    Person.prototype.sayName = function(){
        console.log(this.name);
    }
    // 또는
    Person.prototype = {
        constructor: Person,  // constructor와의 연결이 끊어지면 안될 경우
        name: 'jaem',
        sayName: function () {
            console.log(this.name)
        }
    }
    
    var person1 = new Person();
    var person2 = new Person();
    
    person1.sayName === person2.sayName;  // true

    프로토타입 내부 슬롯은 포인터로 연결되었을 뿐이기 때문에, 런타임에 프로토타입이 변경되면 이미 만들어진 객체에서도 변경이 반영되는 듯이 동작한다.

    통째로 프로토타입을 덮어씌우는 경우는 포인터가 바뀌기 때문에 변화가 제대로 반영되지 않는다.

    네이티브 객체도 이를 활용해서 원하는 함수를 정의하여 쓸 수도 있다.

    문제점: 순수 프로토타입 패턴은 모든 프로퍼티를 공유하게 되기 때문에 불필요한 공유가 발생할 수 있다.
    특히, 객체나 배열같은 참조값을 공유하게 된다면 한 인스턴스에서 만든 변화가 모든 인스턴스에 적용될 수 있다.

    생성자 + 프로토타입 패턴

    생성자 패턴으로 인스턴스의 프로퍼티를 정의, 프로토타입 패턴으로 메서드와 공유 프로퍼티를 정의. 불필요한 공유는 막고, 참조 방식으로 메서드를 공유해 메모리를 절약할 수 있다.

    function Person(name) {
        this.name = name;
        this.friends = [];  // 각각의 객체가 배열을 공유하지 않고 가질 수 있다.
    }
    Person.prototype = {
        constructor: Person,  // constructor와의 연결이 끊어지면 안될 경우
        sayName: function () {
            console.log(this.name)
        }
    }
    
    var person1 = new Person('kim');
    var person2 = new Person('lee');
    
    person1.sayName === person2.sayName;  // true

    가장 흔히 사용되는 패턴!

    동적 프로토타입 패턴

    생성자 내부에 모든 정보를 캡슐화 하여 생성자와 프로토타입 구분으로 인한 혼란을 줄이면서, 생성자+프로토타입 방식의 이점도 활용하기 위한 방식.

    function Person(name) {
        this.name = name;
        this.friends = []; 
    
        if(typeof this.sayName != 'function') {
            Person.prototype = {
                constructor: Person,
                sayName: function () {
                    console.log(this.name)
                }
            }
        }
    
    }
    var person1 = new Person('kim');
    var person2 = new Person('lee');
    
    person1.sayName === person2.sayName;  // true

    if 문을 통해 첫 호출될 때만 프로토타입에 메서드 할당을 실행.

    문제점: 객체 리터럴을 통해 프로토타입을 덮어쓸 수 없다.

    기생 생성자 패턴

    생성자 안에 다른 객체를 생성하고 반환하는 동작을 넣는 것.

    function Person(name) {
        var o = new Object();  // 반환할 객체. this가 아님.
        o.name = new Object();
        o.sayName = function(){
            console.log(name)
        }
        return o;  // this를 반환하지 않음.
    }

    이 방식은 다른 방식으로는 불가능한 객체 생성자를 만들 수 있다. 예를 들어 메서드를 추가한 특별한 배열을 만드는 배열 생성자를 위해서 Array 생성자에 직접 접근하는 대신 이런식으로 wrapper 처럼 생성자를 만들어 주고 내부에서 해당 함수를 붙여주면 새로운 Array 객체를 만드는 생성자처럼 동작한다.

    문제점: 반환된 객체와 생성자, 생성자의 프로토타입 사이에 연결고리가 없다. 그러므로 instanceof 연산자도 작동하지 않는다.

    다른 해결 방법이 가능할 경우 쓰지 말아야함

    방탄 생성자 패턴

    durable object: 공용 프로퍼티가 없고 메서드가 this를 참조하지 않는 객체.

    thisnew를 허용하지 않는 보안환경 등에서 사용할 수 있다.

    function Person(name, age) {
        var o = new Object();  // 반환할 객체
    
        o.sayName = function(){
            console.log(name);
        }
    
        return o;
    }

    위와 같은 방식으로, 내부값에 접근할 방법이 없도록함. 이 방법도 생성자와 인스턴스 사이의 연결이 존재하지 않아서 instanceof 연산자가 동작하지 않음.


    6.2 상속의 이해

    ECMAScript는 인터페이스 상속은 안되고 구현 상속만 가능하다. 이는 프로토타입 체인을 통해 이뤄진다.

    function SuperType () {
        this.property = true
    }
    
    SuperType.prototype.getSuperValue = function() {
        return this.property;
    }
    
    function SubType () {
        this.subproperty =false;
    }
    
    SubType.prototype = new SuperType();
    SubType.prototype.getSubValue = function (){
        return this.subproperty;
    }
    
    var instance = new SubType();
    alert(instance.getSuperValue());  // true

    SubTypeprototypeSuperType 인스턴스를 할당해서 SuperType 인스턴스에 존재했을 프로퍼티와 메서드가 SubType.prototype에도 존재하게 된다. 이런 식으로 프로토타입 체인이 만들어지면서 상속이 구현된다.

    문제점: 프로토타입 프로퍼티에 들어있는 참조 값(배열, 객체 등)이 모든 인스턴스에서 공유되기 때문에 모든 객체가 참조하고 있는 배열이 같아서 하나만 변경하려다가 모두 변경되는 경우가 생길 수 있다. 그래서 프로퍼티는 일반적으로 생성자에 정의한다.

    그렇게 하더라도 프로토타입으로 객체를 생성해서 상속을 구현하면 인스턴스가 다른 타입의 프로토타입이 돼버리므로, 인스턴스 프로퍼티 였던 것들이 프로토타입 프로퍼티로 바뀐다.

    function SuperType () {
        this.colors = ['red', 'green', 'blue'];
    }
    
    function SubType () {}
    
    SubType.prototype = new SuperType();
    var instance1 = new SubType();
    instance1.colors.push('black');  // red, green, blue, black
    
    var instance2 = new SubType();
    console.log(instance2.colors);  // red, green, blue, black

    또다른 문제는 하위 타입 인스턴스를 만들 때 상위 타입 생성자에 매개변수를 전달할 수 없는 것

    그래서 프로토타입 체인만 단독으로 쓰는 경우는 별로 없다.

    생성자 훔치기 (constructor stealing)

    프로토타입과 참조 값에 얽힌 상속 문제를 해결하고자 만들어짐. 위장 객체 (object masquerading), 전통적 상속(classical inheritance)이라 부르기도 함.

    하위 타입 생성자 안에서 상위 타입 생성자를 호출한다.

    function SuperType () {
        this.colors = ['red', 'green', 'blue'];
    }
    
    function SubType () {
        SuperType.call(this)
    }
    
    var instance1 = new SubType();
    instance1.colors.push('black');  // red, green, blue, black
    
    var instance2 = new SubType();
    console.log(instance2.colors);  // red, green, blue

    이 방식을 사용하면 SuperType()에 들어 있는 객체 초기화 코드 전체를 SubType객체에서 실행하는 효과가 있고, 결과적으로 모든 인스턴스가 자신만의 colors 프로퍼티를 갖게 된다.

    • 매개변수 전달

    생성자 훔치기 패턴은 하위 타입 생성자 안에서 상위 타입 생성자에 매개변수를 전달할 수 있다.

    function SuperType (name) {
        this.name = name;
        this.colors = ['red', 'green', 'blue'];
    }
    
    function SubType(){
        SuperType.call(this, 'someName');
        this.age = 29;
    }
    
    var instance = new SubType();
    console.log(instance.name);  // someName
    console.log(instance.age);  // 29

    문제점: 커스텀타입에 생성자 패턴을 쓸 때와 같은 문제가 발생. 메서드를 생성자 내부에서만 정의해야 하므로 함수 재사용이 불가능. 또한 상위 타입의 프로토타입에 정의된 메서드는 하위타입에서 접근할 수 없는 문제도 있다. 그래서 생성자 훔치기 패턴도 단독으로 사용하는 경우는 드물다.

    조합 상속

    가상의 전통적 상속(pseudoclassical inheritance)라 부르기도 함.

    프로토타입 체인과 생성자 훔치기 패턴의 장점을 취하려는 것.

    1. 프로토타입 체인으로 프로토타입프로퍼티와 메서드를 상속한다.
    2. 생성자 훔치기 패턴으로 인스턴스 프로퍼티를 상속한다.

    함수를 재사용하면서 고유 프로퍼티를 가질 수 있도록 하기 위함.

    function SuperType (name) {
        this.name = name;
        this.colors = ['red', 'green', 'blue'];
    }
    
    SuperType.prototype.sayName = function (){
        console.log(this.name);
    }
    
    function SubType(name, age){
    
        //프로퍼티 상속
        SuperType.call(this, name);
        this.age = age;
    }
    
    SubType.prototype = new SuperType();
    SubType.prototype.sayAge = function(){
        console.log(this.age);
    }
    
    var instance1 = new SubType("Nicholas", 29);
    instance1.colors.push('black');
    console.log(instance1.colors);  // red, green, blue, black
    instance1.sayName();  // Nicholas
    instance1.sayAge();  // 29
    
    
    var instance1 = new SubType("Jaem", 27);
    console.log(instance1.colors);  // red, green, blue
    instance1.sayName();  // Jaem
    instance1.sayAge();  // 27

    이 방식은 자바스크립트에서 가장 자주 쓰이는 상속 패턴이다.

    문제점: SuperType 생성자를 두 번 호출하게 된다. -> 기생 조합 상속으로 해결함.

    프로토타입 상속

    엄격히 정의된 생성자를 쓰지 않고 상속을 구현.

    프로토타입을 써서 새 객체를 생성할 때, 반드시 커스텀 타입(생성자)를 정의할 필요는 없다!

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

    object 함수는 임시 생성자(F)를 만들어 주어진 객체를 생성자의 프로토타입으로 할당함. 이후 임시 생성자의 인스턴스를 반환한다.

    var person = {
        name: 'lee',
        friends: ['1', '2', '3'],
    };
    
    var anotherPerson = object(person);
    anotherPerson.name = 'kim';
    anoterPerson.friends.push('4');
    
    var yetAnotherPerson = object(person);
    yetAnotherPerson.name = 'park';
    yetAnoterPerson.friends.push('5');
    
    console.log(person.friends);  // 1, 2, 3, 4, 5  -> 공유되기 때문.

    프로토타입 상속의 개념을 공식적으로 수용한 것이 ES5의 Object.create() 메서드이다.

    생성자를 다 따로 만들 필요가 없다는 점이 매우 유용하다. 하지만, 참조값을 포함하는 프로퍼티들이 모두 그 값을 공유함을 유의해야한다.

    기생 상속

    기생생성자 패턴과 비슷. 상속을 담당할 함수를 만들고, 그 안에서 커스텀객체를 만들어 반환. new 는 사용하지 않는다.

    function createAnother(original) {
        var clone = object(original);
        clone.sayHi = function () {
            console.log('hi');
        }
        return clone;  // 클론을 반환.
    }
    
    var person = {
        name: 'lim'
    }
    
    var anotherPerson = createAnother(person);
    anotherPerson.sayHi();  // hi

    person을 프로토타입으로 하는 새 객체를 반환한다. anotherPerson은 person의 프로퍼티와 메서드를 모두 상속하며, sayHi() 메서드를 추가로 가짐.

    객체가 주 고려사항일 때 사용할 패턴이지, 커스텀 타입과 생성자에 어울리는 패턴은 아니다. 기생상속을 이용해 객체에 함수를 추가하면 생성자 패턴과 비슷한, 함수 재사용 관련 비효율성 문제가 생김을 유의해야한다.

    기생 조합 상속

    조합 상속은 자주 쓰이지만, 비효율적인 면이 있다. 상위 타입 생성자가 항상 두 번 호출되기 때문.

    function SuperType (name) {
        this.name = name;
        this.colors = ['red', 'green', 'blue'];
    }
    
    SuperType.prototype.sayName = function (){
        console.log(this.name);
    }
    
    function SubType(name, age){
        //프로퍼티 상속
        SuperType.call(this, name);  // 두번째 호출 (SubType 객체를 생성할 때; 생성자 훔치기)
        this.age = age;
    }
    
    SubType.prototype = new SuperType();  // 첫번째 호출 (SubType 생성자 함수가 만들어질 때; 프로토타입 체인)
    SubType.prototype.sayAge = function(){
        console.log(this.age);
    }

    이렇게 두 번 호출하게 되면, 첫 번째 호출에서 SubType의 프로토타입에 SuperType() 객체가 할당된다. 즉, 프로토타입 체인에 SuperType의 인스턴스 프로퍼티를 모두 갖는 객체가 등록된다. 이후 두 번째 호출에서 this에 바인딩 되어 SuperType()이 한번 더 호출되는데, 그러면 인스턴스에도 SuperType의 인스턴스 프로퍼티가 모두 등록이 돼서 첫번째 호출에서 프로토타입이 갖게된 인스턴스 프로퍼티를 가리게 된다.

    이런 낭비가 생기기 때문에, 상위 타입의 생성자를 호출하지 않고 프로토타입을 할당한다.

    // superType의 프로토타입만을 통해 생성자 없이 subType에서 superType을 상속.
    function inheritPrototype(subType, superType) {
        var prototype = Object.create(superType.prototype);
        prototype.constructor = subType;
        subType.prototype = prototype;
    }
    
    function SuperType (name) {
        this.name = name;
        this.colors = ['red', 'green', 'blue'];
    }
    
    SuperType.prototype.sayName = function (){
        console.log(this.name);
    }
    
    function SubType(name, age){
        //프로퍼티 상속
        SuperType.call(this, name);
        this.age = age;
    }
    
    inheritPrototype(SubType, SuperType);
    
    SubType.prototype.sayAge = function() {
        console.log(this.age);
    }

    이런 방식으로 작성하여 불필요한 중복을 막고, 프로토타입 체인도 온전히 유지되므로 instanceofisPrototypeOf도 정상 작동한다. 참조타입에서 가장 효율적인 상속패러다임으로 평가 받는다.

    728x90
Designed by Tistory.