《手写jQuery》

237 阅读4分钟

一. jQuery风格

  1. 原始:
window.jQuery = function(selector) {
    const elements = document.querySelectorAll(selector);
    const api = {
        addClass(className) {       //闭包,函数访问了外部的变量
            for (let i = 0; i < elements.length; i++) {
                elements[i].classList.add(className);
            }
        }
    };
    return api;  //不返回得到的元素数组,返回的是可以操作这个数组的对象api
}

定义一个jQuery(),是一个全局函数。通过selector传递参数,用于获取对应的元素。但是,它却不返回这些元素,而是返回一个对象api,这个对象里有一些函数(如addClass)可以操作这些元素。此时我们可以这样调用jQuery():

let api = jQuery(".test");
api.addClass("red")   //获取class是test的元素,并且为他们添加一个red类
  1. 链式操作

此时我们的addClass函数没有返回值,如果给它一个返回值,让它返回调用它的对象,也就是api,那么我执行api.addClass("red")时,这条语句的值就是api,那我就可以直接在后边再调用函数,这就是链式操作

window.jQuery = function(selector) {
    const elements = document.querySelectorAll(selector);
    const api = {
        addClass(className) {       //闭包,函数访问了外部的变量
            for (let i = 0; i < elements.length; i++) {
                elements[i].classList.add(className);
            }
            return api;
        }
    };
    return api;    //不返回得到的元素数组,返回的是可以操作这个数组的对象api
};

此时就可以链式调用函数:

let api = jQuery(".test");
api.addClass("red").addClass("blue"); //链式操作
  1. this

当调用对象的函数时,obj.fn(),JS会自动把函数前边的对象传给this。所以在调用api.addClass时,api就是this。所以我们可以在函数里用this代替api。

window.jQuery = function(selector) {
    const elements = document.querySelectorAll(selector);
    return {
        addClass(className) {       //闭包,函数访问了外部的变量
            for (let i = 0; i < elements.length; i++) {
                elements[i].classList.add(className);
            }
            return this;
        }
    }  //jQuery构造出来的对象
};

既然用this指代了api对象,那就可以直接return这个对象。在外部调用addClass函数时,哪个对象调用的,this就是那个对象。

let api = jQuery(".test");  //api可以是任何名字
api.addClass("red").addClass("blue");   //.前边的对象就是this

jQuery是构造函数吗?

  • 是?因为jQuery函数确实构造并且返回了一个对象。

  • 不是?因为不需要在jQuery前加new。

jQuery是一个不需要加new的构造函数,特殊的构造函数。我们约定:

jQuery对象指jQuery函数构造出来的对象。(类似Object对象)

二. jQuery返回不同的api对象

  1. jQuery('.xxx').find('.child') 查找.xxx里的.child元素
window.jQuery = function(selectorOrArray) {
  let elements;
  if (typeof selectorOrArray === "string") {
    elements = document.querySelectorAll(selectorOrArray);
  } else if (selectorOrArray instanceof Array) {   //对象判断类型用instanceof
    elements = selectorOrArray;
  }

  return {
    addClass(className) {       //闭包,函数访问了外部的变量
      for (let i = 0; i < elements.length; i++) {
        elements[i].classList.add(className);
      }
      return this;
    },
    find(selector1) {
      let array = [];
      for (let i = 0; i < elements.length; i++) {
        array = array.concat(
          Array.from(elements[i].querySelectorAll(selector1)) //伪数组
        );
      }         //得到的array就是查找的.xxx里的.child元素
      return jQuery(array);
    }
  };
};

如果我想找.test元素里的.child元素:jQuery('.test').find('.child')如果我在find函数里 return array,那得到的就是.child数组。

但是我想对查找到的.child元素添加类:jQuery('.test').find('.child').addClass('red'),如果我在find函数里返回得到的数组,数组不能调用函数;如果我在find函数里return this,像addClass函数一样,那么red类会被添加到.test上,因为find函数返回的是操作.test的对象。

所以,我需要用jQuery得到一个新的对象,这个对象是操作我用find函数得到的元素数组的,而不是操作第一次被传进去的.test元素的。所以find函数里:return jQuery(array); 返回一个操作数组的对象。

这样,我执行jQuery('.test').find('.child').addClass('red') 时,red类就会被添加到.child上.

jQuery是一个全局函数,这个函数接收什么参数,就会返回一个操作这个参数的对象。

三. 其他函数

each(fn) {
    for (let i = 0; i < elements.length; i++) {
        fn.call(null, elements[i], i);
    }
    return this; //this就是当前的api对象!
},
parent() {
    const array = []; //容纳爸爸
    this.each(node => {
        if (array.indexOf(node.parentNode) === -1) {
          array.push(node.parentNode);
        } //防止一个爸爸里有三个同名的孩子,这种情况下,如果不做判断,会返回三个一样的爸爸
    });
    return jQuery(array); //返回一个可以操作这些爸爸的对象
},
children() {
    const array = [];
    this.each(node => {
        array.push(...node.children); //node.children是一个数组,新语法在数组前边加...,把数组展开成元素的意思
    });
    return jQuery(array);
},
next() {
    const array = [];
    this.each(node => {
        array.push(node.nextElementSibling);
    });
    return jQuery(array);
},
prev() {
    const array = [];
    this.each(node => {
        array.push(node.previousElementSibling);
    });
    return jQuery(array);
},
print() {
    console.log(elements); //elements就是当前对象操作的元素们
}

四. 关于$

  1. 如果你嫌jQuery太长,可以给jQuery取一个别名:window.$=window.jQuery

    $就代表jQuery

  2. 怎么区分我获取到的对象是DOM对象还是jQuery对象?

const div1=document.querySelector('#test')

const div1=$('#test')

在我对div1这个对象继续操作时,如果是DOM对象,就可以使用appendChild/querySelector等函数;但是如果是$对象,就用find/each等函数。但是这个div1对象不知道是DOM对象还是jQuery对象。所以改成这样:

const $div1=$('#test') 如果是jQuery对象,就在变量前加一个$,用作区分。

约定:所有$开头的变量都是jQuery对象。

五. jQuery.prototype

根据不用的参数得到两个对象:

const $div1=$('#test1')

const $div2=$('#test2')

这两个对象都有find,each等函数,这些函数是他们的共有属性。那么为了节省内存,我就可以把这些对象的共有属性放到jQuery.prototype里,然后让对象的__proto__指向这块内存。同时,jQuery又给prototype起了一个别名,fn。