JavaScript高级程序设计读书笔记6

第6章 面向对象的程序设计

理解对象

属性类型

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

    数据属性

  • 数据属性包含一个数据值的位置
  • 数据属性有4个描述其行为的特性:
  • Configurable:能否通过delete删除属性从而重新定义属性,能否删除属性的特征,或者能否把属性修改为访问器属性。
  • Enumerable:表示能否通过for-in循环返回属性
  • Writable:表示能否修改属性的值
  • Value:包含这个属性的数据值,默认值undefined
  • 直接在对象上定义属性,则Configurable、Enumerable、Writable特性默认值是true,在调用Object.defineProperty()方法创建一个新属性时,如果不指定,则以上三个特性都是false。

    1
    2
    3
    4
    5
    6
    7
    8
    var person={};
    Object.defineProperty(person,"name",{
    writable:false,
    value:"Nicholas"
    });
    alert(person.name); //"Nicholas"
    person.name="Greg";
    alert(person.name); //"Nicholas"
  • 在把configurable设置为false后,就不能再把它变回可配置了,调用Object.defineProperty()方法修改除writable之外的特性就会导致错误

访问器属性

  • Configurable:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。
  • Enumerable:能否通过for-in循环返回属性
  • get:在读取属性时调用的函数,
  • 值undefined
  • set:在写入属性时调用的函数,默认值undefined
  • 访问器属性不能直接定义,必须使用Object.defineProperty()来定义。
  • 属性前面的下划线是一种常用的记号,用于表示只能通过对象方法访问的属性

    定义多个属性

  • Object.defineProperties():一次定义多个属性

    读取属性的特性

  • Object.getOwnPropertyDescriptor():取得给定属性的描述符,返回一个对象

创建对象

工厂模式

  • 解决列创建多个相似对象的问题,但是没有解决对象识别的问题(即怎样知道一个对象的类型)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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");

构造函数模式

1
2
3
4
5
6
7
8
9
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");
  • 工厂模式和构造函数模式的区别:

    1. 没有显式地创建对象
    2. 直接将属性和方法赋给this对象
    3. 没有return语句
  • 构造函数使用的都是大写字母开头,非构造函数则是小写字母开头

  • new操作符调用构造函数实际上会经历以下4个步骤:

    1. 创建一个新对象
    2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
    3. 执行构造函数中的代码(为这个新对象添加属性)
    4. 返回新对象
  • person1对象有一个constructor(构造函数)属性,该属性指向Person。

    1
    2
    alert(person1.constructor==Person);  //true
    alert(person1.constructor==Object); //false
  • person1对象是Person的实例

    1
    2
    alert(person1 instanceof Person);  //true
    alert(person1 instanceof Object); //true

将构造函数当做函数

  • 构造函数与其他函数的唯一区别:调用它们的方式不同
  • 任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那他跟普通函数没有区别
1
2
3
4
5
6
7
8
9
10
11
12
//当做构造函数使用
var person=new Person("Nicholas",29,"software Engineer");
person.sayName();

//作为普通函数调用
Person("Greg",27,"Doctor"); //添加到window
window.sayName();//当在全局作用域中调用一个函数时,this对象总是指向Global对象()(在浏览器中就是window对象)

//在另一个对象的作用域中调用
var o=new Object();
Person.call(o,"Kristen",29,"Nurse"); //在对象o的作用域中调用
o.sayName();

构造函数的问题

  • 构造函数的主要问题:每个定义的方法都要在每个实例上重新创建一遍
  • 在每个实例上创建函数对象没有必要,可以通过把函数定义转移到构造函数外部来解决这个问题
  • 可是又有新的问题
    1. 在全局作用域中定义的函数实际上只能被某个对象调用,折让全局作用域名不副实
    2. 如果对象需要定义很多方法,那么久要定义很多的全局函数,丝毫没有封装性可言

原型模式

  • 每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象。用途:包含可以有特定类型的所有实例共享的属性和方法
  • 原型模式与构造函数模式的区别:新对象的这些属性和方法是由所有实例共享的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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();
    person1.sayName(); //"Nicholas"

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

