js是一個基於對象的語言,因此本文研究一下js對象和類實現的過程和原理。javascript
下面是一個對象的各個部分:java
var person = { name: "Lily", age: 10, work: function(){ console.log("Lily is working..."); } }; person.gender = "F"; //能夠動態添加屬性 Object.defineProperty(person, "salary", { //添加屬性 value: 10000, writable: true, //是否可寫,默認false enumerable: true, //是否可枚舉,默認false configuration: true //是否可配置,默認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...遍歷到;
是否可配置指的是其可寫性,可枚舉性,可配置性是否可修改,而且決定該屬性能否被刪除。c++
這是一個普通的對象和常見操做,很少說,下面是一個具備get/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個性質,對象本身其實也有3個性質:segmentfault
可不可擴展是指一個對象可不能夠添加新的屬性;Object.preventExtensions 可讓這個對象變的不可擴展。嘗試給一個不可擴展對象添加新屬性的操做將會失敗,但不會有任何提示,(嚴格模式下會拋出TypeError異常)。Object.preventExtensions只能阻止一個對象不能再添加新的自身屬性,仍然能夠爲該對象的原型添加屬性,但__proto__屬性的值也不能修改。瀏覽器
var person = { name: "Lily", age: 10 }; //新建立的對象默認是可擴展的 console.log(Object.isExtensible(person)); //true person.salary = 10000; console.log(person.salary) //10000 Object.preventExtensions(person);//將其變爲不可擴展對象 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 }); //因爲函數內部採用嚴格模式,因此報錯TypeError: #<Object> is not extensible(…)
這裏若是不理解__proto__
沒關係,下文會重點解釋這個屬性網絡
若是咱們想讓一個對象即不可擴展,又讓它的全部屬性不可配置,一個個修改屬性的configurable太不現實了,咱們把這樣的對象叫作密封的(Sealed)。用Object.isSealed()判斷一個對象是否密封的,用Object.seal()密封一個對象。 其特性包括不可擴展對象和不可配置屬性的相關特性。數據結構
var person = { name: "Lily", age: 10 }; //新建立的對象默認是不密封的 console.log(Object.isSeal(person)); //false Object.seal(person);//將其變爲密封對象 console.log(Object.isSeal(person)); //true delete person.age; //沒法刪除已有屬性,失敗,不報錯。但嚴格模式會報錯 console.log(person.age); //undefined person.__proto__ = function a(){}; //報錯TypeError: #<Object> is not extensible(...)
此時,這個對象屬性可能仍是可寫的,若是咱們想讓一個對象的屬性既不可寫也不可配置,同時讓該對象不可擴展,那麼就須要凍結這個對象。用Object.freeze()凍結對象,用isFrozen()判斷對象是否被凍結。因爲相比上一個例子,僅僅是現有的變得不可寫了,這裏就不舉太多例子了。
不過值得注意的是,對於具備setter的屬性同樣不可寫。app
var person = { name: "Lily", _age: 10, get age(){ return this._age; }, set age(val){ this._age = val; } }; //新建立的對象默認不是凍結的 console.log(Object.isFrozen(person)); //false Object.freeze(person);//將其變爲不可擴展對象 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"; //失敗,但不報錯,但嚴格模式會報錯。 console.log(person.name); //"Lily" console.log(person.age); //10 person.age = 30; console.log(person.age); //10
深凍結和淺凍結的主要差別出如今可擴展性上,因此你也能夠理解爲深可擴展和淺可擴展。咱們看一下如下代碼:函數
var person = { addr: {} } Object.freeze(person); person.addr.province = "Guangzhou"; //淺凍結:對象的屬性對象能夠繼續擴展 console.log(person.addr.province); //"Guangzhou"
爲了實現深凍結,咱們寫一個函數:
var person = { name: "nihao", addr: {}, family:{ slibing:{}, parents:{} } } deepFreeze(person); person.addr.province = "Guangzhou"; //深凍結:對象的屬性對象沒法繼續擴展 console.log(person.addr.province); //undefined person.family.parents.father = "Bob"; //深凍結:對象的屬性對象沒法繼續擴展 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]); //遞歸調用 } }
注意,這裏遞歸沒有判斷鏈表是否成環,判斷有環鏈表是數據結構的知識,可使用一組快慢指針實現,這裏不贅述。所以在如下狀況會有一個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); //會陷入無休止的遞歸。實際家庭成員關係更復雜,就更糟糕了。RangeError: Maximum call stack size exceeded(…)
當咱們想建立不少我的的時候,就不會像上面這樣一個一個寫了。那咱們就造一個工廠,用來生產人(感受有點恐怖):
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}
可是這樣寫並不符合傳統的編程思路。所以咱們須要一個構造函數(constructor, 也有書譯爲構造器)
關於構造函數和普通函數的區別能夠看javascript中this詳解中」構造函數中的this"一節。
下面定義一個構造函數:
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();
不過這樣寫這個函數,每一個對象都會包括一部分,太浪費內存。因此咱們會把公共的部分放在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__
屬性,這個是一個非標準屬性(ES6已經把它標準化了),不過IE中沒有這個屬性。
在學習原型鏈以前咱們必定要區分清楚:prototype是構造函數的屬性,而__proto__
是對象的屬性。固然咱們依然用代碼說話:
再來一段代碼:
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__); //空函數function(){} console.log(Person.constructor); //function Function(){...}
說到這裏,就有必要學習一下原型鏈了。
js沒有類的概念,這樣就不會有繼承派生和多態,可是實際編程中咱們須要這樣的結構,因而js在發展過程當中,就從一個沒有類的語言模擬出來類的效果,這裏靠的就是prototype。
一個構造函數的prototype永遠指向他的父對象,這樣這個構造函數new出來的對象就能夠訪問其父對象的成員,實現了繼承。
若是他的父對象的prototype又指向一個父對象的父對象,這樣一層層就構成了原型鏈。以下(用瀏覽器內置對象模型舉例):
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); /*......*/
若是你以爲這裏應該有一張圖,那就看看這個完整的對象關係圖(基於DOM),下文的相關例子也基於這個圖:
注意:原型鏈是有窮的,他總會指向Object,而後是null結束
那麼__proto__
是什麼?一言以蔽之:對象的__proto__
屬性指向該對象構造函數的原型。以下:
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
上面圖中發現,對象還有一個constructor屬性,這個屬性也很重要,新建立對象的constructor指向默認對象的構造函數自己,不過現實沒有這麼簡單。例如:
function Person(){ } var p1 = new Person(); console.log(p1.constructor); //function Person(){...} function Children(){ } Children.prototype = p1;//這一行和下一行聯立使用,不能忽略下一行 Children.prototype.constructor = Children; //修正constructor,這個不能省略 console.log(Person.prototype.constructor); //function Person(){...} console.log(p1.constructor); //function Child(){...}
當咱們創建了一個繼承關係後,會使新的構造函數的prototype.constructor指向改構造函數本身,像上面第9行同樣。從第11行也能夠看出,系統自己也是這樣作的。這樣就構成了下面這個圖的關係,此時父對象的constructor指向子構造函數:
注: 圖片來自網絡
從上面的這些例子咱們不難發現,函數也是一個對象。所以構造函數也有了constructor和__proto__屬性。不過這裏會比較簡單:函數的constructor都是Function(){...};函數的__proto__
都是個空函數
其實在js中除了基本類型(null, undefined, String, Number, Boolean, Symbol)之外,都是對象。可能你想反駁我:「js中一切都是對象」。咱們看如下幾個例子:
//以數字類型爲例 var a = 1; //基本類型 console.log(a); //1 console.log(typeof a); //number var b = new Number(1); //對象類型的數字 console.log(b); //Number {[[PrimitiveValue]]: 1} console.log(typeof b); //object
首先,js中基本類型中除了null和undefined之外的類型,都具備對象形式。但對象形式不等於基本類型。從上面的輸出結果來看,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有了方法,對象又能夠參與運算。實際上這是隱式類型轉換的結果,上面第二行,瀏覽器自動調用了new Number()把a轉換成了對象,而第四行利用ValueOf()方法把對象轉換成了數字。
既然函數也是個對象,那麼咱們不只能夠用構造函數new一個對象出來,也能夠爲它定義私有方法(變量)和靜態方法
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); //注意這裏的做用域和調用方式 }; }; Person.speak = function(){console.log("I am a person");}; //靜態方法 var p = new Person("Bob"); p.getInfo(); //Bob 10 Person.speak(); //"I am a person"
固然實現簡單的對象繼承不用這麼複雜,可使用Object.create(obj);返回一個繼承與obj的對象。對與Object.create()方法須要考慮一下幾種狀況:
var o = {}; var r1 = Object.create(o); //建立一個r1繼承於o var r2 = Object.create(null); //建立一個r2繼承於null var r3 = Object.create(Object); //建立一個r3繼承於Object console.log(r1); //是一個繼承自o的對象 console.log(r2); //是一個空對象,沒有__proto__屬性 console.log(r3); //是一個函數
有了先前的知識,咱們能夠寫出來一個函數實現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));
看了這麼多,怎麼寫繼承比較合理,咱們實現2個構造函數,讓Coder繼承Person。比較如下3種方法:
function Person(pname){ this.name = pname; } function Coder(){} //方法一:共享原型 Coder.prototype = Person.prototype; //方法二:實例繼承 Coder.prototype = new Person("Lily"); Coder.prototype.constructor = Coder; //方法三:本質上仍是實例繼承 Coder.prototype = Object.create(Person.prototype);
固然還有其餘的繼承方法:
//方法4:構造繼承 function Person(pname){ this.name = pname; } function Coder(pname){ Person.apply(this, argument); } //方法5:複製繼承 function Person(pname){ this.name = pname; this.work = function() {...}; } var coder = deepCopy(new Person()); //拷貝 coder.code = function(){...}; //擴展新方法 coder.language = "javascript"; //擴展新屬性 coder.work = function() {...}; //重構方法 //下面是深拷貝函數 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; }
既然方法這麼多,咱們該如和選擇,一張表解釋其中的區別
--- | 共享原型 | 實例繼承 | 構造繼承 | 複製繼承 |
---|---|---|---|---|
原型屬性 | 繼承 | 繼承 | 不繼承 | 繼承 |
本地成員 | 不繼承 | 繼承 | 繼承 | 繼承 |
子類影響父類 | Y | N | N | N |
執行效率 | 高 | 高 | 高 | 低 |
多繼承 | N | N | Y | Y |
obj instanceof Parent | true | false | false | false |
子類的修改會影響父類是絕對不行的,因此共享原型是不能用的。在考慮到使用方便,只要不涉及多繼承就用實例繼承,多繼承中構造繼承也好於複製繼承。
instanceof用來判斷對象是否某個構造函數的實例。這個東西很簡單,不只能夠判斷是否直接構造函數實例,還能判斷是否父對象構造函數的實例
function Person(){} var p = new Person(); console.log(p instanceof Person); //true console.log(p instanceof Object); //true
js的方法名不能相同,咱們只能模擬實現相似c++同樣的多態。
注意:這個名字只是用了強類型語言的說法,js是個解釋型語言,沒用編譯過程。
在方法內部判斷參數狀況進行重載
//修改字體,僅用部分屬性舉例: function changeFont(obj, color, size, style){ if(arguments.lenght === 4){ //當傳入了參數爲4個參數時候作的事情 obj.style.fontSize = size; obj.style.fontColor = color; obj.style.fontStyle = style; return; } if(arguments.length === 2 && typeof arguments[1] === "object"){ //當傳入了參數爲2個參數時候作的事情 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..."); }
//構造簡單對象 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..."); } }
java的多態都是編譯時多態。因此這個概念是源於c++的,c++利用虛基類實現運行過程當中同一段代碼調用不一樣的函數的效果。而在js中能夠利用函數傳遞實現運行時多態
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);
咱們都知道子對象能夠重寫父對象中的函數,這樣子對象函數對在子對象中替代父對象的同名函數。但若是咱們但願既在子對象中重寫父類函數,有想使用父類同名函數怎麼辦!分一下幾個狀況討論:
//狀況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(); //測試