JavaScript 中的 prototype

原文

在 js 中創建對象,其中一個方式是使用構造函數,請看下面的一個例子

function Human(firstName, lastName) {
    this.firstName = firstName,
    this.lastName = lastName,
    this.fullName = function() {
        return this.firstName + " " + this.lastName;
    }
}

讓我們使用 Human 構造函數來創建 person1 和 person2 對象

var person1 = new Human("Virat", "Kohli");
var person2 = new Human("Sachin", "Tendulkar");

執行上面的代碼,我們會得到構造函數的兩個副本

這裏寫圖片描述

每一個使用構造函數創建的對象都會擁有自己的屬性和方法。通常情況下這是沒有意義的,因爲不同的對象的的方法如上面的 fullName ,是做一樣的事情的,這隻會浪費內存。下面我們會討論如何解決這個問題。

Prototypes

在,JavaScript 中,當一個函數被創建時 js 引擎會將一個 prototype 屬性添加進這個函數中。這個 prototype 屬性是一個對象(稱爲原型對象),它默認有一個 constructor 屬性,constructor 屬性指向 prototype 所在的這個函數。我們可以使用 functionName.prototype 來得到 prototype 屬性。

這裏寫圖片描述

如上圖所示,Human 構造函數有一個 prototype 屬性,指向原型對象。原型對象有一個 constructor 屬性,指向 Human 構造函數。接下來看下面的例子:

function Human(firstName, lastName) {
    this.firstName = firstName,
    this.lastName = lastName,
    this.fullName = function() {
        return this.firstName + " " + this.lastName;
    }
}

console.log(Human);

這裏寫圖片描述

使用下面的語法獲取 Human 構造函數的 prototype 屬性:

console.log(Human.prototype)

這裏寫圖片描述

從上圖可以看到,prototype 屬性是一個對象,它有兩個屬性:
1. constructor 屬性,指向 Human 函數本身
2. __proto__ 屬性,在講到繼承的時候我們會談論它

使用構造函數創建一個對象

當創建一個對象時,js 引擎爲這個新對象添加了一個 __proto__ 屬性,也稱爲dunder proto ,dunder 是 “double underscore”的簡拼 。dunder proto 或 __proto__ 指向該構造函數的原型對象。

這裏寫圖片描述

如上圖所示,使用 Human 構造函數創建的 person1 對象擁有一個 dunder proto 或是 __proto__ 屬性,且這個屬性指向構造函數的原型對象。

var person1 = new Human("Virat", "Kohli");

這裏寫圖片描述

從上圖可以看出,person1 的 __proto__ 屬性和 Human.prototype 屬性是一樣的,讓我們使用 === 操作符來查看它們是否指向相同的地址。

Human.prototype === person1.__proto__ //true

這說明 person1 的 __proto__ 屬性和 Human.prototype 指向了同一對象。

現在,讓我們創建另一個對象 person2

var person2 = new Human("Sachin", "Tendulkar");
console.log(person2);

這裏寫圖片描述

通過控制檯的輸出,我們發現 person2 的 __proto__ 屬性也是跟 Human.prototype 一樣。

Human.prototype === person2.__proto__ //true
person1.__proto__ === person2.__proto__ //true

從上面的判斷中我們可以確定 person1 和 person2 的 __proto__ 屬性都指向了 Human 構造函數的原型對象。

這裏寫圖片描述

由此我們得出一個結論:構造函數的原型對象被所有由此構造函數創建的對象共享。

原型對象

因爲原型對象是一個對象,我們可以爲其增加屬性和方法,這使得我們可以讓所有由同樣的構造函數創建的對象共享屬性和方法。

要爲原型對象增加屬性,使用點號或者方括號的方式都可以

//使用 '.' 符號
Human.prototype.name = "Ashwin";
console.log(Human.prototype.name)//輸出: Ashwin
//使用 '[]' 符號
Human.prototype["age"] = 26;
console.log(Human.prototype["age"]); //輸出: 26
console.log(Human.prototype);

這裏寫圖片描述

name 和 age 屬性已經被添加到 Human 的 prototype

例子

//創建一個空的構造函數
function Person(){

}
//爲原型對象添加 name 和 age 屬性,和一個 sayName 方法
Person.prototype.name = "Ashwin" ;
Person.prototype.age = 26;
Person.prototype.sayName = function(){
    console.log(this.name);
}

var person1 = new Person();
console.log(person1.name)// 輸出" Ashwin

讓我們來分析以下當我們執行 console.log(person.name) 時,都發生了什麼事情。首先查看 person 對象是否有 name 屬性