理解原型对象

  • 只要创建一个新函数,就会根据特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。
  • 所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性是指向一个prototype属性所在函数的指针。
  • 当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。这个链接存在于实例与构造函数的原型对象之间,实例与构造函数没有直接关系。

  • isPrototyprOf():确定对象之间是否存在在[[prototype]]这种关系

    1
    alert(Person.prototype.isPrototypeOf(person1));  //true
  • Object.getPrototypeOf():返回[[prototype]]的值

    1
    alert(Object.getPrototypeOf(person1).name);   //"Nicholas"
  • hasOwnProperty():可以检测一个属性是存在于实例中,还是存在于原型中。存在于实例中,则返回true。

    1
    2
    3
    4
    5
    6
    var person1=new Person();
    alert(person1.hasOwnProperty("name")); //false 来自原型
    person1.name="Greg";
    alert(person1.hasOwnProperty("name")); //true 来自实例
    delete person1.name;
    alert(person1.hasOwnProperty("name")); //false 来自原型
  • 多个对象实例共享原型所保存的属性和方法的基本原理:每当代码读取某个对象的某个属性时,都会执行一次搜索,首先从对象实例本身开始,如果在事例中找到了给定名字的属性,则返回该属性的值,如果没有找到,则继续搜索指针指向的源性对象,在源性对象中查找具有给定名字的属性。

  • 原型最初只包含constructor属性,而该属性也是共享的,因此可以通过对象实例访问。
  • 使用delete操作符可以删除实例属性

原型与in操作符

  • in操作符有两种使用方法:单独使用和在for-in循环中使用。
  • 在单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实力中还是原型中

    1
    2
    3
    function hasPrototypeProperty(object,name){
    return !object.hasOwnProperty(name)&&(name in object);
    }
  • 在for-in循环中使用时,返回的是所有能够通过对象访问的、可枚举的属性,屏蔽了原型中不可枚举属性的实例属性也会在for-in循环中返回。

  • 所有开发人员定义的属性都是可枚举的。
  • Object.keys():取得对象上所有可枚举的实例属性。PS:只有实例对象上的,和for-in不同,for-in还会包含原型中的属性和方法。
  • Object.getOwnProertyNames():得到所有实例属性,无论是否可枚举。PS:同样,只有实例对象上的,和for-in不同,for-in还会包含原型中的属性和方法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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

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

    alert(Object.getOwnPropertyNames(p1));//name,age

更简单的原型语法

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function Person(){
    }
    Person.prototype={
    name:"Nicholas",
    age:29,
    job:"Software Engineer",
    sayName:function() {
    alert(this.name);
    }
    };
  • 以上方法完全重写了默认的prototype对象,因此constructor属性指向了Object。可以设置为指向Person。
    constructor:Person

  • 但这种重设constructor会导致她的Enumerable特性被设置为true。原生的constructor属性是不可枚举的。因此可以添加以下代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function Person(){
    }
    Person.prototype={
    name:"Nicholas",
    age:29,
    job:"Software Engineer",
    sayName:function() {
    alert(this.name);
    }
    };
    Object.defineProperty(Person.prototype,"constructor",{
    enumerable:false,
    value:Person
    });

原型的动态性

  • 对源性对象所做的任何修改都能够立即从实例上反应出来,即使是先创建了实例后修改原型也是如此。
  • 先创建实例后,重写整个原型对象,就把原型修改为另外一个对象,切断了构造函数与最初原型之间的联系。而实例中的指针还是指向最初的原型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function Person(){
    }
    var friend=new Person();
    Person.prototype={
    name:"Nicholas",
    age:29,
    job:"Software Engineer",
    sayName:function() {
    alert(this.name);
    }
    };
    friend.sayName(); //报错
  • 先修改原型,再创建实例就不会报错了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function Person(){
    }
    Person.prototype={
    name:"Nicholas",
    age:29,
    job:"Software Engineer",
    sayName:function() {
    alert(this.name);
    }
    };
    var friend=new Person();
    friend.sayName(); //报错

