JS面向對象編程之封裝


來源:https://segmentfault.com/a/1190000015843072

我們所熟知的面向對象語言如 C++、Java 都有類的的概念,類是實例的類型模板,比如Student表示學生這種類型,而不表示任何具體的某個學生,而實例就是根據這個類型創建的一個具體的對象,比如zhangsanlisi,由類生成對象體現了抽象模板到具體化的過程,這叫做基於類的面向對象方式,而 JavaScript 沒有類的概念,是基於原型的面向對象方式(雖然 Es6 增加了 class,實質是對原型方式的封裝)。總結起來就是以下兩點:

  • 在基於類的面向對象方式中,對象(object)依靠類(class)來產生。
  • 在基於原型的面向對象方式中,對象(object)則是依靠構造函數(constructor)和原型(prototype)構造出來的。

面向對象語言的第一個特性毫無疑問是封裝,在 JS 中,封裝的過程就是把一些屬性和方法放到對象中“包裹”起來,那么我們要怎么去封裝屬性和方法,或者說怎么去創建對象呢(后文統一說創建對象)?下面用逐步推進的方式闡述:

對象字面量 --> 工廠模式 --> 構造函數 --> 原型模式 --> 構造函數+原型模式

對象字面量

JS中創建對象最原始的方式有兩種:

function Person(name, age) {
  this.name = name;
  this.age = age;
  
  this.greeting = function() {
    alert('Hi!');
  };
  // return this;
}

var person1 = new Person("leon", "20");
var person2 = new Person("jack", "21");

 

 

  • 對象字面量
var  person = {
    name: "leon",
    age: "20",

    greeting: function() {
      alert('Hi!');
    }
}
  • Object實例添加屬性方法
var person = new Object();
person.name = "leon";
person.age = "20";

person.greeting = function() {
  alert('Hi!');
};
  • 優點:代碼簡單
  • 缺點: 創建多個對象會產生大量的代碼,編寫麻煩,且並沒有實例與原型的概念。
  • 解決辦法:工廠模式。

工廠模式

工廠模式是編程領域一種廣為人知的設計模式,它抽象了創建具體對象的過程。JS 中創建一個函數,把創建新對象、添加對象屬性、返回對象的過程放到這個函數中,用戶只需調用函數來生成對象而無需關注對象創建細節,這叫工廠模式:

function createPerson(name, age) {
  var person = new Object();
  person.name = name;
  person.age = age;

  person.greeting = function() {
    alert('Hi!');
  };
  return person;
}

var person1 = createPerson("leon", "20");
  • 優點:工廠模式解決了對象字面量創建對象代碼重復問題,創建相似對象可以使用同一API。
  • 缺點:因為是調用函創建對象,無法識別對象的類型。
  • 解決辦法:構造函數

構造函數

JS 中構造函數與其他函數的唯一區別,就在於調用它的方式不同。任何函數,只要通過new 操作符來調用,那它就可以作為構造函數。來看下面的例子:

function Person(name, age) {
  this.name = name;
  this.age = age;
  
  this.greeting = function() {
    alert('Hi!');
  };
  // return this;
}

var person1 = new Person("leon", "20");
var person2 = new Person("jack", "21");

通過構造函數new一個實例經歷了四步:

  1. 創建一個新對象;
  2. 將構造函數內的this綁定到新對象上;
  3. 為新對象添加屬性和方法;
  4. 返回新對象(JS 引擎會默認添加 return this;)。

而通過構造函數創建的對象都有一個constructor屬性,它是一個指向構造函數本身的指針,因此就可以檢測對象的類型啦。:

alert(person1.constructor === Person) //true
alert(person1 instanceof Person) // true

但是仍然存在問題:

alert(person1.greeting == person2.greeting) //false

同一個構造函數中定義了greeting(),而不同實例上的同名函數卻是不相等的,意味着這兩個同名函數的內存空間不一致,也就是構造函數中的方法要在每個實例上重新創建一次。這顯然是不划算的。

  • 優點:解決了類似對象創建問題,且可以檢測對象類型。
  • 缺點:構造函數方法要在每個實例上新建一次。
  • 解決辦法:原型模式。

原型模式

終於講到了原型模式,JS 中每個構造函數都有一個prototype屬性,這個屬性是一個指針,指向原型對象,而這個原型對象包含了這個構造函數所有實例共享的屬性和方法。而實例對象中有一個proto屬性,它指向原型對象,也就是構造函數.prototype == 原型對象 == 對象._proto_,那么對象就可以獲取到原型對象中的屬性和方法啦。同時,所有對象中都有一個constructor屬性,原型對象的constructor指向其對應的構造函數。

使用原型,就意味着我們可以把希望實例共享的屬性和方法放到原型對象中去,而不是放在構造函數中,這樣每一次通過構造函數new一個實例,原型對象中定義的方法都不會重新創建一次。來看下面的例子:

