Chapter 14DOM یا مدل شیء سند

چه بد! داستانی تکراری! وقتی کار ساخت خانه را تمام می‌کنی، متوجه می‌شوی که چیزی یاد گرفتی که می‌بایست پیش از شروع کار می‌دانستی.

فردریش نیچه, فراسوی نیک و بد
Picture of a tree with letters and scripts hanging from its branches

وقتی یک صفحه وب را در مرورگرتان باز می کنید، مرورگر متن HTML صفحه را گرفته و آن را تفسیر می کند، بسیار شبیه به آنچه تجزیه‌گر ما برای تجزیه‌ی برنامه‌ها در فصل 12 انجام می داد. مرورگر یک مدل از ساختار سند می سازد و از آن برای نمایش سند روی صفحه‌ی نمایش استفاده می کند.

این نمایش از سند، یکی از ابزارهایی است که یک برنامه‌ی جاوااسکریپت در جعبه‌ی شنی (sandbox) خود به آن دسترسی دارد. یک ساختار داده که می‌تواند خوانده شود یا تغییر یابد؛ ساختار داده‌ای زنده: زمانی که تغییری در آن رخ می‌دهد، صفحه‌ای که در مانیتور نمایش داده می شود نیز به‌روز می شود تا تغییرات را منعکس کند.

ساختار سند

می توانید یک سند HTML را به عنوان مجموعه‌ای از مستطیل‌های تودرتو در نظر بگیرید. برچسب‌هایی مثل <body> و </body>، دیگر برچسب‌ها را در بر می گیرند، که خود نیز حاوی برچسب‌های دیگر یا متن می‌باشند. به عنوان مثال، سندی از فصل قبل را مشاهده ‌می کنید:

<!doctype html>
<html>
  <head>
    <title>My home page</title>
  </head>
  <body>
    <h1>My home page</h1>
    <p>Hello, I am Marijn and this is my home page.</p>
    <p>I also wrote a book! Read it
      <a href="http://eloquentjavascript.net">here</a>.</p>
  </body>
</html>

ساختار این صفحه به شکل زیر است:

HTML document as nested boxes

ساختار داده‌ای که مرورگر برای نمایش این سند استفاده می کند از این شکل پیروی می کند. برای هر مستطیل، یک شیء وجود دارد، که می توانیم با آن ارتباط برقرار کرده تا چیزهایی مثل برچسب HTML آن یا برچسب‌ها و متونی که دربر دارد را بدست بیاوریم. این طرز نمایش را مدل شیء سند یا به اختصار DOM می گویند.

متغیر سراسری document امکان دسترسی به این اشیاء را فراهم می سازد. خاصیت documentElement آن به شیئی ارجاع می دهد که نمایانگر برچسب <html> است. چون هر سند HTML دارای یک سرصفحه و یک بدنه می باشد ، در نتیجه دارای خاصیت‌های head و body نیز می باشد که به آن عناصر اشاره می می‌کنند.

درخت‌ها

کمی به درخت‌های گرامر که در فصل 12 معرفی شدند فکر کنید. ساختار آن‌ها بسیار شبیه به ساختار یک سند مرورگر است. هر گره ممکن است به دیگر گره‌ها ارجاع دهد، فرزندان، که خود می توانند فرزندان خود را داشته باشند. این شکل یک ساختار تودرتوی معمول است که عناصر در آن می توانند حاوی زیرعنصرهایی مشابه باشند.

یک ساختار داده را زمانی یک درخت می‌نامیم که دارای شاخه‌هایی بدون دور باشد ( یک گره ممکن نیست به شکل مستقیم یا غیر مستقیم حاوی خودش باشد) و همچنین یک ریشه مشخص داشته باشد. در رابطه با DOM، ریشه‌ی درخت، document.documentElement می‌باشد.

درخت‌ها در علم کامپیوتر کاربرد زیادی دارند. علاوه بر نمایش ساختارهای درختی مانند اسناد HTML یا برنامه‌ها، اغلب از آن‌ها برای نگهداری مجموعه‌های مرتب شده‌ی داده استفاده می شود زیرا معمولا ورود یا جستجوی عناصر در یک درخت با کارایی بیشتری نسبت به یک‌ آرایه‌ی تخت انجام می شود.

یک درخت معمول دارای انواع مختلفی از گره‌ها می‌باشد. درخت گرامر زبان Egg دارای شناسه‌ها، مقادیر، و گره‌های کاربرد (Application) بود. گره‌های کاربرد می توانستند دارای فرزند باشند، درحالیکه شناسه‌ها و مقادیر از جنس برگ بودند؛ منظور گره‌هایی است که فرزندی ندارند.

همین روال برای DOM هم برقرار است. گره‌ها به عنوان عناصر، که برچسب‌های HTML را نمایش می دهند، ساختار سند را تعیین می کنند. این گره‌ها می توانند گره‌های فرزند داشته باشند. یک نمونه از این نوع گره‌ها document.body است. بعضی از این فرزندان می توانند گره‌هایی از نوع برگ باشند، مثل متن‌ها یا گره‌های توضیحات.

هر شیء گره‌ی DOM دارای خاصیتی به نام nodeType است که حاوی کدی (عددی) است که نوع آن گره را مشخص می کند. عناصر دارای کد 1 می‌باشند که همچنین به صورت یک خاصیت ثابت Node.ELEMENT_NODE نیز در دسترس است. گره‌های متنی ، که نمایانگر یک قسمت از متن در سند می باشند دارای کد 3 می‌باشند (Node.TEXT_NODE). کد 8 نیز به توضیحات اختصاص دارد (Node.COMMENT_NODE).

یک روش دیگر برای به تصویر کشیدن درخت مربوط به سندمان، شکل زیر است.

HTML document as a tree

برگ‌ها، گره‌های متنی هستند و پیکان‌ها نمایانگر روابط والد-فرزندی بین گره‌ها می باشند.

استاندارد

استفاده از کدهای عددی رمزگونه برای نمایش نوع گره‌ها چیزی نیست که در جاوااسکریپت معمول باشد. در ادامه فصل می بینیم که دیگر بخش‌های رابط DOM نیز حسی نامانوس ایجاد می کنند. دلیل آن این است که DOM فقط برای جاوااسکریپت طراحی نشده است. بلکه تلاش شده که رابط آن نسبت به زبان‌ها بی طرف باشد که بتوان از آن در دیگر سیستم‌ها به خوبی استفاده شود – نه فقط در HTML بلکه همچنین برای XML که فرمتی عمومی برای داده‌ها است و گرامری شبیه به HTML دارد.

