建立你的第一个JavaScript库

是否曾对Mootools的魔力感到惊奇?是否有想知道Dojo如何作到那样的?是否对jQuery感到好奇?在这个教程中,咱们将了解它们背后的东西而且动手建立一个超级简单的你最喜欢的库。javascript

  咱们其乎天天都在使用JavaScript库。当你刚入门时,利用jQuery是一件很是奇妙的事,主要是由于它的DOM操做。首先,DOM对于入门者来讲多是相对困难的事情;其次用它咱们几乎能够不用考虑跨浏览器兼容的问题。html

  在这个教程中,咱们将试着从头开始实现一个很简单的库。是的,它很是有意思,可是在你高兴以前让我申明几点:java

  • 这不会是全功能的库。咱们有不少方法要写,可是它不是jQuery。咱们将会作足工做来让你感觉到在你建立一个库时会遇到的各类问题。node

  • 咱们不会彻底解决全部浏览器的兼容性问题。咱们写的代码能支持IE8+,Firefox 5+,Opera 10+,Chrome和Safari。express

  • 咱们不会覆盖使用咱们库的全部可能性。好比咱们的append和prepend方法只在你传入一个咱们库的实例时才有效,它们不支持原生的DOM节点或节点集合。数组

 步骤1: 建立库样板文件Creating the Library Boilerplate

  咱们以一些封装代码开始,它将会包含咱们整个库。它就是你常常用到的当即执行函数表达式浏览器

window.dome = (function () {

    function Dome (els) {

    }

    var dome = {

        get: function (selector) {

        }

    };

    return dome;

}());

  如你所见,咱们把咱们的库叫Dome,由于它主要就是一个针对DOM的库,是的,它很不完整。app

  到此咱们作了两件事。首先,咱们定义了一个函数,它最终会是实例化咱们库的构造函数,这些对象将会封装咱们选择或建立的元素。dom

  接下来咱们建立了dome对象,它是咱们实际的库对象;你能看到,它在最后被返回。它有一个空的get函数,咱们将用它来从页面中选择元素。因此,让咱们如今来填充它的代码。函数


 步骤2: 获取元素

  dome.get函数传入一个参数,可是它能够有好几种状况。若是它是一个字符串,咱们假定它是一个CSS选择器;可是咱们也能够传入单个DOM节点或是一个NodeList。

get: function (selector) {

    var els;

    if (typeof selector === "string") {

        els = document.querySelectorAll(selector);

    } else if (selector.length) {

        els = selector;

    } else {

        els = [selector];

    }

    return new Dome(els);

}

  咱们使用document.querySelectorAll来简化元素的查找:固然这有浏览器兼容性问题,可是对于咱们的例子来讲它是ok的。若是 selector不是字符串,咱们将检查它的length属性。若是它存在,咱们就知道它是一个NodeList;不然它是单个元素而后咱们将它放到一个数组中。这就是咱们下面须要将调用Dome的结果传给一个数组的缘由;你能够看到咱们返回一个新的Dome对象。因此让咱们回头看看Dome函数并填充它。


 步骤3: 建立Dome实例

  下面是Dome函数:

function Dome (els) {

    for(var i = 0; i < els.length; i++ ) {

        this[i] = els[i];

    }

    this.length = els.length;

}

  它确实很简单:咱们只是遍历咱们选择的元素并把它们附到带有数字索引的新对象中。而后咱们添加一个length属性。

  可是这的关键是什么呢?为何不直接返回元素?咱们将元素封装到一个对象由于咱们想为这个对象建立方法;这些方法可让咱们与这些元素交互。这实际上就是jQuery采用的方法的简化版本。

  因此,咱们返回了Dome对象,让咱们在它的原型上添加一些方法。我把这些方法直接写在Dome函数中。


 步骤4: 添加一些经常使用工具函数

  咱们要写的第一个方法是一个简单的工具函数。由于咱们的Dome对象能够封装多个DOM元素,几乎每一个方法都须要遍历每一个元素;因此,这些工具函数会很是便利。

  让咱们以一个map函数开始:

Dome.prototype.map = function (callback) {

    var results = [], i = 0;

    for ( ; i < this.length; i++) {

        results.push(callback.call(this, this[i], i));

    }

    return results;

};

  固然,map函数传入单个参数,一个回调函数。咱们遍历数组中的每一项,收集回调函数返回的全部内容放到results数组中。注意咱们如何调用回调函数:

