上一节我们已经介绍了 jQuery 实例对象的创建过程,这一节我们介绍一个方法:jQuery.extend。为什么要先介绍这一个方法呢,首先,jQuery 这个框架除了内部的代码写的很完美之外,社区影响力这么大的原因还有它拥有庞大的插件库。由于大量成熟的插件的存在和完备的生态圈,很多人刚学习前端还是会选择 jQuery 进行入门。那么 jQuery 内部是为什么提供了什么接口来实现让我们用户自定义扩展插件的呢?

其实就是使用了 extend 方法,它本身在 jQuery 内部的源码中也频繁使用,它不仅可以给 jQuery 本身扩展,也可以为jQuery 的实例对象扩展插件。

在探索该方法内部的实现之前我们必须先了解这个方法的作用。

jQuery.extend 方法使用

$.fn.extend({
	eat() {
		console.log('eat');
	}
});

$.extend({
	drink() {
		console.log('drink');
	}
});

$().eat();
$.drink();

var obj = {};
$.fn.extend(obj, {name: 'jerry'});
console.log(obj);

打印结果为:

350ba27d44d58ecd46943a429ab3650a.png

可以看到该方法的三个作用:

  1. 为 jQuery 的实例对象身上扩展属性方法
  2. 为 jQuery 本身扩展属性方法
  3. 为任意对象扩展属性方法

另外第一个参数如果是布尔值,可以指定扩展时是否使用深拷贝。

进入源码

了解了该方法的使用后我们就一起走进源码去一探究竟它是怎么具备这么多功能的。

首先为 jQuery 和其原型对象上添加 extend 方法:

jQuery.extend = jQuery.fn.extend = function() {
 // ...
};

这也就标明该方法可以有两种调用方法。

这里先贴出完整的代码:

jQuery.extend = jQuery.fn.extend = function() {
	// 初始化变量
	var options, name, src, copy, copyIsArray, clone,
		target = arguments[ 0 ] || {},
		i = 1,
		length = arguments.length,
		deep = false;

	// 判断用户是否需要自定义深浅拷贝
	if ( typeof target === "boolean" ) {
		deep = target; // deep 为第一个参数

		// 如果需要自定义,那么 target 就是第二个参数
		target = arguments[ i ] || {};
		i++;
	}

	// 禁止用户的非法传入值
	if ( typeof target !== "object" && !isFunction( target ) ) {
		target = {};
	}

	// 如果只有一个参数,就是为 jQuery 本身或者其实例对象扩展
	if ( i === length ) {
		target = this;
		i--;
	}

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

		// 只处理不是 undefined 和 null 的值
		if ( ( options = arguments[ i ] ) != null ) {

			// 循环其他对象,扩展目标 target 对象
			for ( name in options ) {
				copy = options[ name ]; // copy 是需要遍历对象的属性值

				// 不改变 target 的 __proto__ 属性
				// 如果目标对象和属性值 copy 相等就进行下一轮循环
				if ( name === "__proto__" || target === copy ) {
					continue;
				}

				// 如果是深克隆,并且 copy 有值,并且 copy 是原生对象或者数组
				if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
					( copyIsArray = Array.isArray( copy ) ) ) ) {
					// src 为 target 原来的 name 属性值
					src = target[ name ];

					if ( copyIsArray && !Array.isArray( src ) ) {
						// 如果 copy 是数组并且原来的 name 属性值 src 不是数组
						clone = [];
					} else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {
						// copy 不是数组,src 不是原生对象
						clone = {};
					} else {
						// 其他都是正常情况
						clone = src;
					}
					// 避免影响下次循环
					copyIsArray = false;

					// 使用递归进行深克隆
					target[ name ] = jQuery.extend( deep, clone, copy );

				// 进行的是浅克隆,执行拿到对象的引用
				} else if ( copy !== undefined ) {
					target[ name ] = copy;
				}
			}
		}
	}

	// 最后返回目标对象
	return target;
};

对整体代码进行一遍审视后,可能你读下来感觉代码平平无奇。但是我们可以看到初始化变量的时候i = 1,这种大神写的代码就不会有毛病。

这里我们会发现有很多的方法比如 jQuery.isPlainObject 不知道是如何实现的,其实我们在读源码的过程中根据命名的语义也可以推断方法的作用,我们可以使用 Ctrl + F 键在源码中搜索这些方法的实现。这里我将不在给出,因为重点不在于此。

接下来我们来深挖一些细节。

传参的灵活性

读完源码后你也已经感觉到了对于这样一个 API 的使用,我们可以传递多种不同的参数,不同参数传递时似乎函数的作用都发生了改变。

这就是我们需要学习的一个地方,对于一个 API 接口的设计,我们应该足够灵活,灵活的技巧其实很简单,在 ES6 中我们可以直接使用 rest 参数来获取用户的所有传参,在这以前,都是使用 arguments 类数组来获取,不管通过什么方式,我们的第一部都是获取用户传递的所有参数。

然后我们需要根据用户传递参数的个数,特殊位置的参数的类型来判断出用户究竟是想实现哪种效果。在内部对用户的输入进行获取,判断,初始化以及错误传递处理

这样健壮的代码最终加上逻辑实现,就可以实现出相对完美的接口。

this

这段代码的另一个灵活之处在于 this 关键字的使用,试想我们究竟是怎么判断出用户是想为 jQuery 本身还是其实例对象扩展方法属性呢。

其实很简单,利用 JavaScript 的特性,this (除了箭头函数)指向方法的调用者。

根据这一特性在代码中有:

// 如果只有一个参数,就是为 jQuery 本身或者其实例对象扩展
if ( i === length ) {
	target = this; // 设置需要扩展的对象为当前的调用者
	i--;
}

这一写法就完全解决了如果判断用户究竟是想要给 jQuery本身还是其实例对象扩展属性方法。

感悟

所有的库、框架的出现在方便我们使用的同时也同时推进着语言本身的发展。

在 ES6 中有一个方法:Object.assign

  • Object.assign()方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
  • Object.assign()方法的第一个参数是目标对象,后面的参数都是源对象。

你肯定发现了这个方法与 jQuery.extend() 的第三种为任意对象扩展的作用是一样的,但是这个原生的方法不支持深拷贝,也就是它其实是浅拷贝。毕竟如果采用深拷贝是需要新开一片内存空间的,对语言执行时的效率会有影响。

类似这种库驱动语言的发展的例子还有很多很多,比如你肯定听说过社区版的 promise 或者promise 这一特性最早是由社区提出并实现的这样的话。其实,社区版的 promise 最早就是由 jQuery 内部提出并实现的。ES6 将其写入了语言的规范。接下来的两节将会介绍 jQuery 内部对 promise 实现的源码。

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