面向对象的程序设计(一)

来源:互联网 发布:淘宝入驻村淘的的条件 编辑:程序博客网 时间:2024/05/21 15:38

一、理解对象

1、对象的定义(ECMA-262定义)

无序属性的集合,其属性可以包含基本值、对象或者函数。

2、属性(property)

1)数据属性

包含一个数据值的位置,在这个位置可以读取和写入值。

有4个描述其行为的特性:

  • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。默认值为true。
  • [[Enumerable]]:表示能否通过for-in循环返回属性。默认值为true。
  • [[Writable]]:表示能否修改属性的值。默认值为true。
  • [[Value]]:包含属性的数据值。读取属性值时,从这个位置读;写入属性值时,把新值保存在这个位置。默认值为undefined。

修改属性默认特性的方法:Object.defineProperty()方法(ECMAScript 5)。
该方法接收3个参数:属性所在的对象、属性的名字和一个描述符对象。
其中描述符对象的属性必须是configurable、enumerable、writable和value。

在调用Object.defineProperty()方法创建一个新的属性时,如果不指定,configurable、enumerable和writable特性的默认值都是false。

2)访问器属性

  • 不包含数据值,包含一对getter和setter函数(不必需)。
  • 不能直接定义,必须使用Object.defineProperty()来定义。

有4个特性:

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

3、定义多个属性

方法:Object.defineProperties()

该方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加与修改的属性一一对应。

4、读取属性的特性

方法:Object.getOwnPropertyDescriptor()

该方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。

返回值是一个对象。如果是访问属性,这个对象的属性是configurable、enumerable、get和set;如果是数据属性,这个对象的属性有configurable、enumerable、writable和value。

二、创建对象

使用Object构造函数或对象字面量来创建单个对象的缺点是:使用同一个接口创建很多对象,会产生大量的重复代码。
为解决这个问题,可以使用下面的模式创建对象。

(一)工厂模式

使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。

function createPerson(name, age){    var p = new Object();    p.name = name;    p.age = age;    p.sayName = function(){        alert(this.name);    };    return p;}var person1 = createPerson("John", 20);var person2 = createPerson("Ben", 30);

这个模式解决了创建多个相似对象的问题,但没有解决对象识别问题,即怎么知道一个对象的类型。
这种模式后来被构造函数模式所取代。

(二)构造函数模式

可以创建自定义引用类型,可以像创建内置对象实例一样使用new操作符。

function Person(name, age){    this.name = name;    this.age = age;    this.sayName = function(){        alert(this.name);    };}var person1 = new Person("John", 20);var person2 = new Person("Ben", 30);

在上面两段代码中,可以看出,Person()与createPerson()的不同之处是:没有显示的创建对象;直接将属性和方法赋给了this对象;没有return语句。

使用new操作符创建一个Person实例。以这种方式调用构造函数会经历4个步骤:

  • 创建一个新对象;
  • 将构造函数的作用域赋给新对象,所以this指向了这个新对象;
  • 执行构造函数中的代码,为这个对象添加属性和方法;
  • 返回新对象。

1、构造函数与其他函数

两者的区别就在于调用它们的方式不同。构造函数通过new操作符调用。
按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。

//当作构造函数使用var person = new Person("John", 20);person.sayName();  // John//作为普通函数调用Person("Ben", 30);  // 添加到windowwindow.sayName();  // Benvar o = new Object();Person.call(o, "Greg", 17);o.sayName();  // Greg

2、构造函数的优缺点

1)构造函数模式胜过工厂模式的地方在于,创建自定义的构造函数意味着将来可以将它的实力标识为一种特定的类型。

2)使用构造函数的缺点:每个方法都要在每个实例上重新创建一遍,即它的每个成员都无法得到复用,包括函数。

在前面的例子中,person1和person2都有一个方法sayName()。由于ECMAScript中的函数是对象,那么没定义一次函数就实例化了一个对象,所以这两个方法不是同一个Function的实例。

