jQuery 的 setter/getter 共用一個函數,經過是否傳參來代表它是何種意義。簡單說傳參它是 setter,不傳它是 getter。javascript
一個函數具備多種意義在編程語言中並不罕見,好比函數重載:一組具備相同函數名,不一樣參數列表的函數,這組函數被稱爲重載函數。重載的好處是減小了函數名的數量,避免了名字空間的污染,對於程序的可讀性也大有裨益。css
函數重載主要體現的兩個方面,一是參數的類型、相同個數的參數類型不一樣可稱爲函數重載;二是參數的個數,個數不一樣也稱爲函數重載。注意,重載與函數的返回值並沒有關係。html
因爲 JS 弱類型的特徵,想模擬函數重載就只能經過第二種方式:參數的個數來實現。所以函數內的 arguments 對象就顯得很是重要。java
如下是一個示例node
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 經過判斷函數的參數個數重載實現了三種意義,argsLength 爲 0 時,直接返回 0; argsLength 爲 1 時,該參數與 10 相加;argsLength 爲 2 時兩個參數相加。編程
利用函數重載特性能夠實現 setter/getterapp
function text() { var elem = this.elem var argsLength = arguments.length if (argsLength === 0) { return elem.innerText } else if (argsLength === 1) { elem.innerText = arguments[0] } }
以上簡單的解釋了函數重載及利用它實現 setter/getter。即"取值器"與"賦值器"合一。究竟是取值仍是賦值,由函數的參數決定。jQuery 的不少 API 設計大量使用了這種模式。編程語言
下圖彙總了 jQuery 中採用這種模式的全部 API,共 14 個函數函數
全部這些函數內部都依賴另外一個函數 access, 絕不誇張的說 access 是全部這些函數的核心,是實現 setter/getter 的核心。下面是這個函數的源碼,它是一個私有的函數,外部是調用不到它的。this
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; };
該函數的註釋提到:這是一個多功能的函數,用來獲取和設置一個集合元素的屬性和值。value 能夠是一個可執行的函數。這個函數一共不到 60 行代碼。從上往下讀,第一個 if 是設置多個 value 值,是一個遞歸調用。刨去這個遞歸調用,設置單個值的代碼也就不到 50 行了。寫的很是簡練、耐讀。
爲了理解 access 函數,我畫了兩個圖
access 內部兩個主要分支
access 內部的執行流程
access 定義的形參有 7 個
上面提到了 access 是 jQuery 全部 setter/getter 函數的核心,換句話說全部 14 個函數 setter/getter 函數內部都會調用 access。這也是爲何 access 有 7 個參數,裏面分支衆多。由於它要處理的各類條件就不少呢。但全部這些 setter/getter 有不少類同的代碼,最後仍是提取一個公共函數。
爲了便於理解,我把 access 的調用分類如下,便於咱們理解。
1. 調用 access 時,第三個參數 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 內部執行處
爲何 key 傳 null,由於 DOM API 已經提供了。text 方法使用 el.innerText 設置或獲取;html 方法使用 innerHTML 設置或獲取(這裏簡單說,實際還有一些異常處理)。
2. 與第一種狀況相反,調用 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 內部執行處
各個版本的實現差別
1.1 ~ 1.3 各個 setter/getter 獨自實現,沒有抽取一個公共函數。1.4 ~ 1.9 抽取了獨立的 jQuery.access 這個核心函數爲全部的 setter/getter 服務。1.10 ~ 2.24 同上一個版本區間,但在內部使用了一個私有的 access 函數,不使用公開的 jQuery.access,即弱化了 jQuery.access。3.0 ~ 將來 去掉了 jQuery.access ,內部直接使用私有的 access 。