1. 对象字面量
通过这种方式创建对象极为简单,将属性名用引号括起来,再将属性名和属性值之间以冒号分隔,各属性名值对之后用逗号隔开,最后一个属性不用逗号隔开,所有的属性名值对用大括号括起来,像这样:
var o={"name":"yangyule","age":23,"sex":"male"};
属性名的引号可以是单引号也可以是双引号,大多数的时候属性名可以不加引号,但如果属性名为JavaScript关键字(保留字)的时候则必须加引号。大多数情况下最后一个属性之后加上逗号也没有问题,但是不建议这样做,因为这不符合ECMAScript规范,部分浏览器(IE7及之前的版本,Opera)将报错,可能导致代码无法运行。将上述代码加上一个函数作为属性如下:
var o = {"name":"yangyule", "age":23, "sex":"male", "makeSelfIntroduction":function(){ console.log("My name is"+this.name); console.log("I am "+this.age+" years old"); } };
因为可读性更好,我们其实有些时候更愿意将对象字面量写作上述样式,在应用一些库的时候经常会在初始化的时候传入大量的参数,使用的就是这种方式。我们将代码完善一下:
var o={"name":"yangyule", "age":23, "sex":"male", "makeSelfIntroduction":function(){ console.log("My name is"+this.name); //My name isyangyule console.log("I am "+this.age+" years old"); //I am 23 years old } }; o.makeSelfIntroduction(); //此处输出 for(key in o){ console.log(key+" :"+o[key]); /* name :yangyule age :23 sex :male makeSelfIntroduction :function (){ console.log("My name is"+this.name); console.log("I am "+this.age+" years old"); } */ }
我省略了HTML部分的代码,运行结果以注释给出。以上代码的for部分输出了对象o的所有属性及属性值,在Java中读取未知对象的属性名的技术叫做反射,而在JavaScript中for就可以搞定。
这种方式创建对象简单明了,可读性好,但是缺点就是代码复用问题,当需要大量同类对象的时候重复代码将占据很大的一部分代码量,不易维护且效率极低。
2. Object构造函数
这种方式创建对象和C++、Java稍微有点儿类似,通过在new操作符之后加上Object构造函数创建,属性可以通过点运算符来添加,示例如下:
var o=new Object(); o.name="yangyule"; o.age=23; o.sex="male";
如果对象的属性之前就存在,则将之前的的属性值替换当前赋值的属性值,例如在上述代码之后再次添加属性o.age=24,则此时o的age属性值为24。
这种创建对象的方式比较简便,也容易理解,但是缺点同样明显,如果需要大量的同类对象那意味着大量的重复代码,效率极低。
3. 工厂模式
工厂模式是软件领域一种广为人知的设计模式,JavaScript对象的创建也可以借用这种模式,如下:
function createPerson(name,age,sex,job){ var o=new Object(); o.name=name; o.age=age; o.sex=sex; o.job=job; o.sayName=function(){ console.log(this.name); } return o; } var person1=createPerson("yangyule",23,"male","student"); for(key in person1){ console.log(key+": "+person1[key]); }
工厂模式解决了创建多个对象代码重复的问题,但是缺点也颇为明显,就以上代码来说,每次调用createPerson函数,均会为每个对象创建一次sayName函数,换句话说,这个函数在每个Person中都有一份,而不是所有Person对象共享,在JavaScript中函数都是对象,这意味着每个Person对象都会占用一份函数的内存,造成了内存的浪费,这是其一,其二是对象识别的问题,即使用instanceof运算符无法得到对象的类型
4. 构造函数模式
前面我们介绍了通过Object构造函数创建对象的方式,其实除了使用Object构造函数之外还可以使用自定义构造函数创建对象,使用自定义构造函数的方法重写上一个例子:
function Person(name,age,sex,job){ this.name=name; this.age=age; this.sex=sex; this.job=job; this.sayName=function(){ console.log(this.name); } } var person1=new Person("yangyule",23,"male","student"); for(key in person1){ console.log(key+" :"+person1[key]); }
以上代码中函数名Person首字母大写,这是一种约定,作为构造函数的函数首字母需要大写,普通函数首字母小写,遵循驼峰命名法。创建Person的实例的时候使用了new操作符,这种调用方式实际上经历了以下4个步骤:
1. 创建一个新对象
2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
3. 执行构造函数中的代码(为这个新对象添加属性)
4. 返回新对象
以这种方式调用函数时还有一点需要说明,就是return语句。在这种情况下,如果函数中没有return语句,函数返回新创建的对象;如果有return语句,而且是return一个对象,则返回这个对象,如果return其它类型的值,则返回this。
function Person(name,age,gender){ this.name=name; this.age=age; this.gender=gender; return {}; } var o=new Person("yangyule",23,"male"); console.log(o); //Object {}
这种方式创建的对象就具有了一定的标识,通过使用new操作符创建出来的对象可以通过instanceof操作符检查类型,例如以上代码中可以使用:
console.log(person1 instanceof Person) //true console.log(person1 instanceof Object); //true
这就是自定义构造函数方式优于工厂模式的地方,但是这种方式依然存在对象方法重复占用内存的问题,虽然可以将上述代码改为以下形式解决此问题:
function Person(name,age,sex,job){ this.name=name; this.age=age; this.sex=sex; this.job=job; this.sayName=sayName; } function sayName(){ console.log(this.name) } var person1=new Person("yangyule",23,"male","student");
但是又引发了另一个问题,构造函数没有了封装性,上述代码中仅仅有一个方法,如果方法有10个呢,100个呢,那么此时就完全没有必要使用构造函数的方式创建对象。
5. 原型模式
在JavaScript中,每个函数都是对象,都是通过Function()创建的,每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,这个对象就是函数的原型对象,这个原型对象默认有一个属性constructor,这个属性指向函数本身。而每个对象都有个内部属性,ECMA-262第五版管这个属性叫[[Prototype]],在脚本中并没有标准的访问这个属性的方式,但在Firefox,Safari,Chrome中可以通过__proto__(左右均为两个下划线)访问这个属性,这个属性指向的对象叫做这个对象的原型或者是原型对象,对象的原型指向创建这个对象的函数的原型对象。看如下代码:
function Person(name,age,sex,job){ this.name=name; this.age=age; this.sex=sex; this.job=job; this.sayName=function(){ console.log(this.name); } } var person1=new Person("yangyule",23,"male","student");
以上代码中存在着这样的关系:
Person.prototype == person1.__proto__
在浏览器中验证:
console.log(Person.prototype == person1.__proto__); //true
具体的关于对象,原型对象,函数之间的关系请看我另一篇博客《JavaScript高级特性-对象、原型、函数之间的关系》 ,本文假设读者理解原型对象。
以上介绍的通过工厂模式和自定义构造函数的模式创建对象的方法均有一个问题,就是批量创建的同类对象,每个函数均会占用一份内存空间,造成内存的浪费。在java虚拟机中,对象的非静态内部属性都是在堆中,而对象的方法都在方法区,由同一个类创建的对象共享方法区的方法,所有同类对象均使用一份函数,极大的节省了内存空间。 在JavaScript中对象之间共享变量和方法就可以通过原型对象来实现,原因就是通过同一个构造函数创建的对象的原型对象就是构造函数的原型对象,换句话说就是所有通过同一个构造函数创建的对象的原型对象都是同一个对象,而这个对象就是构造函数的原型对象,通过给构造函数的原型对象添加属性从而实现属性和方法的共享。代码如下:
function Person(){ } Person.prototype.name="yangyule" Person.prototype.age=23; Person.prototype.sex="male" Person.prototype.sayName=function(){ console.log(this.name); } var person1=new Person(); var person2=new Person(); person1.sayName(); //yangyule person2.sayName(); //yangyule
以上代码中person1的sayName输出yangyule,而person2的sayName也输出yangyule,这样就实现了对象之间属性的共享,在上述代码的底部添加如下代码:
person2.name="vile"; person1.sayName(); //yangyule person2.sayName(); //vile
输出结果以注释给出。出现上述结果的原因是,对象在访问自己的方法和属性时,优先搜索自己的属性和方法,自己的属性和方法中没有找到再去搜索原型的属性和方法,如果原型中也没有找到,就去原型的原型中搜索,一直沿着原型这条链搜索,直到找到匹配的为止,如果一直没有找到,如果要找的属性是变量就报undefined,方法就报TypeError。以上代码的第一句其实是给person2对象添加了一个自己的属性name,所以会输出vile。
还可以通过直接给函数的prototype属性赋值为一个自定义对象实现共享:
function Person(){ } Person.prototype={ name:"yangyule", age:23, sex:"male", sayName:function(){ console.log(this.name); } }; var person1=new Person(); var person2=new Person();
此时的Person.prototype即为自定义的对象,但是此时Person原型对象中的constructor属性为Object()方法,而并非Person()方法,所以如果想让constructor属性指向Person(),我们可以这样做:
function Person(){ } Person.prototype={ constructor:Person, name:"yangyule", age:23, sex:"male", sayName:function(){ console.log(this.name); } };
这样就解决了问题。
以上代码虽然解决了对象间属性共享的问题,但是却只有共享属性没有私有属性,所以我们大多数情况下使用下一种方式创建对象——组合使用构造函数和原型模式。
6. 组合使用构造函数和原型模式
这种方式是创建对象的最常用方式,对象私有的属性和方法用构造函数实现,需要共享的属性和方法用原型来实现,结合了构造函数方式和原型模式的优点,摒弃了这两种方式的缺点,示例代码如下:
function Person(name,age,sex){ this.name=name; this.age=age; this.sex=sex; } Person.prototype.sayName=function(){ console.log(this.name); } var person1=new Person("yangyule",23,"male"); var person2=new Person("vile",23,"male"); person1.sayName(); //yangyule person2.sayName(); //vile
或者改为如下方式:
function Person(name,age,sex){ this.name=name; this.age=age; this.sex=sex; } Person.prototype={ constructor:Person, sayName:function(){ console.log(this.name); } }; var person1=new Person("yangyule",23,"male"); var person2=new Person("vile",23,"male"); person1.sayName(); //yangyule person2.sayName(); //vile
7. 动态原型模式
在使用上面的组合构造原型方式创建对象时,对象的私有属性和共享属性并不在一个代码块中,换句话说,私有属性和共享属性并不能在构造函数中完成,必须把共享属性放到函数的外边,这对于其它OO语言的编程人员来说会感到很困惑,而动态原型模式可以很好的解决上述问题。
function Person(name,age,sex){ this.name=name; this.age=age; this.sex=sex; if(typeof this.sayName!= "function"){ Person.prototype.sayName=function(){ console.log(this.name); } } } var person1=new Person("yangyule",23,"male"); var person2=new Person("vile",23,"male"); person1.sayName(); person2.sayName();
在构造函数中写if语句是为了判断是否是第一次执行Person()函数,如果是第一次执行Person()函数,if语句中typeof this.sayName的值为undefined,所以给Person()的原型对象添加函数sayName(),如果不是第一次执行,那表明函数Person()的原型对象已经有了sayName()函数,此时if语句中的typeof this.sayName值为function,所以不再执行语句块中的内容。这样就将共享的函数封装在了构造函数中。上述代码中即使有更多的函数也不需要为每个函数名执行一次判断,可以将所有的函数写在一个if语句块中,判断一次就可以。
8. 寄生构造函数模式
寄生构造函数模式和工厂模式的代码和思想是一模一样的,至于为什么叫工厂模式和寄生构造函数模式,我也不太清楚,因为我也没用过这种模式。它的大致思想就是在已有的对象上添加新的属性和方法但是不能修改原来的对象,例如,给数组添加一个新的方法,但又不能使所有的数组都拥有此方法,就可以使用这种模式:
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","yellow"); console.log(colors.toPipedString()); //red|blue|yellow
以上的代码为数组对象添加了一个新的方法toPipedString,但是这个方法并不是所有数组都具有的。
9. 稳妥构造函数模式
所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。稳妥对象最适合在一些安全环境中(这些环境中会禁止使用 this和new)或者防止数据被其它应用程序修改时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同,一是新创建的对象的实例方法不引用this,二是不使用new操作符调用构造函数。示例如下:
function Person(name,age,job){ var o =new Object(); o.sayName=function(){ console.log(name); }; return o; } var me = Person("yangyule",23,"student"); me.sayName(); //yangyule
这种方式创建的对象很有特点,其实属性都不是这个对象的,而是该对象的上级作用域中的,也就是构造函数的(姑且这样叫吧,我也不知道该叫什么),很像闭包。这样创建对象在外部是没有办法直接访问它的属性的。
总结
这几种创建对象的方式中,最常用的也是最经典的莫过于组合构造原型和动态原型模式,我第一次阿里的面试就是栽在这个上面的/(ㄒoㄒ)/~~