JS创建对象的模式

本文主要介绍创建对象的几大模式,叙述顺序与复杂程度基本一致。
最简单的创建对象方法是通过Object构造函数或对象字面量,如下所示,但这两种方法缺点明显:使用同一个接口创建很多对象时,会产生大量重复代码。

   // Object构造函数
   var obj=new Object(); 
   // 对象字面量
   var person={
         name:"Tina",
         age:26
   }

一. 工厂模式

为了解决通过Object构造函数或对象字面量创建多个对象会有代码重复的问题,引进了工厂模式。该模式抽象了创建具体对象的过程,以函数来封装用特定接口创建对象的细节。

    function createPerson (name, age){
          var obj=new Object();
          obj.name=name;
          obj.age=age;
          obj.SayName=function(){
              alert(this.name);
          }
          return obj;
    }
    var p1=createPerson("Tina",26);
    var p2=createPerson("Gorden",25);

这样就可以多次调用这个函数,每次只需传入初始化所需的参数,不会产生大量重复的创建对象的代码。##工厂模式虽然解决了创建多个类似对象的问题,但却无法知道一个对象的类型##(例如上段代码中,我们只知道p1, p2属于Object,但这有luan用)。

二. 构造函数模式

ECMAScript中本来就有Object、Array这样的原生构造函数,可用来创建特定类型的对象。此外,我们也可以创建自定义构造函数,从而定义自定义对象类型的属性和方法。

function Person(name,age) {
        this.name=name;
        this.age=age;
        this.sayName=function () {
            alert(this.name);
        }
    }
    var p1=new Person("Tina",26);
    var p2=new Person("Gorden",25);

在构造函数模式中,并没有显式地创建对象,而是直接将属性与方法赋予 this 对象(this 在这里指代通过构造函数创建出来的对象),也没有return语句。(另外构造函数一般首字母大写以和其他函数区分)。
要创建Person的实例,必须使用new操作符,这背后其实经历了以下4步:

  1. 创建一个新的对象;
  2. 将构造函数的作用域赋给该新对象(thus ‘this’ points to the newly created object.);
  3. 执行构造函数中的代码(为该新对象添加属性和方法);
  4. 返回这个新对象。

在上段代码中,p1, p2分别保存着Person的实例(并不相同),它们都有一个constructor属性,指向Person,该属性最初是用来标识对象类型的。不过,检测对象类型,最好还是使用 instanceof操作符。
创建自定义的构造函数,就可以将其创建的实例标识为一种特定的类型(比如通过Person构造函数创建的p1,我们知道p1的具体类型是Person,而不仅仅只是宽泛的Object ,证据就是 p1 instanceof Person=true )。这就是构造函数比工厂模式厉害的地方啊,诸位。
不过呢,构造函数模式也有缺点,主要问题就是新建对象时,每个方法都要在每个实例上重新创建一次。例如上段代码里,p1、p2都有一个名为sayName的方法,但这两个方法并不是同一个Function的实例(因为函数也是对象啊,上面第4行代码可等价为

 this.sayName=new Function("alert(this.name)");

,所以不同实例上的同名函数并不相等,而是互相独立的)。既然一个方法(函数)的功能对于各个实例而言都是一致的,那就没必要在每个实例上都绑定一个这样的方法。(打个比方,每个人都需要太阳,但并不意味着人人都要有一个专属的太阳,大家共享一个就好了)。下面就轮到原型模式来解决这个问题了。

三. 原型模式

3-1 基本定义

我们创建的每一个函数都有一个prototype(原型)属性,它是一个指针,指向的对象包含了可由特定类型的所有实例共享的属性与方法。prototype就是通过调用构造函数而创建的对象实例的原型对象。那些共享的属性与方法就不必定义在构造函数里了。

    function Person() {

    }
    Person.prototype.name="Nicholas";
    Person.prototype.age=29;
    Person.prototype.job="Software Engineer";
    Person.prototype.sayName=function(){
        console.log(this.name);
    }
    var p1=new Person();
    var p2=new Person();
    alert(p1.sayName===p2.sayName); //true

