JavaScript 面向对象

理解对象


EAMCScript中没有类的概念,所以它与基于类的语言中的对象有所不同。
ECMAScript-262定义对象为:无序的集合,其属性可以包含基本值、对象或者函数。

属性类型

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

数据属性

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

  • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,能否修改属性为访问器属性。默认值为true。
  • [[Enumerable]]:表示能否通过for-in循环返回属性。默认值为true。
  • [[Writable]]:表示能否修改属性值。默认值为true。
  • [[Value]]:包含这个属性的值。默认值为undefined。

要修改属性默认的特性,必须使用ECMAScript5的Object.defineProperty()方法。这个方法接受三个参数:属性所在的对象,属性名和一个描述对象。

一旦把属性定义为不可配置,就不能再把它变回可配置了。
在调用Object.defineProperty()方法时,如果不指定,configurableenumerablewritable特性的默认值都是false。

由于实现的不彻底,所以不建议在IE8中使用Object.defineProperty()

访问器属性

访问器属性不包含数据值,它们包含一对gettersetter函数。读取时调用getter,写入时调用setter
访问器属性有4个描述其行为的特性:

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

访问器属性不能直接定义,必须使用Object.defineProperty()来定义。

var book = {
    _year:2004,
    edition:1
};
Object.defineProperty(book,"year",{
    get:function(){
        return this._year;
    },
    set:function(){
        if(newValue>2004){
            this._year = newValue;
            this.edition += newValue - 2004;
        }
    }
});

_year前面的下划线是一种常用的记号,用于表示只能通过对象方法访问的属性。
支持ECMAScript5的这个方法的浏览器有IE9+(IE8只是部分实现)、Firefox4+、Safari5+、Opera12+和Chrome。

定义多个属性

利用ECMAScript5中的Object.definedProperties()方法来通过描述符一次定义多个属性。

读取属性的特性

利用ECMAScript5中的Object.getOwnPropertyDescriptor()方法来取得给定属性的描述符。方法接受两个参数:属性所在的对象和属性名,返回值是一个对象。

创建对象


工厂模式

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

function createPerson(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(this.name);
    };
    return o;
}

var person1 = createPerson("a", 29, "s");
var person2 = createPerson("b",27 , "d");

构造函数模式

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

var person1 = new Person("a", 29, "s");
var person2 = new Person("b",27 , "d");

createPerson()Person()的不同之处:

  • 没有显示地创建对象
  • 直接将属性和方法赋予this对象
  • 没有return语句

按照惯例,构造函数始终以大写字母开头。
调用构造函数会经历4个步骤:

  1. 创建一个对象
  2. 将构造函数的作用域赋给新对象(因此this指向了这个新的对象)
  3. 执行构造函数中的代码
  4. 返回新对象

对象都有一个constructor属性。对象的constructor属性用来标识对象的,检测对象还是用instanceof更好一些。

以这种方式定义的构造函数是定义在Global对象(在浏览器中是windows对象)中的。

将构造函数当做函数
任何函数,通过new来调用就作为一个构造函数,不通过new来调用就和普通函数没什么两样。

//当做构造函数来使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName();
//当做普通函数来使用
Person("Greg", 27, "Doctor"); //添加到window
windows.sayName():
//在另一个对象的作用域中调用
var o = new Object();
Person.call(o,"Kristen", 25, "Nurse");
o.sayName();

构造函数的问题
使用构造函数的主要问题是每个方法都要在每个实例上重新创建一遍。
可以将函数定义转移到构造函数外部来解决这个问题:

function Person(name, age, obj){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}
function sayName(){
    alert(this.name);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

上面的问题解决了,可是新的问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,如果方法很多就要定义很多的全局函数于是就没有封装性可言了。好在,可以通过使用原型模式来解决。

原型模式

创建的每个函数都有一个prototype属性,这个属性是一个指针,指向对象实例的原型对象。

使用原型的好处就是可以让所有对象实例共享它所包含的属性和方法。

function Person(){
}

Person.prototype.name = "a";
Person.prototype.age = 29;
Person.prototype.job = "Software";
Person,prototype.sayName = function(){
    alert(this.name);
};

var person1 = new Person();
person1.sayName(); //a

var person2 = new Person();
person2.sayName(); //a
alert(person1.sayName == person2.sayName); //true

理解原型对象
创建一个新函数就会根据一组特定的规则为函数创建一个prototype属性,这个属性指向函数的原型。在默认情况下,所有的原型对象都会自动获得一个constructor属性,这个属性指向包含prototype属性所在的函数指针。
创建了自定义的构造函数以后,其原型对象默认只会获得constructor属性,其他方法则从Object继承而来。

通过isPrototypeOf()来判断对象的原型。

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

ECMAScript5增加了一个新方法,叫Object.getPrototypeOf(),在所有支持的实现中,返回[[Prototype]]的值。

alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //a

可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。
如果在实例中添加了一个属性,且属性名和原型中的一个属性同名,那么会在实例中创建该属性并且屏蔽原型中的同名属性。不过可以使用delete操作符来删除实例属性,从而重新访问到原型中的属性。

使用hasOwnProperty()检测一个属性是存在与实例中还是存在于原型中。

原型与in操作符
可以单独使用和在for-in循环中使用。
单独使用in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。

function Person () {
}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    alert(this.name);
};

