深入理解 JavaScript 對象和原型

1.JavaScript 對象

js中的對象都是都是內置對象 Object 的實例,創(chuàng)建一個自定義對象最簡單的方法就是 new Objedct() 或者 使用對象字面量方式創(chuàng)建,但是在開發(fā)工作中一般都使用對象字面量方式創(chuàng)建對象,而且對象是一組無序的數據和方法的集合,這些數據和方法一般稱為對象的屬性;這些屬性在創(chuàng)建是都帶有一些特征值,JavaScript 通過這些特征值來定義它們的行為。

// 實例化 Object 對象
var person = new Object();
person.name = 'Alex';
person.age = 20;
person.getName = function() {
  return this.name;
}
// 對象字面量創(chuàng)建對象
var person = {
  name: 'Alex',
  age: 20,
  getName: function() {
    return this.name;
  }
}

2.對象屬性的類型

ECMA-262 第 5 版在定義只有內部才用的特性(attribute)時,描述了屬性(property)的各種特征。ECME-262 定義這些特性是為了實現(xiàn) JavaScript 引擎用的,因此在 JavaScript 中不能直接訪問它們,為了表示特性是內部值,該規(guī)范把它們放到了兩對方括號中,例如 [[configurable]]。

JavaScript 對象屬性有兩種類型:數據屬性和訪問器屬性

2.1 數據屬性

數據屬性包含一個數據值的位置。在這個位置可以讀取和寫入。數據屬性有四個描述其行為的特性值:

  1. [[configurable]]:表示能否通過 delete 刪除屬性從而新定義屬性;能否修改屬性的特性;能否把屬性修改為訪問器屬性。像上面例子那樣在對象上直接定義屬性時,它們的這個特性默認值為 true。
  2. [[enumerable]]:表示能否通過 for-in 循環(huán)返回屬性。像上面例子那樣在對象上直接定義屬性時,它們的這個特性默認值為 true。
  3. [[writable]]:表示能否修改屬性的值。像上面例子那樣在對象上直接定義屬性時,它們的這個特性默認值為 true。
  4. [[value]]:包含這個屬性的數據值。讀取屬性值的時候從這個位置讀;寫入屬性的時候把新值保存在這個位置。這個特性的默認值為 undefined。

直接在對象上定義屬性,則屬性的[[configurable]]、[[enumerable]]、[[writable]]特性都被設置為 true,[[value]]特性被設置為指定的值。例如:

var person = {
  name: 'Alex'
}

此時 person 的 name 屬性的 [[value]] 特性被設置為了 'Alex';

要修改屬性的默認特性是,只能使用 ECMAScript 5 的 Object.defineProperty() 方法。該方法接受三個參數:屬性所在的對象、屬性的名稱、一個描述符(descirptor)對象。其中描述符對象的屬性必須是:configurable, enumerable, writable和value,設置其中一個或多個會修改對應的特性。

var person = {}
Object.defineProperty(person, 'name', {
  writable: false,
  value: 'Alex'
})
console.log(person.name) // Alex
person.name = 'ABC'
console.log(person.name) // Alex

上面的例子創(chuàng)建了一個 name 屬性,設置 此屬性的 writable 為 false,則 name 屬性是一個只讀屬性,在非嚴格模式下,復制操作將會被忽略;在嚴格模式下,賦值操作將會拋出錯誤

Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Object>'
    at <anonymous>:8:13

同樣的規(guī)則同樣適用于可配置屬性 configurable,設置 configurable 表示不能從對象中刪除屬性,如果對這個屬性調用 delete,在非嚴格模式下會忽略該操作。

var person = {}
Object.defineProperty(person, 'name', {
  configurable: false,
  value: 'Alex'
})
console.log(person.name) // Alex
delete person.name
console.log(person.name) // Alex

在嚴格模式下會拋出錯誤:

Uncaught TypeError: Cannot delete property 'name' of #<Object>
    at <anonymous>:8:1

一旦將 configurable 配置 false,再調用 Object.defineProperty() 方法修改 name 的特性都會報錯(以 chrome 為例):


屏幕快照 2020-03-14 16.16.14.png

事實上,可以多次調用 Object.defineProperty() 多次修改同一個屬性,但是當把 configurable 設置為 false 之后就有限制了。
在調用 Object.defineProperty() 創(chuàng)建一個新屬性時,如果不指定,configurable、enumerable,writable默認都為false。但是在修改某個已定義的屬性時則無此限制

