changed 6 years ago
Linked with GitHub

JavaScript 底層機制與相關執行原理

tags: javascriptadvancedconcept

Hoisting

console.log(a)
var a = 1

當我們輸入這樣的程式碼所得的結果會是 undefined,很合理,因為變數a 還沒被附值。

而上面這段程式碼因為 hoisting 提升的作用,其實和下面的程式碼是一樣的:

var a
console.log(a)
a = 1

這也就是為什麼結果會是 undefined,而不是顯示「a is not defined」的錯誤訊息。

我們再以 function 來看:

test()

function test() {
  console.log('123')
}

結果會是 123的字串,也就是說在呼叫函式時還沒跑到函式的那一行就有函式的作用,只因為 hoisting 的關係,所以不管 function 被放到哪裡都可以被呼叫。

另外下面這種方式也是賦值的一種,因此不能看做 function 的 hoisting:

test()

var test = function () {
  console.log('123')
}

可以把它看作:

var test
test()

test = function () {
  console.log('123')
}

因為在第一行的時候 test還沒被賦值,因此為undefined,所以test()的結果會是test is not a function

hoisting 的順序

下面這個例子的結果是 undefined 的原因是在 test function 裡面,因為宣告了 var a = 5,導致說 var a 做了一個 hoisting 的動作,再加上作用域的關係,因此接收到的a 值是 undefined,而不是全域變數的10

var a = 10
function test() {
    console.log(a) // undefined
    var a = 5
}

test()

上面的例子可以看成這樣:

var a = 10
function test() {
    var a
    console.log(a) 
    a = 5
}

test()

而因為每個變數如果在被宣告的時候沒有賦值,預設值是undefined,所以結果才是undefined

今天如果是放入 function:

var a = 10
function test() {
    console.log(a) // [Function: a]
    function a(){
      
    }
}

test()

結果會是抓到 function a,表示 function 也變數提升了。

那如果varfunction 同時出現:

function test() {
  console.log(a) // [Function: a]
  var a = 10
  function a() {

  }
}

test()

結果也是抓到 function a,表示說 function 比變數宣告佔有優先權。

那如果是重複出現的 function:

function test() {
  console.log(a) // ya
  a()
  function a() {
    console.log("hello")
  }
  function a() {
    console.log("ya")
  }
}

test()

則會顯示最後的 function 結果,表示後面的 function 會覆蓋掉前面一樣名稱的 function 結果。

如果同時傳參數與var宣告:

function test(a) {
  console.log(a) // 123
  var a = 456
}

test(123)

結果則是引數傳進來的值,因此參數會優先於var的變數宣告,表示說這邊的變數提升:var a實則沒什麼作用,會被忽略。

這邊的var變數提升被忽略的理由是:
當今天變數已經被前面宣告過了(這個例子是參數已經宣告過變數)的話,那麼接下來所有純粹的var a的變數宣告都會被無視,像下面的例子:

function test() {
    var a = 123
    var a
    var a
    console.log(a) // 123
}

test()

結果只有var a = 123 這行被採納,下面兩行的var a在這邊沒有作用產生。

但原本那個例子的順序如果換一下就會被var的值(後來新增的值)覆蓋:

function test(a) {
  var a = 456
  console.log(a) // 456
}

test(123)

那如果傳參數的同時,存在 function:

function test(a) {
  console.log(a) // [Function: a]
  function a() {

  }
}

test(123)

結果會是抓到 function,表示 function 的提升順序優於參數。

總結上面我們可以知道 hoisting 順序為:

  1. function
  2. argument
  3. var

而 Hoisting 底層的運作實際上與 Execution Context 有關,詳情請見參考資料。

參考資料:

我知道你懂 hoisting,可是你了解到多深?

Closure

直接看範例:

function createCounter() {
  var count = 0
  return function () {
    count++
    return count
  }
}

var counter = createCounter()
console.log(counter())
console.log(counter())

Closure 主要目的是為了不讓變數值被外面其他的變數或是因重複命名而影響到,所以利用 function 的 Closure 特性把所有涵蓋的變數值給包起來。

如果不包起來則會是這樣:

var count = 0
function counter() {
  count++
  return count
}

console.log(counter())
console.log(counter())

雖然一樣可以運作,但是在 counter() function 外面的 count變數值可能就會被更改到或者是因重複命名而有所影響

再舉一個運用閉包特性的例子:

function vol(dim) {
  console.log('calculated!')
  return dim * dim * dim
}

function cache(func) {
  let obj = {}
  return function (dim) {
    if(obj[dim]) {
      return obj[dim]
    }

    obj[dim] = func(dim)
    return obj[dim]
  }
}

const cacheVol = cache(vol)
console.log(cacheVol(5)) // calculated! 125
console.log(cacheVol(5)) // 125
console.log(cacheVol(5)) // 125

