js词法作用域----静态作用域

学习自《你不知道的javascript》和:https://github.com/mqyqingfeng/Blog/issues/3

js采用词法作用域(lexical scoping),也就是静态作用域!!!

词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。即编写代码的时候作用域就定义好了而不是调用的时候才开始定义。

核心:函数的作用域在函数定义的时候就决定了!!!(很重要)

一、词法作用域的浅略理解

以下例子来自《你不知道的JavaScript》

1
2
3
4
5
6
7
8
function foo(a) {
var b = a * 2;
function bar(c) {
console.log(a, b, c)
}
bar( b * 3 );
}
foo(2); // 2,4,12

上面的例子有三个逐级嵌套的作用域。
由内到外排列分别是: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,即从最内部作用域不断往外部作用域查找所需要的变量,如果找到就停止否则一直找到最外部作用域。
      还有一个值得注意的是,如果嵌套作用域或者父子作用域中存在相同变量名,当引擎需要解析编译该变量名时,从需要解析编译的位置所属作用域开始从内往外查找,直到找到第一个符合的停止。。

  简单来讲就是作用域查找会在找到第一个匹配的标识符时停止。

例如:

1
2
3
4
5
6
7
8
9
10
var a = 1
function foo() {
var a = 2;
function bar() {
var a = 3;
console.log(a)
};
return bar()
}
foo() // 3

      对照上面代码就很好理解了。其实这里涉及到作用域链以及闭包的知识。代码中的bar()外部一般情况下无法访问,但是通过return关键字然后调用父亲foo()便能访问的到。
      书中给该特性起了个名字叫遮蔽效应
      全局作用域中的a可以通过window.a访问到。其实定义在全局作用域内的变量即方法,都可以认为是window的属性或方法。(注意是浏览器情况下)
      如果我们直接在浏览器控制台输出bar(),将会报错Uncaught ReferenceError: bar is not defined。这是因为bar()是写在foo()函数内的,它的爸爸是foo()而不是全局对象(浏览器下是window)。儿子不能越俎代庖,你也可以理解为等级分明。bar()编写时就在foo()内部,所以它的爸爸或者父级作用域以及自己的作用域都定下来了,几乎不能更改。

所以这里有一个非常重要的点,需要牢记的是无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。

二、欺骗词法

人是不完美的,我一直坚信不完美的人造不出完美的东西,而且正因为存在不完美所以更有奋斗动力。

js有两种办法做到在运行时’修改’词法作用域,分别是evalwith。但是,欺骗词法作用域会导致性能下降。

eval

首先,eval接收一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。
还是用书上的例子:

1
2
3
4
5
6
function foo(str, a) {
eval(str) // 欺骗
console.log(a,b)
}
var b = 2
foo("var b = 3;", 1) // 1,3

      一开始我看到这样的代码也感到不可思议。foo()里面需要打印ab,传过来的参数只有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方法中就有

1
2
3
if (eval(classname)) {
win = Ext.create(classname, config);
}

  默认情况下,如果eval()中所执行的代码包含有一个或多个声明(无论是变量还是函数),就会对eval()所处的词法作用域进行修改。
  严格模式下不能修改作用域

with,不推荐用,自己了解

性能方面

      书中作出了解释,我段位不够,直接copy过来吧。。。
      JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。
      但如果引擎在代码中发现了eval()with,它只能简单的假设关于标识符位置的判断都是无效的,因为无法在词法分析阶段明确知道eval()会接收到什么代码,这些代码会如何对作用域进行修改,也无法知道传递给with用来创建新词法作用域的对象的内容到底是什么。
      最悲观的情况是如果出现了eval()with,所有优化可能都是无意义的,因此最简单的做法就是完全不做任何优化。
      如果代码中大量使用eval()with,那么运行起来一定会变得非常慢。无论引擎多聪明,试图将这些悲观情况的副作用限制在最小范围,也无法避免如果没有这些优化,代码会运行得更慢这个事实。

Newer Post

公司内部Ext框架使用注意事项或者叫开发指南

以下内容不全是我一个人理解分析的,感谢前辈们: 陈德林、徐冬冬、大许磊、沈梁以及黄建雄的帮忙! 公司内部Ext框架是老板在Ext基础上封装好的,使用起来极为方便,基本上就是复制粘贴改数据,但前提是你知道在哪里改。 导航 显示页面 框架学习 OnBeforeFormLoad() tools.Set …

继续阅读
Older Post

学习监听对象属性值改变

首先非常感谢:http://hcysun.me/2016/04/28/JavaScript%E5%AE%9E%E7%8E%B0MVVM%E4%B9%8B%E6%88%91%E5%B0%B1%E6%98%AF%E6%83%B3%E7%9B%91%E6%B5%8B%E4%B8%80%E4%B8%AA%E …

继续阅读