JavaScript 建立對象

JavaScript 建立對象

原文連接javascript

亂七八糟的概念老是阻礙咱們對知識更進一步的理解,因此咱們先來搞清楚幾個概念之間的關係。java

在 JavaScript 中,引用類型的值被稱爲對象(或實例)。es6

強調:對象實例實例對象對象實例 等意。segmentfault

建立一個對象

沒對象怎麼辦?找一個唄,額,是建立一個。spa

初學者最多見到的就是使用這兩種方法來建立單個對象:1. 使用 Object 構造函數建立,2. 使用對象字面量直接建立prototype

其實還能夠用如下的方法建立一個對象:設計

  • 經過構造函數來建立特定類型的對象(見後文構造函數模式)指針

  • 經過原型建立對象(見後文原型模式)

  • 經過 Object.create() 方法建立【MDN】

// 方法 1
var obj1 = new Object();    // 建立空對象
obj1.name = "percy";        // 爲對象添加屬性
obj1.getName = function(){  // 爲對象添加方法
  return this.name;
};

// 方法 2
var obj2 = {
  name: "percy",
  getName: function(){
    return this.name;
  }
};

使用這兩種方式建立對象有個明顯的缺點:即只建立了一個特定的對象,不便於建立多個擁有相同屬性和方法的不一樣對象。爲了解決這個問題,人們便開始使用工廠模式。

工廠模式(The Factory Pattern)

  • 優勢:解決了建立多個類似對象的問題

  • 缺點:沒法判斷工廠模式建立的對象的具體類型,由於它建立的對象都是 Object 整出來的

  • 工廠模式抽象了建立具體對象的過程

  • 因爲 ES6 以前,ECMAScript 沒有類(class)這個概念,因此開發人員用函數封裝了以特定接口建立對象的細節。

  • ES6 中引入了類(class)這個概念,做爲對象的模板。傳送門

舉例以下:

function Person(name,age,job){
  var obj = new Object();
  obj.name = name;
  obj.age = age;
  obj.job = job;
  obj.getName = function(){
    return this.name;
  };
  return obj;
}

var person1 = Person("percy",21,"killer");
var person2 = Person("zyj",20,"queen");

console.log(person1);
// Object {name: "percy", age: 21, job: "killer"}
console.log(person2);
// Object {name: "zyj", age: 20, job: "queen"}
console.log(person1.constructor);
// function Object() { [native code] }
console.log(person1.constructor);
// function Object() { [native code] }
console.log(person1 instanceof Object);  // true
console.log(person1 instanceof Person);  // false

構造函數模式(The Constructor Pattern)

  • 優勢:它能夠將它建立的對象標識爲一種特定的類型

  • 缺點:不一樣實例沒法共享相同的屬性或方法

constructor 屬性始終指向建立當前對象的構造(初始化)函數

使用構造函數模式將前面的例子進行重寫以下:

function Person(name,age,job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.getName = function(){
    return this.name;
  }
}

var person1 = new Person("percy",21,"killer");
var person2 = new Person("zyj",20,"queen");

console.log(person1);
// Object {name: "percy", age: 21, job: "killer"}
console.log(person2);
// Object {name: "zyj", age: 20, job: "queen"}
console.log(person1.constructor);
// function Person() { ... }
console.log(person1.constructor);
// function Person() { ... }
console.log(person1 instanceof Object);  // true
console.log(person1 instanceof Person);  // true

要建立 Person 的新實例,必須使用 new 操做符。以這種方式調用構造函數實際上會經歷如下 4 個步驟:

1.建立一個新對象(新實例)
2.將構造函數的做用域賦給新對象(所以 this 就指向了這個對象)
3.執行構造函數中的代碼(爲這個新對象添加屬性和方法)
4.返回新對象

任何函數,只要經過 new 操做符來調用,那麼它就能夠做爲構造函數,不然就和普通函數沒什麼兩樣