原生对象的原型

  • 创建的自定义类型,原生的引用类型,都是采用原型模式创建的
  • 可以像修改自定义对象的原型那样修改原生对象的原型,因此可以随时添加方法。

    原型对象的问题

  1. 省略了为构造函数传递初始化参数这一环节,所有的实例在默认情况下都取得相同的属性值。
  2. 实例1改变原型中引用类型值的属性,则实例2中的属性也会跟着改变。

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

  • 创建自定义类型的最常见方式,就是组合使用构造函数模式和原型模式
  • 构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性
  • 每个实例都会有自己的一份实例属性的副本,但同时又共享方法的引用,最大限度地节省了内存。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.friends=["Shelby","Count"];
    }

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

    person1.friends.push("Van");
    alert(person1.friends); //Shelby,Count,Van
    alert(person2.friends); //Shelby,Count
    alert(person1.friends===person2.friends); //false
    alert(person1.sayName===person2.sayName); //true

动态原型模式

  • 动态原型模式将所有信息都封装在构造函数中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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);
    };
    }
    }
    var person1=new Person("Nicholas",29,"Software Engineer");
    person1.sayName();
  • 这里原型所做的修改,能够立刻在所有实例中得到反映。

  • if语句检查的可以是初始化之后应该存在的任何属性或方法,不必检查每一个,只要检查其中一个即可。
  • instanceof操作符来确定对象类型

    寄生构造函数模式

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Person(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=new Person("Nicholas",29,"software Engineer");
  • 除了使用new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。

  • 返回的对象与构造函数或者与构造函数的原型属性之间没有关系
  • 不能依赖instanceof操作符来确定对象类型

    稳妥构造函数模式

  • 稳妥对象:没有公共属性,其方法也不引用this的对象
  • 稳妥对象最适合在一些安全的环境中(这些问环境禁止使用this和new)或在防止数据被其他应用程序(如Mashup程序)改动时使用。
  • 稳妥构造函数遵循与计生构造函数类似的模式,担有两点不同:

    1.新创建对象的实例方法不引用this
    2.不适用new操作符调用构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function Person(name,age,job){
    var o=new Object();
    o.name=name;
    o.age=age;
    o.job=job;
    o.sayName=function(){
    alert(name);
    };
    return o;
    }
    var person1=Person("Nicholas",29,"software Engineer");
    person1.sayName(); //"Nicholas"
    person1.name="Greg";
    person1.sayName(); //"Nicholas"就算改变person1的那么也不会改变
  • 返回的对象与构造函数或者与构造函数的原型属性之间没有关系

  • 不能依赖instanceof操作符来确定对象类型

继承

  • 许多OO语言都支持两种继承方式:

    接口继承:只继承方法签名
    实现继承:继承实际的方法

原型链

  • 基本概念:原型对象等于另一个类型的实例,原型对象将包含一个指向另一个原型的指针,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,如此层层递进,构成了实例与原型的链条。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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;
    }
    var instance=new SubType();
    alert(instance.getSuperValue());

  • instance.getSuperValue()会经历三个步骤搜索:

    1.搜索实例
    2.搜索SubType.prototype
    3.搜索SuperType.prototype

别忘记默认的原型

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

确定原型与实例的关系

  • 两种方法:

    1.instanceof操作符:测试实例与原型链中出现打过的构造函数,结果就会返回true
    2.isPrototypeOf():只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型

谨慎地定义方法

  • 给原型添加方法的代码一定要放在替换原型的语句之后,即必须在用SuperType的实例替换原型之后,再定义这两个方法。
  • 通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样做就会重写原型链

原型链的问题

  • 两个问题:

    1.包含引用类型之的原型属性会被所有实例共享
    2.在创建子类型的实例时,不能向超类型的构造函数中传递参数

