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的構造器是它自身 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. function
、Function
、Object
和{}
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);與之相似有的Object
或String
、Number
等,都是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
andprototype
prototype
和length
是每一個函數類型自帶的兩個屬性,而其它非函數類型並沒有(開頭的例子已經說明),這一點之所以比較容易被忽略或誤解,是因為所有類型的構造函數本身也是函數,所以它們自帶了prototype
屬性:
1
2
3
4
|
// Node
console.log(Object.prototype);
//=> {}
console.log(Function.prototype);
//=> [Function: Empty]
console.log(String.prototype);
//=> [String: '']
|
除了prototype
之外,Js中的所有對象(undefined
、null
等特殊情況除外)都有一個內置的[[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();
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面向對象之原型鏈
JS 面向對象之原型鏈
對象的原型鏈
- 只要是對象就有原型
- 原型也是對象
- 只要是對象就有原型, 並且原型也是對象, 因此只要定義了一個對象, 那么就可以找到他的原型, 如此反復, 就可以構成一個對象的序列, 這個結構就被成為原型鏈
- 原型鏈到哪里是一個頭?
- 一個默認的原型鏈結構是什么樣子的?
- 原型鏈結構對已知語法結構有什么修正?
原型鏈的結構
- 原型鏈繼承就是利用就是修改原型鏈結構( 增加、刪除、修改節點中的成員 ), 從而讓實例對象可以使用整個原型鏈中的所有成員( 屬性和方法 )
- 使用原型鏈繼承必須滿足屬性搜索原則
屬性搜索原則
- 所謂的屬性搜索原則, 就是對象在訪問屬性與方法的時候, 首先在當前對象中查找
- 如果當前對象中存儲在屬性或方法, 停止查找, 直接使用該屬性與方法
- 如果對象沒有改成員, 那么再其原型對象中查找
- 如果原型對象含有該成員, 那么停止查找, 直接使用
- 如果原型還沒有, 就到原型的原型中查找
- 如此往復, 直到直到 Object.prototype 還沒有, 那么就返回 undefind.
- 如果是調用方法就包錯, 該 xxxx 不是一個函數
原型鏈結構圖
-
構造函數 對象原型鏈結構圖
function Person (){}; var p = new Person();
-
{} 對象原型鏈結構圖
-
[] 數組原型鏈結構圖
-
Object.prototype
對應的構造函數 - div 對應的構造函數
-
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 中函數與普通對象一樣, 也是一個對象類型( 非常特殊 )
- 函數是對象, 就可以使用對象的動態特性
- 函數是對象, 就有構造函數創建函數
- 函數是函數, 可以創建其他對象(函數的構造函數也是函數)
- 函數是唯一可以限定變量作用域的結構
函數是 Function 的實例
new Function( arg0, arg1, arg2, ..., argN, body );
- Function 中的參數全部是字符串
- 該構造函數的作用是將 參數鏈接起來組成函數
- 如果參數只有一個, 那么表示函數體
- 如果參數有多個, 那么最后一個參數表示新函數體, 前面的所有參數表示新函數的參數
- 如果沒有參數, 表示創建一個空函數
創建一個打印一句話的函數
// 傳統的 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;' )
解決代碼太長的辦法:
-
利用 加法 連接字符串
var func = new Function( 'a', 'b', 'c', 'var res = a > b ? a : b;' + 'res = res > c ? res : c;' + 'return res;' );
-
利用字符串特性( 剛學 )
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 );' );
- ES6 的語法( 少瀏覽器實現 )
- 使用 鍵盤左上角的 左單引號 表示可換行字符串的界定符
-
(最終)利用 DOM 的特性完成該方法
arguments 對象
arguments 是一個偽數組對象. 它表示在函數調用的過程中傳入的所有參數的集合.
在函數調用過程中沒有規定參數的個數與類型, 因此函數調用就具有靈活的特性, 那么為了方便使用,
在 每一個函數調用的過程中, 函數代碼體內有一個默認的對象 arguments, 它存儲着實際傳入的所有參數.
js 中函數並沒有規定必須如何傳參
- 定義函數的時候不寫參數, 一樣可以調用時傳遞參數
- 定義的時候寫了參數, 調用的時候可以不傳參
- 定義的時候寫了一參數, 調用的時候可以隨意的傳遞多個而參數
在代碼設計中, 如果需要函數帶有任意個參數的時候, 一般就不帶任何參數, 所有的 參數利用 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() 得到一個 函數對象
- 函數有
__proto__
屬性 - 函數的構造函數是 Function
- 函數應該繼承自
Function.prototype
Fucntion.prototype
繼承自Object.protoype
- 構造函數有prototype, 實例對象才有__proto__指向原型, 構造函數的原型才有 constructor 指向構造函數
intanceof
array instanceof Array
判斷 構造函數 Array 的原型 是否在 實例對象 array 的原型鏈存在