JavaScript之面向对象(ECMAScript5)

  • 理解对象属性
  • 创建对象
  • 继承

 

理解对象属性

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

由此在ECMAScript中可以把对象想象成散列表,无非就是键值对,值可以为数据或者函数。

//两种定义对象的方式
var person = new Object();
person.name="xiaoming";
person.age=29;
person.showName = function(){
    alert(this.name); 
}
var person = { name:"xiaoming", age:29, showName:function(){ alert(this.name); } }

属性类型

有些特性定义是为了实现JavaScript引擎用的。为了表示特性时内部值,ECMA-262规范把他们放在两对方括号中,例如:[ [ Enumerable ] ]。

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

数据属性    有以下描述行为的特性(多数情况下很少用到这些高级功能)

  • [ [ Configurable ] ]:表示能否通过delete删除属性从而重新定义属性,能否修改属性特性,或者能否把属性修改为访问器属性。默认值都为true
  • [ [ Enumerable ] ]:表示能否通过for-in循环返回属性。默认true
  • [ [ Writable ] ]:表示能否修改属性的值。默认为true
  • [ [ Value ] ]:包含属性的数据值,用作读写。

修改以上属性的默认特性使用Object.defineProperty()方法

var person={}
//接收三个参数:属性所在的对象,属性的名字和配置特性的对象
Object.defineProperty(person,"name",{
    writable:false,
    value:"xiaoming"
})    

访问器属性  包含一对getter和setter函数。有如下4个特性:

  • [ [ Configurable ] ]:表示能否通过delete删除属性从而重新定义属性,能否修改属性特性,或者能否把属性修改为访问器属性。默认值都为true
  • [ [ Enumerable ] ]:表示能否通过for-in循环返回属性。默认true
  • [ [ Get ] ]:在读取属性时调用的函数。默认为undefined
  • [ [ Set ] ]:在写入属性时调用的函数。默认为undefined
 1 var book = {
 2   _year:2017,
 3   edition:1
 4 }
 5 
 6 Object.defineProperty(book,"year",
 7 {
 8    get:function()
 9    {
10         return this._year;
11    }
12    set:function(newValue)
13    {
14         if(newVaule>2017)
15         {
16             this._year = newValue;
17             this.edition+=newVlaue-2017
18         }
19     }
20 })
21 
22 //定义多个属性
23 var book2 ={};
24 Object.defineProperties(book2,
25 {
26     _year:
27     {
28       writable:true,
29       value:2004
30     },
31     
32     edition:
33     {
34       writable:true,
35       value:1
36     },
37 
38      year:
39      {
40        get:function()
41        {
42          return this._year;
43        }
44        set:function(newValue)
45        {
46           if(newVaule>2017)
47           {
48             this._year = newValue;
49             this.edition+=newVlaue-2017;
50            }
51        }
52      }
53 })
View Code

 

若想要读取属性的某些特性,可以利用Object.getOwnPropertyDescriptor()方法,接收两个参数:属性所在的对象和要读取其描述的属性名称。

 

创建对象

问题一: 前面通过Object构造函数或对象字面量的形式创建单个对象,但是这种方式使用同一接口创建很多对象,产生大量重复代码。

解决方法:工厂模式,用函数来封装以特定接口创建对象的细节。

具体实现:函数createSchool()能够根据接收的参数来构建一个包含所有必要信息的School对象。

function createSchool(NO,name,address)
{
    var obj = new Object();

    obj.No = NO;
    obj.name = name;
    obje.address = address;

    return obj;
}

 

问题二:工厂模式解决了创建多个相似对象的问题,但是没有解决对象识别问题,不知道对象是什么类型,如上面的createSchool方法,得不到创建的类型是School类型。

解决方法:构造函数模式。创建自定义的构造函数,从而自定义对象类型的属性和方法。

具体实现:与工厂模式区别在于没有显示的创建对象,直接将属性和方法赋给了this对象,没有return语句。

