初学面向对象

一、对象的创建

 创建自定义对象的最简单方式就是创建一个Object的实例,然后再为它添加属性和方法,如下:

var person = new Object();

person.name = 'wolfte';
person.age = 24;
person.job = 'f2e';

person.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('wolfte', 24, 'f2e');
person1.sayName();

 

可以无数次调用这个函数,每次它都会返回一个包含三个属性和一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

 

  • 构造函数模式
function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        alert(this.name);
    }
}

var person1 = new Person('wolfte', 24, 'f2e');
var person2 = new Person('mahui', 24, 'f2e');

除了与工厂模式中相同的部分外,还有几个不同之处:1、没有显式的创建对象  2、直接讲属性和方法赋给了this对象(这里的this指的是新创建的实例)  3、没有return语句

按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头

要创建新实例,必须用new操作符。以这种方式调用构造函数实际上会经历一下4个步骤:

(1)创建一个新对象
(2)将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
(3)执行构造函数中的代码(为新对象添加属性和方法)
(4)返回新对象

新创建的实例有一个constructor属性,该属性指向构造函数
对象的constructor书香最初是用来标识对象类型的,检测对象类型,instanceof操作符更可靠一些。
创建自定义构造函数意味着将来可以将它的实例标识为一种特定的类型,而这正是构造函数模式胜过工厂模式的地方。

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。person1和person2都有一个名为sayName()的方法,但那两个方法不是同一个Function的实例。 Es中的函数是对象,因此每定义一个函数,也就实例化了一个对象。

  • 原型模式

我们创建的每个函数都有一个prototype属性,这个属性是一个对象,它的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型的好处是可以让所有对象的实例共享它所包含的属性和方法。

function person() {

}

person.prototype.name = 'wolfte';
person.prototype.age = 24;
person.prototype.job = 'f2e';
person.prototype.sayName = function() {
    alert(this.name);
};

var person1 = new person();

理解原型
只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性。在默认情况下,所有prototype都会获得一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针_proto_(内部属性),指向构造函数的原型。所有的实现中都可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。如果对象的_proto_指向调用isPrototypeOf()方法的对象,返回true
alert(person.prototype.isPrototypeOf(person1))   //true

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

可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的指(引用类型除外)。
当为实例添加一个属性时,这个属性就会屏蔽原型对象中的同名属性。即这个属性只会阻止访问原型中的那个属性,但不会修改那个属性。使用delete操作符可以完全删除实例属性,从而能够重新访问原型中的属性。
使用hasOwnProperty()方法可以检测一个属性是否存在实例中。这个方法只在给定属性存在于实例中返回true。而in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。同时使用hasOwnProperty()方法和in操作符,就可以确定属性是否存在于原型中。

function hasPrototypeProperty(object, name) {
    return !object.hasOwnProperty(name) && (name in object);
}

只要in操作符返回true,而hasOwnProperty()返回false,就可以确定属性是原型中的属性。

更简单的原型语法--重写原型对象。重写原型后,constructor属性不在指向构造函数,而指向了Object构造函数。如果constructor
的值真的很重要,可以特意将它设置回适当的值。

原型的动态性
由于原型中查找值的过程是一次搜索,因此对原型对象所做的任何修改都能够立即从实例上反映出来,即使是先创建实例后修改原型也照样如此。但重写原型情况就不一样了。调用构造函数时会为实例添加一个指向最初原型的_proto_指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。   重写原型对象会切断现有原型与任何之前已经存在的对象实例之间的联系,之前存在的实例应用的仍然是最初的原型。

原型模式的问题
1、原型模式省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。
2、原型中所有属性是被实例共享的,这种共享对于包含基本类型值的属性倒也说的过去,然而,对于包含引用类型值的属性来说,问题就比较突出了。例:

function person() {

}

person.prototype = {
    friends: ['jim', 'tom']
}

var person1 = new person();
var person2 = new person();
person1.friends.push('lilei');
alert(person2.friends)

对person1.friends的修改也会通过person2.friends反映出来,这是因为friends数组存在于person.prototype中。

 

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

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时有共享着对方法的引用,最大限度的节省了内存。这是目前使用最广泛、认同度最高的一种创建即定义类型的方法。可以说,这是定义引用类型的一种默认模式。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ['tom', 'jim']
}

Person.prototype = {
    constructor: Person,
    sayName: function() {
        alert(this.name);
    }
};

var person1 = new Person('wolfte', 24, 'f2e');

 

  • 动态原型模式

动态原型模式把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的时候),又保持了同时使用构造函数和原型的优点。

function My(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;

    if (typeof this.sayName != 'function') {
        My.prototype.sayName = function() {
            alert(this.name);
        };
    }

}

