初看原型

JS的所有函数都有一个prototype属性,这个prototype属性本身又是一个object类型的对象。

prototype提供了一群同类对象共享属性和方法的机制。

将一个基类的实例作为子类的原型对象,原型继承

Employee.prototype = new Person();

JS通过简单的重写机制实现对象的“多态”性,与静态对象语言的虚函数和重载概念不谋而合。

可以通过给原型对象动态添加新的属性和方法,从而动态地扩展基类的功能特性。

在原型模型中,为了实现类继承,必须首先将子类构造函数的prototype设置为一个父类的对象实例。创建这个父类实例的目的就是为了构成原型链,以起到共享上层原型方法的作用。但创建这个实例对象时,上层构造函数也会给它设置对象成员,这些对象成员对于继承来说是没有意义的。虽然我们也没有给构造函数传递参数,但确实创建了若干没有用的成员,尽管其值是undefined,这也是一种浪费。

原型扩展

给内置的对象的构造函数添加新的方法和属性,从而增强JS的功能。

String.prototype.trim = function() { /* ... */ };

原型真谛

JS构造对象的过程可以分三步:第一步,创建一个空对象;第二步,将这对象内置的原型对象设置为构造函数prototype引用的原型对象;第三步,将该对象作为this参数调用构造函数,完成成员设置等初始化工作。

对象建立后,对象上的任何访问和操作都只与对象自身及其原型链上的那串对象相关,与构造函数基本扯不上关系了。换句话说,构造函数只是在创建对象时起到介绍原型对象和初始化对象两个作用。

思考:

那么我们能否自己定义一个对象来当做原型,并在这个原型上描述类,然后将这个原型设置给新创建的对象,将其当作对象的类呢?我们又能否将这个原型中的一个方法当作构造函数,去初始化新创建的对象呢?例如,我们定义这样一个原型对象:

var People = {                            // 定义一个对象当作原型
    create: function(name, age) {        // 这个当成构造函数
        this.name = name;
        this.age = age;
    },
    sayName: function() {                // 定义方法
        return this.name;
    },
    howOld: function() {
        return this.age;
    }
};

利用通用函数建立指定原型类的对象

/*
 * 通过函数创建新对象并返回
 * @param {Object} aClass 指定的原型类
 * @param {Array} aParams 参数数组
*/
function New(aClass, aParams) {        // 创建通用函数
    function _New() {                // 定义临时的中转函数壳
        // 调用原型中定义的构造函数,中转构造逻辑和传递参数
        aClass.create.apply(this, aParams);
    }
    _New.prototype = aClass;        // 准备中转原型对象
    return new _New();
}
修复版
/*
 * 通过函数创建新对象并返回
 * @param {Object} aClass 指定的原型类
 * @param {Array} aParams 参数数组
*/
function New(aClass, aParams) {
    function _New() {                // 定义临时的中转函数壳
        this.Type = aClass;            // 给每个对象约定Type属性,据此可以访问到对象所属的类
        // 调用原型中定义的构造函数,中转构造逻辑和传递参数
        // create方法是约定的构造函数
        if(aClass.create) aClass.create.apply(this, aParams);
    }
    _New.prototype = aClass;        // 准备中转原型对象
    return new _New();
}

通用函数New()就是一个语法甘露,不但中转原型对象,还中转了构造函数逻辑和构造参数。

有趣的是,每次创建完对象退出New函数作用域时,临时的_New函数对象会被自动释放。

我们还需要多一些语法甘露,实现类层次及其继承关系。

// 简洁而优雅地书写类层次及其继承关系
var object = {            // 基本类,用于定义最基本的方法
    isA: function(aBase) {                // 判断类与类之间及类与对象的之间的关系
        var self = this;
        while(self) {
            if(self ==aBase) return true;
            else self = self.Type;
        }
        return false;
    }
};
// 创建类的函数,用于声明类及继承关系
function Class(aBaseClass, aClassDefine) {            // 创建类的临时函数壳
    function _class() {
        this.Type = aBaseClass;            // 给每个类约定一个Type属性,引用其继承关系
        // 复制类的全部定义到当前创建的类
        for(var prop in aClassDefine) {
            this[prop] = aClassDefine[prop];
        }
    }
    _class.prototype = aBaseClass;
    return new _class();
}

令人高兴的是,受这些甘露滋养的JS程序效率会更高效。因为其原型对象里既没有了毫无用处的那些对象级的成员,而且还不存在constructor属性体,少了与构造函数之间的牵连,但依旧保持了方法的共享性。

甘露模型

使用Class()甘露,我们已经可以用非常优雅的格式定义一个类。

