天天看点

ECMAScript对象

面向对象

一种面向对象语言需要向开发者提供四种基本能力:

  • 封装 - 把相关的信息(无论数据或方法)存储在对象中的能力
  • 聚集 - 把一个对象存储在另一个对象内的能力
  • 继承 - 由另一个类(或多个类)得来类的属性和方法的能力
  • 多态 - 编写能以多种方法运行的函数或方法的能力

    ECMAScript 支持这些要求,因此可被是看做面向对象的。

对象应用

声明和实例化

对象的创建方式是用关键字 new 后面跟上实例化的类的名字:

var oObject = new Object();
var oStringObject = new String();
           

对象引用

在前面的章节中,我们介绍了引用类型的概念。在ECMAScript中,不能访问对象的物理表示,只能访问对象的引用。每次创建对象,存储在变量中的都是该对象的引用,而不是对象本身。

对象废除

ECMAScript 拥有无用存储单元收集程序(garbage collection routine),意味着不必专门销毁对象来释放内存。当再没有对对象的引用时,称该对象被废除(dereference)了。

。每当函数执行完它的代码,无用存储单元收集程序都会运行,释放所有的局部变量,还有在一些其他不可预知的情况下,无用存储单元收集程序也会运行。

把对象的所有引用都设置为 null,可以强制性地废除对象。例如:

var oObject = new Object;
// do something with the object here
oObject = null;
           

每用完一个对象后,就将其废除,来释放内存,这是个好习惯。

注意:废除对象的所有引用时要当心。如果一个对象有两个或更多引用,则要正确废除该对象,必须将其所有引用都设置为 null。

早绑定和晚绑定

所谓绑定(binding),即把对象的接口与对象实例结合在一起的方法。

早绑定(early binding)是指在实例化对象之前定义它的属性和方法,ECMAScript 不是强类型语言,所以不支持早绑定。

晚绑定(late binding)指的是编译器或解释程序在运行前,不知道对象的类型。使用晚绑定,无需检查对象的类型,只需检查对象是否支持属性和方法即可。ECMAScript 中的所有变量都采用晚绑定方法。这样就允许执行大量的对象操作,而无任何惩罚。

对象类型

一般来说,可以创建并使用的对象有三种:本地对象、内置对象和宿主对象。

本地对象(Boolean, Number, String…)

ECMA-262 把本地对象(native object)定义为“独立于宿主环境的 ECMAScript 实现提供的对象”。简单来说,本地对象就是 ECMA-262 定义的类(引用类型)。它们包括:

  • Object
  • Function
  • Array
  • String
  • Boolean
  • Number
  • Date
  • RegExp
  • Error
  • EvalError
  • RangeError
  • ReferenceError
  • SyntaxError
  • TypeError
  • URIError

内置对象(Global & Math)

ECMA-262把内置对象(built-in object)定义为“由ECMAScript实现提供的、独立于宿主环境的所有对象,在ECMAScript程序开始执行时出现”。这意味着开发者不必明确实例化内置对象,它已被实例化了。ECMA-262 只定义了两个内置对象,即 Global 和 Math (它们也是本地对象,根据定义,每个内置对象都是本地对象)。

宿主对象(BOM & DOM)

所有非本地对象都是宿主对象(host object),即由 ECMAScript 实现的宿主环境提供的对象。

所有 BOM 和 DOM 对象都是宿主对象。

对象作用域

公用、私有和受保护作用域

在传统的面向对象程序设计中,包含公有、私有、保护作用域。

ECMAScript 只有公用作用域,ECMAScript 中的所有对象的所有属性和方法都是公用的。因此,定义自己的类和对象时,必须格外小心。记住,所有属性和方法默认都是公用的!

由于缺少私有作用域,开发者确定了一个规约,说明哪些属性和方法应该被看做私有的。这种规约规定在属性前后加下划线:

obj._color_ = "blue";
           

注意,下划线并不改变属性是公用属性的事实,它只是告诉其他开发者,应该把该属性看作私有的。

静态作用域

静态作用域定义的属性和方法任何时候都能从同一位置访问。在 Java 中,类可具有属性和方法,无需实例化该类的对象,即可访问这些属性和方法,例如 java.net.URLEncoder 类,它的函数 encode() 就是静态方法。

ECMAScript 没有静态作用域

严格来说,ECMAScript 并没有静态作用域。不过,它可以给构造函数提供属性和方法。还记得吗,构造函数只是函数。函数是对象,对象可以有属性和方法。例如:

function sayHello() 
{
  alert("hello");
}

sayHello.alternate = function()     // 为函数添加方法
{   
  alert("hi");
}