خوب این زیاد مطلوب نیست. استانداردها اغلب مفید هستند. اما در این مورد خاص، مزیت آن (سازگاری فرا زبانی) آن قدرها قانع کننده نیست. در دست داشتن رابطی که به خوبی با زبانی که استفاده می کنید یکپارچه است، زمان زیادی برای شما صرفه جویی می کند تا اینکه یک رابط عمومی برای همه‌ی زبان‌ها داشته باشیم.

یک نمونه از این یکپارچگی ضعیف، خاصیت childNodes است که در گره‌های عنصر در DOM، وجود دارند. این خاصیت یک شیء آرایه‌طور را نگهداری می کند که خاصیتی به نام length دارد و همچنین خاصیت‌هایی دارد که توسط اعداد برچسب‌گذاری شده اند تا بتوان به گره‌های فرزند دسترسی داشت. اما این یک نمونه از نوع NodeList است نه یک آرایه‌ی واقعی بنابراین متدهای آرایه مثل slice و map را ندارد.

همچنین مشکلاتی وجود دارد که ناشی از طراحی ضعیف است. به عنوان مثال، راهی برای ایجاد یک گره جدید به همراه گره‌های فرزند و خصوصیت‌ها در یک گام وجود ندارد. بلکه باید ابتدا گره را ایجاد کنید، بعد فرزندان و خصوصیت‌ها را یکی پس از دیگری به وسیله اثرات جانبی بسازید. کدهایی که با DOM تعامل زیادی برقرار می‌کنند، معمولا طولانی، تکراری و بدریخت هستند.

اما این ایرادات، مشکلاتی مهلک محسوب نمی شوند. چون جاوااسکریپت این امکان را به ما می دهد که تجریدهای خودمان را بنویسیم، می توان راه‌های بهتری را برای انجام عملیات برنامه‌تان طراحی کنید. خیلی از کتاب‌خانه‌هایی که برای برنامه‌نویسی مرتبط با مرورگر ایجاد شده اند، این ابزار را فراهم می کنند.

حرکت در درخت

گره‌های DOM حاوی پیوندهای زیادی به دیگر گره‌های نزدیک می‌باشند. نمودار زیر این موضوع را به تصویر می کشد:

Links between DOM nodes

اگرچه این نمودار فقط 1 پیوند برای هر نوع نشان می دهد، اما هر گره خاصیتی به نام parentNode دارد که در صورت وجود به گره‌ای اشاره می کند که خودش بخشی از آن است. به همین صورت، هر گره‌ی عنصر (گره نوع ‍‍1) دارای یک خاصیت childNodes است که به یک شیء آرایه‌طور اشاره می کند که حاوی فرزندان آن گره می باشد.

در تئوری، می توانید با استفاده از پیوندهای والد و فرزند، به هر جای درخت حرکت کنید. اما جاوااسکریپت پیوندهای مناسب دیگری را در اختیار شما قرار می دهد. خاصیت‌های firstChild و lastChild به اولین و آخرین عنصرهای فرزند اشاره می کنند یا مقدار null را در صورتی که فرزندی نداشته باشد خواهند داشت. به طور مشابه، previousSibling و nextSibling به گره‌های همجوار اشاره می کنند که گره‌هایی هستند که تحت والد مشترکی قرار دارند و درست قبل یا بعد از گره‌ی مورد نظر قرار گرفته اند. برای اولین فرزند، previousSibling برابر با null خواهد بود و برای فرزند آخر، nextSibling برابر null خواهد بود.

همچنین خاصیتی به نام children وجود دارد که شبیه به childeNodes است با این تفاوت که فقط فرزندان نوع عنصر (نوع 1) را شامل می شود نه دیگر انواع گره فرزند. این خاصیت می تواند در زمانی که به گره های متنی نیازی ندارید استفاده شود.

وقتی با یک ساختار داده‌ی تودرتو مثل این ساختار کار می کنید، توابع بازگشتی اغلب مفید هستند. تابع مثال زیر یک سند را برای یافتن گره‌های متنی جستجو می کند که دارای یک رشته‌ی خاص باشند و در صورت پیدا کردن آن مقدار true را برمی گرداند.

function talksAbout(node, string) {
  if (node.nodeType == Node.ELEMENT_NODE) {
    for (let i = 0; i < node.childNodes.length; i++) {
      if (talksAbout(node.childNodes[i], string)) {
        return true;
      }
    }
    return false;
  } else if (node.nodeType == Node.TEXT_NODE) {
    return node.nodeValue.indexOf(string) > -1;
  }
}

console.log(talksAbout(document.body, "book"));
// → true

چون childNodes یک آرایه‌ی واقعی نیست نمی توان برای پیمایش آن از for/of استفاده کرد بلکه باید یا از یک حلقه‌ی معمولی for بهره برد یا از Array.from استفاده نمود.

خاصیت nodeValue یک گره‌ی متنی، حاوی رشته‌ی متنی است که گره نشان می دهد.

پیدا کردن عناصر

حرکت در طول پیوندها به گره‌های والد، فرزندان و گره‌های همجوار اغلب مفید است. اما اگر بخواهیم یک گره‌ی خاص را در یک سند پیدا کنیم، شروع از document.body و پیمایش یک مسیر ثابت از خاصیت‌ها برای یافتن گره، ایده‌ی خوبی نیست. برای این کار نیاز است تا فرض‌هایی درباره‌ی یک ساختار دقیق از سند داشته باشیم – ساختاری که احتمالا قرار است آن را تغییر دهید. یکی دیگر از فاکتورهایی که کار را پیچیده می کند این است که گره‌های متنی برای فضاهای خالی بین گره‌ها هم ایجاد می شوند. مثلا در برچسب <body> سند، فقط سه فرزند ندارد (<h1> و دو <p> ) بلکه در واقع دارای هفت فرزند می‌باشد. آن سه عنصر به همراه فضاهای خالی قبل، بعد و بین‌شان.

بنابراین اگر بخواهیم خصوصیت href پیوند موجود در سند را به دست بیاوریم، نمی خواهیم دستوری شبیه به ” فرزند دوم ششمین فرزند body سند را به دست بیاور” داشته باشیم. بهتر می بود اگر می توانستیم دستوری شبیه به “اولین لینکی که در سند آمده است را بگیر” داشته باشیم. و می توانیم این کار را بکنیم.

let link = document.body.getElementsByTagName("a")[0];
console.log(link.href);

