一篇文章帶你理解原型和原型鏈

走在前端的大道上javascript

本篇將本身讀過的相關 javascript原型和原型鏈 文章中,對本身有啓發的章節片斷總結在這(會對原文進行刪改),會不斷豐富提煉總結更新。前端

文章——深刻理解javascript之原型

通常的初學者,在剛剛學習了基本的javascript語法後,都是經過面向函數來編程的。以下代碼:java

var decimalDigits = 2,  
    tax = 5;  
  
function add(x, y) {  
    return x + y;  
}  
  
function subtract(x, y) {  
    return x - y;  
}  
  
//alert(add(1, 3));

經過執行各個函數來獲得最後的結果。可是利用原型,咱們能夠優化一些咱們的代碼,使用構造函數:
首先,函數本體中只存放變量git

var Calculator = function (decimalDigits, tax) {  
    this.decimalDigits = decimalDigits;  
    this.tax = tax;  
};

具體的方法經過prototype屬性來設置編程

Calculator.prototype = {  
    add: function (x, y) {  
        return x + y;  
    },  
  
    subtract: function (x, y) {  
        return x - y;  
    }  
};  
//alert((new Calculator()).add(1, 3));

這樣就能夠經過實例化對象後進行相應的函數操做。這也是通常的js框架採用的方法。segmentfault

原型還有一個做用就是用來實現繼承。首先,定義父對象:框架

var BaseCalculator = function() {  
    this.decimalDigits = 2;  
};  
  
BaseCalculator.prototype = {  
    add: function(x, y) {  
        return x + y;  
    },  
    subtract: function(x, y) {  
        return x - y;  
    }  
};

而後定義子對象,將子對象的原型指向父元素的實例化:編程語言

var Calculator = function () {  
    //爲每一個實例都聲明一個稅收數字  
    this.tax = 5;  
};  
          
Calculator.prototype = new BaseCalculator();

咱們能夠看到Calculator的原型是指向到BaseCalculator的一個實例上,目的是讓Calculator集成它的add(x,y)和subtract(x,y)這2個function,還有一點要說的是,因爲它的原型是BaseCalculator的一個實例,因此無論你建立多少個Calculator對象實例,他們的原型指向的都是同一個實例
上面的代碼,運行之後,咱們能夠看到由於Calculator的原型是指向BaseCalculator的實例上的,因此能夠訪問他的decimalDigits屬性值,那若是我不想讓Calculator訪問BaseCalculator的構造函數裏聲明的屬性值,那怎麼辦呢?只須要將Calculator指向BaseCalculator的原型而不是實例就好了。代碼以下:函數

var Calculator = function () {  
    this.tax= 5;  
};  
  
Calculator.prototype = BaseCalculator.prototype;

在使用第三方庫的時候,有時候他們定義的原型方法不能知足咱們的須要,咱們就能夠本身添加一些方法,代碼以下:學習

//覆蓋前面Calculator的add() function   
Calculator.prototype.add = function (x, y) {  
    return x + y + this.tax;  
};  
  
var calc = new Calculator();  
alert(calc.add(1, 1));

原型鏈

對象的原型指向對象的父,而父的原型又指向父的父,這種原型層層的關係,叫作原型鏈。
在查找一個對象的屬性時,javascript會向上遍歷原型鏈,直到找到給定名稱的屬性爲止,當查找到達原型鏈的頂部,也便是Object.prototype,仍然沒有找到指定的屬性,就會返回undefined。

示例以下:

function foo() {  
    this.add = function (x, y) {  
        return x + y;  
    }  
}  
  
foo.prototype.add = function (x, y) {  
    return x + y + 10;  
}  
  
Object.prototype.subtract = function (x, y) {  
    return x - y;  
}  
  
var f = new foo();  
alert(f.add(1, 2)); //結果是3,而不是13  
alert(f.subtract(1, 2)); //結果是-1

咱們能夠發現,subtrace是按照向上找的原則,而add則出了意外。緣由就是,屬性在查找的時候是先查找自身的屬性,若是沒有再查找原型。

說到Object.prototype,就不得不提它的一個方法,hasOwnProperty。它能判斷一個對象是否包含自定義屬性而不是原型鏈上的屬性,它是javascript中惟一一個處理屬性可是不查找原型鏈的函數。使用代碼以下:

