jQuery 源码学习之路二(prototype 与 jq源码设计)

源码中应用 prototype 原型来做处理。那么用 prototype 处理的好处是什么呢?
首先举个例子:
用构造函数生成实例对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function House(name){
this.name = name;
this.door = "防盗门"; //此属性为实例对象的共有属性
this.getRoomPhoto = function(){
console.log("房照");
}
}
//生成两个实例对象,看看。
var homeownerA = new House("小明");
var homeownerB = new House("小粒");
console.log(homeownerA === homeownerB); //false 两个实例对象不相等,所占内存地址不同
homeownerA.door = "玻璃门"; //更改homeownerA的door属性
console.log(homeownerA.door); // 玻璃门
console.log(homeownerB.door); // 防盗门
console.log(homeownerA.getRoomPhoto === homeownerB.getRoomPhoto); //false

上面例子说明了,每个实例对象都有自己的方法和属性的副本,这样就无法做到数据共享,而且也很浪费资源。这样操作多了,对性能上也是有一定的影响。javascript中的每个对象都有prototype属性,Javascript中对象的prototype属性的解释是:返回对象类型原型的引用。每一个构造函数都有一个属性叫做原型。这个属性非常有用:为一个特定类声明通用的变量或者函数。prototype 就像一把钥匙,只要拥有这把钥匙,你就可以享有 构造函数 这个房子里的一切。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function House(name){
this.name = name;
}
House.prototype = {
door:"防盗门",
getRoomPhoto : function(){
console.log("房照");
}
}
//生成两个实例对象,看看。
var homeownerA = new House("小明");
var homeownerB = new House("小粒");
console.log(homeownerA === homeownerB); //false 两个实例对象不相等,所占内存地址不同
House.prototype.door = "玻璃门"; //更改House原型的door属性,同时影响了两个实例对象
console.log(homeownerA.door); // 玻璃门
console.log(homeownerB.door); // 玻璃门
console.log(homeownerA.getRoomPhoto === homeownerB.getRoomPhoto); //true 共享方法,所指向内存地址什么的都相同

"prototype"

下面这一小段代码就说明了先会在自身实例中查找一个属性或方法。

1
2
3
4
//接着上面的例子
homeownerA.door = "水晶门";
console.log(homeownerA.door); // 水晶门
console.log(homeownerB.door); // 玻璃门

这里面有一个注意点,就是你更改一个实例对象的属性时,并不会影响到原型。
源码中,下面的代码可谓是jQuery的整个架构了。(这是我从jquery源码中抽离出来的)