// 定义Student类,继承People
// Student其实是一个对象,是模拟成的类
// 可以通过New函数创建Student对象
var Student = Class(People, {                // 派生自People类
    create: function(name, age, grade) {
        // 调用超类的构造函数
        People.create.call(this, name, age);
        this.grade = grade;
    },
    getGrade: function() {
        return '我的成绩是' + this.grade;
    }
});

Class()语法甘露实际上是构造一个原型,并将这个原型挂在了相应的原型链上。它返回的是一个对象而不是函数。如果让Class()返回一个函数,不就可以用new Student()这种方式来创建对象吗?而且,我们可以为这个返回函数创建一个继承至相关原型链的原型对象,并设置到函数的prototype属性。这样,我们用new方式创建这个类函数的对象时,就自然地继承该类的原型了。

// 定义类的语法甘露:Class()
// 最后一个参数是JSON表示的类定义
// 如果参数的数量大于1,则第一个参数是基类
// 第一个参数和最后一个参数之间,可以表示类实现的接口
// 返回一个类,类是一个构造函数
function Class() {
    var aDefind = arguments[arguments.length-1];        // 类定义
    if( !aDefind ) return;
    // 确定基类,默认是object基本类
    // aBase 是一个构造函数,会继承它的原型对象
    var aBase = arguments.length > 1? arguments[0] : object;

    function prototype_() {}            // 临时函数, 用于挂接原型链
    prototype_.prototype = aBase.prototype;                // 准备传递prototype
    aPrototype = new prototype_();        // 构造函数的prototype
    // 复制类定义到当前类的原型prototype上
    for(var member in aDefind) {
        if(member != 'create')            // 不复制构造函数create
            aPrototype[member] = aDefind[member];
    }

    var aType;            // 存放构造函数的引用
    if(aDefind.create) aType = aDefind.create;            //  类型即为该构造函数
    else aType = function() {            // 如果未定义create(), 使用默认构造函数
        this.base.apply(this, arguments);                // 调用基类的构造函数
    }
    aType.prototype = aPrototype;        // 给构造函数的原型属性赋值
    aType.Base = aBase;                    // 设置类型关系(默认是object)
    aType.prototype.Type = aType;        // 为本类对象扩展一个Type属性,用于判断对象所属类
    return aType;                        // 返回构造函数作为类
}

// 根类object的定义
function object() { /* object 基本类 */ };
object.prototype.isA = function(aType) {
    var self = this.type;        // 调用isA()的当前类
    while(self) {
        if(self == aType) return true;
        self = self.aBase;        // 检查基类是否相等
    }
    return false;
};
object.prototype.base = function() {    // 用于调用基类的构造函数
    var Caller = object.prototype.base.caller;            // 当前构造函数create()
    Caller && Caller.Base && Caller.Base.apply(this, arguments);
};

使用Class()函数来定义一个类,实际上就是为创建对象准备了一个构造函数,而该构造函数的prototype已经初始化为方法表,并可继承上层类的方法表。这样当用new操作符创建一个对象时,也就很自然地将此构造函数的原型链传递给了新构造的对象。

Class()函数中的小问题

不支持不可枚举的内置方法如toString();object根类的base()方法用到了函数的caller属性,以此判断构造函数的层次。

1. 解决不能覆写不可枚举属性的问题并非难事。既然这些属性特殊,就可以对其进行特殊处理。我们在for-in复制完可枚举属性后,加上下面的处理语句:

if(aDefind.toString !== Object.prototype.toString) {

aPrototype['toString'] = aDefind.toString;

}

2. base()方法之所以要使用只身的caller属性,就是为了确定当前构造函数的层次,从而可以知道该调用上层的构造函数。

实际上,第一层构造函数调用this.base()时,我们可以通过this.Type属性知道一层构造函数,而this.Type.Base就是第二层构造函数。只是,第二层构造函数又会调用this.base(),其本来是想调用第三层的构造函数,但再次进入base()函数时,就无法知晓构造函数的层次了。

如果我们在第一层构造函数调用进入this.base()时,先改变this.base本身,让其在下次被调用时能掉到第3层构造函数。完成这个变身动作之后再调第2层构造函数,而第2层构造函数再调用this.base()时就能调用到第3层构造函数了。这样只要我们在每次的base()调用中都完成一个自我的变身动作,就可以按正确的顺序完成对构造函数的调用。