这里只在sayName()方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要在做什么修改了。不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法确实可以说非常完美。其中,if语句检查的可以是初始化之后应该存在的任何属性或方法--不必用一大堆if语句检查每个属性和方法;只要检查其中一个即可。

 

  • 寄生构造函数模式

通常,在以上集中模式都不适用的情况下,可以使用寄生构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后在返回新创建的对象。

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 person = new Person('wolfte', 24, 'f2e');

在这个例子中,Person函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返回了该对象。除了使用new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回对象实例,而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。

这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组,由于不能直接修改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', 'green', 'blue');

在这个函数内部,首先创建了一个数组,然后push()方法(用构造函数接收到的所有参数)初始化了数组的值。随后,又给数组实例增加了一个toPipedString()方法,该方法返回以竖线分割的数组值。最后,将数组以函数值的形式返回。

关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者构造函数的原型属性之间没有关系,也就是说,构造函数返回的对象与在函数外部创建的对象没有什么不同。为此,不能依赖instanceof操作符来确定对象类型,建议在可以使用其它模式的情况下,不要使用这种模式。

 

二、对象的继承

js只支持实现继承,而且其实现继承主要是依靠原型链来实现的。

 

  • 原型链继承

原型链:
js将原型链作为实现继承的主要方法,其基本思路是利用原型让一个引用类型继承另一个引用类型的属性和方法。
构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的指针。假如让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
实现原型链有一种基本模式,其代码大致如下:

function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue = function() {
    return this.subproperty;
}

function SubType() {
    this.property = false;
}

SubType.prototype = new SubType();
SubType.prototype.getSubValue = function() {
    return this.subproperty;
}

var instance = new SubType();

在上面的例子中,原型是SuperType的实例。新原型不仅具有作为一个SuperType的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了SuperType的原型。最终结果是这样的:instance指向SubType.prototype,SubType.prototype指向SuperType.prototype。

通过实现原型链,本质上扩展了原型搜索机制。

所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。

确定原型和实例的关系
1、使用instanceof操作符,只要使用这个操作符来测试实例与原型链中出现过的构造函数,就会返回true。
2、使用isPrototypeOf(),只要是原型链中出现过的原型,都可以说是该原型链所派生出的实例的原型。因此,isPrototypeOf()也会返回true。


给原型链添加方法的代码一定要写在替换原型的语句之后。

原型链的问题:
最主要的问题来自包含引用类型值的原型。

function SuperType() {
    this.colors = ['red', 'green', 'blue'];
}

function SubType() {

}


SubType.prototype = new SuperType();

var instance1 = new SubType();
var instance2 = new SubType();
instance1.colors.push('yellow');
alert(instance1.colors)
alert(instance2.colors) 

 

 

  • 借用构造函数

这种技术的基本思路相当简单,就是在子类型构造函数内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用apply()或call()方法也可以在新创建的对象上执行构造函数。

function SuperType(name) {
    this.name = name;
}

function SubType() {
    SuperType.call(this, 'wolfte');
    this.age = 24;
}

var instance = new SubType();

这种方法存在的问题:方法都在构造函数中定义,因此函数复用就无从谈起了。

 

  • 组合继承

指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'green', 'blue'];
}

SuperType.prototype.sayName = function() {
    alert(this.name);
}

function SubType() {
    SuperType.call(this, 'wolfte');
    this.age = 24;
}

SubType.prototype = new SuperType();

SubType.prototype.sayAge = function() {
    alert(this.age);
}

组合继承模式最大的问题就是无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。

 

  • 原型式继承
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}


var person = {
    name: 'wolfte',
    friends: ['jim', 'tom']
}

var person1 = object(person);

在没有必要兴师动众的创建构造函数,而只是想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。不过别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。

 

  • 寄生式继承

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

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

var person = {
    name: 'wolfte',
    friends: ['jim', 'tom']
};

var person1 = createAnother(person);

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

 

  • 寄生组合式继承

所谓寄生组合式继承,就是通过借用构造函数来继承属性,通过原型的混成模式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,所需要的就是原型的一个副本。本质上就是使用寄生式继承来继承超类型的原型,然后再将结果指定给值类型的原型。

function inheritPrototype(subType, superType) {
    var prototype = object(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}

function SuperType(name) {
    this.name = name;
    this.friends = ['jim', 'tom'];
}

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);
};

 

这个例子的高效率体现在它只调用了一次超类型构造函数,并且因此避免了在子类型原型上创建不必要的、多余的属性。与此同时,原型链保持不变,因此,能够正常使用instanceof和isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承。

 

——摘自《Javascript高级程序设计》

 

 

 

 

 

 

 

 

posted @ 2013-07-09 00:04  好 孩 子  阅读(157)  评论(0编辑  收藏  举报