理解 JavaScript 中的原型

4Ark發表於2019-01-22

前言

JavaScript 中的原型一直是我很懼怕的一個主題,理由很簡單,因為真的不好理解,但它確實是 JavaScript 中很重要的一部分,而且是面試的必考題,就算現在不懂,以後遲早有一天要把它弄懂,不然的話永遠都沒辦法把自己的技術能力往上提高一個層次,所以今天就來講講 JavaScript 中的原型。

本文是這系列的第四篇,往期文章:

  1. 理解 JavaScript 中的作用域
  2. 理解 JavaScript 中的閉包
  3. 理解 JavaScript 中的 this

什麼是原型

首先要說一下為什麼會有原型這個東西,那是因為在 JavaScript 中並沒有 “類” 的概念,它是靠原型和原型鏈實現物件屬性的繼承,即便在 ES6 中新出了class的語法,但那也只是一個語法糖,它的底層依然是原型。

要理解原型(原型鏈),最重要的是理解兩個屬性以及它們之間的關係:

  • __proto__
  • prototype

__proto__

JavaScript中,萬物皆物件,所有的物件都有__proto__屬性(nullundefined除外),而且指向創造這個物件的函式物件的prototype屬性。

var obj = {};
console.log( obj.__proto__ === Object.prototype ); // true
var arr = [];
console.log( arr.__proto__ === Array.prototype ); // true
var fn = function(){};
console.log( fn.__proto__ === Function.prototype ); // true
var str = "";
console.log( str.__proto__ === String.prototype ); // true
var num = 1;
console.log( num.__proto__ === Number.prototype ); // true
複製程式碼

前面說了,在 JavaScript 中,一切皆物件(可以理解為它們都是從物件那裡繼承過來的),所以:

console.log( Function.prototype.__proto__ === Object.prototype ); // true
console.log( Array.prototype.__proto__ === Object.prototype ); // true
console.log( String.prototype.__proto__ === Object.prototype ); // true
複製程式碼

而因為Object.prototype__proto__已經是終點了,所以它的指向是:

console.log( Object.prototype.__proto__ === null ); // true
複製程式碼

注意,雖然大多數瀏覽器都支援通過__proto__來訪問,但它並不是ECMAScript的標準,在 ES5 中可以通過Object.getPrototypeOf()來獲取這個屬性。

var obj = {};
console.log( Object.getPrototypeOf(obj) === Object.prototype ); // true
複製程式碼

prototype

prototype是每個函式物件都具有的屬性(它也有__proto__,因為函式也是物件),例項化建立出來的物件會共享此prototype裡的屬性和方法(通過__proto__)。

在上面的例子中已經看到過prototype的身影,下面通過一個例子來講述它的作用。

現在我們有一個建構函式Person,並且對它進行例項化:

function Person(name){
    this.name = name;
    this.sayName = function(){
        console.log("我的名字是:" + this.name);
    }
}

var a = new Person("小明");
var b = new Person("小紅");

a.sayName(); // 我的名字是:小明
b.sayName(); // 我的名字是:小紅
複製程式碼

new運算子的缺點

但是,用建構函式生成例項物件,有一個缺點,那就是無法共享屬性和方法。

例如上面例子中的ab,它們都有sayName方法,雖然做的事相同,但它們卻是獨立的,這就會造成極大的資源浪費,因為每一個例項物件,都有自己的屬性和方法的副本。

prototype屬性的引入

考慮到這一點,Brendan Eich 決定為建構函式設定一個prototype屬性。

這個屬性包含一個物件,所有例項物件需要共享的屬性和方法,都放在這個物件裡面,而不需要共享屬性和方法,就放在建構函式裡面,這個物件就是prototype物件。

例項物件一旦建立,將自動引用prototype物件的屬性和方法。也就是說,例項物件的屬性和方法,分成兩種,一種是本地的,另一種是引用的。

現在對上面的例子進行改寫:

function Person(name){
    this.name = name;
}
Person.prototype = {
    sayName : function(){
        console.log("我的名字是:" + this.name);
    }
}

var a = new Person("小明");
var b = new Person("小紅");
a.sayName() // 我的名字是:小明
b.sayName() // 我的名字是:小紅
複製程式碼

現在無論Person被例項化多少次,它的例項物件都共享同一個sayName方法,這就是prototype最大的用處。

原型鏈

講原型一個不可避免的概念就是原型鏈,原型鏈是通過__proto__來實現的。

現在我們以Person的例子來講整個原型鏈。

var a = new Person("小明");

// 例項化物件的 __proto__ 指標指向建構函式的原型
console.log( a.__proto__ === Person.prototype )
// 建構函式的原型是一個物件,它的 __proto__ 指向物件的原型
console.log( Person.prototype.__proto__ === Object.prototype )
// 函式也是一個物件,它的 __proto__ 指向 函式的原型
console.log( Person.__proto__ === Function.prototype )
// 函式的原型是一個物件,它的 __proto__ 指向物件的原型
console.log( Function.prototype.__proto__ === Object.prototype )
// 物件的原型的__proto__ 指向 null
console.log( Object.prototype.__proto__ === null )
複製程式碼

以上就是a物件的整個原型鏈。

屬性查詢機制

當訪問一個物件的屬性時,Javascript 會從物件本身開始往上遍歷整個原型鏈,直到找到對應屬性為止。如果此時到達了原型鏈的頂部,也就是上例中的 Object.prototype,仍然未發現需要查詢的屬性,那麼 Javascript 就會返回 undefined值。

注:此文為原創文章,如需轉載,請註明出處。

相關文章