object.prototype.base = function() {    // 用于调用基类的构造函数
    var Base = this.type.Base;            // 第2层构造函数
    if(!Base.Base) {                    // 如果基类没有基类
        Base.apply(this, arguments);    // 就直接调用基类的构造函数
    }else {                                // 如果基类上面还有基类
        this.base = makeBase_(Base);    // 先覆盖this.base,返回一个函数
        Base.apply(this, arguments);    // 然后调用基类构造函数,其调用this.base
        delete this.base;
    }
    function makeBase_(Type) {            // 包装基类构造函数
        var Base = Type.Base;            // 第3层构造函数
        if (!Base.Base) return Base;    // 不存在基类,直接返回
        return function() {                // 包装为引用临时变量Base的闭包函数
            this.base = makeBase_(Base);// 先覆写this.base
            Base.apply(this, arguments);// 再调用基类的构造函数
        };
    }
};

makeBase_()函数,如果基类还有基类,它就返回一个闭包函数。下次this.base()被构造函数调用时,即调用的是这个闭包函数。但这个闭包函数又可能会调用makeBase_()形成另一个闭包函数,直到基类再无基类。

重写后的完美甘露模型代码

// 根类object的定义
function object() { /* object 基本类 */ };
object.prototype.isA = function(aType) {
    var self = this.type;        // 调用isA()的当前类
    while(self) {
        if(self == aType) return true;
        self = self.aBase;        // 检查基类是否相等
    }
    return false;
};
// object.prototype.base = function() {    // 用于调用基类的构造函数
//     var Caller = object.prototype.base.caller;            // 当前构造函数create()
//     Caller && Caller.Base && Caller.Base.apply(this, arguments);
// };
object.prototype.base = function() {    // 用于调用基类的构造函数
    var Base = this.type.Base;            // 第2层构造函数
    if(!Base.Base) {                    // 如果基类没有基类
        Base.apply(this, arguments);    // 就直接调用基类的构造函数
    }else {                                // 如果基类上面还有基类
        this.base = makeBase_(Base);    // 先覆盖this.base,返回一个函数
        Base.apply(this, arguments);    // 然后调用基类构造函数,其调用this.base
        delete this.base;
    }
    function makeBase_(Type) {            // 包装基类构造函数
        var Base = Type.Base;            // 第3层构造函数
        if (!Base.Base) return Base;    // 不存在基类,直接返回
        return function() {                // 包装为引用临时变量Base的闭包函数
            this.base = makeBase_(Base);// 先覆写this.base
            Base.apply(this, arguments);// 再调用基类的构造函数
        };
    }
};

// 定义类的语法甘露:Class()
// 最后一个参数是JSON表示的类定义
// 如果参数的数量大于1,则第一个参数是基类
// 第一个参数和最后一个参数之间,可以表示类实现的接口
// 返回一个类,类是一个构造函数
function Class() {
    var aDefind = arguments[arguments.length-1];        // 类定义
    if( !aDefind ) return;
    // 确定基类,默认是object基本类
    // aBase 是一个构造函数,会继承它的原型对象
    var aBase = arguments.length > 1? arguments[0] : object;

    function prototype_() {}            // 临时函数, 用于挂接原型链
    prototype_.prototype = aBase.prototype;                // 准备传递prototype
    aPrototype = new prototype_();        // 建立类要用的prototype
    // 复制类定义到当前类的原型prototype上
    for(var member in aDefind) {
        if(member != 'create')            // 不复制构造函数create
            aPrototype[member] = aDefind[member];
    }
    // 处理不可枚举的内置方法
    if(aDefind.toString !== Object.prototype.toString) {
        aPrototype['toString'] = aDefind.toString;
    }
    if(aDefind.valueOf !== Object.prototype.valueOf) {
        aPrototype['valueOf'] = aDefind.valueOf;
    }
    if(aDefind.toJSON !== Object.prototype.toJSON) {
        aPrototype['toJSON'] = aDefind.toJSON;
    }
    var aType;            // 存放构造函数的引用
    if(aDefind.create) aType = aDefind.create;            //  类型即为该构造函数
    else aType = function() {            // 如果未定义create(), 使用默认构造函数
        this.base.apply(this, arguments);                // 调用基类的构造函数
    }
    aType.prototype = aPrototype;        // 设置类的prototype
    aType.Base = aBase;                    // 设置类型关系
    aType.prototype.Type = aType;        // 为本类对象扩展一个Type属性
    return aType;                        // 返回构造函数作为类
}

来源:悟透Javascript

