作用域是什么
传统编译语言过程:
-
分词/词法分析 将字符生成字符串分解为有意义的代码块
-
解析/语法分析
将词法单元数组转换成由元素逐级嵌套的树结构数据-----
抽象语法树(AST) -
代码生成
将AST语法树转换成机器指令
但是JavaScript引擎要更为复杂,因为在语法分析和代码生成阶段还要对代码进行优化,冗余元素进行优化。而且给予JavaScript引擎优化的时间很短,毕竟是在每次执行前进行编译。
编译器
引擎在执行编译器的代码时,会进行对变量的查找 分为以下两种查找方式
- LHS(左查询)
- RHS(右查询)
左查询代表着对于变量的重新赋值
右查询代表着对于值的引用
为什么需要区分左右查询?
在非严格模式下
当在一个作用域中进行右查询 如果没有查到就会报出“ReferenceError”的异常
如果使用左查询时没有找到该变量,就会向上级作用域中查找,如果一直到全局作用域中还是没有该变量 就会声明一个该名称的变量并返还给当前调用的作用域。
词法作用域
作用域主要有两种工作模型
- 词法作用域 (绝大多数编程语言所采用 普遍)
- 动态作用域 (较小众编程语言仍在使用)
JavaScript属于词法作用域 ,在你写代码时将变量和作用域写在哪里来决定的。
欺骗词法
因为词法作用域是写代码期间声明位置所定义的,当在运行时修改作用域。即为欺骗词法作用域
平时不建议使用
主要有以下两种方法
-
eval()
function foo(str,a){ eval(str); // 欺骗 console.log(a,b) } var b = 2; foo("var b =3;",1) // 1,3而且使用修改作用域的操作会导致JavaScript编译优化失效,因为他不知道你会修改成什么样子。
-
with()
通常当做重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身
var obj={ a:1, b:2, c:3, } // 单调乏味的重复obj obj.a=2; obj.b=3; obj.c=4; // 简单快捷的方法 with(obj){ a:3; b:4 c:5; }例如下面:
function foo(obj){ with(obj){ a:1 } } var o1={ a:2 } var o2={ b:3 } foo(o1) console.log(o1.a) // 1 foo(o2) console.log(o2.a) // undefined console.log(a) // 1 // 泄漏到全局变量中了 因为上面使用了 左查询两者的区别: eval函数接收的参数是更改其所处的作用域,with函数 是为传递的参数声明一个全新的作用域。
性能
使用eval和with函数会严重影响性能,因为代码在编译时会进行底层优化,如果你的代码中更改了作用域会导致编译器无法知道变量的作用域,就会导致运行时间长。
函数作用域和块作用域
函数作用域含义:属于这个函数的全部变量都可以在整个函数的范围内使用及复用。
隐藏内部实现
就是为一个代码块增加函数声明,这样可以从原来作用域中重新圈起来。也叫最小授权或最小暴露原则。
主要作用就是为了不暴露变量,只有全局用到的才写在外面。还可以规避冲突,相同名字的变量。
函数作用域
使用函数声明定义作用域的好处是减少变量污染,但是声明函数时,就已经在全局作用域中声明了一个变量。也能严格的说这个函数名已经污染了作用域了。可以使用立即执行函数来解决。
函数式声明和表达式声明的区别就是看function是不是在开头。像立即执行函数就是表达式声明的。
匿名和具名
匿名函数虽然写起来很方便 但是具有以下几点缺点
- 匿名函数在这追踪栈中并不会显示具有意义的名称,使得调试变得困难。
- 如果没有函数名想要使用自身函数进行再次调用就只能使用arguments.callee 例如在递归中的使用。
- 匿名函数缺少可读性。
所以建议尽量为函数指定名称。
立即执行函数表达式(IIFE)
var a =2
(function foo(){
var a=3
console.log(a) // 3
})()
console.log(a) // 2
立即执行函数就是一个匿名函数被执行了。第一个括号将函数变成表达式,第二个括号执行了这个表达式。
另一种普遍的进阶用法是把他们当做函数调用并传递参数进去:
var a=2
(function IIFE(global){
var a=3
console.log(a) // 3
console.log(global.a) // 2
})(window)
console.log(a) // 2
还有另一种方法是倒置代码的运行顺序 看个人喜好
var a =3
(function(foo){
foo(window)
})(function foo(global){
console.log(global.a)
})
提升
只有声明本身会被提升,而赋值或其他运算逻辑会留在原地
每个作用域中都有提升这个操作。
函数声明会被提升,但是函数表达式并不会被提升。
函数优先
当同一个作用域中同时出现函数和变量,会优先提升函数,然后才是变量。(避免同个作用域中多个相同命名的变量和函数名)
作用域闭包
function foo(){
var a = 2;
function bar(){
console.log(a)
}
return bar
}
var bar =foo()
bar() // 2 这就是闭包的效果
foo函数执行后,在正常情况下,JavaScript的垃圾回收会自动进行回收,但是在后面将其内部的函数赋值给一个变量,然后进行调用。组织了垃圾回收,使得foo函数作用域一直存活。
闭包可以一直访问定义时的词法作用域。
循环和闭包
for(var i=0;i<=5;i++){
setTimeout(function(){
console.log(i)
},1000)
}
// 结果为6次6
为什么会出现6次6呢?
因为计时器会在 循环结束后才触发,而你的i变量是一个全局变量
相当于六个计时器共享一个变量i
而当i结束时的条件是6 所以循环六次打印6 最终结果就是6个6
那么如何才能改变这一现状呢?
在for循环中增加作用域空间,隔离每次循环的i变量。这里就想到了闭包
for(var i=0;i<=5;i++){
// 使用立即执行函数创建了个闭包环境
(function(j){
setTimeout(function(){
console.log(j)
})
})(i)
}
// 0
// 1
// 2
// 3
// 4
// 5
以上使用es6进行改写就更简单了
只要使用let定义变量就可以省去了闭包这个环节。
模块
模块就是闭包的完美实现方式。
模块模式具备以下两个必要条件
- 必须有外部封闭函数,,该函数必须至少被调用一次(每次调用都会生成新的模块实例)
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,且可以访问或者修改私有的状态。
附录
this词法
箭头函数
解除了当前函数中的this,继承至上一层的词法作用域。