理解JS中的原型(Prototypes)

該文章是直接翻譯國外一篇文章,關於JS原型。
都是基於原文處理的,其餘的都是直接進行翻譯可能有些生硬,因此爲了行文方便,就作了一些簡單的本地化處理。
若是想直接根據原文學習,能夠忽略此文。javascript

全新對象

在JS中,對象是有不少keyvalue構成的一種數據存儲結構。例如,若是想描述一我的的基本信息,能夠構建一個擁有firstNamelastName的對象,而且分別被賦值爲北宸。在JS對象中的key的值是String類型的。java

在JS中,能夠用Object.create建立一下全新的對象:編程

//構建了一個空對象
var person = Obeject.create(null);

複製代碼

此時有些開發會說,爲何不用var person ={} 來構建一個空對象。其實之因此能用這種處理方式,只是JS引擎給你作了處理。爲了可以用最原始的代碼去了解原型,咱們也須要按部就班的去接觸這些東西。bash

若是經過一個key遍歷對象,可是對象中沒有對應的key能進行匹配,JS就會返回一個 undefinedapp

person["name"] //undefined
複製代碼

若是key是一個合法的標識符,也能夠用以下的語法進行對象數據的訪問:ide

person.name //undefined
複製代碼

合法的標識符格式:函數

in general, an identifier starts with a unicode letter, $, _, followed by any of the starting characters or numbers. A valid identifier must also not be a reserved word. There are other allowed characters, such as unicode combining marks, unicode connecting punctuation, and unicode escape sequences.post

給對象賦值

如今你已經有了一個空對象,可是好像並無啥卵用。在咱們爲對象新增自定義屬性以前,咱們還須要對對象的額外屬性(named data property)有一個更深的瞭解。學習

通常而言,對象的自定義屬性有一個name屬性和與name屬性相對應的value屬性。可是自定義屬性還能夠被enumerableconfigurablewritable這些隱藏屬性控制,來在不一樣的場景中表現出不一樣的行爲。ui

若是一個自定義屬性是enumerable的,經過for(prop in obj)來操做該自定義屬性的宿主對象,這個自定義屬性會被循環操做捕獲。 若是是writable的,你能夠對這個自定義屬性進行賦值。若是是configurable的,能夠對這個自定義屬性進行刪除或者是改變其餘的隱藏屬性。

通常在咱們建立一個新的自定義屬性,咱們老是但願這個屬性是enumerablewritableconfigurable的。

咱們能夠利用Object.defineProperty來向對象新增一個自定義屬性。

var person = Object.create(null);
Object.defineProperty(person, 'firstName', {
  value: "Yehuda",
  writable: true,
  enumerable: true,
  configurable: true
});

Object.defineProperty(person, 'lastName', {
  value: "Katz",
  writable: true,
  enumerable: true,
  configurable: true
});
複製代碼

這樣新增屬性代碼有些冗餘,咱們能夠將配置信息提出來:

var config = {
  writable: true,
  enumerable: true,
  configurable: true
};

var defineProperty = function(obj, name, value) {
  config.value = value;
  Object.defineProperty(obj, name, config);
}

var person = Object.create(null);
defineProperty(person, 'firstName', "Yehuda");
defineProperty(person, 'lastName',   "Katz");

複製代碼

雖然代碼有一些精簡了,可是仍是看起來很彆扭。因此咱們須要一個更加合理或者是更加簡潔的方式來定義屬性。

Prototypes

從上面咱們得知,JS中的對象就是一系列key和對應value組成的數據格式。可是在JS中還存在一個屬性(一個指向其餘對象的指針)。咱們稱這個指針爲對象的原型(prototype)

若是你在某個對象中,查找一個key,可是在該對象範圍內沒有找到。JS會繼續在prototype所指向的對象中繼續尋找。以此類推,直到prototype所指向的對象是一個null,查詢結束。而且返回undefined

讓咱們來回顧一下,在調用Object.create(null)的時候,會發生些什麼。 方法中接收一個null爲參數。其實也就是說,在利用Object.create(paramsObj)構建出的對象,他的prototype指向了paramsObj

能夠經過Object.getPrototyOf來查詢指定對象的prototype.

var man = Object.create(null);
defineProperty(man, 'sex', "male");

var yehuda = Object.create(man);
defineProperty(yehuda, 'firstName', "Yehuda");
defineProperty(yehuda, 'lastName', "Katz");

yehuda.sex       // "male"
yehuda.firstName // "Yehuda"
yehuda.lastName  // "Katz"

Object.getPrototypeOf(yehuda) // returns the man object

複製代碼

咱們能夠經過這種方式來新增函數,這樣這個函數就會被多處使用:

var person = Object.create(null);
defineProperty(person, 'fullName', function() {
  return this.firstName + ' ' + this.lastName;
});

//將man的prototype指向person ,這樣,在man對象還有已man爲prototype的對象均可以訪問這個函數
var man = Object.create(person);
defineProperty(man, 'sex', "male");

var yehuda = Object.create(man);
defineProperty(yehuda, 'firstName', "Yehuda");
defineProperty(yehuda, 'lastName', "Katz");

