前言:面向對象程序設計(Object-oriented programming,簡稱OOP),是一種常見的編程思想。JavaScript 的核心是支持面向對象的,同時它也提供了強大靈活的 OOP 語言能力。本文從對面向對象編程的介紹開始,和您一起探索 JavaScript 的對象模型,最后描述 JavaScript 當中面向對象編程的一些概念。(本文部分文字摘自知乎、MDN、阮一峰JS教程)
1、什么是面向對象
- 比較正經的回答:
把一組數據結構和處理它們的方法組成對象(object),把相同行為的對象歸納為類(class),通過類的封裝(encapsulation)隱藏內部細節,通過繼承(inheritance)實現類的特化(specialization)/泛化(generalization),通過多態(polymorphism)實現基于對象類型的動態分派(dynamic dispatch)。
-
抖機靈的回答:
MDN中的介紹:
面向對象編程是用抽象方式創建基于現實世界模型的一種編程模式。它使用先前建立的范例,包括模塊化,多態和封裝幾種技術。
面向對象編程可以看作是使用一系列對象相互協作的軟件設計。 在 OOP 中,每個對象能夠接收消息,處理數據和發送消息給其他對象。每個對象都可以被看作是一個擁有清晰角色或責任的獨立小機器。
- 可嘗試理解MDN中介紹的面向對象的一些術語
Class 類
定義對象的特征。它是對象的屬性和方法的模板定義.
Object 對象
類的一個實例。
Property 屬性
對象的特征,比如顏色。
Method 方法
對象的能力,比如行走。
Constructor 構造函數
對象初始化的瞬間, 被調用的方法. 通常它的名字與包含它的類一致.
Inheritance 繼承
一個類可以繼承另一個類的特征。
Encapsulation 封裝
一種把數據和相關的方法綁定在一起使用的方法.
Abstraction 抽象
結合復雜的繼承,方法,屬性的對象能夠模擬現實的模型。
Polymorphism 多態
多意為‘許多’,態意為‘形態’。不同類可以定義相同的方法或屬性。
其實面向對象最核心的四個概念就是:抽象、繼承、封裝、多態 。
2、JavaScript中的面向對象編程
首先我們就要來了解一下原型編程,因為JS中實際上是弱類化的編程語言,是基于原型的而不是基于類的編程。
- 基于原型的編程不是面向對象編程中體現的風格,且行為重用(在基于類的語言中也稱為繼承)是通過裝飾它作為原型的現有對象的過程實現的。這種模式也被稱為弱類化,原型化,或基于實例的編程。
- 原始的(也是最典型的)基于原型語言的例子是由大衛·安格爾和蘭德爾·史密斯開發的。然而,弱類化的編程風格近來變得越來越流行,并已被諸如JavaScript,Cecil,NewtonScript,IO,MOO,REBOL,Kevo,Squeak(使用框架操縱Morphic組件),和其他幾種編程語言采用。
完整的內容可看MDN 關于JS 面向對象的介紹。本文僅做基礎的介紹。
3、命名空間
命名空間是一個容器,它允許開發人員在一個獨特的,特定于應用程序的名稱下捆綁所有的功能。 在JavaScript中,命名空間只是另一個包含方法,屬性,對象的對象。
注意:需要認識到重要的一點是:與其他面向對象編程語言不同的是,Javascript中的普通對象和命名空間在語言層面上沒有區別。比如var MySpace = {}
,從語言層面上,可以理解成創建了一個名為MySpace
的對象,也可以理解為一個名為MySpace
的命名空間,到底應該理解成哪一種主要是看你如何使用它。
創造的JavaScript命名空間背后的想法很簡單:一個全局對象被創建,所有的變量,方法和功能成為該對象的屬性。使用命名空間也最大程度地減少應用程序的名稱沖突的可能性。
舉例說明:
我們來創建一個全局變量叫做 MYAPP
var MYAPP = MYAPP || {};
在上面的代碼示例中,我們首先檢查MYAPP
是否已經被定義(是否在同一文件中或在另一文件)。如果是的話,那么使用現有的MYAPP
全局對象,否則,創建一個名為MYAPP
的空對象用來封裝方法,函數,變量和對象。
然后我們就可以創建子命名空間:
MYAPP.event = {};
4、類
JavaScript是一種基于原型的語言,它沒類的聲明語句,比如C+ +或Java中用的。相反,JavaScript可用方法作類。定義一個類跟定義一個函數一樣簡單。在下面的例子中,我們定義了一個新類Person。
function Person() { }
// 或
var Person = function(){ }
對象(類的實例)
我們使用 new obj
創建對象obj
的新實例, 將結果(obj 類型)賦值給一個變量方便稍后調用。舉例說明:
在下面的示例中,我們定義了一個名為Person
的類,然后我們創建了兩個Person
的實例(person1
和 person2
).
function Person() { }
var person1 = new Person();
var person2 = new Person();
5、構造函數
面向對象編程的第一步,就是要生成對象。前面說過,對象是單個實物的抽象。通常需要一個模板,表示某一類實物的共同特征,然后對象根據這個模板生成。
典型的面向對象編程語言(比如 C++ 和 Java),都有“類”(class)這個概念。所謂“類”就是對象的模板,對象就是“類”的實例。但是,JavaScript 語言的對象體系,不是基于“類”的,而是基于構造函數(constructor)和原型鏈(prototype)。
JavaScript 語言使用構造函數(constructor)作為對象的模板。所謂”構造函數”,就是專門用來生成實例對象的函數。它就是對象的模板,描述實例對象的基本結構。一個構造函數,可以生成多個實例對象,這些實例對象都有相同的結構。
構造函數就是一個普通的函數,但是有自己的特征和用法。如:
var Vehicle = function () {
this.price = 1000;
};
上面代碼中,Vehicle
就是構造函數。為了與普通函數區別,構造函數名字的第一個字母通常大寫。
構造函數的特點有兩個:
- 函數體內部使用了
this
關鍵字,代表了所要生成的對象實例。 - 生成對象的時候,必須使用
new
命令。
下面將詳細介紹一下new
命令和this
關鍵字。
6、new
命令
寫之前先放一個鏈接,是方大寫的關于new的文章,私以為能解決很多人關于new的疑惑。
new
命令的作用,就是執行構造函數,返回一個實例對象。如:
var Vehicle = function () {
this.price = 1000;
};
var v = new Vehicle();
v.price // 1000
上面代碼通過new命令,讓構造函數Vehicle生成一個實例對象,保存在變量v中。這個新生成的實例對象,從構造函數Vehicle得到了price屬性。new命令執行時,構造函數內部的this,就代表了新生成的實例對象,this.price表示實例對象有一個price屬性,值是1000。
var v = new Vehicle();
使用new
,JS默默的做了哪些事情呢:
- 創建臨時對象
- 將這個臨時對象賦值給函數內部的
this
關鍵字。 Viehicle.prototype = { constructor: 構造函數 }
臨時對象.__proto__ = Vehicle.prototype
- 執行
Vehicle.apply(this,arguments)
-
return this
即return 創建的臨時對象
7、this
關鍵字
關于this
關鍵字,我之前寫的一篇關于函數的博客初步的介紹過,本文將再次詳細的了解一下這個看起來有點坑的事物。(這里也推薦一篇方大寫的介紹this
的博客 ,下面的內容也借鑒了該博客所說的思路)
- 什么是
this
,最本質的概念是:this
就是你call
一個函數時,傳入的第一個參數。 - 怎么判斷
this
到底指的是什么,將函數的調用形式轉換為 call 形式即可。 - 如果是一些封裝過的函數(比如
onclick()、addEventListener()
等),如何判斷this
- 看源碼中對應的函數是怎么被
call
的(這是最靠譜的辦法) - 看文檔
console.log(this)
- 不要瞎猜,你猜不到的
- 看源碼中對應的函數是怎么被
舉例說明:
- 首先來看如何將普通的函數調用轉換為
call
的形式:
func(p1, p2) // 等價于
func.call(undefined, p1, p2)
obj.child.method(p1, p2) // 等價于
obj.child.method.call(obj.child, p1, p2)
- 下面看幾個例子。
例1:
function func(){
console.log(this)
}
func() // 轉化為下面的句子
func.call(undefined) // 可以簡寫為 func.call()
此時this
即為undefined
,但注意:
如果你的call傳的第一個參數是 null
或者 undefined
,那么 window
對象就是默認的this
(嚴格模式下默認為 undefined
)
- 例2:
var obj = {
foo: function(){
console.log(this)
}
}
obj.foo() // 轉化為下面的語句
obj.foo.call(obj)
本例中的this
即為obj
- 例3:
function fn (){ console.log(this) }
var arr = [fn, fn2]
arr[0]() // 這里面的 this 又是什么呢?
我們可以把 arr[0]( )
想象為arr.0( )
,雖然后者的語法錯了,但是形式與轉換代碼里的 obj.child.method(p1, p2)
對應上了,于是就可以愉快的轉換了:
arr[0]()
假想為 arr.0()
然后轉換為 arr.0.call(arr)
那么里面的 this 就是 arr 了 :)
- 例4:(下面三個例子都是一些常見的經過封裝后的函數的
this
,這些函數就無法轉化成call
的形式了,需要看文檔才能知道,我幫大家看了文檔,所以下面的例子就需要單獨記憶啦)
button.onclick = function(){
console.log(this)
}
// 這里的 this 指的是 觸發事件的元素 即 button
- 例5:
button.addEventListener('click',function(){
console.log(this)
})
// 這里的 this 指的是 觸發事件的元素的引用 即 button
- 例6:(jQuery)
$('ul').on('click','li',function(){
console.log(this)
})
// 這里的 this 指的是 正在執行事件的元素 即 li
- 例7:下面三個例子就比較繞啦
function X() {
return obj = {
name:'obj',
fn1(x) {
x.fn2()
},
fn2() {
console.log(1)
console.log(this) // A
}
}
}
var options = {
name:'options',
fn1() {},
fn2() {
console.log(2)
console.log(this) // B
}
}
var x = X()
x.fn1(options)
問:這段的代碼執行的是A還是B,打印出來的this
指的是什么(不公布答案,自己思考后可在控制臺驗證結果)
- 例8:
function X() {
return obj = {
name:'obj',
fn1(x) {
x.fn2.call(this)
},
fn2() {
console.log(1)
console.log(this) // A
}
}
}
var options = {
name:'options',
fn1() {},
fn2() {
console.log(2)
console.log(this) // B
}
}
var x = X()
x.fn1(options)
問題同上。
- 例9:
function X() {
return obj = {
name: 'obj',
options: null,
fn1(x) {
this.options = x
this.fn2()
},
fn2() {
console.log(1)
this.options.fn2.call(this) // A
}
}
}
var options = {
name: 'options',
fn1() {},
fn2() {
console.log(2)
console.log(this) // B
}
}
var x = X()
x.fn1(options)
問題同上。
8、做幾個小練習吧
- 補全下面的代碼:
function Human(options){
} // 構造函數結束
Human.prototype.______ = ___________
Human.prototype.______ = ___________
Human.prototype.______ = ___________
var human = new Human({name:'Frank', city: 'Hangzhou'})
var human2 = new Human({name:'Jack', city: 'Hangzhou'})
- 補全代碼,使得 human 對象滿足以下條件:
-
human
這個對象本身具有屬性name
和city
-
human.__proto__
對應的對象(也就是原型)具有物種(species)、走(walk)和使用工具(useTools)這幾個屬性 -
human.__proto__.constructor === Human
為 true
-
human2 和 human 類似。
- 參考答案:
function Human(options){
this.name = options.name
this.city = options.city
} // 構造函數結束
Human.prototype.species = 'Human'
Human.prototype.walk = function(){}
Human.prototype.useTools = function(){}
var human = new Human({name:'Frank', city: 'Hangzhou'})
var human2 = new Human({name:'Jack', city: 'Hangzhou'})
- 填空(本題不給答案,不確定的可以直接在控制臺測試):
var object = {}
object.__proto__ === ????填空1???? // 為 true
var fn = function(){}
fn.__proto__ === ????填空2???? // 為 true
fn.__proto__.__proto__ === ????填空3???? // 為 true
var array = []
array.__proto__ === ????填空4???? // 為 true
array.__proto__.__proto__ === ????填空5???? // 為 true
Function.__proto__ === ????填空6???? // 為 true
Array.__proto__ === ????填空7???? // 為 true
Object.__proto__ === ????填空8???? // 為 true
true.__proto__ === ????填空9???? // 為 true
Function.prototype.__proto__ === ????填空10???? // 為 true
- 在 ES5 中如何用函數模擬一個類?
- 參考答案:
ES 5 沒有 class 關鍵字,所以只能使用函數來模擬類。代碼如下:function Human(name){ this.name = name } Human.prototype.run = function(){} var person = new Human('enoch')