function School(No,name,address)
{
    this.No = No;
    this.name = name;
    this.address = address;
    this.showName = function(){
        console.log(this.name);
    }
}

 要创建新实例要使用new操作符,会经历四个步骤

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象,this就指向了这个新对象
  3. 执行构造函数中代码,为对象添加属性
  4. 返回新对象

创建后的对象都会有一个constructor属性,该属性指向School。通过利用创建的实例对象school1.constructor == School来检测对象类型。比较可靠的方法还是使用

school1 instanceof School方式。另外构造函数其实也就是JS中简单的函数,当通过new操作符调用时,作为构造函数来使用,否则它也就是简单的函数。

 

问题三:构造函数虽然解决了工厂模式的缺陷,但是对于每个方法都需要在每个实例上重新创建一遍。也就是说对于上面School中的showName方法,其实等价于this.showName = new Function("console.log(..)");

也就是又实例化了一个对象。以这种方式会导致不同作用域链和标识符解析,不同实例上的同名函数是不相等的,不能实现共享。例如:school1.showName==school2.showName 返回的是false.

解决方法:将函数定义放在构造函数的外部。

function School(No,name,address)
{
    this.No = No;
    this.name = name;
    this.address = address;
    this.showName = showName;
}

function showName(){
    console.log(this.name);
}

 

问题四:将方法定义外面,确保了实例对象调用了相同的事件,但是在全局作用域中定义函数使得在任意地方均可调用,没有封装性可言。

解决方法:原型模式。每个函数都有一个prototype(原型)属性,它是一个指针,指向一个对象(原型对象),用途可以将想要实例间共享的属性和方法定义在这个对象上。

function School(){}

School.prototype.No = 1001;
School.prototype.name = "xxx";
School.prototype.showName = function(){
    console.log(this.name);
}

var school = new School();
school.showName();
var school2 = new School();
school2.showName();
console.log(school.showName==school2.showName);//true

知识点:

  • 原型对象:只要创建一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性是一个指向prototype属性所在函数的指针。

 

 

  • 每当代码读取某个属性时,根据给定名字的属性,搜索首先会从对象的实例本身开始,如果找到则返回该属性的值;如果没有找到,继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性,找到返回值,否则未定义,报错。
  • 更简单的原型语法:
School.prototype = {
    //为了使自动获取的constructor指向School,
    //因为通过这种原型语法constructor等于Object,需要手动设置如下
    constructor:School,
    name:"",
    showName:function(){
        console.log(this.name);
    }
}
  • 原生对象的原型。所有的原生引用类型(Object,Array,String等)都在其构造函数的原型上定义了方法。如在Array.prototype中可以找到sort()方法。我们也可以通过这种方法给某个原生类型添加方法和属性。例如:String.prototype.showText = function(){  alert("Text"); }

 

问题四:通过原型模式中原型对象的方式,会导致所有实例在默认情况下都将取得相同的属性值,造成了不便。同时由于他的共享性本身,对于引用类型的值来说问题较大,因为引用类型保存的是一个指针,共享导致某个实例改变改属性会同步到其他实例中。

解决方法:将构造函数模式和原型模式组合使用:构造函数模式用于定义实例属性,而原型模式用于定义方法和需要共享的属性。  使用最广泛的一种方法。

function School(No,name)
{
    this.No = No;
    this.name = name;
    this.Classes=["一年级","二年级"];
}

School.prototype = {
    constructor:School,
    showName:function(){
        console.log(this.name);        
    }
}

 

继承

说明:ECMAScript支持实现继承,主要依靠原型链

原型链方法:利用原型让一个引用类型继承另一个引用类型的属性和方法。

function SuperType()
{
    this.supername="super";
}

function SubType()
{
    this.subname = "sub";
}

SubType.prototype = new SuperType();

var instance = new SubType();
alert(instance.subname);

解析:在上述代码中,没有使用SubType的默认提供的原型,而是替换为SuperType的实例。新原型不仅作为基类(SuperType)的实例而具有全部的属性和方法,并且其内部还有一个指针,指向了SuperType的原型。从而instance指向SubType的原型,SubType的原型又指向SuperType的原型。通过之前讲过的,原型搜索机制,沿着原型链继续向上,直到找到指定的方法或属性。

