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

本章内容

  • 理解对象属性
  • 理解并创建对象
  • 理解继承

一、理解对象

对象的定义为,无序属性的集合,其属性可以包含基本值、对象或函数。
可以先创建一个object实例,再为它添加属性,也可以利用对象字面量的方法创建对象。

var person = new Object();
person.name = "Nicholas";  //给对象添加属性
person.age = 29;
person.job = "Software Engineer";
person.sayName = function(){
alert(this.name);
};

//使用对象字面量添加属性
var person = {
    name: "Nicholas",
    age:29,
    job:"Software Engineer", 
    sayName: function(){
         alert(this.name);
    }
}
  
1.1 属性类型

对象属性的特性有两种:数据属性和访问器属性。他们是在Javascript内部使用的,因此必须使用Object.defineProperty方法来修改属性默认的特性。
数据属性,默认都为true

  • Configurable 一旦定义为false,就不能在定义为true了
  • Enumerable
  • Writable
  • Value
    Object.defineProperty()可以创建一个新的属性,但多数情况下没有必要利用Object.defineProperty()提供的这些高级功能。
var person = {};
//使用Object.defineProperty创建一个新的属性,如果不指定,其数据属性的默认值都为false
Object.defineProperty(person,"name",{
    writable: false,
    value: "Nicholas"
})
alert(person.name);
person.name = "Gerg";
alert(person.name);

访问器属性,只包含一对getter()和setter()函数。

  • Get 读取属性时调用,默认值为undefined
  • Set 写入属性时调用,默认值为undefined
var book = {
    _year:2014,
    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);

可以使用Object.defineProperties定义多个属性,使用Object.getOwnPropertyDescriptor取得属性的描述。

var book = {};
Object.defineProperties(book,{
    _year:{
        value:2004
    },
    edition:{
        value:1
    },
    year:{
        get:function(){
            return this._year;
        },
        set:function(newValue){
            if(newValue > 2004){
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    } 
})
var descriptor = Object.getOwnPropertyDescriptor(book,"_year");
alert(descriptor.value);  //2004
alert(descriptor.configurable); //false

var descriptor = Object.getOwnPropertyDescriptor(book,"year");
alert(descriptor.enumerable); //false
alert(typeof descriptor.get);  //function

二、创建对象

上节讲过创建对象的两种方式,一种是使用构造函数,一种是使用对象字面量。但这样做会产生大量的重复代码。

2.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("Nicholas", 29, "Software Engineer");

2.2 构造函数模式

ECMAScript可以使用构造函数来创建对象,像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。将上面的例子重写如下

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        alert(this.name);
    };
}
var person = new Person("Nicholas", 29, "Software Engineer");

我们可以注意到工厂模式与构造函数模式的区别如下:

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

按照惯例,构造函数始终应该以一个大写字母开头,创建的实例person会有一个constructor (构造函数)属性,该属性指向Person。
可以使用instanceof操作符检测对象类型。

    alert(person instanceof Object); //true
    alert(person instanceof Person);//true

这里要注意两个问题,一是构造函数也是函数,可以当做普通函数调用。若直接调用,属性和方法会被加到window对象上。二是以构造函数模式定义的方法会在每个实例上都重新创建一遍。然而创建两个完成同样任务的Function实例是不必要的。

2.3 原型模式

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

function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age=  29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};
var person1 = new Person();
person1.sayName(); //Nicholas
1.理解原型对象

无论什么时候只要创建一个,函数,就会为该函数创建一个prototype属性,这个属性指向函数的原型对象。原型对象默认只会取得constructor属性,其他方法是由Object继承来的。当用构造函数创建一个新实例时,这个实例的内部将包含一个指针[[Prototype]],指向构造函数的原型对象。一般无法访问[[Prototype]],但有两个方法可以确定原型操作方法。

//判断Person的原型对象是不是person1的原型对象
alert(Person.prototype.isPrototypeOf(person1)); //true 
//返回对象的原型
alert(Object.getPrototypeOf(person1) === Person.prototype) //true

虽然可以通过对象实例访问保存在原型中的值,但不能重写属性。如果在实例中添加一个新的属性,而该属性与原型中的一个属性同名,那么原型中的属性会被屏蔽。可以通过hasOwnProperty方法,判断什么时候访问实例属性,什么时候访问原型属性。

function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age=  29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};
var person1 = new Person();
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1);//true 关于in操作符,只要对象能够访问该属性
//不论属性是在实例中还是在原型中
alert(hasPrototypetypeProperty(person1,"name")); //true

