第6章 面向对象的程序设计 函数只不过是在特定环境中执行代码的对象

256 阅读10分钟

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

核心:

  1. 原型:每个构造函数都有对应的原型对象,实例都共享原型对象的属性和方法。
  2. 原型链:让子类型的原型对象等于超类型的实例。对象里的属性按原型链搜索。
  3. new的四部曲。

理解对象

属性类型

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

特性:描述属性的各种特征,不能直接访问。

1、数据属性

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

  • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,能否修改为访问器属性。直接在对象上定义的默认为true。
  • [[Enumerable]]:表示能否通过for-in循环返回属性。直接在对象上定义的默认为true。
  • [[Writable]]:表示能否修改属性的值。直接在对象上定义的默认为true。
  • [[Value]]:包含这个属性的数据值。默认值为undefined。
使用Object.defineProperty()可以修改属性默认特性,也可以创建属性(前三个特性默认为false)。接收三个参数:对象、属性名、描述符对象(由特性作为属性,组成的对象)。可同时修改多个特性。

var o=new Object();o.name="xwt";Object.defineProperty(o,"name",{    writable:false,    value:"cm"});alert(o.name); //cmo.name="xwt";alert(o.name); //cm

一旦把属性[[Configurable]]设置为不可配置,则不能使用Object.defineProperty()更改。

2、访问器属性

访问器属性不包含数据值,只包含一对getter和setter函数(都不是必需的)。不能直接定义,必须用Object.defineProperty()来定义。

  • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,能否修改为访问器属性。直接在对象上定义的默认为true。
  • [[Enumerable]]:表示能否通过for-in循环返回属性。直接在对象上定义的默认为true。
  • [[Get]]:在读取属性时调用的函数,默认undefined。
  • [[Set]]:在写入属性时调用的函数,默认undefined。
访问器属性可用于设置一个属性的值,会导致其他属性的变化。

定义多个属性

  • Object.defineProperties()用于定义或修改多个属性。

读取属性的特性

Object.getOwnPropertyDescriptor():可以取得给定属性的描述符。获取两个参数,属性所在的对象和属性名称,返回值是一个对象。

创建对象

所有对象均继承Object。

创建一种包含同种属性和方法的类型。

在函数中新建的变量或者对象,每执行一遍就会创造一次。每次创造的不是同一个。

工厂模式

以函数来封装,以特定接口创建对象。解决了多个相似对象的问题。但没有说明类型。

function creatPerson(name,age){    var o={        name:name,        age:age    };    return o;}var person1=creatPerson("xiao",25);var person2=creatPerson("hongg",18);console.log(person1);

构造函数模式

可以用来创建特定类型的对象。但是不同实例上的同名函数不相等,完成同一个任务的函数没必要,把函数放到全局,又污染全局环境。所以没方法的对象可以用这个好。

