JavaScript创建对象的7种模式详解:从基础到稳妥构造

作者:小括号时间:2026-10-21 22:31:42浏览:132

本文详细解析JavaScript中创建自定义对象的七种核心模式。从基础的Object实例与字面量出发,深入探讨工厂模式、构造函数模式、原型模式的原理与局限。重点讲解当前最广泛使用的构造函数与原型组合模式,以及动态原型、寄生和稳妥构造函数模式的适用场景与安全特性,帮助开发者根据需求选择最佳方案。

JavaScript对象定义与基础创建

根据ECMA-262标准,对象被定义为“一组无需特定顺序的属性集合”,其属性值可以是基本数据类型、其他对象或函数。这意味着对象本质上是一个由名称映射到值的键值对集合,类似于散列表结构,其中值可以是数据或可执行函数。

创建自定义对象最直观的方法是实例化Object类,随后手动添加属性和方法。具体实现方式如下所示:


var person = new Object();、person.name = "liubei";person.age = 29;
person.job = "shayemuyou";
person.sayName = function(){ 
 alert(this.name);
}

上述代码构建了一个名为person的对象,包含三个属性及一个sayName()方法。该方法通过this.name访问当前对象的name属性。早期开发者常采用此方式,但随后对象字面量语法因其简洁性成为更受青睐的选择,改写后的代码如下:


var person = { 
 name:"liubei", 
 age:29, 
 job:"shayemuyou", 
 sayName:function(){  
 alert(this.name); 
 }
}

此处的person对象在结构和功能上与前述示例完全一致,均具备相同的属性与方法定义。

尽管Object构造函数和字面量语法适用于单个对象的创建,但它们在批量生成相似对象时会导致大量重复代码。为克服这一缺陷,开发者引入了工厂模式的变体方案。

工厂模式:解决批量创建问题

一、工厂模式

作为软件工程中成熟的设计模式,工厂模式旨在抽象具体对象的创建过程。鉴于ECMAScript缺乏类定义机制,开发人员通过封装特定接口的函数来隐藏对象创建的细节,示例如下:


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("wei",25,"software");
var person2 = createPerson("bu",25,"software");

createPerson()函数接收参数并构建包含必要信息的Person对象。该函数可被多次调用,每次返回包含三个属性和一个方法的对象实例。虽然工厂模式解决了批量创建相似对象的问题,但未能解决对象识别问题,即无法准确判断返回对象的类型。

构造函数模式:实例与类型识别

二、构造函数模式

除了Array、Object等原生构造函数外,开发者还可以定义自定义构造函数来规范类型属性与方法。例如,使用构造函数重写前例:


function Person(name, age, job){ 
 this.name = name;
 this.age = age;
 this.job = job;
 this.sayName = function(){  
   alert(this.name); 
  }
}
var person1 = new Person("wei",25,"software");
var person2 = new Person("bu",25,"software");

对比Person()与createPerson()函数,主要区别体现在以下方面:

不再显式创建对象变量

直接将属性和方法赋值给this对象

省略了return语句

此外,函数名Person采用大写字母P开头,这符合构造函数命名惯例,旨在区别于普通函数。这一约定借鉴自其他面向对象语言,强调构造函数虽为函数,但具备创建对象的能力。

使用new操作符创建Person实例时,执行过程包含四个关键步骤:

1、在内存中创建一个新对象

2、将构造函数的作用域绑定至该新对象(此时this指向新对象)

3、执行构造函数内的代码逻辑

4、返回新创建的对象引用

在前例末尾,person1和person2分别存储了Person类的不同实例。这两个对象均拥有指向Person的constructor属性,验证方式如下:


console.log(person1.constructor == Person); //true
console.log(person2.constructor == Person); //true

虽然constructor属性用于标识对象类型,但检测对象类型更可靠的方式是使用instanceof操作符。本例中创建的对象既是Object的实例,也是Person的实例,可通过instanceof验证确认。


console.log(person1 instanceof Object);
//trueconsole.log(person1 instanceof Person);
//trueconsole.log(person2 instanceof Object);
//trueconsole.log(person2 instanceof Person);
//true

自定义构造函数使得实例能够被标识为特定类型,这是构造函数模式优于工厂模式的关键。person1和person2同为Object实例,是因为所有对象均继承自Object原型链。