function Person(name, age){    this.name = name;    this.age = age;    this.sayName = new Function("alert(this.name)");  //与声明函数在逻辑上是等价的}var person1 = new Person("John", 20);var person2 = new Person("Ben", 30);alert(person1.sayName == person2.sayName;  //false

以这种方式创建函数会导致不同的作用域链和标识符解析,但创建Function新实例的机制仍然是相同的。所以不同实例上的同名函数是不相等的。
像下面这样,通过把函数定义转移到构造函数外部来解决这个问题。

function Person(name, age){    this.name = name;    this.age = age;    this.sayName = sayName;}function sayName(){    alert(this.name);}

产生的新问题:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。如果对象需要定义很多方法,那么就要定义多个很多个全局函数,于是自定义的引用类型就毫无意义。

(三)原型模式

prototype(原型)属性是一个指针,指向一个对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
可以这么理解,prototype就是通过调用构造函数而创建的那个对象实例的原型对象。

使用原型对象的好处在于可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象的实例信息,而是将这些信息直接添加到原型对象中。

function Person(){}Person.prototype.name = "John";Person.prototype.age = 20;Person.prototype.sayName = function(){    alert(this.name);};var person1 = new Person();person1.sayName();  // Johnvar person2 = new Person();person2.sayName();  // Johnalert(person1.sayName == person2.sayName);  //true

1、原型对象

1)属性

  • prototype属性:指向函数的原型对象。只要创建了一个新函数,就会根据一组特定的规则为该函数创建这个属性。
  • constructor(构造函数)属性:是一个指向prototype属性所在函数的指针。默认情况下,所有原型对象都会自动获得。

2)方法

创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性,而其他方法都是从Object继承而来。
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版把这个指针叫[[Prototype]]。

A)isPrototypeOf():用于指示对象是否存在于另一个对象的原型链中。如果存在,返回true,否则返回false。

alert(Person.prototype.isPrototypeOf(person1));  // truealert(Person.prototype.isPrototypeOf(person2));  // true

B)Object.getPrototypeOf():ECMAScript 5 新增的方法,这个方法返回[[Prototype]]的值。

alert(Object.getPrototypeOf(person1) == Person.prototype);  // truealert(Object.getPrototypeOf(person1).name);  // John

可以通过对象实例访问保存在原型中的值,但不能通过对象实例重写原型中的值。
当为对象添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。但是,使用delete操作法可以完全删除实例属性。

C)hasOwnProperty():检测一个属性是存在于实例中,还是存在于原型中。从Object继承来的。只在给定属性存在于对象实例中时,才会返回true。

function Person(){}Person.prototype.name = "John";Person.prototype.age = 20;Person.prototype.sayName = function(){    alert(this.name);};var person1 = new Person();var person2 = new Person();alert(person1.hasOwnProperty("name"));  // falseperson1.name = "Greg";alert(person1.name);  // Greg——来自实例alert(person1.hasOwnProperty("name"));  // truealert(person2.name);  // John——来自原型alert(person2.hasOwnProperty("name"));  // falsedelete person1.name;alert(person1.name);  // John——来自原型alert(person1.hasOwnProperty("name"));  // false

D)Object.getOwnPropertyDescriptor():获取指定对象的自身属性描述符。自身属性描述符是指直接在对象上定义(而非从对象的原型继承)的描述符。

2、原型与in操作符

两种方式使用in操作符:

1)单独使用

in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原原型中。

function Person(){}Person.prototype.name = "John";Person.prototype.age = 20;Person.prototype.sayName = function(){    alert(this.name);};var person1 = new Person();var person2 = new Person();person1.name = "Greg";alert(person1.name);  // Greg——来自实例alert(person1.hasOwnProperty("name"));  // truealert("name" in person1);  // truealert(person2.name);  // John——来自原型alert(person2.hasOwnProperty("name"));  // falsealert("name" in person2);  // true

2)在for-in循环中使用

返回所有能够通过对象访问的、可枚举的属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。

不可枚举的属性和方法:

  • constructor、prototype
  • hasOwnProperty()、propertyIsEnumerable()、toLocaleString()、toString()和valueOf()

两个可以替代for-in循环的方法:

  • Object.keys():获取对象上所有可枚举的实例属性。该方法接收一个对象作为参数,返回一个包含所有枚举属性的字符串数组。
  • Object.getOwnPropertyNames():获取所有实例属性,无论是否可枚举。
