javascript里的作用域是理解javascript语言的关键所在,正确使用作用域原理才能写出高效的javascript代码,很多javascript技巧也是围绕作用域进行的,今天我要总结一下关于javascript作用域的相关知识。
很多人使用javascript时候会把{}作为作用域的边界,所以我们可以看看下面的代码:
function ftn01(){ var i = 1; if (i == 1){ var a = "ok"; } console.log("a = " + a);// a = ok {var b = "bok";} console.log("b = " + b);// b = bok } ftn01();
我们发现变量a和b都能被打印出来,这就说明if下的{}和单独的{}并不能保护变量a和b的作用域范围,我们将上面的代码修改下:
function ftn01(){ var i = 1; if (i == 1){ var a = "ok"; } console.log("a = " + a);// a = ok {var b = "bok";} console.log("b = " + b);// b = bok } console.log(a); ftn01();
在firebug里会报出如下的错误:
可见函数的{}是可以保护变量的作用域的。
其实在javascript语言里只有函数是可以提供作用域,换句话说javascript里有且只有函数作用域,没有其他的作用域。因此要理解作用域必须从函数讲起,javascript里的函数同时也是一个对象,函数同时也是对象这句话让很多初学者误解,javascript这个特性和其他很多语言不太一样,要解释这个问题就必须从javascript创建函数的机制,javascript语言里有一个对象叫做Function,所有的函数都是该对象的实例,下面我们看看javascript里创建函数的三种方式:
var add01 = function add(a,b){ return a + b; } var add02 = function(a,b){ return a+ b; } var add03 = new Function("a","b","return a + b"); console.log(add01(1,2));// 3 console.log(add02(1,2));// 3 console.log(add03(1,2));// 3 console.log(typeof add01);//function console.log(typeof add02);//function console.log(typeof add03);//function
第一种方式叫做命名函数方式,第二种叫做匿名函数方式,第三种是直接使用Function对象来定义函数,三种方式是等价的,并且都可以用一个变量保存该函数。其实前两种创建函数的方式是第三种的变种,由此可以看到所有函数都是Function对象的实例。
函数的作用域功能是由函数内置的一个属性scope体现的,scope属性是一个类数组的集合,这个类数组的集合叫做函数的作用域链,作用域是函数的一个属性,作用域链其实就是该属性的数据类型。当我们创建一个函数时候,就是上面定义函数add01时候,函数的scope属性就会被同步创建,并且作用域链也会被构建,上面的例子里创建的函数都是属于全局的,因此作用域链只有一个元素即该类数组的length是1,类数组所包含的元素也是一个键值对类型,该元素包含所有全局变量例如:window,document,add01等等。
上面的例子里add01是函数的标识符,当add01(1,2)加上了小括号的时候就是执行该函数了,执行一个函数时候,函数会创建一个运行期上下文,英文全称是:execution context,运行期上下文是函数的一个内部对象和作用域一样也是不能被外部访问的,每个运行期的函数都会有一个自己独有的运行上下文,当函数执行完毕后,该函数的运行上下文也会被销毁。当函数执行的时候,首先会初始化函数自带的scope属性,然后将函数自带的scope里的作用域链复制到运行期上下文里,运行期上下文里也包含一个作用域的属性,该属性也是用一个作用域链的类数组表示,复制出来的函数的作用域链会放到运行期上下文的作用域链里,这一步做好后,运行期上下文会初始化函数内部的局部变量和命名参数例如add01函数里的a,b,把这些变量存储到一个活动对象的变量里,初始化完成后这个活动对象也会加入到运行期上下文的作用域链里,这个活动对象会放到运行期上下文作用域链的最前端。其实在函数执行完毕销毁的对象就是这个活动对象,活动对象被销毁了对应的运行期上下文也就被销毁,但是原来存储在函数里的作用域还是保留的。
作用域链的作用是对变量标识符进行解析,标识符就是变量的名字,例如add01、add02这些就是标识符,标识符的解析就是获取数据的位置或是如何存储数据。当函数执行时候,遇到每一个变量就会搜索运行期上下文的作用域链,这个过程都是从作用域链的头开始查找,也就是我上面说的从活动对象开始找起,如果找到与之对应的标识符,那么搜索过程便会停止,如果没有找到那么接着就会搜索作用域链的下一个对象,直到找到为止。不管是函数的作用域链还是运行期上下文的作用域链,链条的最后一个都是全局对象,其实全局对象是一个特殊的对象,如果我还是套用前面函数作用域的方式去理解全局对象,把一个网页当做一个最大的函数,这个函数对象就是window,网页的打开时这个window函数生命周期的开始,网页的关闭就是window函数的销毁,与window函数对应的还有一个全局的运行期上下文,那么用上面的理论理解全局变量应该就会简单多了。全局变量可以当做所有函数作用域的父作用域,当标识符解析到了全局变量时候,前面一定经过了n多个的作用域链中元素的遍历,因此到了遍历全局变量其实程序执行的效率是很低了,所以所有写高性能javascript代码的建议里都是要尽量减少全局变量的使用,如果非要使用全局变量也要在函数作用域内用一个局部变量替代全局变量,这是高性能javascript代码的一个基本要求,不过时下最新版的浏览器几乎都对这个特性进行了优化,访问全局变量不再像以前那么消耗性能了,不过ie的老版本的市场还是很大,而老版本的ie对全局访变量的访问那就是代码效率的毒瘤了。
下面我要讲讲闭包和作用域的关系,有很多人把闭包等价于作用域或者是作用域链,这个有一定的道理,但是如果真的以为闭包就是等价于的这些的话,这个等价于就是错误的,闭包在javascript语言里是一个特殊的函数,闭包产生于函数执行的时候,也就是函数的名字加上了小括号使用的时候,这个时候就会创建闭包或者叫做定义闭包,这个过程和函数创建作用域的过程一样,而闭包的作用域链就是函数的运行期上下文,当执行闭包或者说使用闭包的时候,那么就会构建出闭包的运行期上下文,这个时候闭包也会构建一个活动对象,这个活动对象被置于它的作用域链的最前端,同时闭包还包含执行函数的活动对象,但这个活动变量是在闭包活动变量的后面,这样就会导致函数销毁时候内存无法及时回收,造成大量的内存资源的占用,因此使用闭包是个十分消耗计算资源的应用,前面我讲到要尽量把全局变量用局部变量替换,其实碰到跨作用域的变量引用,都要将其用局部变量替代,这样代码的效率和安全性会更好。
还有很多童鞋认为this指针和作用域链没什么联系,这种理解是不正确的,其实this指针就是指向作用域链的上一级对象,但这种理解常常会误导某些人对this的理解,因为很多人在函数内部调用函数时候,发现被调用的函数this指针指向了全局变量,我们在实际开发里使用this指针最好是换个角度,this指针是调用某个函数上一级对象,例如obj.ftn(),那么ftn函数内部的this指针就是指向的obj,如果有个函数直接是ftn(),前面没有点,那么ftn函数里的内部指针就是window对象,this指针都是指向点号前面的对象,如果没有点号就是window对象,通过点号理解this指针比较方便,也不容易出错,但是通过作用域理解this为什么会有偏差了,原因就是全局作用域在作祟,在javascript里不管哪里直接调用函数,前面没有点的时候this都是指向全局的,我们不应该看这个方法是放到哪个函数里执行,其实this和作用域关系是紧密的,大家千万别怀疑这点。此外在作用域链的数组型的数据结构里,数组的每一个元素都含有一个this指针,this都是作为一个键值对预先定义好的,它的取值不由作用域链的创建所决定。因此有人认为this指针的使用其实也是跨域访问的观点也是不对,this指针的使用不存在跨域,它的效率也是非常高的。
理解了上面这个问题,那么我们对函数的apply和call方法深入理解就比较容易了,这两个方法的本质作用就是在特定的作用域里调用函数,这个解释比较抽象,这样我们先看下面的例子:
function test(){ console.log(this); } test.call(window);// window var obj = {}; test.call(obj);// object function ftn01(){ test.call(this); } ftn01();// window
这两个方法的第一个参数就是改变当前活动对象里this指针指向哪个对象,第一个参数就是函数this指针指向的对象,其本质也是改变作用域的一种方式,改变作用域的方法可以扩展方法的使用范围,因此调用对象和方法解耦,这样可以精简代码,提高代码的复用程度。
尽量少使用全局变量,多用局部变量是写高效javascript程序的一个基本要求,大家不要怀疑它会过时,不管浏览器如何演进,这点规则都是一条黄金规则。记住最优秀的javascript代码里全局变量最好只有一个。
最后我还要插一句,做过javascript都知道javascript代码压缩是一个提升网站效率的重要手段,我们使用雅虎或者谷歌的压缩代码的工具时候,会发现很多变量都会被A,B这样简单的代码所替换,这个替换规则都是对局部变量进行的,因此多使用局部变量会javascript压缩效果更好。