构造函数的主要缺陷在于每个实例都会重新创建方法,导致内存浪费。在前例中,person1和person2虽拥有同名的sayName()方法,但它们并非同一Function实例。由于ECMAScript中函数也是对象,每次定义函数即实例化一个对象,从逻辑角度看构造函数定义如下:


function Person(name, age, job){ this.name = name;
 this.age = age;
 this.job = job;
 this.sayName = new Function("alert(this.name);")
 //与声明函数在逻辑上是等价的
 }

这种视角揭示了每个Person实例包含不同Function实例的本质。尽管作用域链和标识符解析机制不同,但创建Function新实例的机制保持一致。因此,不同实例上的同名函数并不相等,代码如下验证:

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

然而,为完成相同任务创建多个Function实例并无必要。既然已有this对象,无需在调用前将函数绑定至特定对象。因此,可将函数定义移至构造函数外部以解决此问题:


function Person(name, age, job){
 this.name = name;
 this.age = age;
 this.job = job;
 this.sayName = sayName;
}
function sayName(){
 alert(this.name);
}

此举解决了方法重复创建的问题,却引入了全局作用域污染。若对象需定义大量方法,则需定义大量全局函数,导致自定义引用类型丧失封装性。这些问题可通过原型模式加以解决。

原型模式:共享属性与方法

三、原型模式

每个函数均拥有一个prototype属性,该属性是指向对象的指针,用于存储特定类型所有实例共享的属性和方法。使用原型模式意味着所有实例共享其包含的属性和方法,无需在构造函数中定义实例信息,而是直接添加到原型对象中,示例如下:


function Person(){
}
Person.prototype.name = "wei";
Person.prototype.age = 27;
Person.prototype.job = "Software";
Person.prototype.sayName = function(){
 alert(this.name);
}
var person1 = new Person();
person1.sayName();  //"wei"
var person2 = new Person();
person2.sayName();  //"wei"
alert(person1.sayName == person2.sayName);

在此示例中,sayName()方法及所有属性直接添加至Person的prototype属性,构造函数变为空函数。即便如此,仍可通过构造函数创建新对象,且新对象具备相同属性和方法。与构造函数模式不同,这些属性和方法由所有实例共享,即person1和person2访问同一组属性和同一个sayName()函数。理解原型模式需先掌握ECMAScript中原型对象的性质。

原型对象的具体性质将在后续章节详述。原型模式的优势在于省略了向构造函数传递参数的环节,使所有实例默认具有相同属性值。这虽带来一定不便,但并非最大问题。当为通过原型模式创建的对象添加属性时,该属性会屏蔽原型中保存的同名属性,即阻止访问原型属性,但不改变原型属性本身。

原型模式的最大问题源于其共享本质。对于函数和基本值属性,共享机制运作良好,但对于引用类型属性,问题较为突出,示例如下:


