JavaScript基礎入門12 - 面向對象編程

JavaScript 面向對象編程

前言

面向對象編程(Object Oriented Programming,縮寫爲 OOP)是目前主流的編程範式。它將真實世界各類複雜的關係,抽象爲一個個對象,而後由對象之間的分工與合做,完成對真實世界的模擬。java

每個對象都是功能中心,具備明確分工,能夠完成接受信息、處理數據、發出信息等任務。對象能夠複用,經過繼承機制還能夠定製。所以,面向對象編程具備靈活、代碼可複用、高度模塊化等特色,容易維護和開發,比起由一系列函數或指令組成的傳統的過程式編程(procedural programming),更適合多人合做的大型軟件項目。es6

那麼,「對象」(object)究竟是什麼?咱們從兩個層次來理解。數據庫

(1)對象是單個實物的抽象。編程

一本書、一輛汽車、一我的均可以是對象,一個數據庫、一張網頁、一個與遠程服務器的鏈接也能夠是對象。當實物被抽象成對象,實物之間的關係就變成了對象之間的關係,從而就能夠模擬現實狀況,針對對象進行編程。瀏覽器

(2)對象是一個容器,封裝了屬性(property)和方法(method)。服務器

屬性是對象的狀態,方法是對象的行爲(完成某種任務)。好比,咱們能夠把動物抽象爲animal對象,使用「屬性」記錄具體是那一種動物,使用「方法」表示動物的某種行爲(奔跑、捕獵、休息等等)。app

構造函數建立對象

想要了解對象,咱們先來學習如何建立一個對象,首先是經過構造函數的形式來建立一個對象。編程語言

通常狀況下,咱們能夠將現實生活當中的實物抽象成對象。而想要抽象成對象,咱們一般狀況下須要一個模板,這個模板當中具有這類
實物的公有特性,而後咱們就能夠經過這個模板來實現該類對象的建立。模塊化

在js中,咱們就能夠經過構造函數來建立這類模板。

構造函數是用new建立對象時調用的函數,與普通惟一的區別是構造函數名應該首字母大寫。

function Person(){
    this.age = 30;
}
var person1 = new Person();
console.log(person1.age);//30

根據須要,構造函數能夠接受參數:

function Person(age){
    this.age = age;
}
var person1 = new Person(30);
console.log(person1.age);//30

若是沒有參數,能夠省略括號

function Person(){
    this.age = 30;
}
//等價於var person1 = new Person()
var person1 = new Person;
console.log(person1.age);//30

若是忘記使用new操做符,則this將表明全局對象window。一般這種狀況下會容易發生不少錯誤。
必定要當心。

function Person(){
    this.age = 30;
}
var person1 = Person();
//Uncaught TypeError: Cannot read property 'age' of undefined
console.log(person1.age);

instanceof

當咱們經過構造函數建立了一個對象以後,咱們就能夠經過instanceof來判斷對象的類型以及當前對象是不是經過指定構造函數構建而成。

function Person(){
    //
}
var person1 = new Person;
console.log(person1 instanceof Person);//true

constructor

 每一個對象在建立時都自動擁有一個構造函數屬性constructor,其中包含了一個指向其構造函數的引用。而這個constructor屬性實際上繼承自原型對象,而constructor也是原型對象惟一的自有屬性

function Person(){
    //
}
var person1 = new Person;
console.log(person1.constructor === Person);//true    
console.log(person1.__proto__.constructor === Person);//true

經過打印person1,你會發現,constructor 是一個繼承的屬性。

雖然對象實例及其構造函數之間存在這樣的關係,可是仍是建議使用instanceof來檢查對象類型。這是由於構造函數屬性能夠被覆蓋,並不必定徹底準確

function Person(){
    //
}
var person1 = new Person;
Person.prototype.constructor = 123;
console.log(person1.constructor);//123
console.log(person1.__proto__.constructor);//123

返回值

函數中的return語句用來返回函數調用後的返回值,而new構造函數的返回值有點特殊

若是構造函數使用return語句但沒有指定返回值,或者返回一個原始值,那麼這時將忽略返回值,同時使用這個新對象做爲調用結果

