一个面试题以它来详细介绍js作用域
代码如下 | 复制代码 |
function foo(){ |
我们就直接针对这输出按顺序进行分析,
执行顺序
代码如下 | 复制代码 |
1.foo.prototype.abc = function(){alert('Alisoft');} //这样之后,我们就可以用obj.abc();不懂prototype原型的,可以看这个prototype原型继承 2.foo.abc = function(){alert('Yahoo');} //alert yahoo 3.var obj = new foo(); //创建一个foo的实例obj,同时,执行了foo函数,也就是obj.abc() = function(){alert('Alimama')} 4.foo.abc = function(){alert('Alibaba')} //foo.abc是foo类的静态方法,在实例化foo后执行了代码片段foo.abc = function() //覆盖了原来的foo.abc = function(){alert('Yahoo');},所以foo.abc()输出alibaba 5.this.abc = function(){alert('Alimama')} //这句话把function(){alert('Alimama')赋给了obj.abc,所以obj.abc输出alimama 6.abc = function(){alert('Alipay')}; var abc = function(){alert('Taobao')}; //这两句一起分析,如果没有下一句,那么abc是个全局变量,abc输出alipay //但是因为下一句var abc之后,abc的作用域被限制在foo类里,所以外部的abc()会显示未定义.
|
那么,答案就是
alimama
alibaba
undefine
闭包:表示能访问和操作其他本地作用域的变量的表达式(通常是函数)。
作用域
表示变量或函数起作用的区域,指代了它们在什么上下文中执行。Javascript总作用域一共有两种:全局作用域和本地作用域。在Javascript中本地作用域是按照函数来区分的。
代码如下 | 复制代码 |
var global = "11"; //全局作用域 function fun() { var local = "22";//本地作用域1 } function fun2() { var local2 = "22";//本地作用域2 for (var i=0;i<100;i++) { //本地作用域2 } }
|
不象Java语言那样for循环也是一个作用域,在Javascript中本地作用域是按照函数来区分的。Javascript的作用域概念类似词法作用域。
词法作用域:也叫做静态作用域,变量的作用域是在定义时决定而不是执行时决定。即词法作用域取决于源码,通过静态分析就能确定。
作用域链
与“原型链”类似,Javascript的作用域链也是按有顺序查询的。在访问变量时,先查看本地作用域是否有此变量,如果没有则依次向上一级作用域查找直到全局作用域。如果全局作用域也没有此变量,那么将会在全局作用域中声明这个变量。如下的代码:
代码如下 | 复制代码 |
function f() { name = "global"; var local = "local"; } f(); console.log(name); //global console.log(local);//undefine |
name变量被声明在全局作用域。
上下文
又可以理解为上下文对象,表示当前代码执行时所处的环境。即是this变量所指代的对象;这么理解比较困难,直接看个例子:
代码如下 | 复制代码 |
function Test() { console.log(this); } Test(); //window new Test();//Object |
在执行Test()时,此时的上下文对象是window,即Javascript的全局对象!在执行new Test();时新建了一个Object,此时执行Test函数的上下文对象就是Object。如果还不理解,可以看我的如何用Javascript实现面向对象编程一文中有一段代码如下:
代码如下 | 复制代码 |
function newObj(Fun,arguments) { var o = {}; if (Fun && typeof Fun === "function") { o.__proto__ = Fun.prototype; Fun.apply(o, arguments); return o; } }
|
这主要是new函数操作的模拟,可以看到Fun.apply(o,arguments);这一步,apply的作用是指定执行Fun函数并制定其上下文为o,输入参数为arguments对象。所以new Test()得到的结果是Object。
闭包
表示能访问和操作其他内部作用域的变量的表达式(通常是函数)。用简单的语言描述就是:能操作其他本地作用域变量的函数就是闭包。还是看代码:
代码如下 | 复制代码 |
function fun() { var i = 1; return function() { i++; console.log(i); } } var closure = fun(); closure(); //2 closure(); //3 |
请先按Java语言或其他非闭包语言的逻辑思考。当fun函数执行完毕时,其内部变量i应该会被释放,当closure函数再此执行并调用i时输出错误(因为i未被声明)。但是在Javascript中却打印出了值,这一切都归功于闭包。在执行fun()函数时,闭包就被创建。此时i并没有被释放,closure依然可以被访问到。
另外注意闭包的本质是表达式(通常是函数),所以闭包是在函数生成时定义的。(上面代码的第3行定义闭包)!
闭包的作用
闭包最大的作用就是来阻止外部程序随意访问内部变量,只能通过提供的接口来访问和操作。如下面一个记数器的实现:
代码如下 | 复制代码 |
var counter = (function() { var i=0; return { add:function() { i++; return this; }, get:function() { return i; } } })(); counter.add().add().add(); console.log(counter.get()); //3 |
如果不用闭包直接暴露变量,那就会出现如下的情况产生bug:
代码如下 | 复制代码 |
i=0; function add() { i++; } add(); add(); i+="1"; add(); console.log(i);//22 |
至于为什么产生这么奇怪的结果就要归功与+=和++这两个操作符了。
举一个简单的例子:
代码如下 | 复制代码 |
function test(){
}
var obj=function(){
var name='testObj';
}
obj.objTest=test;
test();
obj.objTest(); |
把这段代码放到HTML中运行这个页面,你会看到首先提示一个警告[object window],然后第二个警告
代码如下 | 复制代码 |
var obj=function(){
} |
我们先定义了一个test()方法,并在方法内部调用alert()方法将this显示出来,然后定义了一个obj函数对象,并给它加了一个私有的字段name,同时给它加了一个静态的方法objTest(),而这个函数则直接指向test()函数。
分别调用test()和obj.objTest()方法,第一次警告框提示的是Window对象,而第二次提示的是我们定义的obj这个函数的代码。这说明了test函数在两次执行的时候this的值是不同的!
这就说明了当调用函数的对象不同的时候,其内部的this关键字指代的对象是不同的。这里需要值得注意的是Javascript是基于对象的语言,当我们的变量或者函数定义在标签的根下的时候其实相当于给window对象加了相应的属性或方法,所以当我们利用function test(){}代码定义一个函数的时候,其实相当于给window对象添加了一个新的函数,即window.test()函数。
我们可以做一个实验:
代码如下 | 复制代码 |
function test(){
}
alert(test===window.test); |
警告框提示的将是true,这说明当我们在调用test()这个函数时相当于调用的是window.test()。所以当我们调用test()函数的时候调用这个函数的对象其实是window对象,this指代的是window对象,所以我们在alert(this)的时候弹出的警告窗口内容是[object Window]。我们将obj.objTest=test相当于把obj.objTest()指向test(),所以当我们调用obj.objTest()函数时相当于在obj调用了test()这个函数,所以现在this指代的是obj对象,提示的就是obj这个Function也就是我们看到的代码。
说到这应该也解释的差不多了,可能上面的例子太抽象,想象不出来它能在什么情况下用到,那我们现在就假设一个需求,做一个贴近实用一点的例子。
假设我们现在页面中的所有超链接在点击之后颜色要改为红色,用Javascript实现。大体的思路应该是获取页面中所有的标签,然后遍历所有的标签,给每一个注册一个click事件,事件触发后我们将它的color值设为red。
示例代码如下:
代码如下 | 复制代码 |
function changeColor(){
this.style.color='#f00';
}
//初始化,给所有 a 标签注册事件
function init(){
var customLinks=document.getElementsByTagName('a');
for(i in customLinks){
//你也可以使用事件侦听器方式来注册事件
//由于要兼容IE,FF等浏览器可能需要更多代码,您可以自行编写
customLinks[i].onclick=changeColor;
}
}
window.onload=init; |
将这段代码添加到HTML文档中,并在文档中添加一些超链接,当超链接点击后颜色会变成红色,这里我们定义的changeColor()函数中this关键字在点击超链接触发函数的时候它指代的是当前这个超链接。而如果你直接调用changeColor()函数浏览器会报错,提示Error: ‘this.style’ is null or not an object或者undefined之类的错误。
不知道说到这能不能让正在看文章的你对Javascript中的this关键字有了一些自己的了解呢?或者你已经不耐烦了?(:P)
其实要想真正对这个问题有更深入的理解那么必须对Javascript的作用域和作用域链有深入的理解。
作用域,顾名思义就是指某一属性或方法具有访问权限的代码空间,简单的说也就是这个变量或方法它在代码中的的适用范围。在大多数的OOP中主要有public,private,protect三种作用域,对着三种作用域在这里就不详细解释了,如果有OOP的经验应该都有深入的了解。在这里我要说的是这三种作用域类型对Javascript来说几乎是毫无意义的,因为Javascript中只有一种公共作用域,在Javascript中作用域是在函数中进行维护的。举个例子:
代码如下 | 复制代码 |
var test2='example variable';
alert(test1);
alert(test2);
}
example();
alert(test1);
alert(test2);
|
根据我们前面解释的,这里的test1变量相当于window的一个属性,所以它会在整个window作用域内起作用,而test2则在example()函数的内部声明,所以它的作用域也就维持在example()方法的内部,如果在函数的外部调用test2浏览器会提示出错。而在example()内部调用test1则没问题。
根据这个我们再举一个例子:
代码如下 | 复制代码 |
function example(){
var test='example variable';
}
example();
alert(test);
|
这个例子运行会是什么结果呢?对,警告框会提示“globle variable”,因为example()函数内部的test变量其作用域只维持在内部,不会影响外部的test变量。如果我们将example()内部test变量的var关键字去掉呢?你可以自己试试。
说到这就有牵扯出另外一个概念,那就是作用域链的概念。作用域链就是可以确定变量值的路径。由上面一个例子可以看出,var关键字是用来维护作用域链的,如果变量使用了var关键字声明那么他就可以看作为作用域链的终点。同样函数的形参的定义也会起到类似的作用。
说到这你对this这个精灵古怪的家伙有了比较清晰的认识了吧?根据它简单的一个诠释,this总是指向调用它所在函数的对象,根据作用域和作用域链,我们会很清晰的确定this的真面目。临末尾再来一个开始那个例子的简单变化:
代码如下 | 复制代码 |
function test(){
}
var obj=function(){
var name='testObj';
}
obj.objTest=test;
obj.objTest2=function(){
test();
}
test();
obj.objTest();
obj.objTest2();
|
你猜会提示什么内容呢?你可以运行一下试试(:P);
既然this是根据调用其所在函数的对象的改变而改变的,那我们可不可以强制改变它的调用对象呢?答案是肯定的,以后的文章会介绍一下这部分内容,以及Javascript中不同类型的数据成员的实现方式,闭包等概念。