1
2
3
4
5
6
7
8
var 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.fn = jQuery.prototype = { //xxx; }
init = jQuery.fn.init = function( selector, context, root ) { //xxx; }
init.prototype = jQuery.fn;

"prototype"
1.jq 无 new 构建,就是因为 jq 为我们封装了 new。

1
2
3
var jQuery = function( selector, context ) {
return new jQuery.fn.init( selector, context );
}

每次调用$()方法等于是创建了一个新的实例,所以说为此为了减少多余空间,以后最好用var a = $();的方式让下文使用提供方便,避免多余的封装和空间的浪费。

  1. $(“xxx”) 这种实例化方式,其内部调用的是 “return new jQuery.fn.init( selector, context );” ,就是构造实例交给 jQuery.fn.init 方法去处理。
  2. init = jQuery.fn.init = function( selector, context, root ) {} ,jQuery.fn.init 的方法就与 jQuery.init方法像等同,
  3. jQuery.init.prototype 指向 jQuery.fn,然后 jQuery.fn 最终指向 jQuery.prototype。所有jq对象都有jQuery.fn的属性和方法。

到这里虽然弄明白了 jQuery 源码的逻辑。这时,俺就又不明白了,为个啥子非要弄得这么麻烦,直接像下面这种写法不就行了么。

1
2
3
4
5
var $$ = yjQuery = function(selector) {
this.selector = selector;
return new yjQuery(selector);
}
console.log($$("yh")); //报错 Maximum call stack size exceeded

看到这个结果,呵呵哒了吧,都像你想的那么简单,你早就是大神了。看没,无限的递归自己,所以造成死循环并溢出了。所以人家 jQuery采取的手段是把原型上的一个init方法作为构造器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var $$ = yjQuery = function(selector) {
//把原型上的init作为构造器
//通过实例init函数,每次都构建新的init实例对象,来分隔this,使new的init跟jquery类的this分离
return new yjQuery.fn.init( selector );
}
yjQuery.fn = yjQuery.prototype = {
name: 'aaron',
init: function(selector) {
this.selector = selector;
console.log(this);
console.log(this.constructor.name); //init
},
constructor: yjQuery
}
console.log($$("yh")); // this 指向 init 而不是 yjQuery。

在上面的代码中再加上这句话:

1
2
yjQuery.fn.init.prototype = yjQuery.fn;
// console.log(this.constructor.name); // this 指向 yjQuery

通过原型传递解决问题,把jQuery的原型传递给jQuery.prototype.init.prototype,换句话说jQuery的原型对象覆盖了init构造器的原型对象。

对上面的代码还有一点需要说明一下,为什么要写 constructor: yjQuery 这句话:用 obj.prototype 去写一个东西的时候,constructor 的指向就不是构造函数,而是object ,所以这里要手动的把 constructor 设置为原构造函数。

还有在jQuery里面,你既可以用$.isArray()调用jq的静态方法,也可以通过$().css()获取对象执行实例方法。慕课网jq源码解析中也有解释说是静态与实例方法共享设计。

1
2
3
4
5
6
7
var j = function () {};
j.func = function () {console.log("静态方法");};
j.prototype.t = function () {console.log("实例方法");};
var t = new j();
t.t();
j.func();

所谓静态方法是jQuery本身得公共方法,并不需要通过实例化来调用,一般也称为工具方法。反之,实例化方法就是需要实例化调用。来段代码瞧一瞧,看一看啦!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function () {
var yh = function(){
return new yh.prototype.init();
};
yh.func = function () {
console.log("静态方法");
};
yh.prototype = {
init:function () {},
t:function () {
console.log("实例方法");
}
};
yh.prototype.init.prototype = yh.prototype;
window.yh = yh;
})();
var t =yh();
t.t(); //实例方法
yh.func(); //静态方法
//静态方法不能被实例对象调用
//若直接用实例对象 t 去调用 func
t.func(); //t.func is not a function

再说一下jq架构中的源码

1
2
3
4
5
6
7
8
var 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.fn = jQuery.prototype = { //静态方法; }
init = jQuery.fn.init = function( selector, context, root ) { //实例方法; }
init.prototype = jQuery.fn; // 点睛之笔,把jQuery.prototype原型的引用赋给jQuery.fn.init.prototype的原型

jQuery的无new构建原理中在$()内部中首先保证是通过new创建,使得我们在函数调用中可以使用this来代表该jq实例,通过原型的共享而实现了静态方法与实例方法的共存。这样 this 的指向一直指的是 jQuery 的对象。

——————————————-华丽的分界线————————————————————–
加个分界线其实是有一点要补充一下:
通过prototype 或是 this 指针来绑定的实例方法,直接定义在实例上的变量的优先级要高于定义在“this”上的,而定义在“this”上的又高于 prototype定义的变量。即直接定义在实例上的变量会覆盖定义在“this”上和prototype定义的变量,定义在“this”上的会覆盖prototype定义的变量。

1
2
3
4
5
6
7
8
9
var Zy = function(){
this.userName = "yh"; //定义在“this”上的变量 优先级第二
}
Zy.prototype.userName = "zy"; //定义在原型上的变量 优先级最后
var zy = new Zy();
zy.userName = "yz"; //直接定义在实例上的变量 优先级最高
console.log(zy.userName);
console.log(zy.userName);

可以把上面的代码copy一下,直接进行测试。

总结:
通过new jQuery.fn.init() 构建一个新的对象,拥有init构造器的prototype原型对象的方法
通过改变prorotype指针的指向,让这个新的对象也指向了jQuery类的原型prototype
所以这样构建出来的对象就继续了jQuery.fn原型定义的所有方法了

2017-11-13