《JavaScript 高级程序设计》学习总结六(1)
引言:前面的一章我们已经介绍了引用类型,其中在那一章我们大概了解到了“对象”这个概念。同时也提到 JavaScript 中函数也是对象,甚至可以提出“万物皆对象的概念”。那么在这一章我开始就对 JavaScript的“对象”开始一个学习总结,首先我们可以了解的是,面向对象都有一个“类”的概念。而通过类可以创建任意多个具有相同属性的方法和对象,但是在ECMAScript 中却没有类的概念。这也是JavaScript 不同于一般的面向对象的特别之处。对象在JavaScript中是一个重要知识点,所以这一章的内容会比较长,总结方面结构上我会依旧采用分章节总结,内容上我尽量不会偏基础的知识点,因为很多知识点我已经在前面有过多次提到(比如创建对象。添加属性等等),所以可以说这个总结方向会随着章节会慢慢的越来越不偏向基础,而是慢慢偏向一些难点,这也是我想要做的。而在一些知识点我也会尽量通过自己的积累来进行解释,尽量不全搬书上,这样一来无论是对自己的学习提升还是内容的补充都是很好的(其实我在前面几章也是这么做的)。
属性类型:
ECMASCript 中有两种属性:数据属性和访问器属性。
数据属性:
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的 特性。
1、 [[Configurable] kən'fɪgərəbl ]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特 性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的 这个特性默认值为 true。
2、[[Enumerable ] ɪ'nju:mərəbəl] ]:表示能否通过 for-in 循环返回属性。像前面例子中那样直接在对象上定 义的属性,它们的这个特性默认值为 true。
3、[[Writable]'raɪtəbəl ]:表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的 这个特性默认值为 true。
4、[[Value] vælju ] :包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候, 把新值保存在这个位置。这个特性的默认值为 undefined。
对于像前面例子中那样直接在对象上定义的属性,它们的[[Configurable]]、[[Enumerable]] 和[[Writable]]特性都被设置为 true,而[[Value]]特性被设置为指定的值。例如:
var person = { name: "Nicholas" }; 这里创建了一个名为 name 的属性,为它指定的值是"Nicholas"。
也就是说,[[Value]]特性将 被设置为"Nicholas",而对这个值的任何修改都将反映在这个位置。
要修改属性默认的特性,必须使用 ECMAScript 5 的 Object.defineProperty()方法。这个方法 接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属 性必须是:configurable、enumerable、writable 和 value。设置其中的一或多个值,可以修改 对应的特性值。
把 configurable 设置为 false,表示不能从对象中删除属性。如果对这个属性调用 delete,则 在非严格模式下什么也不会发生,而在严格模式下会导致错误。而且,一旦把属性定义为不可配置的, 就不能再把它变回可配置了。此时,再调用 Object.defineProperty()方法修改除 writable 之外 的特性,都会导致错误,比如:
1 var person = {}; 2 3 Object.defineProperty(person, "name", { 4 5 configurable: false, 6 7 value: "Nicholas" 8 9 }); //抛出错误 10 11 Object.defineProperty(person, "name", { 12 13 configurable: true, 14 15 value: "Nicholas" 16 17 });
可以多次调用 Object.defineProperty()方法修改同一个属性,但在把 configurable 特性设置为 false 之后就会有限制了。
ps: 在调用 Object.defineProperty()方法时,如果不指定,configurable、enumerable 和 writable 特性的默认值都是 false。IE8 是第一个实现 Object.defineProperty()方法的浏览器版本。然而,这个 版本的实现存在诸多限制:只能在 DOM 对象上使用这个方法,而且只能创建访问器 属性。由于实现不彻底,建议读者不要在 IE8 中使用 Object.defineProperty() 方法。
访问器属性:
访问器属性不包含数值,它们包含一对 getter 和 setter 函数(非必须函数),getter 负责读取访问器属性,setter负责写入访问器属性。访问器属性有如下四个特性:
1、 [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特 性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为 true。
2、[[Enumerable]]:表示能否通过 for-in 循环返回属性。对于直接在对象上定义的属性,这 个特性的默认值为 true。
3、 [[Get]]:在读取属性时调用的函数。默认值为 undefined。
4、[[Set]]:在写入属性时调用的函数。默认值为 undefined。
访问器属性不能直接定义,必须使用 Object.defineProperty()来定义。
在不支持 Object.defineProperty() 方法的浏览器中不能修改 [[Configurable]] 和 [[Enumerable]]。
定义多个属性:
由于为对象定义多个属性的可能性很大,ECMAScript 5 又定义了一个 Object.defineProperties()方法。利用这个方法可以通过描述符一次定义多个属性。这个方法接收两个对象参数:第一 个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对 应。
读取属性特性:
使用 ECMAScript 5 的 Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述 符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果 是访问器属性,这个对象的属性有 configurable、enumerable、get 和 set;如果是数据属性,这 个对象的属性有 configurable、enumerable、writable 和 value。
在 JavaScript 中,可以针对任何对象——包括 DOM 和 BOM 对象,使用 Object.getOwnPropertyDescriptor()方法。支持这个方法的浏览器有 IE9+、Firefox 4+、Safari 5+、Opera 12+和 Chrome。
原型模式:
我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象, 而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那 么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以 让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是 可以将这些信息直接添加到原型对象中。
比如:
1 function person( ){ 2 3 } 4 5 Person.prototype.name = "Nicholas"; 6 7 Person.prototype.age = 29; 8 9 Person.prototype.job = "Software Engineer"; 10 11 Person.prototype.sayName = function(){ 12 13 alert(this.name); 14 15 }; 16 17 var person1 = new Person(); 18 19 person1.sayName(); //"Nicholas" 20 21 var person2 = new Person(); 22 23 person2.sayName(); //"Nicholas" 24 25 alert(person1.sayName == person2.sayName); //true 26 27 (代码案例 1)
在这里我们可以看到,我们通过原型向对象里面添加了一些属性,然后再创建两个对象进行属性访问,都可以访问到name 。说明在这里我们通过原型让对象共享到了属性和方法。这也是原型链的作用之一,因为没有类的概念,在 JavaScript 中如此众多的对象就要依靠原型来进行数据的共享和联系。
理解原型对象:
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor (构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。就拿前面的例子来说, Person.prototype. constructor 指向 Person。而通过这个构造函数,我们还可继续为原型对象 添加其他属性和方法。 创建了自定义的构造函数之后,其原型对象默认只会取得 constructor 属性;
扯那么多,其实总结起来就是:
从图来看,就是我们创建了一个函数,这个函数里有个 prototype 属性(原型),然后这个属性指向 原型对象,原型对象里面有我们添加的属性 name age 等等,同时自带一个构造 constructor 属性,这个属性指向创建原型对象的函数。通过这样的方式,函数与原型对象就联系到了一起。
至于其他方法,则 都是从 Object 继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部 属性),指向构造函数的原型对象。ECMA-262 第 5 版中管这个指针叫[[Prototype]]。虽然在脚本中 没有标准的方式访问[[Prototype]],但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性 __proto__;而在其他实现中,这个属性对脚本则是完全不可见的。不过,要明确的真正重要的一点就 是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
看图:
我们通过构造函数 new(JavaScript 的new 操作符指向构造函数) 出来的的实例包含一个 [prototype]属性,这里我们可以按上面理解成 __proto__(也可以称之为隐式原型,这里这么写方便后面的理解),这个原型属性也指向函数的向原型对象。这样一来,也就可以理解为什么 person 1 与 person 2可以访问原型对象中的属性了。
虽然在所有实现中都无法访问到[[Prototype]],但可以通过 isPrototypeOf()方法来确定对象之 间是否存在这种关系。从本质上讲,如果[[Prototype]]指向调用 isPrototypeOf()方法的对象 (Person.prototype),那么这个方法就返回 true,如下所示:
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
ECMAScript 5 增加了一个新方法,叫 Object.getPrototypeOf(),在所有支持的实现中,这个 方法返回[[Prototype]]的值。例如:
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas"
使用 Object.getPrototypeOf() 可以方便地取得一个对象的原型,而这在利用原型实现继承(本章稍后会讨论)的情况下是非常重要的。 支持这个方法的浏览器有 IE9+、Firefox 3.5+、Safari 5+、Opera 12+和 Chrome。
访问属性原则:向上面的例子,我们通过 创建person1 就可以访问到了 原型中的属性,是因为我们通过 创造的时候就有一个 隐式原型指向这个对象,同时这里也会有一个原则:那就是 person1可以访问原型属性,但是不能修改原型属性,同时,如果person 1自己以及有了一个与原型同名的属性,那么person 1访问这个同名属性时就会访问自己的属性,比如上面原型中已经有name 属性,此时 person 1.name="jack", person1 也有 name 属性,此时person 1访问 name 属性就是访问自己的 name 属性。
使用 hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。
如果是来自原型则返回 false ,如果是来自实例则返回 true 。 比如参考上面: person 1.hasOwnProperty(name) //false 这个属性来自原型。
原型与 in 操作符 :
有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用。在单独使用时,in 操作符会在通 过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。
比如:
alert("name" in person1); //true 依旧参考上面(代码案例 1),此时 name 存在于原型中,通过:在单独使用时,in 操作符会在通 过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。可知这里返回 true.。
由于 in 操作符只要通过对象能够访问到属性就返回 true,hasOwnProperty()只在属性存在于 实例中时才返回 true,因此只要 in 操作符返回 true 而 hasOwnProperty()返回 false,就可以确 定属性是原型中的属性。
在使用 for-in 循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中 既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将 [[Enumerable]]标记为 false 的属性)的实例属性也会在 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 中不会显示
} }
当以上代码运行时,应该会显示一个警告框,表明找到了 toString()方法。这里的对象 o 定义了 一个名为 toString()的方法,该方法屏蔽了原型中(不可枚举)的 toString()方法。在 IE 中,由 于其实现认为原型的 toString()方法被打上了值为 false 的[[Enumerable]]标记,因此应该跳过 该属性,结果我们就不会看到警告框。该 bug 会影响默认不可枚举的所有属性和方法,包括: hasOwnProperty()、propertyIsEnumerable()、toLocaleString()、toString()和 valueOf()。 ECMAScript 5 也将 constructor 和 prototype 属性的[[Enumerable]]特性设置为 false,但并不 是所有浏览器都照此实现。
要取得对象上所有可枚举的实例属性,可以使用 ECMAScript 5 的 Object.keys()方法。这个方法 接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。例如:
1 function Person(){ } 2 3 Person.prototype.name = "Nicholas"; 4 5 Person.prototype.age = 29; 6 7 Person.prototype.job = "Software Engineer"; 8 9 Person.prototype.sayName = function(){ 10 11 alert(this.name); 12 13 }; 14 15 var keys = Object.keys(Person.prototype); 16 17 alert(keys); //"name,age,job,sayName" 18 19 var p1 = new Person(); 20 21 p1.name = "Rob"; 22 23 p1.age = 31; 24 25 var p1keys = Object.keys(p1); 26 27 alert(p1keys); //"name,age" 28 29
这里,变量 keys 中将保存一个数组,数组中是字符串"name"、"age"、"job"和"sayName"。这 个顺序也是它们在 for-in 循环中出现的顺序。如果是通过 Person 的实例调用,则 Object.keys() 返回的数组只包含"name"和"age"这两个实例属性。 如果你想要得到所有实例属性,无论它是否可枚举,都可以使用 Object.getOwnPropertyNames() 方法。
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); //"constructor,name,age,job,sayName"
注意结果中包含了不可枚举的 constructor 属性。Object.keys()和 Object.getOwnPropertyNames()方法都可以用来替代 for-in 循环。支持这两个方法的浏览器有 IE9+、Firefox 4+、Safari 5+、Opera 12+和 Chrome。
--------------------------------------------------------------------------------本章节完-------------------------------------------------------------------------------------------------------------------------------------------------------------
下章节预告:继续对对象中的原型进行学习总结,包括继承的实现,等等总要知识点。
这一章节我自己画了图,其实在书本中的图差不多,但是我个人感觉书中的图内容理解很不舒服,看着很乱,就自己(灵魂画手)画了两张,咳咳,画的不怎么好看。
这一章节主要是对原型有了一个大概了解,原型是什么,原型有什么用?这里提一下,我们可能在其他资料上可能也见过 _prototype_ (显示原型)与_proto_ (隐式原型)这样的说法,不得不说,不同资料上面的表示方式有不同,但是其实归咎总结起来,所表示的意思都是一样的,一个是函数原型,一个是实例对象原型。我们在学习的时候不要被太多的资料上的说法搞迷糊了,其实他们所想表达的一样。