写在前面
这篇文章我的目的是试图讲清楚prototype与__proto__。很多人也许和我一样,很困扰于这两个东西究竟是干啥的,网上的各种资料也是讲的云里雾里,傻傻分不清楚。今天我就来尝试把它们说清楚。
JavaScript里没有类的概念
首先必须明确这一点,如果你学过Java之类的面向对象语言,你肯定熟悉类的概念,在面向对象语言的逻辑里,我们要先设计类(Class),然后再把类实例化成对象(Object),就像是图纸和产品的关系,我们必须先设计“图纸”,然后才能打造“产品”。
而在JavaScript里,根本没有类的概念,它的所有东西都是对象,其实,这并没有什么问题,不是所有语言都必须像Java一样必须依赖类这个概念设计,比如C语言里也没有类的概念,一样活得很好。不过,我们必须承认,类的思想会给工程问题带来很多方便,所以,在JavaScript的不断发展中,一些需求也催生了JavaScript想模仿类这个概念。比如说,如果你想构建一个有一定规模的项目,那你必然会涉及到一些继承问题,而这种继承问题,在Java中就是用类来解决的,那在JavaScript中,我们该怎么办?当然有办法,那就是模仿,我们没有类,但我们可以模仿类,而且随着时代的发展,我们有了更好的方法。prototype与__proto__都是在这个过程中催生的产物,我们一会儿马上讨论,在这之前,我们先明确一下类型(type)的概念。
我们没有类(Class),但我们有类型(Type)
上面说过,JavaScript没有类的概念,它里面的所有东西都是对象,但他的对象是有类型的,英文是type。
关于类型的总结,基本上每本教科书上都会列举的很清楚,分两种,原始类型(Primitive Types)与引用类型(Reference Types)。
原始类型(5种):Boolean、Number、String、Null、Underfined
引用类型(6种):Array、Date、Error、Function、Object、RegExp (其实也不止这些)
在javascript中,可以用typeof
操作符判断一个变量的类型,关于typeof
也要说明一点,5中原始类型都可以用它判断出来(除了Null,Null类型返回object),但对于引用类型,只有两种返回值:function和object,这也就说明了一点,其实引用类型根本上只有两种,那就是Function和Object,其他的引用类型都是Object衍生出来的(它们都返回Object),下面代码可以验证:
//代码1
var boo = true;
var num = 123;
var str = "hello world!"
var nu = null;var und;
var fun = new Function();
var arr = new Array();
var dat = new Date();
var err = new Error();
var obj = new Object();
var reg = new RegExp();
console.log(typeof boo); //boolean
console.log(typeof num); //number
console.log(typeof str); //string
console.log(typeof nu); //object
console.log(typeof und); //undefined
console.log(typeof fun); //function
console.log(typeof arr); //object
console.log(typeof dat); //object
console.log(typeof err); //object
console.log(typeof obj); //object
console.log(typeof reg); //object
//证明Array等类型是Object与Function的下级
console.log(Array instanceof Object) //true
console.log(Array instanceof Function) //true
console.log(Object instanceof Array) //false
console.log(Function instanceof Array) //false
为什么说了这么多关于类型的东西呢,因为我们要研究prototype与__prototype,重点就要放在Function和Object两个类型上。下面我们就来揭开神秘面纱。
Function比Object更强大
上面强调过,javascript中的一切东西都是对象,而对象都可以有属性和方法,所以Function也是一种对象,它也可以有自己的属性和方法,它只是和java里的函数长得比较像而已,地位完全不同(一定要明确在javascript中,Function与Object时平级的,而不是Object的附属品)。而且在javascript中,Function的能力要强于Object,可以说,Object能做的事情Function都能做,但Function能做的事情Object并不都能做。
Function能做但Object做不了的事情:
第一,Fuction可以被执行。这是函数最基本的特征,不必多说。
第二,Fuction可以被当做Object的构造函数。当我们使用new
操作符后面跟着一个Function类型的变量时,这个Function变量会被当成构造函数返回一个Object对象,具体见下面代码:
//代码2
function Foo () { console.log("我是个Function");}
foo = new Foo();console.log(typeof Foo); //function
console.log(typeof foo); //object
第三,Function有内置的prototype属性,而Object没有。其实这一点与上一点有着很大的关系,正是因为有了把Function当做构造函数的功能,我们才需要prototype属性。下面会详细讲到,这里只要记住一点,prototype只有Function才有:
//接上面代码
console.log(Foo.prototype); //{}
console.log(foo.prototype); //undefined
明确了Object和Function的关系,我们下面来讨论javascript中的继承问题。
基于原型链的继承
我想很多人和我一样,多少学过一些Java这种面向对象的语言,关于继承的概念,最初也是在Java世界里形成的,所以形成了一种思维定式,但考虑javascript的继承的时候,我们需要打破这种思维定式。在Java中,继承的概念是通过类与类之间实现的,但javascript根本没有类,都是对象,所以真正的继承,其实直接存在于对象与对象之间。
在我们讨论继承的时候,范围只限于引用类型,不包括原始类型,这点需要明确,其实就是Function与Object两个类型。在考虑javascript的继承的时候,不应该去分别Function和Object,我们只需要统一把它们都看做对象即可,那javascript种究竟是通过什么来明确继承关系的呢,那就是__proto__。__proto__不同于prototype,prototype只有在Function中有,而__proto__在Function和Object中都有。
如果用最简单的话来描述javascript中继承的本质:一个对象A的__proto__属性所指向的那个对象B就是它的原型对象(或者叫上级对象、父对象),对象A可以使用对象B中定义的属性和方法,同时也可以使用对象B的原型对象C的属性与方法,以此递归,这也就是所谓的原型链。
下面的代码就描述了上面那段话:
//代码3
var A = {name:"wangyunok"};
var B = {weibo:"http://weibo.com/wangyunok"};
var C = {github:"https://github.com/wangyunok"}
B.__proto__ = C;
A.__proto__ = B;
console.log(A.name); //wangyunok
console.log(A.weibo); //http://weibo.com/wangyunok
console.log(A.github); //https://github.com/wangyunok
这就实现了javascript中最简单的继承,等等,你也许发现了,这不是__proto__一个东西把继承问题就都解决了么,那要prototype做甚?为什么好多教材上都说prototype与继承有关系?是啊,这也就是前几天我一直困扰的问题,不过现在我弄清楚了,其实,prototype这个东西真正发挥作用的时候,是你把一个Function当做构造函数使用时,而所谓的与继承有关系,不过是因为__proto__并非官方标准中定义的属性,所以他们借助prototype这个属性模仿Java中类与类之间继承的模式。下面我们就来重点分析javascript中用Function类型构造对象的过程,当你知道当你使用new的时候做了什么,你就会很清楚prototype的作用了。
当我们使用new时究竟发生了什么
var foo = new Foo();
前面我们已经分析过,上式中,foo的类型是Object,Foo的类型是Function。但我们想想在Java中,类似的语句有什么区别,对,在Java中,如果你这么写,Foo应该是一个类(Class)。矛盾就在这里,javascript并没有类的概念,但我们又想借用很多传统语言的语法形式,就造就了这种奇葩的现象,这个艰巨的任务只能交给Function。下面让我们把上面的代码写完整:
//代码4
//模仿类
function Foo(name,weibo,github)
{
this.name = name;
this.weibo = weibo;
this.github = github;
}
Foo.prototype.whoami = function(){ console.log("I'm wangyunok, a coder of javascript!") }
//创建对象
var foo = new Foo('wangyunok','http://weibo.com/wangyunok','https://github.com/wangyunok');
console.log(foo.name); //wangyunok
console.log(foo.weibo); //http://weibo.com/wangyunok
console.log(foo.github); //https://github.com/wangyunok
foo.whoami(); //I'm wangyunok, a coder of javascript!
上面这种写法,就是javascript中最典型的构建构造函数,然后创建对象的整个过程,一般会建议将方法定义到prototype属性中,注意prototype默认的类型是Object。但其实这么说你大概也就清楚该怎么做,并不知道new
的过程到底干了啥,下面我就来告诉你。
第一步,Foo函数被执行。Foo函数在foo的作用域下被执行,所以这里this指代的就是foo,这样name、weibo、github三个属性才会被当做foo的属性被创建,如果你在函数Foo中写一个console.log()
语句,它也会在结果中打印出来,见上面代码2。
第二步,将foo.__proto__指向Foo.prototype。这才是javascript构造函数的精髓所在,之后的原理和我们在代码3中讲的一样,foo就继承了Foo.prototype中(以及其原型链上)的属性与方法。下面代码可以佐证:
console.log(foo.__proto__ === Foo.prototype)//true
这样一个鲜活的foo就被创建出来了。
便捷的__proto__
我们再看一下上面的代码,我在其中注释里写到,整个上半部分就是模仿Java中的类的过程,也许在__proto__还没有广泛使用之前,javascript的继承还是通过这种模仿类的过程实现的,我们可以让prototype指向一个父类对象,但这显然麻烦的多。
__proto__目前还不是标准中的东西,不过看一些资料上说,也许会被纳入到ECMAScript6中,不管是不是标准,目前这个属性已经被广泛使用,我们可以用直接为其赋值的方式来指定原型,而不再需要通过prototype那种很麻烦的方式,这是一种进步,应该接纳。
用Stack Overflow上答案来总结
最后,我们用Stack Overflow上关于这个问题得票最多答案作为总结,他解释的非常简单,我也是看到这个之后才豁然开朗的。
原网页在这里: __proto__ VS. prototype in JavaScript
下面是答案:
__proto__ is the actual object that is used in the lookup chain to resolve methods, etc. prototype is the object that is used to build __proto__ when you create an object with new
:
( new Foo ).__proto__ === Foo.prototype( new Foo ).prototype === undefined
翻译一下:
__proto__是真正用来查找原型链去获取方法的对象。
prototype是在用new
创建对象时用来构建__proto__的对象。
还可以参考一下下图: