PS:
本文参考了司徒正美的《JavaScript框架设计》,以及许多其它的书籍和网络文章,感谢作者们分享了这么好的学习资源!
关于JS的类和继承的原理,见:JavaScript里的类和继承
文中提到的库和测试文件戳这里:https://github.com/hellobugme/jsclass/

1、基于拷贝继承


Prototypehttp://prototypejs.org/)和 jQueryhttps://jquery.com/)都是风靡一时的库,功能大而全,并不是纯粹的类工厂,这里只是简单说下里面的继承。
它们的 extend 都是基于拷贝继承,其中 jQuery 支持 深度拷贝(deep copy)。
拷贝继承的缺点在上篇文章中有提过:子类实例无法通过父类的 instanceof 验证。

Prototype 中类和继承相关的代码如下:

  1. var Class = {
  2. create: function() {
  3. return function() {
  4. this.initialize.apply(this, arguments);
  5. }
  6. }
  7. };
  8. Object.extend = function(destination, source) {
  9. for (var property in source) {
  10. destination[property] = source[property];
  11. }
  12. return destination;
  13. };

是吧,够简单的… 只不过是使用 Class.create() 创建出来的类,在实例化时会主动帮你执行下初始化函数 initialize 而已。使用方法大致如下:

  1. var Person = Class.create();
  2. Object.extend(Person.prototype, {
  3. initialize: function(name) {
  4. this.name = name;
  5. },
  6. getName: function() {
  7. return this.name;
  8. }
  9. });
  10. var User = Class.create();
  11. User.prototype = Object.extend(new Person(), {
  12. initialize: function(name, password) {
  13. this.name = name;
  14. this.password = password;
  15. },
  16. getPassword: function() {
  17. return this.password;
  18. }
  19. });

jQuery 在 extend 的实现中加入递归,对数组和对象等引用类型的属性值进行深度拷贝:

(... 表示一大波的代码,下同)

  1. jQuery.extend = jQuery.fn.extend = function() {
  2. //...
  3. for (name in options) {
  4. src = target[name];
  5. copy = options[name];
  6. //...
  7. if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
  8. if (copyIsArray) {
  9. copyIsArray = false;
  10. clone = src && jQuery.isArray(src) ? src : [];
  11. } else {
  12. clone = src && jQuery.isPlainObject(src) ? src : {};
  13. }
  14. // 通过递归对数组和对象进行深度拷贝
  15. target[name] = jQuery.extend(deep, clone, copy);
  16. } else if (copy !== undefined) {
  17. target[name] = copy;
  18. }
  19. }
  20. //...
  21. };

deep 是可选参数,指定是否进行深度拷贝。

通过 jQuery.isPlainObject() 和 jQuery.isArray() 判断属性值是否为纯粹对象或数组,然后决定是递归操作还是直接赋值。

2、Sugar


道爷(Douglas Crockford)在自己的网站上提出了一套简单的方法来模拟类式继承—— Sugarhttp://javascript.crockford.com/inheritance.html)。

  1. // 辅助函数,可以将新函数绑定到对象的 prototype 上
  2. Function.prototype.method = function(name, func) {
  3. this.prototype[name] = func;
  4. return this;
  5. };
  6.  
  7. // 从其它对象继承函数,同时仍然可以调用数据父对象的那些函数
  8. Function.method('inherits', function(parent) {
  9. // 继承父对象的方法
  10. this.prototype = new parent();
  11. this.prototype.constructor = parent;
  12.  
  13. var d = {},
  14. p = this.prototype;
  15. // 创建一个新的特权函数'uber',调用它时会执行所有在继承时被重写的函数
  16. this.method('uber', function uber(name) {
  17. if (!(name in d)) {
  18. d[name] = 0;
  19. }
  20. var f, // 要执行的函数
  21. r, // 函数的返回值
  22. t = d[name], // 记录当前所在的父层次的级数
  23. v = parent.prototype; // 父对象的prototype
  24.  
  25. // 如果已经在某个'uber'函数之内
  26. if (t) {
  27. // 上溯必要的t,找到原始的prototype
  28. while (t) {
  29. v = v.constructor.prototype;
  30. t -= 1;
  31. }
  32. // 从该prototype中获得函数
  33. f = v[name];
  34. // 否则这就是'uber'函数的第一次调用
  35. } else {
  36. // 从prototype获得要执行的函数
  37. f = p[name];
  38. // 如果此函数属于当前的prototype
  39. if (f == this[name]) {
  40. // 则改为调用父对象的prototype
  41. f = v[name];
  42. }
  43. }
  44.  
  45. // 记录在继承堆栈中所在位置的级数
  46. d[name] += 1;
  47.  
  48. // 使用除第一个以外所有的arguments调用此函数
  49. r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
  50. // 恢复继承堆栈
  51. d[name] -= 1;
  52.  
  53. // 返回执行过的函数的返回值
  54. return r;
  55. });
  56. return this;
  57. });
  58.  
  59. // 只继承父对象特定函数的函数(非使用new parent()继承所有的函数)
  60. Function.method('swiss', function(parent) {
  61. // 遍历所有要继承的方法
  62. for (var i = 1; i < arguments.length; i += 1) {
  63. // 需要导入的方法名
  64. var name = arguments[i];
  65. // 将此方法导入this对象的prototype中
  66. this.prototype[name] = parent.prototype[name];
  67. }
  68. return this;
  69. });

