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





5.00/5 (12投票s)
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]
注释
- myClass 的所有属性和方法现在都被继承了
- 被认为是良好的形式,但没有实际价值
- 重写继承的函数
- 扩展新类
实例化的内部机制
理解 JavaScript 创建类实例时会做三件事很重要
- 创建原型的副本。
- 使用原型副本作为
this
参数调用类函数。 - 返回原型副本。
以下两个类定义在功能上是等效的
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
参数为 falsy,createClass
将使用一个通用的空函数。
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
、$-level
和 element-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 和点赞(顶部的按钮):)