console.log(person1);

這裏寫圖片描述

從上圖可知 person1 除了 __proto__ ,沒有任何屬性,那爲什麼 console.log(person.name) 還是輸出了 “Ashwin” 呢?

當我們嘗試去獲得一個對象的屬性時,js 引擎首先在這個對象上尋找這個屬性,如果有,就使用它,如果沒有找到,那麼它會嘗試在原型對象,也就是這個對象的 __proto__ 所指向的原型對象上尋找這個屬性,如果找到了,就使用它,如果沒有,js 引擎就會繼續往上尋找,即在原型對象的原型對象上尋找,就像是一條鎖鏈,直到 null ,此時這個屬性就會是 undefined

所以,當執行到 person1.name 時,js 引擎先檢查 person 對象是否有這個屬性。這時,person 對象沒有 name 屬性,所以 js 引擎會去檢查是否 person 的 __proto__ 指向的原型對象擁有這個屬性,在原型對象這裏找到了 name 屬性,所以輸出了 “Ashwin”。

讓我們創建另一個對象 person2

var person2 = new Person();
console.log(person2.name)// 輸出: Ashwin

現在,在 person1 對象上定義 name 屬性

person1.name = "Anil"
console.log(person1.name)//輸出: Anil
console.log(person2.name)//輸出: Ashwin

在這裏,person1.name 輸出了 “Anil”,就如前面所說,js 引擎首先會嘗試在該對象中尋找該屬性,所以這裏輸出了 person1 的 name 屬性。

prototype 中的問題

由於由同一構造函數創建的對象共享一個原型對象,這個原型對象上的屬性和方法也因此被共享。如果一個對象 A 修改了原型的屬性的值,那麼其它對象並不會被影響。

console.log(person1.name);//輸出: Ashwin
console.log(person2.name);//輸出: Ashwin

person1.name = "Ganguly"

console.log(perosn1.name);//輸出: Ganguly
console.log(person2.name);//輸出: Ashwin

在第一行跟第二行,person1 和 person2 都沒有 name 這個屬性,因此它們使用了原型的屬性,輸出了同樣的值。

當 person1 改變了 name 的值,它在自己本身創建了一個 name 屬性。

考慮下面的例子,這個例子展示了當原型對象包含引用類型的一個問題。

function Person(){
}

Person.prototype.name = "Ashwin" ;
Person.prototype.age = 26;
Person.prototype.friends = ['Jadeja', 'Vijay'],//數組是引用類型
Person.prototype.sayName = function(){
    console.log(this.name);
}

//創建兩個對象
var person1= new Person();
var person2 = new Person();

//爲數組增加元素
person1.friends.push("Amit");

console.log(person1.friends);// 輸出: "Jadeja, Vijay, Amit"
console.log(person2.friends);// 輸出: "Jadeja, Vijay, Amit"

從上面的例子可以看到,person1 和 person2 都指向了原型對象上的 friends 數組。person1 向 friends 屬性增加了一個元素。

由於 friends 數組存在於 Person.prototype 中,而不是 person1,在 person1 中對 friends 屬性所做的改動也反映到了 person2.friends 中。

如果我們確實是希望所有對象共享這個數組,那麼這是沒有問題的,但這裏不是這種情況。

結合構造器和原型

想要解決構造器和原型的問題,我們可以結合使用它們。
問題有兩個:

  1. constructor: 每一個對象有自己的方法實例
  2. prototype: 一個對象對屬性的修改影響其它對象

我們可以通過在構造函數中聲明私有屬性,在原型中聲明方法和公共屬性來解決這個問題。如:

//在構造函數中定義私有屬性
function Human(name, age){
    this.name = name,
    this.age = age,
    this.friends = ["Jadeja", "Vijay"]
}
//在原型中定義公共屬性和方法
Human.prototype.sayName = function(){
    console.log(this.name);
}

var person1 = new Human("Virat", "Kohli");
var person2 = new Human("Sachin", "Tendulkar");

//查看是否這兩個對象的方法是否指向同一位置
console.log(person1.sayName === person2.sayName) // true

//修改 friends 屬性
person1.friends.push("Amit");

console.log(person1.friends)// 輸出: "Jadeja, Vijay, Amit"
console.log(person2.frinds)//輸出: "Jadeja, Vijay"

在上面的例子中,person2 的 friends 屬性沒有被 person1 的修改影響到。

這裏寫圖片描述

發佈了50 篇原創文章 · 獲贊 26 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章