创建对象

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

工厂模式

工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程。考虑到ECMAScript中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节。

function createPerson(name,age,job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName=function(){
        console.log(this.name)
    }
    return o;
}
var person1 = createPerson('Nicholas',29,'SoftWare');
var person2 = createPerson('gres',27,'Doctor');

函数cretePerson()能够根据接受的参数来构建一个包含所有必要信息的Person对象。可以无数次的调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但是却没有解决对象识别问题。

构造函数模式

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.sayName = function(){
        console.log(this.name);
    }
}
var person1 = new Person('Nicholas',29,'Software');
var person2 = new Person('gres',24,'Docotor');

在这个例子中,Person()函数取代了createPerson()函数。不同之处:

1.没有显式的创建对象。
2.直接将属性和方法赋给了this对象。
3.没有return语句。

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

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

上面的例子:person1和person2分别保存着Person的一个不同的实例。 这两个对象都有一个constructor属性,该属性指向Person。

person1.constructor == Person//true
person2.constructor == Person//true

person1 instanceof Object;//true
person1 instanceof Person;//true

创建自定义的构造函数意味着将来可以将它的实例标示为一种特定的类型;而这正是构造模式胜过工厂模式的地方。(以这种方式定义的构造函数是定义在Global对象(在浏览器是window对象)中的)。
构造函数与其它函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那他就可以作为构造函数;任何函数,如果不通过new运算符调用,那它就跟普通函数没有区别。
构造函数的问题:

使用构造函数的主要问题就是每个方法都要在每个实例上重新创建一遍。在上面的例子中,person1和person2都有一个名为sayName()的方法,但那两个方法不是同一个Function的实例。在js中函数也是对象,因此每定义一个函数,也就是实例化了一个对象。
function Person(name, age){
    this.name = name;
    this.age = age;
    this.sayName = new Function(console.log(this.name))
}

从这个角度上看构造函数,更容易明白每个Perosn实例都包含一个不同的Function实例的本质。说明白些,以这种方式创建函数,会导致不同的作用域链和标识符解析,但是创建Function新实例的机制仍然相同。因此不同实例上的同名函数是不相等的。

console.log(person1.sayName==person2.sayName);//false.

然而,创建两个完成同样任务的Function实例没有必要;况且有this对象在,根本不用在执行代码前就把函数绑定到特定对象上面。因此,可以向下面这样,通过把函数定义转移到构造函数外部来解决这个问题。

function Person(name,age){
    this.name = name;
    this.age = age;
    this.sayName = sayName;
}
function sayName(){
    console.log(this.name)
}
var person1 = new Person('ll',24);
var person2 = new Person('kk',25);

原型模式

我们创建的每一个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以有特定类型的所有实例共享的属性和方法。

function Person(){}
Person.prototype.name = 'll';
Person.prototype.age = 24;
Person.prototype.sayName=function(){
    console.log(this.name)
}
var person1 = new Person();
var person2 = new Person();

在此我们将sayName()方法和属性直接添加到Person的prototype属性中,构造函数变成了空函数。即使如此,也仍然可以通过调用构造函数来创建新的对象,而且新的对象还会具有相同的属性和方法。但与构造函数模式不同的是,新对象的属性和方法是由所有实例共享的。
理解原型对象:

无论什么时候,只要创建了一个新函数,就会根据一定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性是一个指向prototype属性所在函数的指针。Person.prototype.constructor指向Person。而通过这个构造函数,我们还可以继续为原型对象创建其它属性和方法。
创建了自定义构造函数之后,其原型对象默认只会得到constructor属性;至于其它属性和方法都是从Object对象继承而来的。当调用构造函数的一个新实例后,该实例内部将包含一个指针,指向构造函数的原型对象。(__proto__);person1和person2都包含一个内部属性,该属性仅仅指向了Person.prototype,和构造函数没有直接的关系。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性,搜索首先从对象实例本身开始,如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,如果在原型对象中找到了该属性,则返回该属性的值。
function Person(){}
Person.prototype.name = 'll';
Person.prototype.age = 24;
Person.prototype.sayName=function(){
    console.log(this.name)
}
var person1 = new Person();
var person2 = new Person();
person1.name = 'kk';
console.log(person1.name)//kk-来自实例
console.log(person2.name)//ll来自原型

