在网上看了一篇不错的JavaScript基础知识文章,感谢Realazy的辛苦翻译,后来才得知原来Realazy就是我同事的朋友,晕,世界真小。(PS:CSDN上的名人孟子e章竟然也是我同事的哥们,ft。。。)
看了这篇文章才发现越是基础的东西越能展示水平,这也就是为什么很多朋友向我索取原代码我都没有回复,并非不想分享,而是觉得自己的作品还不足以达到供别人学习的程度,还需努力。
引用:http://realazy.org/blog/2007/07/18/scope-in-javascript/
作 用域(scope)是JavaScript语言的基石之一,在构建复杂程序时也可能是最令我头痛的东西。记不清多少次在函数之间传递控制后忘记
this关键字引用的究竟是哪个对象,甚至,我经常以各种不同的混乱方式来曲线救国,试图伪装成正常的代码,以我自己的理解方式来找到所需要访问的变量。
这篇文章将正面解决这个问题:简述上下文(context)和作用域的定义,分析可以让我们掌控上下文的两种方法,最后深入一种高效的方案,它能有效解决我所碰到的90%的问题。
我在哪儿?你又是谁
JavaScript 程序的每一个字节都是在这个或那个运行上下文(execution
context)中执行的。你可以把这些上下文想象为代码的邻居,它们可以给每一行代码指明:从何处来,朋友和邻居又是谁。没错,这是很重要的信息,因为
JavaScript社会有相当严格的规则,规定谁可以跟谁交往。运行上下文则是有大门把守的社区而非其内开放的小门。
我们通常可以把这 些社会边界称为作用域,并且有充足的重要性在每一位邻居的宪章里立法,而这个宪章就是我们要说的上下文的作用域链(scope
chain)。在特定的邻里关系内,代码只能访问它的作用域链内的变量。与超出它邻里的变量比起来,代码更喜欢跟本地(local,即局部)的打交道。
具
体地说,执行一个函数会创建一个不同的运行上下文,它会将局部作用域增加到它所定义的作用域链内。JavaScript通过作用域链的局部向全局攀升方
式,在特定的上下文中解析标识符。这表示,本级变量会优先于作用域链内上一级拥有相同名字的变量。显而易见,当我的好友们一起谈论”Mike
West”(本文原作者)时,他们说的就是我,而非bluegrass singer 或是Duke professor,
尽管(按理说)后两者著名多了。
让我们看些例子来探索这些含义:
<script type="text/javascript">
var ima_celebrity = "Everyone can see me! I'm famous!",
the_president = "I'm the decider!";
function pleasantville() {
var the_mayor = "I rule Pleasantville with an iron fist!",
ima_celebrity = "All my neighbors know who I am!";
function lonely_house() {
var agoraphobic = "I fear the day star!",
a_cat = "Meow.";
}
}
</script>
我 们的全明星,ima_celebrity,
家喻户晓(所有人都认识她)。她在政治上积极活跃,敢于在一个相当频繁的基层上叫嚣总统(即the_president)。她会为碰到的每一个人签名和回
答问题。就是说,她不会跟她的粉丝有私下的联系。她相当清楚粉丝们的存在
并有他们自己某种程度上的个人生活,但也可以肯定的是,她并不知道粉丝们在干嘛,甚至连粉丝的名字都不知道。
而在欢乐市
(pleasantville)内,市长(the_mayor)是众所周知的。她经常在她的城镇内散步,跟她的选民聊天、握手并亲吻小孩。因为欢乐市
(pleasantville)还算比较大且重要的邻居,市长在她办公室内放置一台红色电话,它是一条可以直通总统的7×24热线。她还可以看到市郊外山
上的孤屋(lonely_house),但从不在意里面住着的是谁。
而孤屋(lonely_house)是一个自我的世界。旷恐患者时常
在里面囔囔自语,玩纸牌和喂养一个小猫(a_cat)。他偶尔会给市长(the_mayor)打电话咨询一些本地的噪音管制,甚至在本地新闻看到
ima_celebrity后会写些粉丝言语给她(当然,这是pleasantville内的ima_celebrity)。
this? 那是虾米?
每
一个运行上下文除了建立一个作用域链外,还提供一个名为this的关键字。它的普遍用法是,this作为一个独特的功能,为邻里们提供一个可访问到它的途
径。但总是依赖于这个行为并不可靠:取决于我们如何进入一个特定邻居的具体情况,this表示的完全可能是其他东西。事实上,我们如何进去邻居家本身,通
常恰恰就是this所指。有四种情形值得特别注意:
呼叫对象的方法
在经典的面向对象编程中,我们需要识别和引用当前对象。this极好地扮演了这个角色,为我们的对象提供了自我查找的能力,并指向它们本身的属性。
<script type="text/javascript">
var deep_thought = {
the_answer: 42,
ask_question: function () {
return this.the_answer;
}
};
var the_meaning = deep_thought.ask_question();
</script>
这 个例子建立了一个名为deep_thought的对象,设置其属性 the_answer为42,并创建了一个名为ask_question
的方法(method)。当deep_thought.ask_question()执行时,
JavaScript为函数的呼叫建立了一个运行上下文,通过”.“运算符把this指向被引用的对象,在此是deep_thought这个对象。之后这
个方法就可以通过this在镜子中找到它自身的属性,返回保存在 this.the_answer中的值:42。
构造函数
类似地,当定义一个作为构造器的使用new关键字的函数时,this可以用来引用刚创建的对象。让我们重写一个能反映这个情形的例子:
<script type="text/javascript">
function BigComputer(answer) {
this.the_answer = answer;
this.ask_question = function () {
return this.the_answer;
}
}
var deep_thought = new BigComputer(42);
var the_meaning = deep_thought.ask_question();
</script>
我 们编写一个函数来创建BigComputer对象,而不是直白地创建
deep_thought对象,并通过new关键字实例化deep_thought为一个实例变量。当new
BigComputer()被执行,后台透明地创建了一个崭新的对象。呼叫BigComputer后,它的this关键字被设置为指向新对象的引用。这个
函数可以在this上设置属性和方法,最终它会在BigComputer执行后透明地返回。
尽管如此,需要注意的是,那个
deep_thought.the_question()依然可以像从前一样执行。那这里发生了什么事?为何this在the_question内与
BigComputer内会有所不同?简单地说,我们是通过new进入BigComputer的,所以this表示“新(new)的对象”。在另一方面,
我们通过 deep_thought进入the_question,所以当我们执行该方法时,this表示
“deep_thought所引用的对象”。this并不像其他的变量一样从作用域链中读取,而是在上下文的基础上,在上下文中重置。
函数呼叫
假如没有任何相关对象的奇幻东西,我们只是呼叫一个普通的、常见的函数,在这种情形下this表示的又是什么呢?
<script type="text/javascript">
function test_this() {
return this;
}
var i_wonder_what_this_is = test_this();
</script>
在这样的场合,我们并不通过new来提供上下文,也不会以某种对象形式在背后偷偷提供上下文。在此, this默认下尽可能引用最全局的东西:对于网页来说,这就是 window对象。
事件处理函数
比普通函数的呼叫更复杂的状况,先假设我们使用函数去处理的是一个onclick事件。当事件触发我们的函数运行,此处的this表示的是什么呢?不凑巧,这个问题不会有简单的答案。
如果我们写的是行内(inline)事件处理函数,this引用的是全局window对象:
<script type="text/javascript">
function click_handler() {
alert(this); // 弹出 window 对象
}
</script>
...
<button id='thebutton' onclick='click_handler()'>Click me!</button>
但是,如果我们通过JavaScript来添加事件处理函数,this引用的是生成该事件的DOM元素。(注意:此处的事件处理非常简洁和易于阅读,但其他的就别有洞天了。请使用真正的addEvent函数取而代之):
<script type="text/javascript">
function click_handler() {
alert(this); // 弹出按钮的DOM节点
}
function addhandler() {
document.getElementById('thebutton').onclick = click_handler;
}
window.onload = addhandler;
</script>
...
<button id='thebutton'>Click me!</button>
复杂情况
让我们来短暂地运行一下这个最后的例子。我们需要询问deep_thought一个问题,如果不是直接运行click_handler而是通过点击按钮的话,那会发生什么事情?解决此问题的代码貌似十分直接,我们可能会这样做:
<script type="text/javascript">
function BigComputer(answer) {
this.the_answer = answer;
this.ask_question = function () {
alert(this.the_answer);
}
}
function addhandler() {
var deep_thought = new BigComputer(42),
the_button = document.getElementById('thebutton');
the_button.onclick = deep_thought.ask_question;
}
window.onload = addhandler;
</script>
很完美吧?想象一下,我们点击按钮,deep_thought.ask_question被执行,我们也得到了“42”。但是为什么浏览器却给我们一个undefined? 我们错在何处?
其 实问题显而易见:我们给ask_question传递一个引用,它作为一个事件处理函数来执行,与作为对象方法来运行的上下文并不一样。简而言之,
ask_question中的
this关键字指向了产生事件的DOM元素,而不是在BigComputer的对象中。DOM元素并不存在一个the_answer属性,所以我们得到的
是 undefined而不是”42″. setTimeout也有类似的行为,它在延迟函数执行的同时跑到了一个全局的上下文中去了。
这个问题会在程序的所有角落时不时突然冒出,如果不细致地追踪程序的每一个角落的话,还是一个非常难以排错的问题,尤其在你的对象有跟DOM元素或者window对象同名属性的时候。
使用.apply()和.call()掌控上下文
在
点击按钮的时候,我们真正需要的是能够咨询deep_thought一个问题,更进一步说,我们真正需要的是,在应答事件和setTimeout的呼叫
时,能够在自身的本原上下文中呼叫对象的方法。有两个鲜为人知的JavaScript方法,apply和call,在我们执行函数呼叫时,可以曲线救国帮
我们达到目的,允许我们手工覆盖this的默认值。我们先来看call:
<script type="text/javascript">
var first_object = {
num: 42
};
var second_object = {
num: 24
};
function multiply(mult) {
return this.num * mult;
}
multiply.call(first_object, 5); // 返回 42 * 5
multiply.call(second_object, 5); // 返回 24 * 5
</script>
在 这个例子中,我们首先定义了两个对象,first_object和second_object,它们分别有自己的num属性。然后定义了一个
multiply函数,它只接受一个参数,并返回该参数与this所指对象的num属性的乘积。如果我们呼叫函数自身,返回的答案极大可能是
undefined,因为全局window对象并没有一个num属性除非有明确的指定。我们需要一些途径来告诉multiply里面的this关键字应该
引用什么。而multiply的call方法正是我们所需要的。
call的第一个参数定义了在业已执行的函数内this的所指对象。其余
的参数则传入业已执行的函数内,如同函数的自身呼叫一般。所以,当执行multiply.call(first_object,
5)时,multiply被呼叫,5传入作为第一个参数,而this关键字被设置为first_object的引用。同样,当执行
multiply.call(second_object,
5)时,5传入作为第一个参数,而this关键字被设置为second_object的引用。
apply以call一样的方式工作,但可以让你把参数包裹进一个数组再传递给呼叫函数,在程序性生成函数呼叫时尤为有用。使用apply重现上一段代码,其实区别并不大:
<script type="text/javascript">
...
multiply.apply(first_object, [5]); // 返回 42 * 5
multiply.apply(second_object, [5]); // 返回 24 * 5
</script>
apply和call本身都非常有用,并值得贮藏于你的工具箱内,但对于事件处理函数所改变的上下文问题,也只是送佛到西天的中途而已,剩下的还是得我们来解决。在搭建处理函数时,我们自然而然地认为,只需简单地通过使用call来改变this的含义即可:
function addhandler() {
var deep_thought = new BigComputer(42),
the_button = document.getElementById('thebutton');
the_button.onclick = deep_thought.ask_question.call(deep_thought);
}
代 码之所以有问题的理由很简单:call立即执行了函数(译注:其实可以用一个匿名函数封装,例如the_button.onclick =
function(){deep_thought.ask_question.call(deep_thought);},但比起即将讨论的bind来,
依然不够优雅)。我们给onclcik处理函数一个函数执行后的结果而非函数的引用。所以我们需要利用另一个JavaScript特色,以解决这个问题。
.bind()之美
我 并不是 Prototype JavaScript
framework的忠实粉丝,但我对它的总体代码质量印象深刻。具体而言,它为Function对象增加一个简洁的补充,对我管理函数呼叫执行后的上下
文产生了极大的正面影响:bind跟call一样执行相同的常见任务,改变函数执行的上下文。不同之处在于bind返回的是函数引用可以备用,而不是
call的立即执行而产生的最终结果。
如果需要简化一下bind函数以抓住概念的重点,我们可以先把它插进前面讨论的乘积例子中去,看它究竟是如何工作的。这是一个相当优雅的解决方案:
<script type="text/javascript">
var first_object = {
num: 42
};
var second_object = {
num: 24
};
function multiply(mult) {
return this.num * mult;
}
Function.prototype.bind = function(obj) {
var method = this,
temp = function() {
return method.apply(obj, arguments);
};
return temp;
}
var first_multiply = multiply.bind(first_object);
first_multiply(5); // 返回 42 * 5
var second_multiply = multiply.bind(second_object);
second_multiply(5); // 返回 24 * 5
</script>
首 先,我们定义了first_object,
second_object和multiply函数,一如既往。细心处理这些后,我们继续为Function对象的prototype定义一个bind方
法,这样的话,我们程序里的函数都有一个bind方法可用。当执行multiply.bind(first_object)时,JavaScript为
bind方法创建一个运行上下文,把this置为multiply函数的引用,并把第一个参数obj置为first_object的引用。目前为止,一切
皆顺。
这个解决方案的真正天才之处在于method的创建,置为this的引用所指(即multiply函数自身)。当下一行的匿名函数
被创建,method通过它的作用域链访问,obj亦然(不要在此使用this,
因为新创建的函数执行后,this会被新的、局部的上下文覆盖)。这个this的别名让apply执行multiply函数成为可能,而传递obj则确保
上下文的正确。用计算机科学的话说,temp是一个闭包(closure),它可以保证,需要在first_object的上下文中执行
multiply,bind呼叫的最终返回可以用在任何的上下文中。
这才是前面说到的事件处理函数和setTimeout情形所真正需要的。以下代码完全解决了这些问题,绑定deep_thought.ask_question方法到deep_thought的上下文中,因此能在任何事件触发时都能正确运行:
function addhandler() {
var deep_thought = new BigComputer(42),
the_button = document.getElementById('thebutton');
the_button.onclick = deep_thought.ask_question.bind(deep_thought);
}
漂亮
分享到:
相关推荐
JavaScript 首先在当前作用域中查找变量,如果没有定义,则会继续查找上一层作用域,直到找到全局对象。在上面的代码中,我们定义了一个全局变量 rain,并在 rainman 函数中调用 inner 函数, inner 函数中没有定义 ...
接着查找`fruit`,不在当前作用域,于是继续在全局作用域中找到`fruit`并使用。 而在全局环境中,当尝试访问`fruit`和`color`时,由于全局作用域的变量对象只包含`fruit`,所以`color`会导致一个`undefined`的错误...
理解JavaScript变量作用域.pdf 本人还有几十本经典javascript书籍以及无数javascript资料,要的加我qq 568094881,本人网址:www.maoshanhai.com
在 JavaScript 中,执行环境和作用域是两个非常重要和基本的概念,理解了这两个概念对于 JavaScript 中很多脚本的运行结果就能明白其中的道理了。执行环境是一个概念,一种机制,用来完成 JavaScript 运行时在作用域...
JavaScript作用域是编程中至关重要的概念,它规定了变量和函数的可见性及生命周期。JavaScript主要有两种作用域:全局作用域和局部作用域。 全局作用域是指在代码的任何位置都可以访问的变量或函数,这通常包括在最...
首先,全局作用域是最外层的作用域,任何在这个作用域中声明的变量在整个脚本中都是可访问的。如果在函数外部声明一个变量,那么这个变量就是全局变量。全局变量在整个程序的生命周期中都存在,直到页面关闭才会消失...
深化理解javascript作用域其次篇之词法作用域和动态作用域_ 深化理解javascript作用域其次篇之词法作用域和动态作用域,是javascript中非常重要的一部分。理解词法作用域和动态作用域对javascript的编程至关重要。...
JavaScript中,作用域和上下文是理解代码执行逻辑的关键概念。作用域指的是变量和函数的可见性和生命周期,而上下文则关乎`this`关键字的值,它指示了当前代码执行的环境。 **作用域(Scope)** 1. **全局作用域**...
javascript的作用域和块级作用域概念理解.doc
JavaScript作用域是指在JavaScript代码中,变量、常量、对象和函数能够访问的范围。在编程中,变量和函数的使用都受到作用域的限制,决定了它们能够在哪些代码块中被引用。作用域有助于防止变量命名冲突,也使得程序...
作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理。今天这篇文章对JavaScript作用域和作用域链作简单的介绍,希望能帮助大家更好的学习JavaScript。任何程序...
因此,当调用`inner()`时,它可以在`func`的作用域中找到`funcX`变量。 5. 作用域链的动态性 虽然作用域链在函数定义时就已创建,但其内容可以随着执行过程而改变。例如,通过闭包,函数可以访问在其外部定义的变量...
这意味着,在方法执行时,Javascript引擎不会去寻找当前作用域中的变量,而是根据定义时的作用域来确定变量的值。 词法作用域的特点是,它取决于源码,而不是执行时的环境。这意味着,词法作用域可以在编译时被确定...
内容概要:本文详细介绍了 JavaScript 中的函数与作用域。首先解释了函数的基本概念,包括函数声明、函数表达式、匿名函数、箭头函数、函数参数与返回值。接着讨论了 JavaScript 中的全局作用域、局部作用域、块级...
前端面试题,包含JavaScript的闭包,作用域,原型,原型链,上下文环境以及DOM,BOM封装函数深度克隆,以及一些常见的·JS问题,试题简单但是容易混淆,作为前端工程师必考题
JavaScript作用域是编程中至关重要的概念,尤其是在JavaScript这种动态类型的脚本语言中。它规定了变量、函数以及其它标识符的可见性和生命周期,是代码组织和管理的关键元素。本资料"深入理解JavaScript作用域共12...
javascript的变量作用域,与平时使用的类C语言不同,例如C#中的代码: static void Main(string[] args) { if(true) { int number=10; } Console.WriteLine(number); } 这段代码进行编译,是无法通过的,...
JavaScript中的闭包、匿名函数和作用域链是编程中至关重要的概念,它们是理解JavaScript运行机制的关键。在本文中,我们将深入探讨这三个概念,并通过实际示例来展示它们的运用。 首先,我们来讨论“闭包”。闭包是...