补充:

  • 由于所有的引用类型默认都继承Object,所以所有函数的默认原型都是Object的实例,默认原型包含一个内部指针,指向Object.prototype,因此自定义的类型都会有toString(),valueOf()等方法。
  • 如何验证实例兼容哪种原型。可以使用instanceof (instance instanceof Object)或 isPrototypeOf()方法(Object.prototype.isPrototypeOf())。
  • 给原型添加方法要放在替换原型的语句之后。即SubType.prototype.getSubValue=function(){};要放在SubType.prototype = new SubperType();之后
  • 不要使用字面量添加新方法,因为会把原型链切断从而使得SubType和SuperType没有关系。

 

原型链的问题:前面讲过,通过原型链的形式会使得引用类型值的原型属性被所有实例共享,从而造成不便。另外创建子类的实例时,不能向超类型的构造函数传递参数。看如下代码

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

function SubType()
{

}

SubType.prototype = new SuperType();

var instance = new SubType();
instance.colors.push("green");
console.log(instance.colors);//red,bule,greed

var instance2 = new SubType();
console.log(instance2.colors);//red,bule,greed

解决方法:借用构造函数,解决原型中包含引用类型值所带来的问题

实现思路:在子类型构造函数的内部调用超类型构造函数。使用apply()或call()方法,只要将上面的代码中SubType函数里加入SuperType.call(this);即可。同时也可以传递参数,如下

function SuperType(name)
{
    this.name = name;
}
function SubType()
{
    SuperType.call(this,"jack");
}

var instance = new SubType();
console.log(instance.name);//jack

 

借用构造函数方法的问题:仅仅使用这一模式无法使函数复用。而且在基类中定义的方法,对于子类型而言是不可见的。

解决方法:组合继承。

实现思路:使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。

 

function SuperType(name)
{
    this.name = name;
    this.colors = ["red","blue"];
}
SuperType.prototype.showName = function()
{
    console.log(this.name);
}

function SubType(name,age)
{
    SuperType.call(this,name);
    this.age = age;
}
//继承形式
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
//给子类添加方法
SubType.prototype.showAge = function()
{
    console.log(this.age);
}

组合继承避免了原型链和借用构造函数的缺陷,融合了优点。

 

组合继承问题:其无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候(SubType.prototype = new SuperType())。另一次是在子类型构造函数内部(SuperType.call(this,name))。这种情况会造成两组name和colors属性,实例上和SubType原型中。

解决方法:寄生组合式继承

思路:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。我们所需要的就是超类型的副本。所以这种模式的基本代码结构如下

function SuperType(name)
{
    this.name = name;
    this.colors = ["red","blue"];
}
SuperType.prototype.showName = function()
{
    console.log(this.name);
}

function SubType(name,age)
{
    SuperType.call(this,name);
    this.age = age;
}
//将基类与子类联系在一起
inheritPrototype(subType,superType);
//然后给子类添加方法
SubType.prototype.showAge = function()
{
    console.log(this.age);
}


function inheritPrototype(subType,superType)
{
    var prototype = Object(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}

寄生组合式继承,只调用一次SuperType构造函数,避免在SubType.prototype上创建不必要,多余的属性,原型链还能保持不变。

 

总结:ECMAScript支持面向对象编程,使用如下模式创建对象:

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

JavaScript主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样,子类型就能够访问超类型所有属性和方法。原型链的问题是对象实例之间共享所有继承的属性和方法,不适宜单独使用。配和借用构造函数,即在子类型构造函数内部调用超类型构造函数。原型链继承共享的属性和方法,借用构造函数继承实例属性。另外还有寄生组合式继承,原型继承,寄生式继承等。

参考:《JavaScript高级程序设计》

 

posted @ 2017-07-25 22:00  贝同学  阅读(241)  评论(0编辑  收藏  举报