65.9K
CodeProject 正在变化。 阅读更多。
Home

Jooshe - JavaScript 面向对象 HTML 元素子类化

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2013年12月20日

MIT

7分钟阅读

viewsIcon

24655

downloadIcon

160

Jooshe 很有料!

引言

面向对象的 JavaScript 开发缺少一项关键功能——继承 HTML 元素的能力。

本文提出了一个解决方案。

使用 Jooshe

加载 Jooshe JavaScript 库

<script src="jooshe.js"></script>

<script src="jooshe.min.js"></script>

<script> 元素的 src 属性必须指向 Jooshe 的副本。

下载中提供了 Jooshe 的副本以及所有后续的测试代码。

兼容性

所有主流浏览器,IE 9+

回顾面向对象的 JavaScript

我们将从快速回顾面向对象的 JavaScript 开始。JavaScript 中最基本的类是函数

function myClass(){}

使用 new 操作符创建一个类的实例

var myInstance = new myClass;

通过向其原型添加功能来扩展类

function myClass(){}
myClass.prototype.sayHi = function(){ console.log("Hi!"); };

var myInstance = new myClass;
myInstance.sayHi();

继承

子类继承其父类的所有属性和方法

function mySubClass(){}
mySubClass.prototype = new myClass;                                    // [1]
mySubClass.prototype.constructor = mySubClass;                         // [2]
mySubClass.prototype.sayHi = function(){ console.log("Hi there!");     // [3]
mySubClass.prototype.sayBye = function(){ console.log("Goodbye."); };  // [4]

注释

  1. myClass 的所有属性和方法现在都被继承了
  2. 被认为是良好的形式,但没有实际价值
  3. 重写继承的函数
  4. 扩展新类

实例化的内部机制

理解 JavaScript 创建类实例时会做三件事很重要

  1. 创建原型的副本。
  2. 使用原型副本作为 this 参数调用类函数。
  3. 返回原型副本。

以下两个类定义在功能上是等效的

function myClass(){}
function myClass(){ return this; }

但是,如果我们返回的不是 this 而是其他东西,会发生什么?

function myClass(){ return undefined; }
myClass.prototype.sayHi = function(){ console.log("Hi!"); }

console.log(new myClass);  // myClass {sayHi: function}

返回 undefined 时没有变化。让我们尝试返回一个对象字面量

function myClass(){ return {a: "a", b: "b"}; }
myClass.prototype.sayHi = function(){ console.log("Hi!"); }

console.log(new myClass);  // Object {a: "a", b: "b"}

我们用对象字面量替换了实例!

这对该方法与继承 HTML 元素配合使用至关重要。

这适用于任何对象类型。例如,在以下代码中,它适用于第一个类,但不适用于第二个类。

function myClass(){return new String("test");}  // returns a String object
function myClass(){return "test";}              // returns a String literal

我们替换了实例,但丢失了它的所有功能

(new myClass).sayHi();  // Uncaught TypeError: Object # has no method 'sayHi' 

之所以丢失该方法,是因为我们将 this 替换成了我们的对象字面量。我们可以通过将 this 包装到我们的对象上来修复这个问题

function myClass(){
  var i, me = {a: "a", b: "b"};
  for(i in this) me[i] = this[i];
  return me;
}
myClass.prototype.sayHi = function(){ console.log("Hi!"); };

(new myClass).sayHi();  // Hi!

instanceof 仍然正常工作吗?

console.log((new myClass) instanceof myClass);  // false

不,instanceof 已损坏。从类函数返回任何不是 this 的对象都会破坏 instanceof。别担心,有一个解决方法……我们稍后再谈。

现在,让我们返回一个 HTML 元素而不是对象字面量

function myClass(){
  var i, me = document.createElement("div");
  for(i in this) me[i] = this[i];
  return me;
}
myClass.prototype.sayHi = function(){ console.log("Hi!"); };

(new myClass).sayHi();  // Hi!

我们已经继承了一个 HTML 元素!

现在,让我们创建一个简单的、更有用的类

function myClass(){
  var i, me = document.createElement("input");
  me.type = "text";
  for(i in this) me[i] = this[i];
  return me;
}
myClass.prototype.onkeyup = function(){ console.log(this.value); };

document.body.appendChild(new myClass).focus();  // type into the input and watch the console log

我们如何使用事件监听器而不是事件处理程序?

function myClass(){
  var me = document.createElement("input");
  me.type = "text";
  me.addEventListener("keyup", this.keyupHandler);
  return me;
}
myClass.prototype.keyupHandler = function(){ console.log(this.value); };

document.body.appendChild(new myClass).focus();

这只是为了展示功能。现在,让我们扩展这个概念

function myClass(){
  var i, me = document.createElement("input");
  me.type = "text";
  for(i in this.listeners) me.addEventListener(i, this.listeners[i]);
  return me;
}
myClass.prototype.listeners = {
  keydown: function(){ console.log("down", this.value); },
  keyup: function(){ console.log("up", this.value); }
};

document.body.appendChild(new myClass).focus();

现在,让我们进一步扩展

function myClass(){
  var i, j, me = document.createElement("input");
  me.type = "text";
  for(i in this)
    if(i == "listeners") for(j in this[i]) me.addEventListener(j, this[i][j]);
    else me[i] = this[i];
  return me;
}
myClass.prototype.onkeyup = function(){ console.log("handle up", this.value); };
myClass.prototype.listeners = {
  keydown: function(){ console.log("listen down", this.value); },
  keyup: function(){ console.log("listen up", this.value); }
};

document.body.appendChild(new myClass).focus();

现在,让我们添加一些样式!

function myClass(){
  var i, j, me = document.createElement("input");
  me.type = "text";
  for(i in this)
    if(i == "listeners") for(j in this[i]) me.addEventListener(j, this[i][j]);
    else if(i == "style") for(j in this[i]) me[i][j] = this[i][j];
    else me[i] = this[i];
  return me;
}
myClass.prototype.onkeyup = function(){ console.log("handle up", this.value); };
myClass.prototype.listeners = {
  keydown: function(){ console.log("listen down", this.value); },
  keyup: function(){ console.log("listen up", this.value); }
};
myClass.prototype.style = { border: "2px solid black", borderRadius: "8px", padding: "4px" };

document.body.appendChild(new myClass).focus();

现在,让我们决定如何处理我们可能想添加到类中的任何自定义函数。我们可以轻松地使用点表示法向元素添加属性,例如 myElement.newAttribute = value。由于我们需要一种处理任何属性名称的策略,因此我们必须考虑与现有属性意外冲突的情况。简单的解决方案是对自定义属性进行命名空间,例如 myElement.namespace.newAttribute。将命名空间保持简短且易于记忆会有所帮助,因此我们选择 $,即 myElement.$.newAttribute

function myClass(){
  var i, j, me = document.createElement("input");
  me.type = "text";
  for(i in this)
    if(i == "listeners") for(j in this[i]) me.addEventListener(j, this[i][j]);
    else if(i == "style") for(j in this[i]) me[i][j] = this[i][j];
    else if(i == "$") { me.$ = this.$; me.$.el = me; }
    else me[i] = this[i];
  return me;
}
myClass.prototype.onkeyup = function(){ this.$.logIt("handle up"); };
myClass.prototype.listeners = {
  keydown: function(){ this.$.logIt("listen down"); },
  keyup: function(){ this.$.logIt("listen up"); }
};
myClass.prototype.style = { border: "2px solid black", borderRadius: "8px", padding: "4px" };
myClass.prototype.$ = {
  logIt: function(type){ console.log(type, this.el.value); }
};

document.body.appendChild(new myClass).focus();

在此步骤中有一个重要的注意事项是,自定义函数作用于命名空间。换句话说,当您在自定义函数中使用 this 时,它(一如既往)指的是函数的所有者,即 $ 对象。代码 me.$.el = me; 创建了一个指向元素的引用。如果您需要从自定义函数内部访问元素,语法是 this.el.attribute

现在我们已经涵盖了继承 HTML 元素的所有基本功能,让我们来简化一下。我们将创建一个名为 createClass 的函数来完成繁重的工作,并创建一个名为 element 的辅助函数。

Jooshe 组件

Jooshe 由一个命名空间和两个函数组成。

该命名空间目前包含一个辅助函数,但将来可能会扩展。

Jooshe 命名空间

var J$ = {

  wrap:
    function(o,p){
      if(p) for(var i in p)
        if(Object.prototype.toString.call(p[i]) == "[object Object]")
          { if(!(i in o)) o[i] = {}; J$.wrap(o[i],p[i]); }
      else o[i] = p[i];
    }

};

Jooshe createClass 函数

function createClass(className,fn,o,p) {

  fn = fn || function(){};
  window[className] = fn;
  var q = fn.prototype, w = J$.wrap;
  if(p) w(q, o.prototype);
  if(o) w(q, p || o);
  if(!("$" in q)) q.$ = {};
  q.$.__class__ = fn;
  q.$.__parentClass__ = p ? o : null;

}

用法

createClass("myClassName", fn, prototypeObject);  // use when not inheriting from a parent class

createClass("myClassName", fn, parentClass, prototypeObject);  // inherits from a parent class

在继承 HTML 元素时,fn 参数必须是一个返回 Jooshe 元素的 JavaScript 函数。如果 fn 参数为 falsycreateClass 将使用一个通用的空函数。

createClass 函数不返回值(技术上讲,它返回 undefined)。

Jooshe element 函数

function element(tag, a, b, c, d) {

  // a - style-level, b - 'this'-level, c - $-level, d - elemet-level

  var i, j, k, me = document.createElement(tag), o = me.style,
  f = function(a){ return Object.prototype.toString.call(a) == "[object Array]" ? a : [a]; },
  w = J$.wrap;

  // we'll do some 'CSS Reset'-like stuff here...
  //
  o.boxSizing = "borderBox";
  o.margin = 0;
  if(tag == "button") o.whiteSpace = "nowrap";
  else if(tag == "table") { me.cellPadding = 0; me.cellSpacing = 0; }

  a = f(a);
  for(i=0;i<a.length;i++) w(o,a[i]);

  me.$ = {el: me};

  b = f(b);
  for(i=0;i<b.length;i++) if(b[i]) for(j in b[i])
    if(j == "$") w(me.$, b[i].$);
    else if(j == "listeners") for(k in b[i][j]) me.addEventListener(k, b[i][j][k]);
    else if(j == "style") w(o, b[i][j]);
    else me[j] = b[i][j];

  c = f(c);
  for(i=0;i<c.length;i++) w(me.$,c[i]);

  d = f(d);
  for(i=0;i<d.length;i++) w(me, d[i]);

  return me;

}

用法

var el = element("tagName" [, style-level [, 'this'-level [, $-level [, element-level ]]]]);

style-level'this'-level$-levelelement-level 参数中的每一个都可以是对象或对象数组。允许使用数组,因为我发现在为 dbiScript 构建的一些更高级的类时,需要传入多个对象。

element 函数为指定样式级别、this 级别、$ 级别和元素级别属性提供了单独的参数,以提供灵活性并简化创建非基于类的元素的过程。例如,此类元素对于创建要附加到基于类的父元素的子元素很有用。

示例

让我们使用 Jooshe 重做之前的示例

createClass("myClass",

  function(){ return element("input", null, this); },

    {
      onkeyup: function(){ this.$.logIt("handle up"); },
      listeners: {
        keydown: function(){ this.$.logIt("listen down"); },
        keyup: function(){ this.$.logIt("listen up"); }
      },
      style: { border: "2px solid black", borderRadius: "8px", padding: "4px" },
      type: "text",
      $: { logIt: function(type){ console.log(type, this.el.value); } }
    }

  );

document.body.appendChild(new myClass).focus();

或者,我们可以重构示例为

createClass("myClass",

  function(){ return element("input", { border: "2px solid black", borderRadius: "8px", padding: "4px" }, this, null, {type: "text"}); },

    {
      onkeyup: function(){ this.$.logIt("handle up"); },
      listeners: {
        keydown: function(){ this.$.logIt("listen down"); },
        keyup: function(){ this.$.logIt("listen up"); }
      },
      $: { logIt: function(type){ console.log(type, this.el.value); } }
    }

  );

document.body.appendChild(new myClass).focus();

创建 Jooshe 类时,将 this 作为 element 函数的第二个参数传递至关重要——不要忘记!

第一个示例的结构如果该类被继承,效果会更好——子类将继承父类的样式和类型(“text”)。我发现我更喜欢第二个示例的编码风格,因为我喜欢将原型限制在功能上。Jooshe 很灵活——使用适合你的格式!

在深入研究继承之前,让我们看几个例子。让我们为类添加一个自定义属性并演示其用法

createClass("myClass",

  function(i){ return element("input", null, this, {index: i}); },

    {
      onkeyup: function(){ this.$.logIt("handle up"); },
      listeners: {
        keydown: function(){ this.$.logIt("listen down"); },
        keyup: function(){ this.$.logIt("listen up"); }
      },
      style: { border: "2px solid black", borderRadius: "8px", padding: "4px" },
      type: "text",
      $: { logIt: function(type){ console.log(type, this.index, this.el.value); } }
    }

  );

for(var i=0;i<10;i++) document.body.appendChild(new myClass(i)).focus();

与之前的示例相比,有三处更改,都用粗体文本突出显示。我们将循环的索引传递到新实例并将其存储在自定义属性中。当输入框中按下按键时,我们访问存储的属性并将其发送到控制台。

继承

现在,让我们看一个继承 Jooshe 类的示例

createClass("myClass", function(){ return element("input", null, this); },

    {
      style: { border: "2px solid black", borderRadius: "8px", padding: "4px" },
      type: "text"
    }

  );

createClass("mySubClass", function(){ return element("input", null, this); }, myClass,

    {
      style: { background: "rgba(209,42,42,.1)", borderColor: "red" },
      type: "text"
    }

  );


document.body.appendChild(new myClass);

document.body.appendChild(new mySubClass).focus();

要继承父类,只需将父类指定为 Jooshe 的 createClass 函数的第二个参数,如上面示例中的粗体文本所示。

在此示例中,子类保留了其父类的 borderWidth (2px)、borderStyle (solid)、borderRadius (8px) 和 padding (4px),同时重写了 borderColor 并添加了 backgroundColor。

选择性继承

可以有选择地继承一个或多个父类的特定属性

createClass("momClass", null,

    { $: { x: function(){ console.log("I'm x"); } } }

  );

createClass("dadClass", null,

    { $: { y: function(){ console.log("I'm y"); } } }

  );

createClass("childClass", null,

    {
      $: {
        x: momClass.prototype.$.x,
        y: dadClass.prototype.$.y,
        xy: function(){ console.log("I'm xy!"); }
      }
    }

  );

var myChild = new childClass;
myChild.$.x();   // I'm x!
myChild.$.y();   // I'm y!
myChild.$.xy();  // I'm xy!

类 vs. 元素

您可能已经注意到,不一定需要使用 createClass;仅使用 element 函数就可以完成相同的工作。我的经验法则是,每当我想让一个元素具有自定义功能时,就使用一个类。换句话说,如果您的元素需要任何事件处理程序、事件监听器或其他自定义功能,就为其创建一个类。浏览器能够更好地优化内存,将这些函数存储在类原型中(而不是内联到元素中)。它还使代码更简洁、更易于维护。另一方面,如果您的元素不需要任何自定义功能,那么只使用 element 函数是可以的。由于在我开发 dbiScript 时,这类元素非常普遍,因此我将样式级别的属性设置为 element 函数的第一个参数。

instanceof 解决方法

解决方法很简单

createClass("myClass",function(){ return element("div", null, this); });
var o = new myClass;
console.log(o instanceof myClass);      // false - instanceof is broken
console.log(o.$.__class__ == myClass);  // true - effective workaround!

尽管有效,但 Jooshe 的 __class__ 属性不像 instanceof 那样遍历继承链。Jooshe 的 __parentClass__ 属性可用于遍历继承链。

体积

Jooshe 源代码的体积非常小。最小化后的版本为 1058 字节。

Web 应用程序开发

CSS 和 Jooshe

在开发 Web 应用程序时

  • 将样式封装在 Jooshe 类中,而不是使用 CSS 基于选择器的将样式附加到元素的方法。

在我进行 Jooshe 开发时,我仍然使用的唯一 CSS 样式是

<style>
  body, td, input, select, textarea {font: 10pt Verdana, Arial, Helvetica, sans-serif}
</style>

虽然这也可以移入 Jooshe element 函数的“CSS Reset”部分,但将其保留在 CSS 中可以更容易地在不同的 Jooshe 应用程序之间更改字体。

Jooshe 和 CSS 可以一起使用——请记住,Jooshe 样式的 特异性最高(除了 !important)。

jQuery 和 Jooshe

在开发 Web 应用程序时

  • 将功能封装在 Jooshe 类中,而不是使用 jQuery 基于选择器的将功能附加到元素的方法。
  • Ajax 对于 Web 应用程序开发至关重要——使用 jQuery.ajax()

可维护性

我希望您会发现 Jooshe 不仅简化了 Web 应用程序开发,而且通过将结构、样式和功能封装到 Jooshe 类中,它还极大地提高了应用程序的可维护性。

FOUC

除了提高性能和可维护性之外,Jooshe 应用程序还不会受到可怕的 FOUC 的影响。

关注点

  • 我几年前开发 Jooshe 是为了处理 dbiScript 的动态需求。
  • dbiScript 目前由 350 个 Jooshe 类组成。
  • 如果您对大型 Jooshe 应用程序的性能感到好奇,请下载 dbiScript 并亲自体验。

Jooshe 在 Web 上

帮助传播信息

您是否同意 Jooshe 将改善 Web 应用程序开发?

  • 请为此页面添加您的投票、推文、+1 和点赞(顶部的按钮):)
© . All rights reserved.