第三章 对象基础
这一章主要有五大要点:
- 对象引用
- 对象的类型
- 关键字this
- 定义类或对象
- 修改对象
下面为你一 一道来。
首先普及一下面向对象语言四种基本能力:
- 封装——把相关的信息(无论属性或方法)储存在对象中的能力。
- 聚集——把一个对象存储在另一个对象内的能力。
- 继承——由另一个类(或多个类)得来类的属性和方法的能力。
- 多态——编写能以多种方法运行的函数或方法的能力。
另外,对象的创建或销毁都在JavaScript执行过程中发生,理解这种范式的含义对理解整个语言至关重要。
3.1 对象引用
在ECMAScript中,不能访问对象的物理表示,只能访问对象的引用。每次创建对象,存储在变量中的都是该对象的引用,而不是对象本身。
- 对象废除
ECMAScript有无用存储单元收集程序,意味着不必专门销毁对象来释放内存。当再没有对象的引用时,称该对象被废除(dereference)了。
每当函数执行完它的代码,无用存储单元收集程序都会运行,释放所有的局部变量。还有在一些其它不可预知的情况下,无用存储单元收集程序也会运行。
把对象的所有引用都设置为null,可以强制性的废除对象。例如:
var object = new Object();
//do something with the object here
object = null;
每用完一个对象后,就将其废除来释放内存,这是个好习惯。
- 早绑定和晚绑定
所谓绑定(binding),即把对象的接口和对象实例结合在一起的方法。
早绑定(early binding)是指在实例化对象之前定义它的属性和方法,像Java和Visual Basic都支持。但ECMAScript不是强类型语言,所以不支持早绑定。
晚绑定(late binding)指的是编译器或解释程序在运行前,不知道对象的类型。使用晚绑定,无需检查对象的类型,只需要检查对象是否支持属性和方法即可。
ECMAScript中的所有变量都采用晚绑定方法,这样就允许执行大量的对象操作,而无任何报错。
3.2 对象的类型
一般来说,可以创建并使用的对象有三种:
- 本地对象; 本地对象就是ECMA-262定义的类(引用类型)。它们包括:Object,Function,Array,String,Boolean,Number,Date,RegExp,Error,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError。
- 内置对象; ECMA-262之定义了两个内置对象,即Global和Math(它们也是本地对象),同时开发者不必明确实例化内置对象,它已被实例化了。
- 宿主对象; 所有非本地对象都是宿主对象。
3.3 关键字this
在ECMAScript中,要掌握的最重要的概念之一是关键字this的用法,它用在对象的方法中。关键字this总是指向调用该方法的对象,例如:
var car = new Object();
car.color = "red";
car.showColor = function(){
alert(this.color); //outputs "red"
}
在此环境中,this等于car,即:
car.showColor = function(){
alert(car.color); //outputs "red"
}
注意,引用对象的属性时,必须使用this关键字。例如,如果采用如下的代码,则showColor()方法不会运行:
function showColor(){
alert(color);
}
如果不用对象或this关键字引用变量,ECMAScript就会把它们看做局部变量或全局变量。然后该函数将查找名为color的局部或全局变量,但是不会找到的。
那结果如何,该函数将在警告中显示“null”。
3.4 定义类或对象
使用预定义对象的能力只是面向对象语言的能力的一部分,它真正的强大之处在于能够创建自己专用的类和对象。
与ECMAScript中的许多特性一样,可以用各种方法实现这一点。
- 工厂方式;
因为对象的属性可以在对象创建后动态定义,所以许多开发者都是在初次引入javascript时编写类似下面的代码:
var car = new Object();
car.color = "red";
car.door = 4;
car.mpg = 23;
car.showColor = function(){
alert(this.color);
};
最后一个属性实际上是指向函数的指针,意味着该属性是个方法。执行这段代码后,就可以使用对象car。问题是可能需要创建多个car实例。
要解决此问题,开发者创造了能创建并返回特定类型的对象的工厂函数。例如:
function createCar(){
var car = new Object();
car.color = "red";
car.doors = 4;
car.mpg = 23;
car.showColor = function(){
alert(this.color);
};
return car;
}
var car1 = createCar();
var car2 = createCar();
调用此函数时,将创建新对象,并赋予它们所有必要的属性,复制一个前面说明的car对象。当然,还可以修改createCar()函数,给它传递参数而不是赋予属性默认值。
另外,每个对象都共享了同一个函数。
- 构造函数方式; 构造函数看起来很像工厂函数。如下面的例子:
function Car(color, doors, mag){ this.color = color; this.doors = doors; this.mpg = mpg; this.showColor = function(){ alert(this.color); }; } var car1 = new Car("red",4,33); var car2 = new Car("blue",2,133);
你可能注意到差别了,在构造函数内部无创建对象,而是使用this关键字。
- 原型方式; 该方式利用了对象的prototype属性,可把它看成创建新对象所依赖的原型。
重写前面的例子,代码如下所示:
function Car(){
}
Car.prototype.color = "red";
Car.prototype.doors = 4;
Car.prototype.mpg = 23;
Car.prototype.showColor = function(){
alert(this.color);
}
var car1 = new Car();
var car2 = new Car();
首先定义构造函数(Car)无任何代码,接下来通过给Car的prototype属性添加属性去定义Car对象的属性。
从语义上讲,所有属性看起来都属于一个对象。此外,使用该方法,还能用instanceof运算符检查给定变量指向的对象的类型。
但遗憾的是,它并不尽如人意。
函数共享不会造成任何问题,但对象却是很少被多个实例共享的。当一个实例修改属性或方法时,其它对象的属性或方法也被同时修改。
那是否有种合理的创建对象的方法呢?答案是有,需要联合使用构造函数和原型方式。
- 构造函数/原型方式;
联合使用构造函数和原型方式,这种概念非常简单。即用构造函数定义对象的所有非函数属性,用原型方式定义对象的函数属性(方法)。
结果所有函数都只创建一次,而每个对象都具有自己的对象属性实例。再重写前面的例子:
function Car(color, doors, mpg){
this.color = color;
this,doors = doors;
this.mpg = mpg;
this.drivers = new Array("Mike","Sue");
}
Car.prototype.showColor = function(){
alert(this.color);
}
var car1 = new Car("red",4,33);
var car2 = new Car("red",2,133);
car1.drivers.push("Mark");
alert(car1.drivers); //outputs "Mike,Sue,Mark"
alert(car2.drivers); //outputs "Mike,Sue"
同时因为只创建showColor()函数的一个实例,所以没有内存浪费。