重新認識 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是undefinedconsole.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 就會指向呼叫那個執行環境的物件,聽起來有點饒口,來段範例
- 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 會再次提到。