yehuda.sex        // "male"
yehuda.fullName() // "Yehuda Katz"

複製代碼

設置屬性

因爲構建一個具備writableconfigurableenumerable屬性的對象很常見。因此JS爲了讓代碼看起來簡潔,採用另一種給對象新增屬性的方式。

經過簡化方式,讓代碼看起來很短,也不須要額外的處理writableconfigurableenumerable等屬性,同時這些屬性的值都是true

var person = Object.create(null);

//在此處咱們能夠直接定義想要給對象新增的屬性,而writable,
// configurable, 和 enumerable這些屬性由JS統一處理
person['fullName'] = function() {
  return this.firstName + ' ' + this.lastName;
};

//將man的prototype指向person ,這樣,在man對象還有已man爲prototype的對象均可以訪問這個函數
var man = Object.create(person);
man['sex'] = "male";

var yehuda = Object.create(man);
yehuda['firstName'] = "Yehuda";
yehuda['lastName'] = "Katz";

yehuda.sex        // "male"
yehuda.fullName() // "Yehuda Katz"

複製代碼

對象字面量

JS提供了一個種字面量語法來構建一個對象。同時能夠一次性將全部須要新增的屬性都指出並賦初值

var person  ={ firstName: "北宸", lastName: "範" }

複製代碼

其實上面的實現是以下代碼的"語法糖":

var person = Object.create(Object.prototype);
person.firstName = "北宸";
person.lastName  = "範";

複製代碼

person的原型鏈爲Object.prototype。這個能夠在控制檯中實踐一下

Object.prototype對象中包含了不少咱們但願在定義的對象中能夠經過原型鏈訪問的方法和屬性。經過上面的分析能夠得知,咱們經過字面量構建的對象,它的原型就是Object.prototype

固然,咱們也有機會在定義的對象中對存儲在Object.prototype中的方法進行重寫。

var alex = { firstName: "Alex", lastName: "Russell" };

alex.toString() // "[object Object]"

var brendan = {
  firstName: "北宸",
  lastName: "範",
  toString: function() { return "範北宸"; }
};

brendan.toString() // "範北宸"

複製代碼

可是基於字面量構建對象的原型是沒法進行指定的。也就是說,字面量構建的對象的原型永遠都是Object.prototype。這樣作的話,就沒法利用原型來分享共有屬性和方法。因此,咱們須要對這種模式進行改進。

var fromPrototype = function(prototype, object) {
//用於將自定義的原型和目標對象進行關聯,這樣在新的對象中就能夠訪問原型中的方法和屬性(原型搭建)
  var newObject = Object.create(prototype);
    //遍歷目標對象,將屬於目標對象中的屬性都複製到新對象中,(屬性遷移)
  for (var prop in object) {
    if (object.hasOwnProperty(prop)) {
      newObject[prop] = object[prop];      
    }
  }

  return newObject;
};

var person = {
  toString: function() {
    return this.firstName + ' ' + this.lastName;
  }
};

var man = fromPrototype(person, {
  sex: "male"
});

var jeremy = fromPrototype(man, {
  firstName: "北宸",
  lastName:  "範"
});

jeremy.sex        // "male"
jeremy.toString() // "範北宸"

複製代碼

如上的對象構建的對象結構以下:

基於原型的面向對象編程

有一點很明確,原型(prototype)能夠用於繼承(繼承是OOP語言最明顯的特色之一)。爲了利用原型來實現繼承,JS提供了new操做符。

爲了實現面向對象編程,JS容許你使用一個函數對象將prototypeconstructor封裝起來。

//Person的constructor
var Person = function(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}
//Person的prototype
Person.prototype = {
  toString: function() { return this.firstName + ' ' + this.lastName; }
}

複製代碼

自此,咱們就實現了一個用於做爲constructor函數對象還有做爲新對象的prototype的對象。

讓咱們經過構建一個函數來模擬new的操做流程。它的主要目的就是爲了新建指定的構造函數的實例.

function newObject(func) {
  // 構建函數的參數list
  var args = Array.prototype.slice.call(arguments, 1);

  // 基於構造函數的原型構建一個對象
  var object = Object.create(func.prototype);

  // 因爲構造函數中存在this的值,因此在構建實例的時候,須要將this的指向實例對象
  func.apply(object, args);

  // 返回基於指定構造函數構建的新對象
  return object;
}

var brendan = newObject(Person, "範", "北宸");
brendan.toString() // "範北宸"
複製代碼

Note:這裏func.apply(object, args)的操做有一個this指向問題。能夠參考理解JS函數調用和"this"

在JS實際構建對象中,用的是new

var mark = new Person("範", "北宸");
mark.toString() // "範北宸"
複製代碼

Note:關於new的運行機制,大體以下:

  1. 建立一個空對象,做爲將要返回的對象實例。
  2. 將這個空對象的原型,指向構造函數的prototype屬性。
  3. 將這個空對象賦值給函數內部的this關鍵字。
  4. 開始執行構造函數內部的代碼。

本質上,JS函數中的"class"其實就是一個函數對象做爲構造函數同時附帶上一個prototype對象。

相關文章
相關標籤/搜索