تمامی گره‌های عنصر، دارای متدی به نام getElementsByTagName هستند که تمامی عناصری که برچسب داده شده را دارند و زیرمجموعه‌ی آن گره محسوب می شوند (فرزند مستقیم و غیر مستقیم) را جمع آوری می‌کند و به صورت یک شیء آرایه‌طور برمی گرداند.

برای یافتن یک گره‌ی خاص واحد، می توانید به آن یک خصوصیت id اختصاص دهید و از document.getElementById استفاده کنید.

<p>My ostrich Gertrude:</p>
<p><img id="gertrude" src="img/ostrich.png"></p>

<script>
  let ostrich = document.getElementById("gertrude");
  console.log(ostrich.src);
</script>

سومین متد که کاری مشابه انجام می دهد getElementsByClassName است که شبیه به getElementsByTagName عمل می کند و در محتوای یک گره‌ی عنصر به جستجو می پردازد و تمامی عناصری که رشته‌ی داده شده را در خصوصیت class شان دارند برمی گرداند.

ایجاد تغییر در سند

تقریبا تمامی قسمت‌های ساختار داده‌ی DOM را می‌توان تغییر داد. می توان شکل درخت سند را با ایجاد تغییر در روابط والد-فرزندی دستکاری کرد. گره‌ها دارای متدی به نام remove می‌باشند که می توان از آن برای حذف گره از والد کنونی‌اش استفاده نمود. برای افزودن یک گره‌ی فرزند به یک گره‌ی عنصر می‌توانیم از appendChild استفاده کنیم که باعث می شود آن گره به انتهای لیست فرزندان اضافه شود. یا از insertBefore استفاده کنیم که گره‌ای که به عنوان آرگومان اول آمده را قبل از گره‌ای که به عنوان آرگومان دوم آمده است وارد می کند.

<p>One</p>
<p>Two</p>
<p>Three</p>

<script>
  let paragraphs = document.body.getElementsByTagName("p");
  document.body.insertBefore(paragraphs[2], paragraphs[0]);
</script>

یک گره فقط در یک موقعیت از سند می تواند موجودیت داشته باشد. بنابراین، قراردادن پاراگراف 3 جلوی پاراگراف یک 1 آن را از انتهای سند حذف می کند و بعد جلو پاراگراف 1 قرار می دهد که نتیجه به این صورت می شود: 3/2/1. تمامی عملیاتی که گره‌ای را در جایی قرار می دهد به عنوان یک اثر جانبی موجب می شوند که ابتدا آن گره از جایگاه فعلی‌اش حذف شود (اگر جایی بوده باشد).

متد replaceChild برای جایگزین کردن یک گره‌ی فرزند با گره‌ی فرزندی دیگر استفاده می شود. این متد دو گره به عنوان آرگومان دریافت می کند: گره‌ی جدید و گره‌ای که باید جایگزین شود. گره‌ی جایگزین شده باید فرزند عنصری باشد که متد روی آن فراخوانی شده است. توجه داشته باشید که هر دوی replaceChild و insertBefore به عنوان آرگومان اول گره‌ای جدید دریافت می کنند.

ایجاد گره‌ها

فرض کنید می خواهیم اسکریپتی بنویسیم که تمامی عکس‌های موجود در سند (برچسب‌های <img>) را با نوشته‌ای که در خصوصیت alt آن قرار دارد جایگزین کند، نوشته‌ای که برای مشخص کردن یک نمایش متنی برای تصویر استفاده می شود.

این کار هم شامل حذف عکس‌ها می شود و هم ایجاد یک گره‌ی متنی جهت جایگزینی عکس. گره‌های متنی توسط document.createTextNode ایجاد می شوند.

<p>The <img src="img/cat.png" alt="Cat"> in the
  <img src="img/hat.png" alt="Hat">.</p>

<p><button onclick="replaceImages()">Replace</button></p>

<script>
  function replaceImages() {
    let images = document.body.getElementsByTagName("img");
    for (let i = images.length - 1; i >= 0; i--) {
      let image = images[i];
      if (image.alt) {
        let text = document.createTextNode(image.alt);
        image.parentNode.replaceChild(text, image);
      }
    }
  }
</script>

با دادن یک رشته، تابع createTextNode به ما یک گره‌ی متنی تحویل می دهد که می توانیم آن را در سند قرار داده تا در صفحه نمایش نشان داده شود.

حلقه‌ای که عکس‌ها را پیمایش می کند از پایان لیست کارش را شروع می کند. این کار لازم است چرا که لیست گره‌ها که توسط متدی مثل getElementsByTagName برگردانده می شود ( یا خاصیتی مثل childNodes)، لیستی زنده است. به این معنا که با تغییر سند، لیست هم به‌روز می شود. اگر از ابتدا شروع می کردیم، حذف اولین عکس ممکن بود باعث شود لیست اولین عنصرش را از دست بدهد که در این صورت با تکرار حلقه در بار دوم ، زمانی که i برابر 1 می شود، از کار می ایستاد زیرا طول مجموعه اکنون برابر 1 است.

اگر یک مجموعه‌ی ثابت از گره‌ها را لازم دارید، به‌جای یک مجموعه‌ی زنده از آن ها، می توانید مجموعه را با فراخوانی Array.from به یک آرایه‌ی واقعی تبدیل کنید.

let arrayish = {0: "one", 1: "two", length: 2};
let array = Array.from(arrayish);
console.log(array.map(s => s.toUpperCase()));
// → ["ONE", "TWO"]

برای ایجاد گره‌های عنصر، می توانید از متد document.createElement استفاده کنید. این متد یک نام برچسب گرفته و یک گره‌ی تهی از نوع داده شده را بر می گرداند.

در مثال پیش رو یک تابع کاربردی به نام elt ایجاد می شود که یک گره‌ی عنصر را ایجاد کرده و دیگر آرگومان‌هایش را به عنوان فرزندان آن گره در نظر می گیرد. در ادامه از همین تابع برای افزودن یک خصوصیت به یک برچسب نقل قول استفاده می‌شود.

<blockquote id="quote">
  No book can ever be finished. While working on it we learn
  just enough to find it immature the moment we turn away
  from it.
</blockquote>

<script>
  function elt(type, ...children) {
    let node = document.createElement(type);
    for (let child of children) {
      if (typeof child != "string") node.appendChild(child);
      else node.appendChild(document.createTextNode(child));
    }
    return node;
  }

  document.getElementById("quote").appendChild(
    elt("footer", "—",
        elt("strong", "Karl Popper"),
        ", preface to the second editon of ",
        elt("em", "The Open Society and Its Enemies"),
        ", 1950"));
