从理解对象到创建对象

Posted 瓜牛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从理解对象到创建对象相关的知识,希望对你有一定的参考价值。

    javascript不是一门真正的面向对象语言,因为它连最基本的类的概念都没有,因此它的对象和基于类的语言中的对象也会有所不同。ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。” 严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。我们可以把ECMAScript的对象想象成散列表:无非就是一组名值对,其中的值可以是数据或函数。每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型,也可以是开发人员自己定义的。

  

一、理解对象  

  创建自定义对象的最简单方式就是创建一个Object的实例,然后为其添加属性和方法。代码如下:

1 var person = new Object();
2 person.name = "Tom";
3 person.age = 29;
4 person.job = "CEO";
5 
6 person.sayName = function(){
7     alert(this.name);
8 }

  简单点,可以用对象字面量创建对象。代码如下:

1 var person = {
2     person.name = "Tom";
3     person.age = 29;
4     person.job = "CEO";
5 
6     person.sayName = function(){
7         alert(this.name);
8     }
9 }

 

1、属性类型

  ECMA-262第5版在定义只有内部才用的特性(attribute)时,描述了属性(property)的各种特征。定义这些特性是为了实现JavaScript引擎用的,因此在JavaScript中不能直接访问它们。为了表示特性是内部值,该规范把它们放在了俩对儿方括号里,例如[[Enumerable]]。

  ECMAScript中有两种属性:数据属性和访问器属性。

 

  1.1  数据属性数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性。

  [[Configurable]] :  表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
  [[Enumerable]] : 表示能否通过for...in循环返回属性。
  [[Writable]] : 表示能否修改属性的值。
  [[Value]] : 包含这个属性的数据值。

  要修改属性默认的特性,必须使用ECMAScript 5的Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象,属性的名字和一个描述付对象,其中描述符(descriptor)对象的属性必须是:configurable, enumerable, writable 和value。设置其中的一个或多个值,可以修改对应的特性值。
调用Object.defineProperty()方法时,如果不显式指定,configurableenumerablewritable特性的默认值都是false

 

  1.2  访问器属性:访问器属性不包含数据值;访问器属性包含一对getter和setter函数(这两个函数都不是必需的)。在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据。访问器有如下4个特性:

  [[Configurable]] :  表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
  [[Enumerable]] : 表示能否通过for...in循环返回属性。
  [[Get]]:在读取属性时调用的函数。默认值为undefined。

  [[Set]]:在写入属性时调用的函数。默认值为undefined。

  

数据属性和访问器属性详情请参见 :《JS高程》——数据属性和访问器属性.

 

二、创建对象

  虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方法有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。下面来一步一步完善:

 

