JS原型鏈簡單圖解

JS中原型鏈,說簡單也簡單。

首先明確: 函數(Function)才有prototype屬性,對象(除Object)擁有proto。

所謂原型鏈,指的就是圖中的proto這一條指針鏈!

原型鏈的頂層就是Object.prototype,而這個對象的是沒有原型對象的。

可在chrome的控制臺里面輸入:

    Object.__proto__

輸出是:

    function Empty() {}

原型鏈,如此而已。

對于新人來說,JavaScript的原型是一個很讓人頭疼的事情,一來prototype容易與proto混淆,二來它們之間的各種指向實在有些復雜,其實市面上已經有非常多的文章在嘗試說清楚,有一張所謂很經典的圖,上面畫了各種線條,一會連接這個一會連接那個,說實話我自己看得就非常頭暈,更談不上完全理解了。所以我自己也想嘗試一下,看看能不能把原型中的重要知識點拆分出來,用最簡單的圖表形式說清楚。

我們知道原型是一個對象,其他對象可以通過它實現屬性繼承。但是尼瑪除了prototype,又有一個proto是用來干嘛的?長那么像,讓人怎么區(qū)分呢?它們都指向誰,那么混亂怎么記啊?原型鏈又是什么鬼?相信不少初學者甚至有一定經驗的老鳥都不一定能完全說清楚,下面用三張簡單的圖,配合一些示例代碼來理解一下。

一、prototype和proto的區(qū)別

image
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() {}
image
/*1、字面量方式*/
var a = {};
console.log(a.__proto__);  //Object {}

console.log(a.__proto__ === a.constructor.prototype); //true

/*2、構造器方式*/
var A = function(){};
var a = new A();
console.log(a.__proto__); //A {}

console.log(a.__proto__ === a.constructor.prototype); //true

/*3、Object.create()方式*/
var a1 = {a:1}
var a2 = Object.create(a1);
console.log(a2.__proto__); //Object {a: 1}

console.log(a.__proto__ === a.constructor.prototype); //false(此處即為圖1中的例外情況)
什么是原型鏈?
var A = function(){};
var a = new A();
console.log(a.__proto__); //A {}(即構造器function A 的原型對象)
console.log(a.__proto__.__proto__); //Object {}(即構造器function Object 的原型對象)
console.log(a.__proto__.__proto__.__proto__); //null

我在寫一篇圖解prototype和proto的區(qū)別時,搜資料搜到了一個有意思的現象,下面這兩個運算返回的結果是一樣的:

Function instanceof Object;//true
Object instanceof Function;//true

這個是怎么一回事呢?要從運算符instanceof說起。

一、instanceof究竟是運算什么的?

我曾經簡單理解instanceof只是檢測一個對象是否是另個對象new出來的實例(例如var a = new Object(),a instanceof Object返回true),但實際instanceof的運算規(guī)則上比這個更復雜。

首先w3c上有官方解釋,但是一如既往地讓人無法一目了然地看懂……

知乎上有同學把這個解釋翻譯成人能讀懂的語言,看起來似乎明白一些了:

//假設instanceof運算符左邊是L,右邊是R
L instanceof R 
//instanceof運算時,通過判斷L的原型鏈上是否存在R.prototype
L.__proto__.__proto__ ..... === R.prototype ?
//如果存在返回true 否則返回false

注意:instanceof運算時會遞歸查找L的原型鏈,即L.proto.proto.proto.proto...直到找到了或者找到頂層為止。

所以一句話理解instanceof的運算規(guī)則為:

instanceof****檢測左側的proto原型鏈上,是否存在右側的prototype原型。

二、圖解構造器Function和Object的關系

圖解構造器Function和Object的關系

我們再配合代碼來看一下就明白了:

//①構造器Function的構造器是它自身
Function.constructor=== Function;//true

//②構造器Object的構造器是Function(由此可知所有構造器的constructor都指向Function)
Object.constructor === Function;//true



//③構造器Function的__proto__是一個特殊的匿名函數function() {}
console.log(Function.__proto__);//function() {}

