1.什么是作用域
作用域是你的代码在运行时,某些特定部分中的变量,函数,对象的可访问性,也就是作用域决定了变量和函数的可访问范围,即作用域控制者变量与函数的可见性和声明周期 作用域的主要功能是:
收集并维护所有声名的标识符
依照特定的规则对标识符进行查找
确定当前的代码对标识符的访问权限
function outFun2() {
var inVariable = "内层变量2";
}
outFun2();//要先执行这个函数,否则根本不知道里面是啥
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined
上面的代码中 变量inVariable在全局作用域中没有被声明 所以在全局作用域下取值会报错 我们可以这样理解: 作用域就是一个独立的地盘 ,让变量不会外泄 暴漏出去也就是说作用域最大的作用就是隔离变量 ,不同作用域下同名变量不会有冲突
ES6之前js没有块级作用域 只有局部作用域和函数作用域 ES6的出现为我们提供了“块级作用域
可以通过新增的命令 let 和 const 来实现”
2.全局作用域和函数作用域
所谓全局作用域 就是在代码中的任何地方都能访问到的对象拥有全局作用域 一般来说以下情况具有全局作用域
1.最外层函数和最外层函数外面定义的变量拥有全局作用域
var globleVariable= 'global'; // 最外层变量
function globalFunc(){ // 最外层函数
var childVariable = 'global_child'; //函数内变量
function childFunc(){ // 内层函数
console.log(childVariable);
}
console.log(globleVariable)
}
console.log(globleVariable); // global
globalFunc(); // global
console.log(childVariable) // childVariable is not defined
console.log(childFunc) // childFunc is not defined
从上面代码中可以看到globleVariable和globalFunc在任何地方都可以访问到, 反之不具有全局作用域特性的变量只能在其作用域内使用。
2.未定义变量直接赋值的变量(由于变量提升使之成为全局变量)
function func1(){
special = 'special_variable';
var normal = 'normal_variable';
}
func1();
console.log(special); //special_variable
console.log(normal) // normal is not defined
虽然可以在全局作用域中声明函数以及变量 使之成为全局变量但是不建议这么做 因为这可能会导致和其他变量名冲突 一方面如果我们再使用let 和 const声名变量 当命名发生冲突时会报错。
// 变量冲突
var globleVariable = "person";
let globleVariable = "animal"; // Error, thing has already been declared
另外,如果使用var申明变量,第二个申明的同样的变量将会覆盖前面的 这样会使代码很难调试。如:
var name = 'koala'
var name = 'xiaoxiao'
console.log(name); // xiaoxiao
2.局部作用域
局部作用域一般只在固定代码内可以访问到,最常见的就是函数作用域
2.1函数作用域
定在函数中的变量救在函数作用域中 并且函数在每次调用时都有一个不同的作用域,这意味着同名变量可以用在不同的函数中。因为这些变量绑定在不同的函数中,拥有不同的作用域,彼此之间不能访问
functon test(){
var num = 9;
//内部可以访问
console.log("test中:" + num);
}
//test外部不能访问
console.log("test外部" + num);
注意:
- 如果在函数中定义变量时,如果不添加var关键字 造成变量提升 这个变量成为一个全局变量
function doSomeThing(){
// 在工作中一定避免这样写
thing = 'writting';
console.log('内部:'+thing);
}
console.log('外部:'+thing)
- 任何一对花括号{。。。}中的 语句集都属于一个块 在es6之前 在块语句中定义的便来给你将保留在他已经存在的作用域中:
var name = '程序员成长指男';
for(var i=0; i<5; i++){
console.log(i)
}
console.log('{}外部:'+i);
// 0 1 2 3 4 {}外部:5
我们可以看到变量name和变量i是同级作用域。
2.2在ES6块级作用域未讲解之前注意点
变量提升:
变量提升(hosting) 在代码区中任意地方申明变量和在最开始(最上面)申明是一样的。也就是说,看起来一个变量可以在申明之前被使用 这种行为就是变量提升 看起来就像变量的申明被自动移到了函数或全局代码的最顶上。请看一段代码:
var tmp = new Date();
function f() {
console.log(tmp);
if(false) {
var tmp='hello';
}
}
上面的代码会输出undefined 原因是变量的提升 在这里申明提升了 定义的内容并不会提升 提升后对应的代码如下:
var tmp = new Date();
function f() {
var tmp;
console.log(tmp);
if(false) {
tmp='hello';
}
}
f();
console在输出的时候,tmp变量仅仅申明了但未定义。所以输出undefined。虽然能够输出,但是并不推荐这种写法推荐的做法是在申明变量的时候,将所用的变量都写在作用域(全局作用域或函数作用域)的最顶上,这样代码看起来就会更清晰,更容易看出来哪个变量是来自函数作用域的,哪个又是来自作用域链
重复声名
// var
var name = 'koloa';
console.log(name); // koala
if(true){
var name = '程序员成长指北';
console.log(name); // 程序员成长指北
}
console.log(name); // 程序员成长指北
上面的代码中 看起来name被声明了两次 实际上只有最上面的是有用的js的var变量只有在全局作用域和函数作用域两种 且申明会被提升 因此name变量只会在最顶上开始的地方申明一次 var name = '程序员成长指北'; 此句代码的申明将会被忽略 仅仅用于赋值 也就是说上面的代码和下面的其实是一致的
// var
var name = 'koloa';
console.log(name); // koala
if(true){
name = '程序员成长指北';
console.log(name); // 程序员成长指北
}
console.log(name); // 程序员成长指北
变量和函数同时出现的提升
如果有函数和变量同时声明了 会出现什么情况呢???
看下面的代码:
console.log(foo);
var foo ='i am jiangjiejie';
function foo(){}
<!--输出结果是function foo(){} 也就是函数内容-->
如果是另一种形式呢?
console.log(foo);
var foo ='i am koala';
var foo=function (){}
上面代码输出undefined
下面我们对两种结果进行说明:
- 第一种:函数声明 就是上面第一种,function foo(){}这种形式
- 另一种:函数表达式,就是上面第二种 var foo=function(){} 这种形式
第二种形式其实就是var变量的声名定义 因此上面的第二种输出结果为undefined
而第一种函数声名的形式 在提升的时候会被整个提升上去 包括函数定义的部分 因此第一种形式和下面的是等价的:
var foo=function (){}
console.log(foo);
var foo ='i am koala';
可以看到:
- 函数的声名被提到了最顶上;
- 申明只进行了一次 因此后面 var foo ='i am koala';会被忽略
- 函数申明的优先级优于变量申明,且函数声明会连带定义一起被提升(这里与变量不同)
2.3块级作用域
es6新增了let和const命令,可以用来创建块级作用域变量,使用let命令声名的变量只在let命令所在的代码块内有效
let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。块级作用域有以下几个特点:
- 1.变量不会提升到代码块顶部 且不允许从外部访问块级作用域内部变量
console.log(bar);//抛出`ReferenceErro`异常: 某变量 `is not defined`
let bar=2;
for (let i =0; i<10;i++){
console.log(i)
}
console.log(i);//抛出`ReferenceErro`异常: 某变量 `is not defined`
这个特点带来了许多好处 开发者需要检查代码的时候可以在作用域外意外但使用某些变量 而且保证了 变量不会被混乱但复用提升了代码的可维护性 就像上面代码中的例子 一个只在for循环内部使用的变量i不会再去污染整个作用域。
不允许反复声名 ES6的let和const不允许反复声名 和var不同
// var
function test(){
var name = 'koloa';
var name = '程序员成长指北';
console.log(name); // 程序员成长指北
}
// let || const
function test2(){
var name ='koloa';
let name= '程序员成长指北';
// Uncaught SyntaxError: Identifier 'count' has already been declared
}
3.作用域链
3.1 javascript是如何执行的???