藉由閉包,也就是 cache function 回傳的 function,這個 function 可以判定是否已經計算過這個值了,如果已經計算過了,那就直接輸出結果,而不是再回到vol function 重新計算,因此在上面console.log()的結果會發現只有第一次進到vol function 計算數值,後面再呼叫 cacheVol 傳回來的值實際上都是已經儲存在cache function 的 obj物件裡面,只是單純從物件裡面取出值來而已。而這樣的好處有幫助程式執行運算的速度,假如vol function 今天是個裡面佈滿複雜的運算才有辦法得出結果的話,每次我們要拿到vol function 的結果就得需要經過這道複雜的運算才有辦法拿到。我們藉由閉包的特性,使用cachefunction 傳入 function 參數進去(這邊即為vol),變成我們可以靈活運用這個輸入進來的 function ,可以決定我們何時使用它,有一種工具包裡面的其中一項工具的概念,需要用到的時候就拿出來,不需要的時候就簡便處理,而且就算不需要的時候這個工具一直都放在裡面,這也是閉包的特性,執行過的 function 卻依然存在。

參考資料:

所有的函式都是閉包:談 JS 中的作用域與 Closure

Prototype

function Person(name) {
  this.name = name
  this.getName = function () {
    return this.name
  }
}

let nick = new Person('nick')
let mike = new Person('mike')

console.log(nick.getName === mike.getName)

上面console.log的結果為 False,因為兩次呼叫物件的記憶體是不同的。

但是如果我們使用prototype的話,兩次呼叫就會是一樣的,結果會是True,程式碼如下:

function Person(name) {
  this.name = name
}

Person.prototype.getName = function () {
  return this.name
}

let nick = new Person('nick')
let mike = new Person('mike')
console.log(nick.getName === mike.getName)

那麼為什麼會一樣?因為nick.getName === Person.prototype.getName,或是mike.getName === Person.prototype.getName,也就是說都會是相等的。

實務上會先去尋找 Person 這個 instance 有沒有getName的方法,假如沒有,它就會往prototype當中去尋找,那它為什麼會有這個特性?因為其實在每個 instance 被建立的時候,javascript 的底層都會偷偷建立一個叫做Person.__proto__的方法,而這個方法等於Person.prototype

那如果在Person.__proto__找不到getName的方法的話,那又會繼續往下找,也就是Person.__proto__.__proto__,而這個方法等於Person.prototype.__proto__,也等於Object.prototype

也就是說 protoype 的尋找順序為:

  1. nick
  2. nick.__proto__(= Person.protoype
  3. nick.__proto__.__proto__ ( = Person.prototype.__proto__ = Object.prototype)
  4. nick.__proto__.__proto__.__proto__(這層即為底層,顯示出來的結果為 null)

這種一層一層一直尋找底層有沒有相符的物件的機制,叫做「原型鏈(prototype chain)」。

而 ES5 以前因為沒有class的語法,所以是以prototype的方式取代classmethod設定(如上方例子的Person.prototype.getName = function() {}),更深入探討,其實 ES6 之後新增的class語法它的底層運作其實就是prototype機制。

延伸閱讀

該來理解 JavaScript 的原型鍊了


程式執行原理

Call Stack

以 Stack 資料結構為原理的程式執行方式。
比如:

function a() {
  b()
}

function b() {
  c()
}

function c() {
  console.log('123')
}

這段程式碼會先 call function a,再來 call function b,再來 call function c,接著console.log('123')

以 stack 來看,就是這樣的順序進入 call stack: a->b->c->console.log('123');然後再以這樣的順序離開 call stack:console.log('123')->c->b->a。

執行環境(Execution Context:EC)

在 Javascript 的程式碼執行順序,可能不是一行一行執行的,而是由編譯→執行這兩個順序來完成,否則 hoisting 的機制沒辦法做解釋。雖然 javascript 常被稱作「直譯語言」,可是並不是說它所有的機制都是直譯的,這邊我們要介紹的這個機制就是其中之一由編譯→執行這兩個動作所運作,有關於執行環境(EC)的機制。

以上面的 call stack 為例,其實每個 function 的 stack 都有單獨的執行環境,可以看做有 EC of function a、EC of function b、EC of function C,另外還有個 global EC 處理非函數的執行環境,像是變數之類的。

可以把 EC 想像為 Javascript 的物件,每個 EC 當中有這樣子的結構:

{
    Variable Object,
    scopeChain,
    this
}

詳細的結構:

EC: {
 VO: {
     arguments: {
     
     },
     a: 1,
     b: 2,
 },
 scopeChain: [],
 this
}

arguments 為 function 的參數,scopeChain 就像是 prototype chain 那種結構,如果找不到,那就一層一層往上找的結構。(this這邊先跳過,之後有機會補)

Select a repo