this 是啥?

December 20, 2021

在〈定義類別〉中談到,Toy Lang 中的方法,本質上就是個函式。

Toy 語法

事實上對函式來說,類別不過是個…呃…類似名稱空間般東西,這意謂著,方法也可以指定給變數。例如:

class Account {
    balance = 0 

    def init(number, name) {
        this.number = number
        this.name = name
    }

    def deposit(amount) {
        if amount <= 0 {
            throw new Exception('must be positive')
        }

        this.balance += amount
    }
}

acct = new Account('123', 'Justin')
deposit = acct.deposit
println(deposit) # 顯示 <Function deposit>

既然如此,這就出現一個有趣的問題了,deposit 變數參考的函式中,this 代表誰呢?如果直接呼叫 deposit(100) 會出現錯誤,因為 this 沒有可指定的值!

如果函式中存在 this,想要令它有指定的值,方法之一是使用 . 運算子,這是個二元運算子,左運算元必須是類別實例,右運算元必須是函式呼叫,. 運算子會將左運算元指定為右運算元函式中的 this 的值。

在 Toy Lang 中,支援物件個體化(Object individuation),也就是類別的實例建立之後,還可以動態地增減其特性,不一定只能有類別上規範之行為,因此上面的程式範例,可以進一步地:

def toString() {
    return '{0}, {1}, {2}'.format(this.number, this.name, this.balance)
}

acct.toString = toString
println(acct.toString())  # 顯示 123, Justin, 0

由於 . 運算子會將左運算元指定為右運算元函式中的 this 的值,因此上例中,thisacct 參考的是同一實例。

另一個指定 this 值的方式,是透過函式的 apply 方法,例如可以進一步在上面的範例加上:

deposit.apply(acct, [100])
println(acct.toString())    # 顯示 123, Justin, 100

每個函式都是 Function 類別的實例,而 Function 定義了 apply 方法,第一個參數會是 this 的指定值,第二個參數要是個 List,其中的值會依序被指定為函式上參數的值。

Toy 實作

當方法被指定給變數時,是否綁定 this,主要看你的實作而定,Python 就會綁定,然而,JavaScript 不會,而 Toy Lang 的作法,顯然就是學 JavaScript,就連 apply 也是。

為了模仿 JavaScript 的特性,. 被設計為運算子,acct.deposit(100),實際上可以寫成 acct . deposit(100),也就是方才談到的「左運算元必須是類別實例,右運算元必須是函式呼叫」。

更具體地來看到 . 運算子的實作,這可以在 operator.js 中找到:

class DotOperator {
    constructor(receiver, message) {
        this.receiver = receiver;
        this.message = message;
    }

    evaluate(context) {
        const maybeContext = this.receiver.evaluate(context);
        return maybeContext.notThrown(
            receiver => this.message.send(context, receiver.box(context))
        );
    }
}

receiver 是個類別實例,也就是實作中的 Instance 節點(也就是左運算元),message 會是個函式呼叫(也就是右運算元),也就是 FunCall 節點,依 DotOperator 的定義,在執行時,更具體的說法是,將右運算元作為訊息,傳送給左運算元,這個時候,FunCall 會轉換為 MethodCall,這實現在 callable.js 中:

class FunCall {
    constructor(func, argsList) {
        this.func = func;
        this.argsList = argsList;
    } 

    ...

    send(context, instance) {
        const methodName = this.func.name;
        return new MethodCall(instance, methodName, this.argsList).evaluate(context);
    }
}

class MethodCall {
    constructor(instance, methodName, argsList = []) {
        this.instance = instance;
        this.methodName = methodName;
        this.argsList = argsList;
    }

    evaluate(context) {
        return methodBodyStmt(context, this.instance, this.methodName, this.argsList[0])
                        .evaluate(methodContextFrom(context, this.instance, this.methodName))
                        .notThrown(c => {
                            if(this.argsList.length > 1) {
                                return callChain(context, c.returnedValue.internalNode, this.argsList.slice(1));
                            }
                            return c.returnedValue === null ? Void : c.returnedValue; 
                        });
    }
}

function methodBodyStmt(context, instance, methodName, args = []) {
    const f = instance.hasOwnProperty(methodName) ? 
                    instance.getOwnProperty(methodName).internalNode : 
                    instance.clzNodeOfLang().getMethod(context, methodName);
    const bodyStmt = f.bodyStmt(context, args.map(arg => arg.evaluate(context)));
    return new StmtSequence(
        new VariableAssign(Variable.of('this'), instance),  
        bodyStmt,
        bodyStmt.lineNumber
    );
}

在這邊可以注意到 methodBodyStmt,其中有個 new VariableAssign(Variable.of('this'), instance),這就是指定 thisinstance 的地方。

至於 Functionapply 就單純許多了,就純綷是在進行函式呼叫時,將 this 作為環境物件的變數之一,值則是指定的類別實例,雖然還沒正式介紹如何實作內建類別,不過可以偷看一下 func.js 中的內容:

FunctionClass.methods = new Map([
    ...
    ,
    ['apply', func2('apply', {
        evaluate(context) {
            const funcInstance = self(context);            
            const targetObject = PARAM1.evaluate(context); 
            const args = PARAM2.evaluate(context);         // List instance
            const jsArray = args === Null ? [] : args.nativeValue();
            const bodyStmt = funcInstance.internalNode
                                            .bodyStmt(context, jsArray.map(arg => arg.evaluate(context)));

            return bodyStmt.evaluate(context.assign('this', targetObject));
        }    
    })]
]);

targetObject 會是第一個參數指定的值,bodyStmt.evaluate(context.assign('this', targetObject)) 該行,就是指定 this 的地方。