js高级第三版(第六章 面向对象的程序设计)

    面向对象(Object-Oriented,OO)的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。

    ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”

    6.1 理解对象

    属性在创建时都带有一些特征值(characteristic),JavaScript 通过这些特征值来定义它们的行为。

    6.1.1 属性类型

    ECMA-262 第 5 版在定义只有内部才用的特性(attribute)时,描述了属性(property)的各种特征。

    ECMA-262 定义这些特性是为了实现 JavaScript 引擎用的,因此在 JavaScript 中不能直接访问它们。

    为了表示特性是内部值,该规范把它们放在了两对儿方括号中,例如[[Enumerable]]。

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

    1. 数据属性

    数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特性。

     [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true

     [[Enumerable]]:表示能否通过 for-in 循环返回属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true

     [[Writable]]:表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true

     [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为 undefined

    修改属性默认的特性,必须使用 ECMAScript 5 的 Object.defineProperty()方法。

     Object.defineProperty()方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。

    其中,描述符(descriptor)对象的属性必须是:configurable、enumerable、writable 和 value。

    设置其中的一或多个值,可以修改对应的特性值。

    在严格模式下writable 之外的特性都会导致错误。

    2. 访问器属性

    访问器属性不包含数据值;它们包含一对儿 getter 和 setter 函数(不过,这两个函数都不是必需的)。

    访问器属性有如下 4 个特性。

     [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为true。

     [[Enumerable]]:表示能否通过 for-in 循环返回属性。对于直接在对象上定义的属性,这个特性的默认值为 true。

     [[Get]]:在读取属性时调用的函数。默认值为 undefined。

     [[Set]]:在写入属性时调用的函数。默认值为 undefined。

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

        属性前面加下划线是一种常用的记号,用于表示只能通过对象方法访问的属性。如:_year等

        同时此属性将则包含一个getter 函数和一个 setter 函数。

        getter 函数返回_year 的值,setter 函数通过计算来确定正确的版本。

        修改getter和setter函数需要通过Object.defineProperty()中的get:function()和set:function(value)来修改。

    6.1.2 定义多个属性    

    ECMAScript 5 中定义了一个 Object.defineProperties()方法,用于为对象定义多个属性。

    这个方法接收两个对象参数:

    第一个对象是要添加和修改其属性的对象,

    第二个对象的属性与第一个对象中要添加或修改的属性一一对应。

    支持 Object.defineProperties()方法的浏览器有 IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。

    6.1.3 读取属性的特性

    使用 ECMAScript 5 的 Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。

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

    返回值是一个对象

    如果是访问器属性,这个对象的属性有 configurable、enumerable、get 和 set;

    如果是数据属性,这个对象的属性有 configurable、enumerable、writable 和 value。

    在 JavaScript 中,可以针对任何对象——包括 DOM 和 BOM 对象,使用 Object.getOwnPropertyDescriptor()方法。支持这个方法的浏览器有IE9+、Firefox 4+、Safari 5+、Opera 12+和 Chrome。

    6.2 创建对象

    6.2.1 工厂模式(解决大量重复代码书写问题)

    工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

    6.2.2 构造函数模式

    构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。

   方法一: 在function中定义变量:

       this.sayName = new Function("alert(this.name)"); // 与声明函数在逻辑上是等价的

    方法二:通过把函数定义转移到构造函数外部来解决创建多个完成同样任务的 Function 实例。

        在function中定义this.sayName = sayName;

        然后创建

        function sayName(){ 

            alert(this.name);

        }

    方法二中对象就共享了在全局作用域中定义的同一个sayName()函数。

    可是新问题又来了:

        在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。

        如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。

    6.2.3 原型模式

        我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

        不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中

        使用 “函数.prototype.属性” 来添加数据。

    1. 理解原型对象

        无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype属性,这个属性指向函数的原型对象。

        在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。

      这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。

        理解:函数.prototype.constructor  是指针,这个指针的指向是 函数本身

                  函数.prototype指向的是 函数原型。

                   函数.prototype??==函数原型??   考虑中

    虽然在所有实现中都无法访问到[[Prototype]],但可以通过 isPrototypeOf()方法来确定对象之间是否存在这种关系。从本质上讲,如果[[Prototype]]指向调用 isPrototypeOf()方法的对象

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

    支持这个方法的浏览器有 IE9+、Firefox 3.5+、Safari 5+、Opera 12+和 Chrome

    虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。

    换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。

    使用 delete 操作符删除了添加的属性时,属性会指向原型中的属性值。

    使用 hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘了它是从 Object 继承来的)只在给定属性存在于对象实例中时,才会返回 true。

    ECMAScript 5 的 Object.getOwnPropertyDescriptor()方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用 Object.getOwnPropertyDescriptor()方法。

    2. 原型与 in 操作符

    有两种方式使用 in 操作符:

    单独使用和在 for-in 循环中使用。

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

    由于 in 操作符只要通过对象能够访问到属性就返回 true,hasOwnProperty()只在属性存在于实例中时才返回 true,因此只要 in 操作符返回 true 而 hasOwnProperty()返回 false,就可以确定属性是原型中的属性。

    在使用 for-in 循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。

    屏蔽了原型中不可枚举属性(即将[[Enumerable]]标记为 false 的属性)的实例属性也会在 for-in 循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的——只有在 IE8 及更早版本中例外。

    要取得对象上所有可枚举的实例属性,可以使用 ECMAScript 5 的 Object.keys()方法。

    Object.keys()方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。

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

    注意结果中包含了不可枚举的 constructor 属性。Object.keys()和 Object.getOwnPropertyNames()方法都可以用来替代for-in 循环。支持这两个方法的浏览器有 IE9+、Firefox 4+、Safari 5+、Opera12+和 Chrome。

    3. 更简单的原型语法

    函数.prototype={属性:值,属性:值,.....}

    上式中的值可以是数据,也可以是function,当属性是function的时候回出现问题

    当值是function的时候尽量使用 函数.prototype.function名={代码}定义

    注意上式中要添加属性:constructor : 函数名,

    或者使用Object.defineProperty()。添加constructor的指向

    4. 原型的动态性

    尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。

    调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。

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

    5. 原生对象的原型

    原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。

    通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。

    注:尽管可以这样做,但我们不推荐在产品化的程序中修改原生对象的原型。如果因某个实现中缺少某个方法,就在原生对象的原型中添加这个方法,那么当在另一个支持该方法的实现中运行代码时,就可能会导致命名冲突。而且,这样做也可能会意外地重写原生方法。

    6. 原型对象的问题

    首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。原型模式的最大问题是由其共享的本性所导致的。

    原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说得过去,毕竟(如前面的例子所示),通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题就比较突出了。

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

    创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。

    6.2.5 动态原型模式

    对function进行判断,使方法在不存在的情况下,才会将它添加到原型中。

    使用动态原型模式时,不能使用对象字面量重写原型,会切断现有实例与新原型之间的联系。

    6.2.6 寄生构造函数模式

    寄生(parasitic)构造函数模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数。  

function ddd(属性){
   var a=new Object();  //这里可以是对象也可以是数组
   a.属性=值;
   return a;
}
    说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;

             也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。

    6.2.7 稳妥构造函数模式

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

        一是新创建对象的实例方法不引用 this;

        二是不使用 new 操作符调用构造函数。

  6.3 继承        

     ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的。

  6.3.1 原型链  

    ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。

    其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

    简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。

    通过实现原型链,本质上扩展了本章前面介绍的原型搜索机制。

    1. 别忘记默认的原型

    所有引用类型默认都继承了 Object,而这个继承也是通过原型链实现的。

    所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype。这也正是所有自定义类型都会继承 toString()、valueOf()等默认方法的根本原因。

    2. 确定原型和实例的关系

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

    第一种方式是使用 instanceof 操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回 true。

    第二种方式是使用 isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此 isPrototypeOf()方法也会返回 true。

    3. 谨慎地定义方法

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

    4. 原型链的问题

    最主要的问题来自包含引用类型值的原型。实例属性变成了现在的原型属性。

    第二个问题:在创建子类型的实例时,不能向超类型的构造函数中传递参数。

    6.3.2 借用构造函数

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

    这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数

    函数只不过是在特定环境中执行代码的对象,因此通过使用 apply()和 call()方法也可以在(将来)新创建的对象上执行构造函数。

    1. 传递参数

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

    2. 借用构造函数的问题

    方法都在构造函数中定义,因此函数复用就无从谈起了。

    在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。

    6.3.3 组合继承

    组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。

    思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

    组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且,instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象。    

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.constructor = SubType;
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 

    组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且,instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象。

    6.3.4 原型式继承

    ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。

    这个方法接收两个参数:

        一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。

        在传入一个参数的情况下,Object.create()与 object()方法的行为相同。

        Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。

        以这种方式指定的任何属性都会覆盖原型对象上的同名属性。

支持 Object.create()方法的浏览器有 IE9+、Firefox 4+、Safari 5+、Opera 12+和 Chrome。

    6.3.5 寄生式继承

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

    使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。

    6.3.6 寄生组合式继承

    组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

    所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示。    

function inheritPrototype(subType, superType){
 var prototype = object(superType.prototype); //创建对象
 prototype.constructor = subType; //增强对象
 subType.prototype = prototype; //指定对象
} 
    这个示例中的 inheritPrototype()函数实现了寄生组合式继承的最简单形式。

    这个函数接收两个参数:子类型构造函数和超类型构造函数。

    在函数内部,

    第一步是创建超类型原型的一个副本。

    第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。

    最后一步,将新创建的对象(即副本)赋值给子类型的原型。

    这样,我们就可以用调用 inheritPrototype()函数的语句,去替换前面例子中为子类型原型赋值的语句了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值