function Person(){}Person.prototype.name = "John";Person.prototype.age = 20;Person.prototype.sayName = function(){    alert(this.name);};var keys = Object.keys(Person.prototype);alert(keys);  // name,age,sayNamevar p1 = new Person();p1.name = "Bob";p1.age = 31;var p1keys = Object.keys(p1);alert(p1keys);  // name,agevar keyss = Object.getOwnPropertyNames(Person.prototype);alert(keyss);  // constructor,name,age,sayName

3、更简单的原型语法

用一个包含所有属性和方法的对象字面量来重写整个原型对象。

function Person(){}Person.prototype = {    constructor : Person,    name : "John",    age : 20,    sayName : function(){        alert(this.name);    }};

在上述代码中,特意包含了一个constructor属性,并将它的值设为Person,这样就可以确保通过该属性能访问到适当的值。如果没有指定该属性,constructor属性会指向Object构造函数,而不是Person函数。

4、原型的动态性

实例与原型的连接是一个指针而不是一个副本。

把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。

实例中的指针仅指向原型,而不指向构造函数。

5、原生对象模型

通过原生对象的原型,不仅可以取得所有默认方法的引用,还可以定义新方法。

不推荐在产品化的程序中修改原生对象的原型。

6、原型对象的问题

  • 省略了为构造函数传递参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。
  • 原型中所有属性是被很多实例共享的,这一特性对于包含引用类型值的属性而言就会产生问题。
function Person(){}Person.prototype = {    constructor : Person,    name : "John",    age : 20,    friends : ["Bob", "Ben"],    sayName : function(){        alert(this.name);    }};var p1 = new Person();var p2 = new Person();p1.friends.push("Van");alert(p1.friends);  // Bob,Ben,Vanalert(p2.friends);  // Bob,Ben,Van

在上面代码中,可以发现,修改p1的值也会改变p2的值。如果我们的初衷是像这样在所有实例中共享一个数组,那么就没什么问题。但是,实例一般都是要有属于自己的全部属性的。而这个问题就是很少单独使用原型模式的原因所在。

(四)组合使用构造函数模式和原型模式

构造函数用于定义实例属性,原型模式用于定义方法和共享的属性。

function Person(name, age){    this.name = name;    this.age = age;    this.friends = ["Shelby", "Court"];}Person.prototype = {    constructor: Person,    sayName : function () {        alert(this.name);    }};var person1 = new Person("Nicholas", 29);var person2 = new Person("Greg", 27);person1.friends.push("Van");alert(person1.friends);    //"Shelby,Court,Van"alert(person2.friends);    //"Shelby,Court"alert(person1.friends === person2.friends);  //falsealert(person1.sayName === person2.sayName);  //true

优点:

  • 每一个实例都有自己的一份实例属性的副本,同时又共享着对方法的引用,最大限度地节省了内存。
  • 支持向构造函数传递参数。
  • 集两种模式之长。

目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。

(五)动态原型模式

把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
使用动态原型模式时,不能使用对象字面量重写原型。

function Person(name, age){    //属性    this.name = name;    this.age = age;    //方法    if (typeof this.sayName != "function"){        Person.prototype.sayName = function(){            alert(this.name);        };    }}var p = new Person("T",12);p.sayName();

(六)寄生构造函数模式

创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后返回新创建的对象。

function Person(name, age){    var o = new Object();    o.name = name;    o.age = age;    o.sayName = function(){        alert(this.name);    };        return o;}var p = new Person("T",12);p.sayName();

除了使用new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。

【说明】返回的对象与构造函数或者与构造函数的原型属性之间没有关系,也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖instanceof操作符来确定对象类型。

建议在可以使用其他模式的情况下不要使用这种模式。

(七)稳妥构造函数模式

稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。
稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this和new),或者防止数据被其他应用程序改动时使用。

稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:

  • 一是新创建对象的实例方法不引用this;
  • 二是不使用new操作符调用构造函数。
function Person(name, age){    //创建要返回的对象    var o = new Object();    //可以在这里定义私有变量和函数    //添加方法    o.sayName = function(){        alert(name);    };    //返回对象    return o;}var p = Person("T",12);p.sayName();

与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系。