因为JS中的类本身就是一个函数,所以直接为 Function 的原型扩展了 method、inherits、swiss 三个方法,所有类也能获得这些方法。

重点在于 inherits 中添加的特权方法“uber”(为什么不叫“super”叫“uber”?难道道爷是优步忠实粉?好吧,很冷…),它根据继承堆栈级数回溯到相应的祖先类原型方法,让你在子类中可以非常方便地调用被重写的父类原型方法。

  1. function Person(name) {
  2. this.name = name;
  3. }
  4. Person.method('getName', function() {
  5. return name;
  6. });
  7.  
  8. function User(name, password) {
  9. this.name = name;
  10. this.password = password;
  11. }
  12. User.inherits(Person);
  13. User.method('getPassword', function() {
  14. return this.password;
  15. });
  16. // 重写父类原型方法
  17. User.method('getName', function() {
  18. // 通过 uber() 调用父类的 getName()
  19. return "My name is: " + this.uber('getName');
  20. });

3、Base


Basehttp://dean.edwards.name/base/)是 Dean Edwards 开发的一个类工厂库,它提供了一套比较直观的对象继承方法,对后来许多类工厂的实现有不小的影响,如:
JS.Classhttp://dkraczkowski.github.io/js.class/)、
simple-inheritancehttp://ejohn.org/blog/simple-javascript-inheritance/
都借鉴了 Base 的继承系统。

Base 的代码较长,忽略具体实现,只留关键步骤后大致如下:

  1. var Base = function() {
  2. // 并没有什么卵用
  3. };
  4.  
  5. //Base.extend 方法功能:
  6. //1、创建子类;2、继承父类;3、返回子类
  7. Base.extend = function(_instance, _static) {
  8. var proto = new this; //原型继承,proto将作为子类的prototype
  9. Base.prototype.extend.call(proto, _instance); //给子类添加原型方法
  10. //...
  11. var klass = function() {}; //子类
  12. klass.prototype = proto;
  13. Base.prototype.extend.call(klass, _static); //给子类添加特权方法
  14. //...
  15. return klass; //返回子类
  16. };
  17.  
  18. //Base.prototype.extend 方法功能:
  19. //复制新的成员到子类
  20. Base.prototype = {
  21. extend: function(source, value) {
  22. if (arguments.length > 1) {
  23. //传入的是2个参数,为键值对,直接给this的source赋值为value
  24. //该方法通过call来调用,这里的执行环境this为调用时传入的环境
  25. //...
  26. this[source] = value;
  27. } else {
  28. //传入的是1个参数,为对象,遍历对象,把对象的所有属性复制给this
  29. //...
  30. for (var key in source) {
  31. Base.prototype.extend.call(this, key, source[key]);
  32. }
  33. }
  34. return this;
  35. }
  36. };
  37.  
  38. //初始化Base
  39. Base = Base.extend({
  40. constructor: function() { /*...*/ }
  41. }, {
  42. ancestor: Object,
  43. version: "1.1"
  44. // ...
  45. });