function Person() {
}

Person.prototype.name = "leon";
Person.prototype.age = "20";
Person.prototype.greeting = function() {
  alert('Hi!');
};

var person1 = new Person();
var person2 = new Person();
alert(person1.name); //"leon"
alert(person2.name); //"leon"
alert(person1.greeting == person2.greeting); //true
  • 優點:與單純使用構造函數不一樣,原型對象中的方法不會在實例中重新創建一次,節約內存。
  • 缺點:使用空構造函數,實例 person1 和 person2 的 name都一樣了,我們顯然不希望所有實例屬性方法都一樣,它們還是要有自己獨有的屬性方法。並且如果原型中對象中有引用類型值,實例中獲得的都是該值的引用,意味着一個實例修改了這個值,其他實例中的值都會相應改變。
  • 解決辦法:構造函數+原型模式組合使用。

另外 JS 中還定義了一些與原型相關的屬性,這里羅列一下:

  • Object.getPrototypeOf(),取得實例的原型對象。
Object.getPrototypeOf(person1);
  • isPrototypeOf(),判斷是不是一個實例的原型對象。
Person.prototype.isPrototypeOf(person1);
  • hasOwnProperty(),檢測一個屬性是否存在於實例中
person1.hasOwnProperty("name");
  • in,判斷一個屬性是否存在於實例和原型中。
"name" in person1;

構造函數+原型模式

最后一種方式就是組合使用構造函數和原型模式,構造函數用於定義實例屬性,而共享屬性和方法定義在原型對象中。這樣每個實例都有自己獨有的屬性,同時又有對共享方法的引用,節省內存。

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

Person.prototype = {
  constructor: Person,
  nationality: "China",
  greeting: function() {
    alert(this.name);
  }
}

var person1 = new Person("leon", "20");
var person2 = new Person("jack", "21");
alert(person1.greeting == person2.greeting) //true

上面代碼中用對象字面量的形式重寫了原型對象,這樣相當於創建了一個新的對象,那么它的constructor屬性就會指向Object,這里為了讓它繼續指向構造函數,顯示的寫上了constructor: Person

這種構造函數與原型模式混成的模式,是目前在 JS 中使用最為廣泛的一種創建對象的方法。

 

 

 

 

 

 

 

我們所熟知的面向對象語言如 C++、Java 都有類的的概念,類是實例的類型模板,比如Student表示學生這種類型,而不表示任何具體的某個學生,而實例就是根據這個類型創建的一個具體的對象,比如zhangsanlisi,由類生成對象體現了抽象模板到具體化的過程,這叫做基於類的面向對象方式,而 JavaScript 沒有類的概念,是基於原型的面向對象方式(雖然 Es6 增加了 class,實質是對原型方式的封裝)。總結起來就是以下兩點:

  • 在基於類的面向對象方式中,對象(object)依靠類(class)來產生。
  • 在基於原型的面向對象方式中,對象(object)則是依靠構造函數(constructor)和原型(prototype)構造出來的。

面向對象語言的第一個特性毫無疑問是封裝,在 JS 中,封裝的過程就是把一些屬性和方法放到對象中“包裹”起來,那么我們要怎么去封裝屬性和方法,或者說怎么去創建對象呢(后文統一說創建對象)?下面用逐步推進的方式闡述:

對象字面量 --> 工廠模式 --> 構造函數 --> 原型模式 --> 構造函數+原型模式

對象字面量

JS中創建對象最原始的方式有兩種:

  • 對象字面量
var  person = {
    name: "leon",
    age: "20",

    greeting: function() {
      alert('Hi!');
    }
}
  • Object實例添加屬性方法
var person = new Object();
person.name = "leon";
person.age = "20";

person.greeting = function() {
  alert('Hi!');
};
  • 優點:代碼簡單
  • 缺點: 創建多個對象會產生大量的代碼,編寫麻煩,且並沒有實例與原型的概念。
  • 解決辦法:工廠模式。

工廠模式

工廠模式是編程領域一種廣為人知的設計模式,它抽象了創建具體對象的過程。JS 中創建一個函數,把創建新對象、添加對象屬性、返回對象的過程放到這個函數中,用戶只需調用函數來生成對象而無需關注對象創建細節,這叫工廠模式:

function createPerson(name, age) {
  var person = new Object();
  person.name = name;
  person.age = age;

  person.greeting = function() {
    alert('Hi!');
  };
  return person;
}

var person1 = createPerson("leon", "20");
  • 優點:工廠模式解決了對象字面量創建對象代碼重復問題,創建相似對象可以使用同一API。
  • 缺點:因為是調用函創建對象,無法識別對象的類型。
  • 解決辦法:構造函數

構造函數

JS 中構造函數與其他函數的唯一區別,就在於調用它的方式不同。任何函數,只要通過new 操作符來調用,那它就可以作為構造函數。來看下面的例子:

