快速掌握JavaScript面試基礎知識(三)

  • 2019 年 12 月 31 日
  • 筆記

譯者按: 總結了大量 JavaScript 基本知識點,很有用!

本文採用意譯,版權歸原作者所有

根據StackOverflow 調查, 自 2014 年一來,JavaScript 是最流行的程式語言。當然,這也在情理之中,畢竟 1/3 的開發工作都需要一些 JavaScript 知識。因此,如果你希望在成為一個開發者,你應該學會這門語言。

這篇部落格的主要目的是將所有面試中常見的概念總結,方便你快速去了解。(鑒於本文內容過長,方便閱讀,將分為三篇部落格來翻譯, 此為第三部分。第一部分請點擊快速掌握 JavaScript 面試基礎知識(一))

new 關鍵字

如果使用new關鍵字來調用函數式很特別的形式。我們把那些用new調用的函數叫做構造函數(constructor function)。

使用了new的函數到底做了什麼事情呢?

  • 創建一個新的對象
  • 將對象的 prototype 設置為構造函數的 prototype
  • 執行構造函數,this執行新構造的對象
  • 返回該對象。如果構造函數返回對象,那麼返回該構造對象。
// 為了更好地理解底層,我們來定義new關鍵字  function myNew(constructor, ...arguments) {      var obj = {};      Object.setPrototypeOf(obj, constructor.prototype);      return constructor.apply(obj, arguments) || obj;  }

使用new和不使用的區別在哪裡呢?

function Bird() {      this.wings = 2;  }  /* 普通的函數調用 */  let fakeBird = Bird();  console.log(fakeBird); // undefined  /* 使用new調用 */  let realBird = new Bird();  console.log(realBird); // { wings: 2 }

為了便於對比理解,譯者額外增加了測試了一種情況:

function MBird() {      this.wings = 2;      return "hello";  }    let realMBrid = new MBird();  console.log(realMBird); // { wings: 2 }

你會發現,這一句return "hello"並沒有生效!

原型和繼承

原型(Prototype)是 JavaScript 中最容易搞混的概念,其中一個原因是prototype可以用在兩個不同的情形下。

  • 原型關係 每一個對象都有一個prototype對象,裡面包含了所有它的原型的屬性。 .__proto__是一個不正規的機制(ES6 中提供),用來獲取一個對象的 prototype。你可以理解為它指向對象的parent。 所有普通的對象都繼承.constructor屬性,它指向該對象的構造函數。當一個對象通過構造函數實現的時候,__proto__屬性指向構造函數的構造函數的.prototypeObject.getPrototypeOf()是 ES5 的標準函數,用來獲取一個對象的原型。
  • 原型屬性 每一個函數都有一個.prototype屬性,它包含了所有可以被繼承的屬性。該對象默認包含了指向原構造函數的.constructor屬性。每一個使用構造函數創建的對象都有一個構造函數屬性。

接下來通過例子來幫助理解:

function Dog(breed, name) {      (this.breed = breed), (this.name = name);  }  Dog.prototype.describe = function() {      console.log(`${this.name} is a ${this.breed}`);  };  const rusty = new Dog("Beagle", "Rusty");    /* .prototype 屬性包含了構造函數以及構造函數中在prototype上定義的屬性。*/  console.log(Dog.prototype); // { describe: ƒ , constructor: ƒ }    /* 使用Dog構造函數構造的對象 */  console.log(rusty); //  { breed: "Beagle", name: "Rusty" }  /* 從構造函數的原型中繼承下來的屬性或函數 */  console.log(rusty.describe()); // "Rusty is a Beagle"  /* .__proto__ 屬性指向構造函數的.prototype屬性 */  console.log(rusty.__proto__); // { describe: ƒ , constructor: ƒ }  /* .constructor 屬性指向構造函數 */  console.log(rusty.constructor); // ƒ Dog(breed, name) { ... }

JavaScript 的使用可以說相當靈活,為了避免出 bug 了不知道,不妨接入Fundebug線上實時監控

原型鏈

原型鏈是指對象之間通過 prototype 鏈接起來,形成一個有向的鏈條。當訪問一個對象的某個屬性的時候,JavaScript 引擎會首先查看該對象是否包含該屬性。如果沒有,就去查找對象的 prototype 中是否包含。以此類推,直到找到該屬性或則找到最後一個對象。最後一個對象的 prototype 默認為 null。

擁有 vs 繼承

一個對象有兩種屬性,分別是它自身定義的和繼承的。

function Car() {}  Car.prototype.wheels = 4;  Car.prototype.airbags = 1;    var myCar = new Car();  myCar.color = "black";    /*  原型鏈中的屬性也可以通過in來查看:  */  console.log("airbags" in myCar); // true  console.log(myCar.wheels); // 4  console.log(myCar.year); // undefined    /*  通過hasOwnProperty來查看是否擁有該屬性:  */  console.log(myCar.hasOwnProperty("airbags")); // false — Inherited  console.log(myCar.hasOwnProperty("color")); // true