var person1 = new Person();
var person2 = new Person();

alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true

person1.name = "Greg";
alert(person1.name); //"Greg" ------来自实例
alert(person1.hasOwnProperty("name")); //true
alert("name" in person1); //true

alert(person2.name);  //"Nicholas" ------来自原型
alert(person2.hasOwnProperty("name")); //false
alert("name" in person2); //true

delete person1.name;
alert(person1.name);  //"Nicholas" ------来自原型
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true

同时使用hasOwnProperty()in可以确定属性存在于对象上还是原型上。

function hasPrototypeProperty(object, name) {
    return !object.hasOwnProperty(name) && (name in object);
}

在使用for-in循环时,返回的是所有能够通过对象访问的、可枚举(enumerated)属性,其中即包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumeralbe]]标记的属性)的实例属性也会在for-in循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的---只有在IE8及更早的版本例外。
IE早期版本的实现中存在一个bug,即屏蔽不可枚举属性的实例属性不会出现在for-in循环中。

var o = {
    toString : function () {
        return "My Object";
    },
};
for (var prop in o) {
    if (prop == "toString") {
        alert("Found toString"); //在IE中不会显示
    }
}

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

function Person() {
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    alert(this.name);
};

var keys = Object.keys(Person.prototype);
alert(keys); //"name,age,job,sayName"

var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
alert(p1keys); //"name,age"

如果你想要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法。

var keys = Object.getOwnPropertyNames(Person, prototype);
alert(keys); //"constructor,name,age,job,sayName"

更简单的原型语法
更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。

function Person(){
}
Person.prototype = {
    name : "a",
    age : 29,
    job : "Software",
    sayName : function(){
       alert(this.name);
    }
};

以这种形式创建的新对象有一个例外:constructor属性不在指向Person,而指向Object

如果constructor属性很重要,可以将它设置回适当的值。

function Person(){
}
Person.prototype = {
    constructor : Person,
    name : "a",
    age : 29,
    job : "Software",
    sayName : function(){
       alert(this.name);
    }
};

但是这种方式会导致constructor属性的[[Enumerable]]特性被设置为true。默认情况下,原生的constructor不可枚举。
使用兼容ECMAScript的引擎可以用下面方法解决:

function Person(){
}
Person.prototype = {
    name : "a",
    age : 29,
    job : "Software",
    sayName : function(){
       alert(this.name);
    }
};
Object.defineProperty(Person.prototype,"constructor",{
    enumerable : false,
    value : Person
}); 

原型的动态性
对原型对象所做的任何修改都能够立刻从实例上反应出来。

var friend = new Person();
Person.prototype.sayHi = function(){
    alert("Hi");
};
friend.sayHi(); //Hi

但是如果重写整个原型对象,情况就不一样了。

function Person() {
}
var friend = new Person();
Person.prototype = {
    constructor : Person,
    name : "Nicholas",
    age : 29;
    job : "Software";
    sayName : function () {
         alert(this.name);
    }
};
friend.sayName(); //error

原生对象的原型
所有的原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新的方法。

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

原型对象的问题
原型模式也不是没有缺点。原型模式最大的问题是由其共享的本性所导致的。
原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说得过去,通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值得属性来说,问题就比较突出了。

function Person() {
}
Person.prototype = {
    constructor : Person,
    name : "Nicholas",
    age : 29,
    job : "Software",
    sayName : function () {
        alert(this.name);
    }
};

var person1 = new Person();
var person2 = new Person();

person1.friends.push("Van");

alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van"
alert(person1.friends === person2.friends); //true

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

创建自定义类型最常见的方式是组合使用构造函数模式和原型模式。构造函数定义实例属性,原型模式用于定义方法和共享的属性。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}

Person.prototype = {
    constructor : Person,
    syaName : function(){
        alert(this.name);
    }
}

动态原型模式

动态原型模式把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要情况下),又保持了同时使用构造函数和原型的优点。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    if (typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            alert(this.name);
        }
    }
}

寄生构造函数的模式

通常,在前面几种模式都不适用的情况下,可以使用寄生(parasitic)构造函数模式。这种函数的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。

function createPerson(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(this.name);
    };
    return o;
}

var friend = new Person("a", 29, "soft");
friend.sayName();

这个模式跟工厂模式很像。构造函数在不返回值得情况下,默认返回新对象实例,而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。
这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用这个模式。

function SpecialArray () {
    //创建数组
    var values = new Array();

    //添加值
    values.push.apply(values, arguments);

    //添加方法
    values.toPipedString = function () {
        return this.join("|");
    };

    //返回数组
    return values;
}

