jQuery -- jQuery源码(一):核心功能

一、jQuery无new构建实例

1、$就是jQuery的别称

可以在$和jQuery对象挂在在window中,实现全局引用。

给Windows对象扩展一个$的属性,让它拿到jQuery构造函数的引用
可以用$访问到jQuery的构造函数
// jQuery.js
(function(root) {
   var jQuery = function(){}
root.$ = root.jQuery = jQuery;
})(this)

2、$()就是在创建jQuery的的实例对象

调用$()方法的目的是创建一个实例,构造函数直接调用是当做一个普通的函数来处理的
// jQuery.js
(function(root) {
   var jQuery = function() {}
})(this)


具体代码:

// jQuery.js
(function(root) {
    var jQuery = function() {
			console.log('生成一个实例对象');
    };
    root.$ = root.jQuery = jQuery;
})(this)

3、什么是构造函数

①构造函数也是一个普通函数,创建方式和普通函数一样,但习惯上首字母大写(小写也是可以的),用new关键字调用


②和普通函数的区别:
1)调用方式不一样
普通函数调用:直接调用,如,person()
构造函数调用:使用new关键字调用,如,new Person()
2)作用不同
构造函数用来新建实例对象


③构造函数的函数名和类名相同:Person()这个构造函数,Person既是函数名,也是这个对象的类名


④内部用this来构造属性和方法

function Person(name, gender, age) {
    this.name = name;
    this.gender = gender;
    this.age = age;
  	this.sayHi=function(){
    	alert("Hi")
    }
}
当创建上面的函数以后, 我们就可以通过 new 关键字调用,也就是通过构造函数来创建对象了
var p1 = new Person('张三', '男', 14); // 创建一个新的内存 #f1
var p2 = new Person('李四', '女', 28); // 创建一个新的内存 #f2

⑤构造函数的执行流程
A、立刻在堆内存中创建一个新的对象
B、将新建的对象设置为函数中的this
C、逐个执行函数中的代码
D、将新建的对象作为返回值
其他说法:
1、当以new关键字调用时,会创建一个新的内存空间 #f1,并标记为Person的实例
2、函数体内部的this指向该内存空间 #f1
以上两步:每当创建一个实例的时候,就会创建一个新的内存空间(#f1,#f2),创建#f1的时候,函数体内部的this指向#f1,创建#f2的时候,函数体内部的this指向#f2
3、执行函数体内的代码
给this添加属性,就相当于给实例添加属性
4、默认返回this
由于函数体内部的this指向新创建的内存空间(默认返回 this ,就相当于默认返回了该内存空间,也就是上图中的 #f1),而该内存空间(#f1)又被变量p1所接受。也就是说 p1 这个变量,保存的内存地址就是 #f1,同时被标记为 Person 的实例。

⑥普通函数:因为没有返回值,所以为undefined

function person(){
	
}
var per = person()
console.log(per) // undefined

⑦构造函数
1)默认返回this

function Person1(){
	this.name = 'zhangsan';
}
var p1 = new Person1()
console.log(p1) // Person1 {name: "zhangsan"}
console.log(p1.name) // zhangsan
首先,当用 new 关键字调用时,产生一个新的内存空间 #f11,并标记为 Person1 的实例;
接着,函数体内部的 this 指向该内存空间 #f11;
执行函数体内部的代码;
由于函数体内部的this 指向该内存空间,而该内存空间又被变量 p1 所接收,
所以 p1 中就会有一个 name 属性,属性值为 ‘zhangsan’。

2)手动添加一个基本数据类型的返回值,最终还是返回 this

function Person2() {
	this.age = 28;
	return 50;
}
var p2 = new Person2();
console.log(p2) // Person2 {age: 28}
console.log(p2.age);   // 28
如果上面是一个普通函数的调用,那么返回值就是 50
  1. 手动添加一个复杂数据类型(对象)的返回值,最终返回该对象
