JS库的封装之一:入口函数

参考jQuery,使用JavaScript实现库的封装。本文为第一部分:入口函数的编写。

一、需求

  参考jQuery中的实现逻辑,通过JavaScript代码手动实现:
  ● jQuery选择器和css设置
    ৹ $(“div”).css(“color”,”red”)
  ● 要封装的这个库应该是一个独立的单元:模块化
    ৹ 不依赖任何其他第三方库
    ৹ 里面的东西大部分也是与世隔绝的,只有:$、jQuery

二、入口函数

  首先要用一个入口函数,编写一个自执行函数。global 保存了window对象的引用。

    (function (global) {

        function jQuery () {

        }

        window.$ = window.jQuery = jQuery;

    })(window)

三、选择器

  jQuery选择器中,可以通过标签名、类名和id获取元素,其原理是jQuery内部封装了一个Sizzle引擎(Sizzle.js)来获取DOM元素,其中包含了很多正则。
  我们这里就借助HTML5新增的DOM操作方法 document.querySelectorAll(selectors) 来实现。

    (function (global) {

        function jQuery(selector) {
            const elements = document.getElementsByTagName(selector);
            return elements;
        }

        window.$ = window.jQuery = jQuery;

    })(window)

四、jQuery DOM操作方法

  接下来要实现jQuery的 $(selector).css(name,value) 操作,有几种方案可以在功能上可行,但都有缺点:
  ① 在获取元素后,直接设置属性 elements.css=()=>{}
    缺点:随着 $() 操作频次的增加,会产生无数个相同的css方法,造成内存浪费
  ② 将 .css() 方法放到原型中, HTMLCollection.prototype.css=()=>{}
    缺点:破坏了原生的对象结构

  因此,我们想到可以返回一个自定义对象,而不是原生的对象。
  首先创建一个构造函数,把DOM操作方法放在该构造函数上,由于DOM操作方法需要访问获取到的元素,所以还需要把DOM元素放到该对象中。

    (function (global) {

        function jQuery(selector) {
            // 获取页面中所有的元素,把这个元素放在一个特定的对象中
            return new F(selector);
        }

        // jquery对象的构造函数
        function F(selector) {
            // 把DOM元素放到这个对象中
            const elements = document.querySelectorAll(selector)
            // 为了让这些元素可以在css方法中进行访问,所以需要把这些元素放在对象上面进行传递
            this.elements = elements;
        }

        F.prototype.css = function (name, value) {
            for (let i = 0; i < this.elements.length; i++) {
                let element = this.elements[i];
                element.style[name] = value;
            }
        }

        window.$ = window.jQuery = jQuery;

    })(window)

五、jQuery DOM操作方法优化

  在上面的代码中,我们操作DOM元素时,需要通过 this.elements[i] 这样的方式获取到某个元素。
  而在jQuery中,我们只需通过 $(selector)[0] 即可获取到某个元素。那是因为jQuery为了后续的DOM操作方便,将获取到的DOM元素全部放在了对象自己身上,让自己变成了类似数组的结构,可以使用for循环进行遍历,我们把这种对象特性称之为【伪数组】。

  接下来我们要实现把这些所有DOM元素都添加到对象自己身上,这么以来就可以通过 this[i] 这样的方式获取到某个元素了。
  同时,我们使用替换原型对象的方法,简化代码,将DOM操作方法全部放在新原型(F.prototype)中。

    (function (global) {

        function jQuery(selector) {
            return new F(selector);
        }

        function F(selector) {
            const elements = document.querySelectorAll(selector)
            // 实现把这些所有DOM元素都添加到对象自己身上
            for (let i = 0; i < elements.length; i++) {
                this[i] = elements[i];
            }
            // 为了让它更像数组,需要添加一个length属性
            this.length = elements.length;
        }

        F.prototype = {
            constructor: F,
            // 此时的css方法还是雏形,后面的文章会进行完善
            css(name, value) {
                for (let i = 0; i < this.length; i++) {
                    let element = this[i];
                    element.style[name] = value;
                }
            }
        }

        window.$ = window.jQuery = jQuery;

    })(window)