</script>

خصوصیت‌ها

بعضی از خصوصیت‌های عناصر مثل href برای پیوندها را می توان به عنوان خاصیتی با همین نام روی شیء DOM عنصر، مورد دستیابی قرار داد. این برای بیشتر خصوصیت‌های استاندارد رایج، برقرار است.

اما HTML این امکان را فراهم کرده است که هر خصوصیتی که بخواهید را به گره‌ها اضافه کنید. این امکان می‌تواند مفید باشد زیرا به شما اجازه می دهد اطلاعات بیشتری را در یک سند ذخیره کنید. با این وجود اگر نام خصوصیت‌های خودتان را بسازید، این خصوصیت‌ها به عنوان خاصیت شیء گره در دسترس نخواهند بود. برای کار با آن ها باید از متدهای getAttribute و setAttribute استفاده کنید.

<p data-classified="secret">The launch code is 00000000.</p>
<p data-classified="unclassified">I have two feet.</p>

<script>
  let paras = document.body.getElementsByTagName("p");
  for (let para of Array.from(paras)) {
    if (para.getAttribute("data-classified") == "secret") {
      para.remove();
    }
  }
</script>

توصیه شده که نام این گونه‌ خصوصیت‌های ساختگی را با پیشوند data- آغاز کنید تا مطمئن شوید که تداخلی با دیگر خصوصیت‌ها پیش نخواهد آمد.

خصوصیت class که یکی از خصوصیت‌های رایج است، یک کلیدواژه در جاوااسکریپت محسوب می شود. به دلایل تاریخی – بعضی از پیاده‌سازی‌های قدیمی جاوااسکریپت نمی توانستند نام‌های خاصیتی که با کلیدواژه‌ها مطابقت داشتند را مدیریت کنند – خاصیتی که برای دسترسی به این خصوصیت در نظر گرفته شده است className است. همچنین می توانید تحت نام واقعی خودش "class" نیز به وسیله‌ی متدهای getAttribute و setAttribute به آن دسترسی داشته باشید.

طرح‌بندی (layout)

ممکن است متوجه شده باشید که انواع مختلف عنصرها به صورت متفاوتی طرح بندی می شوند. بعضی مانند پاراگراف ها (<p>) یا سرعنوان‌ها (<h1>)، تمامی عرض سند را اشغال کرده و هر کدام در خط جدیدی به نمایش در می‌آیند. این عناصر، عناصر بلاک نامیده می شوند. دیگر عناصر، مثل پیوندها (<a>) یا عنصر <strong> در همان خط کنار متن پیرامونشان نمایش داده می شوند. این عنصرها را عناصر درون خطی (inline) می نامند.

برای هر سند داده شده، مرورگرها می‌توانند با درنظر گرفتن موقعیت و اندازه‌ی هر عنصر، یک طرح را محاسبه کنند. این طرح بعد برای به تصویر کشیدن سند استفاده می شود.

اندازه و موقعیت یک عنصر را می توان از طریق جاوااسکریپت به دست آورد. خاصیت‌های offsetWidth و offsetHeight فضایی که یک عنصر اشغال می کند را در واحد پیکسل نشان می دهند. یک پیکسل واحد بنیادی اندازه‌گیری در مرورگر است. به طور سنتی این واحد به کوچکترین نقطه‌ای که صفحه‌نمایش می تواند به تصویر بکشد مرتبط است اما در مانیتورهای مدرن، که قادرند نقطه‌های خیلی کوچک را به تصویر بکشند، دیگر موضوعیت ندارد و یک پیکسل مرورگر ممکن است چند نقطه‌ را در صفحه نمایش پوشش دهد.

به طور مشابه، clientWidth و clientHeight به شما اندازه‌ی فضای درون یک عنصر را نشان می دهد و عرض خط مرزی (border) در نظر گرفته نمی شود.

<p style="border: 3px solid red">
  I'm boxed in
</p>

<script>
  let para = document.body.getElementsByTagName("p")[0];
  console.log("clientHeight:", para.clientHeight);
  console.log("offsetHeight:", para.offsetHeight);
</script>

موثرترین روش برای بدست آوردن موقعیت دقیق یک عنصر روی صفحه‌ی نمایش، استفاده از متد getBoundingClientRect است. این متد یک شیء بر می گرداند که شامل خاصیت‌های top، bottom، left و right می باشد که مشخص می کنند موقعیت هر ضلع یک عنصر به نسبت بالا و چپ صفحه‌نمایش چند پیکسل است. اگر این اعداد را نسبت به کل سند لازم دارید، بایستی موقعیت اسکرول فعلی را به آن اضافه کنید. این موقعیت را می می‌توان در متغیرهای pageXOffset و pageYOffset بدست آورد.

طرح بندی یک سند می تواند کار زیادی ببرد. برای اعمال سرعت بیشتر، موتور مروگر با هر بار تغییر در سند آن را دوباره طرح بندی نمی کند بلکه تا زمانی که بتواند آن را به تاخیر می اندازد .زمانی که برنامه‌ی جاوااسکریپت که سند را تغییر داده است به پایان اجرایش برسد، مرورگر بایستی یک طرح جدید محاسبه کند تا بتواند سند تغییریافته را به تصویر بکشد. زمانی که یک برنامه موقعیت یا اندازه‌ی چیزی را با خواندن خاصیت‌هایی مثل offsetHeight یا فراخوانی getBoundingClientRect درخواست می کند، فراهم ساختن اطلاعات صحیح نیز نیاز به محاسبه‌ی طرح دارد.

برنامه‌ای که زیاد و به تکرار به خواندن اطلاعات طرح DOM و تغییر DOM می پردازد باعث می شود که محاسبات طرح‌بندی زیاد اتفاق بیفتد که در نتیجه این برنامه به کندی اجرا خواهد شد. کد پیش رو مثالی از این گونه برنامه است. این مثال از دو برنامه تشکیل یافته است که خطی از کاراکترهای X با پهنای 2,000 پیکسل می سازند و زمانی که هرکدام طول می کشد را محاسبه می کنند.

<p><span id="one"></span></p>
<p><span id="two"></span></p>

