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
的值,因此上例中,this
與 acct
參考的是同一實例。
另一個指定 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)
,這就是指定 this
為 instance
的地方。
至於 Function
的 apply
就單純許多了,就純綷是在進行函式呼叫時,將 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
的地方。