【翻译】理念:无冲突的扩展本地DOM原型
2015-05-19 17:26 勤劳的插秧哥 阅读(652) 评论(1) 编辑 收藏 举报菜鸟翻译,望大家多多指正哈
原文:http://lea.verou.me/2015/04/idea-extending-native-dom-prototypes-without-collisions/
理念:无冲突的扩展本地DOM原型
正如我昨天在博文中指出,我不喜欢使用jQuery的原因之一是因为它的包装对象。对于jQuery来说,这是一个明智的决定:早在2006年它被第一次开发出来的时候,IE有一个非常讨厌的内存泄漏bug,当我们给一个元素添加属性时它便很容易被引发出来。哦,那时我们还没有在IE浏览器访问元素的原型,所以我们必须手动在每个元素上添加这些属性。Prototype.js试图走这条路但结果却是一团糟:他们打算改变他们之前在Prototype2.0版和依附包装对象的决定。有人曾写过很长的文章来批判企图扩展本地DOM元素是多么典型的错误想法。
第一个暴露元素原型的IE浏览器是IE8:我们可以访问Node.prototype
,Element.prototype
和其他几种原型。有些是多变的,有些则不是。在IE9,我们得到了全部,包括HTMLElement.prototype及其后代节点,比如HTMLParagraphElement。内存泄漏bug在IE8时得到了改善,到IE9时则得到了修复。但我们还是不要扩展原生的DOM元素,理由很充分:有冲突的风险。没有哪个函数库想在元素上添加一堆方法,这种方式很糟糕, 就像被邀请到别人家做客,结果却把人家家里弄的一团乱。
但是,如果我们可在避免冲突的条件下对元素添加方法呢?(好吧,从技术上讲,可能性很小)。我们只能对元素添加一个属性,然后把我们所有的方法都附着上去。例如:如果我们的函数库为yolo并有两个方法:foo()和bar(),就像这样:
var element = document.querySelector(".someclass"); element.yolo.foo(); element.yolo.bar(); //你甚至可以链式返回他们的元素 element.yolo.foo().yolo.bar();
可以肯定,这比包装对象更别扭,但是我认为使用本地DOM元素所带来的好处要大于它。当然,你可能不这么认为。
这基本上同我们做全局是完全一样的:我们都知道,添加大量的全局变量是不可取的做法,所以每一个函数库都只创建一个全局变量并把所有方法属性都附着在这个全局上。 然而,如果我们试图以这种天真的方式来实施,我们会发现 用我们的命名空间函数来引用元素是有些难度的:
Element.prototype.yolo = { foo: function () { console.log(this); }, bar: function () { /* ... */ }}; someElement.yolo.foo(); // Object {foo: function, bar: function}
这里发生了什么?函数中的this
指向的调用他们的对象,而不是对象所附着的那个元素,想要避开这个问题我们需要更聪明点。
记住:Yolo
里的this
指向我们试图挂载方法的元素,但是我们没有运行任何代码,所以我们没有利用它。除非我们能够得到一个引用该对象的上下文。然而,运行一个function (例如element.yolo(). foo()
)会毁坏我们良好的API。
稍等一下,我们可以通过ES5获得权限!我们这样做:
Object.defineProperty(Element.prototype, "yolo", { get: function () { return { element: this, foo: function() { console.log(this.element); }, bar: function() { /* ... */ } } }, configurable: true, writeable: false}); someElement.yolo.foo(); // It works! (打印出该元素)
这个方法奏效,但是这里有一个相当恼人的问题:我们每次生成该对象和重新定义函数都要调用该属性。这是一个很坏的想法。理想情况下,我们需要生成该对象,然后返回生成的对象。我们也不想让每个元素都有自己完全独立的实例,我们想在原型上定义这些函数,并且用JS完美的继承,因此,我们的库也是动态可扩展的。幸运的是,有一种方法可以做到这一切:
var Yolo = function(element) { this.element = element; }; Yolo.prototype = { foo: function() { console.log(this.element); }, bar: function() { /* ... */ } }; Object.defineProperty(Element.prototype, "yolo", { get: function () { Object.defineProperty(this, "yolo", { value: new Yolo(this) }); return this.yolo; }, configurable: true, writeable: false }); someElement.yolo.foo(); // 成功! (打印出元素) // 它也是可以动态扩展的 Yolo.prototype.baz = function(color) { this.element.style.background = color; }; someElement.yolo.baz("red") // 元素获得了一个红色背景
注意,上面我们所提到的getter只执行一次。之后它用一个静态值:一个yolo
对象的实例重写了yolo
属性。因为我们用 到Object.define Property()
所以也不会遇到破坏枚举的问题(for..in
循环),这些属性默认enumerable: false
。这里任然有一个不足:这些方法需要用this.element
来替代this
。我们可以通过封装他们来解决这一问题:
for (let method in Yolo.prototype) { Yolo.prototype[method] = function(){ var callback = Yolo.prototype[method]; Yolo.prototype[method] = function () { var ret = callback.apply(this.element, arguments); // 可链式返回元素! return ret === undefined? this.element : ret; } } }
然而,现在你不能动态的在Yolo.prototype
上添加方法并让他们像element.yolo
里的本地方法那样自动运行。所以它是有点痛的可扩展性(当然,你仍然可以用this.element
来添加方法,也是可行的)。
你的想法?