初看原型

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. 以向VS 程序打包集成自动写入注册表功能为例,介绍如何实现自由控制安装过程

    最近由于项目部署时需要更灵活的控制程序安装的流程以及自定义安装行为,特意研究了一下VS程序打包,把解决办法和大家分享一下. 以VS2010为例: 这是一个已经设置好最基本的Visual Studio ...

  2. jquery最常用的几个方法。

    jquery使用手册:http://www.eduyo.com/doc/jquery/cheatsheet.html addClass 样式: <style> .textRed { col ...

  3. FIFA halts 2026 bids amid scandal 国际足联在丑闻期间停止2026年足球世界杯申请

    FIFA halts 2026 bids amid scandal 国际足联在丑闻期间停止2026年足球世界杯申请 But official insists 2018 Cup will stay in ...

  4. juqery easyui

    私人做程序开发一直有个头疼的问题就是后台管理界面,以前一般都是自己用jquery+ps自己设计的,效果很一般,很不理想. 今天初次使用Jquery EasyUi,简单的做了个布局页面感觉还不错,给大家 ...

  5. 2016-06-06:X264码率控制

    H.264与x264 H264是一个视频压缩编码标准.https://zh.wikipedia.org/wiki/H.264/MPEG-4_AVC X264实现H264视频压缩标准的开源项目.http ...

  6. atitit.跨平台gui 概览

    atitit.跨平台gui 概览 为什么需要跨平台gui 国际上那些跨平台的GUI程序,除了像Firefox之类的大型项目会重写界面外,中小型的项目基本上都是用GTK+或WxWidgets为多.毕竟要 ...

  7. Ios开发之sqlite

    Sqlite是ios数据存储的一个重要手段,今天我们就一块来看一下,怎样使用sqlite将数据存储到沙盒中去. 第一步:导入一个框架libsqlite3.0.dylib 选中TARGETS在Gener ...

  8. svn: warning: &#39;xxxxxx&#39; is already under version control

    [root@NGINX-APACHE-SVN pm]# svn status ? plugins ? files ? images ? data ? resources [root@NGINX-APA ...

  9. 【VB6笔记-01】 读取Excel绑定到DataGrid

    Private Sub cmdOpen_Click() CommonDialog1.Filter = "Excel???t(*.xlsx)|*.xlsx" CommonDialog ...

  10. 对iframe跨域通信的封装

    github源码:https://github.com/boycy815/topProxy 为了偷懒所以依赖了Kissy:http://docs.kissyui.com/ 用法举例:需求是在http: ...