JavaScript对象(2)——创建对象

来源:互联网 发布:追风打印软件 编辑:程序博客网 时间:2024/05/18 05:13

2. 创建对象

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

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 person1 = createPerson("Nicholas",29,"Software Engineer");    var person2 = createPerson("Greg",27,"Doctor");}

函数createPerson()能够根据接受的参数来构建一个包含所有必要信息的Person对象。可以无数次地调用这个对象,而每次它都会返回一个包含三个属性一个方法的对象。

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

2.2 构造函数模式

ECMAScript中的构造函数可以用来创建特定类型的对象。像Object和Array的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。

使用构建函数模式重写前面的例子:

function Person(name, age, job){    this.name = name;    this.age = age;    this.job = job;    this.sayName = function(newValue){        return this.name;    }}var person1 = new Person("Nicholas", 29, "Software Engineer");var person2 = new Person("Greg", 27, "Doctor");

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

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

此外,按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应以一个小写字母开头。这个做法是借鉴其他OO语言,主要是为了区别于ECMAScript中的其他函数;因为构造函数也是函数,只不过可以用来创建对象而已。

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

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

在前面例子的最后,person1和person2分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person,如下所示:

alert(person1.constructor == Person);       //truealert(person2.constructor == Person);       //true

对象的constructor属性最初是用来标识对象类型的。但是,提到检测对象类型,还是instanceof操作符更可靠以下。我们在这个例子中创建的所有对象既是Object的实例,同时也是Person的实例。

alert(person1 instanceof Object);       //truealert(person1 instanceof Person);       //truealert(person2 instanceof Object);       //truealert(person2 instanceof Person);       //true

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。

在这个例子中,person1和person2之所以同时是Object的实例,是因为所有对象均继承自Object

1. 将构造函数当作函数
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数也不会有什么两样。

//当作构造函数使用var person = new Person("Nicholas", 29, "Software Engineer");person.sayName();       //"Nicholas"//当作普通函数调用Person("Nicholas", 29, "Software Engineer");        //添加到windowwindow.sayName();       //"Greg"//在另一个对象的作用域中调用var o = new Object();Person.call(o, "Kristen", 25, "Nurse");o.sayName();        //"Kristen"

前两行代码是构造函数的典型用法,即,使用new操作符来创建一个新对象;
接下来的两行代码展示了不适用new操作符调用Person()会出现什么结果:属性和方法都被添加到window对象上了;
最后,也可以使用call()(或者apply())在某个特殊对象的作用域中调用Person()函数。这里是在对象o的作用域中调用的,因此调用后o就拥有了所有属性和sayName()方法。

2. 构造函数的问题
使用构造函数的主要问题是:每个方法都要在每个实例上重新创建一遍。不同实例上的同名函数是不相等的。

alert(person1.syaName == person2.sayName);      //false

可以修改为:

function Person(name, age, job){    this.name = name;    this.age = age;    this.job = job;    this.sayName = sayName;}function sayName(){    alert(this.name);}var person1 = new Person("Nicholas", 29, "Software Engineer");var person2 = new Person("Greg", 27, "Doctor");

上面的例子中,虽然person1和person2对象共享了在全局作用域中定义的同一个sayName()函数。但是,在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多歌全局函数,于是我们这个自定义的引用类型就不具备封装性了。
这些问题可以通过使用原型模式来解决。

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"var person2 = new Person();person2.sayName();          //"Nicholas"alert(person1.sayName == person2.sayName);      //true

与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1和person2访问的都是同一组属性和同一个sayName()函数。

原型模式存在如下问题:

  1. 省略了为构造函数传递参初始化参数的环节,结果所有实例在默认情况下都将取得相同的属性值;
  2. 引用类型值属性的任何操作都会被所有实例共享。
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"alert(person1.friends === person2.friends);     //true      

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

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。

重写前面的例子:

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.friends.push("Van");alert(person1.friends);                     //"Shelby,Count,Van"alert(person2.friends);                     //"Shelby,Count"alert(person1.friends == person2.friends);      //falsealert(person1.sayName == person2.sayName);      //true

在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性constructor和方法sayName()则是在原型中定义的。

这种构造函数与原型混成的模式,是目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。

2.5 动态原型模式

有其他OO语言经验的开发人员在看到独立的构造函数和原型时,很可能会感到非常困惑。动态原型模式正是致力于解决这个问题的一个方案,它把所有信息封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。

重写之前的例子:

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

这里只在sayName()方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。这里对原型的修改,能够立即在所有实例中得到反映。对于采用这种模式创的对象,还可以使用instanceof操作符确定它的类型。

使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建实例的情况下重写原型,那么就会切断现有实例与原型之间的联系。

2.6 寄生构造函数模式

通常,在签署的几种模式都不适用的情况下,可以使用寄生构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数。

使用寄生构造函数模式重写之前的例子:

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

在这个例子中,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", "blue", "green");alert(colors.toPipedString());              //"red|blue|green"

关于寄生构造函数:首先,返回的对象与构造函数或者构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么区别。为此,不能依赖instanceof操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。

2.7 稳妥构造函数模式

稳妥对象:使得是没有公共属性,而且其方法也不引用this对象。
稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this和new),或者防止数据被其他应用程序改动时使用。
稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;而是不使用new操作符调用构造函数。

重写之前的例子:

function Person(name, age, job){    //创建要返回的对象    var o = new Object();    //可以在这里定义私有变量和函数    //添加方法    o.sayName = function(){        alert(name);    };    //返回对象    return o;}

以这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访问name的值,可以像下面使用稳妥的Person构造函数。

var friend = Person("Nicholas", 29, "Software Engineer");friend.sayName();                   //"Nicholas"

这样,变量person中保存的是一个稳妥对象,而除了调用sayName()方法外,没有别的方式可以访问其他数据成员。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境。

与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此instanceof操作符对这种对象也没有什么意义。

本文章参考总结 ->JavaScript高级程序设计(第三版)

原创粉丝点击