JavaScript面向对象之我见-阿里云开发者社区

开发者社区> 木的树> 正文

JavaScript面向对象之我见

简介:
+关注继续查看

序言

  在JavaScript的大世界里讨论面向对象,都要提到两点:1.JavaScript是一门基于原型的面向对象语言 2.模拟类语言的面向对象方式。对于为什么要模拟类语言的面向对象,我个人认为:某些情况下,原型模式能够提供一定的便利,但在复杂的应用中,基于原型的面向对象系统在抽象性与继承性方面差强人意。由于JavaScript是唯一一个被各大浏览器支持的脚本语言,所以各路高手不得不使用各种方法来提高语言的便利性,优化的结果就是其编写的代码越来越像类语言中的面向对象方式,从而也掩盖了JavaScript原型系统的本质。

  

基于原型的面向对象语言

  原型模式如类模式一样,都是是一种编程泛型,即编程的方法论。另外最近大红大紫的函数编程也是一种编程泛型。JavaScript之父Brendan Eich在设计JavaScript时,从一开始就没打算为其加入类的概念,而是借鉴了另外两门基于原型的的语言:Self和Smalltalk。

  既然同为面向对象语言,那就得有创建对象的方法。在类语言中,对象基于模板来创建,首先定义一个类作为对现实世界的抽象,然后由类来实例化对象;而在原型语言中,对象以克隆另一个对象的方式创建,被克隆的母体称为原型对象。

  克隆的关键在于语言本身是否为我们提供了原生的克隆方法。在ECMAScript5中,Object.create可以用来克隆对象。


var person = {
    name: "tree",
    age: 25,
    say: function(){
        console.log("I'm tree.")
    }
};

var cloneTree = Object.create(person);
console.log(cloneTree);

原型模式的目的并不在于得到一个一模一样的对象,而提供了一种便捷的方式去创建对象(出自《JavaScript设计模式与开发实践》)。但是由于语言设计的问题,JavaScript的原型存在着诸多矛盾,它的某些复杂的语法看起来就那些基于类的语言,这些语法问题掩盖了它的原型机制(出自《JavaScript语言精粹》)。如:


function Person(name, age){
    this.name = name;
    this.age = age;           
}

var p = new Person('tree', 25)


实际上,当一个函数对象呗创建时,Function构造器产生的函数对象会运行类似这样的一些代码:

this.prototype = {constructor: this}

新的函数对象被赋予一个prototype属性,它的值是一个包含constructor属性且属性值为该新函数的对象。当对一个函数使用new运算符时,函数的prototype的属性的值被作为原型对象来克隆出新对象。如果new运算符是一个方法,它的执行过程如下:


Function.prorotype.new = function() {
    //以prototype属性值作为原型对象来克隆出一个新对象
    var that = Object.create(this.prorotype);
    
    //改变函数中this关键指向这个新克隆的对象
    var other = this.apply(that, arguments);
    
    //如果返回值不是一个对象,则返回这个新克隆对象
    return (other && typeof other === 'object') ? other : that;
}

 从上面可以看出,虽然使用new运算符调用函数看起来像是使用模板实例化的方式来创建对象,但本质还是以原型对象来克隆出新对象。

   由于新克隆的对象能否访问到原型对象的一切方法和属性,加上new运算符的特性,这便成了利用原型模拟类式语言的基石。

 

利用原型模拟类式语言

  抽象

  用原型模式来模拟类,首先是抽象方式。根据JavaScript语言的特点,通常一个类(实际上是伪类)通常是将字段放置于构造函数(实际上是new 运算符调用的函数,JavaScript本身并没有构造函数的概念)中,而将方法放置于函数的prototype属性里。


function Person(name, age) {
    this.name = name;
    this.age = age;
};

Person.prototype.say = function(){
    console.log("Hello, I'm " + this.name);
};

 继承

  继承是OO语言中的一个最为人津津乐道的概念。许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承之继承方法签名,而实现继承则继承实际的方法。但是ECMAScript中无法实现接口继承,只支持实现继承,而且其实现继承主要是依靠原型链来实现的。(出自《JavaScript高级程序设计》 6.3节——继承)在高三中作者探索了各种关于继承的模拟,如:组合继承、原型继承、寄生继承、寄生组合继承,最终寄生组合式成为所有模拟类式继承的基础。


function Person(name, age) {
    this.name = name;
    this.age = age;
};

Person.prototype.say = function(){
    console.log("Hello, I'm " + this.name);
};

function Employee(name, age, major) {
    Person.apply(this, arguments);
    this.major = major;
};

Employee.prototype = Object.create(Person.prototype);
Employee.prorotype.constructor = Employee;

Employee.prorotype.sayMajor = function(){
    console.log(this.major);
}

高三中只给出了单继承的解决方案,关于多继承的模拟我们还得自己想办法。由于多继承有其本身的困难:面向对象语言如果支持了多继承的话,都会遇到著名的菱形问题(Diamond Problem)。假设存在一个如左图所示的继承关系,O中有一个方法foo,被A类和B类覆写,但是没有被C类覆写。那么C在调用foo方法的时候,究竟是调用A中的foo,还是调用B中的foo?

  所以大多数语言并不支持多继承,如Java支持单继承+接口的形式。JavaScript并不支持接口,要在一个不支持接口的语言上去模拟接口怎么办?答案是著名的鸭式辨型。放到实际代码中就是混入(mixin)。原理很简单:


 function mixin(t, s) {
        for (var p in s) {
            t[p] = s[p];
        }
    }

 值得一提的是dojo利用MRO(方法解析顺序(Method Resolution Order),即查找被调用的方法所在类时的搜索顺序)方式解决了多继承的问题。

  

  到此,我们已经清楚了模拟类语言的基本原理。作为一个爱折腾的程序员,我希望拥有自己的方式来简化类的创建:

  • 提供一种便利的方式去创建类,而不暴露函数的prototype属性
  • 在子类中覆盖父类方法时,能够像Java一样提供super函数,来直接访问父类同名方法
  • 以更方便的方式添加静态变量和方法而不去关心prototype
  • 像C#那样支持Attribute

   最终,在借鉴各位大牛的知识总结,我编写了自己的类创建工具O.js:


(function(global) {
    var define = global.define;
    if (define && define.amd) {
        define([], function(){
            return O;
        });
    } else {
        global.O = O;
    }

    function O(){};

    O.derive = function(sub) {
        debugger;
        var parent = this;
        sub = sub ? sub : {};

        var o = create(parent);
        var ctor = sub.constructor || function(){};//如何调用父类的构造函数?
        var statics = sub.statics || {};
        var ms = sub.mixins || [];
        var attrs = sub.attributes || {};

        delete sub.constructor;
        delete sub.mixins;
        delete sub.statics;
        delete sub.attributes;

        //处理继承关系
        ctor.prototype = o;
        ctor.prototype.constructor = ctor;
        ctor.superClass = parent;
        //利用DefineProperties方法处理Attributes
        //for (var p in attrs) {
            Object.defineProperties(ctor.prototype, attrs);
        //}
        //静态属性
        mixin(ctor, statics);
        //混入其他属性和方法,注意这里的属性是所有实例对象都能够访问并且修改的
        mixin(ctor.prototype, sub);
        //以mixin的方式模拟多继承
        for (var i = 0, len = ms.length; i < len; i++) {
            mixin(ctor.prototype, ms[i] || {});
        }

        ctor.derive = parent.derive;
        //_super函数
        ctor.prototype._super = function(f) {
            debugger;
            return parent.prototype[f].apply(this, Array.prototype.slice.call(arguments, 1));
        }

        return ctor;
    }

    function create(clazz) {
        var F = function(){};
        F.prototype = clazz.prototype;
        //F.prototype.constructor = F; //不需要
        return new F();
    };

    function mixin(t, s) {
        for (var p in s) {
            t[p] = s[p];
        }
    }
})(window);

类创建方式如下:


var Person = O.derive({
    constructor: function(name) {//构造函数
        this.setInfo(name);
    },
    statics: {//静态变量
        declaredClass: "Person"
    },
    attributes: {//模拟C#中的属性
        Name: {
            set: function(n) {
                this.name = n;
                console.log(this.name);
            },
            get: function() {
                return this.name + "Attribute";
            }
        }
    },
    share: "asdsaf",//变量位于原型对象上,对所有对象共享
    setInfo: function(name) {//方法
        this.name = name;
    }
});
var p = new Person('lzz');
console.log(p.Name);//lzzAttribute
console.log(Person);

继承:


var Employee = Person.derive({//子类有父类派生
    constructor: function(name, age) {
        this.setInfo(name, age);
    },
    statics: {
        declaredClass: "Employee"
    },
    setInfo: function(name, age) {
        this._super('setInfo', name);//调用父类同名方法
        this.age = age;
    }
});

var e = new Employee('lll', 25);
console.log(e.Name);//lllAttribute
console.log(Employee);


  参考文章:

  [原创]JavaScript继承详解

  Douglas Crockford - Prototypal Inheritance in JavaScript

  Douglas Crockford - Classical Inheritance in JavaScript

   JavaScript实现继承的几种方式

  A Base Class for JavaScript Inheritance

  DOJO中的面向对象__第三章 Dojo中的多继承

   《JavaScript语言精粹》

  《JavaScript设计模式与开发实践》

  《JavaScript高级程序设计第三版》


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
《JavaScript面向对象编程指南(第2版)》——导读
本书是《JavaScript面向对象编程指南》的第二版。前一版由Stoyan Stefanov著(Packet出版社发行),在业界广受好评。然而,自第一版发行至今已过了五个年头。期间,JavaScript由一项主要适用于浏览器客户端的计算机技术,逐渐发展成为一种多功能的程序设计语言,甚至连服务端也能由它来编写。
1171 0
《JavaScript面向对象编程指南(第2版)》——1.6 面向对象的程序设计
现在,我们就来详细了解每个概念。当然,如果您在面向对象程序设计方面是一个新手,或者不能确定自己是否真的理解了这些概念,那也不必太过担心。以后我们还会通过一些代码来为您具体分析它们。尽管这些概念说起来好像很复杂、很高级,但一旦我们进入真正的实践,事情往往就会简单得多。
1313 0
怎么设置阿里云服务器安全组?阿里云安全组规则详细解说
阿里云服务器安全组设置规则分享,阿里云服务器安全组如何放行端口设置教程
7007 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
4519 0
使用OpenApi弹性释放和设置云服务器ECS释放
云服务器ECS的一个重要特性就是按需创建资源。您可以在业务高峰期按需弹性的自定义规则进行资源创建,在完成业务计算的时候释放资源。本篇将提供几个Tips帮助您更加容易和自动化的完成云服务器的释放和弹性设置。
7830 0
JavaScript 面向对象编程之一
一:Class and private And public JS 中的类以 function 进行声明,同时 JS 也支持声明私有 private 和公有 public 成员,只不过跟 C# 不一样,它们不是使用这两个关键字实现的。
629 0
+关注
86
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
文娱运维技术
立即下载
《SaaS模式云原生数据仓库应用场景实践》
立即下载
《看见新力量:二》电子书
立即下载