- 〇、前言
- 一盒至、JavaScript和Java在面向?qū)ο髾C制上的區(qū)別
- 1酗洒、面向?qū)ο缶幊痰奶卣?/li>
- 2、機制差異簡述
- 二枷遂、面向?qū)ο髾C制差異舉例
- 1樱衷、Java版的繼承機制例子
- 2、JavaScript版的派生例子
- 三酒唉、JavaScript的面向?qū)ο蟮幕A(chǔ)知識點
- 四矩桂、結(jié)合代碼和對象圖解讀理解原型鏈
- 五、總結(jié)
〇痪伦、前言
JavaScript是一種面向?qū)ο蟮恼Z言侄榴,但它并沒有采用Java那種基于類
(class-based)的方式來實現(xiàn),而是采用基于原型
(prototype-based)方式----這種方式能提供更靈活的機制网沾,但沒有基于類
方式那么簡潔癞蚕,因而苦澀難懂。
本文嘗試通過一段例子代碼和其對應(yīng)的UML對象圖來闡述JavaScript的面向?qū)ο髾C制辉哥,以讓讀者能快速桦山、深入地理解。
本文前面部分有一些理論知識醋旦,略顯枯燥恒水,你可以先瀏覽一下文中的圖、代碼例子饲齐,以便先有個感性認識寇窑,這里先提前貼一下下文中會出現(xiàn)的圖:
注1:本文假定讀者有一定的JavaScript基礎(chǔ),以及對Java的語法有初步的了解箩张。
注2:ES6(ES2015)引入了class甩骏,但只是語法糖(Syntactic sugar),
所以還是有必要理解JavaScript的基于原型的機制先慷。
注3:本文有時會用JS代替JavaScript饮笛,OO代替Object Oriented(面向?qū)ο螅?
一、JavaScript和Java在面向?qū)ο髾C制上的區(qū)別
ES6(ES2015)引入了class论熙,但只是語法糖
(Syntactic sugar)福青,即是說,核心機制還是基于原型
的脓诡,所以還是有必要理解JavaScript的基于原型的OO機制无午。
下文主要以ES5為基準,所以以“JS中沒有class這樣東西”為論調(diào)祝谚。
一.1宪迟、面向?qū)ο缶幊痰奶卣?/h3>
JS也是一門面向?qū)ο缶幊?/code>的語言,所以也具備這有3個OO特征交惯,這里簡單概述一下次泽。(跟本文關(guān)系最密切的是繼承Inheritance)
- 封裝 Encapsulation:把跟一種對象(一個class)相關(guān)的屬性、操作放在一起席爽,便于理解意荤、編程,同時可以指定這些屬性只锻、操作的訪問權(quán)限玖像;
- 繼承 Inheritance:允許一個類繼承另外一個類的屬性、操作齐饮,必要時改寫這些屬性和行為----被繼承的類被稱之為
父類
(superclass)捐寥,繼承父類的類被稱為子類
(subclass); - 多態(tài) Polymorphism:允許在某個API或某段代碼中指定使用某個類的instances沈矿,而在運行態(tài)時上真,可以傳遞這個類的子類的instances給這個API或這段代碼。
一.2羹膳、機制差異簡述
要快速睡互、準確地理解原型鏈,最好的辦法是通過Java的OO機制對比著來理解陵像。兩者主要差別有:
- Java的OO機制是
基于類
(class-based)的,而JS是基于原型
(prototype-based)的醒颖; - 具體來說妻怎,就是Java中分別有
類
(class)和實例
(instance)的區(qū)分泞歉;而JS中沒有class這個概念匿辩,與class類似的是prototype(原型),而與instance類似的是object(對象)铲球; - Java通過class與class之間的繼承來實現(xiàn)繼承(Inheritance),比較簡潔晰赞、直觀;而JS是通過
構(gòu)造函數(shù)
(constructor)和其對應(yīng)的原型
(prototype)掖鱼,以及原型鏈
(prototype chain)來實現(xiàn)的,比較復雜戏挡。(下文有圖為證)
二、面向?qū)ο髾C制差異舉例
這部分先給出Java的例子增拥,然后用JS實現(xiàn)同樣的功能啄巧,以此展示JS的OO機制的特點掌栅、復雜性秩仆。
Java的OO機制在語法上太簡單了,以至于哪怕你不懂Java猾封,但只要你對JavaScript的OO機制有初步的了解澄耍,也能讀懂下面的Java例子。
二.1晌缘、Java版的繼承機制例子
直接上代碼吧齐莲,請留意代碼中的注釋,以及注釋中通過“->”標識的輸出結(jié)果磷箕。
class InheritanceDemo {
class Clock {
protected String mTimezone; // 實例屬性
public Clock(String timezone) { // 構(gòu)造函數(shù)
mTimezone = timezone;
}
public void greeting() { // 實例方法
System.out.println("A clock - " + mTimezone);
}
}
class PrettyClock extends Clock { // 類的派生
protected String mColor;
public PrettyClock(String timezone, String color) {
super(timezone); // 調(diào)用父類的構(gòu)造函數(shù)
mColor = color;
}
@Override
public void greeting() { // 重寫(override)父類的一個方法
System.out.println("A clock - " + mTimezone + " + "+ mColor);
}
}
public void demo() {
Clock clock1 = new Clock("London");
Clock clock2 = new Clock("Shanghai");
Clock clock3 = new PrettyClock("Chongqing", "RED");
clock1.greeting(); // -> "A clock - London"
clock2.greeting(); // -> "A clock - Shanghai"
clock3.greeting(); // -> "A clock - Chongqing + RED"
}
public static void main(String[] args) {
(new InheritanceDemo()).demo();
}
}
上述的Clock和PrettyClock的繼承關(guān)系选酗,用UML類圖方式表達如下:
注:Clock類沒有指明父類,默認派生于Object這個class岳枷。
二.2芒填、JavaScript版的派生例子
在ES6/ES2015添加class這個語法糖之前,要實現(xiàn)繼承則比較復雜空繁,下面是其中一種方法(這段JS代碼實現(xiàn)跟上面的Java代碼段一樣的功能).
注:這段代碼在Node.js或者Chrome的console中運行通過殿衰。
function Clock(timezone) { // 構(gòu)造函數(shù)
this.timezone = timezone; // 添加實例屬性
}
// JS默認就為構(gòu)造函數(shù)添加了一個prototype屬性
// 為Clock類(通過其prototype屬性)添加實例方法
Clock.prototype.greeting = function() {
console.log("A clock - " + this.timezone);
}
// 定義子類 - 此為子類的構(gòu)造函數(shù)
function PrettyClock(timezone, color) {
Clock.call(this, timezone); // 調(diào)用父類的constructor
this.color = color; // 添加實例屬性
}
// 通過操作子類構(gòu)造函數(shù)的prototype來使其成為子類
PrettyClock.prototype = Object.create(Clock.prototype);
PrettyClock.prototype.constructor = PrettyClock; // 手工添加
PrettyClock.prototype.greeting = function() {
console.log("A clock - " + this.timezone + " + " + this.color);
}
function demo() {
var clock1 = new Clock("London");
var clock2 = new Clock("Shanghai");
var clock3 = new PrettyClock("Chongqing", "RED");
clock1.greeting(); // -> "A clock - London"
clock2.greeting(); // -> "A clock - Shanghai"
clock3.greeting(); // -> "A clock - Chongqing + RED"
}
demo();
對比Java和JS兩個版本的繼承例子代碼,可以看出JS的OO語法遠沒有Java的那么簡潔盛泡、直觀 ---- 這點差異其實不算什么了闷祥,難點在于后面要介紹的原型鏈
機制,這難點可以在這個例子中的JS的OO機制(原型鏈)的對象圖
(object diagram)中看出來:
一時看不懂這張圖傲诵?沒關(guān)系凯砍,因為不是理解能力的問題箱硕,而是JS的OO機制實在太不直觀了。下面需要先補充一些JS知識果覆,然后解釋這幅圖颅痊。
三、JavaScript的面向?qū)ο蟮幕A(chǔ)知識點
這里的幾點知識點很重要局待,建議先好好理解一下,必要時多看幾遍再讀下一部分菱属。
因為它們之間關(guān)系比較緊密,所以就按這種方式來排版了纽门,請理解一下。
- JS中赏陵,對象(object)是指一組
屬性
(property,每個屬性都是一對key-value)的集合蝙搔,例如{x: 11, y: 22}
;實際上吃型,- 一個函數(shù)也是一個object,所以
-
(function() {}) instanceof Object // -> true
勤晚;
-
- 連Object、Function赐写、String等這些關(guān)鍵字對應(yīng)的東西都是objects:
Object instanceof Object // -> true
Function instanceof Object // -> true
String instanceof Object // -> true
- 除了Boolean, Number, String, Null, Undefined, Symbol(ES6/ES2015中加入)類型的值外,其它的都是Object類型的值(即都是objects)挺邀;所以:
-
123 instanceof Object // -> false
-- 數(shù)值123是原始值(Primitive value)而不是對象揉忘; -
"hello" instanceof Object // -> false
-- 字符串也是原始值(Primitive value)癌淮; -
"hello".length // -> 5
-- 字符串用起來像對象,原因是JS隱性地使用了包裹對象
(Wrapper objects乳蓄,詳情)來封裝夕膀; -
"hello".toUpperCase() // -> "HELLO"
-- 同上美侦;
-
- 一個函數(shù)也是一個object,所以
- JS的objects可以被看作是Java中的Map的instances魂奥,并且JS支持用
對象直接量
(Object literal)來創(chuàng)建對象。這一點需要記住耻煤,因為下文很多例子用到這個語法:-
var o1 = {}; o1.age = 18;
- 創(chuàng)建一個object,并添加一個名為age的屬性哈蝇; -
var o2 = {name: "Alan", age: 18}
- 創(chuàng)建了一個object,里面有兩個屬性怜跑; -
({name:"Alan", age:18}).age === 18 // -> true
- 創(chuàng)建了一個object并訪問其中一個屬性;
-
- JS的OO機制中沒有Java的class這東西(ES6/ES2015的class只是語法糖)性芬,JS的OO機制是通過
原型
(Prototype)來實現(xiàn)的剧防,而你看到的Object, Function, String等,其實并不是class(雖然它們是大寫字母開頭的诵姜,而Java要求類名以大寫字母開頭),而是構(gòu)造函數(shù)
(Constructor)棚唆,因為函數(shù)也是對象;typeof Object // -> "function"
typeof String // -> "function"
- JS為每個function都默認添加了一個prototype屬性鞋囊;
- 這個屬性的值是個對象:
typeof (function() {}).prototype // -> "object"
- 默認情況下,每個function的prototype值都是不一樣的:
(function() {}).prototype == (function() {}).prototype // -> false
- 所以溜腐,語法上每個function都可以被當作構(gòu)造函數(shù)來使用挺益,例如:
-
new (function(x) {this.v = x*2})(3) // -> {v: 6}
- 上一句定義了一個匿名函數(shù)乘寒,用它作為構(gòu)造函數(shù),參數(shù)是3烂翰,創(chuàng)建了一個object,里面有一個屬性(key="v", value=6)甘耿;
-
new (function(){}) // -> {}
-- 創(chuàng)建了一個空object -
new (function(x) {x = 2})(3) // -> {}
-- 空object,因為沒有通過this往object里添加屬性捏境;
-
- 這個屬性的值是個對象:
- 除了Object這個object等少數(shù)幾個objects外,JS里每個object都有它的原型典蝌,你可以用
Object.getPrototypeOf()
這種標準方式來獲取头谜,或者使用__proto__
這個非標準屬性來獲取(IE的JS引擎中的objects就沒有__proto__
這個屬性):Object.getPrototypeOf({}) === Object.prototype // -> true
({}).__proto__ === Object.prototype // -> true
Object.getPrototypeOf(function (){}) === Function.prototype // -> true
- 當一個function跟著new操作符時柱告,它就被用作
構(gòu)造函數(shù)
來使用了笑陈,例如:var clock3 = new PrettyClock("Chongqing", "RED")
- 上面一句的new操作符實際上做了4件事情:
- A. 創(chuàng)建一個空的臨時object,類似于
var tmp = {};
涵妥; - B. 記錄臨時object與PrettyClock的關(guān)系(原型關(guān)系),類似于
tmp.__proto__ = PrettyClock.prototype
窒所;- 注:這一點是瀏覽器、JS引擎的內(nèi)部機制吵取,Chrome和Node.js中有
__proto__
,但IE中沒有皮官;
- 注:這一點是瀏覽器、JS引擎的內(nèi)部機制吵取,Chrome和Node.js中有
- C. 以
"Chongqing", "RED"
為參數(shù)实辑,調(diào)用PrettyClock這個構(gòu)造函數(shù)(Constructor),在PrettyClock的body內(nèi)部時this
指向那個新創(chuàng)建的臨時空對象(那個tmp)剪撬;- 這里值得強調(diào)的是:
構(gòu)造函數(shù)
中的this.timezone = timezone;
一類賦值語句,其實是在那個臨時object中添加或者修改實例屬性
-- 這跟Java的機制不一樣,Java是在class內(nèi)部定義了實例屬性(必要時可以同時賦初始值)问慎,而在(Java的)構(gòu)造函數(shù)中挤茄,最多是修改實例屬性值,無法添加屬性或者修改屬性的名字或者類型穷劈。
- 這里值得強調(diào)的是:
- D. PrettyClock函數(shù)結(jié)束執(zhí)行后猫妙,返回臨時object人乓,上述語句中就賦給了clock3乌妒。
- A. 創(chuàng)建一個空的臨時object,類似于
- 當訪問一個object的屬性(普通屬性或者方法)時追葡,步驟如下:
- 如果本object中有這個屬性,就返回它的值宜肉,訪問結(jié)束;
- 如果本object中沒有這個屬性翎碑,就往該object的
__proto__
屬性指向的object(prototype object)里找,如果找到日杈,就返回它的值遣铝,訪問結(jié)束莉擒; - 如果還沒找到,就不斷重復上一步驟的動作啰劲,直到?jīng)]有上級prototype梁沧,此時返回undefined這個值蝇裤;
- 實際運行中,是一直找到Object.prototype這個object的栓辜,因為它的
__proto__
等于null,所以不會再繼續(xù)找了藕甩。相關(guān)信息如下:typeof Object.prototype // -> "object"
Object.prototype.__proto__ === null // -> true
- 實際運行中,是一直找到Object.prototype這個object的栓辜,因為它的
- 上一點提到的尋找object的某個屬性的過程周荐,就是
原型鏈
的工作機制僵娃,它的運行效果跟Java中“在類繼承樹上一直往上找,直到Object類中都找不到為止”差不多默怨。
四、結(jié)合代碼和對象圖解讀理解原型鏈
要理解JS的OO機制匙睹,需要先理解原型鏈
(prototype chain);要理解原型鏈霎槐,則需要理解構(gòu)造函數(shù)
(constructor)、原型對象
(prototype object)丘跌、普通對象(object)之間的關(guān)系,說明如下碍岔。
為了避免你來回滾動網(wǎng)頁朵夏,這里把上面的對象圖再貼一次:
- 圖中每個“大框”(2到4個小方框的集合)都是一個object榆纽,大框中的每個小框(除了最上面的一個)都是一個
屬性
(Property),圖中列舉的這些屬性饥侵,它們的值就是箭頭指向的那個object。其中躏升,- 左邊一列(淺藍色)的objects都是
構(gòu)造函數(shù)
(Constructor),它們都是functions(概念上膨疏,可以認為Function是Object的子類)钻弄,從圖中它們的__proto__
屬性都指向Function.prototype
(紅色的線)就可以看出來; - 中間一列(淺綠色)的objects充當
原型
(Prototype)的角色窘俺,它們本身是objects,這從它們的__proto__
屬性指向Object.prototype
(青色的線)可以看得出來;- 充當prototype的objects都需要有一個
constructor
屬性(留意綠色的線)育八,指向其對應(yīng)的構(gòu)造函數(shù),這個屬性在你手工構(gòu)造prototype時髓棋,則需要自行顯式添加;
- 充當prototype的objects都需要有一個
- 右邊一列(淺橙色)的objects就是普通對象了深纲,它們本身沒有
constructor
或者prototype
屬性,但有JS引擎內(nèi)部為它們添加的__proto__
屬性來指向?qū)?yīng)的prototype對象湃鹊;- 在
var clock3 = new PrettyClock("Chongqing", "RED")
時,new
操作符先創(chuàng)建一個臨時對象怀愧,然后把PrettyClock的prototype
屬性的值(就是圖中淺綠色的PrettyClock.prototype
這個object的引用)添加到這個臨時對象中(作為一個內(nèi)部屬性,名為__proto__
)芯义,最后將臨時對象賦給變量clock3。
- 在
- 左邊一列(淺藍色)的objects都是
- JS的
原型鏈
就是由圖中青色的線+藍色的線所構(gòu)成的扛拨,其運作機制需要用幾個例子來說明:-
clock3.greeting(); // -> "A clock - Chongqing + RED"
- clock3對象本身沒有名為"greeting"的屬性举塔,所以到其
__proto__
指向的object即PrettyClock.prototype
里找,結(jié)果有央渣,所以就調(diào)用;clock3.hasOwnProperty("greeting") // -> false
clock3.__proto__.hasOwnProperty("greeting") // -> true
- 調(diào)用時這個greeting函數(shù)時芽丹,關(guān)鍵字
this
指向的是clock3這個對象,這個對象有timezone
和color
兩個屬性值咕村,所以打印結(jié)果"A clock - Chongqing + RED";- clock3的這兩個屬性值培廓,是在用
new
調(diào)用PrettyClock這個構(gòu)造函數(shù)(圖中淺藍色的PrettyClock)時被添加進去的,其中timezone
這個屬性是在(PrettyClock直接地)調(diào)用Clock這個構(gòu)造函數(shù)添加的 ---- 這一點很重要肩钠,請好好理解一下;
- clock3的這兩個屬性值培廓,是在用
- clock3對象本身沒有名為"greeting"的屬性举塔,所以到其
-
clock3.toString(); // -> "[object Object]"
- clock3本身沒有"toString"這個屬性(例子中沒有添加這個屬性)价匠,估往
PrettyClock.prototype
找(藍線),也沒踩窖,再往Clock.prototype
找(青色虛線),還是沒有洋腮,再往Object.prototype
找(青色線),就找到了啥供。 - 這里需要指出的是,你Chrome的console中能打印出一個object的一個屬性值伙狐,并不代表這個這個屬性值在這個object中,這需要用
hasOwnProperty()
來檢查贷屎,例子如下:-
clock3.toString != null // -> true
- 能讀到這個屬性 -
clock3.hasOwnProperty("toString") // -> false
- 不直接擁有 -
clock3.__proto__.__proto__.__proto__.hasOwnProperty('toString') // -> true
- 終于找到了
-
clock3.__proto__.__proto__.__proto__ === Object.prototype // -> true
- 這一點,結(jié)合圖中的線來理解一下吧咒吐;
-
clock3.__proto__.__proto__ === Clock.prototype // -> true
- 如果上面一點理解了,這一點就不在話下了渤滞。
-
- clock3本身沒有"toString"這個屬性(例子中沒有添加這個屬性)价匠,估往
-
Object.prototype.__proto__ === null // -> true
-
Object.prototype
這個object是原型鏈的最頂端了(就如Java中Object這個class是最頂端的class一樣),所以它沒有原型了,所以它的__proto__
這個屬性的值為null(注意:不是undefined陶舞,至于null與undefined的區(qū)別,請自行搜索一下)肿孵; - 這一點的嚴謹寫法是:
Object.getPrototypeOf(Object.prototype) === null // -> true
- 再重復一遍:Chrome、Node.js中每個object都有
__proto__
這個屬性停做,但IE中就沒有,因為它不是JavaScript的規(guī)范的中定義的蛉腌,Object.getPrototypeOf()
才是標準用法只厘。
-
-
寫到這里就差不多了舅巷。??
五、總結(jié)
JS的OO機制钠右、原型鏈不容易理解,原因有這幾點:
- JS的OO機制本來就復雜(沒有Java的那么簡潔)飒房;
- “對象”、“屬性”狠毯、“原型”這些詞的含義比較模糊,帶來了理解上的困難垃你;
- Object、Function皆刺、String等關(guān)鍵字其實是函數(shù),它們充當著“某個class的門面”的角色羡蛾,但它們并不直接是原型鏈的一部分,因為那些
實例方法
(instance method)不是放在它們里面痴怨,而是放在它們對應(yīng)的原型對象里面。