1. 程式人生 > >javascript面向物件系列第一篇——建構函式和原型物件

javascript面向物件系列第一篇——建構函式和原型物件

前面的話

  一般地,javascript使用建構函式和原型物件來進行面向物件程式設計,它們的表現與其他面向物件程式語言中的類相似又不同。本文將詳細介紹如何用建構函式和原型物件來建立物件

建構函式

  建構函式是用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

constructor

  原型物件預設只會取得一個constructor屬性,指向該原型物件對應的建構函式。至於其他方法,則是從Object繼承來的

function Foo(){};
console.log(Foo.prototype.constructor === Foo);//true
  由於例項物件可以繼承原型物件的屬性,所以例項物件也擁有constructor屬性,同樣指向原型物件對應的建構函式
function Foo(){};
var f1 = new Foo;
console.log(f1.constructor === Foo);//true
proto   例項物件內部包含一個proto屬性(IE10-瀏覽器不支援該屬性),指向該例項物件對應的原型物件
function Foo(){};
var f1 = new Foo;
console.log(f1.__proto__ === Foo.prototype);//true

  [注意]關於proto、constructor和prototype這三者的詳細圖例關係移步至此

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
//
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);
}
Person.prototype.favoraties = [];

var person1 = new Person('bai');
var person2 = new Person('hu');

person1.favoraties.push('pizza');
person2.favoraties.push('quinoa');
console.log(person1.favoraties);//["pizza", "quinoa"]
console.log(person2.favoraties);//["pizza", "quinoa"]

  雖然可以在原型物件上一一新增屬性,但是直接用一個物件字面形式替換原型物件更簡潔

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

  當一個函式被建立時,該原型物件的constructor屬性自動建立,並指向該函式。當使用物件字面形式改寫原型物件Person.prototype時,需要在改寫原型物件時手動重置其constructor屬性

function Person(name){
    this.name = name;
}
Person.prototype = {
    constructor: Person,
    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);//true
console.log(person1.constructor === Object);//false

  由於預設情況下,原生的constructor屬性是不可列舉的,更妥善的解決方法是使用Object.defineProperty()方法,改變其屬性描述符中的列舉性enumerable

function Person(name){
    this.name = name;
}
Person.prototype = {
    sayName: function(){
        console.log(this.name);
    },
    toString : function(){
        return '[person ' + this.name + ']'
    }
};
Object.defineProperty(Person.prototype,'constructor',{
    enumerable: false,
    value: Person
});
var person1 = new Person('bai');
console.log(person1 instanceof Person);//true
console.log(person1.constructor === Person);//true
console.log(person1.constructor === Object);//false

總結

  建構函式、原型物件和例項物件之間的關係是例項物件和建構函式之間沒有直接聯絡

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

  以上程式碼的原型物件是Foo.prototype,例項物件是f1,建構函式是Foo

  原型物件和例項物件的關係

console.log(Foo.prototype === f1.__proto__);//true

  原型物件和建構函式的關係 

console.log(Foo.prototype.constructor === Foo);//true

  而例項物件和建構函式則沒有直接關係,間接關係是例項物件可以繼承原型物件的constructor屬性

console.log(f1.constructor === Foo);//true

  如果非要扯例項物件和建構函式的關係,那隻能是下面這句程式碼,例項物件是建構函式的new操作的結果

var f1 = new Foo;

  這句程式碼執行以後,如果重置原型物件,則會打破它們三個的關係

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

Foo.prototype = {};
console.log(Foo.prototype === f1.__proto__);//false
console.log(Foo.prototype.constructor === Foo);//false

  所以,程式碼順序很重要

參考資料

【1】 阮一峰Javascript標準參考教程——面向物件程式設計概述 http://javascript.ruanyifeng.com/oop/basic.html
【2】《javascript權威指南(第6版)》第6章 物件
【3】《javascript高階程式設計(第3版)》第6章 面向物件的程式設計
【4】《javascript面向物件精要》 第4章 建構函式和原型物件
【5】《你不知道的javascript上卷》第5章 原型