<script>
  function time(name, action) {
    let start = Date.now(); // Current time in milliseconds
    action();
    console.log(name, "took", Date.now() - start, "ms");
  }

  time("naive", () => {
    let target = document.getElementById("one");
    while (target.offsetWidth < 2000) {
      target.appendChild(document.createTextNode("X"));
    }
  });
  // → naive took 32 ms

  time("clever", function() {
    let target = document.getElementById("two");
    target.appendChild(document.createTextNode("XXXXX"));
    let total = Math.ceil(2000 / (target.offsetWidth / 5));
    target.firstChild.nodeValue = "X".repeat(total);
  });
  // → clever took 1 ms
</script>

سبک دهی

تاکنون دیده‌ایم که عناصر مختلف HTML به شکل متفاوتی به تصویر کشیده می‌شوند. بعضی به عنوان بلاک و بعضی درون‌خطی (inline) نشان داده می شوند. بعضی سبک بصری اضافه می کنند – <strong> محتوایش را توپر می کند و <a> محتوایش را آبی رنگ و زیر آن خط می اندازد.

شیوه‌ای که یک برچسب <img> تصویر را نمایش می دهد یا یک برچسب <a> متنی را به یک لینک تبدیل می‌کند، به طور کامل به نوع آن عنصر وابسته است. اما سبک بصری‌ای که به صورت پیش‌فرض به یک عنصر اضافه می شود، مثل رنگ متن یا داشتن زیرخط، را می توانیم تغییر دهیم. در اینجا مثالی که از خصوصیت style استفاده می کند را می بینیم.

<p><a href=".">Normal link</a></p>
<p><a href="." style="color: green">Green link</a></p>

یک خصوصیت style می تواند حاوی یک یا چند اعلان باشد که شامل یک خاصیت (مثل color) که به همراه دونقطه و مقدارش می‌آید. زمانی که بیش از یک اعلان وجود دارد، باید هر اعلان با یک نقطه‌ویرگول جدا شود مثل "color: red; border: none".

خیلی از جنبه‌های مربوط به سند وجود دارند که می توانند توسط سبک‌دهی تاثیر بپذیرند. به عنوان مثال خاصیت display برای کنترل نحوه‌ی نمایش یک عنصر به صورت درون‌خطی یا بلاک استفاده می شود.

This text is displayed <strong>inline</strong>,
<strong style="display: block">as a block</strong>, and
<strong style="display: none">not at all</strong>.

برچسب block در خط خودش به پایان می رسد به دلیل اینکه عناصر بلاکی به صورت درون خطی کنار متن پیرامونشان نشان داده نمی شوند. برچسب آخر اصلا نمایش داده نمی شود – display: none مانع از نمایش یک عنصر در صفحه‌ی نمایش می شود. این روشی برای مخفی کردن عناصر است. معمولا ترجیح داده می شود بجای حذف کامل یک عنصر از سند، از این روش استفاده شود به دلیل این که بازگرداندن آن در آینده در این روش آسان تر است.

کدهای جاوااسکریپت می توانند مستقیما سبک‌دهی یک عنصر را با استفاده از خاصیت style دستکاری کنند. این خاصیت شیئی را نگهداری می کند که خاصیت‌هایی برای همه‌ی ویژگی‌های سبک‌دهی دارد. مقدار این خاصیت‌ها از نوع رشته است که می توانیم برای تغییر یک جنبه‌ی خاص از سبک بصری عنصر مورد نظر آن را بنویسیم.

<p id="para" style="color: purple">
  Nice text
</p>

<script>
  let para = document.getElementById("para");
  console.log(para.style.color);
  para.style.color = "magenta";
</script>

نام بعضی از خاصیت‌های سبک‌دهی حاوی کاراکتر خط پیوند (-) است مانند font-family. به دلیل این که کار با این نوع نام ها در جاوااسکریپت کمی دشوار است (برای دسترسی باید چیزی مثل style["font-family"] داشته باشید)، نام خاصیت‌های این گونه نام‌ها در شیء style آن خط پیوند را حذف کرده و حرف بعد از آن را با حروف بزرگ می نویسند (style.fontFamily).

سبک‌های آبشاری

به سیستم سبک‌دهی بصری در اچ‌تی‌ام‌ال، CSS گفته می‌شود که مخفف برگه‌های سبک آبشاری یا سلسله‌مراتبی (cascading style sheets) است. یک برگه‌ی سبک به مجموعه‌ای از دستورات گفته می شود که برای سبک‌دهی ظاهری به عناصر یک سند استفاده می شوند. می توان آن را درون یک جفت برچسب <style> قرار داد.

<style>
  strong {
    font-style: italic;
    color: gray;
  }
</style>
<p>Now <strong>strong text</strong> is italic and gray.</p>

معنای آبشاری این است که قوانینی با سلسله‌مراتب با هم ترکیب می شوند تا سبک نهایی را برای یک عنصر تولید کنند. در مثال بالا، سبک پیش‌فرض برای برچسب‌های <strong>، که در آن font-weight: bold تعریف شده بود، توسط دستوری دیگر که در برچسب <style> آمده است تغییر یافته و font-style و color به آن اضافه شده است.

وقتی چندین دستور یک مقدار را برای یک خاصیت واحد تعریف می کنند، آخرین دستوری که خوانده شود دارای حق تقدم بالاتری خواهد بود و اعمال می شود. بنابراین اگر دستوری که در برچسب <style> آمده است font-weight: normal را داشته باشد، دستور پیش‌فرض font-weight در نظر گرفته نمی شود و متن به شکل نرمال نمایش داده می شود نه به صورت توپر. دستوراتی که در خصوصیت style به صورت مستقیم به یک گره اعمال می شوند دارای بالاترین حق تقدم هستند و همیشه در سلسله‌مراتب برنده می شوند.

می توان چیزهایی بجز نام برچسب‌ها را در دستورات CSS مورد هدف قرار داد. دستوری به شکل .abc، به همه‌ی عناصری که خصوصیت classشان دارای مقدار "abc" است اعمال می شود. دستوری به شکل #xyz به عنصری اعمال می شود که خصوصیت id آن برابر "xyz" باشد (که باید در سند منحصر به فرد باشد).

.subtle {
  color: gray;
  font-size: 80%;
}
#header {
  background: blue;
  color: white;
}
/* p elements with id main and with classes a and b */
p#main.a.b {
  margin-bottom: 20px;
}