Object.create(obj) 創建一個新的對象,prototype 指向obj

var dog = { legs: 4 };  var myDog = Object.create(dog);    console.log(myDog.hasOwnProperty("legs")); // false  console.log(myDog.legs); // 4  console.log(myDog.__proto__ === dog); // true

繼承是引用傳值

繼承屬性都是通過引用的形式。我們通過例子來形象理解:

var objProt = { text: "original" };  var objAttachedToProt = Object.create(objProt);  console.log(objAttachedToProt.text); // original    // 我們更改objProt的text屬性,objAttachedToProt的text屬性同樣更改了  objProt.text = "prototype property changed";  console.log(objAttachedToProt.text); // prototype property changed    // 但是如果我們講一個新的對象賦值給objProt,那麼objAttachedToProt的text屬性不受影響  objProt = { text: "replacing property" };  console.log(objAttachedToProt.text); // prototype property changed

經典繼承 vs 原型繼承

Eric Elliott 的文章有非常詳細的介紹:Master the JavaScript Interview: What』s the Difference Between Class & Prototypal Inheritance? 作者認為原型繼承是優於經典的繼承的,並提供了一個影片介紹:https://www.youtube.com/watch?v=wfMtDGfHWpA&feature=youtu.be

非同步 JavaScript

JavaScript 是一個單執行緒程式語言,也就是說 JavaScript 引擎一次只能執行某一段程式碼。它導致的問題就是:如果有一段程式碼需要耗費很長的時間執行,其它的操作就被卡住了。JavaScript 使用 Call Stack 來記錄函數的調用。一個 Call Stack 可以看成是一摞書。最後一本書放在最上面,也最先被移走。最先放的書在最底層,最後被移走。

為了避免複雜程式碼佔用 CPU 太長時間,一個解法就是定義非同步回調函數。我們自己來定義一個非同步函數看看:

function greetingAsync(name, callback) {      let greeting = "hello, " + name;      setTimeout(_ => callback(greeting), 0);  }    greetingAsync("fundebug", console.log);  console.log("start greeting");

我們在greetingAsync中構造了greeting語句,然後通過setTimeout定義了非同步,callback函數,是為了讓用戶自己去定義 greeting 的具體方式。為方便起見,我們時候直接使用console.log。 上面程式碼執行首先會列印start greeting,然後才是hello, fundebug。也就是說,greetingAsync的回調函數後執行。在網站開發中,和伺服器交互的時候需要不斷地發送各種請求,而一個頁面可能有幾十個請求。如果我們一個一個按照順序來請求並等待結果,串列的執行會使得網頁載入很慢。通過非同步的方式,我們可以先發請求,然後在回調中處理請求結果,高效低並發處理。

下面通過一個例子來描述整個執行過程:

const first = function() {      console.log("First message");  };  const second = function() {      console.log("Second message");  };  const third = function() {      console.log("Third message");  };    first();  setTimeout(second, 0);  third();    // 輸出:  // First message  // Third message  // Second message
  1. 初始狀態下,瀏覽器控制台沒有輸出,並且事件管理器(Event Manager)是空的;
  2. first()被添加到調用棧
  3. console.log("First message")加到調用棧
  4. console.log("First message")執行並輸出「First message」到控制台
  5. console.log("First message")從調用棧中移除
  6. first()從調用棧中移除
  7. setTimeout(second, 0)加到調用棧
  8. setTimeout(second, 0)執行,0ms 之後,second()被加到回調隊列
  9. setTimeout(second, 0)從調用棧中移除
  10. third()加到調用棧
  11. console.log("Third message")加到調用棧
  12. console.log("Third message")執行並輸出「Third message」到控制台
  13. console.log("Third message")從調用棧中移除
  14. third()從調用棧中移除
  15. Event Loop 將second()從回調隊列移到調用棧
  16. console.log("Second message")加到調用棧
  17. console.log("Second message")Second message」到控制台
  18. console.log("Second message")從調用棧中移除
  19. Second()從調用棧中移除

特別注意的是:second()函數在 0ms 之後並沒有立即執行,你傳入到setTimeout()函數的時間和second()延遲執行的時間並不一定直接相關。事件管理器等到setTimeout()設置的時間到期才會將其加入回調隊列,而回調隊列中它執行的時間和它在隊列中的位置已經它前面的函數的執行時間有關。

更多

版權聲明

轉載時請註明作者 Fundebug以及本文地址: https://blog.fundebug.com/2018/01/29/the-definitive-javascript-handbook-for-a-developer-interview-3/