JavaScript 的 原型鏈

首先要明確的是,JavaScript 是面向對象的語言,但與 Java 、 C# 等語言有別,沒有類的概念,而是基於原型鏈 (即使是ES6的"class"也是基於原型鏈的一種語法糖)。

 

理解原型對象

只要創建一個新函數,系統默認爲其創建一個 prototype 屬性,指向函數的原型對象。默認情況下,所有原型對象都自動獲得一個 constructor 屬性,指向prototype 屬性所在函數得指針。舉個簡單例子:

function Person(){
  this.prop = 'myProp'
}
Person.prototype.name = 'zhangsan';
Person.prototype.sayName = function(){
  return this.name
}
var person1 = new Person();    // {prop:myProp}
var person2 = new Person();    // {prop:myProp}
console.log(person1.sayName);      // 'zhangsan'
person1.sayName = 'lisi';          // 修改原型對象上屬性
console.log(person1.sayName);      // 'lisi'  證明變量是共享的
console.log(Person.prototype.constructor === Person);    // true
console.log(person1.__proto__ === Person.prototype);     // true

上述例子簡單證明了原型鏈內的一些規則:

  • 使用 "new" 運算符時,實際有幾步操作:
    • 創建一個空對象;
    • 爲對象添加 "prop" 屬性;
    • 爲對象的 " [[prototype]] " 屬性指向 "Person.prototype"
  • "Person.prototype"爲實例對象"person1"和"person2"提供了屬性和方法,且實例對象能對原型上的屬性方法做修改;
  • "Person.prototype"的"constructor"屬性指向原型函數;
  • 實例對象的"[[prototype]]"屬性(即__proto__)指向"Person.prototype"。用一個圖表示:

 

更簡單的原型語法

上述例子中,如果要對"Person.prototype"添加多個屬性方法,大可不必一個個列出,因爲"prototype"屬性指向的就是一個對象,可用如下語法寫:

function Person(){}
Person.prototype = {
  name: 'zhangsan',
  age: 24,
  job: 'dazha',
  // ...
}
// 直接賦值方式會導致 prototype 中默認提供的 constructor 屬性消失,可顯示指定
Person.prototype.constructor = Person

但這種方式設定的"constructor"值的[[Enumrable]]爲true,建議使用Object.defineProperty()。

 

判斷/獲取對象的原型對象

繼續以上面例子,有兩種方法可以判斷對象的原型:

  • Object.getPrototypeOf() 靜態方法
var Person_prototype = Object.getPrototypeOf(person1);
console.log(Person.prototype === Person_prototype);        // true
  • instanceof 運算符
console.log(person1 instanceof Person);    // true

 

原型鏈

關於原型鏈的定義不多說,大概就是子對象能從父對象讀取到屬性和方法。先看一個簡單例子:

function Parent(){
  this.age = 50;
}
Parent.prototype.name = 'Parent';
Parent.prototype.getAge = function(){
  return this.age
}
function Son(){
  this.age = 24;
}
// 創建匿名對象用於繼承
Son.prototype = new Parent()
let son = new Son();
console.log(parent.getAge());        // 24

上述例子中,"son"從"parent"獲取"getAge"方法,用於返回自身"age"屬性的值,期間大概經歷瞭如下幾步:

  • 從自身尋找 "getAge" 方法,未找到則從 "__proto__" 往上找, "__proto__" 指向匿名對象 "new Parent()" ;
  • 從匿名對象中找 "getAge" 方法,未找到則從 "__proto__" 往上找, "__proto__" 指向 "Parent.prototype" ;
  • 從 "Parent.prototype" 找 "getAge" 方法,找到並執行,函數作用域是 "son" 對象,所以返回 "24" 。

上述例子的關係圖如下:

如果是"更長"的原型鏈,是同樣的原理,直到最後指向"null"後停止。

 

原型鏈的簡單創造

1. 使用 "Object.create(<proto>, <propertiesObject>?)" 方法可簡化上例子,傳入參數有兩個:(具體用法查看MDN文檔)

  • proto:新創建對象的原型對象。
  • propertiesObject:可選。如果沒有指定爲 undefined,則是要添加到新創建對象的可枚舉屬性(即其自身定義的屬性,而不是其原型鏈上的枚舉屬性)對象的屬性描述符以及相應的屬性名稱。
let parent = {
  age: 50,
  getAge() {
    return this.age
  }
}
let son = Object.create(parent, {
  // 屬性構造
  age: {
    value: 24
  }
});
console.log(son.age);    // 24

2. 使用 "Object.setPrototypeOf(obj, prototype)" 方法:

  • obj:作爲"子"的對象
  • prototype:作爲"父"的對象
let obj1 = { a: 1 }
let obj2 = { b: 2 }
Object.setPrototypeOf(obj1, obj2);    // {a:1}
console.log(obj1.b);                  // 2

 

ES6 的 "class" 關鍵字

ES6的"class"關鍵字本質是ES5原型鏈的語法糖,更詳細介紹請看 阮一峯的ES6

class Parent {
  // 父類構造函數
  constructor(firstname, lastname) {
    this.firstname = firstname;
    this.lastname = lastname
  }
  say() {
    return 'my name is '+this.lastname + this.firstname
  }
  // 靜態方法
  static toString() {
    console.log('Parent static method')
  }
}
// 子類,使用"extents"關鍵字繼承
class Son extends Parent{
  constructor(firstname, lastname, age){
    // 調用"super"方法前無法使用"this"
    super(firstname, lastname)
    this._age = age;
  }
  say(){
    // "super"可獲取父類的方法
    let result = super.say()
    return result + '. my age is ' + this._age
  }
  // getter方法,setter同理
  get age(){
    return this._age
  }
  // 同名靜態方法,當此方法未定義時,會調用父類的靜態方法
  static toString() {
    console.log('Son static method')
  }
}
let parent = new Parent('san', 'zhang');    // "new"創建實例對象
console.log(parent.say());    // 'my name is zhangsan'
Parent.toString();            // 'Parent static method'
let son = new Son('ergou','zhang',16);      // 子類的實例
console.log(son.say());       // 'my name is zhangergou. my age is 16'
Son.toString();               // 'Son static method'
console.log(son.age);         // 16

"class" 有如下特徵:

  • "class" 必須使用 "new" 運算符創建實例,直接調用(如 "Parent()" )會報錯;
  • "constructor" 構造函數,在創建實例對象時提供屬性/方法;
  • 構造函數外部(如 "sayName" )是實例對象的原型方法,即所有繼承自"Parent"的實例對象都可以訪問的方法;
  • 可以創建 "getter" 和 "setter"
  • 和函數一樣,具有 "name" 屬性,可以獲取類名
  • 和函數一樣,可以使用表達式形式定義,如 "const MyClass = class Me {...}" ;
  • 內部可以定義 Generator 方法
  • "static" 定義靜態方法,可以被子類繼承,該方法不會被實例繼承,而是直接通過類來調用;
  • "extends" 實現繼承,子類可以獲得父類的所有屬性和方法;
  • 構造函數內必須調用 "super()" 方法,否則無法使用 "this" 關鍵字
  • 除了構造函數內,其他函數均可調用 "super" 屬性,指向父類
  • ......

 

原型鏈存在的問題

上例子可以看出,實例對象"person1" 和 "person2" 均能訪問原型對象上的屬性方法,但是實例對象卻能對其修改,導致所有實例對象均產生影響。解決辦法是,在實例對象上定義同名屬性,則會優先調用實例對象上的屬性。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章