function fn(){
    this.a = 2;
    return;
}
var test = new fn();
console.log(test);//{a:2}

若是構造函數顯式地使用return語句返回一個對象,那麼調用表達式的值就是這個對象

var obj = {a:1};
function fn(){
    this.a = 2;
    return obj;
}
var test = new fn();
console.log(test);//{a:1}

因此,針對丟失new的構造函數的解決辦法是在構造函數內部使用instanceof判斷是否使用new命令,若是發現沒有使用,則直接使用return語句返回一個實例對象

function Person(){
    if(!(this instanceof Person)){
        return new Person();
    }
    this.age = 30;
}
var person1 = Person();
console.log(person1.age);//30
var person2 = new Person();
console.log(person2.age);//30

使用構造函數的好處在於全部用同一個構造函數建立的對象都具備一樣的屬性和方法

function Person(name){
    this.name = name;
    this.sayName = function(){
        console.log(this.name);
    }
}
var person1 = new Person('bai');
var person2 = new Person('hu');
person1.sayName();//'bai'

構造函數容許給對象配置一樣的屬性,可是構造函數並無消除代碼冗餘。使用構造函數的主要問題是每一個方法都要在每一個實例上從新建立一遍。在上面的例子中,每個對象都有本身的sayName()方法。這意味着若是有100個對象實例,就有100個函數作相同的事情,只是使用的數據不一樣。

function Person(name){
    this.name = name;
    this.sayName = function(){
        console.log(this.name);
    }
}
var person1 = new Person('bai');
var person2 = new Person('hu');
console.log(person1.sayName === person2.sayName);//false

能夠經過把函數定義轉換到構造函數外部來解決問題

function Person(name){
    this.name = name;
    this.sayName = sayName;
}
function sayName(){
    console.log(this.name);
}
var person1 = new Person('bai');
var person2 = new Person('hu');
console.log(person1.sayName === person2.sayName);//true

可是,在全局做用域中定義的函數實際上只能被某個對象調用,這讓全局做用域有點名存實亡。並且,若是對象須要定義不少方法,就要定義不少全局函數,嚴重污染全局空間,這個自定義的引用類型沒有封裝性可言了

若是全部的對象實例共享同一個方法會更有效率,這就須要用到下面所說的原型對象 。

原型對象

提及原型對象,就要說到原型對象、實例對象和構造函數的三角關係 。
例如:

function Foo(){};
var f1 = new Foo;

構造函數

用來初始化新建立的對象的函數是構造函數。在例子中,Foo()函數是構造函數

實例對象

經過構造函數的new操做建立的對象是實例對象,又經常被稱爲對象實例。能夠用一個構造函數,構造多個實例對象。下面的f1和f2就是實例對象

function Foo(){};
var f1 = new Foo;
var f2 = new Foo;
console.log(f1 === f2);//false

原型對象及prototype

經過構造函數的new操做建立實例對象後,會自動爲構造函數建立prototype屬性,該屬性指向實例對象的原型對象。經過同一個構造函數實例化的多個對象具備相同的原型對象。下面的例子中,Foo.prototype是原型對象

function Foo(){};
Foo.prototype.a = 1;
var f1 = new Foo;
var f2 = new Foo;

console.log(Foo.prototype.a);//1
console.log(f1.a);//1
console.log(f2.a);//1

proto

實例對象內部包含一個proto屬性(IE10-瀏覽器不支持該屬性),指向該實例對象對應的原型對象

function Foo(){};
var f1 = new Foo;
console.log(f1.__proto__ === Foo.prototype);//true

isPrototypeOf

通常地,能夠經過isPrototypeOf()方法來肯定對象之間是不是實例對象和原型對象的關係 

function Foo(){};
var f1 = new Foo;
console.log(f1.__proto__ === Foo.prototype);//true
console.log(Foo.prototype.isPrototypeOf(f1));//true

Object.getPrototypeOf()
ES5新增了Object.getPrototypeOf()方法,該方法返回實例對象對應的原型對象

function Foo(){};
var f1 = new Foo;
console.log(Object.getPrototypeOf(f1) === Foo.prototype);//true