var person = {}
Object.defineProperty(person, 'name', {
  value: 'Alex'
})
// Object.getOwnPropertyDescriptor() 后面會說
console.log(Object.getOwnPropertyDescriptor(person, 'name'))
/*
*   value: "Alex"
*   writable: false
*   enumerable: false
*   configurable: false
*/
var person = {
  name: 'Alex'
}
// Object.getOwnPropertyDescriptor() 后面會說
console.log(Object.getOwnPropertyDescriptor(person, 'name'))
/*
*   value: "Alex"
*   writable: true
*   enumerable: true
*   configurable: true
*/

IE8 是第一個實現(xiàn) Object.defineProperty() 方法的瀏覽器版本。然而這個版本存在著諸多限制:只能在 DOM 對象上使用這個方法,而且只能創(chuàng)建訪問器屬性。由于實現(xiàn)不徹底,建議不要在 IE8 中使用 Object.defineProperty()

2.2 訪問器屬性

訪問器屬性不包含數據值;它包含一對 getter 和 setter 函數(這兩個函數都不是必須的)。在讀取訪問器屬性時,會調用 getter 函數,這個函數負責返回有效的值;在寫入訪問器屬性時,會調用 setter 函數,這個函數負責處理數據。訪問器屬性也有四個特性:

  1. [[configurable]]:表示能否通過 delete 刪除屬性從而新定義屬性;能否修改屬性的特性;能否把屬性修改為數據屬性。
  2. [[enumerable]]:表示能否通過 for-in 循環(huán)返回屬性。
  3. [[set]]:寫入屬性是調用的函數。默認值是 undefined。
  4. [[get]]:讀取屬性是調用的函數。默認值是 undefined。

訪問器屬性不能直接定義,必須使用 Object.defineProperty() 方法
在調用 Object.defineProperty() 創(chuàng)建一個新引用類型屬性時,如果不指定,configurable、enumerable默認都為false

var person = {
  name: 'Alex',
  __age: 20
}
Object.defineProperty(person, 'age', {
  get: function() {
    return this.__age;
  },
  set:function(newValue) {
      if (newValue >= 70) {
        this.__age = newValue;
        this.name = 'old Alex';
      }
  }
});
person.age // 20
person.age = 71
person.age // 71
person.name // old Alex

上面的代碼創(chuàng)建了一個對象,并定義了兩個默認屬性 name 和 __age,__age 前面加下劃線是一種常用記號,表明只能通過對象方法訪問的屬性,而訪問器屬性 age 包含 getter 和 setter 函數,getter 函數返回 __age 的值,setter 函數計算當年齡大于 70 的時候把 name 修改為 old Alex,__age 修改為寫入的值。這個訪問器屬性的常見方式,即一個屬性的值會導致其他屬性發(fā)生變化。
不一定非要同時指定 getter 和 setter 函數。只指定 getter 意味著屬性不能寫,嘗試寫入屬性會被忽略,在嚴格模式下,嘗試寫入只指定了 getter 的屬性會拋出錯誤。只指定 setter 意味著屬性不能讀,嘗試讀取屬性會被忽略,只能讀到 undefined(以Chrome為例),
只指定 getter 函數:

屏幕快照 2020-03-14 17.22.23.png

只指定 setting 函數:
屏幕快照 2020-03-14 17.28.33.png

在不支持 Object.defineProperty() 方法的瀏覽器中不能修改 [[configurable]] 和 [[enumerable]]

3.定義多個屬性

由于為對象定義多個屬性的可能性很大,ECMAScript 5 又定義了一個 Object.defineProperties() 方法。這個方法可以通過描述符一次定義多個屬性。該方法接受兩個對象參數:第一個參數是要添加和修改其屬性的對象,第二個參數中的對象屬性要和第一個參數中要添加和修改的屬性一一對應。例如:

var person = {}
Object.defineProperties(person, {
  name: {
    configurable: true,
    enumerable: true,
    writable: true,
    value: 'Alex'
  },
  __age: {
    configurable: true,
    enumerable: true,
    writable: true,
    value: 20
  },
  age: {
    set: function(newValue) {
      if (newValue >= 70) {
        this.__age = newValue;
        this.name = 'old Alex';
      }
    },
    get: function() {
      return this.__age;
    }
  }
});

屬性特性的定義規(guī)則和 Object.defineProperty() 方法的規(guī)則一致

4.獲取屬性特性

ECMAScript 5 的 Object.getOwnPropertyDescriptor() 方法可以獲取給定屬性的描述符。這個方法接受兩個參數:屬性所在的對象和要讀取其描述符的屬性名稱。返回值是一個對象,如果是數據屬性,這個對象的屬性有 configurable, enumerable, writable, value; 如果是訪問器屬性,這個對象的屬性有 configurable, enumerable, set, get。例如:


屏幕快照 2020-03-14 17.54.37.png

5. 創(chuàng)建對象

使用 new Object() 和 對象字面量方式都可以用來創(chuàng)建單個對象,但是如果要創(chuàng)建大量的相似的對象那么勢必會產生大量的重復代碼,為了解決這個問題,開始使用別的方式創(chuàng)建對象。

5.1 工廠模式

工廠模式抽象了創(chuàng)建對象的具體過程,在 ES6 之前 ECMAScript 不能創(chuàng)建類(其實 ES6 中的類本質上還是構造函數),開發(fā)人員發(fā)明了一種函數,以函數來封裝以特定接口創(chuàng)建對象的細節(jié)。

function createPerson(name, age, job) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function() {
    alert(this.name)
  }
  return o;
}

var person1 = createPerson('Alex', 20, 'front-end-Engieer');
var person2 = createPerson('John', 40, 'Doctor');

函數 createPerson 可以根據接受的參數構建一個包含所有必要信息的 Person 的對象。可以無數次的調用這個函數,每次它都會返回一個包含三個屬性和一個方法的對象。工廠模式雖然解決了創(chuàng)建大量相似對象的問題,但是卻沒有解決對象識別問題(即怎樣知道一個對象的類型),隨著 JavaScript 發(fā)展,又一個模式出現(xiàn)了。

5.2 構造函數模式

ECMAScript 中的構造函數可以用來創(chuàng)建特定類型的對象。像 Object 和 Array 這樣的原生構造函數,在運行時會自動出現(xiàn)在運行環(huán)境中。此外,也可以創(chuàng)建自定義構造函數,從而定義自定義對象的屬性和方法。

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {
    alert(this.name)
  }
}
var person1 = new Person('Alex', 20, 'front-end-Engieer');
var person2 = new Person('John', 40, 'Doctor');

在這個例子。Person() 構造函數取代了 createPerson()函數,Person() 構造函數大體與 createPerson() 類似,但也有不同

1.沒有顯示的創(chuàng)建對象
2.直接將屬性和方法賦給了 this 對象
3.沒有 return 語句

此外,構造函數的首字母應該大寫。按照慣例,構造函數應該始終以大寫字母開頭,而非構造函數以小寫字母開頭。這主要是為了區(qū)別構造函數和非構造函數,因為構造函數本身也是函數,只不過用來創(chuàng)建對象而已。
要創(chuàng)建 Person 的實例,必須使用 new 操作符。以這種方式調用構造函數實際會經歷一下四步:

1.創(chuàng)建一個新對象。
2.將構造函數的作用域賦值給新對象(因此 this 就指向了新對象)。
3.執(zhí)行構造函數(給新對象添加屬性)。
4.返回這個新對象。

在上面的例子中,person1 和 person2 分別保存著一個 Person 的不同實例,這兩個對象都有一個 constructor (構造函數)屬性,該屬性指向 Person:

person1.constructor === Person // true
person2.constructor === Person // true

對象的 constructor 屬性最初是用來表示對象類型的。但是,檢測對象類型還是 instanceof 操作符更可靠一些。上面的例子中創(chuàng)建的所有對象既是 Object 的實例,也是 Person 的實例:

person1 instanceof Object // true
person1 instanceof Person // true
person2 instanceof Object // true
person2 instanceof Person // true

創(chuàng)建自定義類型構造函數意味著將來可以將它的實例標識為為一種特定的類型,這正式構造函數模式優(yōu)于工廠模式的地方。
構造函數也是函數,他與普通函數的區(qū)別就是調用方式,所以,任何函數通過 new 操作符調用,那它就是構造函數,不通過 new 操作符調用,那它就是普通函數

// 當作構造函數使用
var person = new Person('Alex', 20, 'Engineer');
person.sayName(); // Alex
// 當作普工函數調用
Person('Alex', 20, 'Engineer');
// 在瀏覽器中頂級執(zhí)行環(huán)境是 window
window.sayName(); // Alex
// 在另一個對象作用域中調用
var o = new Object();
Person.call(o, 'Alex', 20, 'Engineer');
o.sayName(); // Alex

5.2.1 構造函數的問題

