1面向?qū)ο蟮某绦蛟O(shè)計(jì)
1.1 屬性類(lèi)型
ECMAScript 中有兩種屬性:數(shù)據(jù)屬性和訪問(wèn)器屬性。
1.1.1數(shù)據(jù)屬性
數(shù)據(jù)屬性包含一個(gè)數(shù)據(jù)值的位置。在這個(gè)位置可以讀取和寫(xiě)入值。數(shù)據(jù)屬性有 4 個(gè)描述其行為的特性。
[[Configurable]]:表示能否通過(guò) delete 刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問(wèn)器屬性。像前面例子中那樣直接在對(duì)象上定義的屬性,它們的這個(gè)特性默認(rèn)值為 true。
[[Enumerable]]:表示能否通過(guò) for-in 循環(huán)返回屬性。像前面例子中那樣直接在對(duì)象上定義的屬性,它們的這個(gè)特性默認(rèn)值為 true。
[[Writable]]:表示能否修改屬性的值。像前面例子中那樣直接在對(duì)象上定義的屬性,它們的這個(gè)特性默認(rèn)值為 true。
[[Value]]:包含這個(gè)屬性的數(shù)據(jù)值。讀取屬性值的時(shí)候,從這個(gè)位置讀;寫(xiě)入屬性值的時(shí)候,把新值保存在這個(gè)位置。這個(gè)特性的默認(rèn)值為 undefined。
var person = {}; Object.defineProperty(person, "name", {
writable: false,
value: "Nicholas" });
alert(person.name); //"Nicholas" person.name = "Greg"; alert(person.name); //"Nicholas"
這個(gè)例子創(chuàng)建了一個(gè)名為 name 的屬性,它的值"Nicholas"是只讀的。這個(gè)屬性的值是不可修改 的,如果嘗試為它指定新值,則在非嚴(yán)格模式下,賦值操作將被忽略;在嚴(yán)格模式下,賦值操作將會(huì)導(dǎo) 致拋出錯(cuò)誤。
把 configurable 設(shè)置為 false,表示不能從對(duì)象中刪除屬性。如果對(duì)這個(gè)屬性調(diào)用 delete,則 在非嚴(yán)格模式下什么也不會(huì)發(fā)生,而在嚴(yán)格模式下會(huì)導(dǎo)致錯(cuò)誤。而且,一旦把屬性定義為不可配置的, 就不能再把它變回可配置了。此時(shí),再調(diào)用 Object.defineProperty()方法修改除 writable 之外 的特性,都會(huì)導(dǎo)致錯(cuò)誤。也就是說(shuō),可以多次調(diào)用 Object.defineProperty()方法修改同一個(gè)屬性,但在把configurable 特性設(shè)置為 false 之后就會(huì)有限制了。在調(diào)用 Object.defineProperty()方法時(shí),如果不指定,configurable、enumerable 和 writable 特性的默認(rèn)值都是 false。
1.1.2 訪問(wèn)器屬性
訪問(wèn)器屬性不包含數(shù)據(jù)值;它們包含一對(duì)兒 getter 和 setter 函數(shù)(不過(guò),這兩個(gè)函數(shù)都不是必需的)。 在讀取訪問(wèn)器屬性時(shí),會(huì)調(diào)用 getter 函數(shù),這個(gè)函數(shù)負(fù)責(zé)返回有效的值;在寫(xiě)入訪問(wèn)器屬性時(shí),會(huì)調(diào)用 setter 函數(shù)并傳入新值,這個(gè)函數(shù)負(fù)責(zé)決定如何處理數(shù)據(jù)。訪問(wèn)器屬性有如下 4 個(gè)特性。
- [[Configurable]]:表示能否通過(guò) delete 刪除屬性從而重新定義屬性,能否修改屬性的特 性,或者能否把屬性修改為數(shù)據(jù)屬性。對(duì)于直接在對(duì)象上定義的屬性,這個(gè)特性的默認(rèn)值為 true。
- [[Enumerable]]:表示能否通過(guò) for-in 循環(huán)返回屬性。對(duì)于直接在對(duì)象上定義的屬性,這 5 個(gè)特性的默認(rèn)值為 true。
- [[Get]]:在讀取屬性時(shí)調(diào)用的函數(shù)。默認(rèn)值為 undefined。
- [[Set]]:在寫(xiě)入屬性時(shí)調(diào)用的函數(shù)。默認(rèn)值為 undefined。
訪問(wèn)器屬性不能直接定義,必須使用 Object.defineProperty()來(lái)定義。
1.2 創(chuàng)建對(duì)象
1.2.1工廠模式
function createPerson(name, age, job){ var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name); };
return o; }
函數(shù) createPerson()能夠根據(jù)接受的參數(shù)來(lái)構(gòu)建一個(gè)包含所有必要信息的 Person 對(duì)象??梢詿o(wú)數(shù)次地調(diào)用這個(gè)函數(shù),而每次它都會(huì)返回一個(gè)包含三個(gè)屬性一個(gè)方法的對(duì)象。工廠模式解決了創(chuàng)建 多個(gè)相似對(duì)象的問(wèn)題.
但卻沒(méi)有解決對(duì)象識(shí)別的問(wèn)題(即怎樣知道一個(gè)對(duì)象的類(lèi)型)。
1.2.2構(gòu)造函數(shù)模式
function Person(name, age, job){ this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){ alert(this.name);
}; }
要?jiǎng)?chuàng)建 Person 的新實(shí)例,必須使用 new 操作符。以這種方式調(diào)用構(gòu)造函數(shù)實(shí)際上會(huì)經(jīng)歷以下 4 個(gè)步驟:
(1) 創(chuàng)建一個(gè)新對(duì)象;
(2) 將構(gòu)造函數(shù)的作用域賦給新對(duì)象(因此 this 就指向了這個(gè)新對(duì)象);
(3) 執(zhí)行構(gòu)造函數(shù)中的代碼(為這個(gè)新對(duì)象添加屬性);
(4) 返回新對(duì)象。
構(gòu)造函數(shù)模式雖然好用,但也并非沒(méi)有缺點(diǎn)。使用構(gòu)造函數(shù)的主要問(wèn)題,就是每個(gè)方法都要在每個(gè) 實(shí)例上重新創(chuàng)建一遍。
1.2.3原型模式
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name); };
1.2.4組合使用構(gòu)造函數(shù)和原型模式
function Person(name, age, job){
this.name = name; 3 this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = { constructor : Person, sayName : function(){
alert(this.name); }
}
1.2.5寄生構(gòu)造函數(shù)模式
function Person(name, age, job){ var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name); };
return o; }
1.2.6穩(wěn)妥的構(gòu)造函數(shù)模式
function Person(name, age, job){
//創(chuàng)建要返回的對(duì)象
var o = new Object();
//可以在這里定義私有變量和函數(shù)
//添加方法
o.sayName = function(){
alert(name);
};
//返回對(duì)象
return o;
}
1.2.7繼承
可以通過(guò)兩種方式來(lái)確定原型和實(shí)例之間的關(guān)系。第一種方式是使用 instanceof 操作符,只要用 這個(gè)操作符來(lái)測(cè)試實(shí)例與原型鏈中出現(xiàn)過(guò)的構(gòu)造函數(shù),結(jié)果就會(huì)返回 true。
alert(instance instanceof Object); //true
第二種方式是使用 isPrototypeOf()方法。同樣,只要是原型鏈中出現(xiàn)過(guò)的原型,都可以說(shuō)是該 原型鏈所派生的實(shí)例的原型,因此 isPrototypeOf()方法也會(huì)返回 true.
alert(Object.prototype.isPrototypeOf(instance)); //true
1.2.8 class類(lèi)
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
上面代碼定義了一個(gè)“類(lèi)”,可以看到里面有一個(gè)constructor方法,這就是構(gòu)造方法,而this關(guān)鍵字則代表實(shí)例對(duì)象。也就是說(shuō),ES5 的構(gòu)造函數(shù)Point,對(duì)應(yīng) ES6 的Point類(lèi)的構(gòu)造方法。
Point類(lèi)除了構(gòu)造方法,還定義了一個(gè)toString方法。注意,定義“類(lèi)”的方法的時(shí)候,前面不需要加上function這個(gè)關(guān)鍵字,直接把函數(shù)定義放進(jìn)去了就可以了。另外,方法之間不需要逗號(hào)分隔,加了會(huì)報(bào)錯(cuò)。
class Point {
// ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true
上面代碼表明,類(lèi)的數(shù)據(jù)類(lèi)型就是函數(shù),類(lèi)本身就指向構(gòu)造函數(shù)。
constructor方法是類(lèi)的默認(rèn)方法,通過(guò)new命令生成對(duì)象實(shí)例時(shí),自動(dòng)調(diào)用該方法。一個(gè)類(lèi)必須有constructor方法,如果沒(méi)有顯式定義,一個(gè)空的constructor方法會(huì)被默認(rèn)添加。
類(lèi)相當(dāng)于實(shí)例的原型,所有在類(lèi)中定義的方法,都會(huì)被實(shí)例繼承。如果在一個(gè)方法前,加上static關(guān)鍵字,就表示該方法不會(huì)被實(shí)例繼承,而是直接通過(guò)類(lèi)來(lái)調(diào)用,這就稱(chēng)為“靜態(tài)方法”。
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
注意,如果靜態(tài)方法包含this關(guān)鍵字,這個(gè)this指的是類(lèi),而不是實(shí)例。
class Foo {
static bar() {
this.baz();
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}
Foo.bar() // hello
父類(lèi)的靜態(tài)方法,可以被子類(lèi)繼承。
實(shí)例屬性除了定義在constructor()方法里面的this上面,也可以定義在類(lèi)的最頂層。
class foo {
bar = 'hello';
baz = 'world';
constructor() {
// ...
}
}
上面的代碼,一眼就能看出,foo類(lèi)有兩個(gè)實(shí)例屬性,一目了然。另外,寫(xiě)起來(lái)也比較簡(jiǎn)潔。
1.2.9 class繼承
Class 可以通過(guò)extends關(guān)鍵字實(shí)現(xiàn)繼承,這比 ES5 的通過(guò)修改原型鏈實(shí)現(xiàn)繼承,要清晰和方便很多。
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 調(diào)用父類(lèi)的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 調(diào)用父類(lèi)的toString()
}
}
上面代碼中,constructor方法和toString方法之中,都出現(xiàn)了super關(guān)鍵字,它在這里表示父類(lèi)的構(gòu)造函數(shù),用來(lái)新建父類(lèi)的this對(duì)象。
子類(lèi)必須在constructor方法中調(diào)用super方法,否則新建實(shí)例時(shí)會(huì)報(bào)錯(cuò)。這是因?yàn)樽宇?lèi)自己的this對(duì)象,必須先通過(guò)父類(lèi)的構(gòu)造函數(shù)完成塑造,得到與父類(lèi)同樣的實(shí)例屬性和方法,然后再對(duì)其進(jìn)行加工,加上子類(lèi)自己的實(shí)例屬性和方法。如果不調(diào)用super方法,子類(lèi)就得不到this對(duì)象。
ES5 的繼承,實(shí)質(zhì)是先創(chuàng)造子類(lèi)的實(shí)例對(duì)象this,然后再將父類(lèi)的方法添加到this上面(Parent.apply(this))。ES6 的繼承機(jī)制完全不同,實(shí)質(zhì)是先將父類(lèi)實(shí)例對(duì)象的屬性和方法,加到this上面(所以必須先調(diào)用super方法),然后再用子類(lèi)的構(gòu)造函數(shù)修改this。
最后,父類(lèi)的靜態(tài)方法,也會(huì)被子類(lèi)繼承。
2.函數(shù)表達(dá)式
2.1 閉包
閉包是指有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中的變量的函數(shù)。創(chuàng)建閉包的常見(jiàn)方式,就是在一個(gè)函數(shù)內(nèi)部創(chuàng)建另一個(gè)函數(shù)。
當(dāng)某個(gè)函數(shù)被調(diào)用時(shí),會(huì)創(chuàng)建一個(gè)執(zhí)行環(huán)境(execution context)及相應(yīng)的作用域鏈。 然后,使用 arguments 和其他命名參數(shù)的值來(lái)初始化函數(shù)的活動(dòng)對(duì)象(activation object)。但在作用域鏈中,外部函數(shù)的活動(dòng)對(duì)象始終處于第二位,外部函數(shù)的外部函數(shù)的活動(dòng)對(duì)象處于第三位,......直至作為作用域鏈終點(diǎn)的全局執(zhí)行環(huán)境。 在函數(shù)執(zhí)行過(guò)程中,為讀取和寫(xiě)入變量的值,就需要在作用域鏈中查找變量。
作用域鏈的這種配置機(jī)制引出了一個(gè)值得注意的副作用,即閉包只能取得包含函數(shù)中任何變量的最后一個(gè)值。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i; };
}
return result;
}
這個(gè)函數(shù)會(huì)返回一個(gè)函數(shù)數(shù)組。表面上看,似乎每個(gè)函數(shù)都應(yīng)該返自己的索引值,即位置 0 的函數(shù) 返回 0,位置 1 的函數(shù)返回 1,以此類(lèi)推。但實(shí)際上,每個(gè)函數(shù)都返回 10。因?yàn)槊總€(gè)函數(shù)的作用域鏈中 都保存著 createFunctions()函數(shù)的活動(dòng)對(duì)象,所以它們引用的都是同一個(gè)變量 i。當(dāng) createFunctions()函數(shù)返回后,變量 i 的值是 10,此時(shí)每個(gè)函數(shù)都引用著保存變量 i 的同一個(gè)變量 對(duì)象,所以在每個(gè)函數(shù)內(nèi)部 i 的值都是 10。但是,我們可以通過(guò)創(chuàng)建另一個(gè)匿名函數(shù)強(qiáng)制讓閉包的行為 9 符合預(yù)期,如下所示。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(num){
return function(){
return num;
}
}(i)
}
return result;
}
2.2 this
在閉包中使用 this 對(duì)象也可能會(huì)導(dǎo)致一些問(wèn)題。我們知道,this 對(duì)象是在運(yùn)行時(shí)基于函數(shù)的執(zhí)行環(huán)境綁定,在全局函數(shù)中,this 等于 window,而當(dāng)函數(shù)被作為某個(gè)對(duì)象的方法調(diào)用時(shí),this 等 于那個(gè)對(duì)象。不過(guò),匿名函數(shù)的執(zhí)行環(huán)境具有全局性,因此其 this 對(duì)象通常指向 window。