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

关键的一章。

  1. 理解ES实现面向对象的“类”及“对象”的模式的方法。go
  2. 理解对象的两种属性以及属性特性、以及相关的设置和获取的方法。go
  3. 创建对象。8种创建方法的总结。从原型模式初步了解原型链。go
  4. 继承对象。go

“类”及“对象”

  • 一般OO中,类及对象的定义,以及二者关系:
    • 类的概念 : 类是具有相同属性和服务的一组对象的集合。为属于该类的所有对象提供了统一的抽象描述,其内部包括属性和服务两个主要部分。在面向对象的编程语言中,类是一个独立的程序单位,应该有一个类名并包括属性说明和服务说明两个主要部分。
    • 对象的概念:对象是系统中用来描述客观事物的一个实体,是构成系统的一个基本单位。一个对象由一组属性和对这组属性进行操作的一组服务组成。从更抽象的角度来说,对象是问题域或实现域中某些事物的一个抽象,它反映该事物在系统中需要保存的信息和发挥的作用;它是一组属性和有权对这些属性进行操作的一组服务的封装体。客观世界是由对象和对象之间的联系组成的。
    • 类与对象的关系就如模具和铸件的关系,类的实例化结果就是对象,而对一类对象的抽象就是类。类描述了一组有相同特性( 属性 ) 和相同行为 ( 方法 ) 的对象。
    • 总而言之,类是对象的抽象,对象是类的实例
  • ES中没有类的概念,通过原型链(实例->原型->原型的原型->...)实现。

理解对象(数据属性/访问器属性、属性特性、特性的设置方式、特性的获取方式)

  • 对象有两种属性:数据属性和访问器属性,可以用内部属性([[...]])来描述属性的特性(ES5)。
  • 数据属性(存储数据的值,可以进行相关的读取和修改),有如下特性:
    • [[Configurable]]:可否用delete删除属性,可否修改属性,可否修改属性的其他特性,可否将属性类型改成访问器属性。用构造函数或者字面量创建的属性默认值为true。
    • [[Enumerable]]:可否用for-in进行枚举。默认值同上。
    • [[Writable]]:可否修改属性的值,默认值同上。
    • [[Value]]:存储数据属性的值,一般为传入的属性值。
  • 访问器属性:不包含数据,但是包含一对函数getter(), setter()。顾名思义,读取访问器属性的时候,调用getter函数返回有效值;写入访问器属性的时候,通过setter传入新值,并由这个函数来决定如何处理传入的数据。此属性有4个特性 :
    • [[Configurable]]:可否用delete删除属性,可否修改属性,可否将访问器属性改成数据属性。
    • [[Enumerable]]:可否用for-in进行遍历。
    • [[Get]]:在读取的时候调用的函数,默认为undefined。
    • [[Set]]:在设置的时候调用的函数,默认为undefined。
    • 不可直接定义,需调用Object.defineProperty()。举例Object.defineProperty()的例子代码。
    • getter和setter不一定同时需要,非严格模式下返回undefined,严格模式下报错。
  • Object.defineProperty():修改属性的特性。3个参数:待处理对象、待处理属性、待处理属性的特性({名:值})
    • 注意:一旦将[[Configurable]]设置为false,就无法修改回去。那其他许多特性也没办法修改(具体参照[[Configurable]]的定义)
    • 如果调用Object.defineProperty()却不设置具体特性的值得话,将默认为false。
    •  1 var book = {
       2     _year: 2004,
       3     edition: 1  
       4 };
       5 
       6 Object.defineProperty(book, "year", {
       7     get: function(){
       8         return this._year;
       9     },
      10     set: function(newValue){
      11         if (newValue >2004){
      12             this._year = newValue;
      13             this.edition += newValue - 2004;
      14         }
      15     }
      16 });
      17 //未说明的[[Configurable]]属性默认为false。
      18 
      19 book.year = 2005;
      20 alert(book.edition); // 2
      21 /*
      22 这里的_year表示year属性不能直接访问和设置,只能通过getter和setter处理。
      23 也并不代表_year变成了访问器属性,实际上它还是数据属性。具体分析看本节中最后的代码的说明。
      24 */
  • _defineGetter()/_defineSetter():在不支持Object.defineProperty的浏览器中,可以使用_defineGetter_()或_defineSetter_()两个方法来定义getter函数和setter函数,只不过这个时候不能修改[[Configurable]]和[[Enumerable]]。
  • Object.defineProperties() :定义多个属性的多个属性特性。2个参数:待处理对象,待处理对象的属性及其特性({属性:{特性1:xx, 特性2:xx},...})
    • 经测试,与Object.defineProperty()同样,如果调用但是不设置具体特性值,则默认为false。
  • Object.getOwnPropertyDescriptor() :获取属性的特性。2个参数:待处理对象,待获取的属性。返回一个对象,存储特性以及特性值。
  • 属性特性的获取代码,注意区分和理解数据属性和访问器属性:
  • var book = {};
    Object.defineProperties(book, {
        _year: {
            value : 2004
        },//数据属性
        edition: {
            value: 1
        },//数据属性
        year:{
            get: function(){
                return this._year;
            },
            set: function(){
                if(newValue > 2004){
                    this._year = newValue;
                    this.edition += newValue - 2004;
                }
            }//访问器属性
        }
    });
    
    var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
    alert(descriptor.value);//2004
    alert(descriptor.configurable);//false
    alert(typeof descriptor.get);//"undefined"
    
    var descriptor = Object.getOwnPropertyDescriptor(book, "year");
    alert(descriptor.value); // "undefined"
    alert(descriptor.enumerable);//false
    alert(typeof descriptor.get); // "function"
    
    /*
    1、year为访问器属性,_year为数据属性
    2、_year的下划线表示不能直接访问和设置,与其是否是访问器属性无关
    3、调用defineProperties()方法但不说明的[[Configurable]], 
         [[enumerable]]等属性默认为false。
    */

