
js是一個基于對象的語言,所以本文研究一下js對象和類實現(xiàn)的過程和原理。
對象的屬性及屬性特性
下面是一個對象的各個部分:
var person = {
name: "Lily",
age: 10,
work: function(){
console.log("Lily is working...");
}
};
person.gender = "F"; //可以動態(tài)添加屬性
Object.defineProperty(person, "salary", { //添加屬性
value: 10000,
writable: true, //是否可寫,默認(rèn)false
enumerable: true, //是否可枚舉,默認(rèn)false
configuration: true //是否可配置,默認(rèn)false;
});
Object.defineProperties(person, { //添加多個屬性
"father": {
value: Bob,
enumerable: true
},
"mather": {
value: Jelly,
enumerable: true
}
});
delete person.age; // 刪除屬性
Object.getOwnPropertyDescriptor(person, "father"); //{
value:10000,writable:true,enumerable:true,configuration:true}
是否可寫指得是其值是否可修改;
是否可枚舉指的是其值是否可以被for...in...遍歷到;
是否可配置指的是其可寫性,可枚舉性,可配置性是否可修改,并且決定該屬性可否被刪除。
這是一個普通的對象和常見操作,不多說,下面是一個具有g(shù)et/set的對象:
var person = {
_age: 11,
get age(){
return this._age;
},
set age(val){
this._age = val;
}
};
//如下方法訪問:
console.log(o.age); //讀
o.age = 30; //寫
console.log(o.age);
對象的特性
上文,我們只提到了對象屬性的4個性質(zhì),對象自己其實也有3個性質(zhì):
可擴展性
可不可擴展是指一個對象可不可以添加新的屬性;Object.preventExtensions 可以讓這個對象變的不可擴展。嘗試給一個不可擴展對象添加新屬性的操作將會失敗,但不會有任何提示,(嚴(yán)格模式下會拋出TypeError異常)。Object.preventExtensions只能阻止一個對象不能再添加新的自身屬性,仍然可以為該對象的原型添加屬性,但proto屬性的值也不能修改。
var person = {
name: "Lily",
age: 10
}; //新創(chuàng)建的對象默認(rèn)是可擴展的
console.log(Object.isExtensible(person)); //true
person.salary = 10000;
console.log(person.salary) //10000
Object.preventExtensions(person);//將其變?yōu)椴豢蓴U展對象
console.log(Object.isExtensible(person)); //false
person.height = 180; //失敗,不拋出錯誤
console.log(person.height); //undefined
person.__proto__.height = 180; //在其原型鏈上添加屬性
console.log(person.height); //180
delete person.age; //可以刪除已有屬性
console.log(person.age); //undefined
person.__proto__ = function a(){}; //報錯TypeError: #<Object> is not extensible(…)
function fun(){
'use strict'
person.height = 180; //報錯TypeError: #<Object> is not extensible(…)
}
fun();
Object.defineProperty("height", {
value: 180
}); //由于函數(shù)內(nèi)部采用嚴(yán)格模式,所以報錯TypeError: #<Object> is not extensible(…)
<small>這里如果不理解__proto__不要緊,下文會重點解釋這個屬性</small>
密封性
如果我們想讓一個對象即不可擴展,又讓它的所有屬性不可配置,一個個修改屬性的configurable太不現(xiàn)實了,我們把這樣的對象叫做密封的(Sealed)。用Object.isSealed()判斷一個對象是否密封的,用Object.seal()密封一個對象。 其特性包括不可擴展對象和不可配置屬性的相關(guān)特性。
var person = {
name: "Lily",
age: 10
}; //新創(chuàng)建的對象默認(rèn)是不密封的
console.log(Object.isSeal(person)); //false
Object.seal(person);//將其變?yōu)槊芊鈱ο?console.log(Object.isSeal(person)); //true
delete person.age; //無法刪除已有屬性,失敗,不報錯。但嚴(yán)格模式會報錯
console.log(person.age); //undefined
person.__proto__ = function a(){}; //報錯TypeError: #<Object> is not extensible(...)
凍結(jié)性
此時,這個對象屬性可能還是可寫的,如果我們想讓一個對象的屬性既不可寫也不可配置,同時讓該對象不可擴展,那么就需要凍結(jié)這個對象。用Object.freeze()凍結(jié)對象,用isFrozen()判斷對象是否被凍結(jié)。由于相比上一個例子,僅僅是現(xiàn)有的變得不可寫了,這里就不舉太多例子了。
不過值得注意的是,對于具有setter的屬性一樣不可寫。
var person = {
name: "Lily",
_age: 10,
get age(){
return this._age;
},
set age(val){
this._age = val;
}
}; //新創(chuàng)建的對象默認(rèn)不是凍結(jié)的
console.log(Object.isFrozen(person)); //false
Object.freeze(person);//將其變?yōu)椴豢蓴U展對象
console.log(Object.isExtensible(person)); //false
console.log(Object.isSealed(person)); //true
console.log(Object.isFrozen(person)); //true
console.log(person.name); //"Lily"
person.name = "Bob"; //失敗,但不報錯,但嚴(yán)格模式會報錯。
console.log(person.name); //"Lily"
console.log(person.age); //10
person.age = 30;
console.log(person.age); //10
深凍結(jié)和淺凍結(jié)
深凍結(jié)和淺凍結(jié)的主要差異出現(xiàn)在可擴展性上,所以你也可以理解為深可擴展和淺可擴展。我們看一下以下代碼:
var person = {
addr: {}
}
Object.freeze(person);
person.addr.province = "Guangzhou"; //淺凍結(jié):對象的屬性對象可以繼續(xù)擴展
console.log(person.addr.province); //"Guangzhou"
為了實現(xiàn)深凍結(jié),我們寫一個函數(shù):
var person = {
name: "nihao",
addr: {},
family:{
slibing:{},
parents:{}
}
}
deepFreeze(person);
person.addr.province = "Guangzhou"; //深凍結(jié):對象的屬性對象無法繼續(xù)擴展
console.log(person.addr.province); //undefined
person.family.parents.father = "Bob"; //深凍結(jié):對象的屬性對象無法繼續(xù)擴展
console.log(person.family.parents.father); //undefined
function deepFreeze(obj){
Object.freeze(obj);
for(key in obj){
if(!obj.hasOwnProperty(key)) continue;
if(obj[key] !== Object(obj[key])) continue;
deepFreeze(obj[key]); //遞歸調(diào)用
}
}
注意,這里遞歸沒有判斷鏈表是否成環(huán),判斷有環(huán)鏈表是數(shù)據(jù)結(jié)構(gòu)的知識,可以使用一組快慢指針實現(xiàn),這里不贅述。因此在以下情況會有一個bug:
function Person(pname, sname){
this.name = pname || "";
this.spouse = sname || {};
}
var p1 = new Person("Lily");
var p2 = new Person("Bob", p1);
p1.spouse = p2;
deepFreeze(p1); //會陷入無休止的遞歸。實際家庭成員關(guān)系更復(fù)雜,就更糟糕了。RangeError: Maximum call stack size exceeded(…)
構(gòu)造函數(shù)(Constructor)
當(dāng)我們想創(chuàng)建很多個人的時候,就不會像上面這樣一個一個寫了。那我們就造一個工廠,用來生產(chǎn)人(感覺有點恐怖):
function CreatePerson(pname, page){
return {
name: pname,
age: page
};
}
p1 = CreatePerson("Lily", 21);
p2 = CreatePerson("Bob", 12);
console.log(p1); //Object {name: "Lily", age: 21}
console.log(p2); //Object {name: "Bob", age: 12}
但是這樣寫并不符合傳統(tǒng)的編程思路。因此我們需要一個構(gòu)造函數(shù)(constructor, 也有書譯為構(gòu)造器)
關(guān)于構(gòu)造函數(shù)和普通函數(shù)的區(qū)別可以看javascript中this詳解中”構(gòu)造函數(shù)中的this"一節(jié)
。
下面定義一個構(gòu)造函數(shù):
function Person(pname, page){
this.name = pname;
this.age = page;
this.work = function(){
console.log(this.name + " is working...");
};
}
var p1 = new Person("Lily",23);
var p2 = new Person("Lucy", 21);
console.log(p1);
p1.work();
console.log(p2);
p2.work();
不過這樣寫這個函數(shù),每個對象都會包括一部分,太浪費內(nèi)存。所以我們會把公共的部分放在prototype中:
function Person(pname, page){
this.name = pname;
this.age = page;
}
Person.prototype.work = function(){
console.log(this.name + " is working...");
};
var p1 = new Person("Lily",23);
var p2 = new Person("Lucy", 21);
console.log(p1);
p1.work();
console.log(p2);
p2.work();
通過上面的輸出,我們看到,每個對象(p1,p2)都包含了一個__proto__屬性,這個是一個非標(biāo)準(zhǔn)屬性(ES6已經(jīng)把它標(biāo)準(zhǔn)化了),不過IE中沒有這個屬性。
原型鏈與繼承
在學(xué)習(xí)原型鏈之前我們一定要區(qū)分清楚:prototype是構(gòu)造函數(shù)的屬性,而__proto__是對象的屬性。當(dāng)然我們依然用代碼說話:
再來一段代碼:
function Person(pname, page){
this.name = pname;
this.age = page;
}
Person.prototype.work = function(){
console.log(this.name + " is working...");
};
var p = new Person("Lily",23);
console.log(p.constructor); //function Person(){...}
console.log(p.__proto__); //Object
console.log(Person.prototype); //Object
console.log(Person.prototype.constructor); //function Person(){...}
console.log(Person.__proto__);
console.log(Person.constructor);
console.log(Person.__proto__); //空函數(shù)function(){}
console.log(Person.constructor); //function Function(){...}
說到這里,就有必要學(xué)習(xí)一下原型鏈了。
js沒有類的概念,這樣就不會有繼承派生和多態(tài),但是實際編程中我們需要這樣的結(jié)構(gòu),于是js在發(fā)展過程中,就從一個沒有類的語言模擬出來類的效果,這里靠的就是prototype。
一個構(gòu)造函數(shù)的prototype永遠(yuǎn)指向他的父對象,這樣這個構(gòu)造函數(shù)new出來的對象就可以訪問其父對象的成員,實現(xiàn)了繼承。
如果他的父對象的prototype又指向一個父對象的父對象,這樣一層層就構(gòu)成了原型鏈。如下(用瀏覽器內(nèi)置對象模型舉例):
console.log(HTMLDocument);
console.log(HTMLDocument.prototype); //HTMLDocument對象
console.log(HTMLDocument.prototype.constructor.prototype);
console.log(HTMLDocument.prototype.constructor.prototype.constructor.prototype);
console.log(HTMLDocument.prototype.constructor.prototype.constructor.prototype.constructor.prototype);
console.log(HTMLDocument.prototype.constructor.prototype.constructor.prototype.constructor.prototype.constructor.prototype);
/*......*/
如果你覺得這里應(yīng)該有一張圖,那就看看這個完整的對象關(guān)系圖(基于DOM),下文的相關(guān)例子也基于這個圖:

