This 變數
在該章節之前, 要先提及嚴格模式 strict mode
提供開發者語法嚴格、語法受限的模式 (strict mode)
會影響語法的使用但沒支援受限模式的瀏覽器一樣可以跑, 只是行為有很大的可能會跟你想的不一樣
所以別太依賴受限模式, 除非你做過功能性測試
另外這個模式可以混用在普通模式裡, 你可以利用這個特性慢慢把舊的程式碼轉變成完全嚴謹和低變化性的狀態
嚴格模式通常會直接在腳本的第一行撰寫 use "strict"; 啟動, 嚴格模式可以參考MDN的說明
- 透過拋出錯誤的方式消除一些安靜的錯誤(意指不再靜默地忽略某些錯誤)
- 修正會阻礙 JavaScript 引擎進行最佳化的錯誤: 相同的程式碼在嚴格模式有時候能運行得比非嚴格模式來的快
- 禁止使用一些有可能被未來版本 ECMAScript 定義的語法
在開發WEB應用時, 基本上是一定會加上的, 因為他會使JavaScript的行為更接近現代化的程式語言
關於 This
對於一般的function, 查找this的範圍會從調用者(caller)往上查找:
"use strict";
let obj = {
prop: 300,
fn: function() {
return this.prop;
}
}
function outerFn() {
return this.prop;
}
obj.fn() // 300;
obj.fn2 = outerFn;
obj.fn2() // 300, 因為此時 outerFn 繫結於 obj 的成員位址
outerFn() // Error, this 並沒有 prop 這個成員
this的判斷, 是先依照是不是作為某個物件的屬性或方法被調用
在上述的例子中, 因為 outerFn 是直接以一個 function 被調用, 而不是某個物件底下的方法, 所以 this 的值為 undefined
但經過 obj.fn2 指向 outerFn, 此時 obj.fn2 同樣是調用 outerFn, 但是是以 obj的成員被調用
因此 this 的值相當於 obj
舉例來說:
function fn() {
return this.prop;
}
let obj = {
prop: 100,
foo: fn,
sub: {
prop: 200,
foo: fn
}
}
obj.foo(); // 100, 因為此時是以 "obj" 的成員被調用, this = obj.sub
obj.sub.foo(); // 200, 因為此時是以 "obj.sub" 的成員被調用, this = obj.sub
// 修改obj.sub為
obj.sub = {
foo: fn
}
obj.sub.foo() // undefined, 此時同樣以 "obj.sub" 的成員被調用, 但是 obj.sub 已經不存在 prop 屬性了
obj.sub.__proto__.prop = 300;
obj.sub.foo() // 300, 最直接的引用是 obj.sub, 該物件沒有prop成員, 但是原型鏈(__proto__)存在prop成員
閉包與 arrow function
箭頭函數(arrow function), 也被叫作 "Lambda" 表達式, 其表示法如下
// ex.1
const sum = (a, b) => {
return a + b;
};
// ex.2 當 `=>` 後接的是 expression 時, 可以當作回傳值
const sum = (a, b) => a + b // 行為同 ex.1
// ex.3
const sayHello = (name) => `Hello, ${name}`
// ex.4
const sayHello = name => `Hello, ${name}` // 只有一個參數時, 可以省略()
// ex.5
const returnObj = ( user ) => ({
name: user.first + ' ' + user.last,
age: user.age,
})
// 倘若使用 user => {}, 此時的 {} 會被視作 block statement, 使用 ({}) 則視為 object expression
this對於一般函數來說, this 有幾種可能值:
- 作為 new 建構子來說, this指向物件本身
- 對於strict mode下直接調用函式, 函式中的 this 是
undefined - 作為物件的方法呼叫時, 參考至物件上
而 arrow function () => {} 的行為, 是基於詞法域(lexical), 而非語法語境(context)
function a() {} 以及 let a = () => {} 絕對是不同的東西
在一些前端框架, 如 Reactv15 之前或是 Vue 開發的時候, 官方會強調, 何時一定要使用 function, 而不能用 arrow function 代替
function Person() {
this.age = 1;
setInterval(function growUp(){
this.age++; // 錯誤, 因為 setInterval 的調用不是由 Person 的instance 執行, 是由瀏覽器進行排程
// 通常為 undefined, 非嚴格模式下為 window
});
}
// solution
function Person() {
this.age = 1;
const self = this;
setInterval(function growUp(){
self.age++; // 正確運作, 先用 self 把 this 的位址保存下來, 並透過閉包傳遞
});
}
// solution 2
function Person() {
this.age = 1;
setInterval(() => {
this.age++ // 正確, 因為arrow function 的 this 是基於詞法域的
console.log(this.age)
})
}
要快速釐清 arrow function 與 一般 function 的使用時機時:
使用 function() {} 宣告的時機:
- 在物件中, 方法要參照物件本身
- 在類別中, 宣告成員函式的情景
- 使用到 Generator
function*的情況 - 使用 arguments 的情況
除此之外, 都可以直接使用() => {} Arrow Function 的形式來宣告函式
但原則上來說, 盡可能使用展開運算替代arguments, 因此動態參數的情況, 也可以使用 arrow function
"use strict";
let id = "ID-Global"
let obj = {
id: "ID-Object",
fn1: function () {
console.log(this.id, "in" , this);
},
fn2: () => {
console.log(this.id, "in", this);
},
inter: {
fn1: function () {
console.log(this.id, "in" , this);
},
fn2: () => {
console.log(this.id, "in", this);
}
}
}
obj.fn1(); // 會印出 `ID-Object` in obj 中
obj.fn2(); // 會印出 `undefined` in window 中
obj.inter.fn1(); // 會印出 `undefined` in obj.inter 中
obj.inter.fn2(); // 會印出 `undefined` in window 中
以此例來說, fn1 使用 normal function 進行宣告, 因此 this 的值參照到 obj 上;
fn2 使用 arrow function 宣告, 此處的 obj.fn2 與 obj.inter.fn2 的宣告, 因為使用 Object literal
此時的obj的定義是在 window 物件下;而 obj.fn1 與 obj.inter.fn1 分別參照宣告的物件, 所以分別是 obj 與 obj.inter
"use strict";
let outFn;
new (function() {
this.data = 30;
let fn1 = function () {
console.log("fn1: this is",this);
}
let fn2 = () => {
console.log("fn2: this is", this);
}
fn1();
fn2();
outFn = fn2;
});
outFn();
這個例子中, 揭露了 constructor 與 Object literal 的微妙差異:
此例中, fn1 並不是作為特定物件的方法調用, 因此 fn1 下的 this 為 undefined
反之, fn2 使用 arrow function, 參照的是宣告時的位置, 對應到的部分是匿名函數的內部, 因此該處的 this 會顯示 { data: 30 }
"use strict";
class Example {
constructor() {
this.data = "Example Data";
this.fn1 = function () {
console.log(this);
}
this.fn2 = () => {
console.log(this);
}
}
}
class FunctionTrigger {
constructor() {
this.data = "FunctionTrigger";
}
invoke(fn, extraMessage) {
console.group("Invoke");
console.log(extraMessage);
fn();
console.groupEnd();
}
}
const example = new Example;
const ft = new FunctionTrigger;
example.fn1(); // -- stmt1
example.fn2(); // -- stmt2
ft.invoke(example.fn1, "Try invoke example.fn1"); // -- stmt3
ft.invoke(example.fn2, "Try invoke example.fn2"); // -- stmt4
這又是另外一個例子, 當 fn1 與 fn2 作為 example 的方法調用時, 都會順利顯示 this.data = "Example Data"
但在 stmt3 中, example.fn1 作為函式傳入 ft.invoke 中在調用, 此時失去了 example 這個參照, this 的值會是 undefined
在 stmt4 中, example.fn2 即使作為函式傳入 ft.invoke 中在調用, this依然繫結在 example 上
在 JavaScript, 函式本來就有提供 bind 與 apply 兩種原型方法, 用來繫結this 當下值, 其行為如同上一章節 C++ 的 std::bind
此例中, 可以把 stmt3 改成 ft.invoke(example.fn1.bind(example), "Try invoke example.fn1"); 再次調用觀察
Function.prototype.bind 與 Function.prototype.apply 回傳結果是一個 function
example.fn1.call(example) 則是把 example 當作 this 的值直接調用
arrow function 不可使用 bind, apply, call 三種方法, 因為其上不存在
閉包、語法域與詞法域
通俗的解釋, 語法域代表的是執行期間動態決定的行為, 比方說普通函式的this、建構式的super
而詞法域代表的是封閉範圍的前後文, 如同變數的查找一樣, 舉例來說:
{ // -- block 1
let a = 100;
let b = 200;
{ // -- block 2
let c = 300;
function fn() { // -- block 2.1
console.log(a, b, c);
}
fn() // 100, 200, 300
}
{ // -- block 3
function fn() { // -- block 3.1
console.log(a, b, c);
}
fn() // 錯誤, c 不存於該 block 3.1 以及 block 3
}
}
a, b 在同一個 block, 而 c 在的block可以看到外部(block 1)
所以第一個 block 2 可以看到 a,b,c, 但是第二個僅能看到 a,b
這就是詞法域(其行為依照原始碼的樣子), 比較編譯器領域的說法是:Token 被宣告的位置
而閉包則複雜一點, 以上面的例子來說, 可以觀察出:
- block 允許巢狀
- 內部的 block 可以存取外部的 block
- 外部的 block 不可以存取內部的 block
閉包的概念, 就如透過 function, 把內部的變數帶出去外面:
function closure(initValue) {
let sum = initValue;
return function(num) {
sum += num;
return sum;
}
}
const sigmaFn = closure(0);
sigmaFn(100); // 100
sigmaFn(100); // 200
sigmaFn(100); // 300
首先定義了 closure, 僅允許傳入參數來初始化 sum
並回傳一個 function, 可以傳入數字來遞增 sum
每一次呼叫 sigma, 都會遞增 sum 的值, 這看起來似乎沒有什麼特別, 但是稍微修改一下:
function closure(initValue) {
let sum = initValue;
return {
add: function(num) {
sum += num;
return sum;
}
}
}
const calc = closure(100);
calc.add(20);
calc.add(30);
把回傳值改為物件, 並包含了sum值, 這有兩個很特殊的作用:
- 在回傳的物件中, 不需要用到
this - 無法透過 calc 直接存取 sum, 只能透過
add來存取
在早期沒有arrow function時, 閉包可以很好的處理this的context, 而第二點則至關重要:封閉了sum的直接修改, 僅允許公開的方法存取
這個行為, 就像是把 sum 定義為 private 成員, add 定義為 public 成員, 透過閉包, 模擬出物件導向的行為
現在閉包用法則是比較偏向第二點, 因為this已經有arrow function跟 class 關鍵字協助處理了
通常會使用閉包來移轉內部資料的所有權到外部
另外一個層面是, 閉包可以很好的處理早期 var 宣告的問題(請見 語法速覽), 而UMD模組載入模式, 也會看到閉包的使用