先来看两种简单的对象创建方式:

1.Object构造函数方法

 var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer"; person.sayName = function(){
console.log(this.name);
}

2.对象字面量方法

 var person = {
name: "Nicholas",
age: 29,
job: "Software Engineer", sayName: function(){
console.log(this.name);
}
};

如果在html环境下,为了便于输出,可以将 console.log() 替换成 alert() ,以下代码同理。     

和传统的面向对象语言相比较,上述的对象创建方式存在明显的问题。第一个问题,对象的创建代码并不能实现很好的复用,第二个问题,使用同一个接口创建很多对象,会产生大量的重复代码。    

以下提出了几种创建对象的模式,针对每种模式,本文中会针对上述两个问题以及其他问题进行优缺点分析。

一、工厂模式

工厂模式用函数封装了以特定接口创建对象的细节

 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 Engineer");
var person2 = createPerson("Greg", 27, "Doctor"); person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"
17 console.log(person1.sayName == person2.sayName); //false

工厂模式虽然解决了创建多个相似对象的问题,但是没有解决对象识别的问题,即通过这种方式创建的对象通过instanceof确定的对象类型只能是Object。从17行可以看出,每创建一个对象, sayName() 方法都会被重复定义一次,造成了冗余。

二、构造函数模式

使用构造函数模式重写前面的例子,如下:

 function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
console.log(this.name);
};
} var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor"); person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg" console.log(person1 instanceof Object); //true
console.log(person1 instanceof Person); //true
console.log(person2 instanceof Object); //true
console.log(person2 instanceof Person); //true console.log(person1.sayName == person2.sayName); //false

从代码的16-19行可以看出,创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,而这正是构造函数模式胜过工厂模式的地方。但是和工厂模式一样, sayName() 方法被重复定义的问题并没有得到解决,为什么在传统的面向对象语言比如Java,C++中这么写不会产生这个问题呢?因为ECAMScript中的函数是对象,所以每定义一个函数,也就是实例化了一个对象,本质上就造成了对象的重复创建,所以说会造成冗余,而在Java,C++中,函数仅仅是函数而已。

好了,既然这样,那我们就尝试把函数定义转移到构造函数的外部:

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

看起来问题好像解决了,两个实例共享了全局的 sayName() ,但是这么写,却带来新的问题,如果某个类型需要相当多的自定义函数,而这些函数全都被定义为了全局函数,那么这个类型还有什么封装性可言。

三、原型模式

我们所创建的每个函数都有一个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 person1 = new Person();
person1.sayName(); //"Nicholas" var person2 = new Person();
person2.sayName(); //"Nicholas" console.log(person1.sayName == person2.sayName); //true
}

注意到17行, sayName() 只被定义了一次,被所有的实例共享了。

首先来解释一下什么是原型对象,函数的prototype属性指向的就是原型对象,在默认情况下,所有原型对象都会自动获得一个constructor属性,以上面的代码为例, Person.prototype.constructor == Person //true ,当调用构造函数创建了一个新的实例时,实例的内部将包含一个指针,写做 [[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 person1 = new Person();
var person2 = new Person(); person1.name = "Greg";
console.log(person1.name); //"Greg" --来自实例
console.log(person2.name); //"Nicholas" --来自原型

在这个例子中,person1的name被一个新值给屏蔽了,而原型值没有受到影响,因为person2可以正常返回name值。

读者应该注意到,在上例中,每添加一个属性和方法,都要敲一遍 Person.prototype ,为了简便,可以使用对象字面量的形式对上例进行重写:

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

这里使用的语法,本质上完全重写了默认的prototype对象,因此第17行才会得出false的结果,因此,需要在第4行下面加上一行代码: constructor: Person; ,用来弥补完全重写导致的损失。

还要注意的一点就是,原型的修改会动态反映到实例上,也就是说,即使已经声明了实例,在声明之后对原型进行修改,这些修改也会在实例上立刻得到反映。因为实例与原型之间的联系只不过是一个指针,而非一个副本。

