JS库的封装之二:添加方法

参考jQuery,使用JavaScript实现库的封装。本文为第二部分:extend方法、工具类方法和DOM操作方法的封装。

一、需求

  参考jQuery中的实现逻辑,通过JavaScript代码手动实现:

  ● jQuery的拷贝继承方法
    ৹ $.extend(target,source1,source2)
    ৹ $.fn.extend(object)
  ● jQuery的工具类方法
    ৹ $.each
    ৹ $.type
  ● jQuery的DOM操作方法
    ৹ $(selector).css
    ৹ $(selector).show、$(selector).hide、$(selector).toggle

二、extend方法

1. $.extend

  首先实现jQuery中的第一个拷贝继承方法:$.extend

    jQuery.extend = function(...args){
        // 这里的extend方法参数并不确定
        // 所以建议不要指定形参,通过函数内置对象arguments来进行操作
        const target = args[0];
        // 进行对象拷贝,需要将第二个参数及其后面的所有参数中的属性遍历添加到第一个参数中
        for(let i = 1;i<args.length;i++){
            // 每一个实参:对象
            let arg = args[i];
            // 取出对象中的每一个属性
            for (let key in arg) {
                // 把该属性添加到第一个参数中
                target[key] = arg[key];
            }
        }
        return target;
    }

2. $.fn.extend

  这是jQuery中的第二个拷贝继承方法,也是编写jQuery插件的核心方法。
  一般用于编写工具函数的时候,功能就是把这些方法添加到原型中,这样jQuery对象就能直接访问了,比如:

    $.fn.extend({
        dateTimePicker() {

        },
        getDate() {

        }
    })

  接下来我们实现jQuery中的第二个拷贝继承方法:$.fn.extend

  在jQuery中 jQuery.fn.extendjQuery.extend相同的实现代码,但执行过程有所不同。
  2个extend方法区别在于
  ① 接收数据的对象发生了变化
    $.extend:第一个实参
    $.fn.extend:this(也就是 $.fn)
  ② 提供数据的对象发生了变化:
    $.extend:第二个参数及其后面的参数
    $.fn.extend:所有的参数
  后面的拷贝过程都是一样的。

    jQuery.fn.extend = jQuery.extend = function (...args) {
        let target, source = [];
        source = [...args];
        // 判断2种情况
        // $.extend({}) 给$添加属性
        // $.fn.extend({}) 给$.fn添加属性
        if (args.length === 1) {
            // 参数个数=1,目标对象等于this
            target = this;
        } else {
            // 参数个数>1,就是给第一个实参添加属性
            target = args[0];  // 目标对象就是第一个实参
            source.splice(0, 1);  // 删除第一个元素,源对象就是第二个及其后面的实参
        }

        // 实现拷贝部分的逻辑:
        source.forEach(function (item, index) {
            // item:就是每一个数据源对象(提供数据的对象)
            // 取出item对象中的每一个属性:for...in
            Object.keys(item).forEach((key) => {
                // key就是对象中每一个属性名
                target[key] = item[key];
            })
        });

        return target;
    }


    // 验证:添加DOM操作方法
    // DOM操作方法要放在原型中,即init实例的原型,就是jQuery.prototype,也就是jQuery.fn
    // 往jQuery.fn对象中添加方法,可以通过jQuery.fn.extend()传入一个参数,相当于往jQuery.fn身上添加属性和方法
    jQuery.fn.extend({
        attr() {
            console.log('attr方法');
        },
        on() {
            console.log('on方法');
        }
    })

三、工具类方法

  添加工具类方法例如 $.each$.type 时。可以通过

    jQuery.each=function() {

    };
    jQuery.ajax=function() {

    };

来定义。
  可是代码冗余过多(jQuery.xxx),于是我们可以利用上面的拷贝继承来简化代码。
  由于此时是方法调用模式,故函数内部的 this 指向调用该方法的对象 jQuery

    jQuery.extend({
        each() {
            console.log('each方法');
        },
        ajax() {
            console.log('ajax方法');
        }
    })