1、工厂模式

  这种模式抽象了创建对象的过程,考虑到JavaScript中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节,如下:

 1 function createPerson(name, age, job){
 2     var o = new Object();
 3     o.name = name;
 4     o.age = age;
 5     o.job = job;
 6     o.sayName = function(){
 7         alert(this.name);
 8     };
 9     return o;
10 }
11 var person1 = createPerson(\'pretty\', 29, "FE");
12 var person2 = createPerson(\'Grey\', 27, "DT");

  工厂模式虽然解决了多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。于是,新的模式出现了...

 

2、构造函数模式

  ECMAScript中的构造函数可用来创建特定类型的对象。像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。例如:

 1 function Person(name, age, job){
 2     // 默认 var this = new Object();
 3     this.name= name;
 4     this.age = age;
 5     this.job = job;
 6     this.sayName = function(){
 7         alert(this.name);
 8     }
 9     
10     // 默认 return this;
11 }
12 var person1 = new Person(\'pretty\', 29, "FE");
13 var person2 = new Person(\'Grey\', 27, \'Dt\');

  在调用部分,var person1=new Person("nicole",24); 经历了以下4个步骤(即new操作符都做了啥):

(1)创建一个新对象   // var this = new Object();

(2)将构造函数的作用域赋给新对象(this指向新对象)  // this._proto_ = Base.prototype;

(3)执行构造函数中的代码(为新对象添加属性)  // Base.call(this);

(4)返回新对象  // return this;

以上两个实例person1&person2分别保存着Person的不同实例,这两个对象都有一个constructor属性(不可枚举,enumerable=false),该属性指向Person

1 console.log(person1.constructor==Person)  //true
2 console.log(person2.constructor==Person);  //true
3 console.log(person2.constructor==person1.constructor);  //true    

提到检测对象类型,instanceof 操作符要更可靠一些,我们在这个例子中创建的所有对象既是Object的实例,同时也是Person的实例

1 alert(person1 instanceof Person) //true
2 alert(person1 instanceof Object) //true

person1和person2之所以同时是Object的实例,是因为所有对象均继承自Object

 

  2.1  将构造函数当作函数

  构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通的函数也不会有什么俩样。

 1 //当作构造函数使用
 2 var person = new Person(\'pretty\', 29, \'Fe\');
 3 person.sayName(); // \'pretty\'
 4 
 5 //作为普通函数调用
 6 Person(\'Greg\', 27, "Dt");
 7 window.sayName(); //\'Greg\'
 8 
 9 //在另一个对象的做用域中调用
10 var o = new Object();
11 person.call(o, "Kristen" ,25, "Te");
12 o.sayName(); //\'Kristen
View Code

  当作构造函数时好理解,关键看看作为普通函数调用时发生了什么:属性和方法都被添加到window对象。因为在全局作用域调用一个函数时,this对象总是指向Global对象(在浏览器中就是window对象)。因此,在调用完函数之后,可以通过window对象来调用sayName()方法,并且还返回了Greg。最后,也可以使用call()(或者apply())在某个特殊对象的的作用域中调用Person()。这里是在对象o的作用域中调用的,因此,调用后o就拥有了所有的属性和sayName()方法。

 

  2.2  构造函数的问题

  使用构造函数的主要问题就是每个方法都要在每个实例上重新创建一遍。在前面的例子中,person1和person2都有一个名为sayName()的方法。但那两个方法不是同一个function的实例。不要忘了--ECMAScript 中的函数就是对象,因为每定义一个函数,也就是实例化了一个对象。从逻辑上讲,此时的构造函数也可以这样定义:

1 function Person(name, age, job){
2     this.name = name;
3     this.age = age;
4     this.job = job;
5     this.sayName = new Function(\'alert(this.name)\'); //与声明函数在逻辑上是等价的
6 }
View Code

 

3、原型模式

  我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途就是包含可以由特定类型的所有实例共享的属性和方法。

 1 function Person(){}
 2 Person.prototype.name = "pretty";
 3 Person.prototype.age = 18;
 4 Person.prototype.job = "fe";
 5 Person.prototype.sayName = function(){
 6     alert(this.name);
 7 };
 8 var person1 = new Person();
 9 person1.sayName(); //\'pretty\'
10 var person2 = new Person();
11 person2.sayName() //\'\'pretty"
12 
13 alert(person1.sayName == person2.sayName);  //true
View Code

 

  3.1  理解原型对象

  无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象,在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。Person.prototype.constructor指向Person. 而通过这个属性我们可以继续为原型对象添加属性和方法
  创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object继承而来.
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性)指向构造函数的原型对象,ECMA-262第五版中称这个指针为[[Prototype]],没有标准的方式来访问[[Prototype]],但是在Firefox、Safari、Chrome浏览器中每个对象都支持属性__proto__,而在其它实现中该属性是完全不可见的。不过,要明确的真正重要的一点就是,这个连接存在于实例与构造函数原型对象之间,而不是存在于实例与构造函数之间。关系如图:

  虽然所有的实现都无法访问[[Prototype]],但是可以通过原型对象的isPrototypeOf()方法来确定实例与原型对象之间是否存在这种关系,如果实例的[[Prototype]]指向了调用isPrototypeOf()方法的原型对象(Person.prototype),则该返回true.

1 Person.prototype.isPrototypeOf(person1); //true
2 Person.prototype.isPrototypeOf(person2); //true

  在ECMAScript5中有个方法叫做Object.getPrototypeOf(),在所有支持的实现中,该方法返回[[Prototype]]的值:

alert(Object.getPrototypeOf(person) == Person.prototype); //true

支持该方法的浏览器有IE9+、Firefox 3.5+、Safari 5+、Opera 12+、Chrome。

  每当读取某个对象的属性时,都会进行一次搜索,首先从对象实例本身开始,找到了就返回属性值,如果没有找到,则继续搜索指针指向的原型对象,这就是多个对象实例共享原型对象所保存的属性和方法的基本原理。

  虽然可以通过对象实例访问保存在原型对象中的值,但是不能通过对象实例重写原型对象中的值。如果在某个实例中添加了一个属性,且该属性与原型对象中的某个属性同名,那么是在实例中创建该属性,该属性将会屏蔽掉原型对象中的那个同名属性。

  同时也可以使用delete操作符来删除某个实例属性,从而能够重新访问原型对象中的同名属性。

  方法hasOwnProperty()(从Object对象继承而来的)可以用来检测一个属性是存在于实例本身还是存在于原型对象中,只有给定属性存在于实例中时,才返回true。

 

  3.2  原型与in操作符

使用in操作符的两种方式,如下:

    • 单独使用:通过对象能够访问指定属性时返回true,无论该属性存在于实例对象中还是存在于原型对象中,使用方式为:"属性名" in 对象。结合hasOwnProperty方法使用就能确定一个属性是否存在且存在什么对象中。
      /*判断实例属性是否在原型中*/
      function hasPrototypePrototype(object, name){
          return !object.hasOwnProperty(name) && (name in object);
      }
      • 在for-in循环中使用:返回的是所有能够通过对象访问、可枚举(enumerated)的属性,既包括存在于实例中的属性,也包括存在于原型对象中的属性;屏蔽了原型对象中的不可不枚举属性([[Enumerable]]标记的属性)的实例属性也会在该循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的(IE8--例外)。此外,要取得对象上所有可枚举的的实例属性,可以使用ECMAScript5中的Object.keys()方法,返回一个包含所有可枚举属性的字符串数组:Object.keys(Person.prototype)。如果是要取得所有实例属性,而无论该属性是否可枚举,则可以使用Object.getOwnPropertyNames(对象)。支持这两个方法的浏览器包括:IE9+、Firefox4+、Safari5+、Opera12+、Chrome。

 

  要想取得对象上所有可枚举的实例属性,可以使用ECMAScript5的Object.key()方法。这个方法接受一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。例如:

 1 function Person() {}
 2 
 3 Person.prototype.name = "Tom";
 4 Person.prototype.age = 22;
 5 Person.prototype.job = "CEO";
 6 Person.prototype.sayName = function() {
 7     alert(this.name);
 8 }
 9 
10 var keys = Object.keys(Person.prototype);
11 alert(keys);  //"name,age,job,sayName"
12 
13 var p1 =new Person();
14 p1.name = "Rob";
15 p1.age = 32;
16 
17 var p1keys = Object.keys(p1);
18 alert(pekeys);  //"name,aeg";
View Code

 

  3.3  原型的动态性

  由于在原型中查找值的过程是一次搜索,因此我们对原型所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。比如:

1 var friend = new Person();
2 
3 Person.prototype.sayHi = function(){
4     alert("Hi");
5 }
6 
7 friend.sayHi();  //"Hi"  (没有问题)
View Code

  原因是实例与原型之间的松散连接关系。当我们调用person.sayHi()时,首先会在实例中搜索名为sayHi的属性,在没有找到的情况下,会继续搜索原型。因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的sayHi属性并返回保存在那里的函数。

  尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就变的糟糕了。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另一个对象就等于切断了构造函数与最初原型之间的联系。切记:实例中的指针仅指向最初的原型,而不指向构造函数。