一文搞懂原型和原型鏈

前瞻

JavaScript是面對對象編程,但是它又跟其他編程語言不一樣,不同之處是JavaScript面對對象並不是依賴於抽象類,而是通過原型鏈。在C++Java使用new命令時,都會調用"類"的構造函數。而在Javascript語言中,new命令後面跟的不是類,而是構造函數。但是,用構造函數生成實例對象,有一個缺點,那就是無法共享屬性和方法。爲了解決這個問題,於是在構造函數設置了prototype屬性,也就是原型。

從一個構造函數說起

用構造函數生成實例對象,有一個缺點,那就是無法共享屬性和方法。從而造成對系統資源的浪費。具體例子如下:

function Cat(name, color) {
  this.name = name;
  this.color = color;
  this.meow = function () {
    console.log('喵喵');
  };
}

var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');

cat1.meow === cat2.meow
// false

從上面的代碼,可以cat1cat2meow方法是獨立的但是實現的功能都是一樣的,這既沒有必要,又浪費系統資源,因爲所有meow方法都是同樣的行爲,完全應該共享。這個問題的解決方法,就是JavaScript構造函數中添加prototype屬性。

prototype是什麼?

只有函數纔有prototype屬性,又稱爲顯式原型。prototype屬性包含一個對象,所有實例對象需要共享的屬性和方法,都放在這個對象裏面;那些不需要共享的屬性和方法,就放在構造函數裏面。

JavaScript規定,每個函數都有一個prototype屬性,指向一個對象。

function f() {}
typeof f.prototype // "object"

對於普通函數來說,該屬性基本無用。但是,對於構造函數來說,生成實例的時候,該屬性會自動成爲實例對象的原型。

function Animal(name) {
  this.name = name;
}
Animal.prototype.color = 'white';

var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');

cat1.color // 'white'
cat2.color // 'white'

上面代碼中,構造函數Animalprototype屬性,就是實例對象cat1cat2的原型對象。原型對象上添加一個color屬性,結果,實例對象都共享了該屬性。

原型對象的屬性不是實例對象自身的屬性。只要修改原型對象,變動就立刻體現在所有實例對象上

Animal.prototype.color = 'yellow';

cat1.color // "yellow"
cat2.color // "yellow"

如果實例對象自身就有某個屬性或方法,它就不會再去原型對象尋找這個屬性或方法。

cat1.color = 'black';

cat1.color // 'black'
cat2.color // 'yellow'
Animal.prototype.color // 'yellow';

總結一下,原型對象的作用,就是定義所有實例對象共享的屬性和方法。這也是它被稱爲原型對象的原因,而實例對象可以視作從原型對象衍生出來的子對象。

注:Object.prototype中Object是一個全局對象,也是一個構造函數,以及其他基本類型的全局對象也都是構造函數。

_proto_又是什麼?

_proto_是每個對象都有的屬性,又稱爲隱式原型。但是,_proto_不是一個規範屬性,只是部分瀏覽器實現了此屬性,對應的標準屬性是[[Peototype]]

大多情況下,_proto_可以理解爲‘構造函數的原型’,是實例和它的構造函數之間建立的鏈接,即_proto_ ===constructor.prototype

例子:

// 字面量方式
var a = {};
console.log(a.prototype);  //undefined
console.log(a.__proto__);  //Object {}

//構造器方式
var b = function(){}
console.log(b.prototype);  //b {}
console.log(b.__proto__);  //function() {}

// Object.create方式
var a1 = {}
var a2 = Object.create(a1)
console.log(b.prototype);  //undefined
console.log(b.__proto__);  //Object a1

ES6中的_proto_使用建議

本段摘自阮一峯ES6入門,具體解析可以點擊查看。

1、_proto_屬性沒有寫入ES6的正文,而是寫入了附錄,

2、原因是__proto__前後的雙下劃線,說明它本質上是一個內部屬性,而不是一個正式的對外的 API,只是由於瀏覽器廣泛支持,才被加入了ES6

