《JavaScript高级程序设计》:面向对象的程序设计

281 阅读17分钟

前言

本系列是在学习JS的过程中所做的读书笔记,将重点知识提取出来在闲暇时刻翻阅,如果你看过这本书,那收藏起来当复习用;如果你没看过这本书,那还犹豫啥,收藏起来抱佛脚用的啦~~~

注:内容摘抄或总结自《JavaScript高级程序设计》第三到七章


我们可以把ECMAScript的对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。

一、理解对象

1.属性类型

数据属性

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

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

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

  • 可以多次调用 Object.defineProperty()方法修改同一个属性,但在把 configurable 特性设置为 false 之后就会有限制了。

访问器属性

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

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

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

不一定非要同时指定 getter和 setter。但是只定义getter的属性尝试写入非严格模式下会被忽略,严格模式下会抛出错误。 只定义setter的属性尝试读取,非严格模式下会返回undefined,严格模式下抛出错误。

2.定义多个属性

由于为对象定义多个属性的可能性很大,ECMAScript 5 又定义了一个 Object.definePro-perties()方法。利用这个方法可以通过描述符一次定义多个属性。这个方法接收两个对象参数: 第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对 应

3.读取属性特性

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

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

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

二、创建对象

虽然 Object构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同 一个接口创建很多对象,会产生大量的重复代码。

1.工厂模式

为了解决上述问题而产生

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("Nicholas", 29,"Software Engineer"); 
var person2 = createPerson("Greg", 27, "Doctor"); 

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

2.构造函数模式

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

要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4 个步骤:

(1) 创建一个新对象;

(2) 将构造函数的作用域赋给新对象(因此 this就指向了这个新对象);

(3) 执行构造函数中的代码(为这个新对象添加属性);

(4) 返回新对象。

如上新建的person1 和 person2 分别保存着 Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向 Person。

  • 将构造函数当作函数
    • 构造函数与其他函数的唯一区别,就在于调用它们的方式不同
    • 任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数
  • 构造函数的问题
    • 是每个方法都要在每个 实例上重新创建一遍,所以并不同一个方法函数
    • 解决这个问题的方法是在外部定义方法函数,在构造函数中将外部函数方法的引用对象复制给属性。

3.原型模式

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

  • 无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。
  • 在默认情况下,所有原型对象都会自动获得一个 constructor (构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。
  • 虽然在所有实现中都无法访问到[[Prototype]],但可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。 Person.prototype.isPrototypeOf(person1) //true
  • ECMAScript 5增加了一个新方法,叫 Object.getPrototypeOf(),在所有支持的实现中,这个 方法返回[[Prototype]]的值。
  • 虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们 在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该 属性将会屏蔽原型中的那个属性。
  • 我们在实例中添加与原型同名的属性,不会修改而是阻止了实例访问原型当中的属性。即使将这个属性设置为 null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过,使用delete操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性。
  • 使用 hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不 要忘了它是从 Object 继承来的)只在给定属性存在于对象实例中时,才会返回 true。
  • Object.getOwnPropertyDescriptor()方法只能用于实例属 性,要取得原型属性的描述符,必须直接在原型对象上调用 Object.getOwnProperty- Descriptor()方法。
  • 由于 in操作符只要通过对象能够访问到属性就返回 true,hasOwnProperty()只在属性存在于 实例中时才返回 true,因此只要 in 操作符返回 true 而 hasOwnProperty()返回 false,就可以确 定属性是原型中的属性
  • 在使用 for-in 循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中 既包括存在于实例中的属性,也包括存在于原型中的属性。
  • Object.keys()方法。这个方法 接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
  • 所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。
  • 原型模式问题
    • 首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。
    • 实例一般都是要有属于自己的全部属性的。但是我们在实例身上修改原型的属性的时候,所有的实例都会受到影响。

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

创建自定义类型的常见方式,就是组合使用构造函数模式与原型模式。

构造函数模式用于定义实 例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本, 但同时又共享着对方法的引用,大限度地节省了内存

这种混成模式还支持向构造函数传递参 数;可谓是集两种模式之长。

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

5.动态原型模式

通过 检查某个应该存在的方法是否有效,来决定是否需要初始化原型。

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); 
        };            
     } 
}  

使用动态原型模式时,不能使用对象字面量重写原型。前面已经解释过了,如果 在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。

6.寄生构造函数模式

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

除了使用 new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实 是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。

返回的对象与构造函数或者与构造函数的原型属 性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。

不能依赖 instanceof 操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情 况下,不要使用这种模式。

