※ 本文同步發表於隨性筆記
本系列文章討論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的朋友
你們會看到的是Me
、My
。
隱含綁定和隱含遺失
隱含綁定
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
進去。(類似的還有arguments) - 並不能使用 箭頭表達是()=>{} 去申明方法,因為他為綁定詞法環境(lexical variables)的
this
。
隱含遺失
同樣的,當函式離開一個物件的時候,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
小節
在沒有明確綁定的情況下,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,所有用defvar
和defparameter
宣告的,全都隱含著 特殊 的申明。
小後記
this
是我認為JS要進入物件導向改念最大的一個門檻。就我看來,他有些行為...真是有點奇葩,需要細心體會。之後的內容應該不會如此長。