
一. 重新認識面向?qū)ο?/strong>
1. JavaScript是一門面向?qū)ο蟮恼Z言
在說明JavaScript是一個面向?qū)ο蟮恼Z言之前, 我們來探討一下面向?qū)ο蟮娜蠡咎卣鳎?strong>封裝,繼承,多態(tài)。
封裝
把抽象出來的屬性和對方法組合在一起,且屬性值被保護在內(nèi)部,只有通過特定的方法進行改變和讀取稱為封裝。
我們以代碼舉例, 首先我們構(gòu)造一個Person構(gòu)造函數(shù), 它有name和id兩個屬性,并有一個sayHi方法用于打招呼:
- //定義Person構(gòu)造函數(shù)
 - function Person(name, id) {
 - this.name = name;
 - this.id = id;
 - }
 - //在Person.prototype中加入方法
 - Person.prototype.sayHi = function() {
 - console.log('你好, 我是' + this.name);
 - }
 
現(xiàn)在我們生成一個實例對象p1,并調(diào)用sayHi()方法
- //實例化對象
 - let p1 = new Person('阿輝', 1234);
 - //調(diào)用sayHi方法
 - p1.sayHi();
 
在上述的代碼中, p1這個對象并不知道sayHi()這個方法是如何實現(xiàn)的,但是仍然可以使用這個方法。 這其實就是封裝。你也可以實現(xiàn)對象屬性的私有和公有,我們在構(gòu)造函數(shù)中聲明一個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('對不起, 你沒有權(quán)限查看密碼');
 - }
 - }
 
繼承
可以讓某個類型的對象獲得另一個類型的對象的屬性和方法稱為繼承。
以剛才的Person作為父類構(gòu)造器,我們來新建一個子類構(gòu)造器Student,這里我們使用call()方法實現(xiàn)繼承
- function Student(name, id, subject) {
 - //使用call實現(xiàn)父類繼承
 - Person.call(this, name, id);
 - //添加子類的屬性
 - this.subject = subject;
 - }
 - let s1 = new Student('阿輝', 1234, '前端開發(fā)');
 
多態(tài)
同一操作作用于不同的對象產(chǎn)生不同的執(zhí)行結(jié)果,這稱為多態(tài)
JavaScript中函數(shù)沒有重載, 所以JavaScript中的多態(tài)是靠函數(shù)覆蓋實現(xiàn)的。
同樣以剛才的Person構(gòu)造函數(shù)為例,我們?yōu)镻erson構(gòu)造函數(shù)添加一個study方法
- function Person(name, id) {
 - this.name = name;
 - this.id = id;
 - this.study = function() {
 - console.log(name + '在學習');
 - }
 - }
 
同樣, 我們新建一個Student和Teacher構(gòu)造函數(shù),該構(gòu)造函數(shù)繼承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;
 
測試我們新建一個函數(shù)doStudy
- function doStudy(role) {
 - if(role instanceof Person) {
 - role.study();
 - }
 - }
 
此時我們分別實例化Student和Teacher,并調(diào)用doStudy方法
- let student = new Student('前端開發(fā)');
 - let teacher = new Teacher('前端開發(fā)');
 - doStudy(student); //阿輝在學習前端開發(fā)
 - doStudy(teacher); //老夫子為了教學在學習前端開發(fā)
 