callback.call(this, this[i], i));

  这样函数就会在咱们的Dome实例的上下文中被调用,它接受两个参数:当前元素,以及索引号。

  咱们也想要一个forEach函数。它确实很是简单:

Dome.prototype.forEach(callback) {

    this.map(callback);

    return this;

};

  map和forEach间的惟一区别是map须要返回一些东西,所以咱们也能够只传入咱们的回调函数给this.map并忽略返回的数组,咱们将返回 this来使得咱们的库支持链式操做。咱们将常用forEach。因此,注意当返回咱们的this.forEach对函数的调用时,咱们事实上是返回了this。例如,下面的方法实际上返回相同的东西:

Dome.prototype.someMethod1 = function (callback) {

    this.forEach(callback);

    return this;

};

Dome.prototype.someMethod2 = function (callback) {

    return this.forEach(callback);

};

  另外:mapOne。很容易看出这个函数是干什么的,可是问题是为何咱们须要它?它须要一些你能够叫作“库哲学”的东西来解释。

 一个简单的“哲学的”迂回

  若是建立一个库只是写代码,那就不是什么难的工做了。可是我正在作这个项目,我发现困难的部分是决定一些方法应该如何工做。

  很快,咱们将建一个text方法,它返回咱们选择元素的文本。若是咱们的Dome对象封装几个DOM节点(如dome.get("li")),它会返回什么呢?若是你在jQuery作相似的事情($("li").text()),你将会获得一个全部元素的文本拼起来的字符串。它有用吗?我认为没用,可是我不知道更好的返回是什么。

  在这个项目中,我将以数组形式返回多个元素的文本,除非数组中只有一个元素,那咱们就返回一个文本字符串,而不是只有一个元素的数组。我想你最经常使用的是获取单个元素的文本,因此咱们对这个状况进行优化。然而,若是你获取多个元素的文本,咱们也会返回一些你能操做的东西。

 回到代码

  因此,mapOne方法只是简单的运行map,而后要么返回数组,要么返回单元素数组中的元素。若是你仍是不肯定这有什么用,等一会你会发现的!

Dome.prototype.mapOne = function (callback) {

    var m = this.map(callback);

    return m.length > 1 ? m : m[0];

};

 步骤5: 处理文本和HTML

  接下来,让咱们添加text方法。就像jQuery同样,咱们能够给它传入一个字符串并设置元素的文本,或不传参数来获取元素的文本。

Dome.prototype.text = function (text) {

    if (typeof text !== "undefined") {

        return this.forEach(function (el) {

            el.innerText = text;

        });

    } else {

        return this.mapOne(function (el) {

            return el.innerText;

        });

    }

};
Dome.prototype.text = function (text) {

    if (typeof text !== "undefined") {

        return this.forEach(function (el) {

            el.innerText = text;

        });

    } else {

        return this.mapOne(function (el) {

            return el.innerText;

        });
    
    }

};

  你可能也想到了,咱们须要检查text的值来看它是要设置仍是要获取。注意若是只是用if(text)会有问题,由于空字符串会被判断为false。

  若是咱们在设置值,咱们将对元素调用forEach而且设置它们的innerText属性为text。若是咱们要获取,咱们将返回元素的 innerText属性。注意咱们使用mapOne方法:若是咱们在处理多个元素,它将返回一个数组,不然它将就是一个字符串。

  html方法几乎与text同样,除了它使用innerHTML属性而不是innerText。

Dome.prototype.html = function (html) {

    if (typeof html !== "undefined") {

        this.forEach(function (el) {

            el.innerHTML = html;

        });

        return this;

    } else {

        return this.mapOne(function (el) {

            return el.innerHTML;

        });

    }

};

  就像我说的:几乎彻底同样。


 步骤6: 调整样式

  再接下来,咱们但愿能添加和删除样式,所以让咱们来写一个addClass和removeClass方法。

  咱们的addClass方法将接收一个字符串或是样式名称的数组。为了作到这点,咱们须要检查参数的类型。若是是数组,咱们将遍历它并建立一个样式名的字符串。不然,咱们就简单的在样式名前加一个空格,这样它就不会和元素已有的样式混在一些。而后咱们遍历元素而且将新的样式附加到className属性后面。

Dome.prototype.addClass = function (classes) {

    var className = "";

    if (typeof classes !== "string") {

        for (var i = 0; i < classes.length; i++) {

            className += " " + classes[i];

        }

    } else {

        className = " " + classes;

    }

    return this.forEach(function (el) {

        el.className += className;

    });

};

  很直接,对吗?

  那如何删除样式呢?为了保持简单,咱们只容许一次删除一个样式。