话是这么说,这里也要注意另一个问题,即对象字面量形式的重写会彻底切断实例和原型之间的联系,如下:

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

说了这么多,我们对原型模式做一下总结,原型模式很好地解决了共享属性和函数的问题,使用对象字面量形式定义时代码简洁优雅,但原型模式存在的一个很大缺点就是:类型中存在引用类型的属性时,会造成一处修改,处处被改的问题,下面的代码说明了这个问题:

 function Person(){
} Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
friends : ["Shelby", "Court"],
sayName : function () {
console.log(this.name);
}
}; var person1 = new Person();
var person2 = new Person(); person1.friends.push("Van"); console.log(person1.friends); //"Shelby,Court,Van"
console.log(person2.friends); //"Shelby,Court,Van"
console.log(person1.friends === person2.friends); //true

这是由于person1和person2的friends指向了同一块内存区域。因为这个缺陷,原型模式很少被单独使用

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

在这种模式中,构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性。可以说这种方式结合得非常巧妙,对两种模式扬长避短,因此,这种模式也是目前在ECMAScripts中使用最广泛、认同度最高的一种创建自定义类型的方法

 function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
} Person.prototype = {
constructor: Person,
sayName : function () {
console.log(this.name);
}
}; var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor"); person1.friends.push("Van"); console.log(person1.friends); //"Shelby,Court,Van"
console.log(person2.friends); //"Shelby,Court"
console.log(person1.friends === person2.friends); //false
console.log(person1.sayName === person2.sayName); //true

五、动态原型模式

组合模式其实已经做得很好了,为什么还要引出一个动态原型模式,其实动态原型模式和组合模式在本质上是一样的,只不过在代码风格上做了一些调整,更符合面向对象所体现的封装特性。

 function Person(name, age, job){

     //properties
this.name = name;
this.age = age;
this.job = job; //methods
if (typeof this.sayName != "function"){ Person.prototype.sayName = function(){
console.log(this.name);
}; }
} var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

第9-15行代码只有在初次调用构造函数时才会执行,此后,原型已完成初始化。

六、寄生(parasitic)构造函数模式

按笔者理解,寄生构造函数模式很类似于装饰者模式的思想,在无法直接修改某个类型(或者仅仅是为了避免重写每个方法的麻烦),而又希望加强这个类型的某些功能时,就可以使用这种模式

 function SpecialArray(){       

     //create the array
var values = new Array(); //add the values
values.push.apply(values, arguments); //assign the method
values.toPipedString = function(){
return this.join("|");
}; //return it
return values;
} var colors = new SpecialArray("red", "blue", "green");
console.log(colors.toPipedString()); //"red|blue|green" console.log(colors instanceof SpecialArray);

这个例子中,创建的实例基于Array的基础上,添加了 toPipedString() 方法。

这种模式返回的对象与构造函数或者构造函数的原型属性之间没有关系(辨别的方法是看构造函数有没有进行return),不能依赖 instanceof 来确定对象类型,由于这种缺陷,不到万不得已,也不建议使用这种模式

综上,在JavaScript中创建对象,默认情况下我们可以使用组合模式,在组合模式中,每个实例都会有自己的一份实例属性的副本,但同时有共享着方法的引用,最大程度节省了内存。在JavaScript这种弱类型的语言中,很好地还原了传统OO语言的类的概念,可以说是一种完美的妥协。