注意:原型鏈?zhǔn)怯懈F的,他總會指向Object,然后是null結(jié)束
那么__proto__是什么?一言以蔽之:對象的__proto__屬性指向該對象構(gòu)造函數(shù)的原型。如下:
function Person(pname, page){
this.name = pname;
this.age = page;
this.work = function(){
console.log(this.name + " is working...");
};
}
var o = new Person("Lily",23);
o.__proto__ === Person.prototype //true
上面圖中發(fā)現(xiàn),對象還有一個constructor屬性,這個屬性也很重要,新創(chuàng)建對象的constructor指向默認(rèn)對象的構(gòu)造函數(shù)本身,不過現(xiàn)實沒有這么簡單。例如:
function Person(){
}
var p1 = new Person();
console.log(p1.constructor); //function Person(){...}
function Children(){
}
Children.prototype = p1;//這一行和下一行聯(lián)立使用,不能忽略下一行
Children.prototype.constructor = Children; //修正constructor,這個不能省略
console.log(Person.prototype.constructor); //function Person(){...}
console.log(p1.constructor); //function Child(){...}
當(dāng)我們建立了一個繼承關(guān)系后,會使新的構(gòu)造函數(shù)的prototype.constructor指向改構(gòu)造函數(shù)自己,像上面第9行一樣。從第11行也可以看出,系統(tǒng)本身也是這樣做的。這樣就構(gòu)成了下面這個圖的關(guān)系,此時父對象的constructor指向子構(gòu)造函數(shù):