function Person(name,age,job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.getName = function(){
    return this.name;
  }
}

Person("percy",22,"無");
window.getName();       // percy

從這一小節最開始的代碼中,你可能注意到了,person1person2 這兩個對象擁有相同的方法,可是它們相等嗎?

person1.getName === person2.getName  // false

調用同一個方法,卻聲明瞭不一樣的對象,實在是浪費資源,因此就引進了接下來的主角:原型模式

原型模式(The Prototype Pattern)

  • 優勢:它實現了不一樣實例能夠共享屬性或方法

  • 缺點:它省略了構造函數初始化參數這一環節,結果全部實例在默認狀況下都取得了相同的屬性值。而且若是若是原型對象中有屬性的值爲引用類型的,要是實例重寫了這個屬性,那麼全部實例都會使用這個重寫的屬性。

  • 咱們建立的每一個函數都有一個 prototype(原型) 屬性,這個屬性是一個指針,指向一個原型對象,而這個對象的用途是包含能夠由特定類型的全部實例共享的屬性和方法

    • 上面的特定類型能夠是經過 new Person() 造成的 Person 類型。

好,把上面的例子改寫成原型模式:

function Person(){
}

Person.prototype.name = "percy";
Person.prototype.age = 21;
Person.prototype.job = "killer";
Person.prototype.getName = function(){
  return this.name;
};

var person1 = new Person();
var person2 = new Person();

console.log(person1.name);   // percy
console.log(person2.name);   // percy
console.log(person1.getName === person2.getName);  // true

clipboard.png

  • 構造函數的 prototype 屬性指向它的原型對象

  • 全部原型對象都具有一個 constructor 屬性,這個屬性指向包含 prototype 屬性的函數

  • [[Prototype]] 是實例指向構造函數的原型對象的指針,目前不是標準的屬性,但 Firefox、Safari 和 Chrome 在每一個對象上都支持一個 __proto__ 屬性,用來實現 [[Prototype]]。

  • ECMAScript 5 增長的新方法:Object.getPrototypeOf(),它能夠返回 [[Prototype]] 的值,即返回實例對象的原型。

Person.prototype.constructor === Person;              // true
person1.constructor === Person;                       // true
Object.getPrototypeOf(person1) === Person.prototype;  // true
  • 當咱們訪問一個對象中的屬性時,首先會詢問實例對象中有沒有該屬性,若是沒有則繼續查找其原型對象有沒有該屬性。因此要是實例對象中定義了與原型對象中相同名字的屬性,則優先調用實例對象中的屬性。

var p1 = new Person();
var p2 = new Person();

p1.name = "zyj";
console.log(p1.name);   // zyj
console.log(p2.name);   // percy
  • Object.prototype.hasOwnProperty(prop):檢測一個屬性是存在於對象實例中,仍是存在於原型中,若存在於實例中,則返回 true,不然返回 false。

var p1 = new Person();
var p2 = new Person();

p1.name = "zyj";
console.log(p1.hasOwnProperty("name"));  // true
console.log(p2.hasOwnProperty("name"));  // false
  • in 操做符(prop in objectName ):判斷對象實例是否可以訪問某個屬性(不管這個屬性是本身的仍是在原型對象上的),若能訪問則返回 true,不然返回 false。

var p1 = new Person();
var p2 = new Person();

p1.name = "zyj";

console.log("name" in p1);  // true
console.log("name" in p2);  // true
  • Object.keys(obj):返回對象上全部可枚舉的實例屬性

  • Object.getOwnPropertyNames(obj):返回對象上的全部實例屬性(無論能不能枚舉)

var p1 = new Person();
var p2 = new Person();

p1.name = "zyj";
p1.age = 22;
Object.defineProperty(p1,"age",{
  enumerable: false
});    // 將 age 設置爲不可枚舉