// 修改Object.prototype  
Object.prototype.bar = 1;   
var foo = {goo: undefined};  
  
foo.bar; // 1  
'bar' in foo; // true  
  
foo.hasOwnProperty('bar'); // false  
foo.hasOwnProperty('goo'); // true

而爲了判斷prototype對象和某個實例之間的關係,又不得不介紹isPrototyleOf方法,演示以下:

alert(Cat.prototype.isPrototypeOf(cat2)); //true

文章——白話原型和原型鏈

1. 背景知識

JavaScript和Java、C++等傳統面向對象的編程語言不一樣,它是沒有類(class)的概念的(ES6 中的class也只不過是語法糖,並不是真正意義上的類),而在JavaScript中,一切皆是對象(object)。在基於類的傳統面向對象的編程語言中,對象由類實例化而來,實例化的過程當中,類的屬性和方法會拷貝到這個對象中;對象的繼承其實是類的繼承,在定義子類繼承於父類時,子類會將父類的屬性和方法拷貝到自身當中。所以,這類語言中,對象建立和繼承行爲都是經過拷貝完成的。但在JavaScript中,對象的建立、對象的繼承(更好的叫法是對象的代理,由於它並非傳統意義上的繼承)是不存在拷貝行爲的。如今讓咱們忘掉類、忘掉繼承,這一切都不屬於JavaScript。

2. 原型和原型鏈

其實,原型這個名字自己就很容易產生誤解,原型在百度詞條中的釋義是:指原來的類型或模型。按照這個定義解釋的話,對象的原型是對象建立自身的模子,模子具有的特色對象都要具備,這儼然就是拷貝的概念。咱們已經說過, JavaScript的對象建立不存在拷貝,對象的原型實際上也是一個對象,它和對象自己是徹底獨立的兩個對象。既然如此,原型存在的意義又是什麼呢?原型是爲了共享多個對象之間的一些共有特性(屬性或方法),這個功能也是任何一門面向對象的編程語言必須具有的。A、B兩個對象的原型相同,那麼它們必然有一些相同的特徵。

JavaScript中的對象,都有一個內置屬性[[Prototype]],指向這個對象的原型對象。當查找一個屬性或方法時,若是在當前對象中找不到定義,會繼續在當前對象的原型對象中查找;若是原型對象中依然沒有找到,會繼續在原型對象的原型中查找(原型也是對象,也有它本身的原型);如此繼續,直到找到爲止,或者查找到最頂層的原型對象中也沒有找到,就結束查找,返回undefined。能夠看出,這個查找過程是一個鏈式的查找,每一個對象都有一個到它自身原型對象的連接,這些連接組成的整個鏈條就是原型鏈。擁有相同原型的多個對象,他們的共同特徵正是經過這種查找模式體現出來的。

在上面的查找過程,咱們提到了最頂層的原型對象,這個對象就是Object.prototype,這個對象中保存了最經常使用的方法,如toString、valueOf、hasOwnProperty等,所以咱們才能在任何對象中使用這些方法。

3.建立對象常見的三種方式

1.字面量方式

當經過字面量方式建立對象時,它的原型就是Object.prototype。雖然咱們沒法直接訪問內置屬性[[Prototype]],但咱們能夠經過Object.getPrototypeOf()或對象的__proto__獲取對象的原型。

var obj = {};
Object.getPrototypeOf(obj) === Object.prototype;   // true
obj.__proto__  === Object.prototype;            // true

2.函數的構造調用

經過函數的構造調用(注意,咱們不把它叫作構造函數,由於JavaScript中一樣沒有構造函數的概念,全部的函數都是平等的,只不過用來建立對象時,函數的調用方式不一樣而已)也是一種經常使用的建立對象的方式。基於同一個函數建立出來的對象,理應能夠共享一些相同的屬性或方法,但這些屬性或方法若是放在Object.prototype裏,那麼全部的對象均可以使用它們了,做用域太大,顯然不合適。因而,JavaScript在定義一個函數時,同時爲這個函數定義了一個 默認的prototype屬性,全部共享的屬性或方法,都放到這個屬性所指向的對象中。由此看出,經過一個函數的構造調用建立的對象,它的原型就是這個函數的prototype指向的對象。

var f = function(name) { this.name = name };
f.prototype.getName = function() { return this.name; }  
 //在prototype下存放全部對象的共享方法