實際上,Object.getPrototypeOf()方法和proto屬性是一回事,都指向原型對象

function Foo(){};
var f1 = new Foo;
console.log(Object.getPrototypeOf(f1) === f1.__proto__ );//true

關於對象的屬性查找

當讀取一個對象的屬性時,javascript引擎首先在該對象的自有屬性中查找屬性名字。若是找到則返回。若是自有屬性不包含該名字,則javascript會搜索proto中的對象。若是找到則返回。若是找不到,則返回undefined

var o = {};
console.log(o.toString());//'[object Object]'
o.toString = function(){
    return 'o';
}
console.log(o.toString());//'o'
delete o.toString;
console.log(o.toString());//'[objet Object]'

in

in操做符能夠判斷屬性在不在該對象上,但沒法區別自有仍是繼承屬性

var o = {a:1};
var obj = Object.create(o);
obj.b = 2;
console.log('a' in obj);//true
console.log('b' in obj);//true
console.log('b' in o);//false
//Object.create()是建立對象的一種方法,等價於
function Test(){};
var obj = new Test;
Test.prototype.a = 1;
obj.b = 2;
console.log('a' in obj);//true
console.log('b' in obj);//true
console.log('b' in Test.prototype);//false

hasOwnProperty()

經過hasOwnProperty()方法能夠肯定該屬性是自有屬性仍是繼承屬性.

var o = {a:1};
var obj = Object.create(o);
obj.b = 2;
console.log(obj.hasOwnProperty('a'));//false
console.log(obj.hasOwnProperty('b'));//true

因而能夠將hasOwnProperty方法和in運算符結合起來使用,用來鑑別原型屬性

function hasPrototypeProperty(object,name){
    return name in object && !object.hasOwnProperty(name);
}

原型對象的共享機制使得它們成爲一次性爲全部對象定義方法的理想手段。
能夠利用該機制實現完整的面向對象的寫法。

function Person(name){
    this.name = name;
}
Person.prototype.sayName = function(){
    console.log(this.name);
}
var person1 = new Person('bai');
var person2 = new Person('hu');

person1.sayName();//'bai'

雖然能夠在原型對象上一一添加屬性,可是直接用一個對象字面形式替換原型對象更簡潔

function Person(name){
    this.name = name;
}
Person.prototype = {
    sayName: function(){
        console.log(this.name);
    },
    toString : function(){
        return '[person ' + this.name + ']'
    }
};

var person1 = new Person('bai');

console.log(person1 instanceof Person);//true
console.log(person1.constructor === Person);//false
console.log(person1.constructor === Object);//true

構造函數、原型對象和實例對象之間的關係是實例對象和構造函數之間沒有直接聯繫.
例如:

function Foo(){};
var f1 = new Foo;

以上代碼的原型對象是Foo.prototype,實例對象是f1,構造函數是Foo

JS當中實現繼承

學習如何建立對象是理解面向對象編程的第一步,第二步是理解繼承。開宗明義,繼承是指在原有對象的基礎上,略做修改,獲得一個新的對象。javascript主要包括類式繼承、原型繼承和拷貝繼承這三種繼承方式。

類式繼承

大多數面向對象的編程語言都支持類和類繼承的特性,而JS卻不支持這些特性,只能經過其餘方法定義並關聯多個類似的對象,如new和instanceof。不過在後來的ES6中新增了一些元素,好比class關鍵字,但這並不意味着javascript中是有類的,class只是構造函數的語法糖而已

類式繼承的主要思路是,經過構造函數實例化對象,經過原型鏈將實例對象關聯起來。

實現類式繼承 - 原型鏈繼承

javascript使用原型鏈做爲實現繼承的主要方法,實現的本質是重寫原型對象,代之以一個新類型的實例。下面的代碼中,原來存在於SuperType的實例對象中的屬性和方法,如今也存在於SubType.prototype中了。

function Super(){
    this.value = true;
}
Super.prototype.getValue = function(){
    return this.value;
};
function Sub(){}
//Sub繼承了Super
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;

var instance = new Sub();
console.log(instance.getValue());//true

