一、 构造方法创建对象

Javascript创建对象的时候各种方法,工厂模式,构造函数模式,原型模式......以及在此基础上的组合模式,寄生模式.

当然平时使用的最多的应该是构造函数模式了,不过由于其每个方法都得重新创建一个遍,导致每个函数是不一样的,如果将对象方法指向构造函数外的函数,则对于封装没有什么意义。

因为全局的函数都可以访问,比如:

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

这样虽然能够共享,但是许多的全局函数依旧能够进行对象外的调用,没有封装性。

二、原型对象创建对象

原型模式就可以很好地做到函数的共享,当然prototype引起的引用完全共享也是存在问题的。

使用原型模式构造的对象:

    function Person(){}
    Person.prototype.name = "postbird";
    Person.prototype.age = 20;
    Person.prototype.sayName = function(){
        console.log(this.name);
    }
    var person1 = new Person();
    var person2 = new Person();
    console.log(person1.sayName == person2.sayName);// true

祭上《javascript高级程序设计》的一张图:

从下面的图里面能够很明确的看出:

  1. Person 的 prototype 属性指向的是Person的原型对象Person Prototype
  2. 原型对象的 constructor 又指向Person的构造方法
  3. person1 和 person2 两个实例的原型属性也指向Person的原型对象。

原型1.jpg

结果很明显,通过为对象的prototype原型对象增加原型属性或者原型方法能够非常容易的做到对某一个方法的共享。

三、原型对象存在的问题

1. 应用类型的共享问题

上面很明显可以看出,每一个方法和属性都是放在原型对象中的。如果想具体查看原型对象中有哪些东西,可以便利。

对象的 prototype 是不能进行便利的,但是提供了 Object.__proto__ 属性用来查看或者遍历(IE不支持)

console.log(person1.__proto__);

能够比较直观的看出,age/name/sayName 都是在 prototype 中的

3-1.jpg

原型模式中比较尴尬的就是所有的引用都是共享的,数组、方法、对象这些引用类型都是各个实例共享。

一个实例的实例属性可以自己进行控制:

下面的代码中,person1和person2是Person()的两个实例,并且包括name/numbers/sayName属性和方法

    function Person(){}
    Person.prototype.name = "postbird"; 
    Person.prototype.numbers = [1,2,3,4];
    Person.prototype.sayName = function(){
        console.log(this.name);
    }
    var person1 = new Person();
    var person2 = new Person();
    person1.numbers.pop();
    person1.name="newName"
    console.log(person1.name);// newName
    console.log(person2.name);// postbird
    console.log(person1.numbers);// 1,2,3
    console.log(person2.numbers);// 1,2,3

能够发现:

  • numbers 是一个数组,也就是引用类型的原型属性,由于引用类型都是共享的,所以当 person1 执行 person1.bumbers.pop() 时,同样会影响 person2 的numbers属性,因此在输出的时候,person1和person2的numbers是一样的,都是pop()之后的内容
  • person1 修改了 name 属性,没有影响 person2 的name属性是因为此时访问 person1.name 实际上访问的是实例属性,而不是原型属性。而 person2.name 是原型属性。

下面的代码很好的证明了这一点,访问 person1.__proto__.name 的时候,仍然是 postbird

    console.log(person1.name);// newName
    console.log(person1.__proto__.name);// postbird

原型模式的引用类型的共享是一个明显的缺点(有时候也不一定是缺点)

2. 传参数初始化问题

原型模通过prototype指定的属性,没有办法通过构造函数进行参数赋值

3. 重写原型对象的错误

原型模式中是不能重写原定对象的,当一个实例已经创建了,然后在重写原型对象,会切换创建的实例和原型对象的联系。

也就是说,重写之后的原型对象不适用于已经实例化的实例

    function Person(){}
    Person.prototype.sayHello = function(){
        console.log("Hello");
    }
    var person = new Person();
    person.sayHello();// Hello
     Person.prototype={
        name:"name",
        sayHello:function(){
            console.log("World");
        }
    }
    person.sayHello();// Hello
    var person2 = new Person();
    person2.sayHello() // World

上面的代码可以看出,当重写了Person的prototype之后,新的 sayHello() 方法并不适用于已经实例化的person,但是适用于之后实例化的 person2.

注意:

如果使用字面量对对象的 prototype 进行定义(就像是上面代码中的做法),则会被视为重写整个 prototype ,而不是更改某个方法或者属性

四、组合模式

所谓的组合模式就是结合了构造函数原型模式优点:

对于需要初始化的属性,通过构造函数来实现,而对于不需要初始化的属性,在 ptototype 中实现:

    function Person(name,age){
        this.name = name;
        this.age = age;
    }
    Person.prototype.sayName = function(){
        console.log(this.name);
    }
    var person1 = new Person("A",20);
    var person2 = new Person("B",10);
    console.log(person1.name);// A
    console.log(person2.name);// B
    person1.sayName();// A
    person2.sayName();// B

五、动态原型模式

我对于动态原型模式的理解无非就是:

对属性使用构造函数传参初始化,对于方法,进行判断,如果已经是function了(也就是已经指定过了)则不管他, 如果不是function(没有制定过)则指定一个原型方法

这样的好处也比较明显:

  • 所有的属性和方法都是在构造函数中完成的
  • 原型方法只需要指定一次,指定之后就不需要再次指定,同样能够对方法进行共享

动态原型的注意的地方是:

不能使用对象字面量重写原型,会切断现有实例与新原型之间的联系

六、寄生构造函数模式

“寄生”无非就是在构造函数里面创建一个新的对象再返回。

不是特别建议使用这个模式:

  • 返回的对象与构造函数或者与构造函数的原型属性之间没有关系(构造函数返回的对象与在构造函数外部创建的对象没有什么不同)

七、稳妥构造函数模式

稳妥构造函数更加像是其他语言能够声明 private 的属性。

只有指定的方法能够使用,其他不能使用。

可以看到,在这个过程中,没有为 o 指定age属性,因此是不能直接访问age属性的。

但是sayAge() 方法提供了能够访问 age 属性方法,此时认为 age 是安全的

    function Person(name,age){
        var o = new Object();
        o.name = name;
        o.sayAge=function(){
            console.log(o.age);
        }
        return o;
    }