创建对象的方法

 Object 构造函数

 var obj=new Object();obj.name='objName';obj.property=function(){alert(this.name)};

对象字面量

var person={name:'mark',age:20};

缺点:使用同一个接口创建很多对象,会产生大量的重复代码。

工厂模式

工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程

function createPerson(name, age, job){
var o = new Object();
o.name = nafunme;
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");

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

构造函数

ECMAScript 中的构造函数可用来创建特定类型的对象。像 Object 和 Array 这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法:
function Person(name,age,sex){
 this.name=name;
this.age=age;
this.sex=sex;
this.sayName=function(){alert(this.name)}
};
var mark=new Person('mark',20,'male');
var lily=new Person('lily',20,'female');
console.log(lily instanceof Person);
console.log(mark.constructor===Person);
console.log(lily.constructor===Person);
console.log(lily instanceof Object);  
 person1 和 person2 分别保存着 Person 的一个不同的实例。这两个对象都有一个 constructor (构造函数)属性,该属性指向 Person
要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4个步骤:
(1) 创建一个新对象;
(2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
(3) 执行构造函数中的代码(为这个新对象添加属性);
(4) 返回新对象。
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式
胜过工厂模式的地方。在这个例子中, person1 和 person2 之所以同时是 Object 的实例,是因为所
有对象均继承自 Object。
以这种方式定义的构造函数是定义在 Global 对象(在浏览器中是 window 对象) 中的。
缺点:使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍
优化构造函数:
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");
在全局作用域中定义的函数实际上只
能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方
法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在,
这些问题可以通过使用原型模式来解决

原型模式

我们创建的每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么 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
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype
属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor
(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。就拿前面的例子来说,
Person.prototype. constructor 指向 Person 。而通过这个构造函数,我们还可继续为原型对象
添加其他属性和方法。
创建了自定义的构造函数之后,其原型对象默认只会取得 constructor 属性;至于其他方法,则
都是从 Object 继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部
属性),指向构造函数的原型对象。ECMA-262 第 5 版中管这个指针叫 [[Prototype]] 。虽然在脚本中
没有标准的方式访问 [[Prototype]] ,但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性
__proto__ ;而在其他实现中,这个属性对脚本则是完全不可见的。不过,要明确的真正重要的一点就
是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
function Person(){}
Person.prototype.name='mark';
Person.prototype.age=20;
Person.prototype.sayName=function(){alert(this.name)};
console.log(Person.prototype.constructor);//Person
var gao=new Person();
Person.prototype.isPrototypeOf(gao)//true
typeof Person//function
typeof Person.prototype//object
gao. __proto__=== Person. prototype
gao.__proto__.constructor===Person
在此, Person.prototype 指向了原型对象,而 Person.prototype.constructor 又指回了 Person 。原型对象中除了包含 constructor 属性之外,还包括后来添加的其他属性。 Person 的每个实例——person1 和 person2 都包含一个内部属性,该属性仅仅指向了 Person.prototype ;换句话说,它们与构造函数没有直接的关系。此外,要格外注意的是,虽然这两个实例都不包含属性和方法,但我们却可以调用 person1.sayName() 。这是通过查找对象属性的过程来实现的。
Object.getPrototypeOf(gao) == Person.prototype//true
Object.getPrototypeOf() 返回的对象实际就是这个对象的原型
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先
从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,
则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这
个属性,则返回该属性的值。也就是说,在我们调用 person1.sayName() 的时候,会先后执行两次搜
索。首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。”然后,它继续搜索,再
问:“ person1 的原型有 sayName 属性吗?”答:“有。”于是,它就读取那个保存在原型对象中的函
数。当我们调用 person2.sayName() 时,将会重现相同的搜索过程,得到相同的结果。而这正是多个
对象实例共享原型所保存的属性和方法的基本原理
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们
在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该
属性将会屏蔽原型中的那个属性
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();
person1.name = "Greg";
alert(person1.name); //"Greg" —— 来自实例
alert(person2.name); //"Nicholas" ——来自原型
在这个例子中, person1 的 name 被一个新值给屏蔽了。但无论访问 person1.name 还是访问person2.name 都能够正常地返回值,即分别是 "Greg" (来自对象实例)和 "Nicholas" (来自原型)。当在 alert() 中访问 person1.name 时,需要读取它的值,因此就会在这个实例上搜索一个名为 name
的属性。这个属性确实存在,于是就返回它的值而不必再搜索原型了。当以同样的方式访问 person2.name 时,并没有在实例上发现该属性,因此就会继续搜索原型,结果在那里找到了 name 属性
为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这 个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为 null ,也 只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过,使用 delete 操作符则可以完全删 除实例属性,从而让我们能够重新访问原型中的属性,如下所示
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();
person1.name = "Greg";
alert(person1.name); //"Greg"——来自实例
alert(person2.name); //"Nicholas"——来自原型
delete person1.name;
alert(person1.name); //"Nicholas" ——恢复原型中name属性
使用 hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘了它是从 Object 继承来的)只在给定属性存在于对象实例中时,才会返回 true 有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用。在单独使用时, in 操作符会在通 过对象能够访问给定属性时返回 true ,无论该属性存在于实例中还是原型中
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); 
在以上代码执行的整个过程中, name 属性要么是直接在对象上访问到的,要么是通过原型访问到
的。因此,调用 "name" in person1 始终都返回 true ,无论该属性存在于实例中还是存在于原型中。
同时使用 hasOwnProperty() 方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于
原型中,如下所示。
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
由于 in 操作符只要通过对象能够访问到属性就返回 true , hasOwnProperty() 只在属性存在于
实例中时才返回 true ,因此只要 in 操作符返回 true 而 hasOwnProperty() 返回 false ,就可以确
定属性是原型中的属性。下面来看一看上面定义的函数 hasPrototypeProperty() 的用法。
function Person(){}
Person.prototype.name='mark';
Person.prototype.age=20;
Person.prototype.sayName=function(){alert(this.name)};
//console.log(Person.prototype.constructor);
var gao=new Person();
gao.name='gao';
gao.age='20';
console.log(gao.__proto__===Person.prototype);
var keys=Object.keys(Person.prototype);
console.log(keys);
Object.defineProperty(gao,'name',{
    enumerable:false
});
var key2=Object.keys(gao);
console.log(key2);//age
var properties=Object.getOwnPropertyNames(gao);//enumerable:false的属性也能枚举
console.log(properties);//name,age
var properties1=Object.getOwnPropertyNames(Person.prototype);//constructor", "name", "age", "sayName
console.log(properties1);

3. 更简单的原型语法

读者大概注意到了,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype 。为减少
不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的
对象字面量来重写整个原型对象,如下面的例子所示。
function Person(){
}
Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
}; 
在上面的代码中,我们将 Person.prototype 设置为等于一个以对象字面量形式创建的新对象。
最终结果相同,但有一个例外: constructor 属性不再指向 Person 了。前面曾经介绍过,每创建一
个函数,就会同时创建它的 prototype 对象,这个对象也会自动获得 constructor 属性。而我们在
这里使用的语法,本质上完全重写了默认的 prototype 对象,因此 constructor 属性也就变成了新
对象的 constructor 属性(指向 Object 构造函数),不再指向 Person 函数。此时,尽管 instanceof
操作符还能返回正确的结果,但通过 constructor 已经无法确定对象的类型了,如下所示。
var friend = new Person();
alert(friend instanceof Object); //true
alert(friend instanceof Person); //true
alert(friend.constructor == Person); //false
alert(friend.constructor == Object); //true
在此,用 instanceof 操作符测试 Object 和 Person 仍然返回 true ,但 constructor 属性则等于 Object 而不等于 Person 了。如果 constructor 的值真的很重要,可以像下面这样特意将它设置回适当的值。
function Person(){
}
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
以上代码特意包含了一个 constructor 属性,并将它的值设置为 Person ,从而确保了通过该属
性能够访问到适当的值。
注意,以这种方式重设 constructor 属性会导致它的 [[Enumerable]] 特性被设置为 true。默认
情况下,原生的 constructor 属性是不可枚举的,因此如果你使用兼容 ECMAScript 5 的 JavaScript 引
擎,可以试一试 Object.defineProperty() 。
function Person(){
}
Person.prototype = {
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
图灵社区会员 StinkBC(StinkBC@gmail.com) 专享 尊重版权
156 第 6章 面向对象的程序设计
// 重设构造函数,只适用于 ECMAScript 5  兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});

4. 原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上
反映出来——即使是先创建了实例后修改原型也照样如此。请看下面的例子。
var friend = new Person();
Person.prototype.sayHi = function(){
alert("hi");
};
friend.sayHi(); //"hi"(没有问题!)以上代码先创建了 Person 的一个实例,并将其保存在 person 中。然后,下一条语句在 Person.prototype 中添加了一个方法 sayHi() 。即使 person 实例是在添加新方法之前创建的,但它仍然可以访问这个新方法其原因可以归结为实例与原型之间的松散连接关系。当我们调用 person.sayHi()时,首先会在实例中搜索名为 sayHi 的属性,在找到的情况下,会继续搜索原型。因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到sayHi 属性并返回保存在那里的函数。尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototype]] 指针,把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。看下面的例子
function Person(){
}
var friend = new Person();
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
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

friend.sayName(); //error
5. 原生对象的原型

原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式
创建的。所有原生引用类型( Object 、 Array 、 String ,等等)都在其构造函数的原型上定义了方法。
例如,在 Array.prototype 中可以找到 sort() 方法,而在 String.prototype 中可以找到
substring() 方法,如下所示。
alert(typeof Array.prototype.sort); //"function"
alert(typeof String.prototype.substring); //"function"
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自
定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。下面的代码就给基本包装类型
String 添加了一个名为 startsWith() 的方法。
String.prototype.startsWith = function (text) {
return this.indexOf(text) == 0;
};
var msg = "Hello world!";
alert(msg.startsWith("Hello")); //true
这里新定义的 startsWith() 方法会在传入的文本位于一个字符串开始时返回 true 。既然方法被
添加给了 String.prototype ,那么当前环境中的所有字符串就都可以调用它。由于 msg 是字符串,
而且后台会调用 String 基本包装函数创建这个字符串,因此通过 msg 就可以调用 startsWith() 方法尽管可以这样做,但我们不推荐在产品化的程序中修改原生对象的原型。如果因
某个实现中缺少某个方法,就在原生对象的原型中添加这个方法,那么当在另一个支
持该方法的实现中运行代码时,就可能会导致命名冲突。而且,这样做也可能会意外
地重写原生方法。

6. 原型对象的问题

原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在
默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。
原型模式的最大问题是由其共享的本性所导致的。
原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒
也说得过去,毕竟(如前面的例子所示),通过在实例上添加一个同名属性,可以隐藏原型中的对应属
性。然而,对于包含引用类型值的属性来说,问题就比较突出了。来看下面的例子。
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
在此, Person.prototype 对象有一个名为 friends 的属性,该属性包含一个字符串数组。然后,
创建了 Person 的两个实例。接着,修改了 person1.friends 引用的数组,向数组中添加了一个字符
串。由于 friends 数组存在于 Person.prototype 而非 person1 中,所以刚刚提到的修改也会通过
person2.friends (与 person1.friends 指向同一个数组)反映出来。假如我们的初衷就是像这样
在所有实例中共享一个数组,那么对这个结果我没有话可说。可是,实例一般都是要有属于自己的全部
属性的。而这个问题正是我们很少看到有人单独使用原型模式的原因所在

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

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实
例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,
但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参
数;可谓是集两种模式之长。下面的代码重写了前面的例子。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); //false
alert(person1.sayName === person2.sayName); //true

动态原型模式

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() 方法不存在的情况下,才会将它添加到原
型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修
改了。不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法确实可
以说非常完美。其中, if 语句检查的可以是初始化之后应该存在的任何属性或方法——不必用一大堆
if 语句检查每个属性和每个方法;只要检查其中一个即可。对于采用这种模式创建的对象,还可以使
用 instanceof 操作符确定它的类型。
使用动态原型模式时,不能使用对象字面量重写原型。前面已经解释过了,如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。

寄生构造函数模式

通常,在前述的几种模式都不适用的情况下,可以使用寄生(parasitic)构造函数模式。这种模式
的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但
从表面上看,这个函数又很像是典型的构造函数。下面是一个例子。
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"
在这个例子中, 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"

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值