Base 通过两个 extend 方法实现了类的创建和继承:

  1. Base.extend():这个直接添加在 Base 上的静态方法,实现了类的创建、类的继承;
  2. Base.prototype.extend():这个添加在 Base 原型上的方法,其实就是个工具函数,实现了属性拷贝。

调用 Base.extends() 创建子类的过程如下:

  1. 创建子类原型,将父类的实例赋值给子类原型
  2. 将参数中传入的其它子类原型成员通过 Base.prototype.extend() 复制给子类原型
  3. 创建子类,将上面处理后的子类原型赋值给子类的原型属性
  4. 将参数中传入的初始化函数中的特权成员通过 Base.prototype.extend() 复制给子类

Base 的使用方法如下:

  1. var Person = Base.extend({
  2. constructor: function(name) {
  3. this.name = name;
  4. },
  5. getName: function() {
  6. return this.name;
  7. }
  8. });
  9. var User = Person.extend({
  10. constructor: function(name, password) {
  11. this.base(name);
  12. this.password = password;
  13. },
  14. getPassword: function() {
  15. return this.password;
  16. }
  17. });

一个代码块就创建了一个类并实现了继承,用起来是不是很爽?

4、P.js


P.jshttps://github.com/jneen/pjs) 是一个非常精巧的库,它最大的特点是直接把父类的原型抛出来,在调用父类方法时非常方便。

  1. var P = (function(prototype, ownProperty, undefined) {
  2. return function P(_superclass /* = Object */ , definition) {
  3. // 如果只有一个参数,表示没有父类
  4. if (definition === undefined) {
  5. definition = _superclass;
  6. _superclass = Object;
  7. }
  8.  
  9. // C为要返回的子类,init为初始化函数
  10. function C() {
  11. var self = this instanceof C ? this : new Bare;
  12. self.init.apply(self, arguments);
  13. return self;
  14. }
  15.  
  16. // Bare让C不用new就能返回实例
  17. function Bare() {}
  18. C.Bare = Bare;
  19.  
  20. // 为了防止改动子类影响到父类,所以将父类的原型赋值给中介者Bare,然后再将Bare的实例作为子类的原型
  21. var _super = Bare[prototype] = _superclass[prototype];
  22. var proto = Bare[prototype] = C[prototype] = C.p = new Bare;
  23.  
  24. var key;
  25.  
  26. // 修正子类的构造器函数,使其指向自身
  27. proto.constructor = C;
  28.  
  29. C.extend = function(def) {
  30. return P(C, def);
  31. }
  32.  
  33. return (C.open = function(def) {
  34. // 如果def是函数,则直接调用,并传入子类原型、父类原型、子类构造器、父类构造器
  35. if (typeof def === 'function') {
  36. def = def.call(C, proto, _super, C, _superclass);
  37. }
  38.  
  39. // 如果def是对象,则是子类的扩展包,将其属性添加到子类原型
  40. if (typeof def === 'object') {
  41. for (key in def) {
  42. // ownProperty其实就是传入的Object.hasOwnProperty
  43. if (ownProperty.call(def, key)) {
  44. proto[key] = def[key];
  45. }
  46. }
  47. }
  48.  
  49. // 确保有初始化函数可以调用
  50. if (!('init' in proto)) proto.init = _superclass;
  51.  
  52. return C;
  53. })(definition);
  54. }
  55.  
  56. })('prototype', ({}).hasOwnProperty);

里面有几个关键点:

  1. P 是一个函数,调用时会自动生成一个类
  2. 当给 P 传入一个参数且该参数为函数时,P 默认父类为 Object,然后调用该函数,并将创建的类的原型和父类原型传给它,所以你可以在该函数中使用第一个参数为创建的类添加原型成员
  3. 当给 P 传入两个参数时,第一个参数为父类,P 会通过原型式寄生组合继承实现原型继承,使创建的类继承父类原型;第二个参数为函数,原理同 2
  4. P 在处理完传入的参数后,会调用创建的类的原型方法 init() 进行初始化,从而实现特权成员的添加

