从面向对象设计角度,全面解读——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。

构造一个最简单的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() 默认会转换成 func.call(obj),显然这里obj必须是object类型(包括function类型)的对象,而如果obj是其它类型,如string或number类型呢?

答案是不可能的,因为非object类型的对象——无法添加属性函数(func),以令其成为自己的方法。那么,从另一个角度来说,非object类型的对象——是无法成为函数的this上下文的。

  • 第二种,使用内置函数。
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,对其细枝末节早已遗忘淡尽,于是又充满好奇与激情地重新理解了一遍,写成此文——以备后忆。

编辑于 2019-10-11

文章被以下专栏收录

    键盘敲击,字符闪耀,迭代疯狂,编译流畅——我把逻辑和诗歌灌入电光火石般奔腾流淌的指令集\(◎o◎)/