本文,將會拋開__proto__的存在,轉而從JS語言面向對象設計的層面,去全面解讀函數與對象、Object與Function、以及原型鏈與繼承。
主題目錄如下:
- 類與對象的概念
- JS中的對象
- JS中的object
- JS中的函數
- JS中的函數與object
- JS中的對象與native code
- JS函數的new
- JS函數的prototype
- JS內置函數的命名
- JS中的原型鏈
- JS中的繼承
- JS中的instanceof
- JS中的Object與Function
- 結語
- 後記
注:測試代碼,使用chrome,版本77
類與對象的概念
類——就是由程序語法定義的模板,是一種自定義的數據類型。而對象——是類實例化的產物,擁有運行時的動態內存(可釋放),其內存地址可以被存儲在變量或常量(即指針)之中。而類實例化的過程,就是根據類定義分配內存的過程。因此同一個類,實例化的所有對象,都擁有相同的初始化內存結構。
那麼,類作爲一個數據類型,在實例化成對象之前,是不擁有動態內存的。這就如同,在非JS語言中,內置類型int (如同自定義數據類型——類)是不會分配內存的,而int a;(如同實例化類)纔會分配內存。
這裏的內存是指,類定義者的代碼,可以申請和釋放的內存(如堆或棧內存),而類定義(即代碼本身)被編譯成指令,依然需要運行時內存。這個“指令內存”由上層代碼(即執行代碼的環境程序)直接控制,例如:環境程序(解釋器或操作系統)如果提供了,卸載代碼模塊的功能,就可以釋放代碼模塊的“指令內存”。
JS中的對象
在JS中的數據類型,只能是(typeof顯示的)——object,function,string,number,boolean,symbol(ES6),undefined——這些,我們不能創造新的自定義數據類型。
顯然,這是因爲在JS中,我們沒有辦法自定義一個——類模板,然後實例化它。因此,我們也就不能擁有一個自定義的類型。
注:ES2015 增加了關鍵字 class,但定義出來的數據類型,typeof 依然是 function。也就是說,使用 class 依然不是在定義類模板,而是在定義 function。
這裏需要明確一個,容易混淆的對象概念,即:
類是一種自定義數據類型,其實例化的產物是對象,可在JS中,有一種數據類型被命名爲了——object(對象)。而通常,我們在JS中說到對象,指的就是object數據類型的對象,但其它數據類型(如string)的對象——也是對象,並且這些對象與object類型的對象,是不同的(後文會解讀區別)。
因此,在本文中:
- 具體數據類型的對象(指某類對象),使用typeof名稱,如:object,function,string,number,boolean等——這是狹義的對象。
- 所有數據類型的對象,統稱爲對象——這是廣義的對象。
而具體的對象(非指某類),直接使用其名稱,如window,document,prototype等;並在強調類型的時候(指某類型),使用typeof名稱+類型,如object類型,function類型等。
那麼,既然沒有類模板,在JS中,我們又如何去自定義object的數據結構呢?答案就隱藏在,object的設計之中。
JS中的object
JS中的對象有兩種: 自定義object和內置object。
首先,自定義object。
構造一個最簡單的object:
var obj = {};
這裏obj已經就是一個,實例化後的object了。而我們也可以,在實例化object的同時添加屬性,或是之後添加。
var obj = {
name : "name",
value: 100,
};
obj.name2 = "name2";
obj.value2 = 200;
事實上,object的屬性,可以是任意類型(沒有這個屬性即是undefined),而添加的屬性也可以被移除。如:
delete obj.name;
delete obj.value;
但這裏需要注意的是:
{
name : "name",
value: 100,
};
以上是一個對象,但不是一個定義,因爲這個語法形式,包含了實例化(分配內存)的操作,而定義是不存在實例化操作的。
其次,內置object。
內置object,全局可見,無需創建,可以直接使用。例如:document,window,Math……等等。如何證明它們是一個object?
console.log(typeof document) // object
console.log(typeof window) // object
console.log(typeof Math) // object
內置object,擁有自己的屬性,當然我們也可以自由增減屬性。
document.myName = "myName";
console.log(document.myName); // myName
delete document.myName;
console.log(document.myName); // undefined
Math.myName = "myName";
console.log(Math.myName); // myName
delete Math.myName;
console.log(Math.myName); // undefined
需要注意的是,內置object的內置屬性**,有些是隻讀的,不可以被刪除或修改。例如:
console.log(document.nodeType); // 9
delete document.nodeType;
console.log(document.nodeType); // 9
document.nodeType = 0;
console.log(document.nodeType); // 9
最後,綜上可見。
在JS中,內置object與自定義object,都擁有自由增減屬性的特性——這是object的基礎功能。而這種特性,正是JS中可以不提供類模板的關鍵。
因爲類模板的重要作用,就是定義對象所擁有的數據結構,但在JS中object的屬性可以自由增減,所以並不需要一個類模板,來提供一個預先的定義。
然而,類模板還有一個重要作用,就是可以生成,擁有相同數據結構的對象。那麼,在沒有類模板的JS中,如何讓object複製生成呢?並且我們直接創建的object,是根據什麼模板,實例化的呢?答案就隱藏在,函數的設計之中。
JS中的函數
JS中的函數,有兩大類:自定義函數與內置函數 。
首先,自定義函數。
使用function關鍵字創建:
// 創建foo函數變量
function foo() { }
// foo之所以是一個變量,就是因爲可以被賦值
foo = undefined;
console.log(foo); // undefined
同樣,我們可以創建一個匿名函數,賦值給一個變量。
// 創建匿名函數,賦值給foo變量
var foo = function() { }
其次,內置函數。
有很多,例如:Object,Function,Array,String……等等。如何證明它們是一個函數?
console.log(Object); // function Object() { [native code] }
console.log(Function); // function Function() { [native code] }
console.log(Array); // function Array() { [native code] }
console.log(String); // function String() { [native code] }
這些內置函數,由native code實現,關鍵是函數都是可以執行的,那麼這些函數的執行結果是什麼呢?
console.log(Object()); // {}
console.log(Function()); // function() { }
console.log(Array()); // []
console.log(String()); // ""
由此我們可以看到:
- Object函數,可以返回一個空對象(object類型)。
- Function函數,可以返回一個匿名空函數(function類型)。
- Array函數,可以返回一個空數組(object類型)。
- String函數,可以返回一個空字符串(string類型)。
從此,我們已經看出了,在JS中就是利用函數執行,即使用native code,去實例化一個對象的。而這些空對象的模板(包括屬性與方法的定義),應該是存在於native code之中的。
最後,另一種自定義函數的方式。
就是利用Function函數:
var foo = Function("arg1", "arg2", "console.log('this is Function body');");
console.log(foo); // function(arg1, arg2) { console.log('this is Function body'); }
foo(); // this is Function body
由此可見,使用Function函數與使用function關鍵字,其實是等價的。而我們也有理由相信,使用function關鍵字,其實就是把JS代碼的字符串,傳入了Function函數——以使用Function native code來創建一個自定義函數。
所以,自定義函數,也不是一個定義,而是一個對象,儘管它可以**“自定義”, 但它仍然是由Function函數**創建的對象。
那麼,同理:
var obj1 = {};
var obj2 = Object();
console.log(obj2); // {}
這兩種方式也是等價的——也就是使用Object函數,即Object native code,來創建object。
JS中的函數與object
從前面,我們可以看到,object的一個特點就是——可以自由增減屬性。而我們看到:
function foo() { }
foo.myName = "myName";
console.log(foo.myName); // myName
delete foo.myName;
console.log(foo.myName); // undefined
Object.myName = "myName";
console.log(Object.myName); // myName
delete Object.myName;
console.log(Object.myName); // undefined
自定義函數與內置函數,也都可以自由增減屬性。自然我們就會想,function——到底是不是一個object呢?
var foo1 = Function();
var foo2 = Function();
// 每個實例化的函數,都擁有獨立的內存
console.log(foo1 === foo2); // false
var obj1 = Object();
var obj2 = Object();
// 每個實例化的對象,都擁有獨立的內存
console.log(obj1 === obj2); // false
由此可以推理出,在JS中,function也是object。只不過,它們實例化自不同的模板,即Function native code與Object native code。也因此,function在具有object功能的基礎上,還具有額外的功能,其中最大的區別就是——function是可執行的,object則不行。
var foo = function() {};
var obj = {};
console.log(foo()); // undefined
console.log(obj()); // Uncaught TypeError: obj is not a function
JS中的對象與native code
由前面可知:
- object——是Object native code創建的對象。
- function——是Function native code創建的對象。
那麼,以此類推,在JS中typeof檢測的其它數據類型,意義如下:
- string——是String native code創建的對象。
- number——是Number native code創建的對象。
- boolean——是Boolean native code創建的對象。
- symbol——是Symbol native code創建的對象。
- undefined——是未定義對象的表示,即不知道是什麼類型(不知道由哪個native code創建)。
另外,還有一個null,其代表空指針,即object的佔位符。因此,typeof null——得到object類型。
而我們可以發現,很多由native code實現的內置函數,也都可以——創建對象,如:
- Array函數——可由Array native code創建數組對象(object類型)。
- Date函數——可由Date native code創建日期對象(object類型)。
- Window函數——可由Window native code創建窗口對象(object類型)。
- Document函數——可由Document native code創建文檔對象(object類型)。
- ……等等。
那麼顯然,不同的native code就是不同的模板,其實例化的對象,功能也就會不同。而在衆多的模板之中,大部分typeof返回的都是object類型。
那麼,object類型與其它類型最大的不同之處,就是——自由增減屬性。
var str = String();
// string類型無法添加屬性
str.myName = "num";
console.log(typeof str); // string
console.log(str.myName); // undefined
var num = Number();
// number類型無法添加屬性
num.myName = "myName";
console.log(typeof num); // number
console.log(num.myName); // undefined
var bool = Boolean();
// boolean類型無法添加屬性
bool.myName = "myName";
console.log(typeof bool); // boolean
console.log(bool.myName); // undefined
如上可見,string,number,boolean類型,都無法自由添加屬性。但function類型,卻可以像object類型那樣自由添加屬性。
由此,我們有理由相信,Function native code在執行的過程中,調用了Object native code,或是它們共同調用了同一段功能代碼,纔會令它們都擁有object類型的特性(自由增減屬性)。並且只要native code創建了object類型的對象(如Array,Date),那麼其代碼,就有可能調用了Object native code。
而我們也可以認爲,function類型是擴展了可執行能力的object類型,即:function類型繼承了object類型。
JS函數的new
在JS中,new只能修飾函數,其作用是用來——構造一個object,因此函數也被稱爲——構造器(constructor)。
var foo = function() {};
var obj = {};
console.log(new foo); // {}
console.log(new Object); // {}
console.log(new obj); // Uncaught TypeError: obj is not a constructor
那麼,用函數(即構造器)來構造object,就有兩種形式:
- 第一種,使用new function。
function foo() {
this.name = "foo";
}
var obj = new foo();
console.log(obj.name); // foo
我們通過在自定義函數中,使用this——來控制object的結構。其原理就在於,new foo()的過程類似如下的代碼實現:
function NewFoo() {
var obj = {};
// foo函數中的this,指向obj
// 因此foo函數執行,其中對this的操作,都是對obj操作
var ret = foo.call(obj);
// 判斷foo函數是否返回有效的對象
if (typeof ret === "object" && ret !== null) {
return ret;
}
// foo函數沒有return object就返回內建對象
// 缺少令obj指向foo.prototype的操作(後文會討論prototype)
return obj;
}
由此,我們可以看出,自定義函數,如果return了非null的object,那麼new操作就會得到return的object。測試如下:
function foo() {
var obj = {
name: "my foo"
};
this.name = "foo";
return obj;
// return null; // log: foo (new 得到內建object)
// return "AAA"; // log: foo (new 得到內建object)
// return document; // log: undefined (new 得到document)
}
var obj = new foo();
console.log(obj.name); // my foo (new 得到return的object)
而this除了在new的情況下使用,還有另外一個情況下,起作用,即:object調用方法(屬性函數)的時候。如:
function foo() {
console.log(this.myName);
}
var obj = {myName: "obj"};
obj.foo = foo;
foo(); // undefined
obj.foo(); // obj
foo.call(obj); // obj
也就是說,對象調用方法(屬性函數)的時候,默認會把調用對象,作爲this傳入函數,成爲函數的上下文。而方法與函數的區別就在於——有this的是方法,沒有this的是函數。
而如果一個函數,直接執行,沒有調用object,那麼其中的this就會指向內置的window。也就是說,全局自定義的function是被添加在window上的。
function foo() {
console.log(this === window);
}
foo(); // true
console.log(window.foo === foo); // true
由此可見,在JS中,任何函數都是需要調用object的,甚至有些函數切換了調用object,就無法正確的運行。例如:
var doc = document.getElementById;
console.log(document.getElementById("")); // null
console.log(typeof doc); // function
console.log(window.doc === doc); // true
console.log(doc("")); // Uncaught TypeError: Illegal invocation
而所有的內置object和function,都是添加在window上的,甚至包括它自己:
console.log(window.Object === Object); // true
console.log(window.Function === Function); // true
console.log(window.Math === Math); // true
console.log(window.window === window); // true
console.log(window.Window === Window); // true
另外,值得說明的是,obj.func() 顯然這裏obj必須是 object類型(包括function類型) 的對象,而如果obj是其它類型,如string或number類型呢?
答案是不可能的,因爲非object類型的對象——無法添加屬性函數(func),以令其成爲自己的方法。
那麼,從另一個角度來說,非object類型的對象——是無法成爲函數的this上下文的,就算使用call函數強行傳入this也不行。
function foo() {
console.log(this); // Number {100}
console.log(typeof this); // object
}
foo.call(100);
而事實上,call的機制就是——把調用函數,綁定到一個對象上,然後再利用這個對象調用這個函數。如下:
function foo() {
console.log(this);
}
function foo2() {
console.log(2);
}
foo.call(foo2); // foo2() { console.log(2); }
// 綁定函數到對象
foo2.fn = foo;
// 調用對象的函數,等價於foo.call(foo2)
foo2.fn(); // foo2() { console.log(2); }
foo.call.call(foo2); // 2
// fn變成call
foo2.fn = foo.call;
// 相當於foo2.call(),等價於foo.call.call(foo2)
foo2.fn(); // 2
var obj = {};
foo.call.call(obj); // Uncaught TypeError: foo.call.call is not a function
obj.fn = foo.call;
// 等價於foo.call.call(obj)
obj.fn(); // Uncaught TypeError: obj.fn is not a function
console.log(obj.fn); // function call() { [native code] }
那麼,foo.call.call(obj) 和 obj.fn() 的錯誤原因在於,call需要function類型的對象來調用,而obj不是function——這說明call的實現,會把調用對象,當做函數執行,
例如,執行obj(),會得到——Uncaught TypeError: obj is not a function 。
顯然這裏打印錯誤的格式是:執行表達式字 + 執行錯誤信息。
- 第二種,使用內置函數。
var obj1 = String();
var obj2 = new String();
console.log(typeof obj1); // string
console.log(typeof obj2); // object
obj1.myName = "myName";
obj2.myName = "myName";
console.log(obj1.myName); // undefined
console.log(obj2.myName); // myName
如上可見,內置函數可以實創建——對象(廣義),而new function只可以創建——object(狹義)。
而有趣的是:
console.log(String() === String() ); // true
console.log(Number() === Number() ); // true
console.log(Boolean() === Boolean()); // true
console.log(typeof String() ); // string
console.log(typeof Number() ); // number
console.log(typeof Boolean()); // boolean
console.log(typeof new String()); // object
console.log(typeof new Number()); // object
console.log(typeof new Boolean()); // object
我們會發現,在此,object類型與其它類型的又一個重要區別,就是——其它類型的對象,如果數值是一樣的,那麼它們背後,就會共享同一個對象,而object類型,則是每一個都是獨立內存空間的對象。
也正因此,其它類型的對象,不能夠自由增減屬性,也不能成爲函數的this上下文——因爲它們背後的對象是共享的同一個。那麼或許,在JS中,我們把string,number,boolean等類型的對象,看成是基本類型,而不是對象類型,這樣理解起來會更加自然。
但本質上,它們的背後一定不僅僅只是一個基本類型,因爲這些基本類型有內置的屬性和方法。
console.log("AAA".length); // 3
console.log("AAA".hasOwnProperty("length")); // true
JS函數的prototype
每一個函數,無論是內置的還是自定義的,在被創建的時候,就會(由native code)內建了一個prototype屬性,它被稱爲原型,並且只有function纔有,其它的對象沒有。
function foo() {}
console.log(typeof Function.prototype); // function
console.log(typeof Object.prototype); // object
console.log(typeof foo.prototype); // object
console.log(typeof Function().prototype); // object
console.log(typeof String().prototype); // undefined
console.log(typeof Number().prototype); // undefined
console.log(typeof Object().prototype); // undefined
console.log(typeof Boolean().prototype); // undefined
事實上,除了Function.prototype是function類型,其它所有函數的prototype都是object類型。並且除了自定義函數的prototype指向可以被修改以外,其它函數的prototype指向都不可以被修改(但prototype的屬性都可以修改)。
function foo() {}
console.log(foo.prototype); // { …… }
foo.prototype = "AAA";
console.log(foo.prototype); // AAA
foo.prototype = null;
console.log(foo.prototype); // null
foo.prototype = undefined;
console.log(foo.prototype); // undefined
Function.prototype = "AAA";
// 修改無效
console.log(Function.prototype); // ƒunction () { [native code] }
Object.prototype = "AAA";
// 修改無效
console.log(Object.prototype); // { …… }
Array.prototype = "AAA";
// 修改無效
console.log(Array.prototype); // [ …… ]
內置函數的prototype已經擁有了——很多內置的方法,當然我們也可以繼續自定義添加,而自定義函數的prototype則沒有內置方法。
那麼,函數的prototype有什麼作用呢?
實際上,我們會發現,函數構造的對象,其屬性和方法,可以來自於構造函數的prototype,例如:
function foo() {}
var obj1 = new foo();
obj1.name = "obj1";
console.log(obj1.name); // obj1
console.log(obj1.myName); // undefined
console.log(obj1.myFunc); // undefined
foo.prototype.myName = "myName";
foo.prototype.myFunc = foo;
console.log(obj1.myName); // myName
console.log(obj1.myFunc); // function foo() {}
var obj2 = new foo();
obj2.name = "obj2";
console.log(obj2.name); // obj2
console.log(obj2.myName); // myName
console.log(obj2.myFunc); // function foo() {}
可見,添加在函數prototype上的屬性和方法,是函數構造對象所共享的,而添加在對象上的屬性和方法,自然就是對象所獨享的。
於是,prototype安放在函數之上就是一個——顯而易見的設計了。
因爲,函數是對象的構造器,一個函數構造的所有對象,都共享這一個函數構造器。那麼,函數構造器上的prototype就如同——類的靜態字段與方法一樣,是所有對象實例所共享的。
接着,我們會看到,內置函數所構造的對象,其內置的方法,都是來自於——內置函數的prototype,例如:
var str = new String();
console.log(str.hasOwnProperty("charAt")); // false
console.log(String.prototype.hasOwnProperty("charAt")); // true
console.log(str.charAt === String.prototype.charAt); // true
String.prototype.charAt = null;
console.log(str.charAt); // null
那麼,我們得到的結論就是,任何一個對象,都可以直接訪問其構造函數的prototype屬性。
而任何一個prototype都有一個內置的constructor屬性(function類型),指向了這個prototype的所屬函數——也就是說,任何一個對象,都可以通過constructor屬性訪問其構造函數。
console.log("".constructor); // function String() { [native code] }
console.log((0).constructor); // function Number() { [native code] }
console.log(false.constructor); // function Boolean() { [native code] }
console.log({}.constructor); // function Object() { [native code] }
console.log(function(){}.constructor); // function Function() { [native code] }
console.log("".constructor === String.prototype.constructor); // true
console.log((0).constructor === Number.prototype.constructor); // true
console.log(false.constructor === Boolean.prototype.constructor); // true
console.log({}.constructor === Object.prototype.constructor); // true
console.log(function(){}.constructor === Function.prototype.constructor);// true
從此,我們也可以看出constructor內置在prototype之上的好處——就是所有對象,都可以直接訪問到,其自身的構造函數,而構造函數的prototype,就是這個對象可以訪問的prototype。
也就是說,對象(any)可以直接訪問的屬性和方法,可以來自any.constructor.prototype。
JS內置函數的命名
內置函數,即是由native code實現的函數,從命名來看有兩大類:
-
一類是全大寫命名,如:Object,Function,String,Array,Boolean,Window……等等。很明顯,這類內置函數的命名意圖,是充當了對象的構造器。
-
一類是首字母小寫命名,如:Object.prototype.toString,String.prototype.charAt,Array.prototype.push……等等,這類內置函數的命名意圖,是充當了對象的方法。它們被添加在prototype之上,被對象共享使用,調用的時候需要正確的調用對象。
那麼,有趣的是:
console.log(typeof Object.prototype.toString); // function
console.log(Object.prototype.toString.prototype); // undefined
console.log(new Object.prototype.toString()); // toString is not a constructor
可見,並非所有的function都是——構造器,那麼如果function不是構造器,就無法被new修飾,且不存在prototype屬性。
JS的原型鏈
從前文可知,函數構造器擁有prototype,其構造的對象可以直接訪問它的prototype。
於是,這就會引出兩個問題:
- 第一,函數也是對象(function類型),它的構造器(constructor)是誰呢?
- 第二,prototype也是對象(Function的prototype是function類型,其它的是object類型),它的構造器(constructor)是誰呢?
第一個問題
以String這個函數構造器爲例:
// 這是String構造出的對象,所共享的構造器
console.log(String.prototype.constructor); // function String() { [native code] }
// 這是String這個對象,本身的構造器
console.log(String.constructor); // function Function() { [native code] }
console.log(String.constructor === Function.prototype.constructor); // true
意料之中的是,所有函數,都是由Function構造的。
那麼,Function是誰構造的呢?
// 這是Function這個對象,本身的構造器
console.log(Function.constructor); // function Function() { [native code] }
// 這是Function這個對象,本身構造器的構造器
console.log(Function.constructor.constructor); // function Function() { [native code] }
// Function這個對象的構造器,指向了自己的原型構造器
console.log(Function.constructor === Function.prototype.constructor); // true
// 並且Function這個對象的構造器,就是它自己
console.log(Function.constructor === Function); // true
也就是說,Function自己構造了自己。那如何自己能構造自己?——必然是,native code構造了Function,然後再設定了constructor和prototype的指向。
第二個問題
一個對象通過constructor屬性很容就知道,它的構造器是誰。但要找出prototype本身的構造器,需要一些技巧,因爲prototype.constructor指向的不是prototype本身的構造器,而是共享prototype的構造器。
那麼,突破點就在於:
- 一個對象,可以直接訪問其構造器的prototype,
- 如果這個對象,可以訪問的屬性,不屬於這個對象,就必然屬於這個對象構造器的prototype,
- 於是,這個屬性所在的prototype,其構造器,就是這個對象的構造器。
而prototype有object和function類型,所以自然我們會猜測,它是由Object和Function函數所構造的。
先測試Object函數:
function foo() {}
var fooProto = foo.prototype;
// fooProto可以訪問hasOwnProperty,卻不擁有hasOwnProperty屬性
// 說明hasOwnProperty屬性存在於fooProto構造函數的prototype之上
console.log(fooProto.hasOwnProperty("hasOwnProperty")); // false
// hasOwnProperty存在於Object的prototype之上
// 說明由Object函數構造的對象,可以直接訪問hasOwnProperty
console.log(Object.prototype.hasOwnProperty("hasOwnProperty")); // true
Object.prototype.hasOwnProperty = null;
console.log(fooProto.hasOwnProperty); // null
事實上,所有對象都可以訪問hasOwnProperty,但它們卻都不擁有hasOwnProperty屬性:
console.log("".hasOwnProperty("hasOwnProperty")); // false
console.log([].hasOwnProperty("hasOwnProperty")); // false
console.log({}.hasOwnProperty("hasOwnProperty")); // false
console.log((0).hasOwnProperty("hasOwnProperty")); // false
console.log(false.hasOwnProperty("hasOwnProperty")); // false
console.log(function(){}.hasOwnProperty("hasOwnProperty")); // false
這說明了,所有對象,都是通過其構造器的prototype來訪問hasOwnProperty屬性的,而hasOwnProperty屬性只存在於Object.prototype之上,那麼Object就是——所有對象其構造器(包括自定義函數和內置函數)prototype的構造器。
因爲這樣,所有對象,都可以訪問自己構造器的prototype,而這些prototype,都可以訪問其構造器的prototype,即:Object.prototype。
那麼,Object.prototype作爲object類型,其本身也就是Object構造的,於是Object.prototype構造器的prototype就是自己。
再測試Function函數:
console.log(Object.call); // function call { [native code] }
console.log(Object.hasOwnProperty("call")); // false
console.log(Object.prototype.hasOwnProperty("call")); // false
console.log(Function.hasOwnProperty("call")); // false
console.log(Function.prototype.hasOwnProperty("call")); // true
Function.prototype.call = null;
console.log(Object.call); // null
console.log(Function.call); // null
console.log(function(){}.call); // null
事實上,所有函數都可以訪問call,但它們卻都不擁有call屬性。
console.log(Number.call); // function call() { [native code] }
console.log(String.call); // function call() { [native code] }
console.log(Array.call); // function call() { [native code] }
console.log(Object.toString.call); // function call() { [native code] }
console.log(function(){}.call); // function call() { [native code] }
console.log(Number.hasOwnProperty("call")); // false
console.log(String.hasOwnProperty("call")); // false
console.log(Array.hasOwnProperty("call")); // false
console.log(Object.toString.hasOwnProperty("call")); // false
console.log(function(){}.hasOwnProperty("call")); // false
這說明了,所有函數都是通過其構造器的prototype來訪問call屬性的,而call屬性只存在於Function.prototype之上,那麼Function就是——所有函數其構造器prototype的構造器。
但所有函數的構造器,都是Function,所以按理說,Function就應該是Function.prototype的構造器,即Function構造了Function.prototype。然而,事實並不一定是這樣,因爲作爲一個function,Function.prototype並沒有prototype,說明它不是一個構造函數(constructor),而Function無法構造非構造函數。
console.log(Function.prototype.prototype); // undefined
不過,這並不妨礙我們,得到如下結論:
- 所有對象,共享Object.prototype,即:所有對象(包括Function),其構造器的prototype的構造器的prototype指向Object.prototype。
- 所有函數,共享Function.prototype,即所有函數,其構造器(即Function)的prototype指向Function.prototype。
於是,這裏出現了一個問題,即:Function.prototype是function類型,它更不可能是由Object構造的,自然也就不應該指向object類型的Object.prototype。
進行如下測試:
// Function.prototype可以訪問hasOwnProperty,卻不擁有hasOwnProperty
// hasOwnProperty存在於Object.prototype之上
console.log(Function.prototype.hasOwnProperty("hasOwnProperty")); // false
Function.prototype.myFunc = function() {
console.log("function prototype");
}
Object.prototype.myFunc = function() {
console.log("object prototype");
}
// Function.prototype.myFunc 覆蓋了 Object.prototype.myFunc
String.myFunc(); // function prototype
"".myFunc(); // object prototype
console.log(Function.prototype); // ƒunction () { [native code] }
可見,Function.prototype的確可以訪問Object.prototype,顯然這是——超越JS語言層面的設定。
那麼,綜上可見,JS中原型鏈的機制,也就躍然於紙上了:
- 原型——就是指prototype。
- 原型鏈——就是通過prototype,串聯起來的屬性和方法的訪問機制。
其訪問機制就在於:
- 對象可以訪問其構造器的prototype,
- 而prototype也是對象,於是它又可以訪問其構造器的prototype,接着這個prototype還是對象,又可以繼續訪問其構造器的prototype……
- 就這樣,一直到Object.prototype爲止——因爲Object.prototype構造器的prototype就是Object.prototype本身。
那麼,原型鏈抵達Object.prototype,就有三條路徑:
- 第一,由Function構造的對象(如自定義函數) => 訪問Function.prototype => 訪問Object.prototype。
function foo() {}
console.log(foo.myName); // undefined
Function.prototype.myName = "foo";
console.log(foo.myName); // foo
- 第二,由內置函數構造的對象 (如Object(),String(),Array())=> 訪問內置函數.prototype => 訪問Object.prototype。
var str = String();
console.log(str.myName); // undefined
String.prototype.myName = "str";
console.log(str.myName); // str
- 第三,由new constructor構造的對象 => 訪問constructor.prototype => 訪問Object.prototype。
function foo() {}
var obj = new foo();
console.log(obj.myName); // undefined
foo.prototype.myName = "foo";
console.log(obj.myName); // foo
可見,是函數(構造器)提供了原型(prototype),對象提供了訪問原型(prototype)的鏈,從而才形成了——原型鏈。
而我們可以通過修改prototype的指向,構建一個長長的原型鏈,即手動設置每一個 prototype的指向,但最後一個指向的對象一定會是,上面三種情況的一種,從而原型鏈止於Object.prototype。(只能修改自定義函數prototype的指向,其它的prototype只能修改屬性)
不過原型鏈越長,查找效率就會越慢,顯然查找一個屬性,需要對比原型鏈上每個prototype的每一個屬性。
JS中的繼承
我們爲什麼要繼承?顯然是爲了複用類模板——已有的屬性和方法。
而從前文,我們就能夠看出,prototype已經提供了,複用屬性和方法的功能,只不過這種複用是靜態共享,而不是針對每個對象實例的獨立拷貝。
於是,我們能夠做出如下的類比:
- constructor——類模板。
- constructor.prototype——類的靜態屬性與方法。
- new constructor——類的實例化。
那麼在JS中,我們可以通過修改原型鏈的指向,讓(所有)子對象,共享(一個)父對象及其原型的屬性和方法,來達到模擬繼承的目的,而這被稱爲——原型鏈繼承。
function Father() {}
function Child() {}
Father.prototype.myName = "father";
// 子原型指向父對象,同時子對象的原型鏈,指向了父原型
Child.prototype = new Father();
// new Father()對象的構造器會指向Father
// 這裏修改指向Child不會影響其它Father構造的對象
Child.prototype.constructor = Child;
var c = new Child();
console.log(c instanceof Child) // true
console.log(c instanceof Father) // true
console.log(c.myName) // father
// 覆蓋父原型屬性
c.myName = "child";
console.log(new Father().myName); // father
這種繼承的方式,有以下幾個特點:
- 父原型會影響子對象。
- 子原型會影響子對象。
- 子原型不會影響父對象。
- 父對象的非構造屬性與方法,與子對象無關。
而如果Father構造器是已有的複雜結構,那麼Child.prototype = new Father();將會把父對象的構造屬性和方法全部暴露給new Child對象,爲了避免這種問題(有時又是需要的),我們可以構建一箇中間層:
function Mid() {}
// 中間層指向Father原型
Mid.prototype = Father.prototype;
// Child原型指向Mid構造的對象
// Mid構造的對象,其原型鏈指向Father原型
Child.prototype = new Mid();
// 原本構造器指向Mid
Child.prototype.constructor = Child;
這裏Mid層,屏蔽了繼承污染,而我們不能夠如下這樣:
Child.prototype = Father.prototype;
因爲Father.prototype.constructor與Child.prototype.constructor將會無法區分,並且此時,子原型將會影響父原型(兩者是同一個prototype),從而影響父對象。但子原型是不應該影響父對象的,那麼利用Mid中間層,則可以隔離這種問題。
其它,更多繼承實現方式,不在本文討論範圍。
JS中的instanceof
事實上,instanceof的工作機制,就是檢查原型鏈上原型的存在性,即:any intanceof constructor是判斷any對象的原型鏈中,是否存在constructor的原型。
那麼,如果我們修改一個對象的原型鏈,即:any.constructor.prototype的指向,就顯然可以改變instanceof的判斷結果,例如:
function A() {}
function B() {}
var a = new A();
// a的原型鏈指向A的原型
console.log(a.constructor.prototype === A.prototype); // true
console.log(a instanceof A); // true
// a的原型鏈與B的原型沒有關係
console.log(a instanceof B); // false
// 修訂a的原型鏈指向B的原型
A.prototype = B.prototype;
// a的原型鏈已經更新
a = new A();
console.log(a instanceof B); // true
// 修訂a的原型鏈指向Function的原型
A.prototype = Function.prototype;
// a的原型鏈已經更新
a = new A();
console.log(typeof a); // object
// a是Object類型,卻實例化自Function
console.log(a instanceof Function); // true
a(); // Uncaught TypeError: a is not a function
JS中的Object與Function
JS中的類型特點:
- constructor類型——可執行、可增減屬性、可構造其它類型對象(有prototype)。
- function類型——可執行、可增減屬性、不可構造其它類型對象(無prototype)。
- object類型——不可執行、可增減屬性、不可構造其它類型對象(無prototype)。
- 其它類型——不可執行、不可增減屬性、不可構造其它類型對象(無prototype)。
各種對象之間的關係:
- Function——由Function構造,是constructor類型,其Function native code可以構造constructor。
- Object——由Function構造,是constructor類型,其Object native code可以構造object。
- Object.prototype——由Object構造,是object類型。
- Function.prototype——由native code構造,是function類型,可以訪問Object.prototype。
- constructor.prototype——由Object構造,是object類型,可以訪問Object.prototype。
- function——由native code構造,是function類型,指向Function.prototype。
- constructor——由Function構造,是constructor類型,指向Function.prototype,其native code可以構造其它類型對象。
綜上可見,Object與Function的關係在於:
- Function通過Function.prototype,可以訪問Object.prototype。
- Object可以直接訪問Function.prototype。
而instanceof的檢查是基於原型鏈的,那麼如下的結果就很容易解釋了:
// 因爲Function通過Function.prototype,可以訪問Object.prototype
console.log(Function instanceof Object); // true
// 因爲Function,可以訪問Function.prototype
console.log(Function instanceof Function); // true
// 因爲Object,可以訪問Function.prototype
console.log(Object instanceof Function); // true
// 因爲Object通過Function.prototype,可以訪問Object.prototype
console.log(Object instanceof Object); // true
結語
本文,爲什麼要拋開__proto__的存在,來討論呢?
因爲事實上,_proto__是一個隱藏屬性,對JS編程是不可見的,而我們在編寫JS的時候,也幾乎用不到它,更不會去修改它。儘管有API去檢測原型鏈的關係:
// 判斷func.prototype是否存在於object的原型鏈之中
// 等同於判斷:object intanceof func,即:object繼承自func
func.prototype.isPrototypeOf(object);
// Object的方法,返回any指向的prototype,即:obj.__proto__
// EC6
Object.getPrototypeOf(any);
但我們沒事爲什麼要去獲取__proto__呢?
- 判斷原型鏈的繼承關係,我們可以使用:instanceof;
- 使用原型鏈繼承,可以直接控制構造器的prototype。
那麼顯然,__proto__是JS引擎實現原型鏈,所需要的屬性——它其實就是存儲了prototype的值(因此某些prototype不可以被修改),以讓prototype,即原型,可以連起來成爲一條鏈(如鏈表),形成原型鏈。
由此可見,如果實現或瞭解過JS引擎,自然就會對__proto__、prototype、Object和Function有清晰而深刻的認識——因爲你需要用代碼實現它們的功能與關係。
但就JS語言本身的使用,必然是不能依賴——其內部實現屬性__proto__的,也就是說從JS語言設計層面,就應該可以自洽地——理解其功能與行爲。
而這就是構建本文的想法和初衷——從JS語言設計角度,去解讀其語法設定與功能行爲。
後記
前幾天,看了自己在2010年2月寫的一遍技術博文,《
javascript中的Function和Object》,發現Function和Object的關係,以及JS中對象與函數的概念,的確有些饒人。
由於很多年不使用JS,對其細枝末節早已遺忘淡盡,於是又充滿好奇與激情地重新理解了一遍,寫成此文——以備後憶。