面向对象(Object-Oriented,OO)的语言有一个标识,那就是有类的概念,通过类可以创建任意多个具有相同属性和方法的对象。
ECMAScript也是高度抽象的面向对象的语言,但它没有类的概念,所以它的对象也与基于类的语言中的对象有所不同。
ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数”。
我们可以把对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。
创建自定义对象的两种简单方式:
1.使用new操作符创建一个Object引用类型的新实例。它其实是调用了Object打的原生构造函数。
var person = new Object();
person.name = "zhu";
person.age = 24;
person.sayHello = function(){
alert("hello");
};
2.使用对象字面量表示法。这种方法要求的代码量较少,而且给人一种封装数据的感觉。
var person = {
name : "zhu";
age : 24;
sayHello : function(){
alert("hello");
};
};
Function Person(name,age){
this.name = name;
this.age = age;
this.sayHello = function(){
alert("hello");
};
}
var person = new Person("zhu",24);
构造函数与其他函数的唯一区别就是调用它们的方式不同。构造函数只是要通过new操作符来调用,如果不用new操作符来调用,那构造函数和普通函数也没什么区别。
如果把上面定义的Person()函数当做普通函数来调用:
Person("zhu","24");
window.sayHello(); //弹出框显示"hello"
当用上面的构造器函数来创建对象时,我们观察代码的红色部分,它定义了一个函数,由于在ECMAScript中函数也是对象,所以红色代码部分相当于实例化了一个新的函数对象。当我们创建了多个Person对象时,也就创建了多个sayHello函数对象,而这些函数都完成的是相同的任务,所以创建多个实在没有必要。我们可以把代码优化一下:
function Person(name,age){
this.name = name;
this.age = age;
this.sayHello =sayHello; //因为在ECMAScript中函数名本身就是一个变量,所以可以把这个函数作为这个person对象的一个属性值来使用,这里还没调用这个函数
}
function sayHello(){
alert("hello");
}
var person = new Person("zhu",24);
person.sayHello(); //弹出框显示hello
这样,无论我们创建多少个person对象,它们都共享了一个了在全局作用域中定义的同一个sayHello()函数。
但是这样也有新的问题,如果对象定义了很多方法,那么就要定义很多全局函数,导致我们自定义的这个引用类型就没有了封装性可言。由此可以引出对象中最重要的一个属性,原型-prototype。
我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
利用Person构造函数的原型属性,我们也可以把上面的例子改写成如下:
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function(){
alert("hello");
};
var person = new Person("zhu",24);
person.sayHello();
当我们用Person构造函数创建了很多实例person1,person2....时,每一个实例都有一个内部属性[[Prototype]],这个内部属性指向了构造函数的原型对象Person Prototype。注意,这个内部属性对脚本来说是隐藏的。但是FireFox、Safari、Chrome在每个对象上都支持一个属性__proto__(注意每边是双下划线,表示内部属性),但为了保证浏览器兼容性,一般也不直接使用这个属性,我是这么理解的。
如果我们在一个对象实例中添加了一个属性,该属性与原型对象中的一个属性同名,那么属性就是这个实例的属性,该属性会屏蔽原型对象中保存的同名属性,但不会修改原型对象,换句话说,它只是阻止了我们访问原型对象中的那个属性。例如:
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function(){
alert("hello");
};
var person1 = new Person("zhu",24);
//屏蔽原型对象中的保存的同名属性
person1.sayHello = function(){
alert("byebye");
};
person1.sayHello(); //弹出框显示“byebye”
var person2 = new Person("li",24);
//访问的是原型对象中的属性
person2.sayHello(); //弹出框显示“hello”
原型对象中的所有属性都是被很多实例共享的,这种共享对于函数来说十分合适,但对于一些包含引用类型值的属性来说,就不太适合放置在原型对象中了。我们上面例子中的Person对象,实际是组合使用了构造函数模式和原型模式来定义的,对于OO的编程来说,没有体现出封装的特点,所以我们也可以用
动态原型模式来解决这个问题,例如上面的例子又可以改写成:
function Person(name,age){
this.name = name;
this.age = age;
if (typeof this.sayName != "function") {
Person.prototype.sayHello = function(){
alert("hello");
};
}
}
var person1 = new Person("zhu",24);
//屏蔽原型对象中的保存的同名属性
person1.sayHello = function(){
alert("byebye");
};
person1.sayHello(); //弹出框显示“byebye”
var person2 = new Person("li",24);
//访问的是原型对象中的属性
person2.sayHello(); //弹出框显示“hello”
我们把所有信息都封装在了构造函数中,在需要的情况下在构造函数中初始化了原型对象,保持了同时使用构造函数和原型的优点。
原型的另一个关键作用是在原型链中来使用,而原型链是ECMAScript中实现继承的关键依靠。这个下一篇再写。