第六章——面向对象的程序设计

面向对象语言都有一个标志,那就是他们都有类的标志。

ECMA-262定义对象:“无序属性的集合,其属性可以包含基本值、对象或者函数。”我们可以把ECMAScript对象想象成散列表,无非就是一组名值对,其中值可以是数据或函数。

每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型,也可以是开发人员自定义的类型。

理解对象


1.属性类型

数据属性
数据属性有四个描述其行为的特性。

  • Configurable:表示能否通过delete删除属性,从而重新定义属性;能否修改属性的特性,或者说能否把属性修改为访问器属性。(true)

  • Enumerable:能否通过for-in循环返回属性。(true)

  • Writable:能否修改属性的值。(true)

  • Value:数据值。(undefined)

      var person = {
          age:100 
      };
    
      Object.defineProperty(person,"name",{
         configurable:false,
         writable:false,
         value:"xiaochang"
      });
    
      Object.defineProperty(person,"tall",{
          value:160
      });
    
      for(attr in person){
              console.log(attr); //name,age
      }
      console.log(person.name);   //xiaochang
      person.name="CC";           //为name属性指定新值
      console.log(person.name);   //xiaochang
      delete person.name;         //删除name属性
      console.log(person.name);   //xiaochang
      
      console.log(person.age);    //100
      person.age=200;             //为age属性指定新值
      console.log(person.age);    //200
      delete person.age;          //删除age属性
      console.log(person.age);    //undefined
      
      console.log(person.tall);   //160
      person.tall = 160;          //修改tall属性的值
      console.log(person.tall);   //160
      delete person.tall;         //删除name属性
      console.log(person.tall);   //160
    

要修改属性默认的特性,必须使用ECMAScript5的Object.defineProperty()静态方法,接收三个参数:属性所在对象、属性名字、描述符对象。其中描述符对象的属性必须是configurable、enumerable、writable、value的其中一个或多个值。

可以多次调用Object.defineProperty()方法修改同一个属性,但在把configurable属性设为false后就会有限制了,也就是说不能再修改回可配置了,也就是不能修改除Writable之外的特性了。

调用Object.defineProperty()如果不指定的话默认值为false。

访问器属性
访问器属性有四个特性。

  • Configurable:表示能否通过delete删除属性,从而重新定义属性;能否修改属性的特性,或者说能否把属性修改为访问器属性。(true)

  • Enumerable:能否通过for-in循环返回属性。(true)

  • Get:读取属性。(undefined)

  • Set:写入属性。(undefined)

      var person = {
          _name:"xiaochang", //name属性只读不可写
          _age:100,          //age属性只写不可读
          _tel:123456      //tel属性可读可写
      };
      Object.defineProperty(person,"name",{
          get:function(){
              return this._name;
          }
      });
      Object.defineProperty(person,"age",{
          set:function(newage){
              this._age = newage;
          }
      });
      Object.defineProperty(person,"tel",{
          get:function(){
              return this._tel;
          },
          set:function(newtel){
              this._tel= newtel;
          }
      });
      console.log(person.name);   //"xiaochang"
      person.name = "CC";         //尝试修改name属性
      console.log(person.name);   //"xiaochang"
      console.log(person.age);    //不可读属性,undefined
      person.age = 200;           //修改age
      console.log(person._age);   //直接读取对象方法才能访问的属性,可以看到值已更新200
      console.log(person.tel);    //123456
      person.age = 654321;        //更新tel
      console.log(person.tel);    //654321
    

使用访问器属性的常见方法,即设置一个属性的值会导致其他属性发生变化。

两个遗留的方法:

    person.__defineGetter__("tel",function(){
        return this._tel;
    });
    person.__defineSetter__("tel",function(newtel){
        this._tel = newtel;
    });

在不支持Object.defineProperty()方法的浏览器中不能修改configurable、enumerable。

2.定义多个属性

Object.defineProperties()方法。两个参数:要添加和修改其属性的对象、第二个对象的属性与第一个对象要添加和修改的属性一一对应。

    var person = {
        _name:"xiaochang", //name属性只读不可写
        _age:100,          //age属性只写不可读
        _tel:123456      //tel属性可读可写
    };
    Object.defineProperties(person,{
         _test:{
            value:2004
         },
         test:{
            get:function(){
                return this._test;
            }
        },
        name:{
            get:function(){
                return this._name;
            }
        },
        age:{
            set:function(newage){
                this._age = newage;
            }
        },
        tel:{
            get:function(){
                return this._tel;
            },
            set:function(newtel){
                this._tel= newtel;
            }
        }
    });

3.读取属性的特性

