JavaScript | 面向?qū)ο缶幊?/h2>

Reference : JavaScript教程 - 廖雪峰的官方網(wǎng)站

JavaScript哲學(xué):萬(wàn)物皆對(duì)象

JavaScript沒有類 (class) 或?qū)嵗?(instance) 的概念。JavaScript實(shí)現(xiàn)面向?qū)ο缶幊痰墓ぞ呤窃?(prototype)。

原型 prototype

以下內(nèi)容展示JavaScript面向?qū)ο缶幊痰脑怼?/p>

我們定義一個(gè)對(duì)象robot

var robot = {
    name: 'Robot',
    height: 1.6,
    run: function () {
        console.log (this.name + 'is running ...');
    }
};

我們將這個(gè)對(duì)象作為模板對(duì)象,為了方便理解,重新用Student命名它。

var Student = {
    name: 'Robot',
    height: 1.6,
    run: function () {
        console.log (this.name + 'is running ...');
    }
};

現(xiàn)在想要?jiǎng)?chuàng)建對(duì)象xiaoming,同時(shí)讓這個(gè)新的對(duì)象獲得Student對(duì)象相同的屬性和方法。

var xiaoming = {
    name: '小明'
};

xiaoming.__proto__ = Student;

最后一行代碼把xiaoming的原型指向了對(duì)象Student,看上去xiaoming仿佛繼承Student對(duì)象。

xiaoming.name; // '小明'
xiaoming.run(); // 小明 is running ...

如果現(xiàn)在定義一個(gè)新的對(duì)象Bird,然后讓xiaoming的原型指向Bird

var Bird = {
    fly: function () {
        console.log (this.name + 'is flying ...');   
    }
};

xiaoming.__proto__ = Bird;

這時(shí)xiaoming已經(jīng)無(wú)法run()了,他已經(jīng)變成了一只鳥:

xiaoming.fly(); // 小明 is flying ...

注意上面直接修改obj.__proto__的做法在開發(fā)時(shí)不可取,而且低版本的IE不支持這種寫法?,F(xiàn)在我們理解了面向?qū)ο缶幊痰脑恚酉聛?lái)介紹推薦的面向?qū)ο缶幊谭椒ā?/p>

面向?qū)ο缶幊?/h2>

原型鏈

// 原型對(duì)象:
var Student = {
    name: 'Robot',
    height: 1.2,
    run: function () {
        console.log(this.name + ' is running...');
    }
};

Object.create()方法可以傳入一個(gè)原型對(duì)象,并創(chuàng)建一個(gè)基于該原型的新對(duì)象,但是新對(duì)象什么屬性都沒有。

var new_student = Object.create(Student);
new_student; //{}
new_student.height; // 1.6
new_student.name; // 'robot'

可以看出,盡管新的對(duì)象沒有自己的屬性,但實(shí)際上具有了Student對(duì)象的所有屬性?;诖宋覀兛梢圆孪?,在獲取對(duì)象屬性時(shí),先在對(duì)象內(nèi)部查找,然后順著原型依次向上查找。實(shí)際就是這樣,而且如果訪問(wèn)到最頂層的Object.prototype對(duì)象并且還是找不到這個(gè)屬性,就會(huì)返回undefined。

這里引入原型鏈的概念。正如上面所說(shuō),JavaScript的每一個(gè)對(duì)象,其__proto__屬性仍是一個(gè)對(duì)象,因此可以形成一條原型鏈。以內(nèi)置的Array對(duì)象為例,我們可以用[]創(chuàng)建一個(gè)Array對(duì)象。

比如,

var arr = [1, 2, 3];

其原型鏈?zhǔn)牵?/p>

arr ----> Array.prototype ----> Object.prototype ----> null

Array.prototype定義了indexOf()shift()等方法,因此我們可以在所有的Array對(duì)象上直接調(diào)用這些方法。

再舉一個(gè)例子,我們可以用function關(guān)鍵字創(chuàng)建函數(shù)。

function foo () {
    return 0;
}

