JS原型鏈簡單圖解


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

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

首先,我畫了一張圖。

原型鏈

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

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

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

    Object.__proto__

輸出是:

    function Empty() {}

原型鏈,如此而已。

 

 

 

 

 

 

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

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

一、prototype和__proto__的區別

復制代碼
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() {}
復制代碼

復制代碼
/*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__的區別時,搜資料搜到了一個有意思的現象,下面這兩個運算返回的結果是一樣的:

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

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

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

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

首先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的運算規則為:

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的運算規則,Function instanceof Object 和 Object instanceof Function運算的結果當然都是true啦!

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

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

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

 

至於prototype和__proto__的關系如何推導,可以參考我寫的上一篇博客《三張圖搞懂JavaScript的原型對象與原型鏈

 

 

 

 

 

 

 

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

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

1
2
3
4
5
6
7
8
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. functionFunctionObject{}

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

1
2
3
4
5
6
7
8
9
function  f1(){ 
   console.log( 'This is function f1!' );
}
typeof (f1);   //=> 'function'
 
var  f2 =  function (){ 
   console.log( 'This is function f2!' );
}
typeof (f2);   //=> 'function'

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

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

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

1
2
3
4
5
6
7
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屬性:

1
2
3
4
// 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的:

1
2
3
4
5
6
7
8
9
10
11
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

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

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

3. 原型鏈

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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

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

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

1
2
3
4
5
6
7
8
9
10
var  Obj =  function (){}; 
var  o =  new  Obj();
 
instanceof  Obj;  //=> true 
instanceof  Object;  //=> true 
instanceof  Function;  //=> false
 
o.__proto__ === Obj.prototype;  //=> true 
o.__proto__.__proto__ === Object.prototype;  //=> true 
o.__proto__.__proto__ === Function;   //=> false

 

 

 

JS面向對象之原型鏈

 

JS 面向對象之原型鏈

對象的原型鏈

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

原型鏈的結構

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

屬性搜索原則

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

原型鏈結構圖

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

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

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

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

  5. div 對應的構造函數
  6. div -> DivTag.prototype( 就是 o ) -> Object.prototype -> null

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

函數的構造函數 Function

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

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

函數是 Function 的實例

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

創建一個打印一句話的函數

    // 傳統的 function foo () { console.log( '你好' ); } // Function var func = new Function( 'console.log( "你好" );' ); // 功能上, 這里 foo 與 func 等價

創建一個空函數

    // 傳統 function foo () {} // Function var func = new Function();

傳入函數內一個數字, 打印該數字

    // 傳統 function foo ( num ) { console.log( num ); } // Function var func = new Function ( "num" ,"console.log( num );" ); func();

利用 Function 創建一個函數, 要求傳入兩個數字, 打印其和

    var func = new Function( 'num1', 'num2', 'console.log( num1 + num2 );' );

練習: 利用 Function 創建一個函數, 要求允許函數調用時傳入任意個數參數, 並且函數返回這些數字中最大的數字.
練習: 利用 Function 創建一個求三個數中最大數的函數.

    // 傳統 function foo ( a, b, c ) { var res = a > b ? a : b; res = res > c ? res : c; return res; } // Function var func = new Function( 'a', 'b', 'c', 'var res = a > b ? a : b;res = res > c ? res : c;return res;' ) 

解決代碼太長的辦法:

  1. 利用 加法 連接字符串

    var func = new Function( 'a', 'b', 'c', 'var res = a > b ? a : b;' + 'res = res > c ? res : c;' + 'return res;' );
  2. 利用字符串特性( 剛學 )

    function foo ( a, b, c ) { var res = a > b ? a : b; res = res > c ? res : c; return res; } var func = new Function( 'a', 'b', 'c', 'return foo( a, b, c );' );
  3. ES6 的語法( 少瀏覽器實現 )
    • 使用 鍵盤左上角的 左單引號 表示可換行字符串的界定符
  4. (最終)利用 DOM 的特性完成該方法

arguments 對象

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

js 中函數並沒有規定必須如何傳參

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

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

    function foo ( /* ... */ ) { }

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

    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() 得到一個 函數對象
  1. 函數有 __proto__ 屬性
  2. 函數的構造函數是 Function
  3. 函數應該繼承自 Function.prototype
  4. Fucntion.prototype 繼承自 Object.protoype
  5. 構造函數有prototype, 實例對象才有__proto__指向原型, 構造函數的原型才有 constructor 指向構造函數

intanceof

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM