第六章 面向对象程序设计(二)

1. 对象创建

1.5 原型模式

  前文我们提到过,每一个函数都有一个prototype属性,它是一个指针,指向一个对象,找个对象保存了所有函数实例可以共享的属性和方法。因此,定义自定义引用类型时,可以直接将属性和方法添加到原型对象中。请看下面的例子:

function Person(){
}
Person.prototype.name = "Ivan";
Person.prototype.age = 22;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
}
var person1 = new Person();
person1.sayName();  //"Ivan"
var person2 = new Person();
person2.sayName();  //"Ivan"
alert(person1.sayName == person2.sayName);  //true

在上面的例子中,我们将所有的属性和方法都直接添加到Person的prototype中,构造函数变成了空函数。然而我们依然能够通过构造函数来创建新对象,并且通过Person实例来调用方法和访问属性。但与构造函数模式不同的是,上面的这些属性和方法由所有的实例共享,因此这也是一个问题(事实上我们希望方法在实例间共享,而属性则独属于各个实例,后文会解决这个问题)。

1.5.1 理解原型对象

无论在什么时候,只要创建了一个函数,JavaScript引擎就会根据一组特定的规则为该函数创建一个prototype对象,并将函数的prototype属性指向该对象。另外,每个prototype对象也包含一个constructor属性,指向持有它的函数。用前面的例子来说:Person.prototype.constructor = Person

根据构造函数创建出一个实例后,实例内部也包含一个指针,指向构造函数的原型对象。ECMA-262第5版管这个指针叫[[Prototype]]。虽然在JavaScript中没有明确的方式来访问这个指针,但是在Firefox,Safari和Chrome中,都支持一个proto属性来访问这个[[Prototype]]。真正需要明确的一点是,这个指针负责连接实例与构造函数的原型对象,而不是连接实例与构造函数。下面展示了Person构造函数及其实例与原型对象的关系:

image.png

从图中我们可以看到,Person对象的两个实例person1和person2,都包含一个[[Prototype]]属性,指向了Person.prototype对象;换句话说,这两个实例与Person构造函数其实并没有直接的关系。此外,需要格外注意的是,虽然这两个实例都不包含任何属性和方法,却能够成功调用person1.sayName()等Person.prototype定义的方法,这是通过查找对先属性的过程来实现的。

每当代码读取对象的某个属性时,都会经历一次搜索过程,具体搜索过程如下:
(1) 搜索首先从对象本身开始,如果在实例中找到了具有给定名称的属性,则直接返回,否则进行下一步
(2)搜索[[Prototype]]属性指向的原型对象,如果在原型对象中找到了具有给定名称的属性,则返回

也就是说,在执行person1.sayName()时,进行了两次搜索。首先查找person1对象,未找到sayName属性;接着查找person1.[[Prototype]]也就是Person.prototype对象,成功找到sayName方法,因此调用成功。

虽然在所有实现中都无法访问到[[Prototype]],但是可以通过isPrototypeOf()方法来确定对象间是否存在原型关系,例如:

alert(Person.prototype.isPrototypeOf(person1));  //true
alert(Person.prototype.isPrototypeOf(person2));  //true

ECMAScript5增加了一个新方法,叫Object.getPrototypeOf(),在所有支持的视实现中,这个方法返回[[Prototype]]的值:

alert(Object.getPrototypeOf(person1)  == Person.prototype);  //true
alert(Object.getPrototypeOf(person1).name);  //"Ivan"

原型对象最初只包含constructor属性,而该属性也是共享的,因此也可以通过实例访问该属性。

虽然可以通过实例来访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在对象中添加了一个原型中的同名属性,这个属性会与原型中属性相互独立,通过person实例修改这个属性并不会影响到原型中的同名属性。下面是一个例子:

function Person(){
}
Person.prototype.name = "Ivan";
Person.prototype.age = 22;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
}
var person1 = new Person();
var person2 = new Person();
person1.name = "Roy"
alert(person1.name);  //"Roy"
alert(person2.name);  //"Ivan"