person1.name = "Greg";
alert(person1.name);   //"Greg"----来自实例
alert(person1.hasOwnProperty("name")); //true
alert("name" in person1);//true
alert(hasPrototypetypeProperty(person1,"name")); // false

//若要取得对象上所有可枚举的实例属性,Object.keys()方法,接收一个
//对象为参数,返回一个包含所有可枚举属性的字符串数组。
var keys = Object.keys(Person.prototype);
alert(keys);//"name, age, job, sayName"

var p1 = Object.keys(person1);
alert(p1 );//"name"

//若要取得所有实例属性,无论它是否可枚Object.getOwnPropertyNames()方法
var keys = Object.getOwnPropertyNames((Person.prototype);
alert(keys); //"constructor, name, age, job, sayName"
3.更简单的原型语法

我们可以将Person.prototype设置为一个以对象字面量的方法创建的新对象。相当于完全重写了prototype对象。此时默认的constructor属性会指向Object。如有需要可将其设回适当的值。

function Person(){}
Person.prototype = {
    constructor: Person, //重新设置constructor属性,此时可枚举
    name: "Nicholas",
    age: 29,
    job: "Software Engineer",
    sayName: function () {
        alert(this.name);
    }
}
4.原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型的修改能够反映在所有实例上---即使先创建实例后修改原型也是如此。
可以随时随地为原型添加属性方法,但不能使用字面量创建的对象重写原型。重写原型将切断现有原型与原来已经存在的对象实例之间的联系,他们引用的仍然是之前的原型。

5.原生对象的原型

所有的原生引用类型(Object, Array, String)都在其构造函数的原型上添加了方法。

alert(typeof Array.prototype.sort);//function
alert(typeof String.prototype.substring);//function

通过原生对象的原型,不仅可以取得其所有方法的引用,也可以定义新方法,但不推荐在原生对象的原型上添加新方法。

6.原型对象的问题

原型对象省略了为构造函数初始化传递参数这一环节,并且所有的实例能够共享属性和方法。但它也存在问题,这一问题正是由于其共享的本性导致的。对于引用类型的属性值尤为突出。

function Person(){}
Person.prototype = {
    constructor: Person,
    name: "Nicholas",
    age: 29,
    job: "Software Engineer",
    friends:["Shelby", "Court"],
    sayName: function () {
        alert(this.name);
    }
}
var person1 = new Person();
var person2 = new Person();

person1.friends.push("Van");

alert(person1.friends); //"Shelby, Court, Van"
alert(person2.friends); //"Shelby, Court, Van"

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

对于上面6中提到的问题很容易解决,那就是组合使用构造函数模式和原型模式,这也是创建自定义类型最常用的方式。每个实例都保存自己实例属性的副本,同时又共享方法的引用。

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, "Software Engineer");
var person2 = new Person("Greg", 27, "doctor");

person1.push("van");
alert(person1.friends); //"Shelby, Court, Van"
alert(person2.friends); //"Shelby, Court"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName ); //true

2.5 动态原型模式

2.4中的构造函数与原型是分开写的。可以在构造函数中初始化原型,初始化原型的代码只会在初次构造函数的时候调用。

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, "Software Engineer");
friend.sayName();

2.6 寄生构造函数模式

基本思想是创建一个函数,该函数的作用仅仅是创建封装对象的代码,然后再返回新创建的对象。这个模式跟工厂模式是一模一样的,可以在特殊的情况下用来为对象创造构造函数。缺点是不能依赖instanceof操作符来确定对象类型。

function SpecialArray(){
    //创建数组
    var values = new Array();  //构造函数,不用this指针,重写构造函数返回时的值。
    //添加值
    values.push.apply(values, arguments);
    //添加方法
    values.toPipedString = function(){
        return this.join("|");
    };
    //返回数组
    return values;
}
var color = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green"

2.7 稳妥构造函数模式

稳妥对象最适合在一些安全的环境中(这些环境禁止使用this和new),或者防止数据被其他应用程序改动时使用。稳妥构造函数遵循与及生构造函数类似的模式。但有两点不同:一是新创建对象的实例方法不使用this。二是不使用new操作符调用构造函数。
在这种模式下创建的对象,除了使用成员方法外,不允许使用别的方法访问其数据成员

三、继承

继承是OO语言中的一个最为人津津乐道的概念。ECMAScript只支持实现继承,而其实现继承主要是依赖原型链来实现的

3.1原型链

基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。每个实例中都包含着一个指向原型对象的指针,原型对象中又包含着一个指向构造函数的指针,如果将原型对象的指针指向另一个类型的原型对象,那么该原型对象将可以继承其所有的原型属性和方法。

