在〈定義類別〉中談到,ToyLang 中的方法,本質上就是個函式,事實上對函式來說,類別不過是個…呃…類似名稱空間般東西,這意謂著,方法也可以指定給變數。例如:
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
的值。
在 ToyLang 中,支援物件個體化(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
,其中的值會依序被指定為函式上參數的值。
當方法被指定給變數時,是否綁定 this
,主要看你的實作而定,Python 就會綁定,然而,JavaScript 不會,而 ToyLang 的作法,顯然就是學 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
的地方。