代码块3-1
prototype属性中包含的属性与方法是由所有实例共享的。(上段代码中,构造函数为空,p1、p2访问的都是同一组属性与同一个sayName方法。)
注意上段代码中,this 和 prototype 并不等同。this指的是通过构造函数新创建的对象实例,使用"for in" 语句遍历其属性时,返回的是所有能够通过对象访问的、可枚举的属性,结果不仅包括原型对象中的属性和方法,还有在构造函数中为新建对象实例赋予的属性、方法(如果有的话);而prototype原型对象并不包括在构造函数中为新建对象实例赋予的属性、方法。

    Person.prototype.helper=function () {
             console.log(this==Person.prototype); // false
    }
3-2 prototype、[[Prototype]]、constructor

只要创建了一个新函数,就会为其创建一个prototype属性,指向其原型对象。默认情况下,所有原型对象都会自动带有一个 constructor属性,指向该函数。
对于自定义构造函数,当使用它创建一个新实例后,该实例内部含有一个指针,指向构造函数的原型对象(该指针在ECMA中称为'[[Prototype]]',但在浏览器实现中并没有标准的方式去访问,常见的是通过“_proto_”(proto前后各有两个下划线))。
以代码块3-1为例,其中的prototype、[[Prototype]]、constructor指向关系如图1所示。注意:实例中的指针[[Prototype]]仅指向原型prototype,并不指向构造函数

JS创建对象的模式_第1张图片
图1 prototype、[[Prototype]]、constructor指向图

虽然不能在代码中直接访问到[[Prototype]],但是可通过 isPrototypeOf()方法来确定对象之间是否存在原型关系。例如 Person.prototype.isPrototypeOf(p1); //true
而在ES5中增加的方法Object.getPrototypeOf,则可以返回一个实例的原型对象。如 Object.getPrototypeOf(p1)==Person.prototype // true

3-3 对象实例与其原型的中的属性

当代码读取某个对象的一个属性时,首先从对象实例本身开始搜索,若找到了具有给定名字的属性,则访问该属性的值,否则会顺着原型链继续搜索。如果在对象实例中添加了一个属性,且该属性与实例原型中的一个属性同名,该实例中的属性会屏蔽原型中的同名属性。即使将实例中同名属性设置为null,访问该属性依然不会涉及到原型,除非使用delete操作符完全删除掉该实例属性。虽然可通过对象实例访问保存在原型中的值,但不能通过对象实例重写原型中的值。如代码块3-2所示。使用hasOwnProperty方法可以检测一个属性是存在与实例本身中,还是在原型中.

 function Person() {
    }
    Person.prototype.name="Nicholas";
    Person.prototype.age=29;
    Person.prototype.job="Software Engineer";
    Person.prototype.sayName=function(){
        console.log(this.name);
    }
    var p1=new Person();
    var p2=new Person();
    p1.name="Tina";
    alert(p1.name); // "Tina" --来自实例本身
    alert(p2.name); // "Nicholas" --来自原型

代码块3-2

3-4 重写原型对象

在原型对象(如Person.prototype)上逐个添加属性或方法,每添加一个,都要敲一遍Person.prototype。为了减少输入,同时视觉上更好封装原型,可采用对象字面量的方式直接重新整个原型对象,如代码块3-3所示。
不过这样做会使constructor属性不再指向Person构造函数。上文提到,每创建一个函数,就自动创建它对应的默认prototype对象,而这个默认prototype对象也会自动获得指向构造函数的constructor属性。之前的语法并没有重写默认的原型对象,只是为其添加属性或方法。但重写原型对象后,新的原型对象其实就是个普通对象,其中的constructor指向Object构造函数(原因如代码块3-4所示)。不过此时instanceof操作符还是可以返回正确的结果。如果constructor属性很重要,就在重写原型对象时显式指定(不过这样做会使得constructor属性变成可枚举的,与默认相反,可通过Object.defineProperty设置是否可枚举)。

    function Person() {}

    Person.prototype={
        // constructor: Person
        name:"Nicholas",
        age:29,
        job:"Software Engineer",
        sayName:function () {
            console.log(this.name);
        }
    }; // 这对{}代表的对象其实就是个普通对象