借用构造函数

  • 又称为伪造对象或经典继承
  • 思路:子类型构造函数的内部调用超类型构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function SuperType(){
    this.colors=["red","blue","green"];
    }
    function SubType(){
    //继承了SuperType
    SuperType.call(this);
    }
    var instance1=new SubType();
    instance1.colors.push("black"); //red,blue,green,black
    alert(instance1.colors);
    var instance2=new SubType();
    alert(instance2.colors);//red,blue,green
  • 会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码

    传递参数

  • 可以在子类型构造函数中向超类型构造函数传递参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function SuperType(name){
    this.name=name;
    }
    function SubType(){
    //继承了SuperType,同时传递了参数
    SuperType.call(this,"Nicholas");
    //实例属性
    this.age=29;
    }
    var instance1=new SubType();
    alert(instance1.name); //Nicholas
    alert(instance1.age); //29

借用构造函数的问题

  • 方法都在构造函数中定义,因此无法复用函数
  • 超类型的原型定义方法,对子类型而言是不可见。

组合继承

  • 又称为经典继承,将原型链和借用构造函数的技术组合到一起。
  • 思路:使用原型链实现对原型属性和方法的继承,借用构造函数实现对实例属性的继承
  • 好处:既可以在原型上定义方法实现函数复用,又保证每个实例都有它自己的属性
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    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");//red,blue,green,black
    alert(instance1.colors);
    instance1.sayName(); //Nicholas
    instance1.sayAge(); //29

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

原型式继承

  • 借助原型可以基于已有的对象创建新对象,同时不必因此创建自定义类型
  • 其本质是object()对传入其中的对象执行了一次浅复制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function object(o){ //返回一个对象的实例
    function F(){}
    F.prototype=o;
    return new F();
    }
    var person={
    name:"Nicholas",
    friends:["Shelby","Court","Van"]
    };
    var anotherPerson=object(person);
    anotherPerson.name="Greg";
    anotherPerson.friends.push("Rob");
    var yetAnotherPerson=object(person);
    yetAnotherPerson.name="Linda";
    yetAnotherPerson.friends.push("Barbie");
    alert(person.friends); //Shelby,Court,Van,Rob,Barbie
  • 两个新对象都将person作为原型,相当于创建了person对象的两个副本。

  • 使用场景:在想让一个对象与另一个对象保持累心搞得情况使用
  • 缺陷:包含的引用类型值的属性始终都会共享相应的值

    寄生式继承

  • 思路:创建一个金用于封装集成过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是他做了所有工作一样返回对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function object(o){
    function F(){}
    F.prototype=o;
    return new F();
    }
    function createAnother(original){
    var clone=object(original); //返回original类型的实例
    clone.sayHi=function(){
    alert("Hi");
    }; //以某种方式增强这个对象
    return clone; //返回这个对象
    }
    var person={
    name:"Nicholas",
    friends:["Shelby","Court","Van"]
    };
    var anotherPerson=createAnother(person);
    anotherPerson.sayHi(); //Hi
  • 使用场景:主要考虑对象而不是自定义类型和构造函数的情况下使用

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

寄生组合式继承

  • 组合继承最大的问题:无论什么情况下,都会调用两次超类型构造函数,一次是在创建子类型原型的时候,子类型原型会得到超类型属性。另一次是在子类型构造函数内部,子类型实例上会得到超类型属性。
  • 解决方式:寄生组合式继承
  • 思路:不必为了指定子类型的原型而调用超类型的构造函数,所需要的无非是超类型原型的一个副本而已。使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    function object(o){ //返回一个对象的实例
    function F(){}
    F.prototype=o;
    return new F();
    }

    function inheritPrototype(subType,superType){
    var prototype=object(superType.prototype); //创建超类型原型的一个副本
    prototype.constructor=subType;//为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认constructor属性
    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=new SuperType();
    SubType.prototype.constructor=SubType;
    */

    SubType.prototype.sayAge=function(){
    alert(this.age);
    };
  • 优势:只调用一次超类型构造函数,避免在子类型原型上创建不必要的多余属性,同时原型链还能保持不变。集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效的方式。