對于同一函數(shù)doStudy, 由于參數(shù)的不同, 導致不同的調(diào)用結(jié)果,這就實現(xiàn)了多態(tài)。
JavaScript的面向?qū)ο?/strong>
從上面的分析可以論證出, JavaScript是一門面向?qū)ο蟮恼Z言, 因為它實現(xiàn)了面向?qū)ο蟮乃刑匦浴?其實, 面向?qū)ο髢H僅是一個概念或者一個編程思想而已, 它不應(yīng)該依賴于某個語言存在,比如Java采用面向?qū)ο笏枷霕?gòu)造其語言,它實現(xiàn)了類,繼承,派生,多態(tài),接口等機制。 但是這些機制,只是實現(xiàn)面向?qū)ο蟮囊环N手段, 而非必須。換言之, 一門語言可以根據(jù)自身特性選擇合適的方式來實現(xiàn)面向?qū)ο蟆? 由于大多數(shù)程序員首先學習的是Java, C++等高級編程語言, 因而先入為主的接受了“類”這個面向?qū)ο髮嶋H方式,所以習慣性的用類式面向?qū)ο笳Z言中的概念來判斷該語言是否是面向?qū)ο蟮恼Z言。這也是很多有其他編程語言經(jīng)驗的人在學習JavaScript對象時,感覺到很困難的地方。
實際上, JavaScript是通過一種叫原型(prototype)的方式來實現(xiàn)面向?qū)ο缶幊痰摹O旅嫖覀兙蛠碛懻撘幌?strong>基于類(class-basesd)的面向?qū)ο?/strong>和基于原型(protoype-based)的面向?qū)ο?/strong>這兩者的差別。
2. 基于類的面向?qū)ο蠛突谠偷拿嫦驅(qū)ο蟮谋容^
基于類的面向?qū)ο?/strong>
在基于類的面向?qū)ο笳Z言中(比如Java和C++), 是構(gòu)建在類(class)和實例(instance)上的。其中類定義了所有用于具有某一特征對象的屬性。類是抽象的事物, 而不是其所描述的全部對象中的任何特定的個體。另一方面, 一個實例是一個類的實例化,是其中的一個成員。
基于原型的面向?qū)ο?/strong>
在基于原型的語言中(如JavaScript)并不存在這種區(qū)別:它只有對象!不論是構(gòu)造函數(shù)(constructor),實例(instance),原型(prototype)本身都是對象?;谠偷恼Z言具有所謂的原型對象的概念,新對象可以從中獲得原始的屬性。
所以,在JavaScript中有一個很有意思的__proto__屬性(ES6以下是非標準屬性)用于訪問其原型對象, 你會發(fā)現(xiàn),上面提到的構(gòu)造函數(shù),實例,原型本身都有__proto__指向原型對象。其***順著原型鏈都會指向Object這個構(gòu)造函數(shù),然而Object的原型對象的原型是null,不信, 你可以嘗試一下Object.prototype.__proto__ === null為true。然而typeof null === 'object'為true。到這里, 我相信你應(yīng)該就能明白為什么JavaScript這類基于原型的語言中沒有類和實例的區(qū)別, 而是萬物皆對象!
差異總結(jié)
基于類的(Java)基于原型的(JavaScript)類和實例是不同的事物。所有對象均為實例。通過類定義來定義類;通過構(gòu)造器方法來實例化類。通過構(gòu)造器函數(shù)來定義和創(chuàng)建一組對象。通過 new 操作符創(chuàng)建單個對象。相同通過類定義來定義現(xiàn)存類的子類, 從而構(gòu)建對象的層級結(jié)構(gòu)指定一個對象作為原型并且與構(gòu)造函數(shù)一起構(gòu)建對象的層級結(jié)構(gòu)遵循類鏈接繼承屬性遵循原型鏈繼承屬性類定義指定類的所有實例的所有屬性。無法在運行時動態(tài)添加屬性構(gòu)造器函數(shù)或原型指定初始的屬性集。允許動態(tài)地向單個的對象或者整個對象集中添加或移除屬性。
二. ES5中的面向?qū)ο?/strong>
*這里的ES5并不特指ECMAScript 5, 而是代表ECMAScript 6 之前的ECMAScript!
(一) ES5中對象的創(chuàng)建
在ES5中創(chuàng)建對象有兩種方式, ***種是使用對象字面量的方式, 第二種是使用構(gòu)造函數(shù)的方式。該兩種方法在特定的使用場景分別有其優(yōu)點和缺點, 下面我們來分別介紹這兩種創(chuàng)建對象的方式。
1. 使用對象字面量的方式
我們通過對象字面量的方式創(chuàng)建兩個student對象,分別是student1和student2。
- var student1 = {
 - name: '阿輝',
 - age: 22,
 - subject: '前端開發(fā)'
 - };
 - var student2 = {
 - name: '阿傻',
 - age: 22,
 - subject: '大數(shù)據(jù)開發(fā)'
 - };
 
上面的代碼就是使用對象字面量的方式創(chuàng)建實例對象, 使用對象字面量的方式在創(chuàng)建單一簡單對象的時候是非常方便的。但是,它也有其缺點:
- 在生成多個實例對象時, 我們需要每次重復寫name,age,subject屬性,寫起來特別的麻煩
 - 雖然都是學生的對象, 但是看不出student1和student2之間有什么聯(lián)系。
 
為了解決以上兩個問題, JavaScript提供了構(gòu)造函數(shù)創(chuàng)建對象的方式。
2. 使用構(gòu)造函數(shù)的方式
構(gòu)造函數(shù)就其實就是一個普通的函數(shù),當對構(gòu)造函數(shù)使用new進行實例化時,會將其內(nèi)部this的指向綁定實例對象上,下面我們來創(chuàng)建一個Student構(gòu)造函數(shù)(構(gòu)造函數(shù)約定使用大寫開頭,和普通函數(shù)做區(qū)分)。
- function Student (name, age, subject) {
 - this.name = name;
 - this.age = age;
 - this.subject = subject;
 - console.log(this);
 - }
 
