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...');
}
};
// 原型對(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的定義放在xiaoming和xiaohong公共的原型上,而不是構(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ù)可以不用完整地傳遞。
原型繼承
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)。