This 變數

在該章節之前, 要先提及嚴格模式 strict mode

Quote

提供開發者語法嚴格、語法受限的模式 (strict mode)

會影響語法的使用但沒支援受限模式的瀏覽器一樣可以跑, 只是行為有很大的可能會跟你想的不一樣

所以別太依賴受限模式, 除非你做過功能性測試

另外這個模式可以混用在普通模式裡, 你可以利用這個特性慢慢把舊的程式碼轉變成完全嚴謹和低變化性的狀態

嚴格模式通常會直接在腳本的第一行撰寫 use "strict"; 啟動, 嚴格模式可以參考MDN的說明

  1. 透過拋出錯誤的方式消除一些安靜的錯誤(意指不再靜默地忽略某些錯誤)
  2. 修正會阻礙 JavaScript 引擎進行最佳化的錯誤: 相同的程式碼在嚴格模式有時候能運行得比非嚴格模式來的快
  3. 禁止使用一些有可能被未來版本 ECMAScript 定義的語法

在開發WEB應用時, 基本上是一定會加上的, 因為他會使JavaScript的行為更接近現代化的程式語言

關於 This

Quote

this 值由被呼叫的函式來決定。它不能在執行期間被指派, 每次函式呼叫調用的值也可能不同

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

Tip

this 的查找優先以上層物件的調用為主

舉例來說:

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成員

Danger

在上述的例中, 通過 proto 屬性直接修改一個物件的原型(Prototype)

但真正開發中, 直接改變物件的原型是一件很不建議的事情, 同時也會影響到所有參照原型的實例

閉包與 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)

Info

更簡潔的說法是, arrow function 不會為自己的 block 繫結 this 的值

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)
  })
}

Note

要快速釐清 arrow function 與 一般 function 的使用時機時:

使用 function() {} 宣告的時機:

  1. 在物件中, 方法要參照物件本身
  2. 在類別中, 宣告成員函式的情景
  3. 使用到 Generator function* 的情況
  4. 使用 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.fn2obj.inter.fn2 的宣告, 因為使用 Object literal

此時的obj的定義是在 window 物件下;而 obj.fn1obj.inter.fn1 分別參照宣告的物件, 所以分別是 objobj.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();

這個例子中, 揭露了 constructorObject 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

Note

在 JavaScript, 函式本來就有提供 bindapply 兩種原型方法, 用來繫結this 當下值, 其行為如同上一章節 C++ 的 std::bind

此例中, 可以把 stmt3 改成 ft.invoke(example.fn1.bind(example), "Try invoke example.fn1"); 再次調用觀察

Function.prototype

Function.prototype.bindFunction.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模組載入模式, 也會看到閉包的使用