这篇文章是翻译一篇文章《》
这是法国前端工程师Vjeux 2011年写的一篇文章,用假设、代码表现的方法,通俗易懂的解释了一下Javascript的设计原理,给人醍醐灌顶的感觉。希望你能从中能学到知识,如果内容有误,还请指出,多谢!
翻译:
上网一搜,遍地都是写Javascript的原型继承这一概念的文章。但,Javascript实际上只是提供了一种方式,使得Javascript表现得像是其它语言中的原型继承——这个方式就是装饰符 new
。因此,网上大部分的解释很难理解,甚至更让你困惑。这篇文章就是解释了Javascript中的原型继承,和它到底是什么使用的。
原型继承定义
当你读到关于Javascript原型继承的文章时,时常看到它的解释:
当访问一个对象的属性时,Javascript会一直顺着原型链向上查找,直到找到这个属性名的值,或者在链的顶层都没找到,即为undefined。复制代码
一般情况下,我们会使用__proto__
属性来表示原型链中上一层的对象。我们来看看__proto__和prototype的不同。
注:__proto__不是一个正式、标准的属性,不应该出现在你的代码里。 这篇文章里,它只是用来解释Javascript中原型继承怎么实现的。复制代码
下面代码说明了Javascript引擎如何查找一个属性的(只是为了通俗易懂的解释,不代表Javascript中是这么写的)。
function getProperty(obj, prop) { if(obj.hasOwnProperty(prop)) { return obj[prop]; else if(obj.__proto__ !== null) return getProperty(obj.__proto__, prop); else return undefined}复制代码
我们来看个例子:一个抽象类 —— 坐标点 Point
,它有两个坐标值x,y
和一个方法print
。
按照上面的方法 getProperty,我们来实现下原型继承。我们可以定义一个对象Point,有三个属性:x,y,print。要想生成一个新的point,我们只需要将新的Point的__proto__设置成为Point就可以了。(创建了一个原型链,Point为原型链上的上层)
var Point = { x:0, y:0, print: function() {console.log(this.x,this.y);}};var p = { x: 10, y: 20, __proto__: Point}p.print(); //10,20复制代码
Javascript中诡异的继承
奇怪的是,在上面那个定义下写出来的代码是不成立的。实际使用的方式时下面这种:
function Point(x,y) { this.x = x; this.y = y;}Point.prototype = { print: function() { console.log(this.x,this.y); }};var p = new Point(10,20);p.print();复制代码
这个上面的代码完全不同。Point现在是一个函数,我们用了prototype
属性,还用了new
装饰符。怎么回事?
new
是怎么工作的?
Brendan Eich设计Javascript时,参考了当时流行的面向对象语言C++、Java,借鉴了继承的定义方式 new:
:new来生成一个类的实例。
- C++有构造函数的概念,用来初始化实例的属性。因此,new装饰符必须指向一个函数。
- 我们需要找个地方放置公共方法、公共属性。由于我们正在使用的是一个继承性的语言,那么就把它放在函数的一个属性里,名字是prototype。
new装饰符后面跟着一个函数F,参数arguments:new F(arguments...)。它只做了简单的三步:
- 生成类的实例。就是一个只有一个属性__proto__的对象。__proto__的值为F.prototype
- 初始化实例。函数F被调用,且
this
指向该实例。 - 返回这个实例。
现在,我们知道了new做了什么,最后生成了什么,我们用Javascript来演示一下new方法。
function New(f) { var n = {__proto__: f.prototype}; //1 return function() { f.apply(n, arguments); //2 return n; //3 }}//这里可能不好理解,实际上作者是这么设置的,new和函数名称先执行,返回一个函数后,执行这个函数,即:var f = (new F)(10,10);先执行前一个函数,然后再执行后面的入参。复制代码
再举一个小例子,帮助大家理解
function Point(x,y) { this.x = x; this.y = y;}Point.prototype = { print: function() {console.log(this.x,this.y);}}var p1 = new Point(10,10);p1.print(); //10,10console.log(p1 instanceof Point);var p2 = new Point(20,20);p2.print(); //20,20console.log(p2 instanceof Point);复制代码
Javascript中真正的原型继承
Javscript规范中,只提供了new装饰符的使用方式。然后,Douglas Crockford使用了一种方式,让new去做真正的原型继承的工作。那就是重写Object.create()。
Object.create = function(parent) { function F() {} F.prototype = parent; return new F();}复制代码
看起来比较奇怪,实际上它做的工作很简单。就只是创建了一个新对象,它的prototype可以设置为任意值。如果可以使用__proto__的话,它可以更加简略:
Object.create = function(parent){ return { __proto__: parent };}复制代码
还是上面Point的例子,我们用Object.create来实现:
var Point = { x:0, y:0, print: function() {console.log(this.x,this.y);}};var p = Object.create(Point);p.x = 10;p.y = 20;p.print(); //10 20复制代码
总结
上面已经讲清楚了什么是原型继承,Javascript是怎样通过一种特定的方式完成原型继承的。
然而,真正实现继承的方式(Object.create和__proto__)有些缺点:
- 规范不允许:__proto__不是标准的属性,甚至是被反对使用的。原生的Object.create()和douglas Crockford的使用也是不完全相同的。
- 不是最优化方案:Object.create(原生抑或自定义的),远没有使用new的性能好。甚至能慢到10倍以上。