function Person(name,age){    this.name=name;    this.age=age;
    this.sayName:function(){  //每执行一遍都会在各自的对象创建一个函数。可以把创建函数的移动
                              //到全局,里面留个指针,解决这个问题。但这样
          return this.name;
}

   // 隐式返回 return 简单数据类型值或者return this都是创建的对象;}console.log(new Person("xiao",23));

①没有显式的创建对象。

②直接将属性方法赋值给this对象。

③没有return语句

④大写函数名

⑤创建时需要使用new(创建新对象,能将作用域绑定到对象,也就是赋值this为对象,执行代码,返回对象)

创建的对象有constructor属性指向构造函数。

原型模式

解决了公共资源的问题,但当属性是引用对象时,一个属性修改了,另外的对象的这个属性都会发生变化,这不是我们想要的。

只要创建一个新函数,都有prototype属性,指向原型对象。

所有对象实例都共享原型对象里面的属性和方法。

所有原型对象都会自动获得一个constructor属性(不可枚举),指向其所在的构造函数。其他默认方法,都是继承于Object。

创建一个新对象后,对象内部有[[prototype]]指向构造函数的原型对象,但是无法直接访问。

在实例中创建同名属性会屏蔽原型对象的属性。可以用delete 操作符删除属性后解除屏蔽。

function Person(){    }
Person.prototype.name="xiao";Person.prototype.age=29;Person.prototype.sayName=function(){        alert( this.name);    };
person1=new Person();person1.sayName()

也可以用对象字面量重写原型对象,只是constructor指针就没有默认创建了,指向的是Object构造函数(从原型对象的原型对象上继承),这时候可以设置constructor的值,指回函数。同时这个属性也会变成可枚举。

function Person(){    }
Person.prototype={        constructor:Person,        name:"xiao",        age:12,        sayName:function(){            alert(this.name);        }    }  person1=new Person();person1.sayName()

  • Person.prototype.isPrototypeOf(person1):判断person1与Person原型对象有关。
  • Object.getPrototypeOf():返回该对象的原型对象。
  • person1.hasOwnProperty():可以检测这个属性是不是在实例中,在,返回true。
  • in操作符:通过对象能访问的属性时返回true,包括实例和其原型对象中的属性。
  • delete操作符:可以删除对象属性
  • 使用使用 !person1.hasOwnProperty("name")&&"name" in person1 可以判断属性是不是在原型对象中
  • Object.keys():接受一个对象作为参数,返回对象可枚举属性的字符串数组,不包括原型对象的属性。
  • Object.getOwnPropertyName():无论可以枚举属性都返回,其余与Object.keys()相同
原型的动态性:先创建了实例后修改原型对象,操作也能马上反馈到实例上。因为每次找指,都是先搜索实例,不存在,再搜索原型对象。

已经创建了实例,再重写原型,会切断现有实例和原型的联系。

不能在函数中对原型对象用对象字面量重写,如下图,会切断实例和原型的联系。


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

创建自定义类型最常见的方法。

把不共享的属性用构造函数创建,共享的方法放在原型对象中。

function Person(name,age){    this.name=name;    this.age=age;}Person.prototype={    constructor:Person,    sayName:function(){    alert(this.name); //this看调用时的,最后通过person1.sayName()调用的    }};var person1=new Person("xiao",1);person1.sayName();//"xiao"

动态原型模式

在组合使用构造函数和原型模式的基础上,把所有信息都封装在了函数中。

用if判断原型对象是否初始化,不用每个方法、属性都判断,只要判断一个就行了。

注意函数中不能使用对象字面量定义原型对象。

function Person(name,age){    this.name=name;    this.age=age;    if(typeof this.sayName !="function"){         Person.prototype.sayName=function(){            alert(this.name)        };
        Person.prototype.sayAge=function(){
            alert(this.age);
        }    }}var person1=new Person("xiao",1);person1.sayName();//"xiao"

寄生构造函数模式

形式上与工厂模式相同。就是创建对象的时候用到了new。

但是函数中的return屏蔽了new创建的对象,所以返回的对象与原型对象毫无关系。所以不能通过instanceof来判断对象类型。不推荐使用。

稳妥构造函数模式

不使用this和new,其他与工厂模式一致。私有变量,只能通过方法进行访问。适合用于某些安全环境。


继承:在一个类型的基础上创建第二个类型

由于函数没有签名(参数无所谓,不能重载),所以ECMAScript只能支持实现继承,不支持接口继承。

原型链

对象中属性的搜索规则:原型链

对象外标识符的搜索规则:作用域链

概念:让一个类型继续另一个类型,就让其原型对象等于另一个类型的实例。那么这个构造函数的实例就有了原型对象的方法、另一个类型实例的属性方法和另一个类型的原型对象的属性和方法。要注意此时的constructor是从原型对象的原型对象上继承的。

原型链是实现继承的主要方法。利用原型,让一个引用类型继承另一个引用类型的方法和属性。

①所有函数的默认原型对象都是Object的实例,所以引用类型都继承了Object。

②确定原型和实例的关系:

  • 使用instanceof操作符。只要是实例的原型链中出现的构造函数,则返回true。
  • isPrototypeOf()方法。只要是原型链中出现过的原型,都返回true。

③谨慎地定义方法

function SupperType(){    this.property=true;}SupperType.prototype.sayProperty=function(){    alert(this.property)}function SubType(name){    this.subproperty=name;}SubType.prototype=new SupperType();SubType.prototype.saySubProperty=function(){    alert(this.subproperty)}var sub1=new SubType("hh");var sup1=new SupperType();SubType.prototype.sayProperty=function(){    alert(false);}sub1.sayProperty();  //falsesup1.sayProperty(); //true

④不能再定义完子类型的原型后,继续用对象字面量重写原型,会让之前的定义无效。

会导致的问题:①父类型的实例中可能存在引用类型的属性,这个实例变成了原型后,就会产生和原型模式创建对象一样的问题。(详情看原型模式)

②没有办法在不影响所有实例的情况下,向超类型传递参数。

所以实践上,很少单独用这种方法。

借用构造函数

在子类型内部调用超类型的构造函数。

为了确保构造函数不会重写子类型的属性,可以在调用构造函数后,再写子类型属性。

解决了原型链导致的问题①②,但

导致的问题①不能继承超类的原型。②同功能函数没有复用(详见构造函数模式)

✔组合继承

最常用的继承模式。

共享的方法和属性用原型链继承。不共享的属性和传参用借用构造函数继承。

function SupperType(arg){    this.property=arg;    this.str=[1,2,3];}SupperType.prototype.sayProperty=function(){    alert(this.property)}function SubType(arg,name){    SupperType.call(this,arg)    this.subproperty=name;}SubType.prototype=new SupperType();SubType.prototype.saySubProperty=function(){    alert(this.subproperty)}var sub1=new SubType("true","hh");var sub2=new SubType("true","xx");sub1.str.pop();console.log(sub1.str); //[1,2]console.log(sub2.str);//[1,2,3]

其实子原型中依然存在这些属性,只是被实例中的屏蔽了。

原型式继承

本质:浅复制,可以用得到的副本进一步改造。(传什么对象我就复制一个副本出来,赋值是指针指向还是同一个副本)

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

让一个类型的原型对象等于另一个对象的实例。

导致的问题和原型链一样。

快捷方法:Object.create():接受一个对象和描述符,返回一个对象(这个对象继承了传入对象本身所有属性和方法,和它原型对象的所有属性和方法)

只是想让几个对象和另一个对象相似的情况下做比较好,因为子类型没有指定特定的类型(工厂模式问题)。

寄生式继承

其实就是Object.create();

工厂模式版的继承,原型式继承的增强,可以定义自己的属性了。当然类似工厂模式和构造函数模式,同功能函数也得不到复用。

function object(o){    function F(){}    F.prototype=o;    return new F();}function creatAnther(o){    var newObj=object(o);    newObj.sayName=function(){        alert("12");    }    return newObj;}var p={    name:"ni",    age:"1"}var a=creatAnther(p);console.log(a);

寄生组合式继承

最理想的继承范式

让子类型的原型等于超类型的原型副本。(直接等于超类型原型会导致子类型原型改变超类型原型也改变的情况)。

比起组合继承:只调用了一次超类型的构造函数,并且在子类型的原型上避免不必要的属性,与此同时,原型链还能保持不变,可以正常用instanceof和isPrototypeof()。

function clonePrototype (SubType,SupType){    var prototype=object(SubType.prototype);    prototype.constructor=SubType;    SubType.prototype=prototype;}

用 clonePrototype(SubType,SupType)替换SubType.prototype=new SupType();

 让一个类型继承另一个类型,优先使用寄生组合式继承,组合继承。

让一个对象继承另一个对象,优先使用寄生式继承(增强原型式继承)。