var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipenString()); //"red|blue|green"

稳妥构造函数模式

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

function Person (name, age, job) {
    //创建要返回的对象
    var o = new Object();

    //可以在这里定义私有变量和函数

    //添加方法
    o.sayName = function () {
        alert(name);
    };

    //返回对象
    return o;
}

注意,在以这种模式创建的对象中,除了使用sayName()之外,没有其他办法访问name的值。

var friend = Person("Nicholas", 29, "Software");
friend.sayName(); //"Nicholas"

继承


许多的OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名,在ECMAScript中无法实现接口继承。ECMAScript只支持实现继承,而且其继承主要是依靠原型链来实现。

原型链

实现原型链有一种基本模式:

function SuperType() {
    this.prototype = true;
}
Super.prototype.getSuperValue = function() {
    return this.property;
};
function SubType() {
    this.subproperty = false;
}
//继承了SuperType
SubType.prototype = new SuperType();

SubType.prototype.getSubValue() = function () {
    return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true

别忘记默认的原型
所有的引用默认都继承了Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。

确定原型和实例的关系
通过两种方式来确定原型和实例之间的关系:

  • 使用instanceof操作符检测实例和原型中出现过的构造函数,结果返回true。
alert(instance instanceof Object); //true
  • 使用isPrototypeOf()方法。
alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true

谨慎的定义方法
子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎么样,给原型添加方法的代码一定要放在替换原型的语句之后。

function SuperType() {
    this.property = true;
}
SuperType.prototype.getSuperValue = function () {
    return this.property;
};
function SubType () {
    this.subproperty = false;
}

//继承了SuperType
SubType.prototype = new SuperType();

//添加新的方法
SubType.prototype.getSubValue = function () {
    return this.subproperty;
};

//重写超类型中的方法
SubType.prototype.getSuperValue = function () {
    return false;
};

var instance = new SubType();
alert(instance.getSuperValue()); //false

这里要注意的是,必须在用SuperType的实例替换原型以后,再定义这两个方法。并且通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样会重写原型链。

function SuperType() {
    this.property = true;
}
SuperType.prototype.getSuperValue = function () {
    return this.property;
};
function SubType () {
    this.subproperty = false;
}

//继承了SuperType
SubType.prototype = new SuperType();

//使用字面量添加新方法,会导致上一行代码无效
SubType.prototype = {
    getSubValue : function () {
        return this.subproperty;
    },
    someOtherMethod : function () {
        return false;
    }
};

var instance = new SubType();
alert(instance.getSuperValue()); //false

原型链的问题
原型链虽然很强大,可以用它来实现继承,但也存在一些问题。最主要的问题来自包含引用类型值的原型。

function SuperType () {
    this.colors = ["red", "blue", "green"];
}
function SubType () {
}

//继承了SuperType
SubType.prototype = new Super();

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green,black"

原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

借用构造函数

在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数(constructor stealing)的技术(有时候也叫做伪造对象或经典继承)。即在子类型构造函数的内部调用超类型构造函数。

function SuperType () {
    this.colors = ["red", "blue", "green"];
}
function SubType () {
    //继承了SuperType
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"

传递参数
对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。

function SuperType (name) {
    this.name = name;
}

function SubType () {
    //继承了SuperType,同时还传递了参数
    SuperType.call(this, "Nicholas");

    //实例属性
    this.age = 29;
}

var instance = new SubType();
alert(instance.name); //"Nicholas"
alert(instance.age); //29

借用构造函数的问题
如果仅仅是借用构造函数,那么也将无法避免构造函数存在的问题----方法都在构造函数中定义,因此函数复用就无从谈起了。

组合式继承

组合继承(combination inheritance),有时候也叫做伪经典继承。

function SuperType (name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function () {
    alert(this.name);
};
function SubType (name, age) {
    //继承属性
    SuperType.call(this, name);

    this.age = age;
}

//继承方法
SubType.prototype = new SuperType();

SubType.prototype.sayAge = function () {
    alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas"
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg"
instance2.sayAge(); // 27

原型式继承

function object (o) {
    function F () {}
    F.prototype = o;
    return new F();
}

在object()函数内部,先创建了一个临时性的构造函数,然后传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,objcet()对传入其中的对象执行了一次浅复制。

var person = {
    name : "Nicholas",
    friends : ["Shelby", "Court", "Van"]
};
var anotherPerson = objcet(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson.name = objcet(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"

ECMAScript5通过新增Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与objcet()方法的行为相同。

var person = {
    name : "Nicholas",
    friends : ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson.name = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"

寄生式继承

寄生式继承创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

function createAnother (original) {
    var clone = object(original); //通过调用函数创建一个新对象
    clone.sayHi = function () { //以某种方式来增强这个对象
        alert("hi");
    };
    return clone; //返回这个对象
}

你可能感兴趣的:(JavaScript 面向对象)