function Person(name, age) {
  this.name = name;
  this.age = age;
  
  this.greeting = function() {
    alert('Hi!');
  };
  // return this;
}

var person1 = new Person("leon", "20");
var person2 = new Person("jack", "21");

通過構造函數new一個實例經歷了四步:

  1. 創建一個新對象;
  2. 將構造函數內的this綁定到新對象上;
  3. 為新對象添加屬性和方法;
  4. 返回新對象(JS 引擎會默認添加 return this;)。

而通過構造函數創建的對象都有一個constructor屬性,它是一個指向構造函數本身的指針,因此就可以檢測對象的類型啦。:

alert(person1.constructor === Person) //true
alert(person1 instanceof Person) // true

但是仍然存在問題:

alert(person1.greeting == person2.greeting) //false

同一個構造函數中定義了greeting(),而不同實例上的同名函數卻是不相等的,意味着這兩個同名函數的內存空間不一致,也就是構造函數中的方法要在每個實例上重新創建一次。這顯然是不划算的。

  • 優點:解決了類似對象創建問題,且可以檢測對象類型。
  • 缺點:構造函數方法要在每個實例上新建一次。
  • 解決辦法:原型模式。

原型模式

終於講到了原型模式,JS 中每個構造函數都有一個prototype屬性,這個屬性是一個指針,指向原型對象,而這個原型對象包含了這個構造函數所有實例共享的屬性和方法。而實例對象中有一個proto屬性,它指向原型對象,也就是構造函數.prototype == 原型對象 == 對象._proto_,那么對象就可以獲取到原型對象中的屬性和方法啦。同時,所有對象中都有一個constructor屬性,原型對象的constructor指向其對應的構造函數。

使用原型,就意味着我們可以把希望實例共享的屬性和方法放到原型對象中去,而不是放在構造函數中,這樣每一次通過構造函數new一個實例,原型對象中定義的方法都不會重新創建一次。來看下面的例子:

function Person() {
}

Person.prototype.name = "leon";
Person.prototype.age = "20";
Person.prototype.greeting = function() {
  alert('Hi!');
};

var person1 = new Person();
var person2 = new Person();
alert(person1.name); //"leon"
alert(person2.name); //"leon"
alert(person1.greeting == person2.greeting); //true
  • 優點:與單純使用構造函數不一樣,原型對象中的方法不會在實例中重新創建一次,節約內存。
  • 缺點:使用空構造函數,實例 person1 和 person2 的 name都一樣了,我們顯然不希望所有實例屬性方法都一樣,它們還是要有自己獨有的屬性方法。並且如果原型中對象中有引用類型值,實例中獲得的都是該值的引用,意味着一個實例修改了這個值,其他實例中的值都會相應改變。
  • 解決辦法:構造函數+原型模式組合使用。

另外 JS 中還定義了一些與原型相關的屬性,這里羅列一下:

  • Object.getPrototypeOf(),取得實例的原型對象。
Object.getPrototypeOf(person1);
  • isPrototypeOf(),判斷是不是一個實例的原型對象。
Person.prototype.isPrototypeOf(person1);
  • hasOwnProperty(),檢測一個屬性是否存在於實例中
person1.hasOwnProperty("name");
  • in,判斷一個屬性是否存在於實例和原型中。
"name" in person1;

構造函數+原型模式

最后一種方式就是組合使用構造函數和原型模式,構造函數用於定義實例屬性,而共享屬性和方法定義在原型對象中。這樣每個實例都有自己獨有的屬性,同時又有對共享方法的引用,節省內存。

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

Person.prototype = {
  constructor: Person,
  nationality: "China",
  greeting: function() {
    alert(this.name);
  }
}

var person1 = new Person("leon", "20");
var person2 = new Person("jack", "21");
alert(person1.greeting == person2.greeting) //true

上面代碼中用對象字面量的形式重寫了原型對象,這樣相當於創建了一個新的對象,那么它的constructor屬性就會指向Object,這里為了讓它繼續指向構造函數,顯示的寫上了constructor: Person

這種構造函數與原型模式混成的模式,是目前在 JS 中使用最為廣泛的一種創建對象的方法。

 

 

 

個人理解:

//個性化屬性
function Person(code_id, link, selector) {
    this.code_id = code_id;
    this.link = link;
    this.selector = selector;
}


//公共屬性(寫法一)
function setWidth(w) {
    Person.prototype.w = w;
}


function setHeight(h) {
    Person.prototype.h = h;
}


//公共屬性(寫法二,只能用一次)
Person.prototype = {
  constructor: Person,
  nationality: "China",
  greeting: function() {
    alert(this.name);
  }
}

意味着這兩個同名函數的內存空間一致,也就是構造函數中的方法要在每個實例上沒有重新創建一次。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM