jquery.jsを読み解く

第3回jQueryライブラリ(470行目~769行目)

今回は、470行目~769行目までを解説します。

slice()

0470:  slice: function() {
0471:    return this.pushStack( Array.prototype.slice.apply( this, arguments ) );
0472:  },
0473:

sliceメソッドは、Arrayオブジェクトのsliceを実行しています。わざわざ別のメソッドを定義しているのは、pushStackを使って現在の状態をスタックに格納するためです。

map()

0474:  map: function( callback ) {
0475:    return this.pushStack( jQuery.map(this, function(elem, i){
0476:      return callback.call( elem, i, elem );
0477:    }));
0478:  },
0479:

mapメソッドは、配列の各要素に関数を適用します。Array.mapはJavaScript 1.6から実装されていますが、1175行目で独自に実装されています。ここでもスタックに格納しています。

andSelf()

0480:  andSelf: function() {
0481:    return this.add( this.prevObject );
0482:  },
0483:  

andSelfメソッドは、直前に選択されていた要素を現在選択されている要素に加えて返します。

domManip()

0484:  domManip: function( args, table, reverse, callback ) {
0485:    var clone = this.length > 1, elems; 
0486:
0487:    return this.each(function(){
0488:      if ( !elems ) {
0489:        elems = jQuery.clean( args, this.ownerDocument );
0490:
0491:        if ( reverse )
0492:          elems.reverse();
0493:      }
0494:
0495:      var obj = this;
0496:
0497:      if ( table && jQuery.nodeName( this, "table" ) && jQuery.nodeName( elems[0], "tr" ) )
0498:        obj = this.getElementsByTagName("tbody")[0] || this.appendChild( this.ownerDocument.createElement("tbody") );
0499:
0500:      var scripts = jQuery( [] );
0501:
0502:      jQuery.each(elems, function(){
0503:        var elem = clone ?
0504:          jQuery( this ).clone( true )[0] :
0505:          this;
0506:
0507:        // execute all scripts after the elements have been injected
0508:        if ( jQuery.nodeName( elem, "script" ) ) {
0509:          scripts = scripts.add( elem );
0510:        } else {
0511:          // Remove any inner scripts for later evaluation
0512:          if ( elem.nodeType == 1 )
0513:            scripts = scripts.add( jQuery( "script", elem ).remove() );
0514:
0515:          // Inject the elements into the document
0516:          callback.call( obj, elem );
0517:        }
0518:      });
0519:
0520:      scripts.each( evalScript );
0521:    });
0522:  }
0523:};
0524:

domManipメソッドは、第2回で紹介したappend(),prepend(),before()などのメソッドから利用される内部処理用の関数です。 まず、485行目では、選択されている要素が複数あるかどうかを判定しています。次に489行目で初回ループ時のみcleanメソッドを実行し、argsとして渡された処理対象のエレメントをelems変数に格納します。また、もしreverseが真ならばelemsの各要素を逆順にしておきます。

次に497行目ですが、table要素を操作する際にtbody要素がなければ生成して、obj変数に格納します。これは、Internet ExplorerでtbodyがないとうまくDOM操作が動かないための対処です。

次に500行目からは、要素に対してcallback関数を適用する部分なのですが、script要素が含まれていたら変数に待避しておきます。そして、最後に520行目でまとめてscriptの内容を評価します。

initプロトタイプ定義

0525:// Give the init function the jQuery prototype for later instantiation
0526:jQuery.prototype.init.prototype = jQuery.prototype;
0527:

後にインスタンス化するときのために、jQuery.prototype.initメソッドのプロトタイプにjQueryオブジェクトのプロトタイプを設定しています。これによって、ここまで説明してきたjQuery.prototypeに定義された様々なメソッドやプロパティを継承しています。

evalScript()

0528:function evalScript( i, elem ) {
0529:  if ( elem.src )
0530:    jQuery.ajax({
0531:      url: elem.src,
0532:      async: false,
0533:      dataType: "script"
0534:    });
0535:
0536:  else
0537:    jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" );
0538:
0539:  if ( elem.parentNode )
0540:    elem.parentNode.removeChild( elem );
0541:}
0542:

528行目は、evalScript関数の定義です。先ほどのdomManipメソッドの中で登場しましたが、DOM操作の際にscriptタグを後から実行するためのものです。529行目でsrc属性が指定されていればajaxメソッドを使って外部スクリプトを読み込み、実行します。そうでなければ、直接スクリプトを評価して実行します。

