1. 作用域的概念
1.1 作用域是什么?
在程序当中我们会使用到遍历,而对于这些变量,我们如何存储,以及之后当我们需要改变这些变量,又该如何找到它们呢? 因此在程序中有一种规则去存储变量,方便之后我们去查询,而这套规则就是我们说的作用域。
在传统的编译语言流程中,程序的代码在执行前会经历“编译”(分成三部分):
- 分词/词法解析
- 将字符组成的字符串分解成有意义的代码块(词法单元 token)
- var a = 2; => var, a, =, 2 , ;
- 空格的话要具体看空格在这门语言中是否有意义
- 解析/语法解析
- 生成抽象语法树 AST
- 代码生成
- 将语法树AST转换成可执行代码
1.2 理解作用域
- 引擎:从头到尾复杂js的编译和执行过程
- 编译器: 负责之前说到的语法分析,代码生成功能
- 作用域: 收集变量,维护查询,确定当前执行代码对这些变量的访问权限,是一套规则
举个例子:
var a = 2;
这段简单的代码,会由引擎和编译器一起执行。之前说到,在执行一段代码之前,会进行编译
,分解成词法单元,然后生成AST树,再执行。这个编译
的过程可能比我们想象的复杂一点,说回上面的例子:
编译器会进行入下处理:
- var a: 首先编译器会询问作用域(进行变量查询)是否有一个该名称的变量存在同一个作用域中,如果存在,继续编译;否则要求作用域在当前作用域的集合中声明一个新的变量,命名为a
- 编译器生成引擎需要执行的代码,a = 2,赋值。这时候作用域又登场了,引擎会询问作用域(进行变量查询)是否有一个该名称的变量存在同一个作用域中,如果存在,使用这个变量;否则继续查找(往作用域链);
- 如果找到了该变量,就把2赋给它,否则就抛出错误异常。
1.3 LHS RHS
就是在引擎执行代码之前,会通过作用域链查询a是否声明过,但是引擎查找分为两种:
var a = 2; // LHS 变量在赋值的左边 为=a找到一个目标变量
console.log(a); // RHS (只需找到这个值输出)
1.4 小例子
function foo(a) {
console.log(a);
}
foo(2);
以你的角度去分析
function foo(a) {
console.log(a + b); // 没有b
}
var b = 7;// 往全局找
foo(1)
2. 作用域的分类
2.1 词法作用域/动态作用域(了解)
之前我们说过的,语言在编译阶段的第一个步骤就是词法分析,而词法作用域
就是定义在这个阶段的作用域,在你写代码时将变量和块作用域写在哪里来决定的
。
function foo(a) {
var b = a * 2;
function bar(c) {
console.log(a, b, c);
}
bar(b * 3);
}
foo(2)
- 执行foo()
- 执行bar()
- console.log(a,b,c)
- bar的作用域里面没有a,b,只有c
- 往上找到foo,foo的作用域里面有b,a(a作为foo的参数也是在foo的作用域里面的)
2.2 欺骗词法:(尽量不使用哦,了解一下)
2.2.1 evel
function foo(str, a) {
eval(str);
console.log(str) // var b = 3;
console.log(b) // 3
console.log(a, b); // 1 3
}
var b = 2;
foo("var b = 3;", 1);
当使用evel函数,里面传递的字符串,会作为一段代码。比如上面的例子就会变成这样:b在函数foo的作用域里面,因此取到的值是3而不是外部的2
function foo(a) {
var b = 3;
console.log(a, b); // 1 3
}
var b = 2;
foo(1);
2.2.2 with
- 作用1
批量修改对象的属性
var obj = {
a: 1,
b: 2,
c: 3
};
// 不使用with
obj.a = 3;
onj.b = 6;
obj.c = 5
// 使用with
with (obj) {
a = 3;
b = 6;
c = 9;
}
console.log(obj.a, obj.b, obj.c);
- 作用2 根据传递给它的对象凭空创建一个新的词法作用域
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a : 3
};
var o2 = {
b : 3
};
foo(o1);
console.log(o1.a);
// 2 o1里面有a,取到a = 2
foo(o2);
console.log(o2.a); // undefined
// o2里面没b这个属性 找不到
console.log(a) // 2
// 但是全局上也创建了一个a = 2,也就是说当o2传就with里面,
// 在当前作用域和全局都没有找到,于是自己创建了一个全局a,再执行了a = 2这个赋值操作
讲一个对象的引用当作作用域来处理,将对象的属性当作作用域的标识符来处理
2.3 函数作用域
函数作用域指的是:属于这个函数的全部变量都可以在整个函数范围内使用及复用(包含嵌套)
2.3.1 适合的写法
function doSometing(a) {
b = a + doSomethingElse(a * 2);
console.log(b * 3); // 15
}
function doSomethingElse(a) {
return a - 1;
}
var b;
doSometing(2); // 15
但是doSometing里面的b和doSomethingElse应该是私有方法,我们不应该把它用到外面去定义,修改成下面这样更好
function doSometing(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse(a * 2);
console.log(b * 3); // 15
}
doSometing(2); // 15
2.3.2 添加包装函数,
可以放在函数内部影响到外部变量
但是:
- 需要显示定义foo()函数
- 需要执行
var a = 2;
function foo() { // foo作用域 外部不会受到影响
var a = 3;
console.log(a);
}
foo();
console.log(a)
立即执行函数解决:
var a = 2;
(function foo() {
var a = 3;
console.log(a);
})();
console.log(a)
以(开头的包装函数,会被当成函数表达式而不是一个标准的函数去执行
小技巧:区分函数表达式和声明式:如果function是声明的第一个词就是函数声明式,否则就是表达式;
小例子:
- 匿名函数表达式:回调参数
setTimeout(function () {
// 代码
},1000)
- 立即执行函数: 刚才那个
(function foo() {
var a = 3;
console.log(a);
})();
2.4 块级作用域
for (var i = 0; i < 10; i++) {
console.log(i) // 打印1-9
}
console.log(i,'1ss') // 打印10
{
let j;
for (j = 0; j < 10; j++) {
let i = j
console.log(i) // 打印1-9
}
}
es6新增了块级作用域,包括声明let const,这里来看看他们和var的区别有哪些吧:
2.5 var let const
- var声明的变量会挂载在window上,而let和const声明的变量不会:
var a = 1;
let b = 3;
const c = 4;
console.log(a)
console.log(window.a)
console.log(b)
console.log(window.b)
console.log(c)
console.log(window.c)
// 1
// 1
// 3
// undefined
// 4
// undefined
- var声明变量存在变量提升,let和const不存在变量提升
console.log(a)
console.log(b)
console.log(c)
var a = 0; // undefined var = undefined提升到了前面
let b = 5; // 报错
const c = 6; // 报错
- let和const声明形成块作用域
- 同一作用域下let和const不能声明同名变量,而var可以(提升覆盖)
if(1){
var a = 100;
let b = 10; // const也一样
}
console.log(a); // 100
console.log(b) // 报错 b找不到
- 暂存死区
a = 3
let a;
// ReferenceError: Cannot access 'a' before initialization
- const 一申明就要赋值
- const 不能修改值,它是常量
const a = 0;
a = 4
// TypeError: Assignment to constant variable.
- 如果是复合类型数据,可以修改其属性
const a = {
aa:'1'
}
a.aa = 'hh'
console.log(a.aa)
写在后面
这个Plan计划,重读你不知道的javascript系列,一起学习吧。目标是每周更新学习记录一部分,加油!
- 作用域和闭包系列【1】