進階 Javasctipt 概念 (2)

PY
8 min readMar 24, 2019

--

重新認識 JS 奇怪部分

第二篇來看看面試必考的 JS 語法,重新複習一遍 JS 的奇怪部分

重新認識 JS 奇怪部分

Execution Context 執行環境

執行環境又稱上下文環境,當 JS 跑起來時,一定都有屬於它的執行環境,要馬是函式,要馬是 Global ,不然就是特殊的 Eval 方法,eval(string) 可以將要計算的表達式或執行語句透過字串執行。

當你的程式跑在 Global 環境時,會有一個 Global Object 也就是 window 物件,所有宣告於這環境底下的變數,都是 window 物件底下的一個屬性。

Lexical Context 語彙環境

在 JS 中 lexical scope (語彙作用域: 在 function 內可取得的 data + 變數) 決定我們可用的變數,不管這 function 在哪裡被呼叫,而第一個語彙環境就是 Global。

Hoisting 提升

hoisting 是個面試必考的題目,還記得 memory heap 嗎? 當變數或是 function 被宣告時,會先被分配一個記憶體空間,下面是基本的 hoisting

console.log(1)
console.log(teddy)//var teddy 會先被執行
console.log(2)
var teddy = 'ohla ohla'//如果是 let const 則不會提升

function 的 hoisting 就比較需要注意了,先來段程式碼

var favouriteFood = "grapes";function foodThoughts() {console.log("Original favourite food: " + favouriteFood);var favouriteFood = "sushi"; 
//在執行環境時被提升,導致前面的food是undefined
console.log("New favourite food: " + favouriteFood);};foodThoughts()

陷阱是你可能第一眼會看到 favouriteFood 並未被宣告,那就往外找,但別忘了同個function底下還有宣告一個favouriteFood,所以第一個 favouriteFood 並不會是 grapes,當 function 被分配記憶體位置時,favouriteFood 在 function 內被提升了,第一個 favouriteFood 就變成了 undefined。

function 裡的 arguments

在 function 內 , 你可以呼叫 arguments 這個關鍵字,它存在於執行環境中,arguments 是一個像是陣列的物件,使用時得小心別跟陣列搞混了

//argumentsfunction marry(person1, person2) {console.log(arguments) //{ 0: 'Tim', 1: 'Tina' }很像陣列對吧?console.log(Array.from(arguments))return `${person1} is now married to ${person2}`}

第一個參數的 key 為 1,以此類推,所以當你要調用的時候得轉成 array,但更方便的是使用 …args 讓你解構arguments 後轉成陣列

function marry2(...pp) {console.log(pp)}

function 裡的變數

變數在哪個作用域宣告,就是那個作用域的變數,就這麼簡單!

function one(){
var cool;
}
function two(){
var cool = two
one()
}
var cool = global
one

Scope chain

前面提到了 function 的變數,那假如說作用域內沒有宣告變數呢? 這時變數因為在作用域內找不到宣告,所以會往外找,一層一層的直到找到他所宣告的那層,特別的是找到最外層沒找到時,JS 還會幫你在 Global 宣告,你得使用 ‘use strict’ 來避免這奇怪的情況

var a = 'a'
function findName(){
console.log(a)
console.log(b) //global
}
findName()

往內找是不行的

function findName(){
console.log(a)
return function(){
var a = 'a'
}
}
// a not defined

eval 跟 with 造成的效能問題在於因為這兩個會欺騙作用域,並改變作用域鏈,當你在 eval 內執行 var a = ‘a’ ,作用域會被這段程式碼汙染,導致 global 一開始就宣告了 a,with 也是大同小異,詳細可以看看這篇

[[Scopes]] 這屬性在 window 可以找到,代表著 global 的作用域

function scope vs block scope

顧名思義,function scope 就是在 function 裡面的作用域,block scope 就是在 {} 內的作用域,'var' 跟 'let, const' 的不同就是在於前者是 function 後者是 block

IIFE

立即函式會立刻執行當前所在環境作用域

var IIFE = function(name) {
console.log('Welcome ' + name);// welcome hey
return ('Welcome ' + name)
}('hey');
console.log(IIFE) // welcome hey

上述例子在我們建立 function 同時就立即被執行,後面 hey 帶入的是 name 內的參數,意思是 console.log 被執行了,因為立即執行的關係此時 return 也被執行所以 IIFE 被賦值 welcome hey

function statement vs expression

函式陳述式 vs 表達式 ,差別在於有沒有回傳值,當你執行函式時,陳述式執行裡面的語句,而表達式會回傳值

hello() // undefined
name() //'aaaa'
function name(){
console.log('aaaa')
}
var hello = function(){
console.log('aaaa')
}
hello() // 'aaaa'

差別在於說當分配記憶體時,陳述式是完整的 function ,而表達式是會等到呼叫時才創造 function

this

this 常常被考得很複雜,但掌握他創造的邏輯之後其實也會變得很好理解。一個物件被創造的時候,裡面會有個 this 關鍵字並指向這個物件,接下來當執行環境 (function) 被呼叫,this 就會指向呼叫那個執行環境的物件,聽起來有點饒口,來段範例

  1. a 與 b 在 window 被呼叫,所以指向的都是 window
function a(){
console.log(this) //window
}
let b= function(){
console.log(this) //window
}
a()
b()

2. a 是一個被創造的物件所以這裡面有一個 this,當 a 內的執行環境(方法)被呼叫時就會指向這個 a ,有個陷阱是 d 是在 c 被創造時呼叫,此時是window 正在創造這個 c 的執行環境的時刻,所以是 window

let a = {
b: 'cool',
c(){
console.log(this) //a
let d = function(){
console.log(this) //window,c 被創造時呼叫
}
d()
}
}
a.c()

3. new 關鍵字可以假想成將執行函式變成一個物件,所以想當然爾裡面的 this 就變成了 a

function a(){
console.log(this)
}
let b = new a() //a

4. bind, call, apply 都是把物件綁到執行環境上,所以連帶的 this 也會指向被綁的物件上

let a = {
b: 'cool'
}
function c(){
console.log(this)
}
let d = c.bind(a)
d() //a
c.call(a) //a
c.apply(a) //a

5. 箭頭函式會自動幫我們 bind this 至箭頭函式內,所以箭頭函式內的 this 是物件內的 this

let a = {
b: 'cool',
c(){
var d = ()=>{
console.log(this)
}
d() //a
}
}
a.c()

call, apply, bind & currying

call 跟 apply, bind都是把 this 綁至 function,差別在於說 call 跟 apply 兩個是直接執行但後面一個是帶參數陣列一個是直接帶參數,bind 的話是把他在封裝一層 function 變成一個有被綁的 this 的新 function,後面會把原本要帶進舊的 function 的參數直接帶進新的參數

那 Currying 跟 bind 的關係是什麼呢?

function multiply(a, b) {return a*b;}var multipleByTwo = multiply.bind(this, 2);console.log(multipleByTwo(4));var multipleByThree = multiply.bind(this, 3);console.log(multipleByThree(4));

簡單來說,函式的 curry 化就是讓你可以只透過部分的參數呼叫一個 function,它會回傳一個 function 去處理剩下的參數,bind 正好讓你達成這個效果,你每次bind(obj, args)後會回傳新的 function,裡面已經幫你自動填好一個參數了,這在之後 functional programing 會再次提到。

--

--