函數(shù)也是一個(gè)對(duì)象,它的原型鏈?zhǔn)牵?/p>

foo ----> Function.prototype ----> Object.prototype ----> null

由于Function.prototype定義了apply()等方法,因此所有函數(shù)都可以調(diào)用apply()方法。

很容易想到,如果原型鏈很長(zhǎng),那么訪問(wèn)一個(gè)對(duì)象的屬性就會(huì)因?yàn)榛ǜ嗟臅r(shí)間查找而變得更慢,因此要注意不要把原型鏈搞得太長(zhǎng)。

當(dāng)然,我們可以把Object.create這個(gè)方法包裝成一個(gè)創(chuàng)建新對(duì)象的函數(shù)。

function createStudent(name) {
    // 基于Student原型創(chuàng)建一個(gè)新對(duì)象:
    var s = Object.create(Student);
    // 初始化新對(duì)象:
    s.name = name;
    return s;
}

var xiaoming = createStudent('小明');
xiaoming.run(); // 小明 is running...
xiaoming.__proto__ === Student; // true

構(gòu)造函數(shù)

除了直接用{...}創(chuàng)建一個(gè)對(duì)象外,JavaScript還可以用一種構(gòu)造函數(shù)的方法來(lái)創(chuàng)建對(duì)象。用法是定義一個(gè)構(gòu)造函數(shù),比如:

function Student (name) {
    this.name = name;
    this.hello = function () {
        alert ('Hello, ' + this.name + '!');
    }
}

這個(gè)函數(shù)雖然看上去和普通函數(shù)一樣,但只要用關(guān)鍵字new調(diào)用這個(gè)函數(shù),它就默認(rèn)是構(gòu)造函數(shù),且在函數(shù)結(jié)束后一定返回this,無(wú)論在函數(shù)中是否寫有return返回語(yǔ)句。

調(diào)用的寫法如下:

var xiaoming = new Student ('小明');
xiaoming.name; // '小明'
xiaoming.hello(); // Hello, 小明!

值得注意的是,這個(gè)函數(shù)如果不寫new并調(diào)用,則成為了一個(gè)返回undefined的普通函數(shù)。

這樣新建的xiaoming的原型鏈?zhǔn)牵?/p>

xiaoming ----> Student.prototype ----> Object.prototype ----> null

此外,用new Student()類似語(yǔ)句創(chuàng)建的對(duì)象還從原型上獲得了一個(gè)constructor屬性,它指向Student本身。這段話用代碼表示如下。

xiaoming.constructor === Student.prototype.constructor; // true
Student.prototype.constructor === Student; // true

Object.getPrototypeOf(xiaoming) === Student.prototype; // true

xiaoming instanceof Student; // true

用圖片表示如下,其中紅色代表原型鏈:

對(duì)象共享方法

現(xiàn)在這么寫,有一個(gè)問(wèn)題:

xiaoming = new Student ('小明');
xiaohong = new Student ('小紅');

xiaoming.hello === xiaohong.hello; // false

兩個(gè)對(duì)象的方法不相等,顯然浪費(fèi)了內(nèi)存空間,因?yàn)閷?duì)于函數(shù)而言,我們只需要保存一份就可以了。存在兩份的原因是每次調(diào)用new Student(),都會(huì)在構(gòu)造時(shí)執(zhí)行var hello一句。對(duì)這個(gè)問(wèn)題,一個(gè)可行的優(yōu)化是把hello的定義放在xiaomingxiaohong公共的原型上,而不是構(gòu)造函數(shù)里,這樣在調(diào)用hello時(shí),就會(huì)通過(guò)原型鏈查找到hello。具體的代碼如下:

function Student (name) {
    this.name = name;
}

Student.prototype.hello = function () {
   alert ('Hello, ' + this.name + '!');
};

優(yōu)化的原理通過(guò)上面關(guān)于原型鏈的內(nèi)容很容易理解,即用new Student()語(yǔ)法創(chuàng)建的對(duì)象,其原型都指向Student.prototype。

