面向?qū)ο蟪绦蛟O(shè)計(英語:Object-oriented programming砍艾,縮寫:OOP)是種具有對象概念的程序編程范型茧泪,同時也是一種程序開發(fā)的方法清笨。它可能包含數(shù)據(jù)业稼、屬性、代碼與方法横漏。對象則指的是類的實例谨设。它將對象作為程序的基本單元,將程序和數(shù)據(jù)封裝其中缎浇,以提高軟件的重用性扎拣、靈活性和擴展性,對象里的程序可以訪問及經(jīng)常修改對象相關(guān)連的數(shù)據(jù)素跺。在面向?qū)ο蟪绦蚓幊汤锒叮嬎銠C程序會被設(shè)計成彼此相關(guān)的對象。
面向?qū)ο蟪绦蛟O(shè)計可以看作一種在程序中包含各種獨立而又互相調(diào)用的對象的思想指厌,這與傳統(tǒng)的思想剛好相反:傳統(tǒng)的程序設(shè)計主張將程序看作一系列函數(shù)的集合刊愚,或者直接就是一系列對電腦下達的指令。面向?qū)ο蟪绦蛟O(shè)計中的每一個對象都應(yīng)該能夠接受數(shù)據(jù)踩验、處理數(shù)據(jù)并將數(shù)據(jù)傳達給其它對象鸥诽,因此它們都可以被看作一個小型的“機器”,即對象箕憾。目前已經(jīng)被證實的是牡借,面向?qū)ο蟪绦蛟O(shè)計推廣了程序的靈活性和可維護性,并且在大型項目設(shè)計中廣為應(yīng)用厕九。此外蓖捶,支持者聲稱面向?qū)ο蟪绦蛟O(shè)計要比以往的做法更加便于學(xué)習(xí),因為它能夠讓人們更簡單地設(shè)計并維護程序扁远,使得程序更加便于分析俊鱼、設(shè)計、理解畅买。反對者在某些領(lǐng)域?qū)Υ擞枰苑裾J并闲。
當(dāng)我們提到面向?qū)ο蟮臅r候,它不僅指一種程序設(shè)計方法谷羞。它更多意義上是一種程序開發(fā)方式帝火。在這一方面,我們必須了解更多關(guān)于面向?qū)ο笙到y(tǒng)分析和面向?qū)ο笤O(shè)計(Object Oriented Design湃缎,簡稱OOD)方面的知識犀填。許多流行的編程語言是面向?qū)ο蟮?它們的風(fēng)格就是會透由對象來創(chuàng)出實例。
重要的面向?qū)ο缶幊陶Z言包含 Common Lisp 嗓违、Python 九巡、C++ 、Objective-C 蹂季、Smalltalk 冕广、Delphi 疏日、Java 、Swift 撒汉、C# 沟优、Perl 、Ruby 與 PHP 等睬辐。
來自 維基百科挠阁。
面向?qū)ο蟮膶崿F(xiàn)方式
編程語言對對面向?qū)ο蟮膶崿F(xiàn)主流的有兩種方式:基于類的面向?qū)ο蠛突谠偷拿嫦驅(qū)ο蟆?/p>
不管以什么方式實現(xiàn),都具有面向?qū)ο蟮娜筇卣鳎?/p>
封裝性(Encapsulation)
具備封裝性的面向?qū)ο蟪绦蛟O(shè)計隱藏了某一方法的具體運行步驟溉委,取而代之的是通過消息傳遞機制發(fā)送消息給它鹃唯。封裝是通過限制只有特定類的對象可以訪問這一特定類的成員,而它們通常利用接口實現(xiàn)消息的傳入傳出瓣喊。
繼承性(Inheritance)
可以讓某個類型的對象獲得另一個類型的對象的屬性的方法坡慌。
多態(tài)性(Polymorphism)
不同實例的相同方法在不同情形有不同表現(xiàn)形式。多態(tài)機制使具有不同內(nèi)部結(jié)構(gòu)的對象可以共享相同的外部接口藻三。洪橘、
JavaScript 中面向?qū)ο蟮某绦蛟O(shè)計
JavaScript 中沒有類的概念,因此它的對象也與基于類的語言中的對象有所不同棵帽。
ECMA-262 把對象定義為:“無序?qū)傩缘募舷ㄇ螅鋵傩钥梢园局怠ο蠡蛘吆瘮?shù)逗概〉芡恚”嚴(yán)格來講,這就相當(dāng)于說對象是一組沒有特定順序的值逾苫。對象的每個屬性或方法都有一個名字卿城,而每個名字都映射到一個值。正因為這樣(以及其他將要討論的原因)铅搓,我們可以把ECMAScript 的對象想象成散列表:無非就是一組名值對瑟押,其中值可以是數(shù)據(jù)或函數(shù)。
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function(){
alert(this.name);
};
上面的例子創(chuàng)建了一個名為 person
的對象星掰,并為它添加了三個屬性(name
多望、age
和 job
)和一個方法(sayName()
)。其中氢烘,sayName()
方法用于顯示 this.name
(將被解析為 person.name
)的值怀偷。
我們可以直接通過 person.屬性名
的方式去訪問那個變量。
甚至也可以通過 person.屬性名 = 屬性值;
的方式去給對象添加屬性值播玖。
而如果該屬性已存在椎工,則會修改屬性的值。對于對象,我們還可以使用for...in
遍歷對象的屬性晋渺。
早期的 JavaScript 開發(fā)人員經(jīng)常使用這個模式創(chuàng)建新對象。幾年后脓斩,對象字面量成為創(chuàng)建這種對象的首選模式木西。前面的例子用對象字面量語法可以寫成這樣:
var person = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function(){
alert(this.name);
}
};
```
這個例子中的 `person` 對象與前面例子中的 `person` 對象是一樣的,都有相同的屬性和方法随静。這些屬性在創(chuàng)建時都帶有一些**特征值(characteristic)**八千,JavaScript 通過這些特征值來定義它們的行為。
####訪問器屬性
訪問器屬性不包含數(shù)據(jù)值燎猛。它們包含一對 `getter` 和 `setter` 函數(shù)(不過恋捆,這兩個函數(shù)都不是必需的)。
在讀取訪問器屬性時重绷,會調(diào)用 `getter` 函數(shù)沸停,這個函數(shù)負責(zé)返回有效的值;在寫入訪問器屬性時昭卓,會調(diào)用 `setter` 函數(shù)并傳入新值愤钾,這個函數(shù)負責(zé)決定如何處理數(shù)據(jù)。訪問器屬性有如下4 個特性:
* [[Configurable]]:表示能否通過 `delete` 刪除屬性從而重新定義屬性候醒,能否修改屬性的特性能颁,或者能否把屬性修改為數(shù)據(jù)屬性。對于直接在對象上定義的屬性倒淫,這個特性的默認值為 `true` 伙菊。
* [[Enumerable]]:表示能否通過 `for-in` 循環(huán)返回屬性。對于直接在對象上定義的屬性敌土,這個特性的默認值為 `true`镜硕。
* [[Get]]:在讀取屬性時調(diào)用的函數(shù)。默認值為 `undefined`纯赎。
* [[Set]]:在寫入屬性時調(diào)用的函數(shù)谦疾。默認值為 `undefined`。
訪問器屬性不能直接定義犬金,必須使用 `Object.defineProperty()` 來定義念恍。
####創(chuàng)建對象的方式
* 使用 new Object() 創(chuàng)建
* 工廠模式創(chuàng)建
* 構(gòu)造函數(shù)模式創(chuàng)建
####原型模式
我們創(chuàng)建的每個函數(shù)都有一個 `prototype`(原型)屬性,這個屬性是一個指針晚顷,指向一個對象峰伙,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。如果按照字面意思來理解该默,那么 `prototype` 就是通過調(diào)用構(gòu)造函數(shù)而創(chuàng)建的那個對象實例的原型對象瞳氓。使用原型對象的好處是可以讓所有對象實例共享它所包含的屬性和方法。換句話說栓袖,不必在構(gòu)造函數(shù)中定義對象實例的信息匣摘,而是可以將這些信息直接添加到原型對象中店诗。
```
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
```
#####理解原型對象
無論什么時候,只要創(chuàng)建了一個新函數(shù)音榜,就會根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個 `prototype` 屬性庞瘸,這個屬性指向函數(shù)的原型對象。在默認情況下赠叼,所有原型對象都會自動獲得一個 `constructor(構(gòu)造函數(shù))` 屬性擦囊,這個屬性包含一個指向 `prototype` 屬性所在函數(shù)的指針。就拿前面的例子來說嘴办,`Person.prototype. constructor` 指向 `Person` 瞬场。而通過這個構(gòu)造函數(shù),我們還可繼續(xù)為原型對象添加其他屬性和方法涧郊。
創(chuàng)建了自定義的構(gòu)造函數(shù)之后贯被,其原型對象默認只會取得 `constructor` 屬性。至于其他方法妆艘,則都是從 `Object` 繼承而來的刃榨。當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個新實例后,該實例的內(nèi)部將包含一個指針(內(nèi)部屬性)双仍,指向構(gòu)造函數(shù)的原型對象枢希。ECMA-262 第 5 版中管這個指針叫 [[Prototype]]。雖然在腳本中
沒有標(biāo)準(zhǔn)的方式訪問 [[Prototype]]朱沃,但 Firefox苞轿、Safari 和Chrome 在每個對象上都支持一個屬性 `__proto__` 。而在其他實現(xiàn)中逗物,這個屬性對腳本則是完全不可見的搬卒。不過,要明確的真正重要的一點就是翎卓,這個連接存在于實例與構(gòu)造函數(shù)的原型對象之間契邀,而不是存在于實例與構(gòu)造函數(shù)之間。
![展示了各個對象之間的關(guān)系](http://upload-images.jianshu.io/upload_images/2280328-e48d0bae3f1031d6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
#####判斷方法
大家知道失暴,我們用去訪問一個對象的屬性的時候坯门,這個屬性既有可能來自對象本身,也有可能來自這個對象的 [[prototype]] 屬性指向的原型逗扒。
?
那么如何判斷這個對象的來源呢古戴?
?
`hasOwnProperty()` 方法,可以判斷一個屬性是否來自對象本身矩肩。通過`hasOwnProperty()` 這個方法可以判斷一個對象是否在對象本身添加的现恼,但是不能判斷是否存在于原型中,因為有可能這個屬性不存在。也即是說叉袍,在原型中的屬性和不存在的屬性都會返回 `fasle` 始锚。
這個也是唯一的一個處理屬性而不查找原型鏈的方法!
如何判斷一個屬性是否存在于原型中呢喳逛?
`in` 操作符用來判斷一個屬性是否存在于這個對象中疼蛾。但是在查找這個屬性時候,先在對象本身中找艺配,如果對象找不到再去原型中找。換句話說衍慎,只要對象和原型中有一個地方存在這個屬性转唉,就返回 `true` 。
回到前面的問題稳捆,如何判斷一個屬性是否存在于原型中:如果一個屬性存在赠法,但是沒有在對象本身中,則一定存在于原型中乔夯。
```
function Person () {
}
Person.prototype.name = "志玲";
var p1 = new Person();
p1.sex = "女";
//定義一個函數(shù)去判斷原型所在的位置
function propertyLocation(obj, prop){
if(!(prop in obj)){
alert(prop + "屬性不存在");
}else if(obj.hasOwnProperty(prop)){
alert(prop + "屬性存在于對象中");
}else {
alert(prop + "對象存在于原型中");
}
}
propertyLocation(p1, "age");
propertyLocation(p1, "name");
propertyLocation(p1, "sex");
```
###繼承
其實筆者有整理一篇比較完整的繼承的文章砖织,只不過由于最近比較忙沒有整理好。這里先做簡略整理末荐。
繼承是所有的面向?qū)ο蟮恼Z言最重要的特征之一侧纯。大部分的 oop 語言的都支持兩種繼承:接口繼承和實現(xiàn)繼承。比如基于類的編程語言 Java甲脏,對這兩種繼承都支持眶熬。從接口繼承抽象方法 (只有方法簽名),從類中繼承實例方法块请。
?但是對 JavaScript 來說娜氏,沒有類和接口的概念( ES6 之前),所以只支持實現(xiàn)繼承墩新,而且繼承在 **原型鏈** 的基礎(chǔ)上實現(xiàn)的贸弥。等了解過原型鏈的概念之后,你會發(fā)現(xiàn)繼承其實是發(fā)生在對象與對象之間海渊。這是與其他編程語言很大的不同绵疲。
####原型鏈
在 JavaScript 中,將原型鏈作為實現(xiàn)繼承的主要方法臣疑。其基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法最岗。
簡單回顧一下構(gòu)造函數(shù)、原型和實例的關(guān)系:每個構(gòu)造函數(shù)都有一個原型對象朝捆,原型對象都包含一個指向構(gòu)造函數(shù)的指針般渡,而實例都包含一個指向原型對象的內(nèi)部指針。那么,假如我們讓原型對象等于另一個類型的實例驯用,結(jié)果會怎么樣呢脸秽?顯然,此時的原型對象將包含一個指向另一個原型的指針蝴乔,相應(yīng)地记餐,另一個原型中也包含著一個指向另一個構(gòu)造函數(shù)的指針。假如另一個原型又是另一個類型的實例薇正,那么上述關(guān)系依然成立片酝,如此層層遞進,就構(gòu)成了實例與原型的鏈條挖腰。這就是所謂原型鏈的基本概念雕沿。
![示意圖](http://upload-images.jianshu.io/upload_images/2280328-01a0d7efd0e420a5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
#####測試數(shù)據(jù)類型
1. `typeof`:一般用來測試簡單數(shù)據(jù)類型和函數(shù)的類型。如果用來測試對象猴仑,則會一直返回 object审轮,沒有太大意義。
2. `instanceof` : 用來測試一個對象是不是屬于某個類型辽俗。結(jié)果為 `boolean`值疾渣。
3. `isPrototypeOf( 對象 )` : 這是個原型的方法,參數(shù)傳入一個對象崖飘,判斷參數(shù)對象是不是由這個原型派生出來的榴捡。 也就是判斷這個原型是不是參數(shù)對象原型鏈中的一環(huán)。
####借用構(gòu)造函數(shù)調(diào)用繼承
借用構(gòu)造函數(shù)調(diào)用繼承朱浴,又叫偽裝調(diào)用繼承或冒充調(diào)用繼承薄疚。雖然有了繼承兩個字,但是這種方法從本質(zhì)上并沒實現(xiàn)繼承赊琳,只是完成了構(gòu)造方法的調(diào)用而已街夭。
使用 `call` 或 `apply` 這兩個方法完成函數(shù)借調(diào)。這兩個方法的功能是一樣的躏筏,只有少許的區(qū)別板丽。功能都是更改一個構(gòu)造方法內(nèi)部的 `this` 指向到指定的對象上。
代碼示例:
```
function Father (name,age) {
this.name = name;
this.age = age;
}
//如果這樣直接調(diào)用趁尼,那么 father 中的 `this` 只的是 `window` 埃碱。 因為其實這樣調(diào)用的: window.father("李四", 20)
// `name` 和 `age` 屬性就添加到了 `window` 屬性上
Father("李四", 20);
alert("name:" + window.name + "\nage:" + window.age); //可以正確的輸出
//使用call方法調(diào)用,則可以改變this的指向
function Son (name, age, sex) {
this.sex = sex;
//調(diào)用Father方法(看成普通方法)酥泞,第一個參數(shù)傳入一個對象 `this`砚殿,則this(Son類型的對象)就成為了 Father 中的 `this`
Father.call(this, name, age);
}
var son = new Son("張三", 30, "男");
alert("name:" + son.name + "\nage:" + son.age + "\nsex:" + son.sex);
alert(son instanceof Father); //false
```
####組合繼承
```
//定義父類型的構(gòu)造函數(shù)
function Father (name,age) {
// 屬性放在構(gòu)造函數(shù)內(nèi)部
this.name = name;
this.age = age;
// 方法定義在原型中
if((typeof Father.prototype.eat) != "function"){
Father.prototype.eat = function () {
alert(this.name + " 在吃東西");
}
}
}
// 定義子類類型的構(gòu)造函數(shù)
function Son(name, age, sex){
//借調(diào)父類型的構(gòu)造函數(shù),相當(dāng)于把父類型中的屬性添加到了未來的子類型的對象中
Father.call(this, name, age);
this.sex = sex;
}
//修改子類型的原型為父類型的對象芝囤。這樣就可以繼承父類型中的方法了似炎。
Son.prototype = new Father( );
var son1 = new Son("志玲", 30, "女");
alert(son1.name);
alert(son1.sex);
alert(son1.age);
son1.eat();
```
組合函數(shù)利用了原型繼承和構(gòu)造函數(shù)借調(diào)繼承的優(yōu)點辛萍,組合在一起。成為了使用最廣泛的一種繼承方式羡藐。
說明:
1. 組合繼承是我們實際使用中最常用的一種繼承方式贩毕。
2. 可能有個地方有些人會有疑問:Son.prototype = new Father( );這不照樣把父類型的屬性給放在子類型的原型中了嗎,還是會有共享問題呀仆嗦。但是不要忘記了辉阶,我們在子類型的構(gòu)造函數(shù)中借調(diào)了父類型的構(gòu)造函數(shù),也就是說瘩扼,子類型的原型中有的屬性谆甜,都會被子類對象中的屬性給覆蓋掉。就是這樣的集绰。
###作用域和閉包
JavaScript 中的作用域和閉包已經(jīng)發(fā)布规辱,可以移步查閱,這里只做總結(jié)倒慧。
變量的作用域指的是,變量起作用的范圍包券。也就是能訪問到變量的有效范圍纫谅。
**JavaScript 的變量依據(jù)作用域的范圍可以分為:**
######全局變量
定義在函數(shù)外部的變量都是全局變量。
全局變量的作用域是**當(dāng)前文檔**溅固,也就是當(dāng)前文檔所有的 JavaScript 腳本都可以訪問到這個變量付秕。
######局部變量
在函數(shù)內(nèi)聲明的變量,叫局部變量侍郭!表示形參的變量也是局部變量询吴!
局部變量的作用域是局部變量所在的整個函數(shù)的內(nèi)部。 在函數(shù)的外部不能訪問局部變量亮元。
####執(zhí)行環(huán)境
執(zhí)行環(huán)境( execution context )是 JavaScript 中最為重要的一個概念猛计。執(zhí)行環(huán)境定義了變量或函數(shù)有權(quán)訪問的其他數(shù)據(jù),決定了它們各自的行為爆捞。每個執(zhí)行環(huán)境都有一個與之關(guān)聯(lián)的 **變量對象**(variable object)奉瘤,環(huán)境中定義的所有變量和函數(shù)都保存在這個對象中。雖然我們編寫的代碼無法訪問這個對象煮甥,但解析器在處理數(shù)據(jù)時會在后臺使用它盗温。
?全局執(zhí)行環(huán)境是最外圍的一個執(zhí)行環(huán)境。在 Web 瀏覽器中成肘,全局執(zhí)行環(huán)境被認為是 window 對象卖局,因此所有全局變量和函數(shù)都是作為 window 對象的屬性和方法創(chuàng)建的。對全局執(zhí)行環(huán)境變量來說双霍,變量對象就是 window 對象砚偶,對函數(shù)來說批销,變量對象就是這個函數(shù)的 **活動對象** ,活動對象是在函數(shù)調(diào)用時創(chuàng)建的一個內(nèi)部變量蟹演。
?每個函數(shù)都有自己的執(zhí)行環(huán)境风钻,當(dāng)執(zhí)行流進入一個函數(shù)時,函數(shù)的執(zhí)行環(huán)境就會被推入一個執(zhí)行環(huán)境棧中酒请。而在函數(shù)執(zhí)行之后骡技,棧將執(zhí)行結(jié)束的函數(shù)的執(zhí)行環(huán)境彈出,把控制權(quán)返回給之前的執(zhí)行環(huán)境羞反。
#####作用域鏈
**作用域鏈與一個執(zhí)行環(huán)境相關(guān)布朦,作用域鏈用于在標(biāo)示符解析中變量查找。**
?在 JavaScript 中昼窗,函數(shù)也是對象是趴,實際上,JavaScript 里一切都是對象澄惊。函數(shù)對象和其它對象一樣唆途,擁有可以通過代碼訪問的屬性和一系列僅供 JavaScript 引擎訪問的內(nèi)部屬性。其中一個內(nèi)部屬性是 [[Scope]]掸驱,由 ECMA-262 標(biāo)準(zhǔn)第三版定義肛搬,他就指向了這個函數(shù)的作用域鏈。作用域鏈中存儲的是與每個執(zhí)行環(huán)境相關(guān) **變量對象 **(函數(shù)內(nèi)部也是活動對象)毕贼。
?
當(dāng)創(chuàng)建一個函數(shù)( 聲明一個函數(shù) )后温赔,那么會創(chuàng)建這個函數(shù)的作用域鏈。這個函數(shù)的作用域鏈在這個時候只包含一個變量對象( `window` )
####閉包
一句話總結(jié):? **閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)鬼癣。**
詳細請查閱我的文章陶贼。
###其他相關(guān)
想到什么就寫什么。
1. 在 JavaScript 中待秃, `this` 的指向是動態(tài)改變的拜秧,不同的調(diào)用方式,`this` 的執(zhí)行是不同的章郁。調(diào)用一個方法或者函數(shù)的時候腹纳,如果是直接調(diào)用 **方法名()** 則這函數(shù)或方法中的 `this` 指代的就是 `window`。調(diào)用一個方法或者函數(shù)的時候驱犹,使用的是 **對象.方法名()** 則這個函數(shù)或方法中的this指代的就是 **這個對象**當(dāng)做構(gòu)造方法來用嘲恍,使用 `new` 的時候,則 `this` 指代的是將要創(chuàng)建的對象雄驹。
2. ?永遠記椎枧!:只要是在全局作用域聲明的任何變量和函數(shù)默認都是作為 `window` 對象的屬性而存在的.
3. 在 JavaScript 中構(gòu)造函數(shù)和非構(gòu)造函數(shù)沒有本質(zhì)的區(qū)別。**唯一的區(qū)別只是調(diào)用方式的區(qū)別医舆。
* 使用 `new` 就是構(gòu)造函數(shù)俘侠。
* 直接調(diào)用就是非構(gòu)造函數(shù)象缀。
---
>參考書籍:《JavaScript 高級程序設(shè)計(第 3 版)》