学习自《你不知道的javascript》和:https://github.com/mqyqingfeng/Blog/issues/3
js采用词法作用域(lexical scoping),也就是静态作用域!!!
词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。即编写代码的时候作用域就定义好了而不是调用的时候才开始定义。
核心:函数的作用域在函数定义的时候就决定了!!!(很重要)
一、词法作用域的浅略理解
以下例子来自《你不知道的JavaScript》
上面的例子有三个逐级嵌套的作用域。
由内到外排列分别是:bar()
作用域 —— foo()
作用域 —— 全局作用域
我们通过上面代码能看到的是:
- 全局作用域里只有一个函数
foo
foo()
作用域里有变量b
,函数bar
,以及foo()
的形参a
,一共三个标识符。bar()
作用域里只有一个形参c
以上是我们能从代码中看到的。在函数foo
定义完成后通过foo(2)
传参调用,按照我们开发的思维,把2传进去一步一步走,到了function bar(c)
时,我们需要找到调用bar()
的地方,就在下方不远处发现是通过bar(b*3)
来调用的,然后进入bar()
内部,直接就是打印输出了。这是我们一般开发的思路。
引擎遇到console.log(a,b,c)
时会从内往外找a
,b
,c
,即从最内部作用域不断往外部作用域查找所需要的变量,如果找到就停止否则一直找到最外部作用域。
还有一个值得注意的是,如果嵌套作用域或者父子作用域中存在相同变量名,当引擎需要解析编译该变量名时,从需要解析编译的位置所属作用域开始从内往外查找,直到找到第一个符合的停止。。
简单来讲就是作用域查找会在找到第一个匹配的标识符时停止。
例如:
对照上面代码就很好理解了。其实这里涉及到作用域链以及闭包的知识。代码中的bar()
外部一般情况下无法访问,但是通过return
关键字然后调用父亲foo()
便能访问的到。
书中给该特性起了个名字叫遮蔽效应。
全局作用域中的a
可以通过window.a
访问到。其实定义在全局作用域内的变量即方法,都可以认为是window
的属性或方法。(注意是浏览器情况下)
如果我们直接在浏览器控制台输出bar()
,将会报错Uncaught ReferenceError: bar is not defined
。这是因为bar()
是写在foo()
函数内的,它的爸爸是foo()
而不是全局对象(浏览器下是window
)。儿子不能越俎代庖,你也可以理解为等级分明。bar()
编写时就在foo()
内部,所以它的爸爸或者父级作用域以及自己的作用域都定下来了,几乎不能更改。
所以这里有一个非常重要的点,需要牢记的是无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。
二、欺骗词法
人是不完美的,我一直坚信不完美的人造不出完美的东西,而且正因为存在不完美所以更有奋斗动力。
js有两种办法做到在运行时’修改’词法作用域,分别是
eval
和with
。但是,欺骗词法作用域会导致性能下降。
eval
首先,eval
接收一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。
还是用书上的例子:
一开始我看到这样的代码也感到不可思议。foo()
里面需要打印a
和b
,传过来的参数只有a
,并没有b
,内部也没有,只有全局作用域内有且值为2,可是打印出来居然是3,这就真的很奇怪了。
我试着把eval(str)
注释掉,之后打印输出1,2。然后我又把var b = 2
注释掉,报Uncaught SyntaxError: Unexpected end of input
。
结合这段代码理解开头那句话就容易得多了。这里eval
的功能相当于在foo()
内部加了个var b = 3
。(这里其实是在foo()
作用域内动态添加局部变量)
而在实际开发中,eval()
通常被用来执行动态创建的代码。例如公司封装的Ext框架中就有很多地方用到,比如tools.js里面的tools.GetPopupWindow
方法中就有
默认情况下,如果
eval()
中所执行的代码包含有一个或多个声明(无论是变量还是函数),就会对eval()
所处的词法作用域进行修改。
严格模式下不能修改作用域
with,不推荐用,自己了解
性能方面
书中作出了解释,我段位不够,直接copy过来吧。。。
JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。
但如果引擎在代码中发现了eval()
或with
,它只能简单的假设关于标识符位置的判断都是无效的,因为无法在词法分析阶段明确知道eval()
会接收到什么代码,这些代码会如何对作用域进行修改,也无法知道传递给with
用来创建新词法作用域的对象的内容到底是什么。
最悲观的情况是如果出现了eval()
或with
,所有优化可能都是无意义的,因此最简单的做法就是完全不做任何优化。
如果代码中大量使用eval()
或with
,那么运行起来一定会变得非常慢。无论引擎多聪明,试图将这些悲观情况的副作用限制在最小范围,也无法避免如果没有这些优化,代码会运行得更慢这个事实。