在ECMAScript中,兩個核心主題就是對象與函數,而這兩個主題也有些互相纏繞的,在前面幾個博文中大略的過了一遍函數相關的基礎知識,這篇文章再回到對象主題上來。函數
1、對象再認識測試
(1)對象屬性和特性this
什麼是屬性(Property),什麼是特性(Attribute),這有什麼區別?我不想也不會從語義學上去區分,對於這系列文章來講,屬性就是組成對象的一個部分,廣義上也包括對象的方法,而特性則是指被描述主體所具備的特徵,換句話說,屬性是咱們能夠經過編碼來訪問的具體存在,而特性則主要是爲了便於理解概念的抽象存在,固然,特性也能夠經過相應的屬性來具體外化。這一小節所講的對象屬性的特性就是對對象屬性特徵的一個描述,主要來自於ECMA-262規範的第5版,該規範使用兩個中括號的形式來描述不能直接訪問的內部特性。編碼
A、屬性類型(先給屬性分下類):spa
B、對象內部屬性prototype
內部屬性不能經過代碼直接訪問,它主要是爲了描述規範,也是給ECMAScript實現者參考的,而對於開發者來講,http://chang.fuhao2.com 瞭解這些能夠便於理解一些內部機制。好比在給一個屬性賦值時,在實現中會調用[[Put]]內部方法,而讀取一個屬性值時,則調用[[Get]]方法。code
全部對象公共的內部屬性orm |
個別對象特有的內部屬性對象 |
|||
名稱繼承 |
規範 |
名稱 |
規範 |
對象 |
[[Prototype]] |
Object/Null |
[[PrimitiveValue]] |
primitive |
Boolean|Date|Number|String |
[[Class]] |
String |
[[Construct]] |
SpecOp(a List of any) → Object |
new |
[[Extensible]] |
Boolean |
[[Call]] |
SpecOp(any, a List of any) → any|Reference |
call |
[[Get]] |
SpecOp (propName) →any |
[[HasInstance]] |
SpecOp(any) → Boolean |
Function |
[[GetOwnProperty]] |
SpecOp (propName) →Undefined|Property Descriptor |
[[Scope]] |
Lexical Environment |
Function |
[[GetProperty]] |
SpecOp (propName) →Undefined|Property Descriptor |
[[FormalParameters]] |
List of Strings |
Function |
[[Put]] |
SpecOp (propName, any, Boolean) |
[[Code]] |
ECMAScript code |
Function |
[[CanPut]] |
SpecOp (propName) → Boolean |
[[TargetFunction]] |
Object |
Function.prototype.bind |
[[HasProperty]] |
SpecOp (propName) → Boolean |
[[BoundThis]] |
any |
Function.prototype.bind |
[[Delete]] |
SpecOp (propName, Boolean) → Boolean |
[[BoundArguments]] |
List of any |
Function.prototype.bind |
[[DefaultValue]] |
SpecOp (Hint) → primitive |
[[Match]] |
SpecOp(String, index) → MatchResult |
RegExp |
[[DefineOwnProperty]] |
SpecOp (propName, PropDesc, Boolean) → Boolean |
[[ParameterMap]] |
Object |
|
說明:
C、屬性特性(用來描述屬性的特性)。
注意:
上述的默認是指經過構造函數或對象字面量建立的對象所自身擁有的屬性特性的默認值都爲true,而僅僅用Object.defineProperty()方法建立的對象屬性的特性前三個的默認值爲false。
1.當使用構造函數或者對象字面量建立的屬性和使用Object.defineProperty()方法建立的對象屬性的名字相同時,Object.defineProperty()方法建立的對象屬性的特性前三個默認值爲變爲true。
2.在 configurable , writable 特性都爲false 時,不能夠刪除屬性,不能修改屬性的任意特性。也就是說,不能再把configurable修改成true了。
在 configurable 特性爲 false 時且 writable 爲true時,能夠更改 value 和 writable 特性。
3.在 configurable 特性爲 true 時,無論writable是什麼值,均可以把數據屬性修改成訪問器屬性。
在未使用 defineProperty 的狀況下添加的數據屬性
若是您在未使用 Object.defineProperty、Object.defineProperties 或 Object.create 函數的狀況下添加數據屬性,則writable、enumerable 和 configurable 特性都將設置爲 true。在添加屬性後,可使用 Object.defineProperty 函數修改屬性。
咱們能夠經過Object.defineProperty()方法來修改屬性默認的特性。
可使用如下方式來添加數據屬性:
1賦值運算符 (=),以下所示:obj.color = "white";
2對象文本,以下所示:obj = { color: "white", height: 5 };
3構造函數,如使用構造函數定義類型中所述
內部特性 |
配置屬性 |
屬性類型 |
數據類型 |
默認值 |
含義 |
備註 |
[[Configurable]] |
configurable |
數據屬性 訪問器屬性 |
Boolean |
true |
可否經過delete刪除屬性從而從新定義屬性 可否修改屬性的特性 數據屬性和訪問器屬性是否能夠相互修改 |
一旦把屬性定義爲不可配置的,就不能再變爲可配置的 非嚴格模式下會忽略相應操做,嚴格模式下則拋出異常 |
[[Enumerable]] |
enumerable |
數據屬性 訪問器屬性 |
Boolean |
true |
可否經過for-in循環返回屬性 |
爲true時能夠經過for-in來枚舉,不然不能經過for-in枚舉 |
[[Writable]] |
writable |
數據屬性 |
Boolean |
true |
可否修改屬性的值 |
爲false時不能修改屬性值,非嚴格模式下會忽略相應操做,嚴格模式下則拋出異常 |
[[Value]] |
value |
數據屬性 |
任意類型 |
undefined |
屬性值 |
|
[[Get]] |
get |
訪問器屬性 |
Undefined/Function |
undefined |
讀取屬性時調用的函數 |
爲一個函數時,會無參數調用這個函數,並將返回值做爲屬性值返回 |
[[Set]] |
set |
訪問器屬性 |
Undefined/Function |
undefined |
寫入屬性時調用的函數 |
爲一個函數時,會將傳入的值做爲參數調用這個函數,賦給屬性 |
說明:
D、屬性定義方法(用來定義屬性的方法)
最多見的定義屬性的方法就是直接在對象上添加屬性,好比obj.name = 'linjisong',這種狀況下定義的屬性所具備的內部特性都是默認的,若是想定義一個值不能被修改的屬性要怎麼作呢?在ES中給咱們提供了幾個方法用於實現相似的功能。
方法名 |
功能說明 |
參數和返回值 |
說明 |
調用示例 |
defineProperty() |
定義一個屬性 |
(1)目標對象 (2)屬性的名字 (3)屬性描述符對象 |
使用屬性定義方法時 |
// 建立一個包含一個默認屬性job的對象(job屬性能夠修改、刪除、在for-in中枚舉) var person = {job:'it'}; // 添加一個不能被修改、刪除的name屬性 Object.defineProperty(person, 'name', { value:'linjisong',//這裏的配置屬性和上面特性列表中的配置屬性一致 enumerable:true }); // 定義多個屬性(數據屬性year和訪問器屬性age) Object.defineProperties(person, { year:{ value : 2012, configurable:true, writable:true }, age:{ get : function(){ return this.year-1983; } } });
var job = Object.getOwnPropertyDescriptor(person, 'job'); console.info(job.configurable);//true,直接添加屬性時默認爲true
var name = Object.getOwnPropertyDescriptor(person, 'name'); console.info(name.configurable);//false,使用屬性定義方法添加屬性時默認爲false console.info(person.name);//linjisong person.name = 'oulinhai';//因爲不能修改,因此值不會改變,在嚴格模式下會拋出異常 console.info(person.name);//linjisong
person.year = 2015; console.info(person.year);//2015 console.info(person.age);//32,在修改year的同時,也修改了age屬性
|
defineProperties() |
定義一組屬性 |
(1)目標對象 (2)多個屬性描述符組成的一個對象 |
||
getOwnPropertyDescriptor() |
獲取屬性的特性 |
(1)目標對象 (2)屬性的名字 (3)返回一個包括了屬性特性的對象 |
|
注:這些方法設置或獲取的屬性特殊和屬性的類型有關,好比數據屬性只能設置[[Confirurable]]、[[Enumerable]]、[[Writable]]、[[Value]]。
(2)防篡改對象
所謂防篡改對象,就是給對象必定級別的保護以防止在這個級別上對對象的變動,在ES5規範中,定義了依次升高的三種保護級別:
保護級別 |
描述 |
操做方法 |
判斷方法 |
說明 |
不可擴展 |
不能給對象添加新屬性和方法,但能夠修改已有屬性和方法 |
preventExtensions() |
isExtensible():不能擴展時返回false |
|
密封 |
不可擴展,而且已有成員的[[Configurable]]設置爲false,不能刪除屬性,但能夠修改屬性值 |
seal() |
isSeal():被密封時返回true |
isSeal()爲true時必定有isExtensible()爲false |
凍結 |
密封,其[[Writable]]設置爲false,但若是定義了[[Set]],訪問器屬性仍然可寫 |
freeze() |
isFrozen():被凍結時返回true |
isFrozen()爲true時必定有isSeal()爲true,isExtensible()爲false |
注:一旦定義成了防篡改對象,就不能撤銷。
(3)對象的其它方法
名稱 |
描述 |
create(prototype[,descriptors]) |
建立一個具備指定原型且可選擇性地包含指定屬性的對象 |
getOwnPropertyNames(object) |
返回對象的屬性(方法)的名稱 |
getPrototypeOf(object) |
返回對象的原型 |
keys(object) |
返回對象的可枚舉屬性(方法)的名稱 |
這裏的create(prototype[,descriptors])是一個很是有意思的方法,規範中這樣描述它的行爲:
①若是prototype不是Null或Object,拋出TypeError異常
②var obj = new Object()
③設置obj的內部屬性[[Prototype]]爲prototype
④若是descriptors存在且不爲undefined,使用Object.defineProperties(obj,descriptors)來添加屬性
⑤返回obj
因爲通常對象的[[Prototype]]不能直接訪問,可使用函數來進行下面模擬實現:
(function(){
function Base(){};
Object.create = function(prototype, descriptors){
var typeVal = typeof prototype;
if(typeVal !== null && typeVal !== 'object' && typeVal !== 'function'){
throw new TypeError('類型錯誤,請檢查第一個參數的類型');
}
Base.prototype = prototype;
var result = new Base();
if(descriptors){
Object.defineProperties(result, descriptors);
}
return result;
};
})();
測試一下:
try{
var one = Object.create(1);//異常
}catch(e){
console.info(e);//TypeError
}
var base = {
name:'linjisong',
getName:function(){
return this.name;
}
};
var two = Object.create(base);
console.info(two.name);//linjisong
console.info(two.getName());//linjisong
var three = Object.create(base, {
name:{value:'oulinhai'},
age:{value:23}
});
console.info(three.getName());//oulinhai
console.info(three.age);//23
這裏實現了一個簡單的繼承,這也引出下一個主題。
2、原型對象
(1)原型與原型鏈
每一個對象都有一個原型對象,而原型對象自己也是一個對象,也有本身的原型對象,這樣就造成了一個原型鏈直至null對象。對象的內部屬性[[Prototype]]指向的就是對象的原型對象,而Object.prototype的原型對象爲null???原型對象的原型對象?Object.prototype.prototype===undefined
(2)屬性查找
在訪問一個對象的屬性(方法)時,引擎會先查找對象自己有沒有這個屬性,若是有,返回這個屬性值,若是沒有,則查找對象的原型是否有這個屬性,若是有,返回,若是沒有就繼續查找原型對象的原型直至最後一個原型對象。
注意區分屬性查找和和前面說過的標識符查找的異同。屬性查找是沿着原型鏈,標識符查找是沿着做用域鏈,但都有一個逐級查找的過程。
(3)對象的原型對象[[Prototype]]與函數的原型屬性prototype
var obj = {};
console.info(obj.__proto__===Object.getPrototypeOf(obj));//true
console.info(obj.__proto__===Object.prototype);//true
var fn = function(){};
console.info(typeof fn.prototype);//object,一旦定義了函數,就會添加prototype屬性,指向原型對象
var obj1 = new fn();
console.info(fn.prototype === Object.getPrototypeOf(obj1));//true,全部使用fn建立的實例的原型對象都指向fn.prototype
var obj2 = new fn();
console.info(fn.prototype === Object.getPrototypeOf(obj2));//true
console.info(Object.getPrototypeOf(fn) === Function.prototype);//true
固然,fn自己也是一個對象,也有本身的原型對象,它的原型對象就是Function的屬性prototype了(fn.__proto__===Function.prototype)。
咱們知道,每個對象均可以訪問一個屬性constructor,指向建立這個對象的函數(構造函數),實際上,constructor屬性只不過是構造函數的原型對象的一個屬性而已,所以經過構造函數建立的實例都可以訪問constructor。
var fn = function fn(){};
var obj1 = new fn();
console.info(fn.constructor);//Function()
console.info(fn.prototype.constructor);//fn(),函數原型對象的constructor指向函數自己
console.info(obj1.hasOwnProperty('constructor'));//false,實例自己沒有constructor屬性
console.info(fn.prototype.constructor === obj1.constructor);//true,實例能夠訪問到原型對象中的constructor屬性
var fn = function fn(){};
var obj = new fn();
console.info(obj.name);//undefined
fn.prototype.name = 'linjisong';
console.info(obj.name);//linjisong
3、建立對象
建立方式 |
示例 |
說明 |
傳統方式 |
var person = new Object(); person.name = 'linjisong'; person.job = 'it'; |
傳統方式建立對象容易產生大量重複的代碼 |
對象字面量 |
var person = { name : 'linjisong', job : 'it' }; |
使用對象字面量建立簡潔明瞭,很是適合建立做爲函數實參的對象 |
工廠模式 |
function createPerson(name, job){ var o = new Object(); o.name = name; o.job = job; return o; } var person = createPerson('linjisong','it');
|
1、工廠模式能有效解決重複代碼問題。 2、可是不能斷定對象的類型 |
構造函數模式 |
function Person(name, job){ this.name = name; this.job = job; this.getName = function(){ return this.name; } } var person = new Person('linjisong','it');
|
構造函數模式能解決重複代碼問題,也可以斷定對象的類型 可是這種模式下建立的每一個實例都有一份屬性和方法的Copy 對於方法來講,每一個實例都保存一份是沒有必要的 使用new調用構造函數的內部步驟: (1)建立一個新對象 (2)將構造函數的做用域賦給新對象(構造函數內this指向新建立對象) (3)執行構造函數中的代碼 (4)返回新對象 |
原型模式 |
function Person(){} Person.prototype.name = 'linjisong'; Person.prototype.job = 'it; Person.prototype.getName = fucntion(){ return this.name; }; var person = new Person();
|
原型模式可以解決構造函數模式的方法實例有多個副本的問題 可是同時每一個實例的屬性也共享了,對於引用類型的屬性來講 這會致使很是嚴重的問題,修改一個實例的屬性會致使另外一個實例也修改 並且也不能接受參數
function Angle(){}; Angle.prototype.coordinate = [0,0];
var a1 = new Angle(); var a2 = new Angle();
a1.coordinate[0] = 1; console.info(a2.coordinate);//[1,0]修改a1會致使a2變動
|
組合構造原型模式 |
function Person(name, job){ this.name = name; this.job = job; } Person.prototype.getName = fucntion(){ return this.name; }; var person = new Person('linjisong','it');
|
結合構造函數模式和原型模式 使用構造函數模式建立屬性,每一個實例保存一份 使用原型模式共享方法,全部實例共享保存一份 這是目前使用最普遍的對象建立方式 |
動態原型模式 |
function Person(name, job){ this.name = name; this.job = job; if(!Person.prototype.getName){ Person.prototype.getName = fucntion(){ return this.name; }; } } var person = new Person('linjisong','it');
|
這種模式其實是對於不習慣將構造函數和原型分離而引入的 在判斷的時候,能夠只判斷其中一個屬性 |
寄生構造函數模式 |
function Person(name, job){ var o = new Object(); o.name = name; o.job = job; o.getName = fucntion(){ return this.name; }; return o; } var person = new Person('linjisong','it');
|
工廠模式不使用new,寄生構造函數模式使用new操做符 構造函數模式不返回,寄生構造函數模式返回對象 不能使用instanceof判斷類型 |
穩妥構造函數模式 |
function Person(name, job){ var o = new Object(); o.getName = fucntion(){ return name; }; return o; } var person = Person('linjisong','it');
|
穩妥對象:不使用this和new 穩妥構造模式相似寄生構造模式,但只能經過提供的方法訪問成員 不能使用instanceof判斷類型 |
各類建立對象的模式須要根據具體狀況來看,最經常使用的仍是對象字面量和組合構造原型方式。
4、繼承
在ECMAScript中,沒有接口繼承,只有實現繼承,這些繼承主要是經過原型鏈來實現的。像對象建立同樣,下面也經過一張表格來瀏覽一下一些實現繼承的方法。
繼承方式 |
示例 |
說明 |
原型鏈 |
function Square(){//正方形 this.width = 10;//邊長 this.coordinate = [0,0];//左上頂點的座標 } Square.prototype.getArea = function(){//計算面積 return this.width * this.width; };
function ColorSquare(){//有顏色的正方形 this.color = 'red'; } ColorSquare.prototype = new Square();//實現了繼承 ColorSquare.prototype.getColor = function(){//獲取顏色 return this.color; }
var cs = new ColorSquare(); console.info(cs.width);//10 console.info(cs.getArea());//100 console.info(cs.color);//red console.info(cs.getColor());//red
|
1、經過修改子類型建立函數的原型實現繼承。 2、經過原型給子類型添加新方法時,必定要在替換子類型原型以後添加,然後也不能經過對象字面量修改子類型的原型。 3、能夠經過兩種方法肯定原型和實例之間的關係:只要實例原型鏈中出現過構造函數fn,都返回true (1)instance instanceof fn (2)fn.prototype.isPrototype(instance) 4、使用原型鏈繼承時,建立子對象時沒法傳遞參數。 5、引用類型的父類屬性會被全部子類型實例共享從而產生問題: 修改一個子類型實例的引用類型屬性會致使其它全部子類型實例相應的修改 var cs2 = new ColorSquare(); console.info(cs2.coordinate);//[0,0] cs.coordinate[1] = 1; console.info(cs2.coordinate);//[0,1],修改cs會致使cs2也修改 |
借用構造函數 |
function Square(){//正方形 this.width = 10;//邊長 this.coordinate = [0,0];//左上頂點的座標 }
Square.prototype.getArea = function(){//計算面積 return this.width * this.width; };
function ColorSquare(){//有顏色的正方形 Square.call(this);//實現繼承 this.color = 'red'; }
var cs = new ColorSquare(); var cs2 = new ColorSquare(); console.info(cs.coordinate);//[0,0] console.info(cs2.coordinate);//[0,0] cs.coordinate[1] = 1; console.info(cs.coordinate);//[0,1] console.info(cs2.coordinate);//[0,0],互相獨立,修改cs不影響cs2 try{ console.info(cs.getArea());//異常,不能訪問父類原型中方法 }catch(e){ console.info(e);//TypeError }
|
1、使用借用構造函數時,能夠在call調用時傳遞參數。 2、同時也不存在引用類型共享的問題。 3、借用構造函數的缺點是,子類不能訪問父類原型中定義的方法 |
組合繼承 |
function Square(){//正方形 this.width = 10;//邊長 this.coordinate = [0,0];//左上頂點的座標 }
Square.prototype.getArea = function(){//計算面積 return this.width * this.width; };
function ColorSquare(){//有顏色的正方形 Square.call(this);//建立子類實例時,第二次調用父類構造函數 this.color = 'red'; }
ColorSquare.prototype = new Square();//第一次調用 ColorSquare.prototype.getColor = function(){//獲取顏色 return this.color; }
var cs = new ColorSquare(); var cs2 = new ColorSquare(); console.info(cs.coordinate);//[0,0] console.info(cs2.coordinate);//[0,0] cs.coordinate[1] = 1; console.info(cs.coordinate);//[0,1] console.info(cs2.coordinate);//[0,0],互相獨立,修改cs不影響cs2 console.info(cs.getArea());//100,能夠訪問
|
1、組合繼承也稱爲僞經典繼承,是將原型鏈和借用構造函數兩種方式結合起來的繼承方式。 2、基本思想是: (1)使用原型鏈實現對原型屬性和方法的繼承 (2)使用借用構造函數實現對實例屬性的繼承 3、組合繼承避免了原型鏈和借用構造函數的缺點,融合了它們的優勢,是最經常使用的繼承方式。 4、組合繼承的缺點是須要調用兩次父類的構造函數 |
原型式繼承 |
function create(o){ var fn = function(){}; fn.prototype = o; return new fn(); }
var square = { width:10, coordinate:[0,0] };
var cs = create(square); var cs2 = create(square); console.info(cs.coordinate);//[0,0] console.info(cs2.coordinate);//[0,0] cs.coordinate[1] = 1; console.info(cs.coordinate);//[0,1] console.info(cs2.coordinate);//[0,1],和原型鏈同樣,會有共享問題
|
1、這種方式實際上就是前面說的模擬ES5中create函數來實現繼承。 2、ES5及前面模擬的create還能夠接受另外的屬性描述參數。 3、和原型鏈與借用構造函數不一樣的是,這種方式須要先有一個對象,而後直接建立子對象。 前者是構造函數的繼承,然後者是對象實例的繼承。 4、和使用原型鏈繼承同樣,也會有引用類型實例屬性的共享問題。 |
寄生式繼承 |
function create(o){ var fn = function(){}; fn.prototype = o; return new fn(); }
var square = { width:10, coordinate:[0,0] };
function colorSquare(original){ var s = create(original); s.color = 'red'; return s; }
var cs = colorSquare(square); console.info(cs.width);//10 console.info(cs.coordinate);//[0,0]
|
1、首先,這裏的create函數不是必需的,任何返回新對象的函數均可以。 2、其次,這種模式也有引用類型實例屬性共享的問題。 3、這種方式,能夠當作將上面的對象繼承包裝成構造函數。 |
寄生組合式繼承 |
function create(o){ var fn = function(){}; fn.prototype = o; return new fn(); }
function inherit(sub, sup){ var prototype = create(sup.prototype); prototype.constructor = sub; sub.prototype = prototype; }
function Square(){//正方形 this.width = 10;//邊長 this.coordinate = [0,0];//左上頂點的座標 } Square.prototype.getArea = function(){//計算面積 return this.width * this.width; };
function ColorSquare(){//有顏色的正方形 Square.call(this); this.color = 'red'; } inherit(ColorSquare, Square); ColorSquare.prototype.getColor = function(){//獲取顏色 return this.color; }
var cs = new ColorSquare(); console.info(cs.width);//10 console.info(cs.getArea());//100 console.info(cs.color);//red console.info(cs.getColor());//red
var cs2 = new ColorSquare(); console.info(cs2.coordinate);//[0,0] cs.coordinate[1] = 1; console.info(cs2.coordinate);//[0,0]
|
1、這種方式只調用了一次父類構造函數,從而避免了在子類型的原型對象上建立沒必要要的屬性。 2、可以保證原型鏈不變,從而能夠正常使用instanceof和isPrototypeOf()。 |