你不知道的JavaScript
作用域是什么
变量储存在哪里?
作用域中
程序需要时如何找到变量?
依据作用域的规则去调用变量
js实际上也是编译类型语言,但不是提前编译的
编译语言的编译过程(三步)
-
分词/词法分析
-
此过程会将字符组成的字符串分解成对编程语言来说
有意义的代码块
,这些代码块被称为词法单元
。- 例如
var a = 2;
会被分解为var
、a
、=
、2
、;
这几个词法单元
,空格是否会被当做词法单元取决于空格在语言中是否具有实际意义。
- 例如
-
分词
和词法分析
的区别在于词法单元
的识别是通过有状态
还是无状态
的方式进行的。- 例如
词法单元
判断a
是一个独立的词法单元
还是其他词法单元
的一部分时,调用的是有状态
的解析规则,这个过程就是词法分析
- 例如
-
-
解析/语法分析
-
将
词法单元流(数组)
转换成一个由元素逐级嵌套
所组成的代表了程序语法的结构的树(抽象语法树)
。-
例如
var a = 2;
的抽象语法树中可能有一个叫作VariableDeclaration 的顶级节点
,顶级节点下有一个叫作Identifier 值为 a
和一个叫作AssignmentExpression
的子节点
,AssignmentExpression
下有一个叫作NumericLiteral 值为 2
的子节点
。
-
-
-
代码生成
-
将抽象语法树转换成可执行代码的过程(与语言、目标平台等相关)
。- 例如平台通过某种方式将
var a = 2;
的抽象语法树
转化为一组机器指令
,用来创建
一个叫作a 的变量(包括分配内存等)
,并将一个值存储在a中
。
- 例如平台通过某种方式将
-
js与一般编译语言的区别
- js引擎不会拥有像其他语言编译器那么多的时间来进行优化,因为js的编译过程不是发生在构建之前的
- 对于js来说,大部分情况下编译是发生在代码执行前几微妙的时间内(甚至更短),例如js编辑器会对
var a = 2;
这段程序进行编译,然后做好执行他的准备,并且通常马上就会执行它。
作用域
引擎
:负责js程序的编译及执行过程
编译器
:负责在js代码执行前的编译
作用域
:负责收集并维护由所有生命的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限
编译过程
var a = 2;
- 遇到
var a
,编译器会在当前作用域中声明一个变量(如果之前没有声明过)。 - 在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值
a = 2
,否则就会抛出异常。
引擎查找变量的两种类型: LHS
与 RHS
LHS: 赋值操作的目标是谁
- 给查询到的变量进行赋值,如把2赋值给a变量,首先要查询a是否存在,这个时候用的就是LHS查询
RHS: 谁是赋值操作的源头
- 要获取到某个变量的值,如打印变量a,console.log(a); js引擎要去查询这个变量是否存在,得到变量的值,这个时候用的就是RHS查询
- 示例:
function foo(a) { console.log(a); // 2 } foo(2); // 引擎: 我说作用域,我需要为 foo 进行 RHS 引用。你见过它吗? // 作用域: 别说,我还真见过,编译器那小子刚刚声明了它。它是一个函数,给你。 // 引擎: 哥们太够意思了!好吧,我来执行一下 foo。 // 引擎: 作用域,还有个事儿。我需要为 a 进行 LHS 引用,这个你见过吗? // 作用域: 这个也见过,编译器最近把它声名为 foo 的一个形式参数了,拿去吧。 // 引擎: 大恩不言谢,你总是这么棒。现在我要把 2 赋值给 a。 // 引擎: 哥们,不好意思又来打扰你。我要为 console 进行 RHS 引用,你见过它吗? // 作用域: 咱俩谁跟谁啊,再说我就是干这个。这个我也有,console 是个内置对象。 给你。 // 引擎: 么么哒。我得看看这里面是不是有 log(..)。太好了,找到了,是一个函数。 // 引擎: 哥们,能帮我再找一下对 a 的 RHS 引用吗?虽然我记得它,但想再确认一次。 // 作用域: 放心吧,这个变量没有变动过,拿走,不谢。 // 引擎: 真棒。我来把 a 的值,也就是 2,传递进 log(..)。
作用域嵌套
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。
引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止
异常
function foo(a) {
console.log(a + b); // a + undefined
var b = a;
}
foo(2); // NaN
// 对 b 进行RHS时未找到,说明b是个“未声明”的变量,如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。
function foo(a) {
console.log(a + b);
b = a;
}
foo(2); // b is not defined
测试
function foo(a) {
var b = a;
return a + b;
}
var c = foo(2);
- 找出所有的 LHS 查询(这里有 3 处!)
c = ..;
a = 2(隐式变量分配)
b = ..
- 找出所有的 RHS 查询(这里有 4 处!)
foo(2..
= a;
a ..
.. b
词法作用域
作用域的两种工作模式
- 词法作用域(JavaScript)
- 动态作用域(如Bash脚本、Perl中的一些模式等)
词法作用域
词法作用域就是定义在词法阶段的作用域。
(词法作用域是写的代码将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。)
function foo(a) {
var b = a * 2;
function bar(c) {
console.log(a, b, c);
}
bar(b * 3);
}
foo(2); // 2, 4, 12
- 作用域分析
全局作用域
下,一个标识符:foo
foo作用域
下,三个标识符:a
,b
,bar
bar作用域
下,一个标识符:c
- 查找
- 作用域查找会在找到第一个匹配的标识符时停止。
- 在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。
- 作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。
欺骗词法(欺骗词法作用域)
欺骗词法作用域会导致性能下降。
eval (不推荐使用)
-
非严格模式下
function foo(str) { eval(str); // 欺骗! console.log(a); } var a = 2; foo("var a = 3;"); // 3
以上代码可以理解为在foo的作用域下创建了一个变量b,并遮蔽了外部(全局)作用域下的同名变量。
function foo(str) { var a = 3; // eval(str); console.log(a); } var a = 2; foo("var a = 3;"); // 3
-
严格模式下(严格模式下,eval在运行是将拥有自己的词法作用域,意味着其中的声明无法修改所在的作用域。)
function foo(str) { 'use strict'; eval(str); console.log(a); } var a = 2; foo("var a = 3;"); // 2
-
与eval相似的方法(
不提倡使用
)setTimeout('var a = 1;console.log(a);', 5000); // 1
setInterval('var a = 2;console.log(a);', 1000); // 2
new Function('var a = 3; console.log(a)')(); // 3
new Function(...)
构建方法比eval安全一些,但也不提倡使用
with (不推荐使用)
function foo(obj) {
with(obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo(o1);
console.log(o1); // {a: 2} a被修改
console.log(o1.a); // 2 a被修改
console.log(window.a); // undefined a没有被暴露到全局
foo(o2);
console.log(o2); // {b: 3} b未被修改
console.log(o2.a); // undefined 无法访问a
console.log(window.a); // 2 a被暴露到全局
不推荐原因
当浏览器发现
eval(..)
或with
,它只能假设关于标识符位置的判断是无效的,无法在词法分析阶段明确知道eval(...)
接收的是什么代码,无法知道with
创建的新的词法作用域的内容是什么。因此浏览器并不会对内部代码进行优化,如果大量使用eval(..)
和with
方法会使浏览器运行很慢,所以不推荐使用。
函数作用域和块作用域
函数中的作用域
属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。
function foo(a) {
var b = 2;
function bar() {
// ...
console.log(foo, a, b, c, bar); // success
}
var c = 3;
console.log(foo, a, b, c, bar); // success
}
console.log(foo); // success
console.log(a, b, c, bar); // fail
隐藏内部实现
应遵守
最小授权(最小暴露)
原则,指的是在软件设计中,应该最小限度地暴露必要内容,而将其他内容都隐藏
起来,比如某个模块或对象的API设计。
不推荐的方式
function funA(a) { b = a + funB(a); console.log(b); } var b; function funB(b) { return b; } funA(1);
推荐的方式
function funA(a) { var b; function funB(b) { return b; } b = a + funB(a); console.log(b); } funA(1);
规避冲突
规避定义变量、方法或其他属性的冲突,避免错误的赋值和使用。相关如下示例:
-
比如有一个需求需要打印全局作用域下的a,但是经过foo方法执行后a的值发生了变化。如下:
var a = 1; function foo() { a = 2; } foo(); console.log(a); // 2
-
修改结果如下:
// 方式一(推荐) var a = 1; function foo() { var a = 2; } foo(); console.log(a); // 1
// 方式二 var a = 1; function foo() { // b = 2 但b会暴露到全局作用域上 var b = 2; } foo(); console.log(a); // 1
函数作用域
var a = 1;
function foo() {
var a = 2;
console.log(a); // 2
}
foo();
console.log(a); // 1
以上代码虽然规避了变量冲突,但是全局上还存在foo方法污染着全局作用域,此时如果还想执行foo方法单不想污染到全局作用域,只需要执行一次此方法,还想打印全局作用域下的a,那么可以把foo改写成不具名函数。
var a = 1;
(function foo() {
var a = 2;
console.log(a); // 2
}())
console.log(a); // 1
匿名与具名
- 匿名
- 行内函数表达式即使具名也会被处理为匿名表达式
- 函数内部可使用
arguments.callee
进行调用自身
- 具名
- 带有名字的表达式,可以根据名字进行调用
立即执行函数表达式(术语:IIFE)
var a = 2;
(function IIFE(def) {
def(window);
})(function def(global) {
var a = 3;
console.log(a); // 3
console.log(global.a); // 2
});
块作用域
var
- 写在
for
、if
等中的变量污染到了全局变量,如果在其他方法中用到了同名变量就会严重影响代码的可维护性 - for
// for for (var i = 1; i <= 10; i++) { console.log(i); // 1 - 10 } console.log(i); // 11
- if
// if var isTrue = false; if (isTrue) { var a = 1; } else { console.log(a); // undefined } console.log(a); // undefined
- {}
// {} { var a = 2; } console.log(a); // 2
- with
// with var obj = { a: 1 } with (obj) { var a = 2; var b = 3; } console.log(obj, a, b); // {a: 2} undefined 3
- try/catch
// try/catch try { var a = 1; console.log(b); // undefined undefined(); } catch (err) { var b = 2; console.log(a); // 1 } console.log(a, b); // 1 2 console.log(err); // err is not defined
- try/catch
// try/catch try { var a = 1; console.log(b); // undefined } catch (err) { var b = 2; console.log(a); } console.log(a, b); // 1 undefined console.log(err); // err is not defined
let
当把以上代码中的
var
换成let
后就不会出现污染全局的情况了,需要注意的是let不会在块作用域中进行声明提升
。如下:
// for
// console.log(i); // i is not defined
for (let i = 1; i <= 10; i++) {
console.log(i); // 1 - 10
}
console.log(i); // i is not defined
- 当需要定义多个
块
各自执行各自的作用域内的代码时,同时这个机制也与闭包及回收垃圾的回收机制相关,避免不必要的变量污染全局
,可以使用如下方式:function foo(a){ console.log(a); } { let a = 1; foo(a); // 1 console.log(a); // 1 } { let a = 2; foo(a); // 2 console.log(a); // 2 } foo(3); // 3
const
与let类似,但const定义的变量不能进行修改,一般用来创建常量。
var
,let
,const
区别
var
定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。let
定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。const
用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改。
提升
先有鸡还是先有蛋
先有的
声明
还是先有的赋值
,或是先有的打印
操作?
a = 2;
var a;
console.log(a);
console.log(a);
var a = 2;
提升示例
具体参见: https://blog.csdn.net/weixin_43526371/article/details/107360156
foo(); // 2
var foo = function () {
console.log(1); }
foo(); // 1
function foo() {
console.log(2); }
foo(); // 1
/**
* 相当于:
* var foo;
* function foo() { console.log(2); }
* foo(); // 2
* foo = function () { console.log(1); }
* foo(); // 1
* foo(); // 1
*/
作用域闭包
参考地址
https://blog.csdn.net/weixin_43526371/article/details/107360445