[day 01] this & bind: 你不能不知道的


※ 本文同步發表於隨性筆記

本系列文章討論JS 物件導向設計相關的特性。 不含CSS,不含HTML
建議先有些JS基礎再繼續閱讀。
你也可以看看從零開始遲來的Web開發筆記
雖然是「7天寫作松」挑戰,但同樣可以視為系列後續文章

No CSS! No HTML! No Browser!
Just need programming language


this

物件導向必不可少的,就是如何引用參考自己。

要是自己的錢包都拿不出來,你要怎買個冰棒?

寫過C++、Java對於this這個關鍵字應該不陌生,雖然JS的this有著很大的不同,但再說明之前,為了來自其他地方的同鞋,容我再多提幾個相對應的例子。


來自Python、Ruby、Rust的朋友
你們可能習慣看到的是self

※ Note: Python可以不使用self;Rust必須顯示宣告self&self;Ruby則比較像是JS是隱含宣告self


來自VB和VB.NET的朋友

你們會看到的是MeMy

<!--more-->

隱含綁定和隱含遺失

隱含綁定

JS不像C++、Java,一開始就以模板物件導向設計(template OOP),這讓他方法的申明和this看起來有些神奇。

任何函式,都可以成為物件的方法

var p1 = "Global"
function method1(){
    console.log(this.p1);
}

var obj = {
    p1: "Object"
};

obj.method1 = method1;
method1();  // => Global
obj.method1(); // => Object

在上例中,隨意將method1函式指定給obj,成為其中一個函數成員。在一開始,this表示環境物件(global),而給定成為物件函式成員後,指向obj。你也可以在建立物件時這樣寫:

obj = {
    p1: "Object",
    method1: function(){
        console.log(this.p1)
    },
    method2(){
        console.log(this.p1)
    },
    "method3": function(){
        console.log(this.p1)
    },
    "nonmethod": ()=>{
        console.log(this.p1)
    }
};
obj.method1(); // => Object
obj.method2(); // => Object
obj.method3(); // => Object
obj.nonmethod(); // => Global

注意

隱含遺失

同樣的,當函式離開一個物件的時候,this所代指的對象也將改變:

foo = obj.method1;
foo() // => Global

所以下面看是正確的程式,有可能和你想的不一樣:

var obj2 = {
    "p1": "I'm obj2",
};

obj2.callObjMethod1 = obj.method1;
obj2.callObjMethod1() // => I'm obj2

別急,接著看下去,看怎樣讓obj2能有obj1的方法。

明確綁定

obj2.callObjMethod1 = obj.method1.bind(obj);
obj2.callObjMethod1(); // => Object

使用bind()1,會明確榜定this,並回傳一個包裹函式回來(Wrapped function)。有點像是:

function wrap(target, method, new_name){
    target[new_name||method.name] = method;
    return function(){
            target[method.name](...arguments)
        };
}
added_method = wrap(obj, method1, "added_method");
added_method(); // => Object
obj.added_method(); // => Object

其他相關的還有call()2apply()3

小節

在沒有明確綁定的情況下,this與誰呼叫的有關,與於程式碼何處無關。這有點像是動態變數(Dynamic variables)/特殊變數(special variables)。不過在JS並沒有明確這個概念。相關概念請參考下方的Common Lisp

與其他語言比較

隱含宣告 vs 明確宣告

在C++、Java和今天在解說的JS,都隱含宣告了一些變數,這些變數遵循著編譯器或直譯器的規則,看起來有夠像魔法。

但是像是在Python,self比須明確宣告,這符合Python的哲學:

Explicit is better than implicit.
(明確比隱含好)

Python

class C1:
    p1 = "I'm class C1"
    def method1(self):
        print(self.p1)

obj = C1()
obj.method1() # => I'm class C1

第一個參數明確為物件實例本身,儘管非self不可,但遵循慣例是好習慣。

Rust

struct C1{
    p1:String,
}

impl C1{
    fn method1(&self){
        println!("{}", self.p1);
    }
}

fn main() {
    let obj = C1{p1:String::from("Hello, World")};
    obj.method1(); // => Hello, World
}

強調安全性的Rust,必須明確宣告self或是&self,而且不能使用其他名字。

Lua

Lua與JS同樣是原形設計的程式語言,不同的是Lua顯示宣告的。

obj = {
    p1 = "Object",
    method1 = function(instance) print(instance.p1) end,
}
obj.method1(obj) -- => Object
obj:method1() -- => Object

除了明確傳入操作物件外,還可以使用語法糖自動帶入第一個參數。

Ruby

class C1
  def initialize
      @p1 = "Hello, World"
  end

  def method1
      puts @p1
  end

  def method2
      self.method1
  end
end

obj = C1.new
obj.method1 # => Hello, World
obj.method2 # => Hello, World

上例中,method2透過self呼叫了自身的method1方法。

Common Lisp

在示範特殊變數(special variables)之前,先來看看全域變數:

(defvar *p1* "Hello, World")
(defun method1 ()
    (format t "~A~&" *p1*))
(method1)  ;; => Hello, World

除了明確修改全域變數內容外,還可以暫時覆蓋全域變數:

(setf *p1* "Hello, New World")
(method1) ;; => Hello, New World

(let ((*p1* "Hello, Local"))
  (method1)) ;; => Hello, Local

(method1) ;; => Hello, New World

這看起來好像很正常?雖然當中的行為可能和你想的不同,但是勉強可以把let裡的內容看成:

(progn
  (let ((tmp *p1*))
      (setf *p1* "Hello, Local")
      (method1)
      (setf *p1* tmp))) ;; # => Hello, Local

不過神奇的來了(看不懂的就略過吧)!

(defun hello-this ()
  (declare (special this))
  (format t "Hello, ~A~&" this))

(let ((this "World"))
  (declare (special this))
  (let ((this "Daniel"))
    (hello-this)))

上面結果會顯示什麼呢?"Hello, World"還是"Hello, Daniel"?實際執行後的結果是前者,只有"World"是特殊變數(special variables),"Daniel"是詞法變數(lexical variables)。目前大多主流的程式語言都是詞法變數,而沒有特殊變數的概念,剛看到JS的時候還以為有這樣的概念,不過最終看來僅是特例而已。

※ JS禁止在任何地方宣告this同名的變數,以區域生存範圍(local scope)暫時覆蓋全域生存範圍(global scope)。這也使得this總是存取到直譯器內定義的內容。

小節: 特殊變數&詞法變數

特殊變數(special variables),因為與其執行的動態環境有關,又稱作動態變數(Dynamic variables)。在Common Lisp,所有用defvardefparameter宣告的,全都隱含著 特殊 的申明。

小後記

this是我認為JS要進入物件導向改念最大的一個門檻。就我看來,他有些行為...真是有點奇葩,需要細心體會。之後的內容應該不會如此長。

參考資料

#js #EMCAScript #javascript





寫了這麼久的JS,你還在物件之前的時代嗎?只有資料、函式可以用,破破的抽象化,不會難以維護?儘管JS起初並不以物件導向設計,但透過原形鏈設計,其仍然可以具有好維護的物件導向特色。本系列從最基礎的this,深入ES6之後的class。

留言討論