CoffeeScriptJavascript: Prototype and Object-oriented


作者:王納米

首發於《代碼指南》(daimazhinan.com

《代碼指南》編輯部保留全数權利


1 What is and what is not an object

由若干 key-value pair 組成的結構,在 Ruby 裏稱 Hash,在 Python 裏稱爲
Map,在 Javascript 中称之为 Object。

但 Ruby 中還有 Ruby 的 Object,Javascript 中再無 Hash。Javascript 唯有Object,它同時扮演了 Ruby 中的 Hash,Object,Class 甚至
Method。假如處於普遍意義上的 Object-oriented 語境中,則可說它既是
Class,也是 Instance,甚而也是 Method。

有人據此認爲 Javascript 中一切都是 object,這種說法简单產生誤解。

Javascript 中,每個變量都屬於一個類型,能够用 typeof a
來「算出」變量的類型。

<small>說「算出」,是因爲 typeof 是一個 operator。</small>

typeof
的運算結果會是以下幾種之一:’undefined’,’null’,’number’,’string’,’boolean’,’symbol’(ES
6 中新增),以及 ‘function’ 和 ‘object’。

這就如在提醒作者們,一個字符串,或是一個數字,並非是一個
object。但這就無法解釋爲什麼能够執行 '1'.toString() 這樣的語句,因爲
oneVar.oneMethod() 意味着調用在 oneVar 上名爲 oneMethod 的
method,而一個字符串只是一個單純的 value,並非是 key-value
的結構,也就不有所包涵或是調用 method 的力量。

這裏儘只怕簡單解釋一下。Null,Undefined,String,Number,Boolean,以及
Symbol,是 Javascript 中有个别 6 個 primitive
type,借使一個值的類型是這樣的 primitive type,它原本是沒有经常意義上的
method 的。

而此六種 primitive type 中,除了 Null 和 Undefined 之外,都有與之同名的
wrapper,例如 String 就是一個大局的
object,它也便是創建字符串變量時被調用的
constructor。這就解釋了爲什麼一個字符串作爲 primitive value,能够調用
toString() method,而一個值爲 undefined 或 null 的變量卻不得以。

<small>例如 '1'.constructor 的結果是 ‘function String() {
[native code] }’,有關 constructor
的詳細內容會在後面提到。</small>

<small>雖然 primitive 變量的值是 immutable
的(不可能被修改,只好賦予一個新的值),不过上述同名 wrapper
是足以被涂改的,由此得以通過
String.prototype.toString = function() {return 'compromised'}
來修改全数字符串的 toString method。</small>

接下來講到的所謂 object,平日是指 typeof 結果爲 ‘object’ 的非 primitive
變量。

值得注意的是,function 並非是一種 primitive
type。後面會提到,函數只是稍有尤其的 object。

<small>另一個細節是,NaN,Infinity 與 Null 和 Undefined
不一致,前兩項並非 primitive 類型,它們僅是類型爲 ‘number’
的變量的一種值。</small>

Javascript 中,object 與 string 等 non-primitive type
的另一個顯著區別是,假诺一個變量的值是一個
object,那麼它實際僅存儲了這個 object 的
reference(地址/引用),而即便變量的值是一個字符串或一個數字,則存儲了這個值小编。通俗的解釋是,假如兩個變量的值是同一個
object,那麼修改一個變量會同時修改另一個變量。這是個常見的問題,在此不再多做解釋。

2 First constructor

var student = { name: 'a common name' }

這行代碼定義了一個一般性的 object。它有一個名爲 name 的 property,property
attribute 是一個字符串變量。

可以用 student.name 或 student[‘name’] 來訪問或涂改 name
的值,也能够執行 student.toString()。這意味着存在兩個 method 作爲
getter 和 setter,也存在着一個名爲 toString 的
method。那麼,一個理所当然的問題即是:這些 method 在哪裏?

笔者們知道,上边一條語句相當於執行了:

var student = new Object()
student.name = 'a name'

這裏的 new 也是一個 operator。而 Object 是一個 constructor。就像在
C++ 或 Ruby 中,class 是構造 object 的模版;而在 Javascript
中,人們用一個叫做 prototype 的 object 作爲 constructor 來構建另一個
object。

<small>因而人們說 Javascript 是 prototype-based。而 C++ 則是
object-based。</small>

<small>這裏一個更主要的區別是,class 定義後無法修改,而 prototype
則相反。這種運行時能够改變本人結構的特点,叫做
meta-programming,平日說這種語言是 dynamic 的。</small>

更具體地講,一個 constructor 是一個 function(本質上也就是一個
object),它有一個叫作 prototype 的 property,這個 property 的 attribute
又是一個 object,它用來作爲生成新 object 的模版。

試着構造一個 constructor:

var Student = function() {}
Student.prototype.name = 'a common name'

用它來創建新 object

var bob = new Student()
bob.name // 'a common name'
bob.name = 'Bob'
bob.name // 'Bob'

當調用new Student() 時,大略發生了以下工作:

  1. 新建一個 object,不妨稱它爲 ref

  2. ref 是以 Student.prototype 爲模板構造的,它「複製」了
    Student.prototype 的具备 property 作爲自身的 inherited
    property,這一過程正是所謂的 inherit。

    <small>這裏的「複製」必要注意,以前提到過,若是是
    object(包罗 function)的話,那麼只複製了
    reference,這意味着假使對其進行修改的話,也會對其 constructor 中一律
    object 或 function 產生影響,反之亦然,借使改动了
    constructor.prototype 的 method,那麼 instance
    也會受到影響。</small>

  3. 在 ref 的 context 下調用此函數。

    <small>所謂 context,即 execution
    context,大略指運行時所處的變量環境,以及 this
    的值。在內部實現時,這裏必然有一個複雜的處理 context
    的過程,但在此能够簡單的驾驭成:首法郎 ref.constructor 的值爲
    Student() 函數,然後再行調用。而小编們瞭解,調用 object method
    時,函數中的 this 正是這一 object 本人。</small>

    <small>關於 execution context 的內容請參見 ES 5.1:
    10.3
    </small>

    <small>上述代碼中我們沒有傳遞參數到 Student()
    中,但倘使有的話,(例如 new Student(arg)),在調用 Student()
    時也會包罗相同的參數。</small>

  4. 爲了方便的維護 object 的繼承關係,Javascript 引擎會在 ref
    上維護一個名爲 [[prototype]] 的 internal property。

    <small>這裏會令人感觉到思疑,後面會詳細解釋。</small>

  5. 最後將 ref 作爲 new Student() 的重返值。

    <small>假使 Student() 函數內的再次回到值是一個 object
    的話,那麼這個 object 將代替 ref 成爲 new
    語句的重回值。可是日常不會這麼做。</small>

過程有些複雜,而且确实在內部實現起來的過程只會更複雜。但笔者們現在只需記住差不多過程,在那之中的重點是,新
object「繼承」了 constructor 的 prototype。

BTW,上边提到在 new 時 Student 函數會被執行,而執行時 this 是被新建的
object,那麼就有了另一種構造 constructor 的不二法门:

var Student = function(name) {
  this.name = name
}

請自行模擬在執行 var bob = new Student('Bob') 後會發生什麼。

3 Instanceof and Prototype chain

上述內容中,有一個便于令人感到迷惑之處:笔者們境遇了兩個 prototype。

先是個是 constructor 的 prototype(即
Student.prototype),它用來作爲模板構造 instance;

另一個是每個 object 都有的名爲 [[prototype]] 的 internal
property,它的值要麼是 null,要麼正是我繼承自的
prototype,也即構造自个儿所用的 constructor.prototype。

有時人們所說的「變量 bob 的 prototype」正是指這個
[[prototype]]。暫且把它記作
bob.[[prototype]],當然在代碼裏這樣是低效的。因爲作爲 internal
property,根据標準它對於用戶是不可見的。

<small>然则是在有的 implementation
上(比如幾乎全部瀏覽器),提供了 __proto__ 來訪問和操作
[[prototype]],但为数不少人都不推薦使用它。</small>

雖然無法直接訪問 bob.[[prototype]],不过能够使用
Object.getPrototypeOf(bob) 來查看 bob 繼承自何物。

<small>ES 6 中新增了
Object.setPrototypeOf,雖然依舊是很几个人所不推薦使用的。</small>

總結一下,其實你只必要記住一句話:「constructor 的 prototype 是 instance
的 prototype」。

<small>即 Student.prototype === Object.getPrototypeOf(bob) ===
true</small>

<small>當然,constructor 也可能有协调的 constructor 和融洽的
[[prototype]]。</small>

<small>對笔者來講,之所以這裏曾經令俺費解,主固然因爲小编不领悟prototype 這個詞的本義。笔者用過 C++,Java,Ruby,小编瞭解什麼是 OOP 中的
constructor,instance,和 inherite,但在決定深切瞭解 Javascript
以前,笔者一心沒有 prototype 這個概念。</small>

解決了令人猜疑的概念,作者們回到以前的地点。

現在小编們获得了一個名爲 bob 的 object。

這時執行 bob instanceof Student 能够获取結果是 true

instanceof 又是一個 operator。顧名思義,bob instanceof Student
的意思正是 is bob an instance of Student?

起先的比方,constructor 與 instance 之間是一種「互逆」的關係。bob 的
constructor 是 Student,bob 也正是基於 Student 構造出的 instance。

但這比喻其實並不恰當,因爲 bob 只可以有一個 constructor,但是 Student
只怕構造出無數個 instance。更何況,還要考慮 constructor 的 constructor
的問題。也正是,Z 繼承自 X,X 繼承自 Y,……,B 繼承自 A。這時 Z 既
instanceof A,也 instanceof Y

顯然,這種繼承的關係是一個鏈(chain)式結構,小编們在此以前瞭解到,這種關係是通過
[[prototype]] 來維護的。事實上,這種關係就叫做 prototype chain。

接下來試着實現一個簡單的兩重繼承關係:Person -> Student -> bob

率先先實現兩個 constructor

var Person = function() {
  this.name = 'a common name'
}

var Student = function(school) {
  this.school = school
}

現在這兩個 constructor 還沒有聯繫起來,小编期望 Student 能繼承自
Person,於是:

Student.prototype = new Person()

這是单排關鍵而神奇的代碼。首先应用 Person constructor 新建了一個 person
instance。而 Student 作爲一個 constructor,它的 prototype,也便是用來
construct 别的 instance 的模板,現在正是這個 person instance。

這意味着,用 Student 構造新的 student instance 時,首先會繼承全数的
Person 的屬性,而在執行 Student() 函數的時候,又給本身添加了 school
屬性。

var bob = new Student('NEYC')
bob.name   // 'a common name'
bob.school // 'NEYC'

接下來驗證繼承鏈最根本的性質:即只要自己修改了 Person constructor,那麼
Student constructor 也會受到影響,反之則不然:

Person.prototype.showName = function() {
  console.log(this.name)
}

Student.prototype.showSchool = function() {
  console.log(this.school)
}

bob.showName() // 'a common name'

alice = new Person()
alice.showName()   // 'a common name'
alice.showSchool() // TypeError: undefined is not a function

Prototype chain 由 Javascript 引擎負責維護。當訪問 bob 上的 method 或者property 時,會先在 bob 自个儿查找,假如沒有的話,會沿着 [[prototype]]
上溯。

<small>這提示小编們:能够在鏈條的下端進行 override。</small>

接下來驗證一下 instanceof 的結果:

bob instanceof Person  // true
bob instanceof Student // true

這說明,在進行 object instanceof constructor 運算時,同樣會沿着 object
的 prototype chain 上溯,依次檢查鏈中之物是还是不是是 constructor.prototype。

提問:此時 Object.getPrototypeOf(bob)(即
bob.[[prototype]])的值爲何?

4 Truely Object-oriented

前方模擬了簡單的繼承關係。但顯然距離 object-oriented 還有相當差别。

接下來嘗試模擬一個更周全的,勉強能够被稱爲 object-oriented 的 subclass
-> superclass 結構。

先不忙寫代碼,讓笔者們先來確認一下具體有怎么样须求:

<small>以下是 OOP 語境中的概念,毋與 Javascript 中的概念混淆(如
constructor)。</small>

  1. 能夠定義 class 作爲模板來生成 instance,当中涵盖 property 與
    method。
  2. class 中應有 constructor,在建立 instance 時執行。
  3. Inheritance,subclass 能夠繼承 superclass,並且不會影響到上游。
  4. super 關鍵字以訪問 superclass 的內容。
  5. Namespace,在区别的 namespace 中定義的 class 不會相互污染。

以下 feature 實現起來過於繁雜,先行忽略:

  1. Information hiding,能夠控制一個 method 或 property
    的訪問權限,例如將其設爲 private 的。
  2. Polymorphism,根據分裂的參數數目與類型,能够定義多個同名的
    constructor,property,或 method。
  3. Abstract class,或稱 virtual class,即無法被 instantiate 的 class。

先把上一節中的代碼做些改進:

function Person() {}

Person.prototype.setName = function(name) {
  this.name = name
  console.log('my name is now ' + name)
}

function Student() {
  Person.call(this) // KEYPOINT 1
}

Student.prototype = Object.create(Person.prototype) // KEYPOINT 2
Student.prototype.constructor = Student // KEYPOINT 3

var bob = new Student()

bob.setName('Bob')

與上一節中的代碼相比较,有三處区别。

4.1 keypoint 1

在 subclass(Student)的 constructor 內部,執行了 Person.call(this)

事先提到,全局對象 String,是持有字符串變量的 wrapper;同樣,全局對象
Function,是独具 function 的 constructor,在 Function.prototype
中定義了多少怀有函數都具備的 property 與 method,比如 call

顧名思義,call
用來調用别的函數。但更珍视的是,用戶能够钦命這一函數運行時所處的
context。簡單的精通,你能够钦命是由哪個 object
調用的這一函數;最直觀的功效是,你能够钦命運行時的 this。方法 call
所接受的率先個參數,正是钦点的 this

<small>call 的具體語法請參見 MDN:
Function.call
</small>

依前所述,此處的 Student() 是通過 var bob = new Student()
來被調用的。那麼此時的 this 就是剛剛新建的對象 ref,即隨後的
bob。於是,Person.call(this),就是以 refthis 來調用
Person()。

這裏有一個問題。在前一節中,作者們並沒有顯示的 call
Person,然则仿佛也沒有境遇什麼問題,那麼此舉意義何在?這要和下一點位居一块儿來說,請先暫且讀下去。

4.2 keypoint 1 & 2

上一節中 Student.prototype = new Person(),等號右側現在變成了
Object.create(Person.prototype),這種改變的原故是什麼。

<small>Object.create 的具體內容後面還會講到。現在只需瞭解
Object.create 接受一個 prototype 作爲參數,並據此生成一個新的
object。</small>

<small>笔者深信不疑你現在得以精通小编說「一個 prototype」中 prototype
的含義。</small>

先是,到当下地点笔者們的 constructor Person
還是不接受參數的,而只要接受參數的話,在執行
Student.prototype = new Person()
時,小编們還不领会應該傳遞什麼參數進去。

其次,new Person() 會使得 Person()
被執行一遍,而在头里笔者們已經進行了3遍 Person.call(this)。這樣的話
Person() 會被執行兩次。而且從邏輯上講,也應該是在調用 subclass
建立對象時再新建 superclass 比較合理。

从而,這裏的關鍵是,Object.create 僅僅遵照 prototype 新建了
object,而不會執行 constructor 函數本人。

梳理一下方可發現,keypoint 1 和 2 其實是平等處改進。

依据从前對 new 具體執行過程的追究,能够瞭解到,在上一節中的
new Person() 隱含了兩個步驟:

  1. 确立一個新 object
  2. 以此新 object 的名義調用 Person()

而笔者們現在把兩者拆開分別執行:

  1. Object.create(Person.prototype)
  2. Person.call(this)

<small>請思考將兩者分開越来越多的優勢。</small>

<small>能够參見這個問題 StackOverflow:
using-object-create-instead-of-new
。</small>

<small>這個問題中答案的內容和本文近期的過程並不联合,建議讀者先繼續閱讀本文,最後一同進行發散閱讀。</small>

4.3 keypoint 3

觀察 Student.prototype.constructor = Student 一句。

小编們在此之前見到過 constructor 這一 property,但從未操作過它的值。

回憶一下 .constructor 的相關內容:

var Person = function() { console.log('Person constructor') }
Person.constructor  // function Function() { [native code] }

var bob = new Person()
bob.constructor // 即 Person 本身。

var Student = function() { console.log('Student constructor') }
Student.prototype = Object.create(Person.prototype)

bob = new Student()
bob.constructor // 依然是 Person 本身。

能够见到,假如不手動維護 .constructor 的話,那麼唯有最頂層的
superclass 擁有內建的 .constructor,而其餘 subclass 的 .constructor
均爲沿 prototype chain 繼承而得。由此,作者們需求手動 override 這一值。

4.4 Uncomplete inheritance

时下的繼承並不完全。

這是指,subclass 只繼承了 superclass 的 prototype 中的 method 或
property,而這並不包罗 superclass 自己的 method 或 property。也正是
Person.prototype.fooPerson.bar 間的區別,我們的 subclass Student
通過 Person.prototype 繼承,自然不包含 bar

那麼正是手動爲 subclass 添加一個同名的 bar

Student.bar = Person.bar

能够行使 for 循環遍歷 object 的具备 property 與 method

for (var key in Person){
  Student[key] = Person[key]
}

Well… NO!

因爲這時 for in 循環的結果中也蕴藏了 Person.prototype 中定義的
property,還有繼承而來的 .constructor 等等,笔者們只想遍歷真正在 Person
「名下」的 property,此時亟待hasOwnProperty 這一 method。

for ( var key in Person ) {
  if ( Person.hasOwnProperty( key ) ) {
    Student[key] = Person[key]  
  }
}

<small>hasOwnProperty() 這一 method 是定義在 Object.prototype
中的。功用即字面含義。</small>

<small>讀到這裏沒發現什麼不自然之處麼?爲什麼在 for
循環中,hasOwnProperty 本人沒有被遍歷到?還有熟知的 toString()
呢?這個後面再說。</small>

4.5 Super

在 Java 和 Ruby 等諸多語言中,關鍵字 super 用來表示當前 class 的
superclass,能够通過它來訪問 superclass 的 property 與 method。

在解決了事先繼承不完全的問題之後,要實現類似 super 的功力就很簡單了。

Student.superClass = Student.prototype

<small>super 一詞是 Javascript 中的 reversed word,在 ES 5.1中它沒有實際成效,在 ES 6 中被用於錯誤處理方面。由此在代碼中不妨以
superClass 代替。</small>

4.6 Namespace

前边的東西都相對複雜,上面我們來實現一個相對簡單的 feature:Namespace。

一句话来说:不要忘記 constructor 本質上還是一個 function,將其作爲一個
object 名下的 method,這一 object 自然就滿足 namespace
的须要。不做具體實現。

4.7 More

關於在 JS 中實現 OOP
的內容,還能够參見以下篇章(閱讀難度依次回涨,最後一例則是因爲沒有註釋,但其實相當優美而易讀):

  1. MDN: Introduction to Object-Oriented
    JavaScript
  2. phrogz.net:
    OOPinJS
  3. ejohn.org: Simple Javascript
    Inheritance
  4. CoffeeScript: Class, Inherite and
    Super

5 A brand new object

事先反复利用了 Object,在結束从前簡單地說美素佳儿下。

var anEmptyObject = {}
anEmptyObject.toString() // '[Object Object]'

可見,一個「空」的 object 並非真的一無全体,它已經包括若干
method。一個自然的想法是,它亦是繼承自她物。

那麼來看它繼承自何物:

anEmptyObject.constructor            // function Object() { [native code] }
Object.getPrototypeOf(anEmptyObject) // Object {}

Object 是 Javascript 中的一個內建全局對象,就像 String
是字符串類型變量的 wrapper 一樣,Object 是 object 類型變量的
wrapper。但 Object 還有个别别的的用途。

事先涉嫌過 Object.create() 用來根據一個 prototype 新建 object。執行
var a = {}var a = new Object() 時,相當於執行了
var a = Object.create(Object.prototype)。其中 Object.prototype
就带有了 hasOwnPrototype()toString()
method。但這時,若是傳入一個 null 作爲
prototype,那麼小编們就收获了一個真正的空 object。

<small>Object.prototype 包罗的具體內容請參見:ES 5.1:
sec-15.2.3.1
,及
MDN:
Object.prototype
</small>

作者們從這個真正的空 object 來開始實驗:

var anTruelyEmptyObject = Object.create(null)

現在作者們來用更底層的形式來爲這個 object 添加一個 property

Object.defineProperty(anTruelyEmptyObject, 'name', {
  value: 'Sailor Moon',
  writable: true,
  enumerable: true,
  configurable: true
});

Object.defineProperty() method 作用同名,此處在 anTruelyEmptyObject
上定義了一個名爲 name 的 property。重點是第②個參數,其中 value
好驾驭,其餘三個則會令後學者疑惑。

事實上,對於一個 object 中的一個 property 來講,首先它有投机的
propertyName,其次還有 4 個 property
attribute:valuewritableenumerable,及
configurablewritable 決定其值是不是可被改动;configurable 決定其
property attribute 是还是不是可被更改,以及 property 自身是或不是可被刪除;重點是
enumerable,它決定該 property 是或不是在
for (key in anTruelyEmptyObject) 時出現,這正是事先境遇的問題的答案。

假诺老是爲 object 添加 property 都要這麼做的話就太麻煩了,於是
Javascript 提供了簡化的点子。anTruelyEmptyObject.name = 'Sailer Moon'
一句,其背後即隱示地調用了 Object.defineProperty 方法,当中三個
boolean 值均爲默認值 true。

anTruelyEmptyObject.name = {name: 'Sailer Moon'}
是特别簡化的本子。可是要留心的是,使用 {} 來新建的 object,其
[[prototype]] 會被賦爲
Object.prototype。有時這並非是件善事,比如事先在新建 constructor 時。

於是作者們能够团结實現一個 helper,以簡單地形成「由 prototype 建立
object,並設置 property」這一過程:

var createObjectFromPrototype = function(prototype, object) {
  var newObject = Object.create(prototype)

  for (var key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = object[key]
    }
  }

  return newObject
}

var Person = {
  toString: function() {
    console.log('overrided toString method')
    return(this.name)
  }
};

var bob = createObjectFromPrototype(Person, {
  name: 'Bob'
})

bob.name // 'Bob'

這樣小编們就簡單地通過 constructor 而 override 了 toString() 方法。

Fin.