这篇文章是翻译一篇文章《blog.vjeux.com/2011/javasc…》
这是法国前端工程师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,20Javascript中诡异的继承
奇怪的是,在上面那个定义下写出来的代码是不成立的。实际使用的方式时下面这种:
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,10
console.log(p1 instanceof Point);
var p2 = new Point(20,20);
p2.print(); //20,20
console.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倍以上。