上面的例子表明,当我们重写person1.name时,实际上是在person1实例对象中添加了一个名为name的属性,并赋值为"Roy"。此后在读取person1.name时,首先在person1实例中就找到了该属性,因此直接返回"Roy";而读取person2.name时,person2实例对象中不存在name属性,因此继续搜索其原型对象,找到Person.prototype.name后,返回其值"Ivan"。

使用person1.hasOwnProperty("name")方法能够判断name属性是否属于对象实例。hasOwnProperty()方法继承自Object类型,如果给定属性存在于对象实例中(而不是原型中),那么返回true。

当对象能够访问某个属性时(无论本身具有该属性还是通过原型能够访问该属性),使用propertyName in object会返回true。这里的in操作符是判断对象实例是否包含某个属性的关键。通过hasOwnProperty()方法和in操作符,就能够判断某个属性是否独属于原型对象:

function hasPrototypeProperty(object, name){
    return !object.hasOwnProperty(name) && (name in object);
}

使用Object.keys()能够返回一个对象中所有可枚举的实例属性(不搜索原型),这个方法接收一个对象作为参数,返回一个字符串数组。

如果你想要得到所有实例属性,无论它是否可枚举,可以使用Object.getOwnPropertyNames() 方法。

constructor属性是不可枚举的,因此Object.keys(Person.prototype)返回:name, age, job, sayName。

1.5.2 更简单的原型写法

前面的例子中每定义一个属性或方法都得敲一遍Person.prototype。为了减少不必要的输入,也为了从视觉上更好的封装原型的功能,更常见的做法是使用一个包含了所有属性和方法的字面量来重写整个原型对象:

function Person(){}
Person.prototype = {
    name: "Ivan",
    age: 22,
    job: "Software Engineer",
    sayName: function(){
        alert(this.name);
    }
}

上面的例子将Person.prototype设置为了一个以对象字面量形式创建的新对象。因此带来了一个问题:constructor属性不再指向Person了。前面提到,每创建一个函数,就会自动创建一个它的prototype对象,这个对象自动将constructor属性指向源函数。前面的定义诸如Person.prototype.name = "Ivan;"都只是在自动生成的prototype对象中定义属性和方法,而在这个例子中,我们手动将Person.prototype指向了一个新对象(以对象字面量形式创建),这个新对象constructor属性默认指向Object。尽管如此,instancof操作符依旧能返回正确的结果

如果你觉得constructor属性真的很重要,可以再定义时显示的设置其值:

function Person(){}
Person.prototype = {
    constructor: Person,
    name: "Ivan",
    age: 22,
    job: "Software Engineer",
    sayName: function(){
        alert(this.name);
    }
}

注意,以这种方式重设 constructor 属性会导致它的[[Enumerable]]特性被设置为 true。默认 情况下,原生的 constructor 属性是不可枚举的,因此如果你使用兼容 ECMAScript 5的 JavaScript引 擎,可以试一试 Object.defineProperty()。

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

原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒 也说得过去,毕竟(如前面的例子所示),通过在实例上添加一个同名属性,可以隐藏原型中的对应属 性。然而,对于包含引用类型值的属性来说,问题就比较突出了。来看下面的例子:

function Person(){}
Person.prototype = {
    constructor: Person,
    name: "Ivan",
    age: 22,
    job: "Software Engineer",
    friends: ["Nancy", "Bob"],
    sayName: function(){
        alert(this.name);
    }
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push("John");
alert(person1.friends);  //"Nancy, Bob, John"
alert(person2.friends);  //"Nancy, Bob, John"

我们可以看到,对于一个数组的引用类型,我们修改person1对象的friends属性,对于person2对象来说也是可见的,因为本质上person1对象和person2对象都指向原型对象上的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 

这种构造函数与原型混成的模式,是目前在 ECMAScript中使用广泛、认同度高的一种创建自 定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。

1.5.4 动态原型模式

对于拥有其他OO语言开发经验的开发人员来说,看到独立的构造函数和原型很可能会感到困惑。动态原型模式正是致力于解决这一问题的方案。它将所有信息封装在构造函数中,并在构造函数中初始化原型。下面是一个例子:

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 person= new Person("Ivan", 22, "Software Engineer"); 
person.sayName(); //"Ivan"
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容