最詳細的JavaScript高級教程(十六)創建對象

創建一個對象再給這個對象賦值的操作需要大量的代碼,如果要創建多個對象,就要寫很多重複代碼,對象的創建可以使用下面這些方法來避免寫大量的不好維護的重複代碼。

工廠模式

  • 優點:創建一個對象的大量實例
  • 缺點:無法進行對象識別,即使用工廠模式創建的對象,還是Object對象,不是一種新的對象,也就不能使用instanceof進行驗證。總結就是說:工廠模式雖然創建了Person類的實例,但是卻沒有創建Person本身,Person的實例也無法標識出來。
function createPerson(name, age){
    var o = new Object();
    o.age = age;
    o.name = name;
    o.toString = function(){
        return name + ' ' + age;
    }
    return o; // 注意工廠方法要把o給return出去
}
var person1 = createPerson('w', 12);
alert(person1.toString()); // w 12

使用構造函數

使用構造函數方法可以完美的解決工廠方法的問題,它可以創建一種新的類型,並且允許我們使用這個類型創建實例。

先看一下構造函數方法的寫法,它的寫法與函數十分類似:

function Person(name, age){
    this.name = name;
    this.age = age;
    this.sayName = function(){
        alert(this.name);
    }
}
var person1 = new Person('Nic', 29);
var person2 = new Person('Greg', 12);
person1.sayName(); //Nic

我們需要注意:

  • 構造函數的命名,首字母應該大寫,以標識這是一個構造函數
  • 不用顯示的創建對象,在構造函數中我們不需要寫new Object
  • 直接將屬性和方法賦值給this對象
  • 不用return
  • 創建實例的時候,使用new關鍵字

當我們這麼創建了對象之後,就可以使用instanceof來看對象不是是屬於一個類型:

alert(person1 instanceof Person); // true
alert(person2 instanceof Person); // true
var s = {};
alert(s instanceof Person); // false

在對象中,還保存着構造函數的實例,我們通過constructor屬性可以看到一個對象是不是實現了某個構造函數。這個方法雖然可以使用,但是一般來說使用instanceof更好。

var s = {};
alert(person1.constructor == Person); //true
alert(s.constructor == Person); //false

js有意思之處就是,雖然它是構造函數,但是使用了函數的語法,所以,這個方法一定能當作一個普通的函數使用。我們看構造函數,它的函數體中寫的都是this. 表示構造函數其實是在自己的作用域中添加屬性和方法。那麼:

  • 如果在全局對象中直接調用一個構造函數,則屬性和方法被添加到全局作用域中
  • 如果使用apply方法在一個對象的作用域中調用構造函數則這些屬性和方法被添加到調用作用域中
function Person(name, age){
    this.name = name;
    this.age = age;
    this.sayName = function(){
        alert(this.name);
    }
}
// 將構造函數作爲普通函數調用
Person('wh', 15);
alert(window.name); // 嚴格模式下報錯,非嚴格模式下返回wh
// 在另一個對象的作用域中使用
var o = new Object();
Person.apply(o, ['ww', 12]); // 或者使用Person.call(o, 'ww', 12)
o.sayName(); //ww

構造函數雖然好,但是也有無法解決的問題,就是在構造中定義函數的時候,每一個實例對象,都會在自己的內存控件定義一份該方法的副本。而事實上,方法只是一個解決問題的辦法,定義這麼多是不應該的。我們可以通過定義全局方法,然後給其賦值的辦法,如下:

function Person(name, age){
    this.name = name;
    this.age = age;
    this.sayName = sayName;
}
function sayName(){
    alert(this.name);
}
var person1 = new Person('Nic', 29);
person1.sayName(); //Nic

這個方法可以解決這個問題,但是我們發現定義了很多全局的方法,這樣不僅會將增加全局代碼的複雜度,同時也打亂了作用域,讓代碼可讀性變差。所以這個問題,我們還需要下面介紹的原型模式來解決。

原型模式

每一個函數都具有一個prototype屬性。(注意是每一個函數,而不是每一個構造函數,構造函數本質上也是函數)

這個prototype屬性保存了所有實例共享的屬性和方法。我們可以認爲一旦一個屬性寫入了一個函數的prototype中,則這個屬性被全部實例共有。

如果需要訪問原型對象,有下面兩種方法:

  • 使用構造函數的prototype屬性
  • 使用實例的__proto__屬性
function Person() {}
Person.prototype.name = 'jo';
var person1 = new Person();
alert(person1.name); //jo
alert(person1.__proto__.name); //jo

比較好理解的是,一個方法一旦寫入了函數的prototype中,則所有實例共有這個方法:

function Person(){
    this.__proto__.say = function(){
        alert('hello');
    };
}
var person1 = new Person();
var person2 = new Person();
person1.say(); //hello
person2.say(); //hello

而如果原型中定義了一個屬性,大家都公用,那不是所有的實例都一樣了麼?其實不是,所有的實例擁有原型所有的原型對象上的屬性和方法,而他們自己允許重寫這些方法和屬性,當我們訪問一個實例上的屬性的時候,解析器會先去尋找有沒有複寫這個屬性,如果有,用複寫的方法,如果沒有,使用原型對象的方法,下面一個例子說明了這種查找值的做法:

function Person() {}
Person.prototype.name = 'jo';
var person1 = new Person();
alert(person1.name); //jo ---沒有複寫,使用原型對象的值
alert(person1.__proto__.name); //jo
person1.name = 'mo';
alert(person1.name); //mo   --- 複寫過了,來源於複寫的值
alert(person1.__proto__.name); //jo ---原型對象上值不變

下面這張圖有助於我們理解其覆蓋關係:
在這裏插入圖片描述

原型模式的高級用法

  • constructor 在構造函數中,prototype指向了原型對象,在原型對象中constructor指針指向了構造函數,他們互相指,提高了靈活性,如下圖
    在這裏插入圖片描述
    Person.prototype ---原型對象
    Person.prototype.constructor ---指向構造函數
    
  • 判斷實例的原型是否是某一個對象的方法
    function Person() {}
    Person.prototype.name = 'jo';
    var person1 = new Person();
    var b = Person.prototype.isPrototypeOf(person1); // true
    
  • 從實例獲取原型對象,可以使用__proto__也可以使用Object.getPrototypeOf
    function Person() {}
    Person.prototype.name = 'jo';
    var person1 = new Person();
    var b = Object.getPrototypeOf(person1) == person1.__proto__; // true
    
  • 刪除實例上的屬性和方法(只能刪除實例的,不會影響原型上的)
    function Person() {}
    Person.prototype.name = 'jo';
    var person1 = new Person();
    person1.name = 'p';
    alert(person1.name); // p
    delete person1.name;
    alert(person1.name); // jo 原型對象上的屬性仍在
    
  • 判斷一個屬性屬於實例還是屬於原型對象:hasOwnProperty(訪問的是實例上的返回true,訪問的是原型上的返回false)
    function Person() {}
    Person.prototype.name = 'jo';
    var person1 = new Person();
    person1.name = 'p';
    alert(person1.name); // p
    alert(person1.hasOwnProperty('name')); // true
    delete person1.name;
    alert(person1.name); // jo
    alert(person1.hasOwnProperty('name')); // false
    
  • 判斷一個屬性是否在實例上或者原型上:in(只要能訪問到,就返回true)
    function Person() {}
    var person1 = new Person();
    Person.prototype.name = 'jo';
    alert('name' in person1); //true
    person1.name = 'w';
    alert('name' in person1); //true
    
  • 判斷一個屬性是否僅存在於原型上
    // 如果一個屬性僅存在於原型上返回true
    function hasPrototypeProperty(object, name){
        return !object.hasOwnProperty(name) && (name in object);
    }
    
  • 給原生對象添加新的方法
    // 可以給String添加方法,很簡單,就是給其原型添加方法
    String.prototype.startsWith = function(text) {
        return this.indexOf(text) == 0;
    };
    var msg = 'hello';
    alert(msg.startsWith('h'));
    
    雖然可以這麼做,但是我們不建議這麼做,因爲這樣可能導致命名衝突,也可能意外的重寫原生方法。

簡單的原型語法

在上一課中我們遍歷了Person的prototype中所有的屬性,我們發現除了我們定義的屬性,只有一個constructor屬性,指向其構造函數,這個我們在這一課的學習中也看到了相應的說明。

在定義一個構造函數的prototype的時候,需要一遍一遍的寫Person.prototype,這樣很繁瑣,我們可以使用一個新的對象來代替prototype來達到快速構建原型對象的目的。

  function Person() {}
  // 直接構建其原型數組
  Person.prototype = {
    name: '23',
    age: 1
  };
  var p2 = new Person();
  alert(p2.name); //23
  alert(Person.prototype.constructor); //constructor沒有定義,根據之前講的查找順序指向了Object的構造函數

我們發現,新構建的原型沒有constructor,而constructor應該指向Person方法,這時候如果我們在之後的邏輯中用到了constructor,則需要重新構建constructor,如果用不到,不管也行。

需要注意的是,原先的constructor是不可枚舉的,我們可以通過簡單的賦值操作來給constructor賦值,這樣賦值的constructor是可以枚舉的,如果我們要構建與原來一模一樣的constructor,需要使用我們之前學過的defineProperty方法

// 直接恢復原型中的constructor
Person.prototype = {
    constructor: Person,
    name: '23',
    age: 1
  };
// 構建不可枚舉的constructor
function Person() {}
Object.defineProperty(Person.prototype, 'constructor', {
    enumerable: false,
    value: Person
});

直接使用一個對象來定義構造函數的prototype還有一個潛在的風險,就是如果實例化在對象賦值給prototype之前,這個對象的prototype指向會錯誤。我們在驗證這個知識的時候需要知道下面幾個知識:

  • 實例和構造函數之間沒有聯繫,他們倆是通過prototype來聯繫在一起的,實例中有指針指向prototype,prototype和構造函數互相有指向
  • 實例prototype的確定是在實例化的時候(new的時候)
