面向對象編程就是將你的需求抽象成一個對象,針對這個對象分析其特征(屬性)和動作(方法),這個對象稱為“類”。JavaScript 的核心是支持面向對象的,同時它也提供了強大靈活的 OOP 語言能力,遺憾的是對於JavaScript這種解釋性的弱類型語言,沒有強類型語言中那種通過class等關鍵字實現... ...
前言
面向對象編程就是將你的需求抽象成一個對象,針對這個對象分析其特征(屬性)和動作(方法),這個對象稱為“類”。JavaScript 的核心是支持面向對象的,同時它也提供了強大靈活的 OOP 語言能力,遺憾的是對於JavaScript這種解釋性的弱類型語言,沒有強類型語言中那種通過class等關鍵字實現類的方式,但JavaScript可以通過一些特性模仿實現面向對象編程。
面向對象有三個基本特征:封裝
、繼承
、多態
。
封裝
封裝,就是把客觀事物封裝成抽象的類,類中包含了事物的屬性和方法,並且類可以把自己的數據和方法只讓可信的類或者對象操作,對不可信的進行信息隱藏。
JavaScript創建一個類很容易,通過聲明一個函數保存在一個變數里來實現,這個類的類名通常會採用首字母大寫的形式來表示,然後在這個函數(類)的內部使用this
關鍵字來定義類的屬性和方法。例如:
var Person = function(name,sex,age){
this.name = name;
this.sex = sex;
this.age = age;
}
也可以在類的原型上添加屬性和方法。例如:
Person.prototype.say = function(){
// Say something
}
或者
Person.prototype = {
say: function(){ … }
}
這樣就完成了Person類的封裝,當我們要使用這個類時,需要通過new
關鍵字來實例化(創建)一個新的對象,通過.
操作符訪問對象的屬性和方法。例如:
var person = new Person('Scott','male',20);
console.log(person.name); // Scott
通過this
添加的屬性和方法是在當前對象上添加的,而JavaScript是一種基於原型prototype的語言,每創建一種對象時,都有一個原型prototype用於指向其繼承的屬性和方法,通過prototype繼承的屬性和方法不是屬於對象本身的,在使用這些方法時,會通過原型鏈進行查找。當創建一個對象時,會創建this指向的屬性和方法,而通過prototype繼承的屬性或方法是該類的每個對象所共有的,不會再次創建。
當創建一個函數或者對象時都會為其創建一個prototype對象,原對象中的__proto__
屬性指向該原型對象,prototype對象中會有一個constructor
屬性指向擁有整個原型對象的函數或者類。
通過new
關鍵字創建對象時實際上是對新對象中this
的不斷賦值,並將prototype指向類的原型對象,而在類外通過.
操作符定義的屬性和方法是不會添加到新建對象上的,通過對象進行訪問的結果是undefined。例如:
Person.isChinese = true;
Person.eat = function(){ … }
var person = new Person("Alex","female",19);
console.log(person.name); // Alex
console.log(person.isChinese); // undefined
console.log(person.eat()); // undefined
如果你忽略了new
關鍵字直接調用類,如:var person = Person("Alex","female",19);
,此時會直接調用Person
這個函數,如果這個函數是在全局作用域里執行的,則此時類中的this
指向的當前對象就是全局變數,在頁面中全局變數就是window
,所以通過this
添加的屬性或方法會被添加到window
中,並且最終的person
對象會是undefined
。
要解決這個問題可以採用“安全模式”,例如:
var Person = function(name,sex,age){
if(this instanceof Person){
this.name = name;
this.sex = sex;
this.age = age;
}else{ // 未使用 new
return new Person(name,sex,age);
}
}
var person = Person("Scott","male",20);
這樣就不用當心創建對象時忘記使用new
關鍵字了。
繼承
繼承可以使用現有類的所有功能,併在無需重新編寫原來的類的情況下對這些功能進行擴展。繼承所涉及的對象不止一個,JavaScript並沒有提供繼承這一現有的機制,也正因為JavaScript少了這些顯性的限制,使其更具有靈活性。在JavaScript中可以使用類式繼承、構造函數繼承、組合繼承來達到繼承的效果。
類式繼承
// 聲明父類
function Parent(){
this.parentValue = true;
}
// 為父類添加共有方法
Parent.prototype.getParentValue = function(){
return this.parentValue;
}
// 聲明子類
function Child(){
this.childValue = false;
}
// 繼承父類
Child.prototype = new Parent();
// 為子類添加共有方法
Child.prototype.getChildValue = function(){
return this.childValue;
}
類的原型對象的作用是為類的原型添加共有屬性和方法,但類必須通過原型prototype來訪問這些屬性和方法。當實例化一個父類時,新建對象複製了父類構造函數內的屬性和方法,並且將原型__proto__
指向了父類的原型對象,這樣就擁有了父類原型對象上的屬性和方法,新建對象可以直接訪問父類原型對象的屬性和方法,接著將這個新建的對象賦值給子類的原型,那麼子類的原型就可以訪問父類的原型屬性和方法。將這個對象賦值給子類的原型,那麼這個子類就可以訪問父類原型上的屬性和方法,並且可以訪問從父類構造函數中複製的屬性和方法。我們可以來測試一下:
var child = new Child();
console.log(child.getParentValue()); // true
console.log(child.getChildValue()); // false
console.log(child instanceof Parent); // true
console.log(child instanceof Child); // true
console.log(Child instanceof Parent); // false
但這種繼承方式有2個缺點:
由於子類是通過其原型prototype對父類實例化,如果父類中的共有屬性是引用類型,會被所有實例所共用,一個子類的實例修改了該屬性會直接影響到所有實例。例如:
function Parent(){ this.values = ['A','B','C']; } function Child(){} Child.prototype = new Parent(); var child1 = new Child(); var child2 = new Child(); console.log(child2.values); // ["A","B","C"] child1.values.push('D'); console.log(child2.values); // ["A","B","C","D"]
創建父類實例時,是無法向父類傳遞參數的,也就是無法對父類構造函數內的屬性進行初始化。例如這種錯誤的繼承方式:
function Parent(name){ this.name = name; } function Child(){} Child.prototype = new Parent('name'); // 錯誤
構造函數繼承
// 聲明父類
function Parent(name){
this.name = name;
this.values = ['A','B','C'];
}
Parent.prototype.showName = function(){
console.log(this.name);
}
// 聲明子類
function Child(name){
Parent.call(this,name);
}
var child1 = new Child('one');
var child2 = new Child('two');
child1.values.push('D');
console.log(child1.name); // one
console.log(child1.values); // ["A","B","C","D"]
console.log(child2.name); // two
console.log(child2.values); // ["A","B","C"]
child1.showName(); // TypeError
語句Parent.call(this,name);
是構造函數繼承的精華,call
方法可以更改函數的作用環境,在子類中執行該方法相當於將子類的變數在父類中執行一遍,此時父類方法中的this
屬性指的是子類中的this
,由於父類中是給this
綁定屬性的,所以子類也就繼承了父類的屬性和方法。構造函數繼承並沒有涉及原型prototype,所以父類的原型方法不會被子類繼承,子類的每個實例會單獨擁有一份父類的屬性方法而不能共用,如果想被子類繼承就必須放在構造函數中,要實現這樣的效果可以採用組合繼承的方式。
組合繼承
類式繼承是通過子類的原型prototype對父類實例化來實現的,構造函數繼承是通過在子類的構造函數作用環境中執行一次父類的構造函數來實現的,而組合繼承則同時做到這兩點。
// 聲明父類
function Parent(name){
this.name = name;
this.values = ['A','B','C'];
}
Parent.prototype.getName = function(){
console.log(this.name);
}
// 聲明子類
function Child(name,id){
Parent.call(this, name);
this.id = id;
}
Child.prototype = new Parent();
Child.prototype.getId = function(){
console.log(this.id);
}
var child1 = new Child('child1', 1);
child1.values.push('D');
console.log(child1.values); // ["A", "B", "C", "D"]
child1.getName(); // child1
child1.getId(); // 1
var child2 = new Child('child2', 2);
console.log(child2.values); // ["A", "B", "C"]
child2.getName(); // child2
child2.getId(); // 2
子類的實例中更改父類繼承下來的引用類型屬性,不會影響到其它實例,並且子類實例化過程中又能將參數傳遞到父類的構造函數中。
多態
多態就是同一個方法多種調用方式,JavaScript可以通過對傳入的參數列表arguments
進行判斷來實現多種調用方式。例如:
function Add(){
// 無參數
function zero(){
return 0;
}
// 一個參數
function one(num){
return num;
}
// 兩個參數
function two(num1, num2){
return num1 + num2;
}
this.add = function(){
// 獲取參數列表及參數個數
var arg = arguments,
len = arg.length;
switch(len){
case 0:
return zero();
case 1:
return one(arg[0]);
case 2:
return two(arg[0], arg[1]);
}
}
}
var A = new Add();
console.log(A.add()); // 0
console.log(A.add(1)); // 1
console.log(A.add(1,2)); // 3
當調用add進行運算時,會根據參數列表的不同做相應的運算,這就是JavaScript的多態實現方式。
總結
面向對象設計方法的應用解決了傳統結構化開發方法中客觀世界描述工具與軟體結構的不一致性問題,縮短了開發周期,解決了從分析和設計到軟體模塊結構之間多次轉換映射的繁雜過程,是一種高效率的軟體開發方式,特別是在多人協作開發的情況下,可以提高代碼的可復用性和維護性,使開發更有效率。
本文為作者kMacro原創,轉載請註明來源:http://www.jianshu.com/p/c2083cf275ec。