原型鏈最主要的問題在於包含引用類型值的原型屬性會被全部實例共享,而這也正是爲何要在構造函數中,而不是在原型對象中定義屬性的緣由。在經過原型來實現繼承時,原型實際上會變成另外一個類型的實例。因而,原先的實例屬性也就瓜熟蒂落地變成了如今的原型屬性了

function Super(){
    this.colors = ['red','blue','green'];
}
function Sub(){};
//Sub繼承了Super
Sub.prototype = new Super();
var instance1 = new Sub();
instance1.colors.push('black');
console.log(instance1.colors);//'red,blue,green,black'
var instance2 = new Sub();
console.log(instance2.colors);//'red,blue,green,black'

原型鏈的第二個問題是,在建立子類型的實例時, 不能向超類型的構造函數中傳遞參數。實際上,應該說是沒有辦法在不影響全部對象實例的狀況下,給超類型的構造函數傳遞參數。再加上包含引用類型值的原型屬性會被全部實例共享的問題,在實踐中不多會單獨使用原型鏈繼承

實現類式繼承 - 借用構造函數

借用構造函數(constructor stealing)的技術(有時候也叫作僞類繼承或經典繼承)。基本思想至關簡單,即在子類型構造函數的內部調用超類型構造函數,經過使用apply()和call()方法在新建立的對象上執行構造函數。

function Super(){
    this.colors = ['red','blue','green'];
}
function Sub(){
    //繼承了Super
    Super.call(this);
}
var instance1 = new Sub();
instance1.colors.push('black');
console.log(instance1.colors);// ['red','blue','green','black']
var instance2 = new Sub();
console.log(instance2.colors);// ['red','blue','green']

相對於原型鏈而言,借用構造函數有一個很大的優點,便可以在子類型構造函數中向超類型構造函數傳遞參數

function Super(name){
    this.name = name;
}
function Sub(){
    //繼承了Super,同時還傳遞了參數
    Super.call(this,"bai");
    //實例屬性
    this.age = 29;
}
var instance = new Sub();
console.log(instance.name);//"bai"
console.log(instance.age);//29

可是,若是僅僅是借用構造函數,那麼也將沒法避免構造函數模式存在的問題——方法都在構造函數中定義,所以函數複用就無從談起了。

實現類式繼承 - 組合繼承

組合繼承(combination inheritance)有時也叫僞經典繼承,指的是將原型鏈和借用構造函數的技術組合到一塊,從而發揮兩者之長的一種繼承模式。其背後的思路是使用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例屬性的繼承。這樣,既經過在原型上定義方法實現了函數複用,又可以保證每一個實例都有它本身的屬性

function Super(name){
    this.name = name;
    this.colors = ['red','blue','green'];
}
Super.prototype.sayName = function(){
    console.log(this.name);
};
function Sub(name,age){
    //繼承屬性
    Super.call(this,name);
    this.age = age;
}
//繼承方法
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;
Sub.prototype.sayAge = function(){
    console.log(this.age);
}
var instance1 = new Sub("bai",29);
instance1.colors.push("black");
console.log(instance1.colors);//['red','blue','green','black']
instance1.sayName();//"bai"
instance1.sayAge();//29

var instance2 = new Sub("hu",27);
console.log(instance2.colors);//['red','blue','green']
instance2.sayName();//"hu"
instance2.sayAge();//27

組合繼承有它本身的問題。那就是不管什麼狀況下,都會調用兩次父類型構造函數:一次是在建立子類型原型的時候,另外一次是在子類型構造函數內部。子類型最終會包含父類型對象的所有實例屬性,但不得不在調用子類型構造函數時重寫這些屬性

function Super(name){
    this.name = name;
    this.colors = ["red","blue","green"];
}
Super.prototype.sayName = function(){
    return this.name;
};
function Sub(name,age){
     // 第二次調用Super(),Sub.prototype又獲得了name和colors兩個屬性,並對上次獲得的屬性值進行了覆蓋
    Super.call(this,name);
    this.age = age;
}
//第一次調用Super(),Sub.prototype獲得了name和colors兩個屬性
Sub.prototype = new Super(); 
Sub.prototype.constructor = Sub;
Sub.prototype.sayAge = function(){
    return this.age;
};

