1、對象再認識
(1)對象屬性和特性
什麼是屬性(Property),什麼是特性(Attribute),這有什麼區別?我不想也不會從語義學上去區分,對於這系列文章來說,屬性就是組成對象的一個部分,廣義上也包括對象的方法,而特性則是指被描述主體所具有的特征,換句話說,屬性是我們可以通過編碼來訪問的具體存在,而特性則主要是為了便於理解概念的抽象存在,當然,特性也可以通過相應的屬性來具體外化。這一小節所講的對象屬性的特性就是對對象屬性特征的一個描述,主要來自於ECMA-262規范的第5版,該規范使用兩個中括號的形式來描述不能直接訪問的內部特性。
A、屬性類型(先給屬性分下類):
B、對象內部屬性
內部屬性不能通過代碼直接訪問,它主要是為了描述規范,也是給ECMAScript實現者參考的,而對於開發者來說,了解這些可以便於理解一些內部機制。比如在給一個屬性賦值時,在實現中會調用[[Put]]內部方法,而讀取一個屬性值時,則調用[[Get]]方法。
所有對象公共的內部屬性 個別對象特有的內部屬性 名稱 規范 名稱 規范 對象 [[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) [復制代碼 代碼如下:] 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、屬性特性(用來描述屬性的特性)
內部特性 配置屬性 屬性類型 數據類型 默認值 含義 備注 [[Configurable]] configurable數據屬性
訪問器屬性
Boolean
true
能否通過delete刪除屬性從而重新定義屬性
能否修改屬性的特性
能否把屬性修改為訪問器特性
一旦把屬性定義為不可配置的,就不能再變為可配置的
如果為false,不能做刪除、也不能修改屬性特性,但是允許修改屬性值
非嚴格模式下會忽略相應操作,嚴格模式下則拋出異常
[[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)屬性描述符對象
使用屬性定義方法時
[[Enumerable]]
[[Configurable]]
[[Writable]]
默認值為false
(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])是一個非常有意思的方法,規范中這樣描述它的行為:
[code]
①如果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。
(2)屬性查找
在訪問一個對象的屬性(方法)時,引擎會先查找對象本身有沒有這個屬性,如果有,返回這個屬性值,如果沒有,則查找對象的原型是否有這個屬性,如果有,返回,如果沒有就繼續查找原型對象的原型直至最後一個原型對象。
注意區分屬性查找和和前面說過的標識符查找的異同。屬性查找是沿著原型鏈,標識符查找是沿著作用域鏈,但都有一個逐級查找的過程。
(3)對象的原型對象[[Prototype]]與函數的原型屬性prototype
•每一個對象都有一個原型對象,在規范中使用[[Prototype]]來表示,這個對象一般不能直接訪問,但可以通過getPrototypeOf()這個方法來獲取,而在Firefox中還可以通過__proto__直接訪問,來驗證一下:
復制代碼 代碼如下:
var obj = {};
console.info(obj.__proto__===Object.getPrototypeOf(obj));//true
console.info(obj.__proto__===Object.prototype);//true
•每一個函數都有一個屬性prototype,這個屬性是在函數定義過程中添加的,它指向的對象就是所有使用該函數創建的實例對象的原型對象。
復制代碼 代碼如下:
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()。