Object.getOwnPropertyDescriptor()方法。两个参数:属性所在的对象、要读取其描述符的属性名称。返回的是一个对象,如果是数据属性,则这个对象的属性有configurable、enumerable、writable、value,如果是访问其属性,则这个对象的属性有configurable、enumerable、get、set。

    var descriptor = Object.getOwnPropertyDescriptor(person,"tel");
    for(attr in descriptor ){
        console.log(attr+":"+descriptor[attr]);
    }
    
    运行结果如下:
    get:function (){return this._tel;}
    set:function (newtel){this._tel= newtel;}
    enumerable:false
    configurable:false

创建对象


虽然Object构造函数或对象字面量都可以用来创建单个对象,但这个方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,人们开始使用工厂模式的一种变体。

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 person1=createPerson ("DJL1",22,"student1");
var person2=createPerson ("DJL2",22,"student2");
alert(typeof createPerson ("DJL2",22,"student2"));//object
alert(person1 instanceof createPerson);//这肯定是false啦

工厂模式虽然解决了创建多个相似对象这个问题,但却没有解决对象识别问题(即怎样知道一个对象的类型)。

2.构造函数模式

function Person (name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.sayName=function(){
        alert(this.name);
    }
}
var person1=new Person ("DJL1",22,"student1");
var person2=new Person ("DJL2",22,"student2");
alert(person1 instanceof Person);//true

Person与上文不同的地方在于:

  • 没有显式创建对象
  • 直接将属性和方法赋给了this对象
  • 没有return语句

要创建Person的新实例必须使用new操作符,这种方式调用构造函数实际上会经历以下四个步骤:
(1)创建一个新对象
(2)将构造函数的作用域赋给新对象(也就是说,构造函数中的this指向这个新对象,或者说赋给这个新对象)
(3)执行构造函数中的代码(也就是说,为新对象添加属性和方法)
(4)返回新对象

对象的constructor属性用来标识对象类型,但是,提到检测对象类型,还是instanceof更可靠一些。

所有对象均继承自Object。

将构造函数当做函数
构造函数与其它函数的唯一区别,就在于调用他们的方式不同。不过,构造函数毕竟也是函数,不存在定义调用函数的特殊语法。任何函数,只要通过new操作符调用,那它就可以作为构造函数。
一个新对象被创建。它继承自xxx.prototype.

当在全局作用域中调用一个函数时,this对象总是指向Global对象。

构造函数的问题
主要问题:每个方法都要在每个实例上重新创建一遍。(浪费空间?)即每个实例的Function实例都不同。

但是,创建两个完成同样任务的Function实例的确没有必要;况且有this对象在,根本不用在执行代码前就把函数绑定到特定对象上面。因此,可以像下面这样将函数定义转移到构造函数外。

function Person (name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.sayName=sayName;
}

function sayName(){
    alert(this.name);
}
    
var person1=new Person ("DJL1",22,"student1");
var person2=new Person ("DJL2",22,"student2");

这样做虽然解决了两个函数做同一件事的问题,但一是在全局作用域定义的函数实际上只能被某个对象调用,这让全局作用域名不副实。而且更让人无法接受的是如果对象定义了许多方法就要定义许多全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。

3.原型模式

比较好的参考资料在这里。

我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,此对象包含所有实例共享的属性和方法。因此,可以将所有实例共享它包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是将这些信息直接添加到原型对象中。

理解原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。并且所有原型对象自动获得一个constructor属性,这个属性包含指向该函数的指针。

当调用构造函数创建一个新实例后,该实例内部将包含一个指针,指向构造函数的原型对象。这个链接存在于实例与构造函数的原型对象之间。

虽然在所有实现中都无法访问到[[Prototype]],但可以通过isPrototypeOf()来确定对象之间是否存在这种关系。

alert(Person.prototype.isPrototypeOf(person1));//true

Object.getPrototypeOf()可以方便地取得一个对象的原型。

当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。

虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。新创建的属性会屏蔽原型中的属性,且搜索时优先搜索示例中的值。只有使用delete操作才可以删除实例属性,重新回到访问原型属性。

使用hasOwnProperty()可以检测一个属性是否存在与实例中。

原型与in操作符
两种使用方法:单独使用、在for-in中使用。

属性在对象上或者在原型上都返回true

在使用for-in循环时,返回的是所有能通过对象访问的、可枚举属性,包括实例属性和原型属性。

for-in 循环不会遍历那些 enumerable 设置为 false 的属性;比如数组的 length 属性。但是屏蔽属性的那个原则在此仍然成立。

要取得对象上所有可枚举属性,可以使用Object.keys()方法,接收一个对象为参数,返回一个包含所有可枚举属性的字符串数组。

如果想要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法。