實現類式繼承 - 寄生組合繼承

解決兩次調用的方法是使用寄生組合式繼承。寄生組合式繼承與組合繼承類似,都是經過借用構造函數來繼承不可共享的屬性,經過原型鏈的混成形式來繼承方法和可共享的屬性。只不過把原型繼承的形式變成了寄生式繼承。使用寄生組合式繼承能夠沒必要爲了指定子類型的原型而調用父類型的構造函數,從而寄生式繼承只繼承了父類型的原型屬性,而父類型的實例屬性是經過借用構造函數的方式來獲得的

function Super(name){
    this.name = name;
    this.colors = ["red","blue","green"];
}
Super.prototype.sayName = function(){
    return this.name;
};

function Sub(name,age){
    Super.call(this,name);
    this.age = age;
}
if(!Object.create){
    Object.create = function(proto){
    function F(){};
    F.prototype = proto;
    return new F;
  }
}
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;

var instance1 = new Sub("bai",29);
instance1.colors.push("black");
console.log(instance1.colors);//['red','blue','green','black']
instance1.sayName();//"bai"

var instance2 = new Sub("hu",27);
console.log(instance2.colors);//['red','blue','green']
instance2.sayName();//"hu"

這個例子的高效率體如今它只調用了一次Super構造函數,而且所以避免了在Sub.prototype上面建立沒必要要的、多餘的屬性。與此同時,原型鏈還保持不變

  所以,開發人員廣泛認爲寄生組合式繼承是引用類型最理想的繼承範式,YUI的YAHOO.lang.extend()方法就採用了這種繼承模式

類式繼承 -- ES6 class

若是使用ES6中的class語法,則上面代碼修改以下

class Super {
  constructor(name){
    this.name = name;
    this.colors = ["red","blue","green"];
  }
  sayName(){
    return this.name;
  }
}

class Sub extends Super{
  constructor(name,age){
    super(name);
    this.age = age;
  }
}

var instance1 = new Sub("bai",29);
instance1.colors.push("black");
console.log(instance1.colors);//['red','blue','green','black']
instance1.sayName();//"bai"

var instance2 = new Sub("hu",27);
console.log(instance2.colors);//['red','blue','green']
instance2.sayName();//"hu"

ES6的class語法糖隱藏了許多技術細節,在實現一樣功能的前提下,代碼卻優雅很多

使用原型繼承

原型繼承

原型繼承,在《你不知道的javascript》中被翻譯爲委託繼承

道格拉斯·克羅克福德(Douglas Crockford)在2006年寫了一篇文章,《javascript中的原型式繼承》。在這篇文章中,他介紹了一種實現繼承的方式,這種方式並無使用嚴格意義上的構造函數。他的想法是藉助原型能夠基於已有的對象來建立新對象,同時沒必要所以建立自定義類型

原型繼承的基礎函數以下所示:

function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}

在object()函數內部,先建立了一個臨時性的構造函數,而後將傳入的對象做爲這個構造函數的原型,最後返回了這個臨時類型的一個新實例。從本質上講,object()對傳入其中的對象執行了一次淺複製

例如:

var superObj = {
  init: function(value){
    this.value = value;
  },
  getValue: function(){
    return this.value;
  }
}

var subObj = object(superObj);
subObj.init('sub');
console.log(subObj.getValue());//'sub'

ES5經過新增Object.create()方法規範化了原型式繼承

var superObj = {
  init: function(value){
    this.value = value;
  },
  getValue: function(){
    return this.value;
  }
}

var subObj = Object.create(superObj);
subObj.init('sub');
console.log(subObj.getValue());//'sub'

與原型鏈繼承的關係

原型繼承雖然只是看上去將原型鏈繼承的一些程序性步驟包裹在函數裏而已。可是,它們的一個重要區別是父類型的實例對象再也不做爲子類型的原型對象

一、使用原型鏈繼承

function Super(){
    this.value = 1;
}
Super.prototype.value = 0;
function Sub(){};
//將父類型的實例對象做爲子類型的原型對象
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;

//建立子類型的實例對象
var instance = new Sub;
console.log(instance.value);//1

二、使用原型繼承

