JavaScript核心技術(shù)開發(fā)解密讀書筆記(第九章)

第九章 面向?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

?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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