jQuery 是一个快速、简洁的 JavaScript 框架,是继 Prototype 之后又一个优秀的 JavaScript 代码库(或 JavaScript 框架)。jQuery 设计的宗旨是“write Less,Do More”,即倡导写更少的代码,做更多的事情。它封装 JavaScript 常用的功能代码,提供一种简便的 JavaScript 设计模式,优化 HTML 文档操作、事件处理、动画设计和 Ajax 交互。

如此优秀的一个代码库,即便没有以前流行,也是有很多地方值得学习的。

本文并非讲解jquery源码,而是记录从源码学习到的思想、技术等

构造函数与继承

JavaScript是一门面向对象的编程语言,但是与传统面向对象语言不同的是,JavaScript没有提供class关键字,并且JavaScript的是基于原型继承的面向对象语言。那么,在JavaScript中编写类的过程,必定与传统的面向对象语言有所区别。

传统的类与继承

ES6之前,都是通过创建构造函数的方式来创建一个类,比如:

// Book类的原型
const BookPrototype = {
  getName() {
    return this.name
  },
}

// Book类
function Book(name, author, content) {
  this.name = name
  this.author = author
  this.content = content
}

// Book类继承自 BookPrototype
Book.prototype = BookPrototype

const book = new Book('金瓶梅', '佚名', '此处省略999999999字')
console.log(book.getName())

BookPrototypeBook类的原型(基类),使用new关键字创建的Book实例,都会包含BookPrototype里面的属性和方法。这里刚开始然我有些迷惑,参考经典的面向对象语言,基类应该也是一个类,这里却是一个对象字面量。首先,需要深刻明白一点: JavaScript 中,一切皆是对象。比如一个函数,我们可以给它加上任何的属性

function bar() {}
bar.sayHi = function() {
  console.log('hi~')
}
bar.sayHi()
// hi~

其次,也是这里的迷惑点,JavaScript是基于原型的继承,意思就是,一个构造函数要继承某些属性方法,只需要将这些方法放到一个容器(对象)里,再赋值给prototype属性就可以在实例中访问。当我们需要创建静态方法/变量的时候,可以直接在构造函数上绑定(一切皆对象)。在 JavaScript 中,静态方法不会被继承。

function Book(name, author, content) {
  this.name = name
  this.author = author
  this.content = content
}
Book.prototype.getName = function() {
  return this.name
}
// 静态方法
Book.foo = function() {
  console.log('foo')
}

Book.foo()

ES6 中的类与继承

ES6标准中,提出了class关键字,我们可以使用类似于Java创建类的语法来创建一个JavaScript类。

class Book {
  constructor(title, author, content) {
    this.title = title
    this.author = author
    this.content = content
  }

  getName() {
    return this.title
  }
}

const book = new Book('金瓶梅', '佚名', '此处省略999999999字')
console.log(book.getName()) // 金瓶梅

我们可以直接通过extends关键字进行类的继承

class StoryBook extends Book {
  constructor(title, author, content, storyType) {
    super(title, author, content)
    this.storyType = storyType
  }

  getStoryType() {
    return this.storyType
  }
}
book = new StoryBook('金瓶梅', '佚名', '此处省略999999999字', 'fairyTale')
book.getName() // 金瓶梅
book.getStoryType() // fairyTale

ES6中的classextends实际上都是语法糖,根本上还是通过前一节中的方法来创建类和继承类。JavaScript基于原型的继承方式,赋予了更多的灵活性,jQuery的构造函数就是一个非常好的实例。

jQuery 入口

jQuery最基本的使用出发,会发现jQuery好像不是一个构造函数。确实是这样,当我们获取一个元素const box = $('.box1'),将 box 打印出来看看。

从打印结果可以看出几个东西:

  • box对象是jQuery.fn.init的一个实例
  • 改实例是一个包含类数组集合对象
  • 改对象有一个prevObject属性,指向了两一个对象的引用

接下来就分别从这几点出发来分析jQuery源码。

jQuery 构造函数

通常情况下,我们会以函数调用方式($(‘selector’))获取一个jQuery对象,同时,我们也可以使用new $('selector')来获取jQuery实例,我们先来思考这一个问题:如何同时满足函数调用和构造函数调用。实际上不需要太多思考就能想到如下代码:

function myQuery(selector) {
  if (!(this instanceof myQuery)) {
    return new myQuery(selector)
  }
  // ...
  return this
}

但是我们发现jQuery并没有这样写,通过观察发现我们得到的是一个jQuery.fn.init的实例,下面就开始进入源码看看jQuery.fnjQuery.fn.init是个什么东西。

// 源码
jQuery = function(selector, context) {
  // The jQuery object is actually just the init constructor 'enhanced'
  // Need init if jQuery is called (just allow error to be thrown if not included)
  return new jQuery.fn.init(selector, context)
}

这是jQuery的大门,所有故事从这里开始,至此我们知道了jQuery构造函数实际上是个工厂函数,它返回了另一个类的实例。接下来继续看看jQuery.fn

jQuery.fn = jQuery.prototype = {
  // The current version of jQuery being used
  jquery: version,

  constructor: jQuery,

  // The default length of a jQuery object is 0
  length: 0,
  // ...
}

init = jQuery.fn.init = function(selector, context, root) {
  // ...
  return this
}

// Give the init function the jQuery prototype for later instantiation
init.prototype = jQuery.fn

可以看出jQuery.fn就是jQuery.prototype的一个引用,而init是原型上的一个构造函数。里面有非常核心的一行代码init.prototype = jQuery.fn。这行代码将jQuery的原型共享给了init构造函数,jQueryinit的原型是同一个,这样jQuery对象就能访问jQuery原型上的方法和变量。

到这里基本清楚了jQuery创建实例到底是怎么一回事,至于和我们刚开始想的不一样,主要原因是更方便的进行对象的初始化操作。

jQuery 插件

说到了原型顺便再看一下jQuery的插件,jQuery的插件开发基本上使用过jQuery的人都知道怎么操作,分为两种方式,一种方式是直接在$.fn上面添加方法,另一种是调用$.fn.extend()方法。前面这两种方法都是对jQuery的原型进行扩展,在其实例中才能使用,还有一个$.extend()方法,是用来扩展jQuery的静态方法。

// 源码
jQuery.extend = jQuery.fn.extend = function() {}

源码中,jQuery.extendfn.extend引用的是同一个方法,但是作用的上下文却不同。这里是非常巧妙的一个点,当我们直接使用$.extend()时添加的是静态方法,而是用来$.fn.extend()时添加的是实例方法。

小结

jQuery并没有非常高级的技术,内部原理都是非常简单的逻辑封装,但是整个内部架构设计的非常好,有许多值得学习的巧妙设计。

参考文章