当对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。

原型模式的缺点:

原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性也说的过去,通过在实例上添加一个同名属性可以隐藏原型中对应的属性。然而对于包含引用类型值的属性来说,问题就比较突出了:
function Person(){}
Person.prototype={
    constructor:Person,
    name:'kk',
    age:24,
    friends:['ll','jj'],
    sayName:function(){
        console.log(this.name);
    }
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push('aa');
console.log(person1.friends);//ll jj aa
console.log(person2.friends);//ll jj aa
console.log(person1.friends==person2.friends);//true

修改person1.friends引用的数组,person2同样会修改。

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

创建自定义类型的最常见方式,就是组合使用构造函数和原型模式。构造函数模式用于定义实例属性,原型模式用于定义共享的属性和方法。另外这种模式还支持向构造函数穿参数。

function Person(name,age){
    this.name = name;
    this.age = age;
    this.friends=['kk','ll'];
}
Person.prototype={
    constructor:Person,
    sayName:function(){
        console.log(this.name)
    }
}
var person1 = new Person('nnn',24);
var person2 = new Person('mmm',29);
person1.friends.push('aaa');
console.log(person1.friends);//kk ll aa
console.log(person2.friends);//kk ll 
console.log(person1.friends==person2.friends);//false
console.log(person1.sayName==person2.sayName);//true

寄生构造函数模式

function Person(name,age,job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName=function(){
        console.log(this.name);
    }
    return o;
}
var friend = new Person('kk',24,'software');

在这个实例中,Person函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返回了这个对象。除了使用new操作符并把使用的包装函数叫做构造函数之外,这与工厂模式没有什么区别。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数末尾添加一个return语句,可以重写调用构造函数时返回的值。
这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array的构造函数,因此可以使用这个模式:

function SpecialArray(){
    console.log(this)
    var values = new Array();
    values.push.apply(values,arguments);
    values.toPipedString=function(){
        return this.join('|')
    }
    return values;
}
var colors = new SpecialArray('red','blue','pink');
console.log(colors.toPipedString())

关于寄生构造函数模式,返回的对象与构造函数或则构造函数的原型属性之间没有关系。

稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而其方法也不引用this的对象。稳妥构造函数模式与寄生构造函数模式类似,但是有两点不同,一是创建对象的实例方法不引用this,二是不使用new操作符调用构造函数。

function Person(name,age,job){
    //创建要返回的对象
    var o = new Object();
    //可以在这里定义私有变量和属性
    //添加方法
    o.sayName=function(){
        console.log(name);
    }
    //返回对象
    return o;
}
var friend = Person('kk',24,'software');
friend.sayName();//kk

继承

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。

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

以上代码定义了两个类型:SuperType和SubType。每个类型分别有一个属性和一个方法。它们的主要区别在于SubType继承了SuperType,而继承是通过创建SuperType的实例,并将该实例赋值给SubType.prototype实现的。实现的本质是重写原型对象,代之以一个新类型的实例。
在上面的代码中,我们没有使用Subtype默认提供的原型,而是给它换了一个新的原型:这个新的原型就是SuperType的实例。于是新原型不仅具有SuperType的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了SuperType的原型。
原型链的问题:

1.引用类型值的原型属性会被所有实例共享。
2.在创建子类型的实例时,不能向超类型的构造函数中传递参数。

借用构造函数

在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数的技术。这种技术的思想相当简单,即在子类构造函数的内部调用超类型构造函数。

function SuperType(){
    this.colors = ['red','blue','pink']
}
function SubType(){
    //继承了SuperType
    SuperType.call(this);
}
var instance = new SubType();
instance.colors.push('black');
console.log(instance.colors);//red blue pink black
var instance2 = new SubType();
console.log(instance2.colors);//red blue pink

同时,借用构造函数有一个很大的优势,即可以在子类构造函数中向超类型构造函数传递参数。

function SuperType(name){
    this.colors = ['red','blue','pink'];
    this.name = name;
}
function SubType(){
    //继承了SuperType
    SuperType.call(this,'kk');
}
var instance = new SubType();
console.log(instance.name);//kk

借用构造函数的问题:

方法都需要在构造函数中定义,无法复用。

组合继承

function SuperType(name){
    this.name = name;
    this.colors=['red','blue','pink']
}
SuperType.prototype.sayName=function(){
    console.log(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.log(this.age);
}
var instance1 = new SubType('kk',24);
instance1.colors.push('black');
console.log(instance1.colors);//red blue pink black
instance1.sayName();//kk
instance1.sayAge();//24

var instance2 = new SubType('ll',26);
console.log(instance2.colors);//red blue pink
instance2.sayAge();//26
instance2.sayName();//ll

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点。

原型式继承

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

在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,然后返回了这个临时类型的一个实例。从本质上讲object()对传入中的对象进行了浅复制。

var Person={
    name:'kk',
    friends:['ll','aa','cc']
}
var instance1 = object(Person);
instance1.name = 'Greg';
instance1.friends.push('dd');
console.log(instance1)

var instance2 = object(Person);
instance2.name = 'Linda';
instance2.friends.push('oo');
console.log(instance2)

obejct()会返回一个新对象,这个对象将Person作为原型,所以它的原型中包含一个基本类型值属性和一个引用类型值属性。这就意味着friends不仅是Person的,也同时被instance1和instance2共享。

寄生式继承

寄生式继承思路和寄生式构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。

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

function createAnother(original){
    var clone = object(original);
    clone.sayName=function(){
        console.log(this.name)
    };
    return clone;
}
var Person={
    name:'ll',
    friens:['kk','cc','aaa']
}
var instance = createAnother(Person);
console.log(instance)
instance.sayName();

寄生组合式继承

组合继承是js最常用的继承模式;不过,他也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

function SuperType(name){
    this.name = name;
    this.colors=['red','blue','pink'];
}
SuperType.prototype.sayName=function(){
    console.log(this.name)
}
function SubType(name,age){
    SuperType.call(this,name);//第二次调用SuperType()
    this.age = age;
}
SubType.prototype = new SuperType()//第一次调用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge=function(){
    console.log(this.age);
}

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

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;//指定对象
}
function SuperType(name){
    this.name = name;
    this.colors = ['red','blue','pink'];
}
SuperType.prototype.sayName=function(){
    console.log(this.name);
}
function SubType(name,age){
    SuperType.call(this,name);
    this.age = age;
}
var aaa = inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge = function(){
    console.log(this.age);
}
var instance = new SubType('kk',24);
console.log(instance)