sayHello();                         // 输出 "hello"
sayHello.alternate();               // 输出 "hi",类静态方法式的调用
           

即使如此,alternate() 也是 sayHello() 公用作用域中的方法,而不是静态方法。

关键字this

它用在对象的方法中。关键字 this 总是指向调用该方法的对象,例如:

function showColor() {
  alert(this.color);
};

var oCar1 = new Object;
oCar1.color = "red";
oCar1.showColor = showColor;

var oCar2 = new Object;
oCar2.color = "blue";
oCar2.showColor = showColor;

oCar1.showColor();              // 输出 "red"
oCar2.showColor();              // 输出 "blue"
           

注意,引用对象的属性时,必须使用 this 关键字。例如,如果采用下面的代码,showColor() 方法不能运行:

function showColor() {
  alert(color);
};
           

如果不用对象或 this 关键字引用变量,ECMAScript 就会把它看作局部变量或全局变量。然后该函数将查找名为 color 的局部或全局变量,但是不会找到。结果如何呢?该函数将在警告中显示 “null”。

定义类和对象

原始的方式

var oCar = new Object;
oCar.color = "blue";
oCar.doors = ;
oCar.mpg = ;
oCar.showColor = function() {
  alert(this.color);
};
           

工厂方式

function createCar(sColor,iDoors,iMpg) {
  var oTempCar = new Object;
  oTempCar.color = sColor;
  oTempCar.doors = iDoors;
  oTempCar.mpg = iMpg;
  oTempCar.showColor = function() { // 函数对象每次都会重新创建
    alert(this.color);
  };
  return oTempCar;
}

var oCar1 = createCar("red",,);
var oCar2 = createCar("blue",,);

oCar1.showColor();      //输出 "red"
oCar2.showColor();      //输出 "blue"
           

前面的例子中,每次调用函数 createCar(),都要创建新函数 showColor(),意味着每个对象都有自己的 showColor() 版本。而事实上,每个对象都共享同一个函数。

所以改进的版本如下:

function showColor() {
  alert(this.color);
}

function createCar(sColor,iDoors,iMpg) {
  var oTempCar = new Object;
  oTempCar.color = sColor;
  oTempCar.doors = iDoors;
  oTempCar.mpg = iMpg;
  oTempCar.showColor = showColor; // 共享函数对象
  return oTempCar;
}

var oCar1 = createCar("red",,);
var oCar2 = createCar("blue",,);

oCar1.showColor();      //输出 "red"
oCar2.showColor();      //输出 "blue"
           

在createCar()内部,赋予对象一个指向已经存在的showColor()函数的指针。从功能上讲,这样解决了重复创建函数对象的问题;但是从语义上讲,该函数不太像是对象的方法。

构造函数方式

创建构造函数就像创建工厂函数一样容易。第一步选择类名,即构造函数的名字。根据惯例,这个名字的首字母大写,以使它与首字母通常是小写的变量名分开。除了这点不同,构造函数看起来很像工厂函数。请考虑下面的例子:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.showColor = function() { // 每次都会创建函数对象
    alert(this.color);
  };
}

var oCar1 = new Car("red",,);
var oCar2 = new Car("blue",,);
           

下面为您解释上面的代码与工厂方式的差别。首先在构造函数内没有创建对象,而是使用 this 关键字。使用 new 运算符构造函数时,在执行第一行代码前先创建一个对象,只有用 this 才能访问该对象。然后可以直接赋予 this 属性,默认情况下是构造函数的返回值(不必明确使用 return 运算符)。

就像工厂函数,构造函数会重复生成函数,为每个对象都创建独立的函数版本。不过,与工厂函数相似,也可以用外部函数重写构造函数,同样地,这么做语义上无任何意义。这正是下面要讲的原型方式的优势所在。

原型方式(prototype)

该方式利用了对象的 prototype 属性,可以把它看成创建新对象所依赖的原型。

这里,首先用空构造函数来设置类名。然后所有的属性和方法都被直接赋予 prototype 属性。我们重写了前面的例子,代码如下:

function Car() {
}

Car.prototype.color = "blue";
Car.prototype.doors = ;
Car.prototype.mpg = ;
Car.prototype.showColor = function() {
  alert(this.color);
};

var oCar1 = new Car();
var oCar2 = new Car();
           

调用 new Car() 时,原型的所有属性都被立即赋予要创建的对象,意味着所有 Car 实例存放的都是指向 showColor() 函数的指针。从语义上讲,所有属性看起来都属于一个对象,因此解决了前面两种方式存在的问题。