console.log(Object.keys(p1));    // ["name"]
console.log(Object.keys(p2));    // []
console.log(Object.getOwnPropertyNames(p1)); // ["name","age"]
console.log(Object.getOwnPropertyNames(p2)); // []

console.log(Object.keys(Person.prototype));
// ["name", "age", "job", "getName"]
console.log(Object.getOwnPropertyNames(Person.prototype));
// ["constructor", "name", "age", "job", "getName"]

更簡潔的原型語法

也許你已經注意到了,這一節最前面的原型寫法是否是有點囉嗦,爲何每次都要寫一遍 Person.prototype 呢?好,那咱們如今用更簡潔的原型語法以下:

function Person(){
}

Person.prototype = {
  name: "percy",
  age: 21,
  job: "killer",
  getName: function(){
    return this.name;
  }
};

是否是簡潔了許多?可是這裏也出現了一個問題,constructor 屬性再也不指向 Person了,而是指向了 Object 構造函數。記得咱們在上面提到了 Person.prototype 指向的是一個對象(原型對象),而如今咱們徹底重寫了這個原型對象,因此這個原型對象的 constructor 指向了最普遍的 Object。

var p3 = new Person();

console.log(p3 instanceof Person);  // true
console.log(p3 instanceof Object);  // true
console.log(Person.prototype.constructor === Person); // false
console.log(Person.prototype.constructor === Object); // true

因此改寫上面的代碼,使 constructor 指向 Person:

function Person(){
}

Person.prototype = {
  constructor: Person,         
  name: "percy",
  age: 21,
  job: "killer",
  getName: function(){
    return this.name;
  }
};

注意,以這種方式重設constructor 屬性會致使它的 [[Enumerable]] 特性被設置爲 false,從而 constructor 屬性變得能夠枚舉了,可是原生的 constructor 屬性是不可枚舉的,因此咱們利用 Object.defineProperty() 再改寫一下代碼:

function Person(){
}

Person.prototype = {
  name: "percy",
  age: 21,
  job: "killer",
  getName: function(){
    return this.name;
  }
};
Object.defineProperty(Person.prototype,"constructor",{
  enumerable: false,
  value: Person
});
var p3 = new Person();

console.log(p3 instanceof Person);  // true
console.log(p3 instanceof Object);  // true
console.log(Person.prototype.constructor === Person); // true
console.log(Person.prototype.constructor === Object); // false
  • 重寫原型對象應該在建立實例以前完成,不然會出現不可預知的錯誤

function Person(){
}
var p3 = new Person();

Person.prototype = {
  name: "percy",
  age: 21,
  job: "killer",
  getName: function(){
    return this.name;
  }
};
Object.defineProperty(Person.prototype,"constructor",{
  enumerable: false,
  value: Person
});

p3.getName(); // 報錯,TypeError: p3.getName is not a function(…)
  • 當原型對象中有屬性的值爲引用類型時...

function Person(){
}
Person.prototype = {
  name: "percy",
  age: 21,
  job: "killer",
  friends: ['zyj','Shelly','Dj Aligator'],  // 添加
  getName: function(){
    return this.name;
  }
};
Object.defineProperty(Person.prototype,"constructor",{
  enumerable: false,
  value: Person
});

var p1 = new Person();
var p2 = new Person();

p1.job = "programmer";
p1.friends.push('Mary','Iris');

console.log(p1.job);    // programmer
console.log(p2.job);    // killer
console.log(p1.friends);
// ["zyj", "Shelly", "Dj Aligator", "Mary", "Iris"]
console.log(p2.friends);
// ["zyj", "Shelly", "Dj Aligator", "Mary", "Iris"]
console.log(p1.friends === p2.friends);  // true

console.log(Person.prototype.friends);
// ["zyj", "Shelly", "Dj Aligator", "Mary", "Iris"]