我特意在構(gòu)造函數(shù)中打印出this的指向。上面我們提到,構(gòu)造函數(shù)其實就是一個普通的函數(shù), 那么我們使用普通函數(shù)的調(diào)用方式嘗試調(diào)用Student。
- Student('阿輝', 22, '前端開發(fā)'); //window{}
 
采用普通方式調(diào)用Student時, this的指向是window。下面使用new來實例化該構(gòu)造函數(shù), 生成一個實例對象student1。
- let student1 = new Student('阿輝', 22, '前端開發(fā)'); //Student {name: "阿輝", age: 22, subject: "前端開發(fā)"}
 
當我們采用new生成實例化對象student1時, this不再指向window, 而是指向的實例對象本身。這些, 都是new幫我們做的。上面的就是采用構(gòu)造函數(shù)的方式生成實例對象的方式, 并且當我們生成其他實例對象時,由于都是采用Student這個構(gòu)造函數(shù)實例化而來的, 我們能夠清楚的知道各實例對象之間的聯(lián)系。
- let student1 = new Student('阿輝', 22, '前端開發(fā)');
 - let student2 = new Student('阿傻', 22, '大數(shù)據(jù)開發(fā)');
 - let student3 = new Student('阿呆', 22, 'Python');
 - let student4 = new Student('阿笨', 22, 'Java');
 
(二) ES5中對象的繼承
1. prototype的原型繼承
prototype是JavaScript這類基于原型繼承的核心, 只要弄明白了原型和原型鏈, 就基本上完全理解了JavaScript中對象的繼承。下面我將著重的講解為什么要使用prototype和使用prototype實現(xiàn)繼承的方式。
為什么要使用prototype?
我們給之前的Student構(gòu)造函數(shù)新增一個study方法
- function Student (name, age, subject) {
 - this.name = name;
 - this.age = age;
 - this.subject = subject;
 - this.study = function() {
 - console.log('我在學習' + this.subject);
 - }
 - }
 