Dome.prototype.removeClass = function (clazz) {

    return this.forEach(function (el) {

        var cs = el.className.split(" "), i;

        while ( (i = cs.indexOf(clazz)) > -1) {

            cs = cs.slice(0, i).concat(cs.slice(++i));

        }

        el.className = cs.join(" ");

    });

};

  对每一个元素,咱们将el.className分隔成一个数组。而后,咱们使用一个while循环来剔除咱们传入的样式,直到 cs.indexOf(clazz)返回-1。咱们这样作是为了处理一样的样式在一个元素中出现的不止一次的特殊状况:咱们必须保证它真的被删除了。一旦咱们确保删除每一个样式的实例,咱们用空格链接数组的每一项并把它设置到el.className。


 步骤7: 修正一个IE的Bug

  咱们正在处理的最糟糕的浏览器是IE8。在咱们的小小的库中,只有一个IE bug须要咱们处理,很幸运它很简单。IE8不支持Array的indexOf方法;咱们在removeClass中使用到它,因此让咱们修复它:

if (typeof Array.prototype.indexOf !== "function") {

    Array.prototype.indexOf = function (item) {

        for(var i = 0; i < this.length; i++) {

            if (this[i] === item) {

                return i;

            }

        }

        return -1;

    };

}

  它很是简单,而且这不是一个彻底的实现(不支持第二个参数),可是能达到咱们的目的。


 步骤8: 调节属性

  如今,咱们想要一个attr函数。这很容易,由于它与咱们的text或html方法很是相似。像那些方法同样,咱们可以获取或设置属性值:咱们能够传入元素名和值来设置,也能够只传入属性名来获取。

Dome.prototype.attr = function (attr, val) {

    if (typeof val !== "undefined") {

        return this.forEach(function(el) {

            el.setAttribute(attr, val);

        });

    } else {

        return this.mapOne(function (el) {

            return el.getAttribute(attr);

        });

    }

};

  若是val有一个值,咱们将遍历这些元素而且将选择的属性设置为这个值,使用元素的setAttribute方法。不然,咱们使用mapOne经过getAttribute方法来返回属性值。


 步骤9: 建立元素

  像不少好的库同样,咱们应该可以建立新的元素。固然它做为一个Dome实例的一个方法不是很好,因此让咱们直接把它挂到dome对象上去。

var dome = {

    // get method here

    create: function (tagName, attrs) {

    }

};

  你已经看到,咱们使用两个参数:元素的名字,和属性值对象。大部分属性能过attr方法赋值,可是两种方法能够作特殊处理。咱们使用addClass 方法操做className属性,以及text方法操做text属性。固然,咱们首先须要建立元素和Dome对象。下面是整个操做的代码:

create: function (tagName, attrs) {

    var el = new Dome([document.createElement(tagName)]);

    if (attrs) {

        if (attrs.className) {

            el.addClass(attrs.className);

            delete attrs.className;

        }

        if (attrs.text) {

            el.text(attrs.text);

            delete attrs.text;

        }

        for (var key in attrs) {

            if (attrs.hasOwnProperty(key)) {

                el.attr(key, attrs[key]);

            }

        }

    }

    return el;

}

  咱们建立元素并将它传给一个新的Dome对象。而后中咱们处理属性。注意在操做完它们后咱们必须删除className和text属性。这样能够避免当咱们在attrs中遍历剩下的key值时被应用为属性。固然咱们最后要返回这个新建的Dome对象。

  可是如今只是建立了新的元素,咱们但愿把它插入到DOM中对吗?


 步骤10: 附加元素

  下一步,咱们将写append和prepend方法。这些确实是有点难搞的函数,主要是由于有不少种使用状况。如下是咱们但愿能作到的:

dome1.append(dome2);

dome1.prepend(dome2);

  使用状况以下:咱们可能想要append或prepend

  • 一个新的元素到一个或多个已存在的元素

  • 多个新元素到一个或多个已存在的元素

  • 一个已存在的元素到一个或多个已存在的元素

  • 多个已存在的元素到一个或多个已存在的元素

  注意:我使用“新”来表示元素尚未在DOM中;已存在的元素是已经在DOM中有的。

  让咱们一步一步来:

Dome.prototype.append = function (els) {

    this.forEach(function (parEl, i) {

        els.forEach(function (childEl) {

        });
    
    });

};

  咱们指望els参数是一个Dome对象。一个完整的DOM库能够接受一个节点或nodelist做为参数,可是咱们暂时不这样作。咱们必须遍历咱们每个元素,而且在它里面,咱们还要遍历每一个咱们须要append的元素。

  若是咱们将els到多个元素,咱们须要克隆它们。然而,咱们不想在他们第一次被附加的时候克隆节点,而时随后再说。因此咱们这样:

if (i > 0) {

    childEl = childEl.cloneNode(true);

}

  这个i来自外层的forEach循环:它是当前父元素的索引。若是咱们不是附加到第一个父元素,咱们将克隆节点。这样,真正的节点将会放到第一个父节点中,其它父节点将得到一个拷贝。这样很好用,由于传入的Dome对象将只会拥有原始的节点。因此若是咱们只是附加单个元素到单个元素,使用的全部节点都将是各自Dome对象的一部分。

  最后,咱们终于能够附加元素:

parEl.appendChild(childEl);

  因此,汇总起来是这样

Dome.prototype.append = function (els) {

    return this.forEach(function (parEl, i) {

        els.forEach(function (childEl) {

            if (i > 0) {

                childEl = childEl.cloneNode(true);

            }

            parEl.appendChild(childEl);

        });

    });

};
 prepend方法

  咱们想要prepend方法也知足一样的状况,因此这个方法很是相似:

Dome.prototype.prepend = function (els) {

    return this.forEach(function (parEl, i) {

        for (var j = els.length -1; j > -1; j--) {

            childEl = (i > 0) ? els[j].cloneNode(true) : els[j];

            parEl.insertBefore(childEl, parEl.firstChild);

        }

    });

};

  当prepend时所不一样的是若是你顺次prepend一系列元素到另一个元素时,它们是倒序的。由于咱们不能反向forEach,我将使用for循环反向遍历。一样,咱们将克隆节点若是它不是咱们第一个要附件到的父节点。


 步骤11: 移除节点

  对于咱们最后一个节点处理方法,咱们想要从DOM中删除节点。其实很简单:

Dome.prototype.remove = function () {

    return this.forEach(function (el) {

        return el.parentNode.removeChild(el);

    });

};

  就是遍历节点并在每一个元素的parentNode上调用removeChild方法。这里漂亮的地方在于这个Dome对象还将正常工做;咱们能够在它上面使用任何方法,包括从新放回到DOM中去。


 步骤12: 处理事件

  最后,可是确定不是用得最少的,咱们将写一些函数处理事件。你能够知道,IE8使用老式的IE事件,因此咱们须要检查它。同时,咱们将抛出DOM 0事件,就由于咱们能够。

签出方法,而后中咱们将讨论它:

Dome.prototype.on = (function () {

    if (document.addEventListener) {

        return function (evt, fn) {

            return this.forEach(function (el) {

                el.addEventListener(evt, fn, false);

            });

        };

    } else if (document.attachEvent)  {

        return function (evt, fn) {

            return this.forEach(function (el) {

                el.attachEvent("on" + evt, fn);

            });

        };

    } else {

        return function (evt, fn) {

            return this.forEach(function (el) {

                el["on" + evt] = fn;

            });

        };

    }

}());

  在这,咱们使用了一个当即执行函数表达式,在函数里面咱们作了特征检查。若是document.addEventListener存在,咱们将使用它;不然咱们检查document.attachEvent或者求助于DOM 0事件。注意咱们如何返回最后的函数:它将在结束时被赋给Dome.prototype.on。当作特征检测时,很是方便地像这样赋给合适的函数,而不是每次函数运行时都得检查一次。

  off函数用于卸载事件,它与前面很是相似。

Dome.prototype.off = (function () {

    if (document.removeEventListener) {

        return function (evt, fn) {

            return this.forEach(function (el) {

                el.removeEventListener(evt, fn, false);

            });

        };

    } else if (document.detachEvent)  {

        return function (evt, fn) {

            return this.forEach(function (el) {

                el.detachEvent("on" + evt, fn);

            });

        };

    } else {

        return function (evt, fn) {

            return this.forEach(function (el) {

                el["on" + evt] = null;

            });

        };

    }

}());

 就是这样!

  我但愿你能试一试咱们的小小的库,而且能稍稍扩展一点点。

  让我再申明一下,这个教程的目的不是说建议你老是要写一个本身的库。

  有专业的团队在作一个庞大的,稳定的愈来愈好的库。这里咱们只是想让你们看看一个库内部是什么样子的,但愿你能在这学到一些东西。

本文转载自:http://code.tutsplus.com/tutorials/build-your-first-javascript-library--net-26796

相关文章
相关标签/搜索