JS原型-语法甘露的更多相关文章

  1. JS 原型链图形详解

    JS原型链 这篇文章是「深入ECMA-262-3」系列的一个概览和摘要.每个部分都包含了对应章节的链接,所以你可以阅读它们以便对其有更深的理解. 对象 ECMAScript做为一个高度抽象的面向对象语 ...

  2. JS原型链

    JS作为发展了多年了对象语言,支持继承,和完全面向对象语言不同的是,JS依赖原型链来实现对象的继承. 首先JS的对象分两大类,函数对象和普通对象,每个对象均内置__proto__属性,在不人为赋值__ ...

  3. 深入分析JS原型链以及为什么不能在原型链上使用对象

    在刚刚接触JS原型链的时候都会接触到一个熟悉的名词:prototype:如果你曾经深入过prototype,你会接触到另一个名词:__proto__(注意:两边各有两条下划线,不是一条).以下将会围绕 ...

  4. 【09-23】js原型继承学习笔记

    js原型继承学习笔记 function funcA(){ this.a="prototype a"; } var b=new funcA(); b.a="object a ...

  5. js原型

    1.js基本类型和对象类型 js的简单类型包括数字(其中NaN为数字类型).字符串(类似'A'为字符,js没字符类型).布尔值.null值和undefined值.其他所有的值都是对象.数字.字符串和布 ...

  6. js原型链与继承(初体验)

    js原型链与继承是js中的重点,所以我们通过以下三个例子来进行详细的讲解. 首先定义一个对象obj,该对象的原型为obj._proto_,我们可以用ES5中的getPrototypeOf这一方法来查询 ...

  7. js原型解析

    我们都知道javascript因为具有了继承以及变量等等一系列的特性之后才被人们认为具有一门编程语言的资格,在后续的不断发展中,js在原生的基础上扩展了基于jquery等等的库,甚至衍生了像node. ...

  8. 深入理解JS原型链与继承

    我 觉得阅读精彩的文章是提升自己最快的方法,而且我发现人在不同阶段看待同样的东西都会有不同的收获,有一天你看到一本好书或者好的文章,请记得收藏起来, 隔断时间再去看看,我想应该会有很大的收获.其实今天 ...

  9. 学习zepto.js(原型方法)

    学习zepto.js(原型方法)[1] 转载 新的一周,新的开始,今天来学习一下zepto里边的原型方法,就是通过$.进行调用的方法,也是可以通过$.fn进行扩展的方法: $.camelCase(): ...

随机推荐

  1. [转载]Back up all of your mysql databases nightly

    原文地址:http://www.linuxbrigade.com/back-up-all-of-your-mysql-databases-nightly/ Put the following into ...

  2. java中使用sql的like关键字

    String sql = "select * from userinfo where nickname like ?"; PreparedStatement ps = conn.p ...

  3. linux下安装nodejs

    之前安装过windows下的node,感觉还是很方便的,不成想今天安装linux下的坑了老半天,特此记录. 1. 下载node.js,官方有提供源码版本和编译版的,方便起见我使用编译版的,下载后解压缩 ...

  4. QuickFIX/N 动态存储配置信息

    Acceptor或者Initiator能够为您维护尽可能多的FIX会话,因而FIX会话标识的唯一性非常重要.在QuickFIX/N中,一个FIX会话的唯一标识是由:BeginString(FIX版本号 ...

  5. LAMP平台搭建菜鸟入门级实验

    LAMP平台搭建(菜鸟入门级) mysql 安装: (1)二进制安装  二进制安装 ,执行解压配置即可.无须执行三布安装. (2)源码编译安装 安装准备工作: (1)查看系统配置:#uname -a/ ...

  6. 跨平台web调试代理工具---whistle

    whistle是基于Node实现的跨平台web调试代理工具,支持windows.mac.linux等所有安装了Node的操作系统,可以部署在本地机器.虚拟机或远程服务器,并通过本地网页查看或修改HTT ...

  7. Easy Multiple Copy to Clipboard by ZeroClipboard

    要实现在多个复制按钮复制的功能(具体代码在附件中,路径修改一下就行了): <%@ page language="java" import="java.util.*& ...

  8. zw版【转发&#183;台湾nvp系列Delphi例程】HALCON FillUp2

    zw版[转发·台湾nvp系列Delphi例程]HALCON FillUp2 procedure TForm1.Button1Click(Sender: TObject);var op : HOpera ...

  9. css禁止双击dom节点被选中user-select:none

    css禁止dom节点被选中:  当某个dom节点在快速重复点击的时候出现这个节点被选中,有时候其实并不希望出现这种情况,比如我们使用一个span或者a标签做为按钮dom元素的时候,快速双击这个按钮,就 ...

  10. MySQL的复制原理及配置

    MySQL 的数据库的高可用性的架构大概有以下几种:集群,读写分离,主备.而后面两种都是通过复制来实现的.下面将简单介绍复制的原理及配置,以及一些常见的问题. 一.复制的原理 MySQL 复制基于主服 ...