《JavaScript高级程序设计》Chapter 6 面向对象的程序设计

Chapter 6 面向对象的程序设计

理解对象

  1. 使用对象字面量语法创建对象

    var person = {
        name: "Nicholas",
        age: 29,
        job: "Software Engineer",
        sayName: function() {
            alert(this.name);
        }
    };
    
  2. 属性类型

    • 为了表示某些特性是内部值,ES5把这些attribute放到了两对中括号中,例如[Enumerable]
    • ES中有两种属性:数据属性和访问器属性
      • 数据属性:包含一个数据值的位置,在这个位置可以读取和写入值。数据属性有4个描述其行为的特性:

        • 修改数据属性默认的特征,必须使用ES5的Object.defineProperty()方法,接收三个参数:属性所在的对象、属性名和一个描述符对象。其中描述符对象的属性必须是configurable enumerable writable value。设置其中的一或多个值,可以修改对应的特征值。如果不指定默认为false
        var person = {};
        Object.defineProperty(person, "name", {
                writable: false,
                value: "Nicholas"
            }
        );
        
        var person = {};
        Object.defineProperty(person, "name", {
                configurable: false,
                value: "Nicholas"
            }
        );
        Object.defineProperty(person, "name", {
                configurable: true, // 抛出错误:当configurable属性被定义为false时,就再也不能变回true了
                value: "Nicholas"
            }
        );
        
      • 访问器属性:不包含数据值,包含一对getter和setter函数(非必须)。在读取访问器属性时,会调用getter函数,该函数负责返回有效的值;在写入访问器属性时,调用setter函数并传入新值,该函数负责决定如何处理数据。访问器有如下4个特性:

        • 访问器必须使用Object.defineProperty()方法进行定义。
        var book = {
            _year: 2004,
            edition: 1
        };
        Object.defineProperty(book, "year", {
                get: function() {
                    return this._year;
                    },
                set: function(newValue) {
                    if(newValue > 2004) {
                        this._year = newValue;
                        this._edition += newValue - 2004;
                    }
            }
        );
        book.year = 2005;
        alert(book.edition); // 2
        
        • 只指定getter意味着属性不能写,尝试写入会被忽略;只指定setter意味着不能读,否则在非严格模式下会返回undefined。
  3. 定义多个属性

    • Object.defineProperties()
    • 接收两个对象参数:第一个要添加和修改其属性的对象、第二个对象的属性与第一个对象中要添加或修改的属性一一对应。
    var book = {};
    Object.defineProperties(book, {
            _year: {
                writable: true,
                value: 2004
            },
            edition: {
                writable: true,
                value: 1
            },
            year: {
                get: function() {
                    return this._year;
                },
                set: function() {
                    if(newValue > 2004) {
                        this._year = newValue;
                        this.edition += newValue - 2004;
                    }
                }
            }
        }
    );
    
  4. 读取属性的特征

    • getOwnPropertyDescriptor() 取得给定属性的描述符
    • 接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回一个对象:如果是访问器属性,这个对象的属性有configurable enumerable get set,如果是数据属性,这个对象的属性有configurable enumerable writable value。
    var book = {};
    Object.defineProperties(book, {
            _year: {
                writable: true,
                value: 2004
            },
            edition: {
                writable: true,
                value: 1
            },
            year: {
                get: function() {
                    return this._year;
                },
                set: function() {
                    if(newValue > 2004) {
                        this._year = newValue;
                        this.edition += newValue - 2004;
                    }
                }
            }
        }
    );
    var descriptor = Object.getOwnPropertyDescription(book, "_year");
    alert(descriptor.value); // 2004
    alert(descriptor.configurable); // false
    alert(typeof descriptor.get); // undefined
    

创建对象

  1. 工厂模式

    • 抽象创建具体对象的过程,生成函数,用以封装特定接口创建对象的细节。
    function createPerson(name, age, job) {
        var o = new Object();
        o.name = name;
        o.age = age;
        o.job = job;
        o.sayName = function() {
            alert(this.name);
        };
        return o;
    }
    var person = createPerson('Lawrence', 20, "Doctor");
    
  2. 构造函数模式

    • 可以创建自定义构造函数,从而定义自定义对象类型的属性和方法。
    function Person(name, age, job) {
        this.name = name;
        this.age = age;
        this.job = job;
        this.sayName = function() {
            alert(this.name);
        };
    }
    var person = new Person('Lawrence', 20, 'Doctor');
    
    • 使用 new 操作符创建新实例。过程:
      • 创建一个新对象
      • 将构造函数的作用域给新对象(因此this指向了这个新对象)
      • 执行构造函数中的代码
      • 返回新对象
    • 对象的 constructor 属性可以标识对象类型,例如 alert(person.constructor == Person); // true ,但是不建议使用。
    • 构造函数也是函数,直接调用构造函数会将函数定义的方法和属性添加值其执行环境对象上。
    • 问题:每个方法都要在每个实例上重新创造一遍,所以会导致不同的作用域链和标识符解析。所以不同实例上的同名函数是不相等的。解决方法:将函数定义转移到构造函数外部。
    function Person(name, age, job) {
        this.name = name;
        this.age = age;
        this.job = job;
        this.sayName = sayName;
    }
    function sayName() {
        alert(this.name);
    } // 会破坏封装性
    
  3. 原型模式

    • 创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。即不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
    function Person() {
    }
    Person.prototype.name = "Lawrence";
    Person.prototype.age = 20;
    Person.prototype.job = "Doctor";
    Person.prototype.sayName = function() {
        alert(this.name);
    }
    var person = new Person();
    alert(person.sayName()); // "Lawrence"
    
    • 原型对象:

      • 创建了自定义构造函数之后,其原型对象默认只会取得constructor属性。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。
      • 可以使用 isPrototypeOf() 方法来确定对象之间是否存在原型指向关系: alert(Person.prototype.isPrototypeOf(person)); // true
      • 可以使用 getPrototypeOf() 方法来返回对象的原型:alert(Object.prototype.getPrototypeOf(person).name); // 'Lawrence'
      • 每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值(即使是null);如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。
      • constructor属性也是共享的,可以通过对象实例访问。
      • 不能通过对象实例重写原型中的值。
      • 使用delete操作符可以完全删除实例属性,从而重新访问到之前被实例属性屏蔽掉的原型属性。
      function Person() {
      }
      Person.prototype.name = "Lawrence";
      var person = new Person();
      person.name = "Qwerty";
      alert(person.name);
      
      delete person.name;
      alert(person.name)  ;
      
      • 可以使用hasOwnProperty()方法检测属性是否存在于实例(不是原型)中:alert(person.hasOwnProperty('name')); // true
      • 直接在原型上使用Object.getOwnPropertyDescriptor()以获取原型属性的描述符。
    • 原型与 in 操作符

      • in操作符返回true但是hasOwnProperty()返回false则说明属性是原型中的属性。所以可以定义如下函数:
      function hasPrototypeProperty(object, name) {
          return !object.hasOwnProperty(name) && (name in object);
      }
      
      • 对对象使用for-in循环,会返回所有能够通过对象访问的、可枚举([Enumerable]==true)的属性。
      • 可以使用Object.keys()方法获取对象上所有可枚举的实例属性。
      function Person() {
      }
      Person.prototype.name = 'Hello';
      Person.prototype.age = 20;
      Person.prototype.sayName = function() {
          alert(this.name);
      };
      var keys = Object.keys(Person.prototype); // "name,age,job,sayName"
      var person = new Person();
      person.name = "Lawrence";
      var pKeys = Object,keys(person); // "name"
      
    • 更简单的原型语法

      • 使用对象字面量重写prototype
      function Person() {
      }
      Person.prototype = {
          name: 'Lawrence',
          age: 20,
          sayName: function() {
          alert(this.name);
          }
      };
      // 会丢失 Person.prototype.constructor 属性,转而指向 Object.constructor 属性
      alert(person instanceof Person) // true
      alert(person.constructor == Person) // false
      alert(person.constructor == Object) // true
      // 应当在重写prototype时手动添加constructor:
      Person.prototype = {
          constructor: Person, // 这样设置会造成constructor可枚举
          name: 'Lawrence',
          age: 20,
          sayName: function() {
              alert(this.name);
          }
      };
      // 使 constructor 不可枚举
      Object.defineProperty(Person.prototype, 'constructor', {    
          enumerable: false,
          value: Person
      });
      
    • 原型的动态性

    • 可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果重写整个原型对象,将会破坏[[Prototype]]指针。实例中的指针仅仅指向原型,而不指向构造函数

    • 原型对象的问题

      • 原型中所有的属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性,通过在实例上添加一个同名属性可以隐藏原型中的对应属性。然而对于包含引用类型值的属性,则这些引用会共享,造成一些问题。
  4. 组合使用构造函数模式和原型模式(创建自定义类型的最常见方式)

    • Example:
    function Person(name, age, job) {
        this.name = name;
        this.age = age;
        this.job = job;
        this.friends = ['Shelby', 'Court'];
    }
    Person.prototype = {
        constructor: Person,
        sayName: function() {
            alert(this.name);
        }
    }
    
  5. 动态原型模式

    • 把所有信息都封装在构造函数中,通过在构造函数中初始化原型(仅在必要的情况下)保持同时使用构造函数和原型的优点。换言之,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
    function Person(name, age, job) {
        this.name = name;
        this.age = age;
        this.job = job;
        if(typeof this.sayName != 'function') {
            Person.prototype.sayName = function() {
                alert(this.name);
            }
        }
    }
    var friend = new Person('Lawrence', 20, 'Doctor');
    
    • 不能使用对象字面量重写原型!
  6. 寄生构造函数模式

    • 创建一个函数,其作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
    function Person (name, age, job) {
        var o = new Object();
        o.name = name;
        o.age = age;
        o.sayName = function() {
            alert(this.name);
        };
        return o;
    }
    var friend = new Person('Lawrence', 20, 'Doctor');
    
    • 修改类似Array这样不能直接修改其构造函数的类型的时候,可以使用此方法。
    function SpecialArray() {
        var values = new Array();
        values.push.apply(values, arguments);
        values.toPipedString = function() {
            return this.join('|');
        }
        return values;
    }
    var colors = new SpecialArray('red', 'blue', 'green');
    alert(colors.toPipedString());
    
    • 构造函数返回的对象与构造函数的原型属性之间没有关系,不能依赖 instanceof 操作符确定对象的类型。
  7. 稳妥构造函数模式

    • 稳妥对象指的是没有公共属性,而且其方法不引用this的对象。最适合在一些安全的环境中、或者在防止数据被其他应用程序改动时使用。
    • 稳妥构造函数类似寄生构造函数,但不使用this、不使用new。
    function Person(name) {
        var o = new Object();
        this.name = name;
        o.sayName = function() {
            alert(name);
        }
        return o;
    }       
    var friend = Person('Lawrence');
    friend.sayName();
    
    • 除了调用sayName()方法外,没有别的方法能够访问到其数据成员。
    • 构造函数返回的对象与构造函数的原型属性之间没有关系,不能依赖 instanceof 操作符确定对象的类型。

继承

  1. 接口继承 & 实现继承

    • 接口继承:只继承方法签名
    • 实现继承:继承实际的方法
  2. 原型链(实现继承的主要方法)

    • 利用原型让一个引用类型继承另一个引用类型的属性和方法。
    • 实现思路:让原型对象等于另一个类型的实例。
    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();
    }
    
    // *** 此时 SubType对象的constructor指向的是SuperType,因为SubType.prototype中的constructor被重写了(SubType.prototype被指向了SuperType)。
    
    • 实现基础:搜索机制(见“原型对象”)。
    • 确定原型和实例的关系
    var instance = new SubType();
    alert(instance instanceof SuperType); // true
    alert(instance instanceof SubType); // true
    
    alert(SuperType.prototype.isPrototypeOf(instance)); // true
    alert(SubType.prototype.isPrototypeOf(instance)); // true
    
    • 给原型添加方法代码一定要放在替换原型的语句(继承语句)之后。

    • 在使用原型链实现继承时,不能使用对象字面量创建原型方法。这样做会重写原型链。

    • 原型链的问题

      • 在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章的变成了现在的原型的属性了。
      function SuperType() {
          this.colors = ['red'];
      }
      function SubType() {
      }
      SubType.prototype = new SuperType();
      var instance_1 = new SubType();
      instance_1.colors.push('black');
      var instance_2 = new SubType();
      alert(instance_2.colors); // 'red,black'
      
      • 在创建子类型的实例时,不能向超类型的构造函数中传递参数。
  3. 借用构造函数

    • 在子类型构造函数的内部调用超类型的构造函数。通过使用apply()和call()方法在新创建的对象上执行构造函数。
    function SuperType() {
     this.colors = ['red'];
    }
    function SubType() {
        SuperType.call(this);
    }
    var instance = new SubType();
    instance_1.colors.push('black');
    var instance_2 = new SubType();
    alert(instance_2.colors); // 'red'
    
    • 可以在子类型构造函数中向超类型传递参数。
    function SuperType(name) {
        this.name = name;
    }
    function SubType() {
        SuperType.call(this, 'Lawrence');
    }
    
    • 借用构造函数的问题:方法都要在构造函数中定义,无法实现函数复用。
  4. 组合继承

    • 将原型链和借用构造函数结合。使用原型链实现对原型属性和方法的继承,通过借用构造函数来对实例属性进行继承。
    • 既能通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性。
    function SuperType(name) {
        this.name = name;
        this.colors = ['red'];
    }
    SuperType.prototype.sayName = function() {
        alert(this.name);
    };
    function subType(name, age) {
        SuperType.call(this, name);
        this.age = age;
    }
    SubType.prototype = new SuperType();    SubType.prototype.constructor = SubType;
    SubType.prototype.sayAge = function() {
        alert(this.age);
    }
    
  5. 原型式继承

    • 借助原型可以基于已有的对象创建新对象,同时不必因此创建自定义类型。
    function object(o) {
        function F(){}
        F.prototype = o;
        return new F();
    
    • ES5新增了Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
    var person = {
        name: 'Lawrence',
        friends: ['Q', 'W', 'E']
    };
    var anotherPerson = Object.create(person);
    anotherPerson.name = 'Greg'; // 会覆盖掉原型对象上的属性
    anotherPerson.friends.push('Qwerty');
    
  6. 寄生式继承

    • 与寄生构造函数和工厂模式类似,创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是他做了一切工作一样返回对象。
    function createAnother(original) {
        var clone = object(original);
        clone.sayHi = function() {
            alert('Hi');
        };
        return clone;
    }
    
    • 无法函数复用。
  7. 寄生组合式继承

    • 组合继承问题:无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,一次是在子类型构造函数内部。

    • 基本模式

    function inheritPrototype(subType, superType) {
        var prototype = Object(superType.prototype);
        prototype.constructor = subType;
        subType.prototype = prototype;
    }
    
    • Example:
    function SuperType(name) {
        this.name = name;
        this.colors = ['red'];
    }
    SuperType.prototype.sayName = function() {
        alert(this.name);
    };
    function SubType(name, age) {
        SuperType.call(this, name);
        this.age = age;
    }
    inheritPrototype(SubType, SuperType);
    SubType.prototype.sayAge = function() {
        alert(this.age);
    };
    

推荐阅读更多精彩内容