JavaScript. The core.
1.对象 2.原型链 3.构造函数 4.执行上下文堆栈 5.执行上下文 6.变量对象 7.活动对象 8.作用域链 9.闭包 10.this值 11.总结 |
这篇文章是“ECMA-262-3 in detail”系列的一个摘要和总结。每一部分包含了对应章节的连接引用,所以你可以仔细去阅读得到一个更深刻的理解。适合的读者:资深程序员,专家。我们从探讨对象的概念开始,这也是ECMAScript的奠基石。
对象 |
ECMAScript,一个高度抽象的面向对象语言。它按原始方式处理对象,但当我们需要时,它也可以转化对象。
对象是只有一个原型对象的属性集合。这个原型是object或者null
让我们看一个对象的基本实例,对象的原型是通过内置属性[[Prototype]]
引用的。然而专门为了理解原型对象,在图中我们将会用_<internal-property>_
下划线:
代替双括号_proto_
。代码:
1 var foo={
2 x:10,
3 y:20
4 };
我们有了含有两个显式的自身属性和一个隐式的_proto_
属性的结构,这个_proto_
属性是foo原型的一个引用:
图1.一个含有原型的基本对象
这些原型需要什么?让我们思考完原型链的概念后来回答这个问题。
原型链 |
原型对象也仅仅是一个简单的对象,且他们也有自己的原型。如果一个原型对象有一个非null的引用指向它的原型,这样循环下去(译者注:到null终止)。这就形成所谓的原型链。
原型链是一个有限长的对象链,用来实现继承和共享属性的。
考虑这种情况,当我们有两个仅仅只在一些小的部分存在差异而其他绝大部分都相同的对象。为了设计一个更好的系统,很明显的,在单一的对象中我们会重用相似功能/代码,而不用去重复它。在基于类的系统中,这种代码重用方式叫基于类的继承-你把相似的功能放在类A中,然后构造从类A继承而来的类B和类C,他们都有自身的微小变化。
ECMAScript没有类的概念,然而代码重用方式也没有太大不同(尽管在某些方面它比基于类的方式更灵活),是通过原型链来实现代码重用的。这种继承被称为仿类继承(或者为了符合ECMAScript,叫原型链继承)。
类似在例子中用classesA,B和C。在ECMAScript中,你可以创建对象:a,b和c。所以对象a含有对象b和c的相同部分。b和c仅仅包含他们自身的属性或者方法。
1 var a={ 2 x:10, 3 calculate:function(z){ 4 return this.x+this.y+z 5 } 6 }; 7 var b={ 8 y:20, 9 _proto_:a 10 }; 11 var c={ 12 y:30, 13 _proto_:a 14 }; 15 //调用继承的方法 16 b.calculate(30);//60 17 c.calculate(40);//80
很简单吧。我们发现b和c获取了定义在对象a中的calculate
方法。准确的说是通过原型链获取的。
原理很简单:如果一个属性或者方法在对象自身中找不到(也就是说对象中没有一个这样的自身属性),然后就试图在原型链中寻找这个属性/方法。如果这个属性 在原型中没有找到,则考虑到这个原型对象的原型中查找,递推下去,也就是说贯穿整个原型链(在基于类的继承中是完全一样的,在解析一个继承的方法时-遍历 整个类链)。第一个找到的同名属性/方法被使用。所以,被找到的属性被称为继承属性。如果遍历完整个原型链,这个属性还是没找到,返回 undefined。
注意,在使用继承方法时,方法中的this值被设置为初始对象(译者注:调用对象),而不是方法存在的原型对象。也就是说,在上面例子中的this.y是从b和c获取的,而不是从a获取。然而,this.x是通过原型链机制从a中获取。
如果一个对象没有显式的定义其原型,缺省的_proto_值将指向Object.prototype
。Object.prototype
对象也有自身的_proto_,就是这个原型链的最终连接,其值为null。下面这幅图展示了a,b和c对象的继承结构:
图2.原型链
注意:ES5中标准化了一个基于原型继承的替代方法Object.create
函数:
1 var b=Object.create(a,{y:{value:20}}); 2 var c=Object.create(a,{y:{value:30}});
ES6中标准化了__proto__
,可以在对象初始化时使用
经常需要得到有相同或者类似结构(也就是有相同的属性集合),而其值又不同的一些对象。在这种情况下我们应该使用构造函数在特定模式下来创建对象。
构造函数 |
除了通过特定模式创建对象之外,构造函数还干了另外一件有用的事情-为新创建的对象自动设置一个原型对象。这个原型对象放在ConstructorFunction.prototype
的属性中。例如我们用构造函数重写前面例子中的b和c,因此,对象Foo.prototype
扮演着a(原型)的角色:
1 //一个构造函数 2 function Foo(y){ 3 //将会通过特定模式创建对象:他们会创建自身的"y"属性 4 this.y=y; 5 } 6 //"Foo.prototype"存放着新构建对象的原型引用。因此我们可以用它来定义共享/继承的属性和方法。和前面的例子一样,我们有: 7 //可继承属性"x" 8 Foo.prototype.x=10; 9 //可继承方法"calculate" 10 Foo.prototype.calculate=function(z){ 11 return this.y+this.x+z; 12 }; 13 //使用"pattern"Foo来创建我们的b,c对象 14 var b=new Foo(20); 15 var c=new Foo(30); 16 //调用继承的方法 17 b.calculate(30);//60 18 c.calculate(40);//80 19 //我们展示所期望的引用属性 20 console.log( 21 b._proto_===Foo.prototype,//true 22 c._proto_===Foo.prototype,//true 23 //"Foo.prototype"自动构造了特殊的"constructor"属性,它是构造函数自身的引用;实例b和c可以通过委托和检测他的构造函数来找到她 24 b.constructor===Foo,//true 25 c.constructor===Foo,//true 26 Foo.prototype.constructor===Foo,//true 27 b.calculate===b._proto_.calculate,//true 28 b.__proto__.calculate === Foo.prototype.calculate//true 29 );
这段代码可以用如下的关系图表示:
图3.构造函数和对象之间的关系
上图再一次表明每一个对象都有一个原型。构造函数的_proto_
为Function.prototype
,通过它(译者注:Function.prototype
)的 _proto_属性再次引用到
Object.prototype。再次重申,
Foo.prototype
仅仅只是Foo的一个显式属性指向b和c对象的原型。
这里你会找到不同OOP语言的范例和理论以及和ECMAScript的比较。Chapter
7.2. OOP. ECMAScript implementation致力于ECMAScript的OOP。
这个主题的完整和详细的解释在这个系列的第七节能找到。
现在,我们了解了对象的基础知识,然我们看看在ECMAScript中代码执行时如何实现的。这就是所谓的执行上下文堆栈,这里面的每一部分也可以抽象的表示为一个对象。是的,在ECMAScript的操作中处处充斥着对象的理念。
执行上下文堆栈 |
有三种类型的ECMAScript代码:全局代码,函数代码和eval代码。每一种代码都在其执行上下文中运行。只有一个全局上下文,可能有许多函数和eval执行上下文的实例。每次调用函数,进入函数的执行上下文并执行器函数代码。每次调用eval函数,进入eval的执行上下文并运行它的代码。
记住一个函数可能产生无限多个上下文环境,因为每调用一次函数(即使函数递归的调用自己)都会产生一个新的上下文:
1 function foo(bar){ 2 //调用同一个函数,由于不同的上下文状态(比如"bar"参数值)产生不同的上下文 3 foo(10); 4 foo(20); 5 foo(30); 6 }
一个执行上下文可能激活另一个上下文,例如。一个函数调用另一个函数(或者全局上下文调用全局函数),等等。逻辑上这是有一个堆栈来实现的,就是所谓的执行上下文堆栈。激活了另一个上下文的上下文叫做调用者。被激活的上下文叫做被调用者。同时被调用者也可以是其他被调用者的调用者(例如,一个函数在全局上下文调用,又调用了它的一些内部函数)。
当一个调用者激活(调用)一个被调用者,调用者暂停它的上下文并传递控制流进入被调用者。被调用者被压入栈中并成为当前(激活)的执行上下文。当被调用者 的上下文结束后,控制流回到调用者中,运行调用者的上下文处理(此时他可能有激活了其他上下文),直到结束。被调用者可能由于错误返回或调出。一个抛出二 没有没捕获的错误会跳出(从堆栈中弹出)一个或多个上下文。
例如。所有的ECMAScript程序运行时可以表示为一个执行上下文堆栈(EC),栈的顶部是一个激活上下文:
图4.一个执行上下文堆栈
程序开始时便进入全局执行上下文,栈的第一个元素也是最底部的元素。然后全局代码进行了一些初始化,创建了必要的对象和函数。在执行全局上下文时,它的代 码会激活其他(已经创建)函数,并进入到他们的执行上下文,向执行上下文堆栈汇总推入新的元素,一直下去。当初始化结束后,运行的系统会等待一些事件(例 如用户点击鼠标),这些事件会激活一些函数并进入一个新的执行上下文。
在下图中,有函数上下文EC1和全局上下文Global EC
,当全局上下文进入和跳出EC1时堆栈变化如下:
图5,执行上下文堆栈变化
这准确的描述了ECMAScript中的运行程序是如何管理代码的执行的。关于ECMAScript中执行上下文更详细的信息请阅读Chapter 1. Execution context.
执行上下文 |
执行上下文可以抽象的表示为一个简单的对象。每一个执行上下文都有一些必要的属性(可以叫做上下文状态)去追踪它对应代码的执行进程。下图展示了一个上下文的结构:
图6一个执行上下文结构
除了这三个必须的属性(变量对象,this值和作用域链),执行上下文依据具体实现可能有其他附加状态。让我们详细的考虑上下文的这些重要属性。