创建对象

  • 归纳见下表(8种方式创建对象)
  • 创建方法 创建代码                                                                                                     优点 缺点
    通过构造函数或者字面量
    //构造函数
    var o = new Object();
    o.name = "Nicholas";
    o.age = 29;
    o.job = "Doctor";
    o.sayName = function(){
      alert(this.name)
    };
    //字面量
    var o ={
      name : "Nicholas",
      age : 29,
      job: "Doctor",
     sayName: function(){
        alert(this.name);
    }
    }
    较为直观的创建单个对象。 用一个接口创建多个对象的时候会产生大量的重复代码。
    工厂模式
    (用包装函数)
    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 person1 = createPerson("Nicholas", 29, "Teacher");
    var person2 = createPerson("Greg", 27,"Doctor");
    将创建对象的过程抽象,用函数封装以特定接口创建对象的细节,可以多次调用该工厂函数以创建多个相似的对象。
    无法知道一个对象的具体类型(没有解决对象识别的问题)。
    这里指的是更加具体的类型,或者说属于哪些对象的实例。
    构造函数模式(自定义构造函数)
    //首字母大写或者大写驼峰以与一般函数区分
    function Person(name,age,job){
        this.name = name;
        this.age = age;
        this.job = job;
        this.sayName = funciton(){
            alert(this.name)
        };
    }
    var person1 = new Person("Nicholas", 29,"Teacher");
    var person2 = new Person("Greg", 27, "Doctor");
    解决了对象识别的问题:
    1、新对象的constructor属性指向构造函数
    person1.constructor == Person.
    2、instanceof操作符下:
    person1 instanceof Person;
    person1 instanceof Object;
    均返回true
    每创建一个对象实例(此处为Person实例),都会创建一个不同的sayName函数的实例(虽然同名,实际上却创建了不同的作用域和作用域链),并没有必要。
    然而若是将sayName函数的声明放在构造函数之外,并且在构造函数中通过this.sayName = sayName;来进行调用。虽然解决了创建多个实例函数的问题,但有新的问题:
    - 全局环境中定义的函数只在局部作用域使用。
    - 当对象需要的方法(函数)较多的时候,失去了封装性。
    原型模式(利用构造函数的prototype属性)
    function Person(){};
    Person.prototype.name ="Nicholas";
    Person.prototype.age = 29;
    Person.prototype.job = "Doctor";
    Person.prototype.sayName = function(){
        alert(this.name)
    };
    //prototype指向函数原型对象,是所有实例的原型
    //以上这些属性和方法为所有实例共享
    var person1 = new Person();
    var person2 = new Person();
    alert(person1.sayName == person2.sayName);
    //返回true。解决构造函数模式的缺点。
    //或者更简单的写法
    function Person(){};
    Person.prototype = {
        constructor: Person
        //这里完全重写了prototype,所以constructor不会
        //自动指向构造函数(虽然instanceof可以正确运
        //用),可以手动添加。
        name: "Nicholas",
        age: 29,
        job: "Doctor",
        sayName : function(){ alert(this.name);}
    };
        Object.defineProperty(Person.prototype, "constructor",{
            enumerable: false,
            value: Person
    });
    //手动添加的constructor的[[enumerable]]默认为true。
    //所以可以通过这种方式重新设置(ES5)
    解决了构造函数模式的创建多个函数实例的问题,节省了空间、提高了效率。
    这种方式提供了完整的、比较合理的继承和覆盖机制。实际上还应用在原生对象的构造以及继承上。
    过于共享:
    1、会为所有属性和方法赋默认的初值。
    2、对于共享的基本类型属性,可以采用在实例中设置同名属性的方法对原型中的值进行覆盖。
    3、然而对于初衷并不是为了共享的引用类型的值(比如说数组等),一旦在一个实例上进行修改,很容易影响到所有实例的这个值。BIG PROBLEM。
    组合使用构造函数模式和原型模式(常用)
    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); }
    }
    var person1 = new Person("Nicholas", 29, "Teacher");
    var person2 = new Person("Greg", 27, "Doctor");
    person1.friends.push("Van");
    alert(person1.sayName === person2.sayName);
    //true。共享方法
    alert(person1.friends === person2.friends);
    //false。各自的引用类型。
    构造函数用以定义实例属性、原型模式用以产生共享方法和属性。
    同时还可以向构造函数传递必要的参数。
    动态原型模式(动态体现在:共享的属性或者方法如果存在,则不需要添加,不存在则添加。)
    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("Nicholas", 29, "Teacher");
    将所有的信息都封装在构造函数中(包括原型对象的初始化)。
    方法是:判断原型对象的某些属性和方法是否存在,若不存在则会添加。
    注意:这里不能使用字面量来构造原型对象。就像上面说过的,那么做会完全重写原型对象,从而断开实例和原型对象之间的关系。
    这里对原型的修改,可以立刻反应在实例中。
    寄生构造函数
    function Person(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 friend = new Person("Nicholas", 29, "Teacher");
    //为Array添加属性和方法,但不想破坏Array
    function SpecialArray(){
        var values = new Array();
        values.push.apply(values, arguments);
        values.toPipedString = function(){
            return this.join("|");
        };
    }
    var colors = new SpecialArray("red","blue","green");
    alert(colors.toPipedString());
    //"red|blue|green"
    与工厂模式相比:
    - 使用new操作符
    - 把包装函数看成构造函数
    比较少应用,一般用于需要在原生类型基础上添加方法和属性的场合,如左边例子。
    构造函数返回的对象与构造函数的原型属性没有任何关系,无法使用instanceof确定对象类型。所以一般是在其他模式无法使用的时候才会使用。
    稳妥构造函数模式
    (稳妥对象:没有公共属性,方法不引用this,适合在安全环境(不能用this和new的环境)中)
    function Person(name , age, job){
        var o = new Object();
        o.sayName = function(){
            alert(name);
        };
        return o;
    }
    //除了sayName没有其他方法可以访问name
    与寄生构造函数类似,但
    - 实例方法不引用this
    - 不使用new调用构造函数
    即使可以给稳妥对象添加方法和数据成员,但是不可能直接访问构造函数中的原始数据。具有安全性。
    构造函数返回的对象与构造函数的原型属性没有任何关系,无法使用instanceof确定对象类型。
  • 构造函数模式的说明
    • 与工厂模式的区别:没有显示创建对象、直接将属性和方法赋予给this对象,没有return语句。实际上,这部分的工作交给了new操作符完成。
    • new创建对象的时候经历了:创建新对象、将构造函数的作用域赋给这个新对象(this)、返回新对象。
    • 构造函数完全可以当做一般函数使用。
  • 原型模式的说明
    • 构造函数通过prototype属性指向原型对象、原型对象通过constructor指向构造函数。则该构造函数的实例继承所有原型对象的方法和属性(包括constructor)。
    • 除非如表格中为原型对象手动添加属性和方法,不然构造函数的原型对象默认只有constructor属性,以及从Object继承而来的其他方法和属性。实际上,这个原型对象是Object对象的实例。 (涉及到原型链。继承的问题)
    • 实际上,以此构造的对象实例内部有[[Prototype]]属性指向构造函数的原型对象(而非构造函数) 

    • 如图,person1和person2不直接具备除了内部属性[[Prototype]]之外的属性和方法,但是可以通过(向上)查找对象的方法继承原型对象的属性和方法。
    • 方法:
      • isPrototype()/Object.getPrototypeOf(person1):内部属性[[Prototype]]不可以直接访问,所以通过这两个方法找寻实例的原型。
      • hasOwnProperty()检测属性是否在实例中、还是原型中。当实例中存在的时候返回true
      • Object.getOwnPropertyDescriptor()在ES5中只能找到实例的属性特性。想要找到原型的属性特性只能在原型上应用词函数。 
      • in操作符
        • 单独使用:无论属性在原型还是实例都可以
        • for-in:无论属性在原型还是实例都可以,但是若为不可枚举则无法得到。但是若实例中屏蔽(同名可枚举属性)了该不可枚举的属性,同样可以得到。 
        • ES5中的Object.keys():可以得到所有可枚举的实例属性(注意是实例的属性)。 
        • Object.getOwnPropertyNames():可以获得所有实例属性(无论可否枚举) 
    • 注意“覆盖和继承”:向上查找对象(实例)---注意此处只是“覆盖”(或者说屏蔽了上一级的值)而不能通过改变实例的属性和方法来改变原型的属性或者方法。通过delete可以删除实例中的属性,以解除“覆盖” 
    • 原型的动态性:修改原型会立即影响到实例(因为实际上通过指针链接) 
    • 整理二者关系:修改原型-->影响实例;修改实例--->覆盖原型。 
    • 如果直接重写原型(Person.prototype),那么实例中的内部属性[[prototype]]虽然仍然指向原来的原型,但和新的原型之间的关系(引用)就断开了。 

继承对象

  • 在讨论多层原型、实例的时候需要考虑对象的继承。以下为6中继承方式的总结和对比。
  • 继承方法 代码                                                                                        优点 缺点
    原型链
    function SuperType(){
        this.colors = ["red", "blue", "green"];
        //超类型的实例属性
    }
     
    function SubType(){}
     
    SubType.prototype = new SuperType();
    //colors属性变成这个原型对象的属性
     
    var instance1 = new SubType();
    var instance2 = new SubType();
    instance1.colors.push("black");
    alert(instance2.colors);
    //"red,blue,green,black"
    //对于instance1, instance2来说,colors是共享属性
    是ES中实现“继承”的基础。
    - 如果只是简单的一层继承(只有一层构造函数、原型对象和它的实例)是没多大问题的。但如果多层的话,超类型中的实例属性会成为子(孙)类型的原型属性,这个时候对于引用类型(数组等)的属性就会出现问题。
    - 创建子类型实例的时候不能像超类型构造函数传递参数。
    借用构造函数(伪造对象或经典继承):利用call()和apply()来保持实例属性和方法。
    function SuperType(name){
        this.colors = ["red", "blue", "green"];
        this.name = name;
    }
     
    function SubType(){
        SuperType.call(this,"Nicholas");
       //继承了实例属性,并传递了参数
        
        this.age = 29;
        //新的实例属性。
    }
     
    var instance = new SubType();
    解决了原型链的传递参数以及对于实例属性的继承的问题。 显然,没有考虑原型对象中的方法和属性。
    组合继承(伪经典继承)= 原型链+借用构造函数
    (最常用)
    function SuperType(name){
        this.name = name;
        this.colors = ["red", "blue", "green"];
    }
     
    SuperType.prototype.sayName = function(){
        alert(this.name);
    };
     
    function SubType(name, age){
        SuperType.call(this,name);
        this.age = age;
    }//借用构造函数继承实例属性
     
    SubType.prototype = new SuperType();
    //原型链继承原型对象的方法
     
    /*
    两次调用构造函数——
    原理未变,超类构造函数的实例同样变成了
    原型属性和方法。但是借用构造函数的继承
    方式“覆盖”了原型对象中的同名方法和属性。
    从而实现对实例方法和属性的性质的保留。
     */
     
     
    SubType.prototype.sayAge = function(){
        alert(this.age);
    };
    两次调用构造函数——
    原理未变,超类构造函数的实例同样变成了原型属性    和方法。但是借用构造函数的继承方式“覆盖”了原型对象中的同名方法和属性。从而实现对实例方法和属性的性质的保留。
    instanceof()和
    isPrototypeOf()可用。
    在左栏中提到的。其实无论在原生对象中的属性和方法,还是实例中的属性和方法同样存在,不过后者覆盖了前者,才不至于共享超类构造函数中的实例属性。
    由此带来的缺点:实际上,两次调用了构造函数。call()一次,new一次。--- 这个缺点由寄生组合式解决。
    原型式继承
    //原理:
    function object(o){
        function F(){}
        F.prototype = o;
        return new F();
    }
    //以此直接创建传入对象o的实例。
     
    var person ={
        name : "Nicholas",
        friends: ["Sherry", "Court", "Van"]
    };
     
    var anotherPerson = object(person);
    anotherPerson.name = "Greg";
    anotherPerson.friends.push("Rob");
     
    var yetAnotherPerson = object(person);
    yetAnotherPerson.name = "Linda";
    yetAnotherPerson.friends.push("Barrie");
     
    alert(person.name);
    //"Nicholas"
    alert(yetAnotherPerson.name);
    //"Linda"
    alert(person.friends); 
    //"Sherry, Court, Van, Rob, Barrie"
     
    //Object.create()对原型式继承进行规范
    var anotherPerson = Object.create(person);
    //same as 上述例子
    /*
    第二个参数为额外添加的属性或方法的格式与
    Object.defineProperties()的第二个参数的相
    同(考虑描述符),以此方法指定的任何属性都
    会覆盖原型对象的同名属性。
    */
    无严格意义上的构造函数。借助原型,基于已有的对象(原生对象)创建新对象,同时不必因此创建自定义的类型。
    - 要求有一个对象作为依据,以此创建(实际上相当于副本,但是可以对基本类型进行改变对属性方法进行扩展)
    - 只考虑“相似”的时候
    对于引用类型会“共享”
    寄生式继承:(在原型式的基础上)
    function createAnother(original){
        var clone = object(original);
        //原型式,创建一个“相似”对象
        clone.sayHi = function(){
            alert("hi")
        };
     //增强这个对象。原型式是在构造后直接对实例改
     //变的。
        return clone;
    }
     
    var person = {
        name: "Nicholas",
        friends: ["Sherry", "Court", "Van"]
    };
    var anotherPerson = createAnother(person);
    anotherPerson.sayHi();//"hi"
    主要考虑对象(的相似)而非自定义类型或者构造函数的情况下,选择这个模式。
    object()不是必须的,只要可以返回新对象的函数都可以。
    无法函数复用。与创建对象时提到的构造函数模式的缺点相似。
    寄生组合式继承
    (理想方式)
    function inheritPrototype(subType, superType)
    {
        var prototype = object(superType.prototype);
        prototype.constructor = subType;
        subType.prototype =prototype;
    }
     
    function SuperType(name){
        this.name = name;
        this.colors = ["red", "blue", "green"];
    }
     
    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);
    };
    inheritPrototype(SubType, SuperType);
    //本来就只是为了得到超类原型对象的副本
    //用这种方法代替原先的调用构造函数获取原型对象
    //的方法和属性的方式。
  • 一般有两种实现继承的方式:接口继承和实现继承,前者继承方法签名,后者继承实际的方法。显然,ES的函数没有签名,所以只能用实现继承。 
  • 原型链:原型具有原型,原型是另一个原型的实例。通过利用重写原型来实现,如图,即SubType.prototype = new SuperType(); 
  •  

  • 注意
    • 观察书中相关代码,可以发现。构造函数SuperType()中的属性(不共享的),出现在了SuperType.prototype的实例SubType.prototype中,而SubType()中的属性,出现在SubType.prototype的实例instance中。因为这些是实例属性。而prototype中的属性和方法(共享)属于原型属性或方法,仍然存放在原型中。
    • instance.constructor不再指向SubType(),因为其原型对象被重写(成另一个对象的实例),constructor属于继承的属性,所以随着SuperType.prototype指向SuperType()。注意到不需要考虑指向Object()的问题。
    • 同一层的构造函数、原型对象和实例的关系:构造函数的prototype属性指向其原型对象;其原型对象的constructor属性指向构造函数(若constructor不是继承而来的时候,即只考虑原型链上只有一层的情况);而实例中的内部属性[[Prototype]]指向原型对象,若原型对象(用字面量的方式)完全重写,这这个指向就断开了。
  • instanceof操作符、isPrototypeOf()方法。
  • “覆盖”:在子类型中不仅重写原型属性会屏蔽超类型中的属性,重写方法也是。屏蔽的意思是,子类型中调用的是子类型中的值或者方法,超类型中调用的是超类型中原本的属性和方法。
  • 再次注意,慎重使用字面量写对象,此时这个对象的原型默认为Object.prototype,可能会断开预想中的原型链。
posted @ 2017-10-20 14:12  nebulium  阅读(166)  评论(0编辑  收藏  举报