例一:
function Person3() {
 this.height = '180';
 return ['a', 'b', 'c'];
}

var p3 = new Person3();
console.log(p3.height);  // undefined
console.log(p3.length);  // 3
console.log(p3[0]);      // 'a'

例二:
function Person4() {
  this.gender = '男';
  return { gender: '中性' };
}

var p4 = new Person4();
console.log(p4.gender);  // '中性'

⑧不用new关键字,直接运行构造函数,是否会出错?如果不会出错,那么,用new和不用new调用构造函数,有什么区别?
1)用new调用构造函数,函数内部会发生如下变化:
创建一个this变量,该变量指向一个空对象。并且该对象继承函数的原型;
属性和方法被加入到this引用的对象中;
隐式返回this对象(如果没有显性返回其他对象)
用伪程序来展示上述变化:

function Person(name){
  // 创建this变量,指向空对象
  var this = {}; 
  // 属性和方法被加入到this引用的对象中
  this.name = name;
  this.say = function(){
    return "I am " + this.name;
  }
  // 返回this对象
  return this;
}
var person1 = new Person('nicole');
person1.say(); // "I am nicole"
可以看出,用new调用构造函数,最大特点为,this对象指向构造函数生成的对象,
所以,person1.say()会返回字符串: “I am nicole”。

注意:如果指定了返回对象,那么,this对象可能被丢失。

function Person(name){
  this.name = name;
  this.say = function(){
    return "I am " + this.name;
  }
  var that = {};
  that.name = "It is that!";
  return that;
}

var person1 = new Person('nicole');
person1.name; // "It is that!"


2)直接调用函数
如果直接调用函数,this对象指向window,并且,不会默认返回任何对象(除非显性声明返回值)。

var person1 = Person('nicole');
person1; // undefined
window.name; // nicole

二、共享原型设计

按照正常思维,函数里面直接return一个new jQuery即可:

// jQuery.js
(function(root) {
    var jQuery = function() {
  		return new jQuery();
    };
    root.$ = root.jQuery = jQuery;
})(this)
通过new的方式去创建jQuery的实例,实际上会做两步操作:
1、创建一个object对象
2、调用本身的构造函数
这样会引发死循环的问题,因为该函数在不断地创建一个新对象,不断的调用构造函数