قاعده‌ی حق تقدمی که موجب می‌شد دستوری که آخر تعریف شده بود اعمال شود، زمانی موثر است که دستورات دارای specificity (درجه‌ی صراحت) یکسانی باشند. درجه‌ی صراحت یک دستور، معیاری از میزان دقتی است که آن دستور، عناصر هدفش را توصیف می کند که توسط عدد و نوع (برچسب، class و ID) عناصر مورد هدف تعیین می شود. به عنوان مثال، دستوری که p.a را هدف قرار می دهد دارای صراحت بیشتری از دستوراتی است که p یا فقط .a را هدف قرار می دهند و بنابراین حق تقدم بیشتری خواهد داشت.

دستوری به شکل p > a {…} سبک‌های تعریف شده را به همه‌ی برچسب‌های <a> که فرزند مستقیم برچسب‌های <p> محسوب می شوند اعمال می کند. به طور مشابه، p a {…} به همه‌ی برچسب‌های <a> که درون برچسب‌های <p> باشند اعمال می شود فارغ از اینکه فرزند مستقیم یا غیر مستقیم باشند.

گزینشگرهای پرس و جو

ما در این کتاب زیاد از برگه‌های سبک‌دهی استفاده نخواهیم کرد. درک آن ها برای برنامه‌نویسی در مرورگر مفید است اما دامنه‌ی بحث در باره‌ی سبک‌دهی به اندازه‌ای گسترده می‌باشد که نیاز به کتاب مجزایی داشته باشند.

علت اینکه قواعد گزینشگر (selector) – منظور شیوه‌ی نشان‌کذاری استفاده شده در برگه‌های سبک‌دهی برای تعیین عناصر هدف برای اعمال سبک‌ها می‌باشد – را معرفی کردم این است که می توانیم از این زبان نصف و نیمه به عنوای روشی موثر برای پیدا کردن عناصر DOM استفاده کنیم.

متد querySelectorAll که هم در شیء document موجود است و هم روی گره‌های عنصر، یک رشته‌ی گزینشگر دریافت می کند و یک NodeList‍ که حاوی تمامی عناصر تطبیق خورده است را برمی گرداند.

<p>And if you go chasing
  <span class="animal">rabbits</span></p>
<p>And you know you're going to fall</p>
<p>Tell 'em a <span class="character">hookah smoking
  <span class="animal">caterpillar</span></span></p>
<p>Has given you the call</p>

<script>
  function count(selector) {
    return document.querySelectorAll(selector).length;
  }
  console.log(count("p"));           // All <p> elements
  // → 4
  console.log(count(".animal"));     // Class animal
  // → 2
  console.log(count("p .animal"));   // Animal inside of <p>
  // → 2
  console.log(count("p > .animal")); // Direct child of <p>
  // → 1
</script>

برخلاف متدهایی مثل getElementsByTagName، شیءای که توسط querySelectorAll برگردانده می شود زنده یا پویا نیست. با تغییر سند، این شیء به روز نمی شود و هنوز یک آرایه‌ی واقعی نیست و اگر لازم دارید تا از ویژگی‌های آرایه‌ها بهره ببرید باید Array.from را روی آن‌ها فراخوانی کنید.

متد querySelector (بدون All) به شکل مشابهی عمل می کند. این متد برای زمانی که قصد دارید یک عنصر مشخص را هدف قرار دهید مناسب است. این متد فقط اولین عنصری که مطابق گزینشگر بود را برمی گرداند و در صورت پیدا نکردن هیچ عنصری مقدار null را تولید می کند.

موقعیت دهی و متحرک‌سازی

خاصیت position در سبک‌دهی، تاثیر مهمی در در طرح‌بندی صفحه‌ می‌گذارد. به صورت پیش فرض مقدار آن برابر static است که یعنی عنصر مورد نظر در موقعیت نرمال خودش در سند قرار می گیرد. زمانی که این مقدار به relative تغییر می یابد، عنصر همچنان فضایی در سند اشغال می کند اما اکنون می توان از خاصیت‌های top و left برای تغییر مکان آن نسبت به جایگاه نرمالش استفاده کرد. زمانی که position برابر absolute قرار گیرد، عنصر مورد نظر از جریان چیدمان صفحه خارج می شود – به این معنا که دیگر فضایی را اشغال نمی کند و ممکن است روی دیگر عناصر بیفتد. همچنین در این حالت خاصیت‌های top و left را می توان برای موقعیت دهی مطلق عنصر نسبت به گوشه‌ی بالا و چپ نزدیک ترین عنصر والدش (که دارای مقدار position غیر از static است) استفاده کرد یا در صورت نبود آن، نسبت به کل سند موقعیت دهی می شود.

می توانیم از این خاصیت برای ایجاد یک پویانمایی استفاده کنیم. صفحه‌ی پیش رو تصویری از یک گربه را نشان می دهد که به دور دایره‌ای حرکت می کند.

<p style="text-align: center">
  <img src="img/cat.png" style="position: relative">
</p>
<script>
  let cat = document.querySelector("img");
  let angle = Math.PI / 2;
  function animate(time, lastTime) {
    if (lastTime != null) {
      angle += (time - lastTime) * 0.001;
    }
    cat.style.top = (Math.sin(angle) * 20) + "px";
    cat.style.left = (Math.cos(angle) * 200) + "px";
    requestAnimationFrame(newTime => animate(newTime, time));
  }
  requestAnimationFrame(animate);
</script>

تصویر ما در مرکز صفحه قرار گرفته است و مقدار position آن برابر relative است. ما پیوسته مقدار top و left تصویر را برای ایجاد حرکت به‌روز رسانی می کنیم.

این اسکریپت از requestAnimationFrame برای زمانبندی اجرای تابع animate هنگامی که مرورگر آماده است تا تصویر صفحه را دوباره بکشد، استفاده می کنیم. تابع animate خود دوباره requestAnimationFrame را برای زمانبندی به‌روزرسانی بعدی فراخوانی می کند. زمانی که پنجره‌ی مرورگر (یا تب مرورگر) فعال است، این باعث می شود که به‌روز‌رسانی‌ها با نرخ 60 بار در ثانیه انجام شود که پویانمایی روانی را تولید می کند.

اگر فقط DOM را در حلقه به‌روزرسانی می کردیم، صفحه ممکن بود قفل شود چیزی در تصویر نمایش داده نشود. مرورگرها صفحه‌ی نمایش خود را در هنگام اجرای یک برنامه‌ی جاوااسکریپت به‌روزرسانی نمی کنند و اجازه‌ی هیچ تعاملی با صفحه را هم فراهم نمی کنند. به همین دلیل است که به requestAnimationFrame نیاز داریم – این تابع به مرورگر می گوید که کار ما در این لحظه تمام است و می تواند به کارش ادامه دهد، مثل به‌روزرسانی صفحه نمایش و پاسخ به درخواست‌های کاربر.

