第九章 面向?qū)ο?/h3>
面向?qū)ο笫荍avaScript中比較不好理解的地方,去年校招的時(shí)候,每當(dāng)被問到原型鏈,屬性,繼承時(shí),心里都有些需,結(jié)合紅寶書,對(duì)面向?qū)ο筮M(jìn)行下總結(jié)。
1. 基本概念
對(duì)象
在ECMAScript-262中,對(duì)象被定義為無序?qū)傩缘募?,其屬性可以包含基本值、?duì)象或者函數(shù)。下面就是一個(gè)簡單的對(duì)象。
var person = {
// 屬性為基本值
name: 'Tom',
age: 18,
// 屬性為函數(shù)
getName: function () {
return this.name;
},
// 屬性為對(duì)象
parent: {}
}
創(chuàng)建對(duì)象
在了解對(duì)象的定義之后,我們?nèi)绾稳?chuàng)建一個(gè)對(duì)象呢?紅寶書中對(duì)創(chuàng)建對(duì)象總結(jié)了六種方法,由于紅寶書中的內(nèi)容作者理解的有所欠缺,這里只描述本書中創(chuàng)建對(duì)象的方法。
1. 通過關(guān)鍵字new來創(chuàng)建對(duì)象
var obj = new Object();
2. 通過字面量的形式創(chuàng)建對(duì)象
var obj = {};
當(dāng)我們想要給創(chuàng)建的對(duì)象添加屬性與方法時(shí),可以這樣操作。
var person = {};
person.name = 'Tom';
person.getName = function () {
return this.name;
}
// or
var person = {
name: 'Tom',
getName: function () {
return this.name;
}
}
當(dāng)我們需要訪問對(duì)象的屬性與方法時(shí),我們可以這樣。
var person = {
name: 'Tom',
age: 20,
getName: function () {
return this.name;
}
}
// 訪問name屬性
person.name;
// or
person['name'];
// or 注意這里_name是一個(gè)變量
var _name = 'name';
person[_name];
要注意,當(dāng)我們?cè)L問的屬性名是一個(gè)變量時(shí),只能使用中括號(hào)的方式。
構(gòu)造函數(shù)與原型
第八章中提到過,封裝函數(shù)其實(shí)是封裝一些公共的邏輯與功能,通過傳入?yún)?shù)的形式達(dá)到自定義的效果。當(dāng)面對(duì)具有共同特征的一類事物時(shí),就可以結(jié)合構(gòu)造函數(shù)與原型的方式將這類事物封裝成對(duì)象。
例如,我們將“人”這一類事物封裝成一個(gè)對(duì)象,那么可以這樣做。
// 構(gòu)造函數(shù)
var Person = function (name, age) {
this.name = name;
this.age = age;
}
// Person.prototype為Person的原型,這里在原型上添加了一個(gè)方法
Person.prototype.getName = function () {
return this.name;
}
具體某一個(gè)人的特定屬性,通常放在構(gòu)造函數(shù)中。所有人公共的方法與屬性,通常會(huì)放在原型對(duì)象中。
var p1 = new Person('Jake', 20);
var p2 = new Person('Tom', 22);
p1.getName(); // Jake
p2.getName(); // Tom
注意this指向問題,忘記的童鞋請(qǐng)看之前第七章的筆記或參照你不知道的JS對(duì)象章節(jié)部分。
new關(guān)鍵字在創(chuàng)建實(shí)例時(shí)經(jīng)歷了如下過程:
- 先創(chuàng)建一個(gè)新的、空的實(shí)例對(duì)象
- 將實(shí)例對(duì)象的原型,指向構(gòu)造函數(shù)的原型
- 將構(gòu)造函數(shù)內(nèi)部的this,修改為指向?qū)嵗?/li>
- 最后返回改實(shí)例
它們之間的關(guān)系如下圖所示。