現(xiàn)在我們來實例化Student構(gòu)造函數(shù), 生成student1和`student2, 并分別調(diào)用其study方法。
- let student1 = new Student('阿輝', 22, '前端開發(fā)');
 - let student2 = new Student('阿傻', 22, '大數(shù)據(jù)開發(fā)');
 - student1.study(); //我在學習前端開發(fā)
 - student2.study(); //我在學習大數(shù)據(jù)開發(fā)
 
這樣生成的實例對象表面上看沒有任何問題, 但是其實是有很大的性能問題!我們來看下面一段代碼:
- console.log(student1.study === student2.study); //false
 
其實對于每一個實例對象studentx,其study方法的函數(shù)體是一模一樣的,方法的執(zhí)行結(jié)果只根據(jù)其實例對象決定,然而生成的每個實例都需要生成一個study方法去占用一份內(nèi)存。這樣是非常不經(jīng)濟的做法。新手可能會認為, 上面的代碼中也就多生成了一個study方法, 對于內(nèi)存的占用可以忽略不計。
那么我們在MDN中看一下在JavaScript中我們使用的String實例對象有多少方法?

上面的方法只是String實例對象中的一部分方法(我一個屏幕截取不完?。?, 這也就是為什么我們的字符串能夠使用如此多便利的原生方法的原因。設(shè)想一下, 如果這些方法不是掛載在String.prototype上, 而是像上面Student一樣寫在String構(gòu)造函數(shù)上呢?那么我們項目中的每一個字符串,都會去生成這幾十種方法去占用內(nèi)存,這還沒考慮Math,Array,Number,Object等對象!
現(xiàn)在我們應(yīng)該知道應(yīng)該將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);
 - }
 
現(xiàn)在我們實例化student1和student2
- let student1 = new Student('阿輝', 22, '前端開發(fā)');
 - let student2 = new Student('阿傻', 22, '大數(shù)據(jù)開發(fā)');
 - student1.study(); //我在學習前端開發(fā)
 - student2.study(); //我在學習大數(shù)據(jù)開發(fā)
 - console.log(student1.study === student2.study); //true
 
從上面的代碼我們可以看出, student1和student2的study方法執(zhí)行結(jié)果沒有發(fā)生變化,但是study本身指向了一個內(nèi)存地址。這就是為什么我們要使用prototype進行掛載方法的原因。接下來我們來講解一下如何使用prototype來實現(xiàn)繼承。
如何使用prototype實現(xiàn)繼承?
“學生”這個對象可以分為小學生, 中學生和大學生等。我們現(xiàn)在新建一個小學生的構(gòu)造函數(shù)Pupil。
- function Pupil(school) {
 - this.school = school;
 - }
 
那么如何讓Pupil使用prototype繼承Student呢? 其實我們只要將Pupil的prototype指向Student的一個實例即可。
- Pupil.prototype = new Student('小輝', 8, '小學義務(wù)教育課程');
 - Pupil.prototype.constructor = Pupil;
 - let pupil1 = new Pupil('北大附小');
 
代碼的***行, 我們將Pupil的原型對象(Pupil.prototype)指向了Student的實例對象。
- Pupil.prototype = new Student('小輝', 8, '小學義務(wù)教育課程');
 
代碼的第二行也許有的讀者會不能理解是什么意思。
- Pupil.prototype.constructor = Pupil;
 
Pupil作為構(gòu)造函數(shù)有一個protoype屬性指向原型對象Pupil.prototype,而原型對象Pupil.prototype也有一個constructor屬性指回它的構(gòu)造函數(shù)Pupil。如下圖所示:

然而, 當我們使用實例化Student去覆蓋Pupil.prototype后, 如果沒有第二行代碼的情況下, Pupil.prototype.constructor指向了Student構(gòu)造函數(shù), 如下圖所示:

而且, pupil1.constructor會默認調(diào)用Pupil.prototype.constructor, 這個時候pupil1.constructor指向了Student:
- Pupil.prototype = new Student('小輝', 8, '小學義務(wù)教育課程');
 - let pupil1 = new Pupil('北大附小');
 - console.log(pupil1.constructor === Student); //true
 
這明顯是錯誤的, pupil1明明是用Pupil構(gòu)造函數(shù)實例化出來的, 怎么其constructor指向了Student構(gòu)造函數(shù)呢。所以, 我們就需要加入第二行, 修正其錯誤:
- Pupil.prototype = new Student('小輝', 8, '小學義務(wù)教育課程');
 - //修正constructor的指向錯誤
 - Pupil.prototype.constructor = Pupil;
 - let pupil1 = new Pupil('北大附小');
 - console.log(pupil1.constructor === Student); //false
 - console.log(pupil1.constructor === Pupil); //ture
 
上面就是我們的如何使用prototype實現(xiàn)繼承的例子, 需要特別注意的: 如果替換了prototype對象, 必須手動將prototype.constructor重新指向其構(gòu)造函數(shù)。
2. 使用call和apply方法實現(xiàn)繼承
使用call和apply是我個人比較喜歡的繼承方式, 因為只需要一行代碼就可以實現(xiàn)繼承。但是該方法也有其局限性,call和apply不能繼承原型上的屬性和方法, 下面會有詳細說明。
使用call實現(xiàn)繼承
同樣對于上面的Student構(gòu)造函數(shù), 我們使用call實現(xiàn)Pupil繼承Student的全部屬性和方法:
- //父類構(gòu)造函數(shù)
 - function Student (name, age, subject) {
 - this.name = name;
 - this.age = age;
 - this.subject = subject;
 - }
 - //子類構(gòu)造函數(shù)
 - function Pupil(name, age, subject, school) {
 - //使用call實現(xiàn)繼承
 - Student.call(this, name, age, subject);
 - this.school = school;
 - }
 - //實例化Pupil
 - let pupil2 = new Pupil('小輝', 8, '小學義務(wù)教育課程', '北大附小');
 
需要注意的是, call和apply只能繼承本地屬性和方法, 而不能繼承原型上的屬性和方法,如下面的代碼所示, 我們給Student掛載study方法,Pupil使用call繼承Student后, 調(diào)用pupil2.study()會報錯:
- //父類構(gòu)造函數(shù)
 - function Student (name, age, subject) {
 - this.name = name;
 - this.age = age;
 - this.subject = subject;
 - }
 - //原型上掛載study方法
 - Student.prototype.study = function() {
 - console.log('我在學習' + this.subject);
 - }
 - //子類構(gòu)造函數(shù)
 - function Pupil(name, age, subject, school) {
 - //使用call實現(xiàn)繼承
 - Student.call(this, name, age, subject);
 - this.school = school;
 - }
 - let pupil2 = new Pupil('小輝', 8, '小學義務(wù)教育課程', '北大附小');
 - //報錯
 - pupil2.study(); //Uncaught TypeError: pupil2.study is not a function
 
使用apply實現(xiàn)繼承
使用apply實現(xiàn)繼承的方式和call類似, 唯一的不同只是參數(shù)需要使用數(shù)組的方法。下面我們使用apply來實現(xiàn)上面Pupil繼承Student的例子。
- //父類構(gòu)造函數(shù)
 - function Student (name, age, subject) {
 - this.name = name;
 - this.age = age;
 - this.subject = subject;
 - }
 - //子類構(gòu)造函數(shù)
 - function Pupil(name, age, subject, school) {
 - //使用applay實現(xiàn)繼承
 - Student.apply(this, [name, age, subject]);
 - this.school = school;
 - }
 - //實例化Pupil
 - let pupil2 = new Pupil('小輝', 8, '小學義務(wù)教育課程', '北大附小');
 
3. 其他繼承方式
JavaScript中的繼承方式不僅僅只有上面提到的幾種方法, 在《JavaScript高級程序設(shè)計》中, 還有實例繼承,拷貝繼承,組合繼承,寄生組合繼承等眾多繼承方式。在寄生組合繼承中, 就很好的彌補了call和apply無法繼承原型屬性和方法的缺陷,是最***的繼承方法。這里就不詳細的展開論述,感興趣的可以自行閱讀《JavaScript高級程序設(shè)計》。
三. ES6中的面向?qū)ο?/strong>
基于原型的繼承方式,雖然實現(xiàn)了代碼復用,但是行文松散且不夠流暢,可閱讀性差,不利于實現(xiàn)擴展和對源代碼進行有效的組織管理。不得不承認,基于類的繼承方式在語言實現(xiàn)上更健壯,且在構(gòu)建可服用代碼和組織架構(gòu)程序方面具有明顯的優(yōu)勢。所以,ES6中提供了基于類class的語法。但class本質(zhì)上是ES6提供的一顆語法糖,正如我們前面提到的,JavaScript是一門基于原型的面向?qū)ο笳Z言。
(一) ES6中對象的創(chuàng)建
我們使用ES6的class來創(chuàng)建Student
- //定義類
 - class Student {
 - //構(gòu)造方法
 - constructor(name, age, subject) {
 - this.name = name;
 - this.age = age;
 - this.subject = subject;
 - }
 - //類中的方法
 - study(){
 - console.log('我在學習' + this.subject);
 - }
 - }
 - //實例化類
 - let student3 = new Student('阿輝', 24, '前端開發(fā)');
 - student3.study(); //我在學習前端開發(fā)
 
上面的代碼定義了一個Student類, 可以看到里面有一個constructor方法, 這就是構(gòu)造方法,而this關(guān)鍵字則代表實例對象。也就是說,ES5中的構(gòu)造函數(shù)Student, 對應(yīng)的是E6中Student類中的constructor方法。
Student類除了構(gòu)造函數(shù)方法,還定義了一個study方法。需要特別注意的是,在ES6中定義類中的方法的時候,前面不需要加上function關(guān)鍵字,直接把函數(shù)定義進去就可以了。另外,方法之間不要用逗號分隔,加了會報錯。而且,類中的方法全部是定義在原型上的,我們可以用下面的代碼進行驗證。
- console.log(student3.__proto__.study === Student.prototype.study); //true
 - console.log(student3.hasOwnProperty('study')); // false
 
上面的***行的代碼中, student3.__proto__是指向的原型對象,其中Student.prototype也是指向的原型的對象,結(jié)果為true就能很好的說明上面的結(jié)論: 類中的方法全部是定義在原型上的。第二行代碼是驗證student3實例中是否有study方法,結(jié)果為false, 表明實例中沒有study方法,這也更好的說明了上面的結(jié)論。其實,只要理解了ES5中的構(gòu)造函數(shù)對應(yīng)的是類中的constructor方法,就能推斷出上面的結(jié)論。
(二) ES6中對象的繼承
E6中class可以通過extends關(guān)鍵字來實現(xiàn)繼承, 這比前面提到的ES5中使用原型鏈來實現(xiàn)繼承, 要清晰和方便很多。下面我們使用ES6的語法來實現(xiàn)Pupil。
- //子類
 - class Pupil extends Student{
 - constructor(name, age, subject, school) {
 - //調(diào)用父類的constructor
 - super(name, age, subject);
 - this.school = school;
 - }
 - }
 - let pupil = new Pupil('小輝', 8, '小學義務(wù)教育課程', '北大附小');
 - pupil.study(); //我在學習小學義務(wù)教育課程
 
上面代碼代碼中, 我們通過了extends實現(xiàn)Pupil子類繼承Student父類。需要特別注意的是,子類必須在constructor方法中首先調(diào)用super方法,否則實例化時會報錯。這是因為子類沒有自己的this對象, 而是繼承父類的this對象,然后對其加工。如果不調(diào)用super方法,子類就得不到this對象。
四.結(jié)束語















 
 
 










 
 
 
 