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

Fundebug發表於2018-02-12

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

為了保證可讀性,本文采用意譯而非直譯。另外,本文版權歸原作者所有,翻譯僅用於學習。

快速掌握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/

相關文章