文章出處

前面的話

  一般地,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
//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);
}
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章 原型


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()