میخواهیم ساختار و استایل این source code را بررسی کنیم. نمیخواهیم بررسی کنیم که هر ویژگی چطور پیاده سازی شده، میخواهیم سعی کنیم کد را بخوانیم و بفهمیم چگونه ساختاربندی شده است. کتابخانه jQuery هیچ ویژگی ای به بروزر اضافه نمیکند، فقط یک کتابخانه است. تنها کاری که میکند این است که تایپ سینتکسی کدها را راحت تر میکند و همچنین مسائل مربوط به بروزرها را برای ما هندل میکند. بروزرها با هم متفاوت هستند و هسته های مختلفی دارند. ما دیگر نگران این مسئله نیستیم، آن کدی که میخواهیم را مینویسیم و کدهای داخل کتابخانه jQuery این مسئله را هندل میکنند. این کتابخانه به ما اجازه دستکاری DOM را میدهد. DOM همان document object modelling داخل بروزر است و از موتور js مجزا است. چیزی است که به بروزر اجازه میدهد تا به html نگاه کند و تصمیم بگیرد چگونه آن را render کند و در صفحه نمایش دهد. js به DOM دسترسی دارد و میتواند آن را تغییر دهد. میتواند با دستکاری DOM در حافظه، ساختار صفحه html را بعد از لود شدن دستکاری کند. DOM ساختاری شبیه به درخت دارد که html را در خود قرار داده است. js نگاه کردن به DOM را راحت میکند تا المنت ها را در صفحه مان پیدا کنیم و آنها را دستکاری کنیم.
اسم این کتابخانه در کد $ یا jQuery است. مثلا کد html زیر را داریم:
<div id='main' class='container'>
<h1>People</h1>
<ul class='people'><li>John Doe</li><li>Jane Doe</li><li>Jim Doe</li></ul>
<script src='jquery-1.11.2.js'><script src='app.js'>
کد js خودمان را در app.js مینویسیم. کتابخانه jQuery دو نسخه دارد. نسخه دوم در بروزرهای خیلی قدیمی ساپورت نمیشود. اما ساختار کدها یکسان است. کل html به صورت ساختار درختی در حافظه داخل بروزر ذخیره شده است و js به همه آن دسترسی دارد. اگر در app.js بنویسیم:
var q = $('ul.people li');
console.log(q);
همه li هایی که داخل ul ها با کلاس people هستند را داخل درخت انتخاب میکند. خروجی کد بالا:
jQuery.fn.init[3]
یک شیء با نوع jQuery برگردانده که یک آرایه است. هر عضو از این آرایه یک المنت از DOM است. یک المنت استاندارد از بروزر است.در همین خروجی در قسمت __proto__ داریم:
__proto__:jQuery[0]
این یک شیء jQuery است که داخل آن متدهای خیلی زیادی وجود دارد. قبلا گفته شده بود که بهتر است متدها را در prototype قرار دهیم تا در فضای حافظه صرفه جویی کنیم.
میخواهیم بدانیم jQuery.fn.init یعنی چه. همان طور که در کد مشاهده میکنید از عملگر new استفاده نشده است:
var q = $('ul.people li');
میخواهیم بدانیم این کد چگونه کار میکند. کد minify نشده jQuery حدود 10 هزار خط کد است که پر از کامنت است. اولین خط کد این کتابخانه:
(function(global, factory) { ...
این یک IIFE است. از تریک پرانتز استفاده کرده است تا syntax parser بداند این یک function expression است. همه کدها را در یک تابع wrap کرده است. این تابع دو پارامتر میگیرد. میخواهد بداند که global object چی هست. در خط اول کد داخل این تابع یک ماژول را چک میکند:
if (typeof module==='object' && typeof module.exports==='object') { ... }
در مورد این خط کد توضیح داده که برای commonJS یا commonJs-like environment ها یا Node.js استفاده میشه. در واقع داره چک میکنه که jQuery در چه environment ای هست تا بفهمد global object چی هست. این تابع در آخر تعریف ش invoke شده است:
(function(global, factory) {
...
return factory(w);
...
}(typeof window!=='undefined' ? window : this, function(window, noGlobal){...})
یعنی برای پاس دادن پارامتر global چک میکند که آیا شیء window وجود دارد یا نه. پس اگر داخل یک بروزر باشیم global object همان window است، در غیر این صورت this را برمیگرداند (مثلا ممکن است node.js باشد).
پارامتر دوم که پاس داده میشود factory است که آخر همین تابع برگردانده میشود. موقع فراخوانی تابع، این پارامتر خودش به صورت یک تابع پاس داده شده است. پس تابع اول تابعی است که دارد چک میکند که jQuery کجا قرار گرفته است و تابع factory پاس داده شده کد اصلی jQuery است. در پاس دادن پارامتر factory یک function expression دیگر داریم که window و noGlobal را میگیرد. شیء window را وقتی تابع factory فراخوانی میشود (return factory(w)) میگیریم. کد function(window, noGlobal) به عنوان factory پاس داده میشود و factory داخل کد invoke میشود. وقتی که function(window, noGlobal) فراخوانی میشود چه اتفاقی می افتد؟ در یک execution context جدید یک سری متغیر ست میشوند. در واقع وقتی factory آخر تابع invoke میشود یک execution context جدید داریم. یکی از متغیرهای مهمی که ست میشود jQuery است که یک تابع است:
var jQuery = function (selector, context){ return new jQuery.fn.init(selector, context); };
پارامتر selector همان رشته ای است که به jQuery میدهیم تا بفهمد چه المنتی از html را پاس داده ایم. همان طور که دیده میشود داخل تعریف jQuery کد زیادی نداریم. وقتی شیء jQuery را در کد زیر ایجاد کردیم:
var q = $('ul.people li');
به ما jQuery.fn.init[3] را برگرداند. چون تابع jQuery یک function constructor نیست. به همین دلیل در خط کد بالا نیازی نبود از عملگر new استفاده کنیم. چون تعریف jQuery یک تابع است که یک شیء را برمیگرداند. شیء را با فراخوانی یک function constructor برمیگرداند (new jQuery.fn.init(selector, context)). پس دیگر نیازی نیست موقع استفاده از jQuery ما خودمان همیشه از کلمه کلیدی new استفاده کنیم.
jQuery.fn = jQuery.prototype = { ... };
مقدار jQuery.fn را برابر با Query.prototype قرار داده یعنی by reference است. پس fn به همان قسمت از حافظه اشاره میکند که ویژگی prototype تابع jQuery در آن قرار دارد. همه تابع ها ویژگی .prototype دارند که وقتی به عنوان function constructor استفاده شوند از آن ویژگی استفاده میشود. اگر به عنوان function constructor هم استفاده نکنیم، همچنان این ویژگی را داریم که خالی است. پس از این شیء خالی استفاده کردیم. با توجه به خط کد بالا نیازی نیست که هر بار prototype را تایپ کنیم و میتوانیم به جای آن از fn استفاده کنیم. پس fn همان prototype تابع است که با یک شیء جدید مجددا نوشته میشود (= { ... }). حال میخواهیم بدانیم داخل jQuery.fn چه داریم. داخل آن ویژگی های زیادی داریم، از جمله each و map که از callback استفاده میکنند:
each: function(callback, args) { return jQuery.each(this, callaback, args); },
map: function(callback) { return this.pushStack(jQuery.map(this, function(elem, i){ return callback.call(elem, i, elem); })); }
در این دو یک callback را به یک تابع پاس داده ایم. برای مثال در map از callback.call استفاده کرده ایم. یعنی داریم تابعی که به map پاس داده شده است را invoke میکنیم.
بعد از تمام شدن قطعه کد jQuery.fn = jQuery.prototype = { ... }; داریم:
jQuery.extend = jQuery.fn.extend = function() {};
یعنی به یک تابع دو تا اسم داده شده است و by reference است. در بخش های قبل در مورد extend در underscore توضیح داده شد.
jQuery.extend = jQuery.fn.extend = function() {
var src, copyIsArray, copy, name, options, clone, target=arguments[0]||{}, i=1, length=arguments.length, deep=false;
if (typeof target === 'boolean') { deep=target; target=arguments[i]||{}; i++; }
if (typeof target !== 'object' && !jQuery.isFunction(target)) { target={} }
if (i === length) { target=this; i--; }
for ( ; i<length; i++) {
if ((options=arguments[i]) != null) {
for (name in options) {
src=target[name];
copy=options[name];
if(target===copy){continue;}
if(...){ ... target[name]=jQuery.extend(deep, clone, copy); }
else if(copy!==undefined) { target[name]=copy; }
}
return target;
};
}
};
در این کد چیزها را به یک شیء اضافه میکند. ویژگی ها و متدهای یک شیء را میگیرد و به شیء دیگری اضافه میکند. قسمت length=arguments.length یعنی میتوانیم به هر تعداد شیء که میخواهیم داشته باشیم، چون حلقه for ( ; i<length; i++) را داریم. در اولین خط کد این حلقه دسته ای از شیء ها را پاس میدهیم:
options=arguments[i]
برای اگر 3 شیء داشته باشیم، length برابر با 3 است. سپس در حلقه for (name in options) از reflection استفاده میکنیم و همه ویژگی ها و متدها را بررسی میکنیم. یک src و یک copy داریم. همه ویژگی ها و متدهای یک شیء را به شیء دیگری اضافه میکند. همان طور که در کد target=arguments[0]||{} مشاهده میشود target آرگومان اول مان است. پس اولین پارامتری که به extend پاس میدهیم همان چیزی است که میخواهیم extend کنیم و همان شیئی است که میخواهیم در نهایت داشته باشیم. پس طبق src=target[name]; یعنی src جایی است که ویژگی ها و متدها در آن کپی میشوند. در خط کد
target[name]=jQuery.extend(deep, clone, copy);
دوباره از extend استفاده کرده ایم برای اینکه شیء ها میتوانند خودشان شامل شیء دیگری باشند و میخواهیم مطمئن شویم که همه ویژگی ها و متدها حتی sub-object ها هم به target اضافه شده اند.
در ادامه کد میبینیم:
jQuery.extend({...});
یعنی دارد ویژگی ها و متدهایی را به شیء jQuery اضافه میکند و اولین آگورمان آن target است.