P.js 的使用方法如下:

  1. // 将一个函数传给P,P会先创建一个类,然后后调用该函数,并将创建的类的原型和父类原型传入,所以在该函数中可以通过第一个参数来给类添加原型成员
  2. var Person = P(function(proto, superProto) {
  3. // init为初始化函数,P会在完成类的处理后自动调用该方法
  4. proto.init = function(name) {
  5. // 添加特权成员
  6. this.name = name;
  7. };
  8. proto.getName = function() {
  9. return this.name;
  10. };
  11. });
  12. var User = P(Person, function(user, person) {
  13. user.init = function(name, password) {
  14. // 通过P传入的父类原型,可以很方便的访问父类原型成员
  15. person.init.call(this, arguments);
  16. this.password = password;
  17. };
  18. user.getPassword = function() {
  19. return this.password;
  20. };
  21. });

妈妈再也不用担心我调不到父类方法了!

5、def.js


如果说有什么库最能体现 JS 的灵活性,def.jshttp://badassjs.com/post/811837523/def-js-ruby-style-inheritance-in-javascript) 肯定名列前茅!
它试图在形式上模拟 Ruby 那种继承,让学过 Ruby 的人一眼就看到哪个是父类,哪个是子类。

Ruby 的继承是这样的:

  1. class User < Person
  2. #...
  3. end

而 def.js 能做到这样:

  1. def("Person")({
  2. init: function(name) {
  3. this.name = name;
  4. },
  5. getName: function() {
  6. return this.name;
  7. }
  8. });
  9.  
  10. def("User") < Person({
  11. init: function(name, password) {
  12. this._super();
  13. this.password = password;
  14. },
  15. getPassword: function() {
  16. return this.password;
  17. }
  18. });

是不是很神奇!!

这里面有几个魔法:

  1. def()():能这么用,说明 def() 执行后返回的是一个函数
  2. “<”:就这么一个操作符,它是怎么实现继承的?
  3. _super():简单的一行 this._super(),就神奇地调用了父类同名方法!

先来说说“<”操作符的运用。
在 JS 中,当两个对象进行算术运算或大小比较时,如果它们不是数值,则会尝试将其转换为数值,即调用其 valueOf 方法。
“<”操作符的目的就是强制两边计算自身,从而调用自己的 valueOf 方法,def.js 就是通过重写父类与子类的 valueOf,在里面偷偷实现了原型继承。
先来看一个简单的例子:

  1. var a = {
  2. valueOf: function() {
  3. console.log("aaaaa");
  4. }
  5. },
  6. b = {
  7. valueOf: function() {
  8. console.log("bbbbb");
  9. }
  10. };
  11. console.log(a < b);

执行结果如下:

可以看到,先是执行了 a 的 valueOf(),然后执行了 b 的 valueOf(),最后返回比较结果false。

