jQuery 3.0 的 setter和getter 模式詳解
jQuery 的 setter/getter 共用一個函數(shù),通過是否傳參來表明它是何種意義。簡單說傳參它是 setter,不傳它是 getter。
一個函數(shù)具有多種意義在編程語言中并不罕見,比如函數(shù)重載:一組具有相同函數(shù)名,不同參數(shù)列表的函數(shù),這組函數(shù)被稱為重載函數(shù)。重載的好處是減少了函數(shù)名的數(shù)量,避免了名字空間的污染,對于程序的可讀性也大有裨益。
函數(shù)重載主要體現(xiàn)的兩個方面,一是參數(shù)的類型、相同個數(shù)的參數(shù)類型不同可稱為函數(shù)重載;二是參數(shù)的個數(shù),個數(shù)不同也稱為函數(shù)重載。注意,重載與函數(shù)的返回值并無關(guān)系。
由于 JS 弱類型的特征,想模擬函數(shù)重載就只能通過第二種方式:參數(shù)的個數(shù)來實現(xiàn)。因此函數(shù)內(nèi)的 arguments 對象就顯得非常重要。
以下是一個示例
function doAdd() {
var argsLength = arguments.length
if (argsLength === 0) {
return 0
} else if (argsLength === 1) {
return arguments[0] + 10
} else if (argsLength === 2) {
return arguments[0] + arguments[1]
}
}
doAdd() // 0
doAdd(5) // 15
doAdd(5, 20) // 25
doAdd 通過判斷函數(shù)的參數(shù)個數(shù)重載實現(xiàn)了三種意義,argsLength 為 0 時,直接返回 0; argsLength 為 1 時,該參數(shù)與 10 相加;argsLength 為 2 時兩個參數(shù)相加。
利用函數(shù)重載特性可以實現(xiàn) setter/getter
function text() {
var elem = this.elem
var argsLength = arguments.length
if (argsLength === 0) {
return elem.innerText
} else if (argsLength === 1) {
elem.innerText = arguments[0]
}
}
以上簡單的解釋了函數(shù)重載及利用它實現(xiàn) setter/getter。即"取值器"與"賦值器"合一。到底是取值還是賦值,由函數(shù)的參數(shù)決定。jQuery 的很多 API 設(shè)計大量使用了這種模式。
下圖匯總了 jQuery 中采用這種模式的所有 API,共 14 個函數(shù)

所有這些函數(shù)內(nèi)部都依賴另一個函數(shù) access, 毫不夸張的說 access 是所有這些函數(shù)的核心,是實現(xiàn) setter/getter 的核心。下面是這個函數(shù)的源碼,它是一個私有的函數(shù),外部是調(diào)用不到它的。

access 的源碼如下
// Multifunctional method to get and set values of a collection
// The value/s can optionally be executed if it's a function
var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
var i = 0,
len = elems.length,
bulk = key == null;
// Sets many values
if ( jQuery.type( key ) === "object" ) {
chainable = true;
for ( i in key ) {
access( elems, fn, i, key[ i ], true, emptyGet, raw );
}
// Sets one value
} else if ( value !== undefined ) {
chainable = true;
if ( !jQuery.isFunction( value ) ) {
raw = true;
}
if ( bulk ) {
// Bulk operations run against the entire set
if ( raw ) {
fn.call( elems, value );
fn = null;
// ...except when executing function values
} else {
bulk = fn;
fn = function( elem, key, value ) {
return bulk.call( jQuery( elem ), value );
};
}
}
if ( fn ) {
for ( ; i < len; i++ ) {
fn(
elems[ i ], key, raw ?
value :
value.call( elems[ i ], i, fn( elems[ i ], key ) )
);
}
}
}
return chainable ?
elems :
// Gets
bulk ?
fn.call( elems ) :
len ? fn( elems[ 0 ], key ) : emptyGet;
};
該函數(shù)的注釋提到:這是一個多功能的函數(shù),用來獲取和設(shè)置一個集合元素的屬性和值。value 可以是一個可執(zhí)行的函數(shù)。這個函數(shù)一共不到 60 行代碼。從上往下讀,第一個 if 是設(shè)置多個 value 值,是一個遞歸調(diào)用。刨去這個遞歸調(diào)用,設(shè)置單個值的代碼也就不到 50 行了。寫的非常簡練、耐讀。
為了理解 access 函數(shù),我畫了兩個圖
access 內(nèi)部兩個主要分支

access 內(nèi)部的執(zhí)行流程

