javascript面向對象編程基礎

js是一個基於對象的語言,所以本文研究一下js對象和類實現的過程和原理。

對象的屬性及屬性特性

下面是一個對象的各個部分:

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…遍歷到;
是否可配置指的是其可寫性,可枚舉性,可配置性是否可修改,並且決定該屬性可否被刪除。

這是一個普通的對象和常見操作,不多說,下面是一個具有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個性質:

可擴展性

可不可擴展是指一個對象可不可以添加新的屬性;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: # is not extensible(…)

function fun(){
    'use strict'
    person.height = 180;  //報錯TypeError: # is not extensible(…)
}
fun();

Object.defineProperty("height", {
    value: 180
});  //由於函數內部采用嚴格模式,所以報錯TypeError: # 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: # is not extensible(...)

凍結性

此時,這個對象屬性可能還是可寫的,如果我們想讓一個對象的屬性既不可寫也不可配置,同時讓該對象不可擴展,那麼就需要凍結這個對象。用Object.freeze()凍結對象,用isFrozen()判斷對象是否被凍結。由於相比上一個例子,僅僅是現有的變得不可寫瞭,這裡就不舉太多例子瞭。
不過值得註意的是,對於具有setter的屬性一樣不可寫。

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(…)

構造函數(Constructor)

當我們想創建很多個人的時候,就不會像上面這樣一個一個寫瞭。那我們就造一個工廠,用來生產人(感覺有點恐怖):

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);  //創建一個robot繼承於o
var r2 = Object.create(null);  //創建一個robot繼承於null
var r3 = Object.create(Object);   //創建一個robot繼承於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 = Object.create(Person.prototype);

第一個方法如果Coder修改瞭prototype會影響Person,不推薦使用;
第二個方法,Coder會繼承Person中定義的屬性,如this.name,如果該屬性對Coder沒用那就是浪費內存;
第三個方法,Coder不會繼承Person中定義的屬性。
應根據需要選擇使用二、三兩種方法。

當然還有以下這種方法,相當於上面第二種方法:

function Person(pname){
    this.name = pname;
}
function Coder(pname){
    Person.apply(this, argument);
}

instanceof

instanceof用來判斷對象是否某個構造函數的實例。這個東西很簡單,不僅可以判斷是否直接構造函數實例,還能判斷是否父對象構造函數的實例

function Person(){}

var p = new Person();

console.log(p instanceof Person); //true
console.log(p instanceof Object); //true

多態/重構

js的方法名不能相同,我們隻能模擬實現類似c++一樣的多態。

編譯時多態

註意:這個名字隻是用瞭強類型語言的說法,js是個解釋型語言,沒用編譯過程

在方法內部判斷參數情況進行重載
1. 參數數量不同做不同的事情

//修改字體,僅用部分屬性舉例:
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();  //測試

發佈留言