在說明JavaScript是一個面向對象的語言以前, 咱們來探討一下面向對象的三大基本特徵: 封裝, 繼承, 多態。javascript
封裝html
把抽象出來的屬性和對方法組合在一塊兒, 且屬性值被保護在內部, 只有經過特定的方法進行改變和讀取稱爲封裝
咱們以代碼舉例, 首先咱們構造一個Person
構造函數, 它有name
和id
兩個屬性, 並有一個sayHi
方法用於打招呼:前端
//定義Person構造函數 function Person(name, id) { this.name = name; this.id = id; } //在Person.prototype中加入方法 Person.prototype.sayHi = function() { console.log('你好, 我是' + this.name); }
如今咱們生成一個實例對象p1
, 並調用sayHi()
方法java
//實例化對象 let p1 = new Person('阿輝', 1234); //調用sayHi方法 p1.sayHi();
在上述的代碼中, p1
這個對象並不知道sayHi()
這個方法是如何實現的, 可是仍然可使用這個方法. 這其實就是封裝. 你也能夠實現對象屬性的私有和公有, 咱們在構造函數中聲明一個salary
做爲私有屬性, 有且只有經過getSalary()
方法查詢到薪資.程序員
function Person(name, id) { this.name = name; this.id = id; let salary = 20000; this.getSalary = function (pwd) { pwd === 123456 ? console.log(salary) : console.log('對不起, 你沒有權限查看密碼'); } }
繼承es6
可讓某個類型的對象得到另外一個類型的對象的屬性和方法稱爲繼承
以剛纔的Person
做爲父類構造器, 咱們來新建一個子類構造器Student
, 這裏咱們使用call()
方法實現繼承web
function Student(name, id, subject) { //使用call實現父類繼承 Person.call(this, name, id); //添加子類的屬性 this.subject = subject; } let s1 = new Student('阿輝', 1234, '前端開發');
多態編程
同一操做做用於不一樣的對象產生不一樣的執行結果, 這稱爲多態
JavaScript中函數沒有重載, 因此JavaScript中的多態是靠函數覆蓋實現的。數組
一樣以剛纔的Person
構造函數爲例, 咱們爲Person
構造函數添加一個study
方法架構
function Person(name, id) { this.name = name; this.id = id; this.study = function() { console.log(name + '在學習'); } }
一樣, 咱們新建一個Student
和Teacher
構造函數, 該構造函數繼承Person
, 並也添加study
方法
function Student(subject) { this.subject = subject; this.study = function() { console.log(this.name + '在學習' + this.subject); } } Student.prototype = new Person('阿輝', 1234); Student.prototype.constructor = Student; function Teacher(subject) { this.subject = subject; this.study = function() { console.log(this.name + '爲了教學而學習' + this.subject); } } Teacher.prototype = new Person("老夫子", 4567); Teacher.prototype.constructor = Teacher;
測試咱們新建一個函數doStudy
function doStudy(role) { if(role instanceof Person) { role.study(); } }
此時咱們分別實例化Student
和Teacher
, 並調用doStudy
方法
let student = new Student('前端開發'); let teacher = new Teacher('前端開發'); doStudy(student); //阿輝在學習前端開發 doStudy(teacher); //老夫子爲了教學在學習前端開發
對於同一函數doStudy
, 因爲參數的不一樣, 致使不一樣的調用結果,這就實現了多態.
JavaScript的面向對象
從上面的分析能夠論證出, JavaScript是一門面向對象的語言, 由於它實現了面向對象的全部特性. 其實, 面向對象僅僅是一個概念或者一個編程思想而已, 它不該該依賴於某個語言存在, 好比Java採用面向對象思想構造其語言, 它實現了類, 繼承, 派生, 多態, 接口等機制. 可是這些機制,只是實現面向對象的一種手段, 而非必須。換言之, 一門語言能夠根據自身特性選擇合適的方式來實現面向對象。 因爲大多數程序員首先學習的是Java, C++等高級編程語言, 於是先入爲主的接受了「類」這個面向對象實際方式,因此習慣性的用類式面嚮對象語言中的概念來判斷該語言是不是面向對象的語言。這也是不少有其餘編程語言經驗的人在學習JavaScript對象時,感受到很困難的地方。
實際上, JavaScript是經過一種叫原型(prototype)的方式來實現面向對象編程的。下面咱們就來討論一下基於類(class-basesd)的面向對象和基於原型(protoype-based)的面向對象這二者的差異。
基於類的面向對象
在基於類的面嚮對象語言中(好比Java和C++), 是構建在類(class)和實例(instance)上的。其中類定義了全部用於具備某一特徵對象的屬性。類是抽象的事物, 而不是其所描述的所有對象中的任何特定的個體。另外一方面, 一個實例是一個類的實例化,是其中的一個成員。
基於原型的面向對象
在基於原型的語言中(如JavaScript)並不存在這種區別:它只有對象!不管是構造函數(constructor),實例(instance),原型(prototype)自己都是對象。基於原型的語言具備所謂的原型對象的概念,新對象能夠從中得到原始的屬性。
因此,在JavaScript中有一個頗有意思的__proto__
屬性(ES6如下是非標準屬性)用於訪問其原型對象, 你會發現,上面提到的構造函數,實例,原型自己都有__proto__
指向原型對象。其最後順着原型鏈都會指向Object
這個構造函數,然而Object
的原型對象的原型是null
,不信, 你能夠嘗試一下Object.prototype.__proto__ === null
爲true
。然而typeof null === 'object'
爲true
。到這裏, 我相信你應該就能明白爲何JavaScript這類基於原型的語言中沒有類和實例的區別, 而是萬物皆對象!
差別總結
基於類的(Java) | 基於原型的(JavaScript) |
---|---|
類和實例是不一樣的事物。 | 全部對象均爲實例。 |
經過類定義來定義類;經過構造器方法來實例化類。 | 經過構造器函數來定義和建立一組對象。 |
經過 new 操做符建立單個對象。 | 相同 |
經過類定義來定義現存類的子類, 從而構建對象的層級結構 | 指定一個對象做爲原型而且與構造函數一塊兒構建對象的層級結構 |
遵循類連接繼承屬性 | 遵循原型鏈繼承屬性 |
類定義指定類的全部實例的全部屬性。沒法在運行時動態添加屬性 | 構造器函數或原型指定初始的屬性集。容許動態地向單個的對象或者整個對象集中添加或移除屬性。 |
*這裏的ES5並不特指ECMAScript 5, 而是表明ECMAScript 6 以前的ECMAScript!
在ES5中建立對象有兩種方式, 第一種是使用對象字面量的方式, 第二種是使用構造函數的方式。該兩種方法在特定的使用場景分別有其優勢和缺點, 下面咱們來分別介紹這兩種建立對象的方式。
咱們經過對象字面量的方式建立兩個student
對象,分別是student1
和student2
。
var student1 = { name: '阿輝', age: 22, subject: '前端開發' }; var student2 = { name: '阿傻', age: 22, subject: '大數據開發' };
上面的代碼就是使用對象字面量的方式建立實例對象, 使用對象字面量的方式在建立單一簡單對象的時候是很是方便的。可是,它也有其缺點:
name
,age
,subject
屬性,寫起來特別的麻煩student1
和student2
之間有什麼聯繫。爲了解決以上兩個問題, JavaScript提供了構造函數建立對象的方式。
構造函數就其實就是一個普通的函數,當對構造函數使用new
進行實例化時,會將其內部this
的指向綁定實例對象上,下面咱們來建立一個Student
構造函數(構造函數約定使用大寫開頭,和普通函數作區分)。
function Student (name, age, subject) { this.name = name; this.age = age; this.subject = subject; console.log(this); }
我特地在構造函數中打印出this
的指向。上面咱們提到,構造函數其實就是一個普通的函數, 那麼咱們使用普通函數的調用方式嘗試調用Student
。
Student('阿輝', 22, '前端開發'); //window{}
採用普通方式調用Student
時, this
的指向是window
。下面使用new
來實例化該構造函數, 生成一個實例對象student1
。
let student1 = new Student('阿輝', 22, '前端開發'); //Student {name: "阿輝", age: 22, subject: "前端開發"}
當咱們採用new
生成實例化對象student1
時, this
再也不指向window
, 而是指向的實例對象自己。這些, 都是new
幫咱們作的。上面的就是採用構造函數的方式生成實例對象的方式, 而且當咱們生成其餘實例對象時,因爲都是採用Student
這個構造函數實例化而來的, 咱們可以清楚的知道各實例對象之間的聯繫。
let student1 = new Student('阿輝', 22, '前端開發'); let student2 = new Student('阿傻', 22, '大數據開發'); let student3 = new Student('阿呆', 22, 'Python'); let student4 = new Student('阿笨', 22, 'Java');
prototype
的原型繼承prototype
是JavaScript這類基於原型繼承的核心, 只要弄明白了原型和原型鏈, 就基本上徹底理解了JavaScript中對象的繼承。下面我將着重的講解爲何要使用prototype
和使用prototype
實現繼承的方式。
爲何要使用prototype
?
咱們給以前的Student
構造函數新增一個study
方法
function Student (name, age, subject) { this.name = name; this.age = age; this.subject = subject; this.study = function() { console.log('我在學習' + this.subject); } }
如今咱們來實例化Student
構造函數, 生成student1
和`student2
, 並分別調用其study
方法。
let student1 = new Student('阿輝', 22, '前端開發'); let student2 = new Student('阿傻', 22, '大數據開發'); student1.study(); //我在學習前端開發 student2.study(); //我在學習大數據開發
這樣生成的實例對象表面上看沒有任何問題, 可是實際上是有很大的性能問題!咱們來看下面一段代碼:
console.log(student1.study === student2.study); //false
其實對於每個實例對象studentx
,其study
方法的函數體是如出一轍的,方法的執行結果只根據其實例對象決定,然而生成的每一個實例都須要生成一個study
方法去佔用一分內存。這樣是很是不經濟的作法。新手可能會認爲, 上面的代碼中也就多生成了一個study
方法, 對於內存的佔用能夠忽略不計。
那麼咱們在MDN中看一下在JavaScript中咱們使用的String
實例對象有多少方法?
上面的方法只是String
實例對象中的一部分方法(我一個屏幕截取不完!), 這也就是爲何咱們的字符串可以使用如此多便利的原生方法的緣由。設想一下, 若是這些方法不是掛載在String.prototype
上, 而是像上面Student
同樣寫在String
構造函數上呢?那麼咱們項目中的每個字符串,都會去生成這幾十種方法去佔用內存,這還沒考慮Math
,Array
,Number
,Object
等對象!
如今咱們應該知道應該將study
方法掛載到Student.prototype
原型對象上纔是正確的寫法,全部的studentx
實例都能繼承該方法。
function Student (name, age, subject) { this.name = name; this.age = age; this.subject = subject; } Student.prototype.study = function() { console.log('我在學習' + this.subject); }
如今咱們實例化student1
和student2
let student1 = new Student('阿輝', 22, '前端開發'); let student2 = new Student('阿傻', 22, '大數據開發'); student1.study(); //我在學習前端開發 student2.study(); //我在學習大數據開發 console.log(student1.study === student2.study); //true
從上面的代碼咱們能夠看出, student1
和student2
的study
方法執行結果沒有發生變化,可是study
自己指向了一個內存地址。這就是爲何咱們要使用prototype
進行掛載方法的緣由。接下來咱們來說解一下如何使用prototype
來實現繼承。
prototype
實現繼承?「學生」這個對象能夠分爲小學生, 中學生和大學生等。咱們如今新建一個小學生的構造函數Pupil
。
function Pupil(school) { this.school = school; }
那麼如何讓Pupil
使用prototype
繼承Student
呢? 其實咱們只要將Pupil
的prototype
指向Student
的一個實例便可。
Pupil.prototype = new Student('小輝', 8, '小學義務教育課程'); Pupil.prototype.constructor = Pupil; let pupil1 = new Pupil('北大附小');
代碼的第一行, 咱們將Pupil
的原型對象(Pupil.prototype
)指向了Student
的實例對象。
Pupil.prototype = new Student('小輝', 8, '小學義務教育課程');
代碼的第二行也許有的讀者會不能理解是什麼意思。
Pupil.prototype.constructor = Pupil;
Pupil
做爲構造函數有一個protoype
屬性指向原型對象Pupil.prototype
,而原型對象Pupil.prototype
也有一個constructor
屬性指回它的構造函數Pupil
。以下圖所示:
然而, 當咱們使用實例化Student
去覆蓋Pupil.prototype後
, 若是沒有第二行代碼的狀況下, Pupil.prototype.constructor
指向了Student
構造函數, 以下圖所示:
並且, pupil1.constructor
會默認調用Pupil.prototype.constructor
, 這個時候pupil1.constructor
指向了Student
:
Pupil.prototype = new Student('小輝', 8, '小學義務教育課程'); let pupil1 = new Pupil('北大附小'); console.log(pupil1.constructor === Student); //true
這明顯是錯誤的, pupil1
明明是用Pupil
構造函數實例化出來的, 怎麼其constructor
指向了Student
構造函數呢。因此, 咱們就須要加入第二行, 修正其錯誤:
Pupil.prototype = new Student('小輝', 8, '小學義務教育課程'); //修正constructor的指向錯誤 Pupil.prototype.constructor = Pupil; let pupil1 = new Pupil('北大附小'); console.log(pupil1.constructor === Student); //false console.log(pupil1.constructor === Pupil); //ture
上面就是咱們的如何使用prototype
實現繼承的例子, 須要特別注意的: 若是替換了prototype對象, 必須手動將prototype.constructor
從新指向其構造函數。
call
和apply
方法實現繼承使用call
和apply
是我我的比較喜歡的繼承方式, 由於只須要一行代碼就能夠實現繼承。可是該方法也有其侷限性,call
和apply
不能繼承原型上的屬性和方法, 下面會有詳細說明。
使用call
實現繼承
一樣對於上面的Student
構造函數, 咱們使用call
實現Pupil
繼承Student
的所有屬性和方法:
//父類構造函數 function Student (name, age, subject) { this.name = name; this.age = age; this.subject = subject; } //子類構造函數 function Pupil(name, age, subject, school) { //使用call實現繼承 Student.call(this, name, age, subject); this.school = school; } //實例化Pupil let pupil2 = new Pupil('小輝', 8, '小學義務教育課程', '北大附小');
須要注意的是, call
和apply
只能繼承本地屬性和方法, 而不能繼承原型上的屬性和方法,以下面的代碼所示, 咱們給Student
掛載study
方法,Pupil
使用call
繼承Student
後, 調用pupil2.study()
會報錯:
//父類構造函數 function Student (name, age, subject) { this.name = name; this.age = age; this.subject = subject; } //原型上掛載study方法 Student.prototype.study = function() { console.log('我在學習' + this.subject); } //子類構造函數 function Pupil(name, age, subject, school) { //使用call實現繼承 Student.call(this, name, age, subject); this.school = school; } let pupil2 = new Pupil('小輝', 8, '小學義務教育課程', '北大附小'); //報錯 pupil2.study(); //Uncaught TypeError: pupil2.study is not a function
使用apply
實現繼承
使用apply
實現繼承的方式和call
相似, 惟一的不一樣只是參數須要使用數組的方法。下面咱們使用apply
來實現上面Pupil
繼承Student
的例子。
//父類構造函數 function Student (name, age, subject) { this.name = name; this.age = age; this.subject = subject; } //子類構造函數 function Pupil(name, age, subject, school) { //使用applay實現繼承 Student.apply(this, [name, age, subject]); this.school = school; } //實例化Pupil let pupil2 = new Pupil('小輝', 8, '小學義務教育課程', '北大附小');
JavaScript中的繼承方式不只僅只有上面提到的幾種方法, 在《JavaScript高級程序設計》中, 還有實例繼承,拷貝繼承,組合繼承,寄生組合繼承等衆多繼承方式。在寄生組合繼承中, 就很好的彌補了call
和apply
沒法繼承原型屬性和方法的缺陷,是最完美的繼承方法。這裏就不詳細的展開論述,感興趣的能夠自行閱讀《JavaScript高級程序設計》。
基於原型的繼承方式,雖然實現了代碼複用,可是行文鬆散且不夠流暢,可閱讀性差,不利於實現擴展和對源代碼進行有效的組織管理。不得不認可,基於類的繼承方式在語言實現上更健壯,且在構建可服用代碼和組織架構程序方面具備明顯的優點。因此,ES6中提供了基於類class
的語法。但class
本質上是ES6提供的一顆語法糖,正如咱們前面提到的,JavaScript是一門基於原型的面嚮對象語言。
咱們使用ES6的class
來建立Student
//定義類 class Student { //構造方法 constructor(name, age, subject) { this.name = name; this.age = age; this.subject = subject; } //類中的方法 study(){ console.log('我在學習' + this.subject); } } //實例化類 let student3 = new Student('阿輝', 24, '前端開發'); student3.study(); //我在學習前端開發
上面的代碼定義了一個Student
類, 能夠看到裏面有一個constructor
方法, 這就是構造方法,而this
關鍵字則表明實例對象。也就是說,ES5中的構造函數Student
, 對應的是E6中Student
類中的constructor
方法。
Student
類除了構造函數方法,還定義了一個study
方法。須要特別注意的是,在ES6中定義類中的方法的時候,前面不須要加上function
關鍵字,直接把函數定義進去就能夠了。另外,方法之間不要用逗號分隔,加了會報錯。並且,類中的方法所有是定義在原型上的,咱們能夠用下面的代碼進行驗證。
console.log(student3.__proto__.study === Student.prototype.study); //true console.log(student3.hasOwnProperty('study')); // false
上面的第一行的代碼中, student3.__proto__
是指向的原型對象,其中Student.prototype
也是指向的原型的對象,結果爲true
就能很好的說明上面的結論: 類中的方法所有是定義在原型上的。第二行代碼是驗證student3
實例中是否有study
方法,結果爲false
, 代表實例中沒有study
方法,這也更好的說明了上面的結論。其實,只要理解了ES5中的構造函數對應的是類中的constructor
方法,就能推斷出上面的結論。
E6中class
能夠經過extends
關鍵字來實現繼承, 這比前面提到的ES5中使用原型鏈來實現繼承, 要清晰和方便不少。下面咱們使用ES6的語法來實現Pupil
。
//子類 class Pupil extends Student{ constructor(name, age, subject, school) { //調用父類的constructor super(name, age, subject); this.school = school; } } let pupil = new Pupil('小輝', 8, '小學義務教育課程', '北大附小'); pupil.study(); //我在學習小學義務教育課程
上面代碼代碼中, 咱們經過了extends
實現Pupil
子類繼承Student
父類。須要特別注意的是,子類必須在constructor
方法中首先調用super
方法,不然實例化時會報錯。這是由於子類沒有本身的this
對象, 而是繼承父類的this
對象,而後對其加工。若是不調用super
方法,子類就得不到this
對象。
JavaScript 被認爲是世界上最受誤解的編程語言,由於它身披 c 語言家族的外衣,表現的倒是 LISP 風格的函數式語言特性;沒有類,卻實也完全實現了面向對象。要對這門語言有透徹的理解,就必須扒開其 c 語言的外衣,重新回到函數式編程的角度,同時摒棄原有類的面向對象概念去學習領悟它(摘自參考目錄1)。如今的前端中不只廣泛的使用了ES6的新語法,並且在JavaScript的基礎上還出現了TypeScript、CoffeeScript這樣的超集。能夠預見的是,目前在前端生態圈一片繁榮的狀況下,對JSer的需求也會愈來愈多,但同時也對前端開發者的JavaScript的水平提出了更加嚴苛的要求。使用面向對象的思想去開發前端項目也是將來對JSer的基本要求之一!