原型方式看起来是个不错的解决方案。遗憾的是,它并不尽如人意。

首先,这个构造函数没有参数。

真正的问题出现在属性指向的是对象,而不是函数时。函数共享不会造成问题,但对象共享却会造成问题。请思考下面的例子:

function Car() {
}

Car.prototype.color = "blue";
Car.prototype.doors = ;
Car.prototype.mpg = ;
Car.prototype.drivers = new Array("Mike","John");
Car.prototype.showColor = function() {
  alert(this.color);
};

var oCar1 = new Car();
var oCar2 = new Car();

oCar1.drivers.push("Bill");

alert(oCar1.drivers);   //输出 "Mike,John,Bill"
alert(oCar2.drivers);   //输出 "Mike,John,Bill"
           

混合的构造函数/原型方式

由于创建对象时有这么多问题,你一定会想,是否有种合理的创建对象的方法呢?

答案是有,需要联合使用构造函数和原型方式。

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");  // 避免对象共享
}

Car.prototype.showColor = function() {      // 使用函数共享
  alert(this.color);
};

var oCar1 = new Car("red",,);
var oCar2 = new Car("blue",,);

oCar1.drivers.push("Bill");

alert(oCar1.drivers);   // 输出 "Mike,John,Bill"
alert(oCar2.drivers);   // 输出 "Mike,John"
           

动态原型方法(_initialized)

对于习惯使用其他语言的开发者来说,使用混合的构造函数/原型方式感觉不那么和谐。毕竟,定义类时,大多数面向对象语言都对属性和方法进行了视觉上的封装。

动态原型方法的基本想法与混合的构造函数/原型方式相同,唯一的区别是赋予函数属性的位置。下面是用动态原型方法重写的 Car 类:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");

  if (typeof Car._initialized == "undefined") { // 保证函数属性只定义一次
    Car.prototype.showColor = function() {
      alert(this.color);
    };

    Car._initialized = true;                    // 定义后,"typeof"运算符返回"boolean"
  }
}
           

直到检查typeof Car._initialized是否等于”undefined”之前,这个构造函数都未发生变化。这行代码是动态原型方法中最重要的部分。如果这个值未定义,构造函数将用原型方式继续定义对象的方法,然后把Car._initialized设置为true。如果这个值定义了(它的值为true时,typeof的值为boolean),那么就不再创建该方法。

如前所述,目前使用最广泛的是混合的构造函数/原型方式。此外,动态原始方法也很流行,在功能上与构造函数/原型方式等价。可以采用这两种方式中的任何一种。

修改对象

prototype 属性不仅可以定义构造函数的属性和方法,还可以为本地对象添加属性和方法。

创建新方法

通过已有的方法创建新方法

Number.prototype.toHexString = function() {
  return this.toString();
};
           

重命名已有方法

Array.prototype.enqueue = function(vItem) {
  this.push(vItem);
};

Array.prototype.dequeue = function() {
  return this.shift();
};
           

添加与已有方法无关的方法

Array.prototype.indexOf = function (vItem) {
  for (var i=; i<this.length; i++) {
    if (vItem == this[i]) {
      return i;
    }
  }

  return -;
}
           

为所有本地对象添加新方法

所有本地对象都继承了 Object 对象,所以对 Object 对象做任何改变,都会反应在所有本地对象上。

Object.prototype.showValue = function () {
  alert(this.valueOf());
};

var str = "hello";
var iNum = ;
str.showValue();        // 输出 "hello"
iNum.showValue();       // 输出 "25"
           

重定义已有方法

如前面的章节所述,函数名只是指向函数的指针,因此可以轻松地指向其他函数。

Function.prototype.toString = function() {
  return "Function code hidden";
}
           

不过,toString()指向的原始函数怎么了呢?

它将被无用存储单元回收程序回收,因为它被完全废弃了。没有能够恢复原始函数的方法,所以在覆盖原始方法前,比较安全的做法是存储它的指针,以便以后的使用。有时你甚至可能在新方法中调用原始方法:

Function.prototype.originalToString = Function.prototype.toString;

Function.prototype.toString = function() {
  if (this.originalToString().length > ) {
    return "Function too long to display.";
  } else {
    return this.originalToString();
  }
};
           

极晚绑定(Very Late Binding)

在大多数程序设计语言中,必须在实例化对象之前定义对象的方法。 ECMAScript允许在对象实例化后再定义它的方法。

var o = new Object();

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

o.sayHi();
           

注意:不建议使用极晚绑定方法,因为很难对其跟踪和记录。不过,还是应该了解这种可能。

更多请参考:W3School