JavaScript--06 原型模式及对象属性详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_27127385/article/details/87988796

一、prototype(原型)属性示例

  我们创建的每个函数都有一个 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 

  在此,与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1person2 访问的都是同一组属性和同一个 sayName() 函数。要理解原型模式的工作原理,必须先理解 ECMAScript 中原型对象的性质。

二、理解原型对象

  无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。
  就拿前面的例子来说, Person.prototype.constructor 指向 Person。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。
  创建了自定义的构造函数之后,其原型对象默认只会取得 constructor 属性;至于其他方法,则都是从Object继承而来的。
  以前面使用 Person 构造函数和 Person.prototype 创建实例的代码为例,下图展示了各个对象之间的关系。


  上图展示了 Person 构造函数、Person 的原型属性以及 Person 现有的两个实例之间的关系。
  在此,Person.prototype 指向了原型对象,而 Person.prototype.constructor 又指回了 Person
  原型对象中除了包含 constructor 属性之外,还包括后来添加的其他属性。
  Person 的每个实例 —— person1person2 都包含一个内部属性,该属性仅仅指向了 Person.prototype;换句话说,它们与构造函数没有直接的关系。此外,要格外注意的是,虽然这两个实例都不包含属性和方法,但我们却可以调用 person1.sayName()。这是通过查找对象属性的过程来实现的。

2.1 Object.getPrototypeOf() 方法 ---- 获取对象的原型

  使用 Object.getPrototypeOf() 可以方便地取得一个对象的原型:

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

2.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(); 

person1.name = "Greg"; 
alert(person1.name); //"Greg"——来自实例
alert(person2.name); //"Nicholas"——来自原型

  在这个例子中,person1name 被一个新值给屏蔽了。但无论访问 person1.name 还是访问 person2.name 都能够正常地返回值,即分别是"Greg"(来自对象实例)和"Nicholas"(来自原型)。

  当在 alert() 中访问 person1.name 时,需要读取它的值,因此就会在这个实例上搜索一个名为 name 的属性。这个属性确实存在,于是就返回它的值而不必再搜索原型了。当以同样的方式访问 person2. name 时,并没有在实例上发现该属性,因此就会继续搜索原型,结果在那里找到了 name 属性。

2.3 delete 操作符 ---- 删除实例属性

  当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。
  即使将这个属性设置为 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"——来自原型

2.4 批量写原型方法

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

  前面曾经介绍过,每创建一个函数,就会同时创建它的 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 

  如果 constructor 的值真的很重要,可以像下面这样特意将它设置回适当的值。

function Person(){ 
} 

Person.prototype = { 
	constructor : Person,   // 加上这一行
	name : "Nicholas", 
	age : 29, 
	job: "Software Engineer", 
	sayName : function () { 
		alert(this.name); 
	} 
}; 

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

function Person(){ 
} 

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

//重设构造函数,只适用于 ECMAScript 5 兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", { 
	enumerable: false, 
	value: Person 
}); 

三、检测属性

3.1 hasOwnProperty() 方法 ---- 检测属性是否存在于实例中

  使用 hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘了它是从 Object 继承来的)只在给定属性存在于对象实例中时,才会返回 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 

person1.name = "Greg"; 
alert(person1.name); //"Greg"——来自实例
alert(person1.hasOwnProperty("name")); //true 

alert(person2.name); //"Nicholas"——来自原型
alert(person2.hasOwnProperty("name")); //false 

delete person1.name; 
alert(person1.name); //"Nicholas"——来自原型
alert(person1.hasOwnProperty("name")); //false 

  通过使用 hasOwnProperty() 方法,什么时候访问的是实例属性,什么时候访问的是原型属性就一清二楚了。

  下图展示了上面例子在不同情况下的实现与原型的关系(为了简单起见,图中省略了与 Person 构造函数的关系)。

3.2 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); //true 

person1.name = "Greg"; 
alert(person1.name); //"Greg" ——来自实例
alert(person1.hasOwnProperty("name")); //true 
alert("name" in person1); //true 

alert(person2.name); //"Nicholas" ——来自原型
alert(person2.hasOwnProperty("name")); //false 
alert("name" in person2); //true 

delete person1.name; 
alert(person1.name); //"Nicholas" ——来自原型
alert(person1.hasOwnProperty("name")); //false 
alert("name" in person1); //true 

  同时使用 hasOwnProperty() 方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中,如下所示。

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

function Person(){ 
} 

Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function(){ 
	alert(this.name); 
}; 

var person = new Person(); 
alert(hasPrototypeProperty(person, "name")); //true
 
person.name = "Greg"; 
alert(hasPrototypeProperty(person, "name")); //false 

  由于 in 操作符只要通过对象能够访问到属性就返回 truehasOwnProperty() 只在属性存在于实例中时才返回 true,因此只要 in 操作符返回 truehasOwnProperty() 返回 false,就可以确定属性是原型中的属性。
  在这里,name 属性先是存在于原型中,因此 hasPrototypeProperty() 返回 true。当在实例中重写 name 属性后,该属性就存在于实例中了,因此 hasPrototypeProperty() 返回 false。即使原型中仍然有 name 属性,但由于现在实例中也有了这个属性,因此原型中的 name 属性就用不到了。

四、枚举属性

4.1 for-in() 方法 ---- 这里可用于枚举属性

  在使用 for-in 循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。

4.2 Object.keys() 方法 ---- 返回所有可枚举属性

  这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。例如:

function Person(){ 
} 

Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function(){ 
	alert(this.name); 
}; 

var keys = Object.keys(Person.prototype); 
alert(keys); //"name,age,job,sayName" 

var p1 = new Person(); 
p1.name = "Rob"; 
p1.age = 31; 

var p1keys = Object.keys(p1); 
alert(p1keys); //"name,age" 

4.3 Object.getOwnPropertyNames() 方法 ---- 返回(包括不可枚举)属性

function Person(){ 
} 

Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function(){ 
	alert(this.name); 
}; 

var keys = Object.getOwnPropertyNames(Person.prototype); 
alert(keys); //"constructor,name,age,job,sayName" 

五、原型的动态性

例一:

var friend = new Person(); 

Person.prototype.sayHi = function(){ 
	alert("hi"); 
}; 

friend.sayHi(); //"hi"(没有问题!)

  这个例子中,即使 person 实例是在添加新方法之前创建的,但它仍然可以访问这个新方法。其原因可以归结为实例与原型之间的松散连接关系。
  当我们调用 person.sayHi() 时,首先会在实例中搜索名为 sayHi 的属性,在没找到的情况下,会继续搜索原型。因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的 sayHi 属性并返回保存在那里的函数。

例二(重写原型的情况):

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 

  在这个例子中,我们先创建了 Person 的一个实例,然后又重写了其原型对象。然后在调用 friend.sayName() 时发生了错误,因为 friend 指向的原型中不包含以该名字命名的属性。
  重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。

六、原生对象的原型

  所有原生引用类型(ObjectArrayString,等等)都在其构造函数的原型上定义了方法。
  例如,在 Array.prototype 中可以找到 sort() 方法,而在 String.prototype 中可以找到 substring() 方法。
  通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。不过不建议这样操作

七、原型对象的缺点

  它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。

  原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。然而,对于包含引用类型值的属性来说,问题就比较大了。来看下面的例子:

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 指向同一个数组)反映出来。

`

猜你喜欢

转载自blog.csdn.net/qq_27127385/article/details/87988796
今日推荐