تابع پویانمایی، زمان فعلی را به عنوان یک آرگومان دریافت می کند. برای کسب اطمینان از اینکه میزان حرکت گربه در هزارم ثانیه مداوم و باثبات است، این تابع سرعت را در قسمت‌هایی که زاویه تغییر می کند بر اساس تفاوت بین زمان فعلی و آخرین باری که تابع اجرا شد در نظر می گیرد. اگر با هر قدم مقدار ثابتی حرکت در زاویه انجام می‌شد، ممکن بود حرکت تصویر لنگ بزند، درصورتی‌ که به عنوان مثال یک برنامه‌ی سنگین دیگر روی همان کامپیوتر اجرا می شد که تابع را برای کسری از ثانیه از حرکت می انداخت.

برای حرکت دایره‌ای از توابع مثلثات مثل Math.cos و Math.sin استفاده می شود. برای افرادی که با این مفاهیم آشنا نیستند، به طور خلاصه آن ها را معرفی خواهم کرد چرا که کم و بیش از آن ها در کتاب استفاده خواهیم کرد.

متدهای Math.cos و Math.sin برای پیدا کردن نقاطی که روی محیط دایره‌ای با مرکز (0,0) و شعاع یک استفاده می شوند. هر دوی این متدها ورودی‌هایشان را به عنوان موقعیت‌هایی روی این دایره تفسیر می کنند، که صفر به معنای نقطه‌ای است که در راست‌ترین قسمت دایره قرار گرفته است و حرکت در جهت گردش عقربه‌های ساعت تا می باشد (حدود 6.28) که یک دور کامل دایره انجام می شود. Math.cos مختصات x نقطه‌ای که مربوط به موقعیت داده شده است را برمی‌گرداند در حالیکه Math.sin مختصات y آن را می دهد. موقعیت‌هایی (یا زاویه‌ها) که از بزرگتر باشند یا از 0 کوچکتر باشند معتبر محسوب می شوند – چرخش تکرار می شود بنابراین a+2π معادل همان زاویه‌ی a خواهد بود.

این واحد اندازه‌گیری برای زاویه‌ها را رادیان می نامند- یک دایره‌ی کامل برابر رادیان است، مشابه 360 در واحد درجه. ثابت π در جاوااسکریپت توسط Math.PI در دسترس است.

Using cosine and sine to compute coordinates

کد پویانمایی گربه یک شمارنده هم نگهداری می کند، angle، تا زاویه‌ی فعلی حرکت را داشته باشد و با هر بار فراخوانی تابع animate آن را افزایش می دهد. بعدا می تواند از این زاویه برای محاسبه موقعیت فعلی عنصر تصویر استفاده کند. مقدار خاصیت سبکی top هم به وسیله‌ی Math.sin و ضرب آن در 20 محاسبه می شود که نمایانگر شعاع عمودی در بیضی ما است. مقدار left نیز بر اساس Math.cos و ضرب آن در 200 است بنابراین بیضی ما دارای عرض بسیار بیشتری نسبت به طولش است.

توجه داشته باشید که مقادیر مربوط به سبک‌ها معمولا به واحد نیاز دارند. در این مثال، ما باید "px" را به عدد اضافه کنیم تا به مرورگر اعلام کنیم که شمارش در واحد پیکسل می باشد (نه سانتیمتر، “em”، یا دیگر واحدها). به راحتی ممکن است این نکته فراموش شود. استفاده از اعداد بدون واحدها باعث می شود که سبک‌دهی شما اعمال نگردد- مگر اینکه آن عدد 0 باشد که همیشه معنای یکسانی دارد.

خلاصه

برنامه‌های جاوااسکریپت می توانند در صفحه‌ای که مرورگر به نمایش می گذارد، با استفاده از یک ساختار داده به نام DOM، دخالت و دستکاری کنند. این ساختار داده نمایانگر مدل مرورگر از صفحه است و یک برنامه‌ی جاوااسکریپت می تواند آن را تغییر دهد و در سندی که به نمایش درمی آید تغییر ایجاد کند.

DOM به شکل یک درخت سازماندهی شده است که در آن عناصر به صورت سلسله‌مراتبی براساس ساختار سند مرتب می شوند. اشیائی که نماینده‌ی عناصر هستند دارای خاصیت‌هایی مانند parentNode و childNodes هستند که می توان از آن ها برای حرکت در این درخت استفاده کرد.

نحوه‌ی نمایش یک سند را می توان با سبک‌دهی تغییر داد و این کار به دو روش چسباندن سبک‌ها به عناصر به صورت مستقیم و یا با تعریف دستوراتی که عناصر خاصی را هدف قرار می دهند صورت می‌پذیرد. خاصیت‌های سبک‌دهی زیاد و متنوعی وجود دارد مثل color یا display. کدهای جاوااسکریپت می توانند سبک یک عنصر را مستقیما از طریق خصوصیت style دستکاری کنند.

تمرین‌ها

ساخت یک جدول

یک جدول HTML به وسیله‌ی ساختار برچسب‌های زیر ساخته می شود:

<table>
  <tr>
    <th>name</th>
    <th>height</th>
    <th>place</th>
  </tr>
  <tr>
    <td>Kilimanjaro</td>
    <td>5895</td>
    <td>Tanzania</td>
  </tr>
</table>

برای هر ردیف، برچسب <table> یک برچسب <tr> خواهد داشت. درون این برچسب‌های <tr> می توانیم سلول‌های جدول را قرار دهیم: سلول‌های معمولی (<td>) یا سلول‌های عنوان (<th>).

با در دست داشتن مجموعه اطلاعاتی در باره‌ی کوه‌ها، آرایه‌ای از اشیاء شامل name، height و place ، یک ساختار DOM برای جدولی که این اشیاء را می شمارد ایجاد کنید. این جدول باید یک ستون برای هر کلید و یک ردیف برای هر شیء داشته باشد، مازاد بر آن یک ردیف عنوان به وسیله‌ی عنصرهای <th> در قسمت بالا که نام ستون‌ها را لیست کند.

این برنامه را به صورتی بنویسید که در آن ستون‌ها به طور خودکار از اشیاء گرفته می می‌شوند و این کار با گرفتن نام‌های خاصیت‌های اولین شیء در مجموعه‌ی داده‌ها رخ می دهد.

به این صورت بنویسید که ستون‌ها به صورت خودکار با گرفتن نام‌ خاصیت‌های اولین شیء در مجموعه‌ی داده‌ها ایجاد شوند.