由上圖可得,構(gòu)造函數(shù)的prototype與所有實(shí)例的proto都指向原型對(duì)象,而原型對(duì)象的constructor則指向構(gòu)造函數(shù)。
因?yàn)樵跇?gòu)造函數(shù)中聲明的變量與方法只屬于當(dāng)前實(shí)例,因此我們可以將構(gòu)造函數(shù)中聲明的屬性與方法稱為該實(shí)例的私有屬性和方法,它們只能被當(dāng)前實(shí)例訪問。
而原型中的方法與屬性能夠被所有的實(shí)例訪問,因此我們將原型中聲明的屬性與方法稱為公有屬性與方法。
與在原型中添加一個(gè)方法不同,當(dāng)在構(gòu)造函數(shù)中聲明一個(gè)方法時(shí),每創(chuàng)建一個(gè)實(shí)例,該方法都會(huì)被重新創(chuàng)建一次。而原型中的方法僅僅只會(huì)被創(chuàng)建一次。
因此在構(gòu)造函數(shù)中,聲明私有方法會(huì)消耗更多的內(nèi)存空間。
如果再構(gòu)造函數(shù)中聲明的私有方法/屬性與原型中的公有方法/屬性重名,那么會(huì)優(yōu)先訪問私有屬性/方法。
function Person (name) {
this.name = name;
this.getName = function () {
return this.name + ' ,你正在訪問私有方法';
}
}
Person.prototype.getName = function () {
return this.name;
}
var p1 = new Person('Tom');
p1.getName(); // Tom,你正在訪問私有方法
判斷對(duì)象是否擁有某個(gè)屬性/方法
可以通過in來判斷一個(gè)對(duì)象是否擁有某一個(gè)方法/屬性,無論該方法/屬性是否公有。
// 接上面創(chuàng)建的p1實(shí)例
'name' in p1; // true
'getName' in p1; // true
'age' in p1; // false
原型鏈
原型對(duì)象也是普通對(duì)象,因此,在創(chuàng)建原型方法時(shí)也可按照創(chuàng)建對(duì)象的方法去創(chuàng)建。下面幾句話有些繞,還需讀者好好理解。
當(dāng)一個(gè)對(duì)象A作為原型時(shí),它有一個(gè)constructor屬性指向它的構(gòu)造函數(shù),即A.constructor。
當(dāng)一個(gè)對(duì)象B作為構(gòu)造函數(shù)時(shí),它有一個(gè)prototype屬性指向它的原型,即B.prototype。
當(dāng)一個(gè)對(duì)象C作為實(shí)例時(shí),它有一個(gè)proto屬性指向它的原型,即C.proto。
當(dāng)想要判斷一個(gè)對(duì)象foo是否是構(gòu)造函數(shù)Foo的實(shí)例時(shí),可以使用instanceof關(guān)鍵字。
foo instanceof Foo; // true: foo是Foo的實(shí)例,false:不是
當(dāng)創(chuàng)建一個(gè)對(duì)象時(shí),可以使用new Object()來創(chuàng)建。因此Object其實(shí)是一個(gè)構(gòu)造函數(shù),而其對(duì)應(yīng)的原型Object.prototype則是原型鏈的終點(diǎn)。
foo instanceof Foo; // true: foo是Foo的實(shí)例,false:不是,Object.prototype.__proto__ === null
// 所有的函數(shù)與對(duì)象都有一個(gè)toString與vallueOf方法,就是來自于Object.prototype
Object.prototype.toString = function () {}
Object.prototype.valueOf = function () {}
當(dāng)創(chuàng)建函數(shù)時(shí),除可以使用function關(guān)鍵字外,還可以使用Function對(duì)象。
var add = new Function('a', 'b', 'return a+ b');
// 等價(jià)于
var add = function (a, b) {
return a + b;
}
因此這里創(chuàng)建的add方法是一個(gè)實(shí)例,它對(duì)應(yīng)的構(gòu)造函數(shù)是Function,它的原型是Function.prototype。
add.__proto__ === Function.prototype; // true
需要注意的是,當(dāng)構(gòu)造函數(shù)與原型擁有同名的方法/屬性時(shí),如果用創(chuàng)建的實(shí)例訪問該方法/屬性,則優(yōu)先訪問構(gòu)造函數(shù)的方法/屬性。
function Person (name) {
this.name = name;
this.getName = function () {
return 'name in Person';
}
}
Person.prototype.getName = function () {
return 'name in Person.prototype';
}
var p1 = new Person('alex');
p1.getName(); // name in Person
實(shí)例方法、原型方法、靜態(tài)方法
構(gòu)造函數(shù)中的方法稱之為實(shí)例方法,通過prototype添加的方法,將會(huì)掛載到原型上,稱之為原型方法,被直接掛在在構(gòu)造函數(shù)上的方法稱之為靜態(tài)方法。
靜態(tài)方法不能通過實(shí)例訪問,只能溝通過構(gòu)造函數(shù)來訪問。
function Foo () {
this.bar = function () {
return 'bar in Foo'; // 實(shí)例方法
}
}
Foo.bar = function () {
return 'bar in static'; // 靜態(tài)方法
}
Foo.prototype.bar = function () {
return 'bar in prototype'; // 原型方法
}
靜態(tài)方法又稱為工具方法,常用來實(shí)現(xiàn)一些常用的,與具體實(shí)例無關(guān)的功能,如遍歷方法each。
繼承
紅寶書中對(duì)繼承總結(jié)了六種方法,由于紅寶書中的內(nèi)容作者理解的有所欠缺,這里只描述本書中創(chuàng)建對(duì)象的方法。(還是建議讀者去看下紅寶書,比這本講的詳細(xì)一些)
繼承被分為兩種,一種是有構(gòu)造函數(shù)的繼承,一種是原型繼承。
假設(shè)已經(jīng)封裝好了一個(gè)父類對(duì)象Person。
var Person = function (name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function () {
return this.name;
}
Person.prototype.getAge = function () {
return this.age;
}
構(gòu)造函數(shù)的繼承比較簡單,可以借助call/apply來實(shí)現(xiàn)。假設(shè)想要通過繼承封裝一個(gè)Student的子類對(duì)象,那么構(gòu)造函數(shù)的實(shí)現(xiàn)如下。
var Student = function (name, age, grade) {
// 通過call方法還原Person構(gòu)造函數(shù)中的所有處理邏輯
Student.call(Person, name, age);
this.grade = grade;
}
// 等價(jià)于
var Student = function (name, age, grade) {
this.name = name;
this.age = age;
this.grade = grade;
}
原型的繼承則需要一點(diǎn)思考。首先應(yīng)該考慮,如何將子類對(duì)象的原型加到原型鏈中?其實(shí)只需讓子類對(duì)象的原型成為父類對(duì)象的一個(gè)實(shí)例,然后通過proto訪問富磊對(duì)象的原型,這樣就繼承了父類原型中的方法與屬性了。
可以先封裝一個(gè)方法,該方法會(huì)根據(jù)父類對(duì)象的原型創(chuàng)建一個(gè)實(shí)例,該實(shí)例即為子類對(duì)象的原型。
function create (proto, options) {
// 創(chuàng)建一個(gè)空對(duì)象
var tmp = {};
// 讓這個(gè)新的空對(duì)象成為父類對(duì)象的實(shí)例
tmp.__proto__ = proto;
// 傳入的方法都掛載到新對(duì)象上,新對(duì)象將作為子類對(duì)象的原型
Object.defineProperties(tmp, options);
return tmp;
}
在簡單封裝了create方法之后,就可以使用該方法來實(shí)現(xiàn)原型的繼承了。
Student.prototype = create(Person.prototype, {
// 不要忘了重新指定構(gòu)造函數(shù)
constructor: {
value: Student
}
getGrade: {
value: function () {
return this.grade
}
}
})
下面來驗(yàn)證這里實(shí)現(xiàn)的繼承是否正確。
var s1 = new Student('ming', 22, 5);
s1.getName(); // ming
s1.getAge(); // 22
s1.getGrade(); // 5
在ES5里面直接提供了一個(gè)Object.create方法來完成上面封裝的create功能。
屬性類型
在ES5中,對(duì)每個(gè)屬性都添加了幾個(gè)屬性類型,用來描述這些屬性的特點(diǎn)。
- configurable:表示該屬性是否能被delete刪除。當(dāng)其值為false時(shí),其他的特性也不能被改變,默認(rèn)為true。
- enumerable:是否能枚舉。即是否能被for-in遍歷,默認(rèn)為true。
- writable:是否能修改值,默認(rèn)為true。
- value:該屬性的具體值是多少,默認(rèn)是undefined。
- get:當(dāng)通過person.name訪問name屬性的值時(shí),get將被調(diào)用。該方法可以自定義返回的具體值是多少,get的默認(rèn)值為undefined。
- set:當(dāng)通過person.name = 'Jake'設(shè)置name的值時(shí),set方法將被調(diào)用。該方法可以自定義設(shè)置值的具體方式,set的默認(rèn)值為undefined。
以上是我對(duì)JavaScript核心技術(shù)開發(fā)解密第九章的讀書筆記,碼字不易,請(qǐng)尊重作者版權(quán),轉(zhuǎn)載注明出處。
By BeLLESS 2018.7.30 21:11