var obj = new f('JavaScript');
obj.getName();                  // JavaScript
obj.__proto__ === f.prototype;  // true
//建立構造函數
function Person(name){
    this.name = name
}

//每一個構造函數JS引擎都會自動添加一個prototype屬性,咱們稱之爲原型,這是一個對象
//每一個由構造函數建立的對象都會共享prototype上面的屬性與方法
console.log(typeof Person.prototype) // 'object'


//咱們爲Person.prototype添加sayName方法
Person.prototype.sayName = function(){
    console.log(this.name)
}

//建立實例
var person1 = new Person('Messi')
var person2 = new Person('Suarez')

person1.sayName() // 'Messi'
person2.sayName() // 'Suarez'

person1.sayName === person2.sayName //true

咱們藉助上面的例子來理解構造函數-原型-實例,三者之間的關係,主要有幾個基本概念

  • 構造函數默認會有一個protoype屬性指向它的原型
  • 構造函數的原型會有一個consctructor的屬性指向構造函數自己, 即
Person.prototype.constructor === Person
  • 每個new出來的實例都有一個隱式的__proto__屬性,指向它們的構造函數的原型,即
person1.__proto__ === Person.prototype
person1.__proto__.constructor === Person

Oject自己是一個構造函數,它也是一個對象,那麼

Object.__proto__ === Function.prototype

還有幾個須要咱們知道的特殊概念:

  • Function的原型屬性與Function的原型指向同一個對象. 即
  • Function.__proto__ == Function.prototype
Object.prototype.__proto__ === null
  • typeof Function.prototype === 'function'

3.Object.create()

第三種經常使用的建立對象的方式是使用Object.create()。這個方法會以你傳入的對象做爲建立出來的對象的原型。

var obj = {};
var obj2 = Object.create(obj);
obj2.__proto__ === obj;       // true

這種方式還能夠模擬對象的「繼承」行爲。

function Foo(name) {
    this.name = name;
}

Foo.prototype.myName = function() {
    return this.name;
};

function Bar(name,label) {
    Foo.call( this, name );   //
    this.label = label;
}

// temp對象的原型是Foo.prototype
var temp = Object.create( Foo.prototype );  

// 經過new Bar() 建立的對象,其原型是temp, 而temp的原型是Foo.prototype,
// 從而兩個原型對象Bar.prototype和Foo.prototype 有了"繼承"關係
Bar.prototype = temp;

Bar.prototype.myLabel = function() {
    return this.label;
};

var a = new Bar( "a", "obj a" );

a.myName(); // "a"
a.myLabel(); // "obj a"
a.__proto__.__proto__ === Foo.prototype;  //true

4. __proto__和prototype

這是容易混淆的兩個屬性。__proto__指向當前對象的原型,prototype是函數才具備的屬性,默認狀況下,new 一個函數建立出的對象,其原型都指向這個函數的prototype屬性。

5. 三種特殊狀況

1.對於JavaScript中的內置對象,如String、Number、Array、Object、Function等,由於他們是native代碼實現的,他們的原型打印出來都是ƒ () { [native code] }。

2.內置對象本質上也是函數,因此能夠經過他們建立對象,建立出的對象的原型指向對應內置對象的prototype屬性,最頂層的原型對象依然指向Object.prototype。

'abc'.__proto__ === String.prototype;   // true 
new String('abc').__proto__ === String.prototype;  //true

new Number(1).__proto__  ==== Number.prototype;   // true

[1,2,3].__proto__ === Array.prototype;            // true
new Array(1,2,3).__proto__ === Array.prototype;   // true

({}).__proto__ === Object.prototype;               //true
new Object({}).__proto__ === Object.prototype;     // true

var f = function() {};
f.__proto__ === Function.prototype;            // true
var f = new Function('{}');
f.__proto__ === Function.prototype;            // true

3.Object.create(null) 建立出的對象,不存在原型。

var a = Object.create(null); 
a.__proto__;               // undefined

此外函數的prototype中還有一個constructor方法,建議你們就當它不存在,它的存在讓JavaScript原型的概念變得更加混亂,並且這個方法也幾乎沒有做用。

推薦閱讀

Javascript中的原型鏈、prototype、__proto__的關係

相關文章
相關標籤/搜索