本文,将会抛开__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,对其细枝末节早已遗忘淡尽,于是又充满好奇与激情地重新理解了一遍,写成此文——以备后忆。