そして、親要素が存在するときは渡された要素を削除します。

extend()

0543:jQuery.extend = jQuery.fn.extend = function() {
0544:  // copy reference to target object
0545:  var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options;
0546:
0547:  // Handle a deep copy situation
0548:  if ( target.constructor == Boolean ) {
0549:    deep = target;
0550:    target = arguments[1] || {};
0551:    // skip the boolean and the target
0552:    i = 2;
0553:  }
0554:
0555:  // Handle case when target is a string or something (possible in deep copy)
0556:  if ( typeof target != "object" && typeof target != "function" )
0557:    target = {};
0558:
0559:  // extend jQuery itself if only one argument is passed
0560:  if ( length == 1 ) {
0561:    target = this;
0562:    i = 0;
0563:  }
0564:
0565:  for ( ; i 0566:    // Only deal with non-null/undefined values
0567:    if ( (options = arguments[ i ]) != null )
0568:      // Extend the base object
0569:      for ( var name in options ) {
0570:        // Prevent never-ending loop
0571:        if ( target === options[ name ] )
0572:          continue;
0573:
0574:        // Recurse if we're merging object values
0575:        if ( deep && options[ name ] && typeof options[ name ] == "object" && target[ name ] && !options[ name ].nodeType )
0576:          target[ name ] = jQuery.extend( target[ name ], options[ name ] );
0577:
0578:        // Don't bring in undefined values
0579:        else if ( options[ name ] != undefined )
0580:          target[ name ] = options[ name ];
0581:
0582:      }
0583:
0584:  // Return the modified object
0585:  return target;
0586:};
0587:

extendメソッドは、jQueryオブジェクトを拡張するメソッドです。代表的な使い方は、jQuery.extend(target, object1, [objectN])で、targetのプロパティをobjectNのプロパティで上書きします。複数のObjectを指定した場合は、右に指定したObjectのプロパティが優先されます。jQuery.fnオブジェクトを拡張して独自のプラグインを作成する場合にもこのメソッドを利用します。

実際に中でどのような処理が行われているかですが、548行目にあるように第1引数にBooleanが渡された時は動作が少し変わってきます。この場合は、第1引数をdeep変数に格納し、第2引数以降をスライドして処理対象とします。

560行目は、引数が1つだけの場合の処理です。この場合は、jQueryオブジェクトへの拡張だと判断してtargetにjQueryオブジェクトを設定します。

そして、565行目からが拡張処理を行う部分になります。引数の数だけ繰り返し処理を行いますが、値がnullやundefinedの場合には何もしません(567行目⁠⁠。そして、ディープコピー(deep==true)の場合で、DOMツリーのNodeエレメントではないオブジェクトならば、再帰的にextend()を呼び出して対象のオブジェクトをマージしていきます。また、571行目は循環参照により、無限ループが発生するのを抑えるための処理のようです。

変数定義

0588:var expando = "jQuery" + (new Date()).getTime(), uuid = 0, windowData = {};
0589:
0590:// exclude the following css properties to add px
0591:var exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i;
0592:

4つの変数を定義しています。expandoは、現在のjQueryオブジェクトにユニークなIDを割り当てます。uuidとwindowDataは要素にユニークIDを割り振るために使われます。excludeは、CSSプロパティで単位pxを付加するかどうかの判定に利用されます。

noConflict()

0593:jQuery.extend({
0594:  noConflict: function( deep ) {
0595:    window.$ = _$;
0596:
0597:    if ( deep )
0598:      window.jQuery = _jQuery;
0599:
0600:    return jQuery;
0601:  },
0602:

ここからは、jQuery.…()で呼び出されるメソッドの定義になります。

標準では「$」にjQueryオブジェクトのショートカットが割り当てられます。このまま他のprototype.jsなどのライブラリと一緒に使うと競合してしまいますが、noConflict()をコールすることでこれを回避できます。595行目において、24行目で待避しておいた_$を$に戻しています。

また、hoge = noConflict(true)と引数付きでコールすることで、598行目にあるようにjQueryオブジェクト自体も完全に元に戻すことができます。

isFunction()

0603:  // See test/unit/core.js for details concerning this function.
0604:  isFunction: function( fn ) {
0605:    return !!fn && typeof fn != "string" && !fn.nodeName && 
0606:      fn.constructor != Array && /function/i.test( fn + "" );
0607:  },
0608:  

isFunctionメソッドは、関数オブジェクトであるかどうかを判定するためのメソッドです。prototype.jsにも同名のメソッドがあるのですが、こちらは次のように非常に単純です。

  isFunction: function(object) {
    return typeof object == "function";
  },

どうしてこんな複雑なことをしているのか不思議に思うかもしれませんが、答えはテストコード(test/unit/core.jsの136行目以降)を見ると分かります。実はブラウザごとに挙動が違っていて、より正確を期すためにこのような処理になっています。たったの2行でブラウザごとの差異を吸収しているわけです。

isXMLDoc()

0609:  // check if an element is in a (or is an) XML document
0610:  isXMLDoc: function( elem ) {
0611:    return elem.documentElement && !elem.body ||
0612:      elem.tagName && elem.ownerDocument && !elem.ownerDocument.body;
0613:  },
0614:

610行目のisXMLDocメソッドでは、引数elemがXMLドキュメントもしくはその一部であるかどうかをチェックします。document.bodyが存在するかどうかで判断しているようです。

globalEval()

0615:  // Evalulates a script in a global context
0616:  globalEval: function( data ) {
0617:    data = jQuery.trim( data );
0618:
0619:    if ( data ) {
0620:      // Inspired by code by Andrea Giammarchi
0621:      // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html
0622:      var head = document.getElementsByTagName("head")[0] || document.documentElement,
0623:        script = document.createElement("script");
0624:
0625:      script.type = "text/javascript";
0626:      if ( jQuery.browser.msie )
0627:        script.text = data;
0628:      else
0629:        script.appendChild( document.createTextNode( data ) );
0630:
0631:      head.appendChild( script );
0632:      head.removeChild( script );
0633:    }
0634:  },
0635:

globalEvalメソッドも内部的に使用するメソッドで、グローバルスコープでスクリプトを評価するものですが、中身は意外と単純です。渡されたdataから余計な空白を取り除き、623行目でscriptエレメントを生成しています。ブラウザがInternet Exploerの場合はtextプロパティにスクリプトを設定、それ以外の場合はテキストノードを作成して子要素を追加しています。そして、最後にheadエレメントにscriptを追加するという流れになっています。

nodeName()

0636:  nodeName: function( elem, name ) {
0637:    return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase();
0638:  },
0639:  

nodeNameメソッドは、第1引数で渡されたエレメントのnodeNmaeと第2引数の文字列が同じかどうかを判定して返します。

cache()

0640:  cache: {},
0641:  

一時的にキャッシュとして利用するためのプロパティです。jQuery.data()とjQuery.removeData()から利用されます。

data()

0642:  data: function( elem, name, data ) {
0643:    elem = elem == window ?
0644:      windowData :
0645:      elem;
0646:
0647:    var id = elem[ expando ];
0648:
0649:    // Compute a unique ID for the element
0650:    if ( !id ) 
0651:      id = elem[ expando ] = ++uuid;
0652:
0653:    // Only generate the data cache if we're
0654:    // trying to access or manipulate it
0655:    if ( name && !jQuery.cache[ id ] )
0656:      jQuery.cache[ id ] = {};
0657:    
0658:    // Prevent overriding the named cache with undefined values
0659:    if ( data != undefined )
0660:      jQuery.cache[ id ][ name ] = data;
0661:    
0662:    // Return the named cache data, or the ID for the element  
0663:    return name ?
0664:      jQuery.cache[ id ][ name ] :
0665:      id;
0666:  },
0667:  

dataメソッドは、要素のユニークなIDを返します。もしidが定義されていなければ、588行目で定義されたuuidをインクリメントして利用します。

また、主にイベント処理で利用するのですが、要素に関連付けられた一時データをcache属性に格納しておくことができます。それが653行目以降の処理で、引数nameのみが指定されていたら値を取得し、引数dataも指定されていたら値を設定します。

removeData()

0668:  removeData: function( elem, name ) {
0669:    elem = elem == window ?
0670:      windowData :
0671:      elem;
0672:
0673:    var id = elem[ expando ];
0674:
0675:    // If we want to remove a specific section of the element's data
0676:    if ( name ) {
0677:      if ( jQuery.cache[ id ] ) {
0678:        // Remove the section of cache data
0679:        delete jQuery.cache[ id ][ name ];
0680:
0681:        // If we've removed all the data, remove the element's cache
0682:        name = "";
0683:
0684:        for ( name in jQuery.cache[ id ] )
0685:          break;
0686:
0687:        if ( !name )
0688:          jQuery.removeData( elem );
0689:      }
0690:
0691:    // Otherwise, we want to remove all of the element's data
0692:    } else {
0693:      // Clean up the element expando
0694:      try {
0695:        delete elem[ expando ];
0696:      } catch(e){
0697:        // IE has trouble directly removing the expando
0698:        // but it's ok with using removeAttribute
0699:        if ( elem.removeAttribute )
0700:          elem.removeAttribute( expando );
0701:      }
0702:
0703:      // Completely remove the data cache
0704:      delete jQuery.cache[ id ];
0705:    }
0706:  },
0707:

removeDataメソッドは、expandoおよびdataメソッドで要素に関連付けられたデータを削除します。特に難しい部分はありませんが、Internet Explorerの場合のみ注意が必要でdeleteの代わりにremoveAttributeメソッドを利用しています。

each()

0708:  // args is for internal usage only
0709:  each: function( object, callback, args ) {
0710:    if ( args ) {
0711:      if ( object.length == undefined ) {
0712:        for ( var name in object )
0713:          if ( callback.apply( object[ name ], args ) === false )
0714:            break;
0715:      } else
0716:        for ( var i = 0, length = object.length; i 0717:          if ( callback.apply( object[ i ], args ) === false )
0718:            break;
0719:
0720:    // A special, fast, case for the most common use of each
0721:    } else {
0722:      if ( object.length == undefined ) {
0723:        for ( var name in object )
0724:          if ( callback.call( object[ name ], name, object[ name ] ) === false )
0725:            break;
0726:      } else
0727:        for ( var i = 0, length = object.length, value = object[0]; 
0728:          i 0729:    }
0730:
0731:    return object;
0732:  },
0733:  

一般的なイテレータの機能を提供するeachメソッドです。

引数のargs配列があるかないかでcall()とapply()を、またArrayかそれ以外のObjectかでループ処理を使い分けています。コメントやJohnのコミットログを見る限りでは、call()を使った方が速いということでこのようにしているようです。

prop()

0734:  prop: function( elem, value, type, i, name ) {
0735:      // Handle executable functions
0736:      if ( jQuery.isFunction( value ) )
0737:        value = value.call( elem, i );
0738:        
0739:      // Handle passing in a number to a CSS property
0740:      return value && value.constructor == Number && type == "curCSS" && !exclude.test( name ) ?
0741:        value + "px" :
0742:        value;
0743:  },
0744:

prop()は、内部的に使用するメソッドです。引数valueとして関数オブジェクトが渡された場合は、引数elemに対してその関数を実行します。また、引数valueに数値が渡され、CSSメソッドからの呼び出しの場合に単位"px"を付加します。

className()

0745:  className: {
0746:    // internal only, use addClass("class")
0747:    add: function( elem, classNames ) {
0748:      jQuery.each((classNames || "").split(/\s+/), function(i, className){
0749:        if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) )
0750:          elem.className += (elem.className ? " " : "") + className;
0751:      });
0752:    },
0753:
0754:    // internal only, use removeClass("class")
0755:    remove: function( elem, classNames ) {
0756:      if (elem.nodeType == 1)
0757:        elem.className = classNames != undefined ?
0758:          jQuery.grep(elem.className.split(/\s+/), function(className){
0759:            return !jQuery.className.has( classNames, className );  
0760:          }).join(" ") :
0761:          "";
0762:    },
0763:
0764:    // internal only, use is(".class")
0765:    has: function( elem, className ) {
0766:      return jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1;
0767:    }
0768:  },
0769:

classNameは、内部的に使用されるためのものです。

className.addメソッドは、addClass()から利用されます。748行目で複数のクラス名が指定されていて、nodeType=1つまりElementの場合で既にそのクラスが割り当てられていなければ、空白に続いてそのクラスを追加します。

また、className.removeメソッドはremoveClass()から利用され、addと同様にそのクラス名を取り除きます。

最後にclassName.hasメソッドはそのクラス名が既に指定されているかを判別するためのメソッドになります。1111行目で定義されているinArray()メソッドを利用しています。

おすすめ記事

記事・ニュース一覧