6.1 理解对象
6.1.1 对象属性类型
ECMS属性有两种类型:数据属性和访问器属性
1 数据属性
[[configurable]]
表示能否通过Delete 删除属性从而从新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。对象上定义的属性默认值为true
[[enumerable]]
表示能否通过for-in循环返回属性。直接在对象上定义的属性,它们的这个特性默认值为true
[[writable]]
表示能否修改属性值。像前面例子中那样直接在对象上定义的属性,它们默认值为true
[[value]]
包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置,这个特性的默认值为undefined;
<1> 要想修改该属性的默认值时,必须借助ECMScript5中,
/*
@param1 属性所在对象
@param2 属性名称
@param3 属性描述对象(对象属性类型必须是 Configurable、Enumerable、Writable、Value)
*/
Object.defineProperty(); 使用其定义的属性的时,默认的描述对象属性值为false 这与 {name:"age",age:20}不同
通过设置一个或多个、可以修改对象属性。
例如:使用defineProperty方法定义时
var c = {};
Object.defineProperty(c,"name",{
//configurable:false, default
//enumerable:false,
//writable:false,
value:"WJ"
});
console.log(c.name);//WJ
c.name = "WL"; // 在严格模式下,这种方式是会导致抛出错误
console.log(c.name);//WJ
delete c.name;
console.log(c.name);//WJ
例如直接使用 Object 或 {} 创建对象时
var ownB = {};
ownB.name = "WJ";
console.log(ownB.name);//WJ
ownB.name ="WL";
console.log(ownB.name);//WL writable
for(i in ownB){
console.log(i); //name enumerable
}
console.log(delete ownB.name); //@return true configurable
console.log(ownB.name); //undefind
注意:当设置configurable:false之后,就不能再把它变回可配置了。此时,再调用Object.defineProperty()方法修改除 writable 之外的特性,都会导致错误
var c = {};
Object.defineProperty(c,"name",{
configurable:false, //default
//enumerable:false,
//writable:false,
value:"WJ"
});
//抛出错误 Uncaught TypeError: Cannot redefine property: name
Object.defineProperty(c,"name",{
configurable:true, //default
//enumerable:false,
//writable:false,
value:"WJ"
});
6.1.2 访问器属性
访问器属性不包含数据值;它们包含一对儿getter和setter 函数(不过,这两个函数都不是必需的)。
在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下4个特性。
[[Configurable、Enumerable]]
[[Get Set]]:在读取或写入属性时调用的函数。
访问器属性不能直接定义,必须使用Object.defineProperty()来定义。请看下面的例子。
var book = {
_year:2004,
edition:1
};
Object.defineProperty(book,"year",{
get:function(){
return this._year;
},
set:function(newValue){
if(newValue>2004){
this._year = newValue;
this.edition += newValue- 2004;
}
}
});
book.year = 2005;
alert(book.edition);//2
_year : 前面的下划线是一种常用的记号,用于表示只能通过对象方法访问的属性。
而访问器属性year则包含一个getter函数和一个setter函数。getter 函数返回_year的值,setter函数通过计算来确定正确的版本。
因此,把year属性修改为2005会导致_year变为2005
而edition变为2. 这是使用访问器属性的常见方式,既设置一个属性的值会导致其它属性发生变化。
不一定非要同时指定getter和setter。只指定getter意味着属性是不能写,尝试写入属性会被忽略。在严格模式下,尝试写入只指定了getter函数的属性会抛出错误。类似地,没有制定
指定setter函数的属性也布恩那个读,否则在非严格模式下返回undefined,而在严格模式下回抛出错误。
6.1.3 定义多个属性:
由于为对象定义多个属性的可能性很大,ECMAScript5 又定义了一个Object.defineProperties()方法。
/**
* @param1 指定对象是要添加和修改其属性的对象,
* @param2 对象的属性与第一个对象中要添加或修改的属性一一对应。
* Object.defineProperties(param1,param2);
/
var book = {};
Object.defineProperties(book,{
_year:{
value:2004,
writable:true
},
edition:{
value:1
},
year:{
get:function(){
return this._year;
},
set:function(newValue){
if(newValue>2004){
this._year = newValue;
this.edition += newValue -2004;
}
}
}
});
book.year = 2005;
console.log(book.year); //2005
上定义了两个数据属性和一个访问器属性
支持Object.defineProperties()方法的浏览器有IE9+ Firefox 4+ Safari 5+ Opera 12+ 和 Chrome。
6.1.4 读取属性的特性
使用ECMAScript 5的Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。这个方法接收两个参数
/**
*@param1 属性所在对象
*@param2 要读取其描述符的属性名称
*@return Object
*getOwnPropertyDescriptor(param1,param2)
*/
如果是
访问属性:这个对象有 configurable\enumerable\get\set
数据属性:这个对象有 configurable\enumerable\writable\value
var book = {};
Object.defineProperties(book,{
_year:{
value:2004,
writable:true
},
edition:{
value:1
},
year:{
get:function(){
return this._year;
},
set:function(newValue){
if(newValue>2004){
this._year = newValue;
this.edition += newValue -2004;
}
}
}
});
book.year = 2005;
// console.log(book.year); //2005
var edition_desc = Object.getOwnPropertyDescriptor(book,"edition");
console.log(edition_desc.value);//1
console.log(typeof edition_desc.get);//undefind
var year_desc = Object.getOwnPropertyDescriptor(book,"year");
console.log(year_desc.value);//undefind
console.log(typeof year_desc.get);// function
6.2 创建对象
虽然Object 构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:
使用同一个接口创建很多对象,会产生大量的重复代码。
为了解决这个问题,人们开始使用工厂模式的一种变体。
6.2.1 工厂模式 - 创建多个相同对象
function createPerson(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
return o;
}
var person1 = createPerson("Nicholas",20,"soft");
var person2 = createPerson("Greg",27,"IT");
优点:能够无数次调用该函数,生成相同属性的对象。
缺点:但却没有解决对象识别的问题。
6.2.2 构造函数模式
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",20,"soft");
var person2 = new Person("Greg",27,"IT");
在这个实例中,Person()函数取代了createPerson函数。我们注意到,Person()中的代码除了与createPerson中相同的部分外,还存在以下不同之处:
1 没有显示的创建对象
2 直接将属性和方法赋给了this对象
3 没有return语句
要创建Person的新实例,必须使用new 操作符。以这种方式调用构造函数实际上会经历以下4个步骤
(1) 创建一个新对象
(2) 将构造函数的作用域赋给新对象
(3) 执行构造函数中的代码
(4) 返回新对象
前面例子的最后,person1 \ person2 分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person
console.log(person1.constructor == Person); //true
console.log(person2.constructor == Person); //true
最初标识对象类型的方式:constructor属性。但是,提到检测检测类型,还是 instancof 操作符更靠谱些。
console.log(person1 instancof Person); //true
console.log(person2 instancof Person); //true
console.log(person1 instancof Object); //true
console.log(person2 instancof Object); //true
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。
在这个例子中,person1 和 person2 之所以同时是Object 的实例,是因为所有对象均继承自Object;
1 构造函数用法
将构造函数当作函数
构造函数与其它函数的区别,就是调用的方式不一样。不过构造函数也是函数,也不存其定义方式不一样。
任何函数通过new操作符来调用,那他就可以作为构造函数。如果任何函数不同过new来调用,则和普通函数没有啥区别;
<1> 当作为构造函数调用时
var person1 = new Person("Nicholas",20,"soft");
person1.sayName();// Nicholas
<2> 当作为普通函数调用时 == window
Person("Nicholas",20,"soft");
sayName();//Nicholas
<3> 在另外一个对象作用域中调用
var o = new Object();
Person.cell(o,"Nicholas",20,"soft");
o.sayName(); //Nicholas
2 构造函数问题
每个函数在每个实例上多创建了一遍; person1 与 person2 都有一个名为 sayName 的方法,但那两个方法不是相同的实例;
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)")
}
在ECMScript中函数也是对象,因此定义一个函数也就是实例化了一个对象(Person.sayName);
从这个角度来看每个实例对象是存在不同的Function实例的本质。
创建两个完全相同的Function实例确实没有必要;况且还有this,不要代码执行前就把函数绑定到特定的对象上面。因此大可以像这样
把函数转移到外部方式来解决这个问题;
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
这样的方式来确实解决一个问题是,解决两个函数做同一件事情,但是当一个对象存在多个方法的时候,这对于自定的函数来说丝毫没有封装可言。好在这些问题可以通过原型来解决。
6.2.3 原型模式
每个函数都有一个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
1 理解原型对象
无论什么时候,只要创建一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。
在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。
就拿前面来说 Person.prototype.constructor 指向 person。而通过这个构造函数,我们还可以继续为原型对象。
而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。
创建自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于其它方法,则都是从Object 继承而来的。
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版中管这个指针叫[[Prototype]].
虽然脚本中没有标准的 方式访问,但 Firefox\ Safari\ Chrome 在每个对象上都支持一个属性__proto__ 而在其它实现中,这个属性对象脚本则是完全不可见的。
不过,要明确的 [[Prototype]] 连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
虽然在所有实现过程中无法访问到[[Potottype]],但可以通过isPrototypeOf()方法来确定对象与原型之间的关系。
从本质讲,如果[[Potottype]]指向调用isPrototypeOf()方法的对象(Person.property),那么这个方法就返回true 如下
console.log(Person.prototype.isPrototypeOf(person1)); //true
ECMScript5增加一个新方法
/*
@param1 对象
@return 对象关联的原型对象
IE9+ Firefox3.5+,Safari5+,Opera 12+ 和 Chrome
*/
Object.getPrototypeOf()
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。
1 搜索首先从对象实例开始 找到则返回 否 继续
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";
console.log(person1.name); //Greg --- 来自实例
console.log(person2.name); //Nicholas --- 来自原型
该实例说明一个问题:实例中添加与原型中声明同名的变量,只会阻止其访问其原型。并不会修改那个属性。 即使将这个属性设置为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();
person1.name = "Greg";
delete person1.name;
console.log(person1.name); //Nicholas --- 来自原型
注意:判断一个属性是存在于实例中,还是存在于原型中。这个方法只在给定属性存在于对象实例中时,才会返回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();
person1.hasOwnProperty("name"); //false
person1.name = "WJ";
person1.hasOwnProperty("name"); //true
2 原型与in 操作符
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();
console.log(person1.hasOwnProperty("name")); //false
console.log("name" in person1) // true
person1.name = "Greg";
console.log(person1.name); // 来自实例
console.log(person1.hasOwnProperty("name")); //true
console.log("name" in person1); //true
delete person1.name
console.log(person1.name); // Nicholas 来自原型
console.log(person1.hasOwnProperty("name")); // false
console.log("name" in person1) // true
从上面可以看出要么是从对象,要么是从原型中访问到的。因此,调用“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 person1 = new Person();
hasPrototypeProperty(person1,"name"); // true 原型
person1.name = "Greg"; // 实例
hasPrototypeProperty(person1,"name"); // false
该属性显示存在于原型中时 hasPrototypeProperty 返回true
当第二次的时返回false 因为实例中存在该同名属性时,就不要原型中的同名属性
注意:在使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举的属性(即将[[Enumerable]])
标记的属性)的实例实例属性也会在for-in循环中返回,因为根据规定,所有开发人员定义的属性是可枚举的----只有在IE8及更好版本中例外。
IE早起版本的实现中存在一个bug,既屏蔽不可枚举属性的实例属性不会出现在for-in循环中
例
var o = {
toString:function(){
return "My Object";
}
}
for(var prop in o){
if(prop == "toString"){
alert("Found toString"); //在IE中不会显示
}
};
要获得对象上的可枚举属性,可以利用 ECMScript5 增加一个新方法 Object.keys();
/*
@param object
*/
Object.keys();
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software";
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
如果想获得所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法。
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); // constructor name age job sayName
keys()\getOwnPropertyNames Export: IE9+ Firefox4+ Safari5+ Opera12+ Chrome
3 更简单的原型语法
读者大概注意到了,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype. 为减少不必要的输入,也为了从视觉上更好的封装原型的功能,
更常见的做法是用一个包含所有属性和方法的对象自面量来重写整个原型对象,如下
function Person(){}
Person.prototype = {
name:"Nicholas",
age:29,
job:"Safari5",
sayName:function(){
alert(this.name);
}
};
在上面的代码中,我们将Person.prototype 设置为等于一个以对象自面量形式创建的新对象。
最终结果相同,但有一个例外:constructor 属性不再指向Person了。在这里本质上完全重写了默认的prototype对象,
因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。
此时, instanceof 操作符还能返回正确结果,但通过constructor已经无法确定对象的类型了。
var friend = new Person();
alert(friend instanceof Object); //true
alert(friend instanceof Person); //true
alert(friend.constructor == Object); //true
alert(friend.constructor == Person); //false
在此,用 instanceof 操作符测试Object 和 Person 任然返回 true, 但constructor 属性则等于 Object 而不等于Person 了。如果constructor的值真的很重要,可以象
下面这样特意将它设置回适当的值。
function Person(){}
Person.prototype = {
constructor:Person,
name:"Nicholas",
age:29,
job:"Safari5",
sayName:function(){
alert(this.name);
}
};
以上代码特意包含了一个constructor属性,并将它的值设置为Person,从而确保了通过该属性能够访问到适当的值。
但是这种方式设置constructor属性的[Enumerable]特性被设置为true。默认情况下,原生的constructor 属性是不可枚举的,因此如果你使用兼容ECMAScript5 的JavaScript引擎,可以试一试
Object.defineProperty().
4 原型的动态性
原型中查找值得过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来- 即使是先创建实例后修改原型也照样如此。
5 原生对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。
所有原生的引用类型(Object、Array、String 等)都在其构造函数的原型上定义了方法。
6 原型对象的问题
<1> 就是其共享本质问题
原型中所有属性是本很多实例共享的,这种共享对于函数非常合适。对于包含引用类型值得属性来说,问题就比较突出了。
function Person(){
}
Person.prototype = {
constructor:Person,
name:"Nicholas",
age:29,
job:"Software",
friends:["Shelby","Court"],
sayName:function(){
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends);
alert(person2.friends);
alert(person1.friends === person2.friends);
从上面的一个实例看出来,person1与perosn2共用一个字符串数组,其中一方作修改都会反映出来。假如我们的初衷是两个对象实例共享一个数组
的话,那是没有问题;可是,实例一般是要由属于自己的全部属性,而这个问题正是我们很少有人会单独使用原型;
6.2.4
组合使用构造函数和原型模式
1 构造函数定义实例属性
2 原型定义方法和共享的属性
每个实例都有自己的实例属性的副本,但同时又共享着对象的引用,最大限度地节省了内存。 另外这种混成模式还支持想构造函数传递参数;可谓是集两种模式之长。
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 perosn1 = new Person("Nicholas",29,"Software");
var perosn1 = new Person("Greg",27,"Software");
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
这种方式,使用最多,最广,认同度最高的一种创建自定义类型的方式。
6.2.5 动态原型模式
对于OO语言经验的人来说,由于构造函数、原型相互独立的时候,会感到非常的困惑。动态原型模式正是致力于解决这个问题的一个方案,它把所有信息封装在一个构造
函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的有点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否
需要初始化原型。
function Person(){
if(typeof this.sayName !="function"){
Person.prototype.sayName = function(){
alert(this.name);
}
}
}
var friend = new Person();
friend.sayName();
这种方式主要是:只有当sayName方法不存在的情况下,才会将它添加到原型中。
6.2.6 寄生构造函数模式