JavaScript基础:创建对象的更多相关文章

  1. JavaScript基础

    JavaScript基础 JavaScript是一门编程语言,浏览器内置了JavaScript语言的解释器,所以在浏览器上按照JavaScript语言的规则编写相应代码之,浏览器可以解释并做出相应的处 ...

  2. 一步步学习javascript基础篇(0):开篇索引

    索引: 一步步学习javascript基础篇(1):基本概念 一步步学习javascript基础篇(2):作用域和作用域链 一步步学习javascript基础篇(3):Object.Function等 ...

  3. 前端之JavaScript基础

    前端之JavaScript基础 本节内容 JS概述 JS基础语法 JS循环控制 ECMA对象 BOM对象 DOM对象 1. JS概述 1.1. javascript历史 1992年Nombas开发出C ...

  4. 一步步学习javascript基础篇(3):Object、Function等引用类型

    我们在<一步步学习javascript基础篇(1):基本概念>中简单的介绍了五种基本数据类型Undefined.Null.Boolean.Number和String.今天我们主要介绍下复杂 ...

  5. Javascript基础回顾 之(三) 面向对象

    本来是要继续由浅入深表达式系列最后一篇的,但是最近团队突然就忙起来了,从来没有过的忙!不过喜欢表达式的朋友请放心,已经在写了:) 在工作当中发现大家对Javascript的一些基本原理普遍存在这里或者 ...

  6. JavaScript 基础回顾——对象

    JavaScript是基于对象的解释性语言,全部数据都是对象.在 JavaScript 中并没有 class 的概念,但是可以通过对象和类的模拟来实现面向对象编程. 1.对象 在JavaScript中 ...

  7. Javascript基础知识总结一

    Javascript基础知识总结一 <!DOCTYPE html> <html> <head lang="en"> <meta chars ...

  8. javascript基础部分

    javascript基础部分 1  数据类型: 基础数据类型(通过typeof来检测):Number,string,undefined,null,boolean,function typeof只能检测 ...

  9. 【javascript基础】系列

    这是本人记录的javascript基础知识,希望能给大家的学习带来一点帮助. [javascript基础]1.基本概念 [javascript基础]2.函数 [javascript基础]3.变量和作用 ...

随机推荐

  1. 安卓图表引擎AChartEngine(六) - 框架源码结构图

    包结构: org.achartengine: org.achartengine.model: org.achartengine.renderer: org.achartengine.tools: 安卓 ...

  2. Cow Hopscotch

    Cow Hopscotch 题目描述 Just like humans enjoy playing the game of Hopscotch, Farmer John's cows have inv ...

  3. CSS——宽高问题大汇总

    1.宽高继承 他们是要属性的,并不是直接就能继承,inherit. 2.浮动的盒子不要给宽,宽度由内容来决定

  4. Tesseract-OCR使用记录

    Tesseract是一个开源的OCR(Optical Character Recognition,光学字符识别)引擎,可以识别多种格式的图像文件并将其转换成文本,目前已支持60多种语言(包括中文).  ...

  5. JDBC 数据库连接池

    http://www.cnblogs.com/lihuiyy/archive/2012/02/14/2351768.html JDBC 数据库连接池 小结   当对数据库的访问不是很频繁时,可以在每次 ...

  6. highcharts第一篇---简介和使用

    Highcharts 是一个用纯JavaScript编写的一个图表库, 能够很简单便捷的在web网站或是web应用程序添加有交互性的图表,并且免费提供给个人学习.个人网站和非商业用途使用.HighCh ...

  7. GCJ Round 1C 2009 Problem C. Bribe the Prisoners

    区间DP.dp[i][j]表示第i到第j个全部释放最小费用. #include<cstdio> #include<cstring> #include<cmath> ...

  8. STM32F103外部中断编程

    STM32F103外部中断编程   中断,顾名思义就是停下手头的活,去干另外一件急活,干完急活然后回来继续干手头的活. 单片机和人一样,有时候也有更急的程序需要执行,执行完之后再回来执行之前正在执行的 ...

  9. iOS通过代码关闭程序

    //-------------------------------- 退出程序 -----------------------------------------// - (void)exitAppl ...

  10. c++中vector使用

    不多说,先看代码: #include <IOSTREAM> #include <VECTOR> using namespace std; int main() { cout&l ...