1. $.each

  $.each 即遍历函数,例如:
  遍历数组,$.each([1,3,4], function(index, value){})
  遍历对象,$.each({age:18,height:200}, function(key, value){})

  在实现过程中,数组我们使用for循环,对象使用for…in循环

    jQuery.extend({
        each(obj, callback) {
            // 不仅仅可以遍历数组,也可以遍历伪数组{ length:0 }或{ 0:"a",1:"b",2:"c",length:3 }形式
            // 在这里,由于存在数组、伪数组2种情况,只能使用一种约定俗成的方式来通过他们的特征来进行判断:length属性,并且值>=0
            if ((length in obj) && obj.length >= 0) {
                for (let i = 0; i < obj.length; i++) {
                    callback(i,obj[i]);
                }
            } else {
                for (let i in obj) {
                    callback(i,obj[i]);
                }
            }
        }
    })

  调用时只需 $.each([1, 3, 5], function (index, value) {});,但此时如果在回调函数中打印 this 值,会发现它指向window。而在jQuery中,它修改了这里的this值,使之指向每一个遍历项。
  下面来实现这一步:

    jQuery.extend({
        each(obj, callback) {
            // 不仅仅可以遍历数组,也可以遍历伪数组{ length:0 }或{ 0:"a",1:"b",2:"c",length:3 }形式
            // 在这里,由于存在数组、伪数组2种情况,只能使用一种约定俗成的方式来通过他们的特征来进行判断:length属性,并且值>=0
            if ((length in obj) && obj.length >= 0) {
                for (let i = 0; i < obj.length; i++) {
                    callback.call(obj[i], i, obj[i]);
                }
            } else {
                for (let i in obj) {
                    callback.call(obj[i], i, obj[i]);
                }
            }
        }
    })

  上面我们实现了 each 方法遍历数组和对象,接下来我们实现第二个 each 方法。它不属于工具类方法,但很有意义,它常用于遍历jQuery选择器选中的每一个标签。
  例如:$("div").each(function (index, element) {})
  具体的实现如下:
  我们将它放到原型中,依然是一个 each 方法,但此时要遍历的对象不再是 obj,只需要指定一个 callback

    jQuery.fn.extend({
        each(callback) {
            // this:jquery对象,是个伪数组
            jQuery.each(this, callback)
        }
    })

2. $.type

  $.type 用于判断数据类型,也是jQuery中典型的工具类方法。,例如:
  判断数字,console.log($.type(1)); // "number"
  判断字符串,console.log($.type("abc")); // "string"
  判断数组,console.log($.type([1, 3, 5])); // "array"
  判断函数1,console.log($.type(function () {})); // "function"
  判断函数2,console.log($.type(Number)); // "function"

  在实现过程中,我们需要传递一个参数,即需要判断的数据data。
  我们使用 Object.prototype.toString.call() 来判断数据类型,如果传递数字1时,将产生这样的输出:"[object Number]",接着我们需要对输出的字符串转换一下,获取后半部分的小写形式这里就是 number

    jQuery.extend({
        each(obj, callback) {
            // 代码省略,见上面 $.each 部分的代码
        },

        type(data) {
            var type = Object.prototype.toString.call(data);
            return type
                .replace("[object ", "")
                .replace("]", "")
                .toLowerCase();
        }
    })

四、DOM操作方法