再来看看 def.js 的源码,我们在其中几个关键点输出日志(代码中的console.log):

  1. (function(global) {
  2. // deferred 是整个库中最重要的部件,扮演3个角色
  3. // 1、def("SuperClass")时就是返回deferred,此时我们可以直接接括号对原型进行扩展
  4. // 2、在继承父类时 < 触发两者调用 valueOf,此时会执行 deferred.valueOf 里面的逻辑
  5. // 3、在继承父类时,父类的后面还可以接括号(此时构造器当普通函数使用),当作传送器,保存着父类与扩展包到 _super._props
  6. var deferred;
  7.  
  8. // 扩展自定义的原型
  9. function extend(source) {
  10. var prop, target = this.prototype;
  11.  
  12. for (var key in source)
  13. if (source.hasOwnProperty(key)) {
  14. prop = target[key] = source[key];
  15. if ('function' == typeof prop) {
  16. // 在每个原型方法上添加2个自定义属性,保存其名字与当前类
  17. prop._name = key;
  18. prop._class = this;
  19. }
  20. }
  21.  
  22. return this;
  23. }
  24.  
  25. function base() {
  26. // 取得调用 this._super() 这个函数本身,如果是在init内,那么就是当前类
  27. var caller = arguments.callee.caller;
  28. // 执行父类的同名方法,有2种形式,一是用户自己传,二是只能取当前函数的参数
  29. return caller._class._super.prototype[caller._name]
  30. .apply(this, arguments.length ? arguments : caller.arguments);
  31. }
  32.  
  33. function def(context, klassName) {
  34. console.log("def called");
  35. klassName || (klassName = context, context = global);
  36. // 偷偷在给定的全局作用域或某对象上创建一个类
  37. var Klass = context[klassName] = function Klass() {
  38. if (context != this) {
  39. // 如果不使用 new 操作符,大多数情况下 context 与 this 都为 window
  40. return this.init && this.init.apply(this, arguments);
  41. }
  42. // 实现继承的第二步,复制自身与扩展包到 deferred
  43. console.log("Klass constructor called");
  44. deferred._super = Klass;
  45. deferred._props = arguments[0] || {};
  46. }
  47.  
  48. // 让所有自定义类都共用一个 extend 方法
  49. Klass.extend = extend;
  50.  
  51. // 实现继承的第一步,重写 deferred,为新创建的自定义类的扩展函数
  52. deferred = function(props) {
  53. return Klass.extend(props);
  54. };
  55.  
  56. // 一个中介者,用于切断子类与父类的原型连接,会被反复读写
  57. function Subclass() {}
  58.  
  59. // 实现继承的第三步,重写 valueOf,方便在 def("SubClass") < SuperClass({}) 执行它
  60. deferred.valueOf = function() {
  61. console.log("deferred.valueOf called");
  62. var Superclass = deferred._super;
  63.  
  64. if (!Superclass) {
  65. return Klass;
  66. }
  67. // 先将父类的原型赋给中介者,然后再将中介者的实例作为子类的原型
  68. Subclass.prototype = Superclass.prototype;
  69. var proto = Klass.prototype = new Subclass;
  70. // 引用自身与父类
  71. Klass._class = Klass;
  72. Klass._super = Superclass;
  73. // 获取该类名字
  74. Klass.toString = function() {
  75. return klassName;
  76. };
  77. // 修复原型中 constructor 指向自身
  78. proto.constructor = Klass;
  79. // 让所有自定义类都共用这个 base 方法,它是构成方法链的关系
  80. proto._super = base;
  81. // 把父类后来传入的扩展包混入子类的原型中
  82. deferred(deferred._props);
  83. };
  84.  
  85. return deferred;
  86. }
  87.  
  88. global.def = def;
  89. }(this));

结果如下:

第一个“def called”是创建 Person 时输出的,忽略。
从接下来 3 个日志的输出顺序可以推测出,def("User") < Person({/*...*/}) 执行顺序如下:

  1. 执行 def("User"),创建了 User 类,重新擦写了 deferred 和 deferred.valueOf
  2. 执行 Person({/*...*/}),Person 作为普通函数接受子类的扩展包 {/*...*/},从而使 def.js 中的 deferred._super = Person,deferred._props = {/*...*/}
  3. “<”左边计算自身,执行 valueOf 操作,继承了 deferred._super 中存储的父类的原型,并拷贝了 deferred._props 中存储的子类扩展包

太妙了!!这需要对 def("User") < Person({/*...*/}) 这一行代码各个部分的执行顺序有充分的了解,才能做出如此巧妙的设计。

接下来说说第 3 点:_super 的原理。
这里其实是对 arguments.callee.caller 的运用,通过 caller 可以获取到当前调用的原型方法的引用。
而 def.js 在 extend() 中为每个原型方法都添加了 _name(方法名)和 _class(指向当前类)两个属性,因此,通过 caller._name 可以获得方法名,caller._class._super 可以获得父类引用,有了这两样东西,自然就能调用父类同名方法了。
不过遗憾的是, caller 是被废弃的属性,在 ES5 的严格模式下不可用。当然也可以修改,只是会少了些智能化。