更简单的原型语法
可以使用一个包含所有属性和方法的对象字面量来重写整个原型对象。但是这种方法本质上重写了默认的prototype对象,因此constructor属性不再指向原有类型了。但还可以通过instanceof来确定类型。如果constructor很重要可以自行设定其值。但注意自行设置的值的Enumerable属性被置为true,因此,也可以自己通过defineProperty来设置。

原型的动态性
尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象的实例中反映出来,但如果是重写整个原型对象就不一样了。因为会切断构造函数与最初原型之间的指针。

原生对象的原型

原型对象的问题
问题是:省略了为构造函数传递初始化参数这一环节,所有实例默认情况下都取得相同的值。这种共享对于函数非常合适,对于那些包含基本值的属性也说得过去,毕竟可以覆盖,但对于包含引用类型值的属性来说,问题就比较突出了,一个push了那就都修改了。

4.组合使用构造函数模式和原型模式

构造函数模式:定义实例属性;
原型模式:定义方法和共享属性;

5.动态原型模式

function Blog(name, url) {
    this.name = name;
    this.url = url;

    if (typeof this.alertInfo != 'function') {
        // 这段代码只执行了一次
        alert('exe time');
        Blog.prototype.alertInfo = function() {
            alert(thia.name + this.url);
        }
    }
}

6.寄生构造函数模式

function SpecialArray(){
    var values = new Array();
    values.push.apply(values, arguments);
    values.toPipedString = function(){
        return this.join("|");
    }
    return values;
}
var a = new SpecialArray(2,6,8,9,4);
a.toPipedString();
var b = SpecialArray(2,6,8,9,4);
b.toPipedString();

寄生构造函数模式和工厂模式没有本质区别,通过new 操作符的就叫寄生构造函数模式,直接调用的就叫工厂模式。

JS里的构造函数就是一个用来构造对象的普通函数,和JAVA不同

你要知道,通过new 来调用函数,会自动执行下面操作:
(1)创建一个全新的对象;
(2)这个对象会被执行[[prototype]]连接原型prototype上;
(3)构造函数中的this值会绑定到新对象;
(4)执行构造函数中的代码;
(5)如果函数没有返回其他对象,那么new构造就会自动返回这个新对象;
由于这里new调用和直接调用都返回values,所以a,b引用的数组对象是一样的

function Foo(name){
   this.name = name;
}
new Foo("bar");
1.var obj = {};
2.obj.__proto__ = Foo.prototype;
3.4.Foo.call(obj);
5.return obj;

这里使用寄生构造函数的目的是希望扩展Array的一个方法toPipeMessage,作者本意是期望能像使用普通Array一样使用SpecialArray,故虽然把specialarray当成函数也一样能用,但是这并不是作者的本意,也变得不优雅。

7.稳妥构造函数模式

稳妥对象指的是,没有公共属性,而且其方法也不引用this的对象。

function Person(name, age, job) {
    var o = new Object();
 
    // private members
    var nameUC = name.toUpperCase();

    // public members
    o.sayName = function() {
        alert(name);
    };
    o.sayNameUC = function() {
        alert(nameUC);
    };

    return o;
}

var person = Person("Nicholas", 32, "software Engineer");

person.sayName(); // "Nicholas"
person.sayNameUC(); // "NICHOLAS"

alert(person.name);  // undefined
alert(person.nameUC);  // undefined

凡是想设为 private 的成员都不要挂到 Person 返回的对象 o 的属性上面,挂上了就是 public 的了。
当然,这里的 private 和 public 都是从形式上类比其他 OO 语言来说的,其实现原理还是 js 中作用域、闭包和对象那一套。感觉实现得挺巧妙的。

稳妥对象最适合在一些安全环境中(这些环境会禁用this和new)使用,或者在防止数据被其他应用程序改动时使用。

稳妥构造函数采用的是与寄生构造函数模式相似的模式,除了下列两点不同:
1.创建对象的实例方法不引用this;
2.不使用new调用构造函数;
所以构造函数适合在禁用this和new的环境中使用(或者说设计的出发点)。

继承


1.原型链

基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。

注意不要把构造函数和对象搞混了,不要把[[Prototype]]和prototype搞混了,不要把带this和不带this的函数属性搞混了!

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

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

alert(instance.property);

上面,property一会继承,二会覆盖。继承属性,继承方法,属性会覆盖,constructor是父类。子对象的原型是父对象的实例。

调用instance.getSuperValue()会经历三个步骤:
(1)搜索实例;
(2)搜索subType.prototype;
(3)搜索superType.prototype;

别忘记默认的原型
所有引用类型默认都继承了Object,当然也会继承Object的属性和方法。