代码块3-3

    var a={
        age:15
    }
    alert(a.constructor===Object); //true

代码块3-4

重写整个原型对象,还可能导致以下问题。如代码块3-5所示,先创建了Person的一个实例对象,再去重写了其原型对象,后面调用friend.sayName()时报错,因为friend指向的原型中不包含以该名字命名的属性。该过程的内幕如图2所示,在重写原型对象之前就已存在的对象实例,引用的仍是最初的原型;后面重写了整个原型对象,但新的原型对象与重写前的实例对象之间并未建立起联系。(就像女朋友换了一任有一任,但心里有个位置,放的却始终都是初恋)

    function Person() {}
    var friend = new Person();
    Person.prototype={
        name:"Nicholas",
        age:29,
        job:"Software Engineer",
        sayName:function () {
            console.log(this.name);
        }
    };
    friend.sayName();//Error

代码块3-5

JS创建对象的模式_第2张图片
图2 重写原型对象前后

3-5 原型对象的问题

原型对象最大的问题源于其共享的本质。原型中的所有属性都是被特定类型的所有实例所共享,这对于本来就需要公用的方法(函数)而言正求之不得;对于包含基本值的属性无所谓,毕竟通过在实例上添加一个同名属性就可以屏蔽原型中的对应属性。But,对于包含引用类型值的属性而言,麻烦就大了。

   function Person() {}
   Person.prototype={
       constructor:Person,
       name:"Nick",
       friends:["Amy","Bob"],
       sayName:function () {
           console.log(this.name);
       }
   };

   var p1=new Person();
   var p2=new Person();
   p1.friends.push("Cindy");
   console.log(p1.friends); // ["Amy","Bob","Cindy"]
   console.log(p2.friends);// ["Amy","Bob","Cindy"]
   console.log(p1.friends===p2.friends);// true

代码块3-6
如代码块3-6所示,Person.prototype对象的friends属性包含的就是引用类型值(数组)。p1修改了该属性,而由于该属性不存在于p1实例本身之中,而是原型对象中的属性,所以这个修改会影响到所有Person类型实例。这就是为什么很少单独使用原型模式,而是组合使用构造函数模式和原型模式。
Note不过在代码块3-7中,直接将一个新的数组赋予实例p1的friends属性,结果却大不一样,欢迎在评论区留下你的见解!

   var p1=new Person();
   var p2=new Person();
   // p1.friends.push("Cindy");
   p1.friends=["Cindy"];
   console.log(p1.friends);// ["Cindy"]
   console.log(p2.friends);// ["Amy","Bob"]
   console.log(p1.friends===p2.friends);// false

代码块3-7
我推测原因是:直接将一个新的数组赋予实例p1的friends属性,背后执行的操作将为p1直接添加一个同名实例属性friends,屏蔽了原型对象中的同名属性。不过这仅是本人推测,并未验证。

四. 构造函数和原型组合模式

构造函数和原型组合模式才是目前在ES中应用最广泛、认同度最高的创建自定义类型的方法。前者用于定义实例属性,后者定义方法和共享的属性。

    function Person(name,age) {
        this.name=name;
        this.age=age;
        this.friends=["Amy","Bob"];
    }
    Person.prototype={
        constructor:Person,
        sayName: function () {
            alert(this.name);
        }
    }

    var p1=new Person("Tina",26);
    var p2=new Person("Knock",25);
    p1.friends.push("Cindy");
    console.log(p1.friends); // ["Amy","Bob","Cindy"]
    console.log(p2.friends); // ["Amy","Bob"]
    console.log(p1.friends===p2.friends); // false
    console.log(p1.sayName===p2.sayName); // true

代码块3-8
写了这么多,挺累的,其实还有动态原型模式、寄生构造函数模式、稳妥构造函数模式,不过这篇文章里就不提了。

你可能感兴趣的:(JS创建对象的模式)