如何创建对象
什么是对象?
ECMA-262中把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数。
每个对象都是基于一个引用类型创建的。
对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。
无非就是一组名:值对,其中值可以是数据或函数。
方法
通过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("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"
根据接受的参数来构建一个包含必要信息的Person
对象,可以无数次调用这个函数,每次都会返回一个包含三个属性一个方法的对象。
工厂模式的优缺点?
优点:解决了创建多个相似对象的问题。
缺点:没有解决对象识别的问题,即怎样知道一个对象的类型。
二、构造函数模式
使用构造函数模式将前面的例子重写:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
alert(person1.sayName == person2.sayName); //false
代码分析:
新建出来的person1
和person2
对象都有一个constructor
属性,该属性都指向Person
.constructor
属性是用来标识对象类型的。
这就意味着用自定义的构造函数创建的实例都会标识为一种特性的类型。这是胜过工厂模式的地方。
调用构造函数经历的4个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此
this
就指向了这个新对象) - 执行构造函数中的代码
- 返回新对象
与工厂模式的区别:
- 没有显式创建对象
- 直接将属性和方法赋给了this对象
- 没有return语句
按照惯例:构造函数始终都应该以一个大写字母开头。
为什么惯例要求构造函数以一个大写字母开头?
因为构造函数本身也是函数,这样是为了区别于ECMAScript中的其他函数。
上述代码缺点或者说是问题?
Person实例里都有一个Function的实例。不同实例上的同名函数是不相等的。这样就会导致创建几个实例就会多创建几个同样任务的Function实例。
如何解决?
将函数移到构造函数外部。
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.sayName(); //"Nicholas"
person2.sayName(); //"Greg"
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
alert(person1.sayName == person2.sayName); //true
缺点就是,定义在全局作用域的函数,成了专门为这个构造函数打造的,只能它实例化的对象可以使用,这让全局作用域名不副实 。
三、原型模式
我们创建的每一个函数都有一个
prototype
属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含所有实例共享的属性和方法。
这个原型对象会有一个constructor
属性,这个属性是指向prototype属性所在函数的指针。
意思就是:prototype
就是通过调用构造函数而创建的那个对象实例的原型对象。
好处:可以让所有实例共享它所包含的属性和方法。就是不用再在构造函数中定义对象实例的信息,可以将这些信息直接添加到原型对象中。
代码示例1:
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
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
//only works if Object.getPrototypeOf() is available
if (Object.getPrototypeOf){
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas"
}
分析:
从图片可以看出,实例对象与构造函数没有直接关系。
可以通过isPrototypeOf()
方法来确定对象直接是否存在这种关系,例:Person.prototype.isPrototypeOf(person1)
。
ECMAScript5增加了一个方法Object.getPrototypeOf()
,如:Object.getPrototypeOf(person1) == Person.prototype
代码示例2:
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();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
person1.name = "Greg";
alert(person1.name); //"Greg" – from instance,来自实例
alert(person1.hasOwnProperty("name")); //true
alert("name" in person1); //true
alert(person2.name); //"Nicholas" – from prototype,来自原型
alert(person2.hasOwnProperty("name")); //false
alert("name" in person2); //true
delete person1.name;
alert(person1.name); //"Nicholas" - from the prototype,来自原型
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
分析:
多个实例共享原型所保存的属性和方法的基本原理:
每当代码读取一个属性的时候,都会先从对象实例本身开始,如果没有找到,则会继续搜索指针指向的原型对象,在原型对象中查找。
如果在对象实例中找到了该属性,就不会再向上查找,原型中的属性就相当于是被屏蔽了,只会阻止我们访问原型中的属性,但并不会修改那个属性。
delete
可以删除实例属性,删除之后,就可以继续访问原型中的属性了。
hasOwnProperty()
只有在给定的属性存在于实例对象时才会返回true
。
in
会在通过对象能够访问给定属性时返回true
,无论这个属性在实例中还是原型中。
实例对象的constructor
属性不可枚举。
for-in
返回的是所有能够通过对象访问的、可枚举的属性,无论这个属性在实例中还是原型中。
Object.keys()
返回的是对象上所有可枚举的实例属性。
Object.getOwnPropertyNames()
可以返回对象的所有的实例属性,包括不可枚举的。
通过字面量简写原型语法
function Person(){
}
Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
//重设构造函数,只适用于ECMAScript兼容的浏览器
Object.defineProperty(Person.prototype,"constructor",{
enumerable:false,
value:Person
})
为什么给Person.prototype
添加constructor
属性?
如果不添加的话,Person.prototype
的constructor
指向的就变成了Object
,不再指向Person
函数。
为什么不直接给Person.prototype
添加constructor
属性?
因为直接添加的话,constructor
的枚举特性enumerable
就变成了true
。
原型的动态性
通过两段代码来理解一下
function Person(){
}
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
var friend = new Person();
Person.prototype.sayHi = function(){
alert("hi");
};
friend.sayHi(); //"hi" – works!
即使friend
实例是在添加方法之前创建的,但它还是可以访问这个新方法,为什么?
因为实例与原型之间的连接是一个指针,而不是一个副本。
function Person(){
}
var friend = new Person();
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
friend.sayName(); //error
为什么会访问不了?
因为重写了原型对象,相当于切断了现有原型与任何之前已经存在的对象实例之间的联系,它们引用的仍然是最初的原型。
原型对象的问题
前面说在实例中添加一个同名属性的话,会屏蔽原型中对应的属性,但不会改变原型中的属性。但是,如果是一个引用类型的属性的话,就会被改变。
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
四、组合使用构造函数模式和原型模式
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,Court,Van"
alert(person2.friends); //"Shelby,Court"
alert(person1.friends === person2.friends); //false
alert(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(){
alert(this.name);
};
}
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();
分析:
- 只在
sayName
不存在的情况下才会添加到原型中,这段代码只会在初次调用构造函数的时候才会执行。 - 这里对原型所做的修改,也能够立即在所有实例中得到反映。
if
语句检查的可以是初始化之后应该存在的任何属性和方法,检查其中一个就行,不用都检查。- 可以使用
instanceof
确定它的类型。 - 不要使用字面量重写原型,否则会切断现有实例与新原型之间的联系。
为什么整合到构造函数中?
有其他OO语言经验的开发人员在看到独立的构造函数和原型时,很可能会感到困惑。
六、寄生构造函数模式
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 friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
除了使用new
操作符、使用的包装函数叫做构造函数外,这个模式和工厂模式一模一样。
返回的对象与构造函数或者构造函数的原型没有任何关系,不能依赖instanceof
操作符来确定对象的类型。
在其他模式可以使用的情况下不推荐使用该模式。
七、稳妥构造函数模式
所谓稳妥对象,指的是没有公共属性,而且其方法也不引用
this
的对象。
function Person(name,age,job){
//创建要返回的对象
var o = new Object();
//可以再这里定义私有变量和函数
//添加方法
o.sayName = function(){
alert(name)
}
//返回对象
return o;
}
分析:
除了使用sayName()
方法外,没有别的方法可以访问其数据成员。即使有其他代码会给这个对象添加方法或者数据成员,但也不可能有别的方法访问传入到构造函数中的原始数据。