access 定義的形參有 7 個
1.elems 元素集合,實際調(diào)用時傳的都是 this,這里的 this 是 jQuery 對象,我們知道 jQuery 對象本身是一個集合,具有 length 屬性和索引。必傳。
2.fn 實現(xiàn) setter/getter 的函數(shù),就是說這個函數(shù)里需要有條件能判斷哪部分是 setter,哪部分是 getter。必傳。
3.key 比如 attr 和 prop 方法要傳,設(shè)置或獲取哪個 key 的值。有的則不用傳,但為了占位用以 null 替代,比如 text、html 方法??蛇x。
4.value 僅當(dāng) setter 時要傳,即 value 為 undefined 時是 getter,否則是 setter。可選。
5.chainable 當(dāng)為 true 時,進(jìn)入 setter 模式,會返回 jQuery 對象。false 則進(jìn)入 getter模式。調(diào)用時通過 arguments.length 或 arguments.length>1 傳入。
6.emptyGet 當(dāng) jQuery 對象為空時,返回的結(jié)果,默認(rèn)不傳為 undefined,data 方法調(diào)用時傳的是 null。
7.raw 當(dāng) value 為函數(shù)類型時 raw 為 false,否則為 true。
上面提到了 access 是 jQuery 所有 setter/getter 函數(shù)的核心,換句話說所有 14 個函數(shù) setter/getter 函數(shù)內(nèi)部都會調(diào)用 access。這也是為什么 access 有 7 個參數(shù),里面分支眾多。因為它要處理的各種條件就很多呢。但所有這些 setter/getter 有很多類同的代碼,最后還是提取一個公共函數(shù)。
為了便于理解,我把 access 的調(diào)用分類以下,便于我們理解。
1. 調(diào)用 access 時,第三個參數(shù) key 傳值為 null,分別是 text/html 方法
text: function( value ) {
return access( this, function( value ) {
return value === undefined ?
jQuery.text( this ) :
this.empty().each( function() {
if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
this.textContent = value;
}
} );
}, null, value, arguments.length );
},
html: function( value ) {
return access( this, function( value ) {
var elem = this[ 0 ] || {},
i = 0,
l = this.length;
if ( value === undefined && elem.nodeType === 1 ) {
return elem.innerHTML;
}
// See if we can take a shortcut and just use innerHTML
if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
!wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) {
value = jQuery.htmlPrefilter( value );
try {
for ( ; i < l; i++ ) {
elem = this[ i ] || {};
// Remove element nodes and prevent memory leaks
if ( elem.nodeType === 1 ) {
jQuery.cleanData( getAll( elem, false ) );
elem.innerHTML = value;
}
}
elem = 0;
// If using innerHTML throws an exception, use the fallback method
} catch ( e ) {}
}
if ( elem ) {
this.empty().append( value );
}
}, null, value, arguments.length );
},
圖示這兩個方法在 access 內(nèi)部執(zhí)行處

為什么 key 傳 null,因為 DOM API 已經(jīng)提供了。text 方法使用 el.innerText 設(shè)置或獲取;html 方法使用 innerHTML 設(shè)置或獲?。ㄟ@里簡單說,實際還有一些異常處理)。
2. 與第一種情況相反,調(diào)用 access 時 key 值傳了且不為 null。除了 text/html 外的其它 setter 都是如此
attr: function( name, value ) {
return access( this, jQuery.attr, name, value, arguments.length > 1 );
},
prop: function( name, value ) {
return access( this, jQuery.prop, name, value, arguments.length > 1 );
},
// 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;
}
}, method, val, arguments.length );
};
} );
css: function( name, value ) {
return access( this, function( elem, 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, name, value ) :
jQuery.css( elem, name );
}, name, value, 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, function( elem, 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, type, extra ) :
// Set width or height on the element
jQuery.style( elem, type, value, extra );
}, type, chainable ? margin : undefined, chainable );
};
} );
} );
data: function( key, value ) {
var i, name, 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, name, 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, key, value );
} );
}, null, value, arguments.length > 1, null, true );
},
圖示這些方法在 access 內(nèi)部執(zhí)行處

- 關(guān)于__defineGetter__ 和__defineSetter__的說明
- 實現(xiàn)了一個PHP5的getter/setter基類的代碼
- JavaScript之Getters和Setters 平臺支持等詳細(xì)介紹
- javascript中的__defineGetter__和__defineSetter__介紹
- ECMAScript5中的對象存取器屬性:getter和setter介紹
- JavaScript中setter和getter方法介紹
- 使用node+vue.js實現(xiàn)SPA應(yīng)用
- 基于Vue.js的表格分頁組件
- Vue.js每天必學(xué)之組件與組件間的通信
- 談?wù)勔騐ue.js引發(fā)關(guān)于getter和setter的思考
相關(guān)文章
Ionic實現(xiàn)仿通訊錄點擊滑動及$ionicscrolldelegate使用分析
這篇文章主要介紹了Ionic實現(xiàn)仿通訊錄點擊滑動及$ionicscrolldelegate使用分析的相關(guān)資料,需要的朋友可以參考下2016-01-01
jQuery Easyui使用(二)之可折疊面板動態(tài)加載無效果的解決方法
這篇文章主要介紹了jQuery Easyui使用之可折疊面板動態(tài)加載無效果的解決方案,非常不錯,具有參考借鑒價值,感興趣的朋友一起看下吧2016-08-08
jquery1.5.1中根據(jù)元素ID獲取元素對象的代碼
盡管聽說jquery的大名幾年了,但是一直沒有使用過。這兩天想在項目中使用被一些小細(xì)節(jié)折騰的夠嗆,看來jquery沒有傳說中的那么好學(xué)。2011-04-04
用原生JavaScript實現(xiàn)jQuery的$.getJSON的解決方法
本篇文章介紹了,用原生JavaScript實現(xiàn)jQuery的$.getJSON的解決方法。需要的朋友參考下2013-05-05
jQuery EasyUI Accordion可伸縮面板組件使用詳解
這篇文章主要為大家詳細(xì)介紹了jQuery EasyUI Accordion可伸縮面板組件的使用方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-02-02