3.1分析阶段
JavaScript编译器编译完成,生成代码后进行分析
- 分析函数参数
- 分析变量声名
- 分析函数声名
分析阶段的核心就是再分析完成后(也就是接下来函数执行阶段的瞬间)会创建一个AO(active Object活动对象)
3.1.2执行阶段
分析阶段成功后,会把ao给执行阶段
- 引擎询问作用域,作用域中是否有这个叫x的变量
- 如果作用域有x变量 ,引擎会使用这个变量
- 如果作用域中没有,引擎会自动寻找(向上层作用域)如果到了最后都没有找到这个变量 引擎会抛出错误。
执行阶段的核心就是找,具体怎么找,后面会讲解lhs查询与RHS查询
3.1.3 JavaScript执行举例说明
function a(age) {
console.log(age);
var age = 20
console.log(age);
function age() {
}
console.log(age);
}
a(18);
首先进入分析阶段 前面已经说到 ,函数运行的瞬间 创建一个AO(active object活动对象)
AO = {}
第一步:分析函数参数
形式参数:AO.age = undefined
实参:AO.age = 18
第二步:分析变量声明:
// 第3行代码有var age
// 但此前第一步中已有AO.age = 18, 有同名属性,不做任何事
即AO.age = 18
第三步:分析函数声明
// 第5行代码有函数age
// 则将function age(){}付给AO.age
AO.age = function age() {}
函数声明注意点:AO上如果有与函数名同名的属性,则会被此函数覆盖,但是下面这种情况声明的函数不会覆盖AO链中同名的属性
var age = function () {
console.log('25');
}
进入执行阶段
分析阶段分析成功后,会把给AO(Active Object 活动对象)给执行阶段,引擎会询问作用域,找的过程。所以上面那段代码AO链中最初应该是
AO.age = function age() {}
//之后
AO.age=20
//之后
AO.age=20
输出结果是:
function age(){
}
20
20
3.2:作用域链概念
看了前面一个完整的JavaScript函数执行过程,让我们来说下作用域链的概念吧JavaScript上每个函数执行时,会在自己创建的ao上找对应属性值,若找不到则往父函数的ao上找,再找不到则再上一层的ao,知道找到最后的全局作用域,而这一条形成的“ao链”就是JavaScript中的作用域链
3.3找 过程LHS和RLHS查询特殊说明
LHS = 变量赋值或写入内存。想象为将文本文件保存到硬盘中。 RHS = 变量查找或从内存中读取。想象为从硬盘打开文本文件。
3.3.1LHS和RHS特性
- 都会在所有作用域中查询
- 严格模式下,找不到所需的变量时,引擎都会抛出ReferenceError 异常
- 非严格模式下,LHs会比较特殊,会自动创建一个全局变量
- 查询成功时,如果对变量的值进行不合理的操作,比如:对一个非函数类型的值进行函数调用,引擎会抛出Typeerror异常
3.3.2LHS和RHS举例说明
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
直接看引擎在作用域找这个过程:LSH(写入内存):
c=, a=2(隐式变量分配), b=
RHS(读取内存):
读foo(2), = a, a ,b
(return a + b 时需要查找a和b)
作用域链总结
