时间紧张,先记一笔,后续优化与完善。
1.1.1 摘要
在我们学习Javascript过程中,常常会遇到作用域(Scope)和执行上下文(Context)等概念。其中,执行上下文与this关键字的关系密切。
有面向对象编程教训的各位,对于this关键字再熟习不过了,因此我们很容易地把它和面向对象的编程方式联系在一起,它指向利用构造器新创建出来的对象;在ECMAScript中,也支撑this,然而, 正如大家所熟知的,this不仅仅只用来表示创建出来的对象。
在接下来的博文我们讲分析Javascript的作用域和执行上下文,以及它们的异同之处。
目录
1.1.2 正文
执行环境(Execution context)也称为“环境”是Javascript中最为重要的一个概念。执行环境定义了变量或函数有权拜访的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
看到了执行环境的定义有摇头昏了,简而言之“每个执行环境都有一个与之关联的变量对象”;这里我们有一个疑问就是这个变量对象是怎样定义的呢?
接下来,让我们看一下变量对象的定义,详细实现如下:
/** * Execution context skeleton. */
activeExecutionContext = {
// variable object.
VO: {...},
this
: thisValue};
通过下面的伪代码我们晓得对象字面量activeExecutionContext,它包含一个变量对象VO和this属性。
这说明了this与上下文的可执行代码类型有关,其值在进入上下文阶段就肯定了,并且在执行代码阶段是不能转变的(关于this使用可以浏览《Javascript this 的一些学习总结》)。
作用域(Scope)控制着变量和参数的可见性及生命周期。
简而言之,执行环境是基于对象的,而作用域是基于函数的。
作用域
我们将通过一个例子分析作用域的使用,首先,我们定义了一个函数FooA()和FooB,示例代码如下:
/** * Defines a function. */
var
FooA =
function
(){
var
a = 1;
var
FooB =
function
(){
var
b = 2; console.log(a, b);
// outputs: 1, 2
} console.log(a, b);
// Error! b is not defined
}FooA();
在示例中,第二个log输出变量为未定义,这是由于在Javascript中定义在函数里头的参数和变量在函数外部是弗成见的,而在一个函数外部任何位置定义的参数和变量,在该函数外部任何地方都是可见的。
执行环境
首先,我们定义了对象字面量o,它包含一个属性x和方法m(),示例代码如下:
/** * Defines a literal object. * @type {Object} */
var
o = { x:23, m:
function
(){
var
x = 1; console.log(x,
this
.x);
// outputs 1, 23
}}o.m();
示例中的两个变量和属性x都能被拜访,但它们被拜访的方式是一模一样,在log中拜访第一个x是通过作用域方式拜访了本地变量x,而this.x是通过执行上下文方式拜访对象o的属性x,因此输出值也不尽相同。
上下文问题
接下来,我们修改一下前面的例子,在方法m()中添加一个函数f(),示例代码如下:
/** * Defines a literal object. * @type {Object} */
var
o = { x:23, m:
function
(){
var
x = 1;
var
f =
function
(){ console.log(x,
this
.x);
// outputs 1, undefined
} f(); }}o.m();
下面,我们通过调用方法m()来输出x的值,由于方法m()的详细实现是通过调用函数f()来实现。
当我们调用对象o的方法m()时,发现this.x是未定义的。
这究竟是什么原因呢?回忆前面的例子,由于方法m()获得了对象o的上下文,所以this是指向对象o的,难道是函数f()没有获得对象o的上下文,因此它不清晰this指向哪个对象?
首先让我们回顾一下函数和方法以及属性和变量的区分:方法和对象关联,如:object.myMethod = function() {},而函数非对象关联:var myFunc = function {};一样属性也是对象关系的,如:object.myProperty = 23,而变量:var myProperty = 23。
因为我们提到上下文是基于对象的,所以函数f()不能获得对象o的执行上下文。
我们是否可以让函数f()获得对象o的执行上下文呢?我们细心地想一下,既然函数f()不清晰this指向的对象,那么可以直接调用对象的属性就OK了。
/** * Fixs broken context issue. * @type {Object} */
var
o = { x:23, m:
function
(){
var
x = 1;
var
f =
function
(){ console.log(x, o.x);
// outputs 1, 23
} f(); }}o.m();
我们在函数f()中直接调用对象o的属性x,这样函数f()就无需获得执行上下文直接调用对象的属性了。
现在,我们又遇到一个新的问题了,如果对象不是o而是p,那么我们就须要修改函数f()中的对象了,更严峻的情况就是我们没有办法肯定详细是哪个对象,示例代码如下:
/** * Defines a literal object. * @constructor */
var
C =
function
(){}C.prototype = { x:23, m:
function
(){
var
x = 1;
var
f =
function
(){ console.log(x,
this
.x);
// outputs 1, undefined
} f(); }}
var
instance1 =
new
C();instance1.m();
上下文实例问题
下面,我们定义了函数C和它的原型对象,而且我们可以通过new方式创建C对象实例instance1,按照前面的方法解决Broken Context问题,详细实现如下:
/** * Defines a literal object. * @constructor */
var
C =
function
(){}C.prototype = { x:23, m:
function
(){
var
x = 1;
var
f =
function
(){ console.log(x, instance1.x);
// outputs 1, undefined
} f(); }}
var
instance1 =
new
C();instance1.m();
如果我们在创建一个C的对象实例instance2,那么我们就不能指定函数f()中的对象了。
其实,this是对象实例的抽象,当实例有多个甚至成千上百个的时候,我们须要通过this引用这些对象实例。
因此,指定对象方法不能有效解决Broken Context问题,我们还是须要使用this来引用对象,前面我们讲到由于函数f()没有获得对象o的执行上下文,因此它不清晰this指向哪个对象,所以输出this.x未定义,那么我们是否可以让函数f()获得对象的执行上下文。
跨作用域的上下文
我们想想既然方法是基于对象的,而且可以获得对象的执行上下文,那么我们直接把f()定义为方法好了。
现在,我们在C对象原型中定义方法f(),示例代码如下:
/** * Defines a literal object. * @constructor */
var
C =
function
(){}C.prototype = { x:10, m:
function
(){
var
x = 1;
this
.f(); }, f:
function
(){ console.log(x,
this
.x);
// Reference ERROR!!
}}
var
instance1 =
new
C();instance1.m();
好啦,我们在C对象原型中定义方法f(),那么方法f()就可以获得对象的执行上下文。
现在,我们在Firefox运行以上代码,结果输出Reference ERROR,这究竟是什么原因呢?我们想了一下问题出于变量x中,由于方法f()不能获得方法m()的作用域,所以变量x不在方法f()中。
使用上下文解决作用域问题
我们处于两难的境地方法f()既要获得执行上下文,又要获得方法m()的作用域;如果我们要获得方法m()的作用域,那么我们须要把方法f()定义在m()中。
接下来,我们把方法f()定义在m()中,详细实现如下:
/** * Defines a literal object. * @constructor */
var
C =
function
(){}C.prototype = { x:23, m:
function
(){
var
x = 1;
this
.f =
function
(){ console.log(x,
this
.x);
// outputs 1, 23
}
this
.f(); }}
var
instance1 =
new
C();instance1.m();
现在我们通过this调用方法f(),它现在可以获得对象instance1执行上下文,并且也可以获得方法m()的作用域,所以方法f()可以获得属性和变量x的值。
使用作用域解决上下文问题
接下来,继承看一个例子,我们要在函数setTimeout()中调用方法onTimeout(),详细定义如下:
/** * setTimeout function with Broken Context issue * @type {Object} */
var
o = { x:23, onTimeout:
function
(){ console.log(
"x:"
,
this
.x); }, m:
function
(){ setTimeout(
function
(){
this
.onTimeout();
// ERROR: this.onTimeout is not a function
}, 1); }}o.m();
一样在函数setTimeout()中调用方法onTimeout()失败,我们晓得这是由于方法onTimeout()不能获得对象执行上下文。
我们晓得在方法m()中可以获得对象执行上下文,所以可以通过临时变量引用this指向的对象,实例代码如下:
/** * Fixs setTimeout function with Broken Context issue. * @type {Object} */
var
o = { x:23, onTimeout:
function
(){ console.log(
"x:"
,
this
.x);
// outputs 23
}, m:
function
(){
// Keeps instance reference.
var
self =
this
; setTimeout(
function
(){
// Gets m scrope.
self.onTimeout(); }, 1); }}o.m();
下面,我们通过临时变量self保存了this的引用,由于setTimeout()函数可以获得m()的作用域,所用我们可以通过self. onTimeout()的方式调用onTimeout()方法。
1.1.3 总结
本博文通过分析执行上下文和作用域的异同、this的使用以及变量对象,让我们加深对Javascript 语言特性的懂得。
首先,我们分析了执行上下文和this的的关系,并且执行上下文是拥有对象的;然后,分析了作用域使变量在作用域范围内可见,并且作用域是基于函数的。
我们通过详细的例子分析了在不同的作用域和执行上下文中,对this和变量的影响加深了作用域和执行上下文的懂得,从而帮助我们更好的浏览和编写代码。
参考
- http://blog.goddyzhao.me/post/10020230352/execution-context
- http://clubajax.org/javascript-scope-and-context-not-the-same-thing/
文章结束给大家分享下程序员的一些笑话语录: 神灯新篇
一个程序员在海滩上发现了一盏神灯。他在灯上擦了几下,一个妖怪就从灯里跳出来说:“我是世界上法术最强的妖怪。我可以实现你的任何梦想,但现在,我只能满足你一个愿望。”程序员摊开了一幅中东地图说:“我想让中东得到永久的和平。”妖怪答道:“哦,我没办法。自打创世纪以来,那里的战火就没有停息过。这世上几乎没有我办不到的事,但这件事除外。”程序员于是说:“好吧,我是一个程序员,为许多用户编写过程序。你能让他们把需求表述得更清楚些,并且让我们的软件项目有那么一两次按进度按成本完成吗?”妖怪说:“唔,我们还是来看中东地图吧。”