function Person() {}
// 先實例化,此時p2中原型對象指向Person的原型
var p2 = new Person();
// 直接構建其原型數組,prototype的指向被更改,p2中的prototype失效
Person.prototype = {
    name: '23',
    age: 1
};
alert(p2.name); //undefined

用下面的圖也可以解釋這種現象在這裏插入圖片描述

原型模式的缺點

我們說原型模式用於在各個實例中共享一些屬性,它強於共享方法,甚至我們提出原型模式就是爲了解決共享方法的問題。

當我們提到了共享屬性,接受了原型模式會共享屬性的時候,我們發現這個特性用處並不大,因爲一般來說,實例需要有自己的屬性,原型屬性當個默認值尚且合格。

而我們之前在做例子的時候使用的屬性都是值類型的,當我們使用引用類型的屬性的時候,我們就發現了一個很蛋疼的現象,修改了其中一個引用類型的值,另一個也跟着變了

function Person() {}
Person.prototype.arr = [1, 2];
var p1 = new Person();
var p2 = new Person();
p1.arr.push(3);
alert(p2.arr); //1,2,3

雖然我們能理解這種在引用屬性上面發生的異常情況,但是這通常不是我們想要的。那麼怎麼解決這種問題呢?我們想想之前講的兩種模式,工廠模式和構造函數模式,他們兩個都可以解決在初始化的時候給對象賦值的操作,而構造函數方法更加的方便快捷,我們通過構造函數方和原型方法的結合,就可以最舒服的完成對象的創建。

組合構造函數模式和原型模式

有了我們之前的積累,我們發現,結合構造函數模式和原型模式可以解決我們的問題,事實上,這也是我們創建自定義類型最常見的方式。我們要達到的效果:

  • 共享的屬性和方法可以共享
  • 不共享的屬性每個實例都有一份自己的副本

要實現這種“智能”的創建對象,也十分容易:

  • 在原型對象中構建共享的屬性和方法
  • 在構造函數中構建各自的屬性
  function Person(name, job) {
    this.name = name;
    this.job = job;
    // 每個實例都擁有一個friends數組,下面的方法等於設置了默認值
    this.friends = ['Shel', 'Cour'];
  }
  Person.prototype = {
    constructor: Person,
    sayName: function() {
      alert(this.name);
    }
  };
  var p1 = new Person('Nic', 'SE');
  var p2 = new Person('Greg', 'Doctor');

  p1.friends.push('Van');
  alert(p1.friends); //Shel,Cour,Van
  alert(p2.friends); //Shel,Cour
  alert(p1.sayName == p2.sayName); //true

組合構造模式解決了之前提到的問題,但是,構造函數和原型對象分開的寫法既不好理解也帶來了額外的理解成本,所以我們也可以在構造函數中初始化原型。下面介紹的動態原型模式可以解決這個問題。

動態原型模式(推薦)

這是最好的定義對象的實踐,我們推薦使用這種方法。注意其中方法的初始化,判斷方法是否存在以期只初始化一次方法。

function Person(name, age){
    // 初始化屬性
    this.name = name;
    this.age = age;
    // 這樣寫原型只會初始化一次,是比較好的方法
    // 如果有好幾個函數,不需要一個一個的判斷,可以只判斷其中一個,目的就是看看是不是第一次實例化
    if(typeof this.sayName != 'function'){
        Person.prototype.sayName = function(){
            alert(this.name);
        }
    }
}

寄生構造函數模式(不常用)

寄生模式是爲了解決之前提到的:最好不要給基本類型的原型添加方法,容易造成命名衝突的問題。如果我確實想給Array添加一個方法,就最好用寄生模式。

在正常情況下,不推薦使用這個模式,只有在上面提到的特定的情況之下使用才比較好。

寄生模式的定義與工廠模式一摸一樣,只是在使用的時候,用new來進行初始化,其初始化的對象也無法解決工廠模式的問題,即也存在對象無法識別的問題,所以這種模式只是工廠模式的變體,實際使用的時候儘量不用。

function SpecialArray(){
    var values = new Array();
    values.push.apply(values, arguments);
    values.toPipedString = function(){
        return this.join('|');
    }
    return values;
}
var colors = new SpecialArray('red', 'blue');
alert(colors.toPipedString()); //red|blue

穩妥構造函數模式(不常用)

穩妥構造模式是在原來工廠模式的基礎上,去掉了公共的屬性,只保留方法。而構造函數中定義的變量是私有變量,不暴露出來,這樣,構造函數保護了所有的內部變量,只通過函數將需要進行的操作暴露出去,構建了一個安全的環境。

這種方式同樣無法進行對象識別。與寄生一樣,在特地情況使用。

function Person(name){
    var o = new Object();
    var name = name; // var定義的name,沒有寫 o.name = name; name沒有被暴露出去,保護了局部變量
    o.toString = function(){
        return name;
    }
    return o;
}
var p = Person('wh');
alert(p.toString()); 'wh'
alert(name); //''
alert(p.name); // undefined
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章