通常創建對象來表示真實世界中的實體,如用戶和訂單等:
let user = { name: "John", age: 30 };
並且,在現實世界中,用戶可以進行 操作:從購物車中挑選某物、登錄和注銷等。
在 JavaScript 中,行爲(action)由屬性中的函數來表示。
剛開始,我們來教 user
說 hello:
let user = { name: "John", age: 30 }; user.sayHi = function() { alert("Hello!"); }; user.sayHi(); // Hello!
這裏我們使用函數表達式創建了壹個函數,並將其指定給對象的 user.sayHi
屬性。
隨後我們像這樣 user.sayHi()
調用它。用戶現在可以說話了!
作爲對象屬性的函數被稱爲 方法。
所以,在這我們得到了 user
對象的 sayHi
方法。
當然,我們也可以使用預先聲明的函數作爲方法,就像這樣:
let user = { // ... }; // 首先,聲明函數 function sayHi() { alert("Hello!"); } // 然後將其作爲壹個方法添加 user.sayHi = sayHi; user.sayHi(); // Hello!
面向對象編程
當我們在代碼中用對象表示實體時,就是所謂的 面向對象編程,簡稱爲 “OOP”。
OOP 是壹門大學問,本身就是壹門有趣的科學。怎樣選擇合適的實體?如何組織它們之間的交互?這就是架構,有很多關于這方面的書,例如 E. Gamma、R. Helm、R. Johnson 和 J. Vissides 所著的《設計模式:可複用面向對象軟件的基礎》,G. Booch 所著的《面向對象分析與設計》等。
在對象字面量中,有壹種更短的(聲明)方法的語法:
// 這些對象作用壹樣 user = { sayHi: function() { alert("Hello"); } }; // 方法簡寫看起來更好,對吧? let user = { sayHi() { // 與 "sayHi: function(){...}" 壹樣 alert("Hello"); } };
如上所示,我們可以省略 "function"
,只寫 sayHi()
。
說實話,這種表示法還是有些不同。在對象繼承方面有壹些細微的差別(稍後將會介紹),但目前它們並不重要。在幾乎所有的情況下,更短的語法是首選的。
通常,對象方法需要訪問對象中存儲的信息才能完成其工作。
例如,user.sayHi()
中的代碼可能需要用到 user
的 name 屬性。
爲了訪問該對象,方法中可以使用 this
關鍵字。
this
的值就是在點之前的這個對象,即調用該方法的對象。
舉個例子:
let user = { name: "John", age: 30, sayHi() { // "this" 指的是“當前的對象” alert(this.name); } }; user.sayHi(); // John
在這裏 user.sayHi()
執行過程中,this
的值是 user
。
技術上講,也可以在不使用 this
的情況下,通過外部變量名來引用它:
let user = { name: "John", age: 30, sayHi() { alert(user.name); // "user" 替代 "this" } };
……但這樣的代碼是不可靠的。如果我們決定將 user
複制給另壹個變量,例如 admin = user
,並賦另外的值給 user
,那麽它將訪問到錯誤的對象。
下面這個示例證實了這壹點:
let user = { name: "John", age: 30, sayHi() { alert( user.name ); // 導致錯誤 } }; let admin = user; user = null; // 重寫讓其更明顯 admin.sayHi(); // TypeError: Cannot read property 'name' of null
如果我們在 alert
中以 this.name
替換 user.name
,那麽代碼就會正常運行。
在 JavaScript 中,this
關鍵字與其他大多數編程語言中的不同。JavaScript 中的 this
可以用于任何函數,即使它不是對象的方法。
下面這樣的代碼沒有語法錯誤:
function sayHi() { alert( this.name ); }
this
的值是在代碼運行時計算出來的,它取決于代碼上下文。
例如,這裏相同的函數被分配給兩個不同的對象,在調用中有著不同的 “this” 值:
let user = { name: "John" }; let admin = { name: "Admin" }; function sayHi() { alert( this.name ); } // 在兩個對象中使用相同的函數 user.f = sayHi; admin.f = sayHi; // 這兩個調用有不同的 this 值 // 函數內部的 "this" 是“點符號前面”的那個對象 user.f(); // John(this == user) admin.f(); // Admin(this == admin) admin['f'](); // Admin(使用點符號或方括號語法來訪問這個方法,都沒有關系。)
這個規則很簡單:如果 obj.f()
被調用了,則 this
在 f
函數調用期間是 obj
。所以在上面的例子中 this 先是 user
,之後是 admin
。
在沒有對象的情況下調用:this == undefined
我們甚至可以在沒有對象的情況下調用函數:
function sayHi() { alert(this); } sayHi(); // undefined
在這種情況下,嚴格模式下的 this
值爲 undefined
。如果我們嘗試訪問 this.name
,將會報錯。
在非嚴格模式的情況下,this
將會是 全局對象(浏覽器中的 window
,我們稍後會在 全局對象 壹章中學習它)。這是壹個曆史行爲,"use strict"
已經將其修複了。
通常這種調用是程序出錯了。如果在壹個函數內部有 this
,那麽通常意味著它是在對象上下文環境中被調用的。
解除 this
綁定的後果
如果妳經常使用其他的編程語言,那麽妳可能已經習慣了“綁定 this
”的概念,即在對象中定義的方法總是有指向該對象的 this
。
在 JavaScript 中,this
是“自由”的,它的值是在調用時計算出來的,它的值並不取決于方法聲明的位置,而是取決于在“點符號前”的是什麽對象。
在運行時對 this
求值的這個概念既有優點也有缺點。壹方面,函數可以被重用于不同的對象。另壹方面,更大的靈活性造成了更大的出錯的可能。
這裏我們的立場並不是要評判編程語言的這個設計是好是壞。而是要了解怎樣使用它,如何趨利避害。
箭頭函數有些特別:它們沒有自己的 this
。如果我們在這樣的函數中引用 this
,this
值取決于外部“正常的”函數。
舉個例子,這裏的 arrow()
使用的 this
來自于外部的 user.sayHi()
方法:
let user = { firstName: "Ilya", sayHi() { let arrow = () => alert(this.firstName); arrow(); } }; user.sayHi(); // Ilya
這是箭頭函數的壹個特性,當我們並不想要壹個獨立的 this
,反而想從外部上下文中獲取時,它很有用。在後面的 深入理解箭頭函數 壹章中,我們將深入介紹箭頭函數。
存儲在對象屬性中的函數被稱爲“方法”。
方法允許對象進行像 object.doSomething()
這樣的“操作”。
方法可以將對象引用爲 this
。
this
的值是在程序運行時得到的。
壹個函數在聲明時,可能就使用了 this
,但是這個 this
只有在函數被調用時才會有值。
可以在對象之間複制函數。
以“方法”的語法調用函數時:object.method()
,調用過程中的 this
值是 object
。
請注意箭頭函數有些特別:它們沒有 this
。在箭頭函數內部訪問到的 this
都是從外部獲取的。
重要程度: 5
這裏 makeUser
函數返回了壹個對象。
訪問 ref
的結果是什麽?爲什麽?
function makeUser() { return { name: "John", ref: this }; } let user = makeUser(); alert( user.ref.name ); // 結果是什麽?
答案:壹個錯誤。
試壹下:
function makeUser() { return { name: "John", ref: this }; } let user = makeUser(); alert( user.ref.name ); // Error: Cannot read property 'name' of undefined
這是因爲設置 this
的規則不考慮對象定義。只有調用那壹刻才重要。
這裏 makeUser()
中的 this
的值是 undefined
,因爲它是被作爲函數調用的,而不是通過點符號被作爲方法調用。
this
的值是對于整個函數的,代碼段和對象字面量對它都沒有影響。
所以 ref: this
實際上取的是當前函數的 this
。
我們可以重寫這個函數,並返回和上面相同的值爲 undefined
的 this
:
function makeUser(){ return this; // 這次這裏沒有對象字面量 } alert( makeUser().name ); // Error: Cannot read property 'name' of undefined
我們可以看到 alert( makeUser().name )
的結果和前面那個例子中 alert( user.ref.name )
的結果相同。
這裏有個反例:
function makeUser() { return { name: "John", ref() { return this; } }; } let user = makeUser(); alert( user.ref().name ); // John
現在正常了,因爲 user.ref()
是壹個方法。this
的值爲點符號 .
前的這個對象。
重要程度: 5
創建壹個有三個方法的 calculator
對象:
read()
提示輸入兩個值,並將其保存爲對象屬性,屬性名分別爲 a
和 b
。
sum()
返回保存的值的和。
mul()
將保存的值相乘並返回計算結果。
let calculator = { // ……妳的代碼…… }; calculator.read(); alert( calculator.sum() ); alert( calculator.mul() );
運行 demo
打開帶有測試的沙箱。
let calculator = { sum() { return this.a + this.b; }, mul() { return this.a * this.b; }, read() { this.a = +prompt('a?', 0); this.b = +prompt('b?', 0); } }; calculator.read(); alert( calculator.sum() ); alert( calculator.mul() );
使用沙箱的測試功能打開解決方案。
重要程度: 2
有壹個可以上下移動的 ladder
對象:
let ladder = { step: 0, up() { this.step++; }, down() { this.step--; }, showStep: function() { // 顯示當前的 step alert( this.step ); } };
現在,如果我們要按順序執行幾次調用,可以這樣做:
ladder.up(); ladder.up(); ladder.down(); ladder.showStep(); // 1 ladder.down(); ladder.showStep(); // 0
修改 up
,down
和 showStep
的代碼,讓調用可以鏈接,就像這樣:
ladder.up().up().down().showStep().down().showStep(); // 展示 1,然後 0
這種方法在 JavaScript 庫中被廣泛使用。
打開帶有測試的沙箱。
解決方案就是在每次調用後返回這個對象本身。
let ladder = { step: 0, up() { this.step++; return this; }, down() { this.step--; return this; }, showStep() { alert( this.step ); return this; } }; ladder.up().up().down().showStep().down().showStep(); // 展示 1,然後 0
我們也可以每行壹個調用。對于長鏈接它更具可讀性:
ladder .up() .up() .down() .showStep() // 1 .down() .showStep(); // 0
使用沙箱的測試功能打開解決方案。