作用域和作用域链是JavaScript开发中经常遇到的问题,面试中也是常见问题。
JavaScript作用域
JavaScript作用域是变量与函数的可访问范围,作用域控制着变量与函数的可见性和生命周期。
全局作用域
全局作用域中的对象在代码中的任何地方都可以被访问到,有以下几种情况:
- 最外层函数
- 最外层函数外面定义的变量
- 所有未定义直接赋值的变量(即没有用var定义的变量)
- 所有window对象的内置属性(window.document、window.location等)
局部作用域
局部作用域中的对象只在固定的代码片段中可访问到,最常见的就是函数内部,在JS中,局部作用域也可以被称为函数作用域。
无块级作用域
上面说到JS中局部作用域也被称为函数作用域也许就会有人疑惑,块级作用域不也是局部的么?然而,JS中并没有块级作用域。即由花括号封闭的代码块没有自己的作用域,导致在运用for循环的时候会出现一些意想不到的事情,之后关于闭包的学习笔记会详细说明。
JavaScript作用域链
JS中一切都是对象,对象不仅拥有可以通过代码访问的属性还有一系列仅供JavaScript引擎访问的内部属性。
执行环境(execution context)
执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。该对象代码无法访问,但JavaScript引擎会在后台使用它。
全局执行环境是最外围的一个执行环境。某个执行环境中所有的代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数也随之销毁。
每个函数都有自己的执行环境,当执行流进入一个函数时(该函数被执行时),该函数的环境就会被推入一个环境栈中。函数执行完成后,栈将其环境弹出,把控制权返回给之前的执行环境。
作用域链
当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。该对象包含了函数的所有局部变量、命名参数、参数集合以及this。
作用域链中的下一个变量则来自下一个包含环境,这样,一直延续到全局执行环境。全局执行环境的变量对象始终都是作用域链中的最后一个对象。
程序执行时,标识符的解析始终是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终是从作用域链的前端开始,然后逐级向后回溯,知道找到标识符为止。
代码优化
从作用域链的结构可以看出,在执行环境的作用域链中,标识符所在的位置越深,读写速度就会越慢。因为全局变量总是存在于作用域链的最末端,因此在标识符解析的时候,查找全局变量是最慢的。所以,在编写代码的时候应尽量少使用全局变量,尽可能使用局部变量。一个好的经验法则是:如果一个全局对象被用了一次以上,则先把它存储到局部再使用。
延长作用域链
虽然执行环境的类型只有全局和局部两种,但是有些情况会延长作用域链,因为这些语句会在作用域的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。两种情况下会发生这种现象。就是当执行流进入下列任何一个语句时,作用域链会得到加长:
- try-catch语句的catch块
- with语句
with语句会将指定的对象添加到作用域链中,catch会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。
所以要尽量避免使用with语句,由于try-catch语句在代码调试和异常处理中非常有用,不建议完全避免使用它。可以通过优化代码来减少catch语句对性能的影响。一个很好的方法是将错误委托给一个函数处理,例如:
|
|
handleError方法是catch子句中唯一执行的代码。该函数接收异常对象作为参数,这样可以更加灵活和统一的处理错误。由于只执行一条语句,且没有局部变量的访问,作用域链的临时改变就不会影响代码性能了。