<small>注: 圖片來自網(wǎng)絡(luò)</small>
從上面的這些例子我們不難發(fā)現(xiàn),函數(shù)也是一個對象。因此構(gòu)造函數(shù)也有了constructor和proto屬性。不過這里會比較簡單:函數(shù)的constructor都是Function(){...};函數(shù)的__proto__都是個空函數(shù)
其實在js中除了基本類型(null, undefined, String, Number, Boolean, Symbol)以外,都是對象??赡苣阆敕瘩g我:“js中一切都是對象”。我們看以下幾個例子:
//以數(shù)字類型為例
var a = 1; //基本類型
console.log(a); //1
console.log(typeof a); //number
var b = new Number(1); //對象類型的數(shù)字
console.log(b); //Number {[[PrimitiveValue]]: 1}
console.log(typeof b); //object
首先,js中基本類型中除了null和undefined以外的類型,都具有對象形式。但對象形式不等于基本類型。從上面的輸出結(jié)果來看,var a = 1;和var a = new Number(1);完全不是一回事。你或許會反駁我:"a有方法呀,基本類型怎么會有方法!!",我們再看下一個例子:
var a = 1;
console.log(a.toFixed(2)); //1.00
var b = new Number(1);
console.log(b + 2); //3
上面的例子看似基本類型a有了方法,對象又可以參與運算。實際上這是隱式類型轉(zhuǎn)換的結(jié)果,上面第二行,瀏覽器自動調(diào)用了new Number()把a轉(zhuǎn)換成了對象,而第四行利用ValueOf()方法把對象轉(zhuǎn)換成了數(shù)字。
既然函數(shù)也是個對象,那么我們不僅可以用構(gòu)造函數(shù)new一個對象出來,也可以為它定義私有方法(變量)和靜態(tài)方法
function Person(pname){
var age = 10; //私有變量,外面訪問不到
function getAge(){ //私有方法,外面訪問不到
console.log(age);
}
this.name = pname;
this.getInfo = function(){ //公有方法,也可以定義在prototype中
console.log(this.name);
getAge.call(this); //注意這里的作用域和調(diào)用方式
};
};
Person.speak = function(){console.log("I am a person");}; //靜態(tài)方法
var p = new Person("Bob");
p.getInfo(); //Bob 10
Person.speak(); //"I am a person"
當(dāng)然實現(xiàn)簡單的對象繼承不用這么復(fù)雜,可以使用Object.create(obj);返回一個繼承與obj的對象。對與Object.create()方法需要考慮一下幾種情況:
var o = {};
var r1 = Object.create(o); //創(chuàng)建一個r1繼承于o
var r2 = Object.create(null); //創(chuàng)建一個r2繼承于null
var r3 = Object.create(Object); //創(chuàng)建一個r3繼承于Object
console.log(r1); //是一個繼承自o的對象
console.log(r2); //是一個空對象,沒有__proto__屬性
console.log(r3); //是一個函數(shù)
有了先前的知識,我們可以寫出來一個函數(shù)實現(xiàn)Object.create()
function inherit(o){
//if(Object.create) return Object.create(o);
if(o !== Object(o) && o !== null) throw TypeError("Object prototype may only be an Object or null");
function newObj(){};
newObj.prototype = o || {};
var result = new newObj();
if(o === null) result.__proto__ = null;
return result;
}
var obj = {};
console.log(Object.create(obj));
console.log(inherit(obj));
console.log(Object.create(null));
console.log(inherit(null));
console.log(Object.create(Object));
console.log(inherit(Object));
看了這么多,怎么寫繼承比較合理,我們實現(xiàn)2個構(gòu)造函數(shù),讓Coder繼承Person。比較以下3種方法:
function Person(pname){
this.name = pname;
}
function Coder(){}
//方法一:共享原型
Coder.prototype = Person.prototype;
//方法二:實例繼承
Coder.prototype = new Person("Lily");
Coder.prototype.constructor = Coder;
//方法三:本質(zhì)上還是實例繼承
Coder.prototype = Object.create(Person.prototype);
當(dāng)然還有其他的繼承方法:
//方法4:構(gòu)造繼承
function Person(pname){
this.name = pname;
}
function Coder(pname){
Person.apply(this, argument);
}
//方法5:復(fù)制繼承
function Person(pname){
this.name = pname;
this.work = function() {...};
}
var coder = deepCopy(new Person()); //拷貝
coder.code = function(){...}; //擴展新方法
coder.language = "javascript"; //擴展新屬性
coder.work = function() {...}; //重構(gòu)方法
//下面是深拷貝函數(shù)
function deepCopy(obj){
var obj = obj || {};
var newObj = {};
deeply(obj, newObj);
function deeply(oldOne, newOne){
for(var prop in oldOne){
if(!oldOne.hasOwnProperty(prop)) continue;
if(typeof oldOne[prop] === "object" && oldOne[prop] !== null){
newOne[prop] = oldOne[prop].constructor === Array ? [] : {};
deeply(oldOne[prop], newOne[prop]);
}
else
newOne[prop] = oldOne[prop];
}
}
return newObj;
}
既然方法這么多,我們該如和選擇,一張表解釋其中的區(qū)別
| --- | 共享原型 | 實例繼承 | 構(gòu)造繼承 | 復(fù)制繼承 |
|---|---|---|---|---|
| 原型屬性 | 繼承 | 繼承 | 不繼承 | 繼承 |
| 本地成員 | 不繼承 | 繼承 | 繼承 | 繼承 |
| 子類影響父類 | Y | N | N | N |
| 執(zhí)行效率 | 高 | 高 | 高 | 低 |
| 多繼承 | N | N | Y | Y |
| obj instanceof Parent | true | true | false | false |
子類的修改會影響父類是絕對不行的,所以共享原型是不能用的。在考慮到使用方便,只要不涉及多繼承就用實例繼承,多繼承中構(gòu)造繼承也好于復(fù)制繼承。
instanceof
instanceof用來判斷對象是否某個構(gòu)造函數(shù)的實例。這個東西很簡單,不僅可以判斷是否直接構(gòu)造函數(shù)實例,還能判斷是否父對象構(gòu)造函數(shù)的實例
function Person(){}
var p = new Person();
console.log(p instanceof Person); //true
console.log(p instanceof Object); //true
多態(tài)/重構(gòu)
js的方法名不能相同,我們只能模擬實現(xiàn)類似c++一樣的多態(tài)。
編譯時多態(tài)
注意:這個名字只是用了強類型語言的說法,js是個解釋型語言,沒用編譯過程。
在方法內(nèi)部判斷參數(shù)情況進行重載
- 參數(shù)數(shù)量不同做不同的事情
//修改字體,僅用部分屬性舉例:
function changeFont(obj, color, size, style){
if(arguments.lenght === 4){
//當(dāng)傳入了參數(shù)為4個參數(shù)時候做的事情
obj.style.fontSize = size;
obj.style.fontColor = color;
obj.style.fontStyle = style;
return;
}
if(arguments.length === 2 && typeof arguments[1] === "object"){
//當(dāng)傳入了參數(shù)為2個參數(shù)時候做的事情
obj.style.fontSize = arguments[1].size || obj.style.fontSize;
obj.style.fontStyle = arguments[1].style || obj.style.fontStyle;
obj.style.fontColor = arguments[1].color || obj.style.fontColor;
return;
}
throw TypeError("the font cannot be changed...");
}
- 參數(shù)類型不同做不同的事情
//構(gòu)造簡單對象
function toObject(val){
if(val === Object(val)) return val;
if(val == null) throw TypeError("'null' and 'undefined' cannot be an Object...");
switch(typeof val){
case "number": return new Number(val);
case "string": return new String(val);
case "boolean": return new Boolean(val);
case "symbol": return new Symbol(val);
default: throw TypeError("Unknow type inputted...");
}
}
運行時多態(tài)
java的多態(tài)都是編譯時多態(tài)。所以這個概念是源于c++的,c++利用虛基類實現(xiàn)運行過程中同一段代碼調(diào)用不同的函數(shù)的效果。而在js中可以利用函數(shù)傳遞實現(xiàn)運行時多態(tài)
function demo(fun, obj){
obj = obj || window;
fun.call(this);
}
function func(){
console.log("I'm coding in " + this.lang);
}
var lang = "C++";
var o = {
lang: "JavaScript",
func: function(){
console.log("I'm coding in " + this.lang);
}
};
demo(func);
demo(o.func);
demo(func, o);
重寫
我們都知道子對象可以重寫父對象中的函數(shù),這樣子對象函數(shù)對在子對象中替代父對象的同名函數(shù)。但如果我們希望既在子對象中重寫父類函數(shù),有想使用父類同名函數(shù)怎么辦!分一下幾個情況討論:
//情況1
function Person(){
this.doing = function(){
console.log("I'm working...");
};
}
function Coder(){
Person.call(this);
var ParentDoing = this.doing;
this.doing = function(){
console.log("My job is coding...");
ParentDoing();
}
}
var coder = new Coder();
coder.doing(); //測試
//情況2
function Person(){
}
Person.prototype.doing = function(){
console.log("I'm working...");
};
function Coder(){
Person.call(this);
this.doing = function(){
console.log("My job is coding...");
Person.prototype.doing.call(this);
};
}
var coder = new Coder();
coder.doing(); //測試
//情況3
function Person(){
}
Person.prototype.doing = function(){
console.log("I'm working...");
};
function Coder(){
}
Coder.prototype = Object.create(Person.prototype);
Coder.prototype.constructor = Coder;
Coder.super = Person.prototype;
Coder.prototype.doing = function(){
console.log("My job is coding...");
Coder.super.doing();
};
var coder = new Coder();
coder.doing(); //測試