1. $(selector).css

  在JS库的封装之入口函数一文中我们编写过一个简单的CSS方法,当时备注了该CSS方法还是雏形,现在我们进行完善。
  这里我们的CSS方法可以不跟前面提到的 第二个 each 方法 放在一起,因为它是通用的,可以作为单独的模块进行划分。

  例如将样式操作部分放在一起,有css方法、show方法、hide方法。后面如果有属性操作、事件操作,再把它们各自的api放在一起。

  对于css方法,功能主要有两大类:
  ① 获取样式,$("div").css("color") // 只能获取到第一个div的颜色
  ② 设置样式,
        $("div").css("color","red") // 设置每一个div的字体颜色
        $("div").css({ color:"red","background-color":"blue" })
  我们通过参数个数来区分这三大功能。

    // 样式操作部分
    jQuery.fn.extend({
        css(...args) {
            var arg1 = args[0],
                arg2 = args[1];
            // 参数个数:1
            if (args.length === 1) {
                if (jQuery.type(arg1) === "string") {
                    // 获取样式:只能获取第一个元素的样式
                    let firstDom = this[0];
                    // 错误写法
                    // return firstDom.style[arg1];  只能获取行内样式
                    // 正确的写法
                    let domStyleObj = window.getComputedStyle(firstDom, null)
                    return domStyleObj[arg1];
                } else {
                    // 设置多个样式
                    // arg1:{ color:"red",font-size:"20px" }
                    var _that = this;
                    // 遍历出所有要添加的样式
                    jQuery.each(arg1, function (key, value) {
                        // 遍历每一个DOM元素,添加指定的样式
                        _that.css(key, value);
                    });
                    // 返回对象本身,便于实现链式编程
                    return _that;
                }
            } else {
                // 参数个数:2,设置单个样式
                // 第一步:遍历每一个DOM
                // 第二步:给DOM添加样式
                // this:表示一个jQuery对象
                this.each(function (index, dom) {
                    // this:表示一个DOM元素,等价于dom
                    this.style[arg1] = arg2;
                });
                // 返回对象本身,便于实现链式编程
                return this;
            }

        },
        show() {

        },
        hide() {

        }
    })

  小结上述代码的几个知识点:
  ① 获取样式不能使用 xxx.style.color 因为只能获取行内样式
  ② 获取样式的正确方式:
    现代浏览器:window.getComputedStyle(dom,null)
    IE6-8:dom.currentStyle

2. $(selector).show和$(selector).hide

  jQuery源码中的show和hide是支持动画的,这里我们略过,仅做样式的变换。
  让所有的元素显示,$("div").show()
  让所有的元素隐藏,$("div").hide()
  判断每一个元素,如果隐藏就显示,如果显示就隐藏,$("div").toggle()

    jQuery.fn.extend({
        css(...args) {
            // 代码省略,见上面 $(selector).css 部分的代码
        },
        show() {
            this.css("display", "block");
            return this;
        },
        hide() {
            this.css("display", "none");
            return this;
        },
        toggle() {
            this.each(function () {
                // this:dom元素,并不能直接访问到css方法
                // 需要将dom元素转换为jQuery对象(这部分代码在《JS库的封装之入口函数》一文的最后补充过)
                jQuery(this).css("display") === "none" ? jQuery(this).show() : jQuery(this).hide()
            })
        }
    })

  代码优化
  在上述代码中,每一次使用 jQuery(this) 都会产生一个新的jquery对象,每一次产生一个新的jQuery对象都会开辟一块新的内存,而这里的dom元素是唯一的,所以导致了一些不必要的内存浪费。
  解决方案
  ① 只创建一个jQuery对象就可以了,没有必要创建那么多次:创建一个变量 $this 保存这个jQuery对象。
  ② 不管是show还是hide方法,都是对 $this 执行的操作,可以通过变化方法名中括号语法 $this[xxx] 来决定执行哪个方法。

    jQuery.fn.extend({
        css(...args) {
            // 代码省略,见上面 $(selector).css 部分的代码
        },
        show() {
            this.css("display", "block");
            return this;
        },
        hide() {
            this.css("display", "none");
            return this;
        },
        toggle() {
            this.each(function () {
                let $this = jQuery(this);
                // $this.css("display") === "none" ? $this.show() : $this.hide()  // 等价于下方代码,性能上差异不大
                $this[$this.css("display") === "none" ? "show" : "hide"]();
            })
        }
    })

五、总结

  有了 $.extend 方法和 $.fn.extend 方法后,
  如果需要添加工具类方法,就使用 jQuery.extend()
  如果需要添加DOM操作方法,就使用 jQuery.fn.extend()

  本文主要是完成了extend方法、工具类方法和DOM操作方法的封装,接下来将添加事件。

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


  目录