function Super(){
    this.value = 1;
}
Super.prototype.value = 0;
function Sub(){};

Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;

//建立子類型的實例對象
var instance = new Sub;
console.log(instance.value);//0

 上面的Object.create函數一行代碼Sub.prototype = Object.create(Super.prototype)能夠分解爲

function F(){};
F.prototype = Super.prototype;
Sub.prototype = new F();

由上面代碼看出,子類的原型對象是臨時類F的實例對象,而臨時類F的原型對象又指向父類的原型對象;因此,實際上,子類能夠繼承父類的原型上的屬性,但不能夠繼承父類的實例上的屬性

拷貝繼承

拷貝繼承在《javascript面向對象摘要》中翻譯爲混入繼承,jQuery使用的就是拷貝繼承

拷貝繼承不須要改變原型鏈,經過拷貝函數將父例的屬性和方法拷貝到子例便可

拷貝函數

下面是一個深拷貝的拷貝函數

function extend(obj,cloneObj){
    if(typeof obj != 'object'){
        return false;
    }
    var cloneObj = cloneObj || {};
    for(var i in obj){
        if(typeof obj[i] === 'object'){
            cloneObj[i] = (obj[i] instanceof Array) ? [] : {};
            arguments.callee(obj[i],cloneObj[i]);
        }else{
            cloneObj[i] = obj[i]; 
        }  
    }
    return cloneObj;
}

var obj1={a:1,b:2,c:[1,2,3]};
var obj2=extend(obj1);
console.log(obj1.c); //[1,2,3]
console.log(obj2.c); //[1,2,3]
obj2.c.push(4);
console.log(obj2.c); //[1,2,3,4]
console.log(obj1.c); //[1,2,3]

對象間的拷貝繼承

因爲拷貝繼承解決了引用類型值共享的問題,因此其徹底能夠脫離構造函數實現對象間的繼承

function extend(obj,cloneObj){
    if(typeof obj != 'object'){
        return false;
    }
    var cloneObj = cloneObj || {};
    for(var i in obj){
        if(typeof obj[i] === 'object'){
            cloneObj[i] = (obj[i] instanceof Array) ? [] : {};
            arguments.callee(obj[i],cloneObj[i]);
        }else{
            cloneObj[i] = obj[i]; 
        }  
    }
    return cloneObj;
}

var superObj = {
  arrayValue:[1,2,3],
  init: function(value){
    this.value = value;
  },
  getValue: function(){
    return this.value;
  }
}
var subObj = extend(superObj);
subObj.arrayValue.push(4);
console.log(subObj.arrayValue);//[1,2,3,4]
console.log(superObj.arrayValue);//[1,2,3]

使用構造函數的拷貝組合繼承

若是要使用構造函數,則屬性可使用借用構造函數的方法,而引用類型屬性和方法使用拷貝繼承。至關於再也不經過原型鏈來創建對象之間的聯繫,而經過複製來獲得對象的屬性和方法

function extend(obj,cloneObj){
    if(typeof obj != 'object'){
        return false;
    }
    var cloneObj = cloneObj || {};
    for(var i in obj){
        if(typeof obj[i] === 'object'){
            cloneObj[i] = (obj[i] instanceof Array) ? [] : {};
            arguments.callee(obj[i],cloneObj[i]);
        }else{
            cloneObj[i] = obj[i]; 
        }  
    }
    return cloneObj;
}
function Super(name){
    this.name = name;
    this.colors = ["red","blue","green"];
}
Super.prototype.sayName = function(){
    return this.name;
};
function Sub(name,age){
    Super.call(this,name);
    this.age = age;
}
Sub.prototype = extend(Super.prototype);
var instance1 = new Sub("bai",29);
instance1.colors.push("black");
console.log(instance1.colors);//['red','blue','green','black']
instance1.sayName();//"bai"

var instance2 = new Sub("hu",27);
console.log(instance2.colors);//['red','blue','green']
instance2.sayName();//"hu"

上面介紹了幾種繼承方式,其中最多見的是類式繼承。再加上ES6語法糖的緣故,因此致使更多的人使用。對於 通常開發來講,類式繼承也足以應付。

相關文章
相關標籤/搜索