看出問題來了嗎?當原型對象中有屬性的值爲引用類型時,要是一個實例重寫了這個屬性,那麼全部的實例都會使用這個重寫後的屬性。要是還不瞭解的話,能夠看看我之前的文章,談的是基本類型和引用類型在內存中的存儲方式,以及改變它們的值時,內存中是如何變化的。

組合使用構造函數模式和原型模式(Combination Constructor/Prototype Pattern)

  • 原理:構造函數模式用於實例本身的屬性,而原型模式用於定義方法和須要共享的屬性。結果,每一個實例都會有本身的一份實例屬性的副本,但同時又共享着對方法的引用,最大限度地節省了內存。

function Person(name,age,job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ['zyj'];
}
Person.prototype = {
  getName: function(){
    return this.name;
  }
};
Object.defineProperty(Person.prototype,"constructor",{
  enumerable: false,
  value: Person
});

var person1 = new Person("percy","21","killer");
var person2 = new Person("Bob","26","developer");

person1.friends.push("Iris","Alice");

console.log(person1.name);      // percy
console.log(person2.name);      // Bob
console.log(person1.friends);   // ["zyj", "Iris", "Alice"]
console.log(person2.friends);   // ["zyj"]

console.log(person1.friends === person2.friends); // false
console.log(person1.getName === person2.getName); // true

這種構造函數與原型混合模式,是目前在 ECMAScript 中使用最普遍、認同度最高的一種建立自定義類型的方法。

  • 爲上面的代碼補一張圖吧 :)!

啊啊啊

動態原型模式(Dynamic Prototype Pattern)

  • 原理:將全部信息封裝到構造函數中。

function Person(name,age,job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ['zyj'];
  if(typeof this.getName != "function" ){
    Person.prototype.getName = function(){
      return this.name;
    };
    Person.prototype.getJob = function(){
      return this.job;
    };
  }
}

var person = new Person("percy",21,"programmer");
console.log(person.getName()); // percy
console.log(person.getJob());  // programmer

將全部信息封裝到構造函數裏,很完美,有木有?

  • 這裏使用 if 語句檢查原型方法是否已經初始化,從而防止屢次初始化原型方法。

  • 這種模式下,不能使用對象自面量重寫原型對象。由於在已經建立了實例的狀況下再重寫原型對象的話,會切斷現有實例與新原型對象之間的聯繫。

  • 看這裏,有更詳細的對上面代碼的解釋,連接

寄生構造函數模式(Parasitic Constructor Pattern)

似曾相識哈!

一句話闡明:除了使用 new 操做符並把包裝函數叫作構造函數以外,這個模式跟工廠模式實際上是如出一轍的。

function Person(name,age,job){
  var obj = new Object();
  obj.name = name;
  obj.age = age;
  obj.job = job;
  obj.getName = function(){
    return this.name;
  };
  return obj;
}

var person1 = new Person("percy",21,"killer");
var person2 = new Person("zyj",20,"queen");

person1.getName();   // percy
person2.getName();   // zyj
  • 建議在可使用其餘模式的狀況下,不要使用這種模式。

穩妥構造函數模式(Durable Constructor Pattern)

  • 穩妥構造函數遵循與寄生構造函數相似的模式,可是有 2 點不一樣:

    • 一是新建立對象的實例方法不引用 this

    • 二是不使用 new 操做符調用構造函數

function Person(name,age,job){
  var obj = new Object();

  // 能夠在這裏定義私有變量和函數

  obj.getName = function(){
    return name;
  };
  return obj;
}

var person1 = new Person("percy",21,"killer");
var person2 = new Person("zyj",20,"queen");

person1.getName();   // percy
person2.getName();   // zyj

注意,在這種模式下建立的對象中,除過調用 getName() 方法外,沒有其餘方法訪問 name 的值。

我想問個問題,最後的這個模式能夠用在哪些地方呢?但願有經驗的朋友解答一下。

參考資料

  • 【書】《JavaScript 高級程序設計(第三版)》

相關文章
相關標籤/搜索