那么jQuery是如何设计的呢?我们来看下jquery的共享原型设计图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vlwgJ7K6-1577168361128)(https://cdn.nlark.com/yuque/0/2019/png/467105/1567079008264-e9368f7c-41ec-4b66-bbed-a5ad0e90a2ba.png#align=left&display=inline&height=1328&name=jquery%E5%85%B1%E4%BA%AB%E5%8E%9F%E5%9E%8B%E8%AE%BE%E8%AE%A1%E5%9B%BE.png&originHeight=1328&originWidth=2376&size=505896&status=done&width=2376)]


从图中可以看出,当我们调用$的时候,实际上是在调用jQuery原型上的init方法,把init方法当成一个构造函数,然后返回它的实例对象。这样会产生一个问题,就是我们实际是要创建jQuery对象,并访问jquery原型上扩展的属性和方法。如何解决这个问题?jquery用了一个共享原型的设计,那就是将jQuery原型上的init构造函数和jQuery本身共享一个原型对象。


具体代码如下:

// jQuery.js
(function(root) {
    var jQuery = function() {
      // 如果想直接调用$()方法,我们尝试直接return
      // 但这种写法是错误的,如果直接return本身,会造成死循环
      // return new jQuery();
      return new jQuery.prototype.init;
    };
		给jQuery的原型扩展了一个init的方法
    jQuery.prototype = {
      init: function() {

      }
    }
  // 共享原型对象
  找到jQuery原型对象上的init方法,把它当做一个构造函数,
  设置它的原型对象指向jQuery的原型对象,达到共享原型的目的
	jQuery.prototype.init.prototype = jQuery.prototype;
  root.$ = root.jQuery = jQuery;
})(this)


这样我们打印console.log($()),就可以看到jquery原型对象的init方法了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SWBArZFf-1577168361138)(https://cdn.nlark.com/yuque/0/2019/png/467105/1567079723886-e9ea1980-6a09-42d3-b1d5-515a1262d28b.png#align=left&display=inline&height=201&name=image.png&originHeight=402&originWidth=1028&size=58076&status=done&width=514)]

这时候的 ( ) 实 际 指 向 的 是 j Q u e r y 原 型 上 的 i n i t 方 法 , 而 i n i t 的 原 型 和 j Q u e r y 本 身 的 原 型 共 享 一 个 原 型 对 象 , 这 样 往 j Q u e r y 上 扩 展 的 方 法 就 可 以 通 过 ()实际指向的是jQuery原型上的init方法,而init的原型和jQuery本身的原型共享一个原型对象,这样往jQuery上扩展的方法就可以通过 ()jQueryinitinitjQueryjQuery()获取。

例如我们在jQuery的原型上扩展一个css的方法:

// jQuery.js
jQuery.prototype = {
    init: function() {

    },
    css: function() {

    }
}

console.log($())就可以看到css这个方法了
在这里插入图片描述

三、extend源码解析

jQuery的核心函数extend,用于对对象进行扩展,一般有三种方式:

1、对任意对象进行扩展,参数必须是两个及两个以上

第一个对象没有事扩展,有,是重新赋值
// demo.html
$.extend({}, { name: 'arbor'});

2、对jQuery本身扩展属性或者方法

// demo.html
$.extend({
	work: function() {
  
  }
})
jQuery.work();

3、对jQuery实例对象扩展属性或方法

// demo.html
$.fn.extend({
  sex: '男'
})
$().sex;

这里的$.fn_指的是jQuery的prototype原型对象_

那如何实现呢?
很简单,就是给jQuery扩展一个fn的方法,去等于jQuery的原型对象

// jQuery.js
jQuery.fn = jQuery.prototype = {
  init: function() {

  },
  css: function() {

  }
}
// 当然,共享原型对象也得写成:
// 共享原型对象
jQuery.fn.init.prototype = jQuery.fn;

下面,正题来了,核心函数extend是如何实现的呢?
首先需要保证 . f n 和 .fn和 .fn.都可以调用到,其次_在jQuery的原型对象和jQuery对象上新建了同一个extend的函数_

// jQuery.js
jQuery.fn.extend = jQuery.extend = function() {

}

如何判断是给什么进行扩展属性和方法呢?
通过参数来判断
A、给任意对象扩展,参数必须是两个及两个以上
B、给jQuery本身或者jQuery实例对象扩展,只需要传递一个参数
不管是哪种,第一个参数必须是object
**
其实,我们分析extend函数的调用方式,无外乎两种情况,一种是给任意对象扩展,从第二个参数开始作为对第一个参数的补充,一种是直接扩展,既然这样,我们就可以对第一个参数做文章了:

1、如果第一个参数是个对象,那么我们就可以把从第二个开始的参数进行遍历,然后填充到第一个参数中。这里可以用到arguments;


2、如果第一个参数不是对象,那么就简单了,直接把参数扩展到_jQuery对象或者jQuery的实例对象上即可,因为参数只有一个。这里要特别注意的是,this的指向问题,this是指向jQuery对象本身?还是jQuery的实例对象上?什么意思呢?当你往jquery对象上扩展一个方法或者属性时,这时候传参只有一个,那是不是要告诉程序,往哪里扩展?是往jQuery上扩展呢?还是往jQuery实例的原型对象扩展呢?详见代码18-24行。

既然如此,我们就来看看代码是如何实现的。

// 浅拷贝
jQuery.fn.extend = jQuery.extend = function() {
  
  // 定义一个变量target,用于存储传入的第一个值,如果值为空,那默认为空对象的引用
  /* 
  * max
  * 通过arguments获取用户调用时传递的参数,获取第一个参数,如果没有传就创建一个对象并赋值给一个变量
  * 此时通过arguments[0]获取用户传递过来的第一个参数,但是并没有100%保证这个参数一定是一个object
  * 就要进行数据类型的判断:见
  * */
  var target = arguments[0] || {} 
  
  // 定义一个变量len,用于存储传参的长度
  var len = arguments.length;
  // 定义一个遍历i,
  var i = 1;
  // option 第二个参数后面的参数遍历过程中单个的存储
  var option;
	// 判断参数一的类型,如果不是object就创建一个对象并赋值给它
  if (target !== 'object') {
    target = {};
  }

  // 如果参数只有一个,那么就是对jQuery对象或者jQuery的实例对象扩展
  // 例如: 1.jQuery对象的扩展 $.extend({ work: function() {}})
  //       2.jQuery的实例对象扩展 $.fn.extend({ sex: '男' })
  if (length === i) {
    // 这里的this等于函数调用的对象本身,即jQuery对象或者jQuery的实例对象
    target = this;
  }

  // 如果参数不止一个,给任意对象进行扩展的逻辑,例如$.extend({}, {name: 'arbor'});
  // 也就是浅拷贝
  for(; i < len; i++) {
    if ((option = arguments[i]) !== null) {
      for (key in option) {
        // target指的是第一个参数对象,进行扩展
        target[key] = option[key];
      }
    }
  }

  return target; // 一定要有返回值,不然是undefined
}


我们可以验证一下:

<script>
  var ret = { name: 'arbor', list: { age: 30 }};
  // 给任意对象扩展
  var obj = $.extend({}, ret);
  console.log(obj)
</script>

执行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CDJTfG6p-1577168361140)(https://cdn.nlark.com/yuque/0/2019/png/467105/1567134677301-1565f953-7af9-494d-a980-6f4c141484ca.png#align=left&display=inline&height=72&name=image.png&originHeight=144&originWidth=1024&size=24784&status=done&width=512)]


到这里,其实extend的基本逻辑已经捋清楚了。但是,如果代码是这样子的呢?结果是否符合我们的预期?

<script>
  var ret = { name: 'arbor', list: { age: 30 }};
  var res = { list: { sex: 'male' }};
  // 给任意对象扩展
  var obj = $.extend(true, {}, ret, res);
  console.log(obj)
</script>

执行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D1aPjf04-1577168361141)(https://cdn.nlark.com/yuque/0/2019/png/467105/1567135747923-e70ff4ef-719d-4ffd-bd36-4e4f3ef0c4ef.png#align=left&display=inline&height=67&name=image.png&originHeight=134&originWidth=1018&size=24585&status=done&width=509)]


从执行结果来看,按照正常思维,list对象里面应该是包含有age和sex。那是否需要一个参数去控制呢?答案是肯定的,jQuery中就提供了一种接口调用方法,当第一个对象是布尔类型时,且等于true的时候,就会去执行深拷贝,而我们上面代码中的for循环其实只是一种浅拷贝。那么,我们来想想要如何实现?


1、需要对传参的第一个变量进行判断,是否为布尔类型,如果是布尔类型,我们就需要将第二个参数作为需要扩展的对象,然后从第三个参数开始遍历,对第二参数进行扩展;


2、在遍历过程中,在上述浅拷贝的代码基础上,需要判断对象中是否已经有当前的key了,如果没有,直接object[key] = value即可,如果有,那么要拿到拥有key的对象再次去递归循环第二层。什么意思呢?其实就是在上述例子中,要使用res对第一个变量进行扩展,会发现变量上已经有list了,那就把{ sex: ‘male’ }拿到做第二次递归拷贝,看不懂没事,等会我们会分析整个执行的过程。


3、在拷贝过程中,甚至jQuery的整个实现过程中,我们可能会抽象出来一些工具类,例如判断是否为对象,或者是否为数组等,这时候就可以利用extend方法往jQuery上附加方法了。
而之说以说extend是jQuery的核心函数,这里就体现出来了:jQuery几乎所有的模块和函数都是通过extend函数扩展的。
浅拷贝只拷贝第一层,如果是基本类型就是拷贝,否则就是引用(后者会在对象修改属性时互相影响)
深、浅拷贝是针对对象、数组来说的,深拷贝是创建一个新的对象或数组,然后把需要拷贝的属性或元素添加至新建的对象或数组上,相当于把要拷贝的信息复制了一份。浅拷贝是把属性指向要拷贝的对象或数组。深拷贝的对象修改后,不会影响原对象,浅拷贝则会。
用通俗的例子来说,深拷贝就像是把张三克隆了一下,浅拷贝是给张三取了个小名,比如小张。假如克隆人摔倒了,张三不会受伤,因为他们是两个人。但如果小张摔倒了,张三也会受伤,因为他们是同一个人,只是取了两个名字而已

好了,到这里,那我们来看看代码是如何实现的吧:

// demo.html
<script>
  // 深拷贝、浅拷贝
  // 特征:第一个参数是布尔类型,true、false,否则就是浅拷贝
  var ret = {name:'菠菜',list:{age:10}}
  var res = {list:{sex:'女'}}
  /*浅拷贝:只做替换的关系
  * 在遍历ret的时候会给{}扩展一个list的属性,并把它的值赋值过去
  * 当遍历res的时候,会找到list的属性,并把指向的对象的引用替换掉{}里面list属性的值的引用
  */
  var obj = $.entend({},ret,res) //obj: {name:'菠菜',list:{sex:'女'}}
  // 深拷贝
  var obj = $.entend(true,{},ret,res)
</script>
  
// jQuery.js
(function(root) {
  jQuery.fn.extend = jQuery.extend = function() {
    // 拿到第一个参数对象
    var target = arguments[0] || {}
    var length = arguments.length;
    // 对任意对象进行扩展,可忽略第一个对象,因为从第二个参数开始,都是作为第一个对象的扩展
    var i = 1;
    // deep变量是用于对第一个参数进行判断,是进行深拷贝(追加)还是浅拷贝(覆盖)
    var deep = false;
    // option 需扩展对象{}后面的参数对象
    // key 参数对象的key
    // copy 参数对象的value
    // src 是判断需扩展对象上是否已存在key
    // copyIsArray 是防止多调用一次判断是否为数组的方法,简化代码
    var option, key, copy, src, copyIsArray, clone;

    if (typeof target === 'boolean') {
      // 判断深浅拷贝
      deep = target;
      // 将需要扩展的对象赋值为第二个
      target = arguments[1];
      // 循环从第三个开始
      i = 2;
    }

    if (typeof target !== 'object') {
      target = {};
    }

    // 如果参数只有一个,那么就是对jQuery对象或者jQuery的实例对象扩展
    // 例如: 1.jQuery对象的扩展 $.extend({ work: function() {}})
    //       2.jQuery的实例对象扩展 $.fn.extend({ sex: '男' })
    if (length === i) {
      // 这里的this等于函数调用的对象本身,即jQuery对象或者jQuery的实例对象
      target = this;
      // 这里的用意是拿到jQuery内置的一些方法,例如:
      i--;//让i的值变为0
    }

    // 深浅拷贝
    // 给任意对象进行扩展的逻辑,例如$.extend({}, {name: 'arbor'});
    for(; i < length ; i++) {
      // option:需要遍历的属性的值
      if ((option = arguments[i]) !== null) {
        for(key in option) {
          // 将第二个参数之后的值赋值给第一个对象
          // 注意如果有多个参数,参数的key相同的话,会实现覆盖
          // target[key] = option[key];
          copy = option[key];
          // 首次遍历的时候src可能是undefined,因为第二个参数值可能是空{}
          src = target[key];
          // 判断是深浅拷贝,有值是深拷贝,并且后面的参数是Object还是Array类型
          if (deep && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
            if (copyIsArray) {// 如果是数组
              // 每次都要重置
              copyIsArray = false;
              // src有值并且是数组就直接赋值,否则就创建空数组
              clone = src && jQuery.isArray(src) ? src : [];
            } else {// 如果是对象
              // src有值并且是对象就直接赋值,否则就创建空对象
              clone = src && jQuery.isPlainObject(src) ? src : {};
            }
            // 做一次浅拷贝
            target[key] = jQuery.extend(deep, clone, copy);
          } else if (copy !== undefined) { //判断浅拷贝是否有值
            target[key] = copy;
          }
        }
      };
    }

    return target;
  }
  // 共享原型对象
  jQuery.fn.init.prototype = jQuery.fn;
  jQuery.extend({
    // 类型检测的方法
    isPlainObject: function(obj) {
      return toString.call(obj) === '[object Object]';
    },
    isArray: function(obj) {
      return toString.call(obj) === '[object Array]';
    }
  })
  root.$ = root.jQuery = jQuery;  
})(this)


注意到36行的i–没有,如果我们是往jQuery本身附加一些属性或者方法时,循环是需要从下标为0开始的,而不能从初始化的下标为1开始。

62行是核心代码,就是对存在多层级的对象或者数组递归地去拷贝,从而达到深拷贝的效果。

最后记得return出来,否则你拿到的肯定是undefined。


最后,我们再来捋一下执行过程吧:

// for循环中
deep = true;
i = 2;
// 1.首层循环
option = ret = { name: 'arbor', list: { age: 30 }};
    // 1.1循环option对象的key和value,即name: 'arbor'
    key = 'name'
    copy =	option[key] = 'arbor';
    src = target[key] = undefined;
    // 判断走else-if
    target[key] = copy
    // 此时的target的值
    target: {
      name: 'arbor',
    }

    // 1.2 for..in继续循环list: { age: 30 }
    key = 'list'
    copy =	option[key] = { age: 30 };
    src = target[key] = undefined;
    // 判断走if
    copyIsArray = false;
    clone = {};
    // 递归执行jQuery.extend,进行浅拷贝
    target[key] = jQuery.extend(true, {}, { age: 30 }) = { age: 30 }
    // 此时的target的值, 执行1.2.1得到的值
    target: {
      name: 'arbor',
      list: { age: 30 }
    }

        // 1.2.1 list
        option = { age: 30 }
        key = 'age'
        copy =	option[key] = 30;
        src = target[key] = undefined;
        // 判断走else-if
        target[key] = copy
        // 此时的target的值
        target: {
          age: 30,
        }
        // 子循环结束,return target
      
// 2.首层循环
deep = true;
i = 2;
option = res = { list: { sex: 'male' }};
    // 2.1 for..in继续循环list: { sex: 'male' }
    key = 'list'
    copy =	option[key] = { sex: 'male' };
    src = target[key] = { age: 30 };
    // 判断走if
    copyIsArray = false;
    clone = { age: 30 };
    // 递归执行jQuery.extend,进行浅拷贝
    target[key] = jQuery.extend(true, { age: 30 }, { sex: 'male' }) = { sex: 'male' }
    // 此时的target的值, 执行2.1.1得到的值,此时return最终的target
    target: {
      name: 'arbor',
      list: { age: 30, sex: 'male' }
    }

        // 2.1.1 list
        option = { sex: 'male' }
        key = 'sex'
        copy =	option[key] = 'male';
        src = target[key] = undefined;
        // 判断走else-if
        target[key] = copy
        // 此时的target的值
        target: {
          sex: 'male',
        }
    		// 子循环结束,return target

至此,jQuery的整体架构和核心功能就到一段落了。

你可能感兴趣的:(JavaScript,jQuery,前端)