說起面向對象,大部分程序員首先會想到 類 。通過類可以創建許多具有共同屬性以及方法的實例或者說對象。但是JavaScript並沒有類的概念,而且在JavaScript中幾乎一切皆對象,問題來了,JavaScript中如何面向對象?
JavaScript中將對象定義為:一組無序的 鍵值對的集合,屬性以及方法的名稱就是鍵,鍵的值可以是任何類型(字符串,數字,函數……)
在JavaScript中,所有對象繼承自Object,所有對象繼承自Object,所有對象繼承自Object!
創建
1 簡單創建對象:

var o = new Object(); o.name = 'mncu'; o.age = 120; o.sayName = function(){ alert(this.name); }; o.sayName(); // 'mncu' // 字面量創建 與上面的代碼等價 var o = { name:'mncu', age : 120, sayName:function(){ alert(this.name); } }; o.sayName(); //'mncu'
使用上面的方法雖然可以創建對象,但缺點也很明顯,假如再次創建一個具有相同屬性以及方法的對象,還得把代碼復制修改一遍,會產生大量的重復代碼。
2 工廠模式
這種模式抽象了創建對象的具體過程
function createperson(name,age){ var o = new Object(); o.name = name; o.age = age; o.sayName = function(){ alert(this.name); }; return o; } var p1 = createperson('mncu',120); var p2 = createperson('linghuchong',120); p1.sayName(); //'mncu' p2.sayName(); //'linghuchong'
console.log(typeof p1); //object
p1 instanceof createperson // false ,無法判斷類型
但這種方式有一個缺點:無法判斷某個對象是什么類型。
3 構造函數模式
function Person(name,age){ // 按照慣例,構造函數的首字母要大寫 this.name = name; this.age = age; this.sayName = function(){ alert(this.name); }; } var p1 = new Person('mncu',120); // 必須使用new關鍵字創建對象 var p2 = new Person('linghuchong',120); p1.sayName(); //'mncu' alert(p1 instanceof Person); // true,可判斷類型
構造函數模式解決了工廠模式中不能判斷對象類型的問題。
在使用構造函數模式時要注意:
--必須使用new關鍵字創建對象! new 關鍵字相當於做了以下幾步:
1 創建一個新對象
2 將構造函數的作用域賦值給這個新對象(因此this就指向了這個新對象)
3 執行構造函數的代碼(為這個新對象添加屬性)
4 返回新對象
--使用構造函數創建的對象都有一個constructor屬性,該屬性可以標識對象類型,但一般我們還是常用instanceof來判斷對象類型, 因為constructor屬性僅返回構造函數類型。
p1.constructor === Person //true
p1.constructor === Object //false
p1 instanceof Object // true p1 instanceof Person //true
--構造函數實際上和普通函數並無多大區別,因為其不存在特殊的語法,任何函數,只要通過new調用,那么他就可以作為構造函數。
var p3 = Person('dongfangbubai',120); // 不使用new時,函數的作用域會是全局作用域,this就會指向window對象。 p3.sayName() //報錯 sayName() // 'dongfangbubai'
--構造函數也存在問題,每個方法都要在實例上創建一遍。也就是說p1和p2的sayName()方法雖然作用相同,但這兩個方法並不是同一個函數。
p1.sayName == p2.sayName // false
--無new創建:
function Person(name,age){ //alert(this); if(!(this instanceof Person)){ return new Person(name,age); } this.name = name; this.age = age; this.sayName = function(){ alert(this.name); } } var p1 = Person('hah',20); console.log(p1.name)
4 原型模式
原型模式解決了構造函數模式中同功能的方法的代碼無法重用的問題。
我們創建的每個函數都有一個名為prototype的屬性,這個屬性是一個指針,指向一個對象,這個對象被稱為原型對象。原型對象有一個名叫constructor的屬性,這個屬性是一個指針,指向構造函數。默認情況下,所有函數的原型都是Object的實例。
使用原型模式創建對象:
function Person(){ Person.prototype.name = 'noOne'; Person.prototype.age = 'noAge'; Person.prototype.sayName = function(){ return this.name; }; } var p1 = new Person(); p1.name; // 'noOne'
var p2 = new Person()
p2.name; //'noOne'
在本例中,構造函數創建的對象p1的name屬性是如何展現的?
首先p1會查找自身有沒有name屬性,如果有的話,就返回自身的name屬性的值,如果沒有的話,則查找原型對象中有沒有name屬性,若原型對象中有name屬性,則返回其值,否則,就報錯。
p1.name = 'mncu'; p1.name; // 'mncu' p2.name; //'noOne'
我們可以通過hasOwnProperty()方法檢測一個屬性是存在於具體的對象中,還是存在於該對象的原型對象中
p1.hasOwnProperty('name') //true p2.hasOwnProperty('name') //false
我們也可以通過 in 關鍵字來判斷一個屬性是否存在於具體的對象或者該對象的原型對象中
'name' in p1 // true 'name' in p2 // true
原型對象的簡寫格式:
function Person(){ } Person.prototype={ // 將原型對象重寫,但重寫后原型對象的constructor就不會指向Person了(指向Object)。所以我們一般會添加:constructor:Person constructor:Person, name:'noOne', age:'noAge', sayName:function(){ alert(this.name); } };
原型模式存在的問題:
function Person(){ } Person.prototype={ constructor:Person, name:'noOne', age:'noAge', brothers : ['xiaoming'], sayName:function(){ alert(this.name); } }; var p1 = new Person(); var p2 = new Person(); p1.brothers.push('xiaohong'); console.log(p2.brothers) // ['xiaoming','xiaohong']
當我們改變 值為引用類型的對象的屬性 時,這個改變的結果會被其他對象共享。
5 將構造函數模式和原型模式結合
構造函數模式的屬性沒毛病。缺點是:無法共享方法
原型模式的方法沒毛病。缺點是:當原形對象的屬性的值為引用類型時,對其進行修改會反映到所有實例中
那我們就將兩者的結合,對象的屬性使用構造函數模式創建,方法則使用原型模式創建
這種方式是最為常見的一種面向對象編程模式
function Person(name,age){ this.name = name; this.age = age; } Person.prototype={ constructor:Person, sayName:function(){ alert(this.name); } }; var p1 = new Person('mncu',120); p1.name; // 'mncu'
繼承
1 原型鏈
JavaScript中引入了原型鏈的概念,具體思想: 子構造函數的原型對象初始化為父構造函數的實例,孫構造函數的原型對象初始化為子構造函數的實例…… ,這樣子對象就可以通過原型鏈一級一級向上查找,訪問父構造函數中的屬性以及方法。
構建原型鏈:

function Animal(name){ this.name = name || 'null'; this.action = ['move','wow']; } function Cat(){ this.wow = function miao(){ alert('miao'); }; } function Dog(){ this.wow = function(){ alert('wang') }; } Cat.prototype = new Animal(); Dog.prototype = new Animal(); var c1 = new Cat(); var d1 = new Dog(); console.log(c1.action); // ['move','wow'] d1.action.push('run'); var c2 = new Cat(); var d2 = new Dog(); console.log(d1.action); //["move", "wow", "run"] console.log(c1.action); //["move", "wow"] console.log(c2.action); //["move", "wow"] console.log(d2.action); //["move", "wow", "run"]
原型鏈繼承的問題:
function SuperObject(){ this.colors = ['red','blue']; } function SubObject(){ } SubObject.prototype = new SuperObject(); var instance1 = new SubObject(); instance1.colors.push('yellow'); var instance2 = new SubObject(); console.log(instance2.colors) // ["red", "blue", "yellow"]
當我們改變 值為引用類型的原型對象的屬性 時,這個改變的結果會被所有子對象共享。這個缺點某些時候相當致命,所以我們很少使用這種方法來繼承
2 借用構造函數繼承
function SuperObject(){ this.colors = ['red','blue']; this.sayBye= function(){ console.log('Bye') } } function SubObject(){ SuperObject.call(this); // 在子類中調用父類的構造方法,實際上子類和父類已經沒有上下級關系了 } var instance1 = new SubObject(); instance1.colors.push('yellow'); var instance2 = new SubObject(); console.log(instance2.colors); //['red','blue'] console.log(instance2 instanceof SuperObject); // false console.log(instance1.sayBye === instance2.sayBye) // false
這個方法雖然彌補了原型鏈的缺點,但是又暴露出了新的缺點:
1 子類和父類沒有上下級關系,instance2 instanceof SuperObject 結果是false
2 父類中的方法在每個子類中都會生成一遍,父類中的方法沒有被復用。
3 組合繼承
組合繼承就是將原型鏈繼承和借用構造方法繼承組合,發揮兩者之長。
function SuperObject(){ this.colors = ['red','blue']; } SuperObject.prototype.sayBye= function(){ console.log('bye') }; function SubObject(){ // 引用父類型的屬性,又調用了一次父函數 SuperObject.call(this); } // 繼承父類型的方法,調用了一次父函數 SubObject.prototype = new SuperObject(); var instance1 = new SubObject(); instance1.colors.push('yellow'); var instance2 = new SubObject(); console.log(instance2.colors); //['red','blue'] console.log(instance2 instanceof SuperObject); // true console.log(instance1.sayBye === instance2.sayBye); // true
4 寄生組合式繼承----道格拉斯方法
雖然組合繼承沒啥大缺點,但是愛搞事情的有強迫症的程序猿們覺得,組合繼承會調用兩次父類型函數(在上面的代碼中標注了),不夠完美。於是道格拉斯就提出了寄生組合繼承。
思路是構造一個中間函數,將中間函數的prototype指向父函數的原型對象,將子函數的prototype指向中間函數,並將中間函數的constructor屬性指向子函數。
function inherits(Child, Parent) { var F = function () {}; F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; } function Student(props) { this.name = props.name || 'Unnamed'; } Student.prototype.hello = function () { alert('Hello, ' + this.name + '!'); } function PrimaryStudent(props) { Student.call(this, props); this.grade = props.grade || 1; } // 實現原型繼承鏈: inherits(PrimaryStudent, Student); // 綁定其他方法到PrimaryStudent原型: PrimaryStudent.prototype.getGrade = function () { return this.grade; };
這個方法只調用了一次父構造函數,並因此避免了在父函數的原型對象上創建不必要的、多余的屬性。
開發人員都表示:這種方法是最理想的繼承方式(我沒看出來,只覺得這是最燒腦的繼承方式,看來我離開發人員還有一定距離。。。)