構造函數雖然好用,但也有問題,那就是每個方法都要在實例上重新創(chuàng)建一遍,在上面的例子中,person1 和 person2 都有一個 sayName 方法,但是這兩個方法不是同一個實例;在 ECMAScript 中函數也是對象,因此每定義一個函數,也就是實例化了一個對象;在使用 new 操作符調用構造函數執(zhí)行構造函數給新對象添加屬性的時候就會實例化一個函數。

person1.sayName === person2.sayName // false

但是,創(chuàng)建兩個完成相同任務的 Function 實例實在是沒有必要,完全可以通過將函數定義放到構造函數外來解決這個問題:

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = sayName;
}
function sayName() {
  alert(this.name);
}
var person1 = new Person('Alex', 20, 'front-end-Engieer');
var person2 = new Person('John', 40, 'Doctor');

這樣兩個實例就共享了一個 sayName 的 Function 實例,但是這樣又回造成全局作用域污染,沒有任何封裝可言。這個問題可以通過原型模式解決。

5.3 原型模式

我們創(chuàng)建的每個函數都有一個 prototype (原型)屬性,這個屬性是一個指針,指向一個對象,這個對象的用途是包含可以由特定類型的所有實例所共享的屬性和方法。
在字面上理解,prototype 就是通過調用構造函數而創(chuàng)建的那個實例的原型對象。
使用原型對象好處就是可以讓所有實例對象共享它所包含的熟悉過和方法。

function Person() {}
Person.prototype.name = 'Alex';
Person.prototype.age = 20;
Person.prototype.job = 'Engieer';
Person.prototype.sayName = function () {
  alert(this.name);
}

var person1 = new Person();
person1.sayName(); // Alex

var person2 = new Person();
person2.sayName(); // Alex

person1.sayName === person2.sayName // true

理解原型對象
當創(chuàng)建了一個新函數,就會根據一組特定的規(guī)則為該函數創(chuàng)建一個 prototype 屬性,這個屬性指向函數的原型對象,默認情況下,所有的原型對象都會自動獲得一個 constructor (構造函數)屬性,這個屬性是一個指向 prototype 所在函數的指針。就那前面的例子來說,Person.prototype.conscructor === Person,通過這個構造函數,還可以繼續(xù)為原型對象添加屬性和方法。
創(chuàng)建了自定義構造函數之后,其原型對象默認只會獲得 constructor 屬性;其他的方法都是從 Object 繼承來的。當調用構造函數創(chuàng)建一個實例之后。該實例內部將包含一個指針(內部屬性),指向構造函數的原型對象,ECMA-262 第五版 管這個指針叫 [[prototype]],在腳本中沒有標準的方法訪問該屬性,但部分瀏覽器在每個對象上都支持一個屬性 __ proto__;其他實現(xiàn)中這個屬性對腳本完全不可見。但是,這里要明確一點這個鏈接存在與實例與構造函數的原型對象之間,而不是實例與構造函數之間

在實例中定義與原型對象中同名的屬性或方法,那么定義在實例中的屬性和方法會屏蔽原型對象中的同名屬性或方法,即使設置為 null 也同樣會屏蔽,想要能訪問原型對象重的同名屬性或方法,可以使用 delete 操作符刪除實例中的同名屬性和方法,這樣就可以繼續(xù)訪問原型對象中的屬性和方法。
但是原型模式也有自己的問題

function Person() {}

Person.prototype.name = 'Alex';
Person.prototype.age = 20;
Person.prototype.job = 'Engieer';
Person.prototype.friends = ['John', 'Court'];
Person.prototype.sayName = function() {
  alert(this.name)
}

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

person1.friends.push('XXX');
console.log(person2.friends); // ["John", "Court", "XXX"]

上面代碼兩個實例共享了原型對象上的 friends 屬性,但其實我們一般情況下都是希望每個實例能完全擁有自己的屬性,這個時候就可以使用組合使用構造函數模式原型模式。

5.4 組合使用構造函數模式和原型模式

構造函數模式有實例化多個實例會實例化多余的方法的問題,而原型模式有引用類型的原型屬性會被所有實例共享的問題,這時候就要用到祝賀構造函數模式和原型模式。

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ['John', 'Court']
}

Person.prototype.sayName = function() {
  alert(this.name);
}

var person1 = new Person('Alex', 20, 'Engieer');
var person2 = new Person('Scorrt', 30, 'Doctor');
person1.friends.push('XXX');
console.log(person1.friends); // ["John", "Court", "XXX"]
console.log(person2.friends); // ["John", "Court"]
person1.sayName(); // Alex
person2.sayName(); // Scorrt

這樣的話每個實例都完全擁有自己的屬性,同時又公用原型對象上的方法。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容