function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
}
function SubType(){
    this.subproperty  = false;
}
//继承了Super
Super.prototype = new SuperType();//将原型对象等于另一个类型的实例
SubType.prototype.gerSubValue = function(){
    return this.subproperty;
}
var instance = new SubType();
alert(instance.getSuperValue());//true

SubType的原型会保留有SuperType类型的实例属性,其prototype指针会指向SuperType的原型,也就是SuperType的原型属性可以通过原型链找到。

1. 别忘记默认的原型

所有的引用类型都默认继承了Object,这个继承也是通过原型链实现的。函数的默认原型也是Object实例。例如上述SubType继承了SuperType,而SuperType继承了Object。当调用instance.toString()是会调用保存在Object.prototype中的那个方法。

2.确定原型和实例的关系

可以通过两种操作符来确定实例和原型的关系,一是instanceof操作符,二是isPrototypeOf()方法。

alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true
alert(SuperType.prototype.isPrototypeOf(instance));//true
alert(SubType.prototype.isPrototypeOf(instance));//true
3.谨慎的定义方法

子类型有时候需要覆盖超类型中的某个方法,或者添加超类型中不存在的方法。但不管怎样给原型添加方法的代码一定要放在替换原型的语句之后。
另外,在通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样会重写原型链。

4.原型链的问题

原型链最主要的问题来自包含引用类型的原型。因为构造函数中定义的引用属性会被继承为子类型的原型属性,这时候就出现数组被所有实例共享的问题。
第二个问题是:在创建子类型的实例时,不能向超类型传递参数。或者说没办法在不影响所有对象实例的情况下,给超类型传递参数。

3.2 借用构造函数

为解决上述原型链中引用类型值带来的问题,开发人员开始使用一种叫做借用构造函数的技术。这种技术的思想相当简单,即在子类型构造函数的内部调用超类型的构造函数。别忘了,函数只不过是在特定环境下执行代码的对象。

function SuperType(){
    this.colors = ["red","blue","green"];
}
function SubType(){
    //继承了SuperType
    SuperType.call(this); //通过call方法借调了超类型的构造函数。
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);  //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors);  //"red,blue,green"

相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型中向超类型的构造函数传递参数。
但仅仅是借用构造函数,会有一个构造函数模式无法避免的问题------方法都在构造函数中了,方法复用就无从谈起了。在超类型中定义的方法,对子类型而言是不可见的。考虑这个问题,借用构造函数技术很少单独使用。

3.3 组合继承

组合继承指的是将原型链和借用构造函数的技术组合到一起,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,使用借用构造函数实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数的复用,又能保证每个函数有自己的实例属性。这是JavaScript中最常用的继承模式。

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
};
function SubType(name, age ){
    //继承属性
    Super.call(this.name);
    this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
}
var instance1 = new SubType("Nicholas",29);
instance1.colors.push("black");
alert(instance1.colors); //”red, blue,green,black”
instance1.sayName(); //”Nicholas”
instance1.sayAge(); //29

var instance2 = new SubType("Greg",27);
alert(instance2.colors); //”red,blue,green”
instance2.sayName(); //”Greg”
instance2.sayAge(); //27

3.4 原型式继承

原型式继承要求必须有一个对象作为另一个对象的基础。如果有一个对象的话,可以把它传递给object函数,然后再根据具体需求对得到的对象加以修改。ECMAScript5通过新增Object.create()方法规范了原型式继承。

var person = {
    name:"Nicholas",
    friends:["Shelby", "Court", "Van"]
}
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person);
anotherPerson.name = "Linda";
anotherPerson.friends.push("Barbie");

alert(person.friends); //” Shelby, Court, Van, Rob, Barbie”

3.5寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路,与及生构造函数模式和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。

function createAnother(original){
    var clone = object(original); //通过调用函数创建一个新对象
    clone.sayHi = function(){  //以某种方式来增强这个对象
    alert("hi");
}
return clone;  //返回这个对象
}

var person = {
    name: "Nicholas",
    friends:["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi();  //”hi”

3.6寄生组合式继承

前面说过,组合继承是Javascript中最常用的继承模式,但它每次都会调用两次超类型的构造函数,一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。子类型会包含超类型的全部实例属性,但又不得不在子类型的构造函数中重写这些属性。解决办法就是寄生组合式继承。

function inheriPrototype(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 = new SuperType();少调用一次超类型的构造函数
SubType.prototype.sayAge = function(){
    alert(this.age);
};

上述例子的高效率体现在它只调用了一次SuperType构造函数,并且避免了在Subtype.prototype上面创建不必要的多余的属性,与此同时,原型链还能保持不变。因此开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

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