جدول نهایی را به عنصری که دارای خصوصیت id برابر با "mountains" می‌باشد اضافه کنید تا در صفحه نمایش داده شود.

بعد از این که این قسمت را تکمیل کردید سلول‌هایی که حاوی مقادیر عددی هستند را راست چین کنید و این کار را با تنظیم style.textAlign برابر با "right" انجام دهید.

<h1>Mountains</h1>

<div id="mountains"></div>

<script>
  const MOUNTAINS = [
    {name: "Kilimanjaro", height: 5895, place: "Tanzania"},
    {name: "Everest", height: 8848, place: "Nepal"},
    {name: "Mount Fuji", height: 3776, place: "Japan"},
    {name: "Vaalserberg", height: 323, place: "Netherlands"},
    {name: "Denali", height: 6168, place: "United States"},
    {name: "Popocatepetl", height: 5465, place: "Mexico"},
    {name: "Mont Blanc", height: 4808, place: "Italy/France"}
  ];

  // Your code here
</script>

می توانید از document.createElement برای ایجاد گره‌های جدید استفاده کنید، همچنین از document.createTextNode برای ایجاد گره‌های متنی و از appendChild نیز برای قرار دادن گره‌ها در دیگر گره‌ها.

در ادامه لازم دارید تا نام کلید‌ها را پیمایش کنید؛ یک بار برای پر کردن ردیف بالایی و بعد دوباره برای هر همه‌ی اشیاء موجود در آرایه تا بتوانید ردیف‌های داده را بسازید. برای بدست آوردن یک آرایه از نام کلید‌ها از شیء اول، Object.keys به دردتان خواهد خورد.

برای اضافه کردن جدول به گره‌ی والد فعلی، می توانید از document.getElementById یا document.querySelector برای پیدا کردن گره‌ای با خاصیت id مورد نظر استفاده کنید.

گرفتن عناصر به وسیله‌ی نام برچسب‌ها

متد document.getElementsByTagName تمامی عناصر فرزند را برای نام برچسب داده شده برمی گرداند. نسخه‌ی خودتان از این متد را به عنوان یک تابع بنویسید که یک گره و یک رشته (نام برچسب) را به عنوان ورودی‌ها بگیرد و آرایه‌ای که حاوی تمامی گره‌های فرزند متعلق به آن برچسب را برگرداند.

برای پیدا کردن نام برچسب یک عنصر، از خاصیت nodeName استفاده کنید. اما توجه داشته باشید که این خاصیت نام برچسب را با حروف بزرگ برمی گرداند. از متدهای رشته، toLowerCase یا toUpperCase برای درست کردن آن استفاده کنید.

<h1>Heading with a <span>span</span> element.</h1>
<p>A paragraph with <span>one</span>, <span>two</span>
  spans.</p>

<script>
  function byTagName(node, tagName) {
    // Your code here.
  }

  console.log(byTagName(document.body, "h1").length);
  // → 1
  console.log(byTagName(document.body, "span").length);
  // → 3
  let para = document.querySelector("p");
  console.log(byTagName(para, "span").length);
  // → 2
</script>

ساده‌ترین روش پیاده‌سازی این راه‌حل استفاده از یک تابع بازگشتی است، شبیه به talksAbout تابع که پیش‌تر در این فصل تعریف گردید.

می‌توانید عناصر آرایه‌ی تولیدی را به وسیله‌ی فراخوانی تابع byTagname به صورت بازگشتی به هم بچسبانید تا خروجی را تولید کنید. یا می توانید تابعی درونی تعریف کنید که خودش را به صورت بازگشتی فراخوانی کند که این تابع به یک متغیر آرایه که در تابع بیرونی‌اش تعریف شده دسترسی دارد و می تواند عناصری که پیدا‌ می‌کند را به آن اضافه نماید. فراموش نکنید که باید تابع درونی را یک بار از تابع بیرونی فراخوانی کنید تا روند کار شروع شود.

تابع بازگشتی باید نوع گره را بررسی کیند. در اینجا فقط مایلیم تا گره‌ نوع 1 (Node.ELEMENT_NODE) را در نظر بگیریم. برای این نوع گره‌ها، ما باید فرزندانشان را پیمایش کنیم و برای هر فرزند، مشاهده کنیم که آیا با پرس‌وجوی‌ ما مطابقت دارد یا خیر و همچنین یک فراخوانی بازگشتی روی آن نیز داشته باشیم تا فرزندان آن را نیز پوشش داده باشیم.

کلاه گربه

پویانمایی ایجاد شده در قبل را توسعه دهید تا گربه و کلاهش (<img src="img/hat.png">) هر کدام در جهت مخالف هم بچرخند.

یا کاری کنید که کلاه دور گربه بچرخد. یا پویانمایی را به صورتی که جالب باشد تغییر دهید.

برای ساده سازی روند موقعیت دهی چند شیء، احتمالا ایده‌ی خوبی است که به سراغ موقعیت دهی مطلق برویم. به این معنا که top و left بر اساس گوشه‌ی چپ و بالای سند محاسبه بشوند. برای جلوگیری از مختصات منفی، که باعث می شود که تصاویر به بیرون از فضای قابل مشاهده صفحه منتقل شوند، می توانید یک عدد مشخص و ثابت را به مقادیر موقعیت ها اضافه کنید.

<style>body { min-height: 200px }</style>
<img src="img/cat.png" id="cat" style="position: absolute">
<img src="img/hat.png" id="hat" style="position: absolute">

<script>
  let cat = document.querySelector("#cat");
  let hat = document.querySelector("#hat");

  let angle = 0;
  let lastTime = null;
  function animate(time) {
    if (lastTime != null) angle += (time - lastTime) * 0.001;
    lastTime = time;
    cat.style.top = (Math.sin(angle) * 40 + 40) + "px";
    cat.style.left = (Math.cos(angle) * 200 + 230) + "px";

    // Your extensions here.

    requestAnimationFrame(animate);
  }
  requestAnimationFrame(animate);
</script>

Math.cos و Math.sin زاویه‌ها را در واحد رادیان اندازه‌گیری می کنند، جایی‌که یک دایره‌ی کامل برابر با می‌باشد. برای یک زاویه‌ی داده شده، می توانید برای به‌دست آوردن زاویه‌ی مخالف، نصف که می‌شود Math.PI. این کار برای قرار دادن کلاه در طرف دیگر دایره کاربرد دارد.