從面向對象角度(非__proto__角度),全面解讀——JS中函數與對象、Object與Function、以及原型鏈與繼承

本文,將會拋開__proto__的存在,轉而從JS語言面向對象設計的層面,去全面解讀函數對象ObjectFunction、以及原型鏈繼承

主題目錄如下:

  • 類與對象的概念
  • 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 codeObject 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,然後再設定了constructorprototype的指向。

第二個問題

一個對象通過constructor屬性很容就知道,它的構造器是誰。但要找出prototype本身的構造器,需要一些技巧,因爲prototype.constructor指向的不是prototype本身的構造器,而是共享prototype的構造器。

那麼,突破點就在於:

  • 一個對象,可以直接訪問其構造器的prototype
  • 如果這個對象,可以訪問的屬性,不屬於這個對象,就必然屬於這個對象構造器的prototype
  • 於是,這個屬性所在的prototype,其構造器,就是這個對象的構造器。

prototypeobjectfunction類型,所以自然我們會猜測,它是由ObjectFunction函數所構造的。

先測試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.prototypefunction類型,它更不可能是由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.constructorChild.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可以構造其它類型對象。

綜上可見,ObjectFunction的關係在於:

  • 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__、prototypeObjectFunction有清晰而深刻的認識——因爲你需要用代碼實現它們的功能與關係。

但就JS語言本身的使用,必然是不能依賴——其內部實現屬性__proto__的,也就是說從JS語言設計層面,就應該可以自洽地——理解其功能與行爲。

而這就是構建本文的想法和初衷——從JS語言設計角度,去解讀其語法設定與功能行爲。

後記

前幾天,看了自己在2010年2月寫的一遍技術博文,《
javascript中的Function和Object
》,發現Function和Object的關係,以及JS中對象與函數的概念,的確有些饒人。

由於很多年不使用JS,對其細枝末節早已遺忘淡盡,於是又充滿好奇與激情地重新理解了一遍,寫成此文——以備後憶。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章