六、入口函数仿jQuery化

  上面写的代码其实都是入口函数,而在jQuery中,构造函数的名字叫 init,即我们下面要把构造函数 F 改名为 init。
  且jQuery做了一个大的调整:把DOM操作方法放到了jQuery函数的原型中,那么此时jQuery函数的原型和init实例就没有了关系,我们需要通过 jQuery.prototype.init(selector) 来访问init函数。
  此时创建的jQuery是init构造函数的实例,css方法在jQuery.prototype对象中,为了让jQuery对象可以访问到css方法,我们需要让init的原型继承自jQuery的原型(这也是jQuery源码做的比较精妙/恶心的事情,应该是有其历史原因的)

  执行步骤:
  ① 创建了一个init的对象
  ② 执行css方法
    a. 找对象本身有没有css方法,并没有
    b. 找对象的原型:init.prototype,且它被我们等于了 jQuery.prototype
    c. 发现 jQuery.prototype 中有一个css方法
  ③ 此时init原型可以访问css方法,那么init构造函数创建出来的jQuery对象也能够访问css方法了

    (function (global) {

        function jQuery(selector) {
            return new jQuery.prototype.init(selector);
        }

        jQuery.prototype = {
            constructor: jQuery,
            // init是一个构造函数
            // 构造函数内部的this指向init实例
            init: function (selector) {
                const elements = document.querySelectorAll(selector)
                // 为了让css方法中可以访问到DOM元素,所以需要把elements里面的元素存放在this中
                for (let i = 0; i < elements.length; i++) {
                    this[i] = elements[i];
                }
                this.length = elements.length;

            },
            // 此时的css方法还是雏形,后面的文章会进行完善
            css(name, value) {
                for (let i = 0; i < this.length; i++) {
                    let element = this[i];
                    element.style[name] = value;
                }
            }
        }

        // 此时创建的jQuery对象是init构造函数的实例
        // 为了让jquery对象可以访问到jQuery的原型中的css方法,让init的原型继承自jQuery的原型
        jQuery.prototype.init.prototype = jQuery.prototype;

        window.$ = window.jQuery = jQuery;

    })(window)

七、代码简化

  因为所有的DOM操作都在jQuery的原型中,因此后续编码中会经常访问到 jQuery.prototype,为了操作方便,我们令 jQuery.fn=jQuery.prototype,这样每次就可以少些几个字母了。

    (function (global) {

        function jQuery(selector) {
            return new jQuery.fn.init(selector);
        }

        // 给jquery添加了一个fn属性,fn属性等价于prototype属性
        jQuery.fn = jQuery.prototype = {
            constructor: jQuery,
            init: function (selector) {
                const elements = document.querySelectorAll(selector)
                for (let i = 0; i < elements.length; i++) {
                    this[i] = elements[i];
                }
                this.length = elements.length;
            },
            // 此时的css方法还是雏形,后面的文章会进行完善
            css(name, value) {
                for (let i = 0; i < this.length; i++) {
                    let element = this[i];
                    element.style[name] = value;
                }
            }
        }

        jQuery.fn.init.prototype = jQuery.fn;

        // 假如后面再要往原型对象中添加DOM操作方法时,只需如下:
        jQuery.fn.attr = () => {

        }
        jQuery.fn.animate = () => {

        }

        window.$ = window.jQuery = jQuery;

    })(window)

八、入口函数功能补充

  以上,我们通过传入一个选择器,可以获取一个jQuery对象。
  而在实际的jQuery源码中,还支持用户传入一个dom元素,返回一个该元素对应的jQuery对象。
  下面对init函数进行升级,增加条件判断:
  如果selector是字符串:是选择器
  如果selector有nodeType属性:是dom元素

    (function (global) {

        function jQuery(selector) {
            return new jQuery.fn.init(selector);
        }

        // 给jquery添加了一个fn属性,fn属性等价于prototype属性
        jQuery.fn = jQuery.prototype = {
            constructor: jQuery,
            init: function (selector) {
                if (jQuery.type(selector) === "string") {
                    // 选择器
                    // 把DOM元素放到这个对象中
                    const elements = document.querySelectorAll(selector)
                    // 为了让css方法中可以访问到DOM元素,所以需要把elements里面的元素存放在this中
                    for (let i = 0; i < elements.length; i++) {
                        this[i] = elements[i];
                    }
                    this.length = elements.length;
                    // 对象结构:{ 0:div,1:div,2:div,length:3 }
                } else if (selector.nodeType) {
                    // dom元素 -> { 0:div,length:1 }
                    this[0] = selector;
                    this.length = 1;
                }
            },
            // 此时的css方法还是雏形,后面的文章会进行完善
            css(name, value) {
                for (let i = 0; i < this.length; i++) {
                    let element = this[i];
                    element.style[name] = value;
                }
            }
        }

        jQuery.fn.init.prototype = jQuery.fn;

        // 假如后面再要往原型对象中添加DOM操作方法时,只需如下:
        jQuery.fn.attr = () => {

        }
        jQuery.fn.animate = () => {

        }

        window.$ = window.jQuery = jQuery;

    })(window)

九、总结

  本文主要是完成了对jQuery入口函数的封装,后面将完善一些常用方法、工具类方法和DOM操作方法。

  这个系列的文章,主要是通过仿照jQuery来学习JS库的封装,共写了如下几篇博客:
  1. 《JS库的封装之一:入口函数》
  2. 《JS库的封装之二:添加方法》
  3. 《JS库的封装之三:添加事件》
  4. 《JS库的封装之四:代码整合》


  目录