确定原型和实例的关系
(1)使用instanceof操作符,原型链中出现过的类型就会返回true。
(2)使用isPrototype(),原型链中出现过的类型就会返回true。

alert(Object.prototype.isPrototypeOf(instance));

谨慎地定义方法
(1)给原型添加方法的代码一定要放在替换原型的语句之后。
(2)通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样做就会重写原型链,把原来的原型链切断。

原型链的问题
(1)包含引用类型值的原型;
(2)创建子类型的实例时,不能向超类型的构造函数中传递参数。也就是说,没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

2.借用构造函数

即在子类型构造函数的内部调用超类型构造函数。函数是特定环境中执行代码的对象,因此通过使用apply()和call()方法也可在将来新创建的对象上执行构造函数。

function SuperType(){  
    this.colors = ["red", "blue", "green"]; // 父类对象中存在着引用类型,这里是一个数组类型  
}  
  
function SubType(){  
    SuperType.call(this);   // 继承了SuperType,别忘记将this对象传递进去  
}  
window.onload = function(){  
    var instance_1 = new SubType();  
    instance_1.colors.push("black");  
    alert(instance_1.colors);    // >> "red, blue, green, black"  
      
    var instance_2 = new SubType();  
    alert(instance_2.colors);   // >> "red, blue, green"  
}

细节请参见上文使用new构造对象的过程。

通过使用call方法,我们实际上在未来新创建的SubType实例的环境下调用了SuperType构造函数。这样就会在新的SubType对象上执行SuperType函数中定义的所有对象初始化代码。

传递参数
借用构造函数的优势:可以在子类型构造函数中向超类型构造函数传递参数。

为了确保SuperType构造函数不会重写子类型属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。

借用构造函数的问题
(1)没有函数复用;
(2)在超类型的原型中定义的方法,对于类型而言也是不可见的,结果是所有类型都只能使用构造函数模式;

3.组合继承

使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承。这样既通过在原型上定义方法实现了函数复用,有能够保证每个实例都有它自己的属性。

function SuperType(name) { 
    this.name = name 
    this.numbers = [1,2,3] 
} 
SuperType.prototype.sayName = function (){ 
    console.info(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 (){ 
    console.info(this.age) 
} 
var instance1 = new SubType('A',23) 
instance1.numbers.push(-1) 
console.info(instance1.numbers) // [ 1, 2, 3, -1 ] 
instance1.sayName() // A 
instance1.sayAge() // 23 
var instance2 = new SubType('B',27) 
console.info(instance2.numbers) // [ 1, 2, 3 ] 
instance2.sayName() // B 
instance2.sayAge() // 27

4.原型式继承

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

这种方法并没有使用严格意义上的构造函数。而是借助原型基于已有的对象创建新对象,同时还不必因此创建自定义类型。

ECMAScript5规范化了原型式继承,使用Object.create()方法,两个参数:一个用作新对象原型的对象、(可选)一个为新对象定义额外属性的对象。

第一个参数和上述clone方法相同,第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。

在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承完全可以胜任。但要注意引用类型值的属性会共享。

5.寄生式继承

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

function createAnother(original) {
    var clone = object(original);
    clone.sayHi = function() {
        alert("hi");
    };
    return clone;
}

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示范继承模式时使用的object()函数不是必须的;任何能够返回新对象的函数都使用于此模式。

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。

6.寄生组合式继承

组合式继承有它的不足,即每次都会调用两次超类型构造函数。

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);       //第二次调用SuperType()
    this.age = age;
}

SubType.prototype = new SuperType();  //第一次调用SuperType()
SubType.prototype.construcotr = SubType;
SubType.prototype.sayAge = function() {
    alert(this.age);
}

加粗字体的行中是调用SuperType构造函数的代码。在第一次调用SuperType构造函数时,SubType.prototype会得到两个属性:name和colors;它们都是SuperType的实例属性,只不过现在位于SubType的原型中。当调用SubType构造函数时,又会调用一次SuperType构造函数,这一次又在新对象上创建了实例属性name和colors。于是,这两个属性就屏蔽了原型中的两个同名属性。

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

其思路就是:不必为了指定子类型的原型而调用超类型的构造函数,我们需要的无非是超类型的一个副本而已。本质上就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

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

function inheritPrototype(subType, superType) {
    var prototype = object(superType.prototype);  //创建对象
    prototype.constructor = subType;              //增强对象
    subType.prototype = prototype;                //指定对象
}

注意,这里弥补了重写原型而失去的默认constructor属性。

这个方法的高效率体现在它只调用了一次SuperType构造函数,并且因此避免了在SubType-prototype上创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能正常使用instanceof和isPrototypeOf。

开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

你可能感兴趣的:(第六章——面向对象的程序设计)