7.稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象适合在 一些安全的环境中(这些环境中会禁止使用 this 和 new),或者在防止数据被其他应用程序(如 Mashup 程序)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的 实例方法不引用 this;二是不使用 new 操作符调用构造函数。

与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也 没有什么关系,因此 instanceof 操作符对这种对象也没有意义。

三、继承

1.原型链

构造函数、原型和实例的关系:每 个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型 对象的内部指针

  • 所有函数的默认原型都是 Object 的实例,因此默认原 型都会包含一个内部指针,指向 Object.prototype。这也正是所有自定义类型都会继承 toString()、 valueOf()等默认方法的根本原因。
  • 确定原型和实例的关系
    • instanceof 操作符
    • isPrototypeOf()
  • 给原型添加方法的代码一定要放在替换原型的语句之后
  • 原型链的问题
    • 主要的问题来自包含引 用类型值的原型。我们前面介绍过包含引用类型值的原型属性会被所有实例共享;而 这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。
    • 在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上, 应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

2.借用构造函数

  • 在子类型构造函数的内部调用超类型构造函数
  • 函数只不过是在特定环境中执行代码的对象, 因此通过使用 apply()和 call()方法也可以在(将来)新创建的对象上执行构造函数
  • 相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函 数传递参数
  • 借用构造函数的问题
    • 方法都在构造函数中定 义,因此函数复用就无从谈起了
    • 在超类型的原型中定义的方法,对子类型而言也是不可见的,结 果所有类型都只能使用构造函数模式

3.组合继承

是将原型链和借用构造函数的 技术组合到一块,从而发挥二者之长的一种继承模式。

  • 使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
  • 组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript中常用的继 承模式。
  • instanceof 和isPrototypeOf()也能够用于识别基于组合继承创建的对象。

4.原型式继承

function object(o){    
    function F(){}   
    F.prototype = o;  
    return new F(); 
}  
  • ECMAScript 5通过新增 Object.create()方法规范化了原型式继承。这个方法接收两个参数:一 个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
  • Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同。

5.寄生式继承

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

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

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式.

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

6.寄生式组合继承

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

其背 后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型 原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型 的原型。

基本模式

 function inheritPrototype(subType, superType){   
    //创建对象 
    var prototype = object(superType.prototype);    
    //增强对象
    prototype.constructor = subType; 
    //指定对象
    subType.prototype = prototype;                
 }  

书上示例

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;
}  

inheritPrototype(SubType, SuperType);  

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

这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType. prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf()。开发人员普遍认为寄生组合式继承是引用类型理想的继承范式。

小结

在没有类的情况下,可以采用下列模式创建对象。

  • 工厂模式,使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。这个模式后来被构造函数模式所取代。
  • 构造函数模式,可以创建自定义引用类型,可以像创建内置对象实例一样使用 new操作符。不过,构造函数模式也有缺点,即它的每个成员都无法得到复用,包括函数。由于函数可以不局 限于任何对象(即与对象具有松散耦合的特点),因此没有理由不在多个对象间共享函数。
  • 原型模式,使用构造函数的 prototype属性来指定那些应该共享的属性和方法。组合使用构造函数模式和原型模式时,使用构造函数定义实例属性,而使用原型定义共享的属性和方法。

JavaScript 主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函 数的原型实现的。这样,子类型就能够访问超类型的所有属性和方法,这一点与基于类的继承很相似。 原型链的问题是对象实例共享所有继承的属性和方法,因此不适宜单独使用。解决这个问题的技术是借 用构造函数,即在子类型构造函数的内部调用超类型构造函数。这样就可以做到每个实例都具有自己的 属性,同时还能保证只使用构造函数模式来定义类型。使用多的继承模式是组合继承,这种模式使用 原型链继承共享的属性和方法,而通过借用构造函数继承实例属性。

此外,还存在下列可供选择的继承模式。

  • 原型式继承,可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅 复制。而复制得到的副本还可以得到进一步改造。
  • 寄生式继承,与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强 对象,后返回对象。为了解决组合继承模式由于多次调用超类型构造函数而导致的低效率问 题,可以将这个模式与组合继承一起使用。
  • 寄生组合式继承,集寄生式继承和组合继承的优点与一身,是实现基于类型继承的有效方式。

恭喜完成本章节的阅读!再看看其他章节吧!继续加油哦!

《JavaScript高级程序设计》:JS基础概念

《JavaScript高级程序设计》:JS变量、作用域和内存问题

《JavaScript高级程序设计》:引用类型

《JavaScript高级程序设计》:面向对象的程序设计

《JavaScript高级程序设计》:函数表达式