1. prototype/__proto__/constructor
JS原型链和继承网上已经烂大街了,5毛可以买一堆,这里只提一下:
constructor:普通对象和函数对象都有,指向创建它的函数
prototype: 函数对象才有,指向构造函数的原型对象(另一个普通对象)
__proto__: 普通对象和函数对象都有,指向创建它的构造函数的原型对象
function Fun1(){};
Fun1.prototype.constructor == Fun1 //true
var f2 = new Fun1();
f2.__proto__ == Fun1.prototype //true
2. 对象实例的创建过程
当实例化新对象时,JS将使用与来自相同构造函数的先前对象相同的初始隐藏类创建它们。
当添加属性时,对象从一个隐藏类转换为另外一个隐藏类,通常遵循所谓的“转换树”中的先前转换。例如,如果我们有以下构造函数:
function fun1(){
this.a = 1;
this.b = 2;
}
创建过程大致如下(对于普通对象{a:1, b:2}也会有类似的过程):
fun1: M0 {}
|
this.a: M1 {a:?}
|
this.b: M2 {a:?,b:?}
- 初始化一个对象时(如: var o = new fun1()),会使用一个附加在fun1上的没有任何属性的初始隐藏类M0。
- 当添加一个属性a时,会从隐藏类M0转变为M1以描述新的属性a。
- 当再次添加属性b时,会得到M2来体现属性a和b。
在第二次用同样的构造函数创建一个新的对象实例时(如: var o2 = new fun1()),会复用M0,M1和M2。
这种机制有下面几个优点:
- 只有第一次创建对象实例时会有比较大的性能开销,接下来如果再次创建实例时,将会复用之前的缓存,速度很快。
- 这种方法创建的对象实例通常比采用创建整个属性字典的对象实例要小,仅需要存储值在对象实例中而不必存储属性相关信息。
- 使用唯一的位置来存储隐藏类,可以很快的被再次使用。
可以参考如下:
M0
{}
|
M1
{a:?}
/ \
M2 M3
{a:?,b:?} {a:?,c:?}
3. prototype的创建过程
与实例的创建过程不同,原型的创建过程并不会使用隐藏类,因为原型prototypes通常情况下是一个唯一的对象,并且不会被其他的不同对象所共享结构。
- 不同对象将拥有不同的原型,共享原型并不会带来性能上的收益,因此没有必要。
- 原型通常只会创建一次,为原型创建隐藏类只会带来很多无用的内存碎片。
Prototype创建有二个主要的阶段:
- 建立
原型在建立阶段使用dictionary键值对存储,速度非常快。对比隐藏类的建立过程,使用键值对存储不会切换到底层运行环境。 - 使用
任何对原型的访问,或者通过原型链的访问,都会切换到使用阶段。
function Foo() {}
// Prototype在'建立'模式
var proto = Foo.prototype;
proto.method1 = function() { ... }
proto.method2 = function() { ... }
var o = new Foo();
// 从'建立'模式切换到'使用'模式
o.method1();
// 同样切换到'使用'模式
proto.method1.call(o);
4. prototype优化
那么了解上面的原型创建过程有什么用呢?
JS很难在编译阶段进行代码分析,即使某个类被用作原型。当我们发现一个类被当做原型,如:
var o = {x:1};
func.prototype = o;
因为原型是可以改变的,因此以上代码并不能确定O被用作原型,除非直到所有步骤结束。
所以:JS不得不首先进行隐藏类的创建过程,并转化为原型建立过程,这非常消耗性能。
那么应该怎么做呢?如下:
var o = {};
func.prototype = o;
o.x = 1;
如果一个对象要作为原型,那么尽量在给对象添加属性之前就把该对象赋给原型属性。
更好的方式是下面这种:
func.prototype = Object.create(…);
func.prototype.method1 = …
func.prototype.method2 = …
最后,优雅的写法是:
var proto = func.prototype = Object.create(…);
proto.method1 = …
proto.method2 = …