JS面向对象编程详解


Posted in Javascript onMarch 06, 2016

序言

在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?

JS面向对象编程详解

所以大多数语言并不支持多继承,如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 相关文章推荐
项目实践之javascript技巧
Dec 06 Javascript
js用typeof方法判断undefined类型
Jul 15 Javascript
JavaScript 实现的 zip 压缩和解压缩工具包Zip.js使用详解
Dec 14 Javascript
Active控件问题小结(附解决办法)
Jun 09 Javascript
基于Vue2实现的仿手机QQ单页面应用功能(接入聊天机器人 )
Mar 30 Javascript
js 获取元素的具体样式信息getcss(实例讲解)
Jul 05 Javascript
使用javaScript实现鼠标拖拽事件
Apr 03 Javascript
javascript实现文件拖拽事件
Mar 29 Javascript
小白教程|一小时上手最流行的前端框架vue(推荐)
Apr 10 Javascript
详解vue 动态加载并注册组件且通过 render动态创建该组件
May 30 Javascript
vue实现导航标题栏随页面滚动渐隐渐显效果
Mar 12 Javascript
微信小程序开发打开另一个小程序的实现方法
May 17 Javascript
深入学习JavaScript的AngularJS框架中指令的使用方法
Mar 05 #Javascript
使用Jasmine和Karma对AngularJS页面程序进行测试
Mar 05 #Javascript
JavaScript的React框架中的JSX语法学习入门教程
Mar 05 #Javascript
在AngularJS框架中处理数据建模的方式解析
Mar 05 #Javascript
简单讲解AngularJS的Routing路由的定义与使用
Mar 05 #Javascript
整理AngularJS框架使用过程当中的一些性能优化要点
Mar 05 #Javascript
详解JavaScript的AngularJS框架中的表达式与指令
Mar 05 #Javascript
You might like
用PHP代替JS玩转DOM的思路及示例代码
2014/06/15 PHP
ThinkPHP分页实例
2014/10/15 PHP
PHP原生函数一定好吗?
2014/12/08 PHP
两种php去除二维数组的重复项方法
2015/11/04 PHP
php 使用fopen函数创建、打开文件详解及实例代码
2016/09/24 PHP
2017年最好用的9个php开发工具推荐(超好用)
2017/10/23 PHP
PHP设计模式之模板方法模式实例浅析
2018/12/20 PHP
总结PHP中初始化空数组的最佳方法
2019/02/13 PHP
Javascript实例教程(19) 使用HoTMetal(3)
2006/12/23 Javascript
JavaScript Date对象使用总结
2009/05/14 Javascript
理解Javascript_03_javascript全局观
2010/10/11 Javascript
JavaScript与jQuery实现的闪烁输入效果
2016/02/18 Javascript
初步使用Node连接Mysql数据库
2016/03/03 Javascript
jQuery ajax提交Form表单实例(附demo源码)
2016/04/06 Javascript
深入理解jquery中的事件与动画
2016/05/24 Javascript
原生JS查找元素的方法(推荐)
2016/11/22 Javascript
详解JavaScript中this的指向问题
2017/01/20 Javascript
vue指令做滚动加载和监听等
2019/05/26 Javascript
[02:20]DOTA2亚洲邀请赛 IG战队出场宣传片
2015/02/07 DOTA
python对html代码进行escape编码的方法
2015/05/04 Python
Python中str.join()简单用法示例
2018/03/20 Python
Python实现读取SQLServer数据并插入到MongoDB数据库的方法示例
2018/06/09 Python
浅谈Django中view对数据库的调用方法
2019/07/18 Python
django3.02模板中的超链接配置实例代码
2020/02/04 Python
Django Admin 上传文件到七牛云的示例代码
2020/06/20 Python
python跨文件使用全局变量的实现
2020/11/17 Python
css3 实现滚动条美化效果的实例代码
2021/01/06 HTML / CSS
用html5的canvas画布绘制贝塞尔曲线完整代码
2013/08/14 HTML / CSS
德国低价购买灯具和家具网站:Style-home.de
2016/11/25 全球购物
纽约著名的服装辅料来源:M&J Trimming
2017/07/26 全球购物
汽车检测与维修专业求职信
2013/10/30 职场文书
数学系毕业生的自我评价
2014/01/10 职场文书
公司面试感谢信
2014/02/01 职场文书
幼儿园春季开学寄语
2014/04/03 职场文书
后勤工作个人总结
2015/02/28 职场文书
同意报考证明
2015/06/17 职场文书