jQuery源码学习笔记系列(二)

时间:2022-12-02 20:56:28

在上一篇笔记中主要学习了作用域链的一些情况,而在这一篇中,将会注意到js中另一个重要的知识点(this),另外,推荐一本书,才发现的,《你不知道的javaScript》,这本书主要是有针对性的去探讨js中一些比较关键的东西。比如,this。


function( window, noGlobal ) {
"use strict";
var arr = [];
var document = window.document;
var getProto = Object.getPrototypeOf;
var slice = arr.slice;
var concat = arr.concat;
var push = arr.push;
var indexOf = arr.indexOf;
var class2type = {};
var toString = class2type.toString;
var hasOwn = class2type.hasOwnProperty;
var fnToString = hasOwn.toString;
var ObjectFunctionString = fnToString.call( Object );
var support = {};
function DOMEval( code, doc ) {
doc = doc || document;
var script = doc.createElement( "script" );
script.text = code;
doc.head.appendChild( script ).parentNode.removeChild( script );
}

首先这是之前提到过的匿名函数的第二个实参,一个函数,用于传递给之前的factory的形参,而factory在存在window变量的情况下,将会执行factory(global),而这个global看第一个实参表达式可以知道是this或者是window(当然,这里会有差别,暂时不谈),先撇开这,往下。

首先是之前忽略的严格模式,这里必须要谈了,严格模式和非严格模式有很大不同,比如我们在控制台下默认就是非严格模式,有如下例子,

function A(){console.log(this);
A()//window
function B(){"use strict" ; console.log(this)}
B()//undefined

这里仅选取需要的例子,也就是,严格模式下this将会不同,或者这样说不够贴切,首先,应该是这样的:

什么是this

我最开始碰见this是在cpp中,用于在类中省略一个用于指向调用这个成员函数或正在被构造的对象的指针,也就是说,this指针是用于方便在执行时调用的上下文的(对象而不是类,所以是执行,上下文(context),这个理解可能有偏差,我的理解就是函数执行必然依靠一个环境,多个大大小小的环境,就是上下文(context),如有不对,请指出,避免误导),而js学习了cpp很多东西(用cpp写成),也学习到了它的this,用于方便调用上下文的一个东西,所以,this指向谁,首先要看谁调用函数。

如何调用函数

在书中说,调用函数有四种方法(有不符合规定的情况不作讨论):

1.作为函数
2.作为方法
3.作为构造函数
4.通过它们的call()或apply()方法间接调用

初看时我是很疑惑的,函数调用不就是A()这样吗,其它几种是什么个搞法嘛,后面看过一些讨论解析才有点头绪,这里所谓的函数调用的四种方法或许称为函数执行的四种情况,也就是,谁在调用函数让其执行。

作为函数与作为方法

首先使用函数调用表达式即可用作普通的函数调用也可以用作方法调用,也就是A(),而方法表达式与其区别就是多了一个对象爸爸,o.m=f;o.m(),这个样,所以,上面代码中有一系列如push=arr.push这样的形式,看起来也是搞了一个函数别名,简化操作,比如var a={b:3,c:function(){console.log(this.b)}} ;var k=a.c;k(),但是就像这个例子一样,k()不会有正确结果,而push(3,4),也不能使arr为[3,4],因为(push经常被举例各种问题,这里就上push的源码):

function ArrayPush() {
var n = ToUint32(this.length);
var m = %_ArgumentsLength();
for (var i = 0; i < m; i++) {
this[i+n] = %_Arguments(i);
}
this.length = n + m;
return this.length;
}

InstallFunctions($Array.prototype, DONT_ENUM,$Array(
“toString”, ArrayToString,
“toLocaleString”, ArrayToLocaleString,
“join”, ArrayJoin,
“pop”, ArrayPop,
“push”, ArrayPush,
))

从代码可以看出,push的操作首先要衡量一下参数的个数(%_ArgumentsLength();),然后就简单的this[i+n]这样的为其赋值,所以经常被用来作爆内存的示例,因为push看起来就是这样的–不能有效的控制其使用,但有这样的一个有意思代码:

var obj={};
obj.m = push// push = arr.push
obj.m(3)
obj //{0:3,length:1,m:f}

this.length =n+m这样对length为1很好理解, 但是,var n = ToUnit32(this.length);,似乎表明了this.length—也就是undefined,被ToUnit32转化为了0,首先,ToUnit32肯定是cpp函数了,先不去看这个里面具体,用别的方式去看看:

obj.length = undefined
obj.m(5)
obj // {0: 5, length: 1, m: ƒ}

首先undefined不是关键字,而是一个全局变量,用于表示“空值”,类似的关键字null也表示空值,但是,从cpp来看,空指针并不是不存在,而是这个指针指向了一个无法存储值(或者说特别定义其无法存储值),而undefined就是为了表示不存在而设立的,可能这一点从cpp角度来说有些疑惑,既然不存在,又为何又去需要确认其存在不存在?我个人认为很大程度上是历史遗留问题(说不清,但从一个动态语言角度来说,不存在是有道理的,不过我个人更倾向就是因为开始制定时的设计问题),
而将obj.length=NaN或者为null,都可以得到类似的结果,这一点看起来很奇怪,但实际上在我们自己写代码中,就会不经意间犯下类似的错误。
话题再回到函数调用,所谓作为函数调用或者方法调用,实际上就是一个上下文问题,如果是存在一个环境(比如在全局中调用,或者某个函数中),那么this指向全局,注意,可以看下面的例子:

function A(){console.log(this);
function B(){A()};
A();//window
B();//window

这种被称作默认绑定,看着有些疑惑,为何B调用的A,却不指向B,所以类似会有下面的疑惑:

var a = 3;
function D(){var a = 5; console.log(this.a)}
D()//3 ,如何得到函数里面的5?
function C(){var a=5;console.log(a)}
C()//5,看起了很奇怪

这个时候话题又转到了作用域链,在函数定义时产生的作用域链用于保存局部变量,在函数执行时从里往外查找,所以直接用a就可以访问到函数里面的a,但是为何this.a不能这样指向当前调用者?这里给出我的一点想法(不准确,仅供提供一个想法,欢迎讨论:js中函数更像是cpp中的内联函数,也就是仅仅是嵌入代码而已,而这样的嵌入,虽然会形成一个新的作用域,但其所依赖的执行环境还是其最外层调用者,因为内部可以视作并无其它函数,而仅仅只是一行行代码,所以this才有这样诡异的表现,这在cpp中调用其它函数时的this表现其实是一致的,(比较混乱,因为我自己也不能理清里面具体的东西)。
而作为方法调用,如:

var obj ={a:3;}
function A(){console.log(this.a)}
obj.m = A
obj.m()//3

这样的方式称作隐式绑定,也就是,在函数作为一个对象的方法时,函数的this实际指向该对象,(默认其实类似,可以认为是this.C(),也就是实际是window对象调用了这个函数),作为对比,给个下面例子 :

var obj = {a:3,b:{}}
obj.b.a=5
function C(){console.log(this.a)}
obj.b.m=C
obj.b.m()//5

这样的结果看起来比较舒服,也从侧面说明了,函数是对象的依附,有对象才能执行js中那些“无家可归”的函数,而隐式绑定优先级是比默认高的,事实上,默认绑定只在无法适用其它规则时才有效。

使用call()或者apply()调用

代码var ObjectFunctionString = fnToString.call( Object );
在js中,绝大多数函数都有call和apply方法,用于显式的指定this的绑定,也就是显式绑定,比如:

funcion A(){consolo.log(this.a)}
var obj = {a:4}
A.call(obj)//4

这样在某种程度上就是给函数找个家住,但又没有成为这个家的成员,顶多算个旅客,所以这个时候this明确的被指定为执行obj。
关于显式绑定用的地方很多,很多js的应用技巧都会用到这,在后面源码中有的地方将会具体举例子

作为构造函数调用

js中有new关键字,但是和cpp中不同,js中new实际只是调用函数的一种方式,而不是所谓的初始化,new的很大作用,就是改变this,这样的绑定被特称为new绑定,而new调用函数时,会有以下步骤:
1.创建或者说构建一个全新的对象
2.这个新对象将会执行原型连接
3.这个新对象会绑定到这个函数调用的this
4.如果函数没有返回值,那么new会返回这个新对象
而new调用实际上会涉及到js中类与原型相关概念,在后面作出具体解释。

函数调用的四种方式,对应了四种this的绑定,而四种绑定是有优先级的,一般是显式>隐式>默认,构造情况比较特殊,在这里不作比较。

再接下来,看源码

function DOMEval( code, doc ) {
doc = doc || document;
var script = doc.createElement( "script" );
script.text = code;
doc.head.appendChild( script ).parentNode.removeChild( script );
}

先看这个操作,doc = doc||document。这是js中对默认参数的做法,当然,这种做法缺点很多,所以在ES6中已经特地做出了默认参数,和cpp中很接近。
这段代码是第一次对dom树进行操作,其也给定了一般标准:
即createElment,然后注入内容,然后添加。比如:

var script = window.document.createElement("script")
script.text("alert('test')")
window.document.head.appendChild(script)//弹出警告框

这里需要注意,要注意返回值再进行这种链式操作
这个函数DOMEval,从名字可以看出,就是执行操作DOM的函数,使其作用一次,然后消除影响,eval()这个函数用于执行动态代码,比如eval("alert('test')",但一般不推荐使用这样的操作,这样会使得页面结构凌乱。

之后源码,重头戏:

var
version = "3.2.1",
// Define a local copy of jQuery
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=function (selector,context),这样是不是很熟悉?jQuery(“p”).html(),这样的,这就是其对外的接口,让我们仔细看里面的东西:
首先注释里面提到其是其是local copy of jQuery,这样有些疑惑,但下面再一个注释,the init constructor ’enhanced‘,这样看了就很明确,就是一个接口嘛,而这个接口,返回的就是一个 new jQuery.fn.init(selector,context),于是,话题留到下一篇,也就是,类以及原型