當(dāng)然了,即使有了方便的構(gòu)造函數(shù)工具,我們?nèi)匀唤ㄗh將new的過(guò)程封裝在函數(shù)里完成。原因有二,一是不需要new來(lái)調(diào)用,避免了漏寫new的可能;二是參數(shù)更靈活,參數(shù)可以不用完整地傳遞。

原型繼承

原文:原型繼承 - 廖雪峰的官方網(wǎng)站

function inherits( Child, Parent) {
    var F = function () {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
}

關(guān)于這個(gè)函數(shù)的內(nèi)容,請(qǐng)?jiān)谠闹袑ふ医忉尅?/p>

這里需要增加的解釋是為什么不直接用

PrimaryStudent.prototype = Student.prototype

可以從圖中看出,prototype實(shí)際指向一個(gè)對(duì)象,如果用了上面的語(yǔ)句,那么PrimaryStudent就會(huì)和Student共享同一個(gè)原型對(duì)象,這樣綁定到PrimaryStudent.prototype上的屬性(特別是對(duì)象共享的方法)也會(huì)被綁定到Student.prototype上,因?yàn)樗鼈儗?shí)際是同一個(gè)對(duì)象,這樣就不符合子類和父類之間的關(guān)系。因此可以看出,我們創(chuàng)建的new F()對(duì)象,就是為了獨(dú)立地綁定PrimaryStudent對(duì)象共享的方法。

class關(guān)鍵字 [ES6]

在上面的章節(jié)中我們看到了JavaScript的對(duì)象模型是基于原型實(shí)現(xiàn)的,特點(diǎn)是簡(jiǎn)單,缺點(diǎn)是理解起來(lái)比傳統(tǒng)的類-實(shí)例模型要困難,最大的缺點(diǎn)是繼承的實(shí)現(xiàn)需要編寫大量代碼,并且需要正確實(shí)現(xiàn)原型鏈。

新的關(guān)鍵字class正是為了簡(jiǎn)化類的定義而引入。對(duì)于下面這個(gè)用構(gòu)造函數(shù)實(shí)現(xiàn)的Student

function Student (name) {
   this.name = name;
}

Student.prototype.hello = function () {
    alert ('Hello, ' + this.name + '!');
};

如果用新的關(guān)鍵字class來(lái)實(shí)現(xiàn),可以這樣寫:

class Student {
    constructor(name) {
        this.name = name;
    }

    hello() {
        alert('Hello, ' + this.name + '!');
    }
}

顯然用class的代碼簡(jiǎn)介明了,既包含了構(gòu)造函數(shù)constructor的定義,也包含了原先定義在原型對(duì)象上的函數(shù)hello()(注意沒有function關(guān)鍵字),這樣就避免了代碼分散可能造成的理解障礙。

最后,這樣定義的Student也是用new關(guān)鍵字調(diào)用,得到的對(duì)象與之前得到的對(duì)象用發(fā)法相同。

var xiaoming = new Student ('小明');

class繼承 [ES6]

不需要考慮橋接的原型對(duì)象,直接用extends關(guān)鍵字完成繼承。

class PrimaryStudent extends Student {
    constructor(name, grade) {
        super(name); // 記得用super調(diào)用父類的構(gòu)造方法!
        this.grade = grade;
    }

    myGrade() {
        alert('I am at grade ' + this.grade);
    }
}

幾個(gè)要點(diǎn):

  • class關(guān)鍵字引導(dǎo)
  • extends關(guān)鍵字引出父類
  • constructor定義開頭用super()調(diào)用父類的構(gòu)造方法

由于現(xiàn)在很多瀏覽器還不支持ES6的所有新特性,特別是class,在這里介紹一個(gè)小工具,用于將下一代的JavaScript代碼轉(zhuǎn)換為同義的較低版本代碼:Babel - The compiler for next generation JavaScript。注:這個(gè)工具為原文推薦,而本文寫作的時(shí)間比參考的文章晚3年,現(xiàn)在的主流數(shù)瀏覽器已經(jīng)支持了ES6標(biāo)準(zhǔn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容