jQuery 的 setter/getter 共用一个函数,通过是否传参来表明它是何种意义。简单说传参它是 setter,不传它是 getter。
一个函数具有多种意义在编程语言中并不罕见,比如函数重载:一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。重载的好处是减少了函数名的数量,避免了名字空间的污染,对于程序的可读性也大有裨益。
函数重载主要体现的两个方面,一是参数的类型、相同个数的参数类型不同可称为函数重载;二是参数的个数,个数不同也称为函数重载。注意,重载与函数的返回值并无关系。
由于 JS 弱类型的特征,想模拟函数重载就只能通过第二种方式:参数的个数来实现。因此函数内的 arguments 对象就显得非常重要。
以下是一个示例
doAdd 通过判断函数的参数个数重载实现了三种意义,argsLength 为 0 时,直接返回 0; argsLength 为 1 时,该参数与 10 相加;argsLength 为 2 时两个参数相加。
利用函数重载特性可以实现 setter/getter
以上简单的解释了函数重载及利用它实现 setter/getter。即"取值器"与"赋值器"合一。到底是取值还是赋值,由函数的参数决定。jQuery 的很多 API 设计大量使用了这种模式。
下图汇总了 jQuery 中采用这种模式的所有 API,共 14 个函数
所有这些函数内部都依赖另一个函数 access, 毫不夸张的说 access 是所有这些函数的核心,是实现 setter/getter 的核心。下面是这个函数的源码,它是一个私有的函数,外部是调用不到它的。
access 的源码如下
该函数的注释提到:这是一个多功能的函数,用来获取和设置一个集合元素的属性和值。value 可以是一个可执行的函数。这个函数一共不到 60 行代码。从上往下读,第一个 if 是设置多个 value 值,是一个递归调用。刨去这个递归调用,设置单个值的代码也就不到 50 行了。写的非常简练、耐读。
为了理解 access 函数,我画了两个图
access 内部两个主要分支
access 内部的执行流程
access 定义的形参有 7 个
1.elems 元素集合,实际调用时传的都是 this,这里的 this 是 jQuery 对象,我们知道 jQuery 对象本身是一个集合,具有 length 属性和索引。必传。
2.fn 实现 setter/getter 的函数,就是说这个函数里需要有条件能判断哪部分是 setter,哪部分是 getter。必传。
3.key 比如 attr 和 prop 方法要传,设置或获取哪个 key 的值。有的则不用传,但为了占位用以 null 替代,比如 text、html 方法。可选。
4.value 仅当 setter 时要传,即 value 为 undefined 时是 getter,否则是 setter。可选。
5.chainable 当为 true 时,进入 setter 模式,会返回 jQuery 对象。false 则进入 getter模式。调用时通过 arguments.length 或 arguments.length>1 传入。
6.emptyGet 当 jQuery 对象为空时,返回的结果,默认不传为 undefined,data 方法调用时传的是 null。
7.raw 当 value 为函数类型时 raw 为 false,否则为 true。
上面提到了 access 是 jQuery 所有 setter/getter 函数的核心,换句话说所有 14 个函数 setter/getter 函数内部都会调用 access。这也是为什么 access 有 7 个参数,里面分支众多。因为它要处理的各种条件就很多呢。但所有这些 setter/getter 有很多类同的代码,最后还是提取一个公共函数。
为了便于理解,我把 access 的调用分类以下,便于我们理解。
1. 调用 access 时,第三个参数 key 传值为 null,分别是 text/html 方法
图示这两个方法在 access 内部执行处
为什么 key 传 null,因为 DOM API 已经提供了。text 方法使用 el.innerText 设置或获取;html 方法使用 innerHTML 设置或获取(这里简单说,实际还有一些异常处理)。
2. 与第一种情况相反,调用 access 时 key 值传了且不为 null。除了 text/html 外的其它 setter 都是如此
1 );
},prop: function( name,jQuery.prop,// Create scrollLeft and scrollTop methods
jQuery.each( { scrollLeft: "pageXOffset",scrollTop: "pageYOffset" },function( method,prop ) {
var top = "pageYOffset" === prop;
jQuery.fn[ method ] = function( val ) {
return access( this,function( elem,method,val ) {
var win = getWindow( elem );
if ( val === undefined ) {
return win ? win[ prop ] : elem[ method ];
}
if ( win ) {
win.scrollTo(
!top ? val : win.pageXOffset,top ? val : win.pageYOffset
);
} else {
elem[ method ] = val;
}
},val,arguments.length );
};
} );
css: function( name,value ) {
var styles,len,map = {},i = 0;
if ( jQuery.isArray( name ) ) {
styles = getStyles( elem );
len = name.length;
for ( ; i < len; i++ ) {
map[ name[ i ] ] = jQuery.css( elem,name[ i ],false,styles );
}
return map;
}
return value !== undefined ?
jQuery.style( elem,value ) :
jQuery.css( elem,name );
},arguments.length > 1 );
}
// Create innerHeight,innerWidth,height,width,outerHeight and outerWidth methods
jQuery.each( { Height: "height",Width: "width" },function( name,type ) {
jQuery.each( { padding: "inner" + name,content: type,"": "outer" + name },function( defaultExtra,funcName ) {
// Margin is only for outerHeight,outerWidth
jQuery.fn[ funcName ] = function( margin,value ) {
var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
return access( this,type,value ) {
var doc;
if ( jQuery.isWindow( elem ) ) {
// $( window ).outerWidth/Height return w/h including scrollbars (gh-1729)
return funcName.indexOf( "outer" ) === 0 ?
elem[ "inner" + name ] :
elem.document.documentElement[ "client" + name ];
}
// Get document width or height
if ( elem.nodeType === 9 ) {
doc = elem.documentElement;
// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],// whichever is greatest
return Math.max(
elem.body[ "scroll" + name ],doc[ "scroll" + name ],elem.body[ "offset" + name ],doc[ "offset" + name ],doc[ "client" + name ]
);
}
return value === undefined ?
// Get width or height on the element,requesting but not forcing parseFloat
jQuery.css( elem,extra ) :
// Set width or height on the element
jQuery.style( elem,extra );
},chainable ? margin : undefined,chainable );
};
} );
} );
data: function( key,value ) {
var i,data,elem = this[ 0 ],attrs = elem && elem.attributes;
// Gets all values
if ( key === undefined ) {
if ( this.length ) {
data = dataUser.get( elem );
if ( elem.nodeType === 1 && !dataPriv.get( elem,"hasDataAttrs" ) ) {
i = attrs.length;
while ( i-- ) {
// Support: IE 11 only
// The attrs elements can be null (#14894)
if ( attrs[ i ] ) {
name = attrs[ i ].name;
if ( name.indexOf( "data-" ) === 0 ) {
name = jQuery.camelCase( name.slice( 5 ) );
dataAttr( elem,data[ name ] );
}
}
}
dataPriv.set( elem,"hasDataAttrs",true );
}
}
return data;
}
// Sets multiple values
if ( typeof key === "object" ) {
return this.each( function() {
dataUser.set( this,key );
} );
}
return access( this,function( value ) {
var data;
// The calling jQuery object (element matches) is not empty
// (and therefore has an element appears at this[ 0 ]) and the
// `value` parameter was not undefined. An empty jQuery object
// will result in `undefined` for elem = this[ 0 ] which will
// throw an exception if an attempt to read a data cache is made.
if ( elem && value === undefined ) {
// Attempt to get data from the cache
// The key will always be camelCased in Data
data = dataUser.get( elem,key );
if ( data !== undefined ) {
return data;
}
// Attempt to "discover" the data in
// HTML5 custom data-* attrs
data = dataAttr( elem,key );
if ( data !== undefined ) {
return data;
}
// We tried really hard,but the data doesn't exist.
return;
}
// Set the data...
this.each( function() {
// We always store the camelCased key
dataUser.set( this,value );
} );
},arguments.length > 1,true );
},
图示这些方法在 access 内部执行处