function Person(){
}
Person.prototype = {
 constructor:Person,
 name:"wei",
 age:29,
 friends:["乾隆","康熙"],
 sayName:function(){
  alert(this.name);
 }
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push("嬴政");
console.log(person1.friends); //["乾隆","康熙","嬴政"]
console.log(person2.friends); //["乾隆","康熙","嬴政"]
console.log(person1.friends === person2.friends); //true

上述示例中,Person.prototype对象包含名为friends的字符串数组属性。创建两个Person实例后,修改person1.friends引用的数组(添加字符串),由于数组位于Person.prototype而非person1中,person2.friends同样被修改。鉴于每个对象通常需拥有独立属性,单独使用原型模式创建对象的情况较少见。

组合模式:当前最广泛使用的方案

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

创建自定义类型最常见的方式是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,原型模式用于定义方法和共享属性。结果,每个实例拥有独立的实例属性副本,同时共享方法引用,最大程度节省内存。此外,该混合模式支持向构造函数传递参数,集两种模式之长。重写前例代码如下:


function Person(name, age){
 this.name = name;
 this.age = age;
 this.friends = ["乾隆","康熙"];
}
Person.prototype = {
 constructor:Person,
 sayName:function(){
  alert(this.name);
 }
}
var person1 = new Person("wei",29);
var person2 = new Person("bu",25);
person1.friends.push("嬴政");
console.log(person1.friends); //["乾隆", "康熙", "嬴政"]
console.log(person2.friends); //["乾隆", "康熙"]
console.log(person1.friends === person2.friends); //false
console.log(person1.sayName === person2.sayName); //true

在此示例中,实例属性在构造函数中定义,而由所有实例共享的constructor属性和sayName()方法在原型中定义。因此,修改person1.friends不会影响person2.friends,因为二者引用不同的数组实例。

这种构造函数与原型模式的混合模式是目前ECMAScript中使用最广泛、认可度最高的创建自定义类型方法,可视为定义引用类型的默认形式。

动态原型模式:封装所有信息

五、动态原型模式

具备其他面向对象语言经验的开发者可能对独立的构造函数和原型感到困惑。动态原型模式旨在解决这一问题,它将所有信息封装在构造函数中,并通过在构造函数中初始化原型(仅在必要时)来保持构造函数和原型的优点。即通过检查某个应存在的方法是否有效,决定是否需要初始化原型。示例如下:


function Person(name, age){
 this.name = name;
 this.age = age;
 this.friends = ["乾隆","康熙"];
 //注意if语句
 if(typeof this.sayName!="function"){
  Person.prototype.sayName = function(){
   alert(this.name);
  }
 }
}
var person1 = new Person("wei",29);
person1.friends.push("嬴政");
person1.sayName();

注意构造函数代码中的if语句,仅在sayName()方法不存在时将其添加至原型。该代码仅在第一次调用构造函数时执行,此后原型已初始化,无需修改。需注意的是,此处所做的修改能立即在所有实例中反映。该方法通过检查任一初始化的方法或属性即可,无需逐一检查每个属性和方法。采用此模式创建的对象,仍可使用instanceof操作符确定其类型。

注意:使用动态原型模式时,严禁使用对象字面量重写原型。若在已创建实例的情况下重写原型,将切断现有实例与新原型之间的联系。

寄生构造函数模式:特殊场景应用

六、寄生构造函数模式

通常,当上述几种模式均不适用时,可使用寄生构造函数模式。该模式的基本思想是创建一个函数,仅封装创建对象的代码,然后返回新创建的对象。从表面看,该函数类似于典型构造函数。示例如下:


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("wei",29,"banzhuan");
person.sayName(); //"wei"

在此示例中,Person函数创建对象,初始化相应属性和方法,然后返回该对象。除将包装函数称为构造函数外,该模式与工厂模式无显著区别。构造函数默认返回新对象实例,若在构造函数末尾添加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");
console.log(colors.toPipedString()); //red|blue|green

在此示例中,创建名为SpecialArray的构造函数。函数内部首先创建数组,通过push()方法初始化数组值。随后为数组实例添加toPipedString()方法,用于返回以竖线分隔的数组值。最后将数组以函数形式返回。接着调用SpecialArray构造函数,传入初始值,并调用toPipedString()方法。

关于寄生构造函数模式,需声明一点:返回的对象与构造函数或其原型无直接关系;即构造函数返回的对象与在构造函数外部创建的对象无异。因此,不能依赖instanceof操作符确定对象类型。鉴于此问题,建议在可使用其他模式时避免使用此模式。

稳妥构造函数模式:安全环境适用

七、稳妥构造函数模式

道格拉斯·克拉克福德提出了JavaScript中的稳妥对象概念。所谓稳妥对象,指不含公共属性且其方法不引用this对象。稳妥对象适用于禁止使用this和new的安全环境,或防止数据被其他应用程序修改的场景。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;二是不使用new操作符调用构造函数。按稳妥构造函数要求,重写前例Person构造函数如下:


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

注意,在此模式创建的对象中,除使用sayName()方法外,无其他途径访问name的值。可使用稳妥的Person构造函数如下:


var person =Person("weiqi",22,"banzhuan");
person.sayName(); //weiqi

如此,变量person保存的是稳妥对象,除sayName()方法外,无其他途径访问其他数据成员。即使其他代码向该对象添加方法或数据成员,也无法访问传入构造函数中的原始数据。稳妥构造函数模式提供的安全性使其非常适合于某些安全执行环境,例如ADsafe环境。

注意:与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间无直接关系,因此instanceof操作符对此类对象无意义。

以上内容为本文全部结论,希望本文内容对您的学习或工作有所帮助,同时也感谢对本站的支持!

作者最新文章