3、標準明確規定,只有瀏覽器必須部署這個屬性,其他運行環境不一定需要部署,而且新的代碼最好認爲這個屬性是不存在的。

4、無論從語義的角度,還是從兼容性的角度,都不要使用這個屬性,而是使用下面的Object.setPrototypeOf()(寫操作)、Object.getPrototypeOf()(讀操作)、Object.create()(生成操作)代替。

原型鏈是什麼?

每個對象都會在其內部初始化一個屬性,就是prototype(原型),當我們訪問一個對象的屬性時,如果這個對象內部不存在這個屬性,那麼他就會去prototype裏找這個屬性,這個prototype又會有自己的prototype,於是就這樣一直找下去,也就是我們平時所說的原型鏈的概念。

原型鏈的終點

所有對象的原型最終都可以上溯到Object.prototypeObject.prototype對象有沒有它的原型呢?回答是Object.prototype的原型是nullnull沒有任何屬性和方法,也沒有自己的原型。因此,原型鏈的盡頭就是null

1、字符串原型鏈終點

let test = 'hello'

// 字符串原型
let stringPrototype = Object.getPrototype(test) 

// 字符串的原型是String對象
stringPrototype === String.Prototype  // true 

// String對象的原型是Object對象
Object.getPrototypeOf(stringPrototype) === Object.prototype // true 

2、函數原理鏈的終點


let test = function () {}
let fnPrototype = Object.getPrototype(test)
fnPrototype === Function.prototype // true
Object.getPrototypeOf(Function.prototype) === Object.Prototype // true

原型鏈是用來做什麼?

1、屬性查找

當JS引擎查找對象的屬性時,先查找對象本身是否存在該屬性,如果不存在,會去原型鏈上查找,但不會查找本自身的prototype。

用一個例子來形象說明一下:

let test = 'hello'

// 字符串原型
let stringPrototype = Object.getPrototype(test) 

// 字符串的原型是String對象
stringPrototype === String.Prototype  // true 

// String對象的原型是Object對象
Object.getPrototypeOf(stringPrototype) === Object.prototype // true 

2、拒絕查找原型鏈

hasOwnPrototype:指示對象自身屬性中是否具有指定的屬性。

語法:obj.hasOwnPrototype(prop)

參數prop:查找的屬性。

返回值:Boolean判斷是否存在。

let test = {'ABC': 'EDF' }
test.hasOwnPrototype('ABC') // true
test.hasOwnPrototype('toString') // false

該方法是在Object.prototype上,所有對象都可以使用,且會忽略掉那些從原型鏈上繼承的屬性。

擴展

constructor 屬性

prototype對象有一個constructor屬性,默認指向prototype對象所在的構造函數。

function P() {}
P.prototype.constructor === P // true

由於constructor屬性定義在prototype對象上面,意味着可以被所有實例對象繼承。

function P() {}
var p = new P();

p.constructor === P // true
p.constructor === P.prototype.constructor // true
p.hasOwnProperty('constructor') // false

上面代碼中,p是構造函數P的實例對象,但是p自身沒有constructor屬性,該屬性其實是讀取原型鏈上面的P.prototype.constructor屬性。

constructor 屬性的作用

constructor屬性的作用是,可以得知某個實例對象,到底是哪一個構造函數產生的。

舉個小栗子:

function F() {};
var f = new F();

f.constructor === F // true
f.constructor === RegExp // false

另一方面,有了constructor屬性,就可以從一個實例對象新建另一個實例。

我再舉個小栗子:

function Constr() {}
var x = new Constr();

var y = new x.constructor();
y instanceof Constr // true

總結

天下無難事,只怕有心人。原型和原型鏈算是前端工程師的開發基礎了,原型雖然不太好理解,但是我相信只要有心學習和實踐,也沒有想象中的那麼難。希望,這篇文章對我自己幫組的同時,童鞋們看完之後也有一定的收穫。

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