//④這個特殊的匿名函數的__proto__指向Object的prototype原型。
Function.__proto__.__proto__ === Object.prototype//true

//⑤Object的__proto__指向Function的prototype,也就是上面③中所述的特殊匿名函數
Object.__proto__ === Function.prototype;//true
Function.prototype === Function.__proto__;//true

三、當構造器Object和Function遇到instanceof

我們回過頭來看第一部分那個“奇怪的現象”,從上面那個圖中我們可以看到:

Function.__proto__.__proto__ === Object.prototype;//true
Object.__proto__ === Function.prototype;//true

所以再看回第一點中我們說的instanceof的運算規(guī)則,Function instanceof Object 和 Object instanceof Function運算的結果當然都是true啦!

如果看完以上,你還覺得上面的關系看暈了的話,只需要記住下面兩個最重要的關系,其他關系就可以推導出來了:

1、所有的構造器的constructor都指向Function

2、Function的prototype指向一個特殊匿名函數,而這個特殊匿名函數的proto指向Object.prototype

本文嘗試闡述Js中原型(prototype)、原型鏈(prototype chain)等概念及其作用機制。上一篇文章([圖解Javascript上下文與作用域]介紹了Js中變量作用域的相關概念,實際上關注的一個核心問題是:“在執(zhí)行當前這行代碼時Js解釋器可以獲取哪些變量”,而原型與原型鏈實際上還是關于這一問題。

我們知道,在Js中一切皆為對象(Object),但是Js中并沒有類(class);Js是基于原型(prototype-based)來實現的面向對象(OOP)的編程范式的,但并不是所有的對象都擁有prototype這一屬性:

var a = {}; 
console.log(a.prototype);  //=> undefined

var b = function(){}; 
console.log(b.prototype);  //=> {}

var c = 'Hello'; 
console.log(c.prototype);  //=> undefined

prototype是每個function定義時自帶的屬性,但是Js中function本身也是對象,我們先來看一下下面幾個概念的差別:

1. functionFunction、Object{}

function是Js的一個關鍵詞,用于定義函數類型的變量,有兩種語法形式:

function f1(){ 
  console.log('This is function f1!');
}
typeof(f1);  //=> 'function'
 
var f2 = function(){ 
  console.log('This is function f2!');
}
typeof(f2);  //=> 'function'

如果用更加面向對象的方法來定義函數,可以用Function

var f3 = new Function("console.log('This is function f3!');"); 
f3();        //=> 'This is function f3!' 
typeof(f3);  //=> 'function'
 
typeof(Function); //=> 'function'

實際上Function就是一個用于構造函數類型變量的類,或者說是函數類型實例的構造函數(constructor);與之相似有的ObjectString、Number等,都是Js內置類型實例的構造函數。比較特殊的是Object,它用于生成對象類型,其簡寫形式為{}

var o1 = new Object(); 
typeof(o1);      //=> 'object'
 
var o2 = {}; 
typeof(o2);     //=> 'object'
 
typeof(Object); //=> 'function'

2. prototype VS __proto__

清楚了上面的概念之后再來看prototype

Each function has two properties: length and prototype

prototypelength是每一個函數類型自帶的兩個屬性,而其它非函數類型并沒有(開頭的例子已經說明),這一點之所以比較容易被忽略或誤解,是因為所有類型的構造函數本身也是函數,所以它們自帶了prototype屬性:

// Node
console.log(Object.prototype);  //=> {} 
console.log(Function.prototype);//=> [Function: Empty] 
console.log(String.prototype);  //=> [String: '']

除了prototype之外,Js中的所有對象(undefinednull等特殊情況除外)都有一個內置的[[Prototype]]屬性,指向它“父類”的prototype,這個內置屬性在ECMA標準中并沒有給出明確的獲取方式,但是許多Js的實現(如Node、大部分瀏覽器等)都提供了一個__proto__屬性來指代這一[[Prototype]],我們通過下面的例子來說明實例中的__proto__是如何指向構造函數的prototype的:

var Person = function(){}; 
Person.prototype.type = 'Person'; 
Person.prototype.maxAge = 100;
 
var p = new Person(); 
console.log(p.maxAge); 
p.name = 'rainy';
 
Person.prototype.constructor === Person;  //=> true 
p.__proto__ === Person.prototype;         //=> true 
console.log(p.prototype);                 //=> undefined

上面的代碼示例可以用下圖解釋:

image

Person是一個函數類型的變量,因此自帶了prototype屬性,prototype屬性中的constructor又指向Person本身;通過new關鍵字生成的Person類的實例p1,通過__proto__屬性指向了Person的原型。這里的__proto__只是為了說明實例p1在內部實現的時候與父類之間存在的關聯(指向父類的原型),在實際操作過程中實例可以直接通過.獲取父類原型中的屬性,從而實現了繼承的功能。

3. 原型鏈

清楚了prototype__proto__的概念與關系之后我們會對“Js中一切皆為對象”這句話有更加深刻的理解。進而我們會想到,既然__proto__是(幾乎)所有對象都內置的屬性,而且指向父類的原型,那是不是意味著我們可以“逆流而上”一直找到源頭呢?我們來看下面的例子:

// Node
var Obj = function(){}; 
var o = new Obj(); 
o.__proto__ === Obj.prototype;  //=> true 
o.__proto__.constructor === Obj; //=> true
 
Obj.__proto__ === Function.prototype; //=> true 
Obj.__proto__.constructor === Function; //=> true
 
Function.__proto__ === Function.prototype; //=> true 
Object.__proto__ === Object.prototype;     //=> false 
Object.__proto__ === Function.prototype;   //=> true
 
Function.__proto__.constructor === Function;//=> true 
Function.__proto__.__proto__;               //=> {} 
Function.__proto__.__proto__ === o.__proto__.__proto__; //=> true 
o.__proto__.__proto__.__proto__ === null;   //=> true
image

從上面的例子和圖解可以看出,prototype對象也有__proto__屬性,向上追溯一直到null。

new關鍵詞的作用就是完成上圖所示實例與父類原型之間關系的串接,并創(chuàng)建一個新的對象;instanceof關鍵詞的作用也可以從上圖中看出,實際上就是判斷__proto__(以及__proto__.__proto__...)所指向是否父類的原型:

var Obj = function(){}; 
var o = new Obj();
 
o instanceof Obj; //=> true 
o instanceof Object; //=> true 
o instanceof Function; //=> false
 
o.__proto__ === Obj.prototype; //=> true 
o.__proto__.__proto__ === Object.prototype; //=> true 
o.__proto__.__proto__ === Function;  //=> false

JS 面向對象之原型鏈

對象的原型鏈

  1. 只要是對象就有原型
  2. 原型也是對象
  3. 只要是對象就有原型, 并且原型也是對象, 因此只要定義了一個對象, 那么就可以找到他的原型, 如此反復, 就可以構成一個對象的序列, 這個結構就被成為原型鏈
  4. 原型鏈到哪里是一個頭?
  5. 一個默認的原型鏈結構是什么樣子的?
  6. 原型鏈結構對已知語法結構有什么修正?

原型鏈的結構

  1. 原型鏈繼承就是利用就是修改原型鏈結構( 增加、刪除、修改節(jié)點中的成員 ), 從而讓實例對象可以使用整個原型鏈中的所有成員( 屬性和方法 )
  2. 使用原型鏈繼承必須滿足屬性搜索原則

屬性搜索原則

  1. 所謂的屬性搜索原則, 就是對象在訪問屬性與方法的時候, 首先在當前對象中查找
  2. 如果當前對象中存儲在屬性或方法, 停止查找, 直接使用該屬性與方法
  3. 如果對象沒有改成員, 那么再其原型對象中查找
  4. 如果原型對象含有該成員, 那么停止查找, 直接使用
  5. 如果原型還沒有, 就到原型的原型中查找
  6. 如此往復, 直到直到 Object.prototype 還沒有, 那么就返回 undefind.
  7. 如果是調用方法就包錯, 該 xxxx 不是一個函數

原型鏈結構圖

  1. 構造函數 對象原型鏈結構圖
    function Person (){}; var p = new Person();

    image

  2. {} 對象原型鏈結構圖


    image
  3. [] 數組原型鏈結構圖


    image
  4. Object.prototype 對應的構造函數

    image

  5. div 對應的構造函數

  6. div -> DivTag.prototype( 就是 o ) -> Object.prototype -> null

var o = {
    appendTo: function ( dom ) {
    }
};
function DivTag() {}
DivTag.prototype = o;

var div = new DivTag();
image

函數的構造函數 Function

在 js 中 使用 Function 可以實例化函數對象. 也就是說在 js 中函數與普通對象一樣, 也是一個對象類型( 非常特殊 )

  1. 函數是對象, 就可以使用對象的動態(tài)特性
  2. 函數是對象, 就有構造函數創(chuàng)建函數
  3. 函數是函數, 可以創(chuàng)建其他對象(函數的構造函數也是函數)
  4. 函數是唯一可以限定變量作用域的結構
image

函數是 Function 的實例

<pre style="margin: 10px 0px; padding: 0px; white-space: pre-wrap; word-wrap: break-word;">new Function( arg0, arg1, arg2, ..., argN, body );</pre>

  1. Function 中的參數全部是字符串
  2. 該構造函數的作用是將 參數鏈接起來組成函數
  • 如果參數只有一個, 那么表示函數體
  • 如果參數有多個, 那么最后一個參數表示新函數體, 前面的所有參數表示新函數的參數
  • 如果沒有參數, 表示創(chuàng)建一個空函數

arguments 對象

arguments 是一個偽數組對象. 它表示在函數調用的過程中傳入的所有參數的集合.
在函數調用過程中沒有規(guī)定參數的個數與類型, 因此函數調用就具有靈活的特性, 那么為了方便使用,
在 每一個函數調用的過程中, 函數代碼體內有一個默認的對象 arguments, 它存儲著實際傳入的所有參數.

js 中函數并沒有規(guī)定必須如何傳參

  1. 定義函數的時候不寫參數, 一樣可以調用時傳遞參數
  2. 定義的時候寫了參數, 調用的時候可以不傳參
  3. 定義的時候寫了一參數, 調用的時候可以隨意的傳遞多個而參數

在代碼設計中, 如果需要函數帶有任意個參數的時候, 一般就不帶任何參數, 所有的 參數利用 arguments 來獲取.
一般的函數定義語法, 可以寫成:

 function foo (  ) {
    }

利用 Function 創(chuàng)建一個函數, 要求允許函數調用時傳入任意個數參數, 并且函數返回這些數字中最大的數字.

function foo ( ) {
    // 所有的參數都在 arguments 中. 將其當做數組使用
    // 問題而已轉換成在有一個數組中求最大值
    var args = arguments;
    var max = args[ 0 ];
    for ( var i = 1; i < args.length; i++ ) {
        if ( max < args[ i ] ) {
            max = args[ i ];
        }
    }
    return max;
}

練習: 利用 Function 寫一個函數, 要求傳入任意個數字 求和

函數的原型鏈結構

任意的一個函數, 都是相當于 Function 的實例. 類似于 {} 與 new Object() 的關系

function foo () {};
// 告訴解釋器, 有一個對象叫 foo, 它是一個函數
// 相當于 new Function() 得到一個 函數對象</pre>

  1. 函數有 __proto__ 屬性
  2. 函數的構造函數是 Function
  3. 函數應該繼承自 Function.prototype
  4. Fucntion.prototype 繼承自 Object.protoype
  5. 構造函數有prototype, 實例對象才有proto指向原型, 構造函數的原型才有 constructor 指向構造函數

intanceof

array instanceof Array
判斷 構造函數 Array 的原型 是否在 實例對象 array 的原型鏈存在

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容