小结

在没有类的情况下,可以采用下列模式创建对象。
1.工厂模式,使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。这个模式后来被构造函数模式所替代。
2.构造函数模式,可以创建自定义引用类型,可以像创建内置对象实例一样使用new操作符。不过,构造函数也有缺点,即他的每个成员都无法得到复用,包括函数。
3.原型模式,使用构造函数的prototype属性来指定那些应该共享的属性和方法。组合使用构造函数模式和原型模式,使用构造函数定义实例的属性,而使用原型定义共享的属性和方法。

JavaScript主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型来实现的。这样,子类型就能够访问超类型的所有属性和方法,这一点基于类的继承很相似。原型链的问题是对象实例共享所有属性和方法,因此不宜单独使用。解决这个问题的技术是借用构造函数,即在子类型构造函数内部调用超类型构造函数。这样就可以做到每个实例都具有自己的属性,同时还能保证只使用构造函数模式来定义类型。使用最多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,而通过借用构造函数来继承实例属性。
1.原型式继承,可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制。而复制得到的副本还可以得到进一步的改造。
2.寄生式继承,与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,最后返回对象。为了解决组合继承模式由于多次调用超类型构造函数而导致的低效率问题,可以将这个模式和组合继承一起使用。
3.寄生组合式继承,集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方法。


Lessong
101 声望4 粉丝