JavaScript几种类工厂实现原理剖析的更多相关文章

  1. 基本功 | Litho的使用及原理剖析

    1. 什么是Litho? Litho是Facebook推出的一套高效构建Android UI的声明式框架,主要目的是提升RecyclerView复杂列表的滑动性能和降低内存占用.下面是Litho官网的 ...

  2. c#封装DBHelper类 c# 图片加水印 (摘)C#生成随机数的三种方法 使用LINQ、Lambda 表达式 、委托快速比较两个集合,找出需要新增、修改、删除的对象 c# 制作正方形图片 JavaScript 事件循环及异步原理(完全指北)

    c#封装DBHelper类   public enum EffentNextType { /// <summary> /// 对其他语句无任何影响 /// </summary> ...

  3. 《java学习三》并发编程 -------线程池原理剖析

    阻塞队列与非阻塞队 阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞.试图从空的阻塞队列中获取元素的线程将会被阻塞,直到 ...

  4. JavaScript中的工厂方法、构造函数与class

    JavaScript中的工厂方法.构造函数与class 本文转载自:众成翻译 译者:谢于中 链接:http://www.zcfy.cc/article/1129 原文:https://medium.c ...

  5. Kotlin泛型与协变及逆变原理剖析

    在上一次https://www.cnblogs.com/webor2006/p/11234941.html中学习了数据类[data class]相关的知识,这次会学习关于泛型相关的东东,其中有关于泛型 ...

  6. CDN 工作原理剖析

    CDN 工作原理剖析 CDN / Content Delivery Network / 内容分发网络 https://www.cloudflare.com/zh-cn/learning/cdn/wha ...

  7. ext文件系统机制原理剖析

    本文转载自ext文件系统机制原理剖析 导语 将磁盘进行分区,分区是将磁盘按柱面进行物理上的划分.划分好分区后还要进行格式化,然后再挂载才能使用(不考虑其他方法).格式化分区的过程其实就是创建文件系统. ...

  8. ASP.NET Core 运行原理剖析2:Startup 和 Middleware(中间件)

    ASP.NET Core 运行原理剖析2:Startup 和 Middleware(中间件) Startup Class 1.Startup Constructor(构造函数) 2.Configure ...

  9. ASP.NET Core 运行原理剖析1:初始化WebApp模版并运行

    ASP.NET Core 运行原理剖析1:初始化WebApp模版并运行 核心框架 ASP.NET Core APP 创建与运行 总结 之前两篇文章简析.NET Core 以及与 .NET Framew ...

随机推荐

  1. iOS-开发日志-UITextView介绍

    UITextView 属性 1.     text: 设置textView中文本 _textView.text = @"Now is the time for all good develo ...

  2. IOS开发之KVC与KVO简述

    KVC:Key-Value Coding KVO:Key-Value Observing Person.m #import <Foundation/Foundation.h> @inter ...

  3. 互联网金融爬虫怎么写-第三课 雪球网股票爬虫(ajax分析)

    大家好啊,话说好久没有出来活动了,组织上安排写代码写了很久,终于又被放出来写教程了,感谢大家一直的支持和厚爱,我会一如既往的帮助大家完成爬虫工程师从入门到放弃的升华. 好,Previous on  系 ...

  4. == 和 equals()的区别

    package com.liaojianya.chapter1; /** * This program demonstrates the difference between == and equal ...

  5. 关于Spring AOP和IOC的一些总结

    Spring官方网站:https://spring.io/ 最早对象的创建是有new关键字,但是如果产生的类比较繁多或者复杂,就用工厂代替new关键字,但是工厂的控制能力有限,譬如对产生对象的生命周期 ...

  6. bzoj1007:[HNOI2008]水平可见直线

    思路:首先按斜率排序,如果斜率相同就取截距最大的,显然截距小的会被覆盖而对答案没有贡献,然后考虑斜率不同的如何统计答案,可以用一个单调栈维护,当前新插入的直线显然斜率是要比当前栈顶斜率要大的,然后如果 ...

  7. IOS分类(Category)

    分类(Category):拓展原有类的方法,而不影响原有类的子类,分类中不能创建成员变量. 分类的使用: 1.创建分类: 如图点击(File)选择(New)->(File).. 或者使用快捷键c ...

  8. php 简单的验证码

    注意事项: 验证的隐藏域的位置一定要在调用JS前.. 如: <input type="text" name="yzm" value="" ...

  9. Yii 权限分级式访问控制实现(非RBAC法)

    以下由我们在信易网络公司开发项目的时候终结出的一些经验 主要参考资料:yii官网http://www.yiiframework.com/wiki/60/yii framework 提供了2套权限访问系 ...

  10. to disable the entity lazy load, The ObjectContext instance has been disposed and can no longer be used for operations that require a connection.

    The ObjectContext instance has been disposed and can no longer be used for operations that require a ...