腊月的季节

jQuery原理深究

jQuery的无new构建

直接使用$(‘’)其实就是一种无new构造方式的时候,其本质就是相当于new jQuery(),那么jQuery内部是如何运行的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(function(window, undefined) {
var
// ...
jQuery = function(selector, context) {
// The jQuery object is actually just the init constructor 'enhanced'
return new jQuery.fn.init(selector, context, rootjQuery);
},

jQuery.fn = jQuery.prototype = {
init: function(selector, context, rootjQuery) {
// ...
}
}
jQuery.fn.init.prototype = jQuery.fn;
})(window);

首先我们要知道函数声明和函数表达式

在ECMAScript中,创建函数的最常用的两个方法是函数表达式和函数声明,两者期间的区别是函数声明必须带有标识符(就是我们常说的函数名),而函数表达式则可以省略这个标识符。

1
2
3
4
5
6
function foo(){} // 声明,因为它是程序的一部分
var bar = function foo(){}; // 表达式,因为它是赋值表达式的一部分
new function bar(){}; // 表达式,因为它是new表达式
(function(){
function bar(){} // 声明,因为它是函数体的一部分
})();

还有一种函数表达式不太常见,就是被括号括住的(function foo(){}),他是表达式的原因是因为括号()是一个分组操作符,它的内部只能包含表达式。

1
2
3
(function(window, undefined) {
/...
})(window)

可以将上面的代码结构分成两部分:(function(){window,undefined})和(window),
第一个()是表达式,而这个表达式本身是一个匿名函数,所以在第一个表达式后面加(window)就表示执行这个匿名函数并传入参数window。

原型prototype

在JavaScript中,原型也是一个对象,通过原型可以实现对象的属性继承,JavaScript的对象中都包含了一个”[[Prototype]]”内部属性,这个属性所对应的就是该对象的原型。对于”prototype”和”proto“这个两个属性有的时候可能会弄混,”Person.prototype”和”Person.proto“是完全不同的。
说一下”prototype”和”proto“进行简单的介绍:
1、对于所有的对象,都有proto属性,这个属性对应该对象的原型。
2、对于函数对象,除了proto属性之外,还有prototype属性,当一个函数被用作构造函数来创建实例时,该函数的prototype属性值将被作为原型赋值给所有对象实例(也就是设置实例的proto属性)

1
2
3
4
5
6
7
8
9
10
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.getInfo = function(){
console.log(this.name + " is " + this.age + " years old");
};
//调用
var will = new Person("Will", 28);
will.getInfo();//"Will is 28 years old"

闭包

闭包的定义:
当一个内部函数被其外部函数之外的变量引用时,就形成了一个闭包。
闭包的作用:
在了解闭包的作用之前,我们先了解一下JavaScript中的GC机制:
在JavaScript中,如果一个对象不再被引用,那么这个对象就会被GC回收,否则这个对象一直会保存在内部中。
举个例子,B定义在A中,因此B依赖于A,而外部变量C又引用了B,所以A间接的被C引用,也就是说,A不会被GC回收,会一直保存在内存中。

1
2
3
4
5
6
7
8
9
10
11
12
function A(){
var count = 0;
function B(){
count ++;
console.log(count);
}
return B;
}
var c = A();
c();// 1
c();// 2
c();// 3

count是A中的一个变量,它的值在B中被改变,函数B每执行一次,count的值就在原来的基础上累加1,。因此,A中的count一直保存在内存中。
这就是闭包的作用,有时候我们需要一个模块中定义这样一个变量:希望这个变量一直保存在内存中但又不会“污染”全局的变量,这个时候,我们就可以用闭包来定义这个模块。

分析jQuery源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function(window, undefined) {
var
// ...
  jQuery = function(selector, context) {
// The jQuery object is actually just the init constructor 'enhanced'
return new jQuery.fn.init(selector, context, rootjQuery);
},
jQuery.fn = jQuery.prototype = {
init: function(selector, context, rootjQuery) {
// ...
}
}
jQuery.fn.init.prototype = jQuery.fn;
})(window);

jQuery.fn的init函数被jQuery的构造函数调用了,这里形成了一个闭包。
构造函数及调用代码:

1
2
3
4
5
// ...
  jQuery = function(selector, context) {
// The jQuery object is actually just the init constructor 'enhanced'
return new jQuery.fn.init(selector, context, rootjQuery);
},

如何实现无new构建

JavaScript是函数式语言,函数可以实现类,类就是面向对象编程中最基本的概念

1
2
3
4
5
6
7
8
9
10
var aQuery = function(selector, context) {
//构造函数
}
aQuery.prototype = {
//原型
name:function(){},
age:function(){}
}
var a = new aQuery();
a.name();

jQuery不是这样实现的,要实现,首先要把jQuery看成一个类,那么$()应该是返回类的实例才对,所以修改代码:

1
2
3
4
5
6
7
var aQuery = function(selector, context) {
return new aQuery();
}
aQuery.prototype = {
name:function(){},
age:function(){}
}

通过new aQuery(),虽然返回的是一个实例,但是也能看出很明显的问题,死循环。
解决如何返回一个正确的实例:那么就要想到实例this只跟原型有关系。
利用工厂方法来创建实例,把这个方法放到aQuery.prototype原型中

1
2
3
4
5
6
7
8
9
10
var aQuery = function(selector, context) {
return aQuery.prototype.init(selector);
}
aQuery.prototype = {
init:function(selector){
return this;
}
name:function(){},
age:function(){}
}

很明显aQuery()返回的是aQuery类的实例,那么在init中的this其实也是指向的aQuery类的实例,问题来了init的this指向的是aQuery类,如果把init函数也当作一个构造器,那么内部的this要如何处理?

1
2
3
4
5
6
7
8
9
10
11
12
var aQuery = function(selector, context) {
return aQuery.prototype.init(selector);
}
aQuery.prototype = {
init: function(selector) {
this.age = 18
return this;
},
name: function() {},
age: 20
}
aQuery().age //18

因为this只是指向aQuery类的,所以aQuery的age属性是可以被修改的。
这样看似没有问题,其实问题很大的。
为什么是new jQuery.fn.init?
看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var aQuery = function(selector, context) {
return aQuery.prototype.init(selector);
}
aQuery.prototype = {
init: function(selector) {
if(selector=="a")
this.age = 18
return this;
},
name: function() {},
age: 20
}
aQuery("a").age //18
aQuery("b").age //18

当我调用传入”a” 的时候,修改age=18,及aQuery(“a”).age的值为18
但是当我传入”b”的时候并没有修改age的值,我也希望得到默认age的值20,但是aQuery(“b”).age的值为18.
因为在调用aQuery(“a”).age的时候age被修改了。
这样的情况下就出错了,所以需要设计出独立的作用域才行。

jQuery框架分隔作用域的处理

1
2
3
4
jQuery = function( selector, context ) {
// The jQuery object is actually just the init constructor 'enhanced'
return new jQuery.fn.init( selector, context, rootjQuery );
},

很明显通过实例init函数,每次都构建新的init实例对象,来分隔this,避免交互混淆。
再次修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var aQuery = function(selector, context) {
return new aQuery.prototype.init(selector);
}
aQuery.prototype = {
init: function(selector) {
if(selector=="a")
this.age = 18
return this;
},
name: function() {},
age: 20
}
aQuery("a").age //18
aQuery("b").age //undefined
aQuery("a").name() //Uncaught TypeError: Object [object Object] has no method 'name'

又来一个问题
age:undefined,
name():抛出错误,无法找到这个方法,所以很明显new的init跟jQuery类的this分离了。
7、怎么访问jQuery类原型上的属性与方法
做到既能隔离作用域还能使用jQuery原型对象的作用域,还能在返回实例中访问jQuery的原型对象?

关键点

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

我们改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var aQuery = function(selector, context) {
return new aQuery.prototype.init(selector);
}
aQuery.prototype = {
init: function(selector) {
if(selector=="a")
this.age = 18
return this;
},
name: function() {
return age;
},
age: 20
}
aQuery.prototype.init.prototype = aQuery.prototype;

aQuery("a").age //18
aQuery("b").age //20
aQuery("a").name() //20

我们再看一遍jQuery源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function(window, undefined) {
var
// ...
  jQuery = function(selector, context) {
// The jQuery object is actually just the init constructor 'enhanced'
return new jQuery.fn.init(selector, context, rootjQuery);
},
jQuery.fn = jQuery.prototype = {
init: function(selector, context, rootjQuery) {
// ...
}
}
jQuery.fn.init.prototype = jQuery.fn;
})(window);

发现一切都是套路,哈哈。

说一下jQuery的几个绝妙之处:

1
jQuery.fn.init.prototype = jQuery.fn

解析一下:

1、首先要明确,使用$(‘xxx’) 这种实例化方式,其内部调用的是

1
return new jQuery.fn.init(selector, context, rootjQuery)

这一句话,也就是构造实例时交给了

1
jQuery.fn.init()

方法去完成。
2、将jQuery.fn.init的prototype属性设置为jQuery.fn,那么使用new jQuery.fn.init()生成的对象的原型对象就是jQuery.fn,所以挂载到jQuery.fn上面的函数就相当于挂载到jQuery.fn.init()生成的jQuery对象上,所有使用new jQuery.fn.init()生成的对象也能访问到jQuery.fn上的所哟原型方法。
3、所以实例化方法存在这么一个关系链
    1.jQuery.fn.init.prototype=jQuery.fn=jQuery.prototype;
     2.new jQuery.fn.init()相当于new jQuery();
     3.jQuery()返回的是new jQuery.fn.init(),而var obj=new jQuery(),所以这2者是相当的,所以我们可以无new 实例化jQuery对象。

热评文章