首页 > 编程笔记 > JavaScript笔记
JS作用域(全局作用域+局部作用域)
作用域(Scope)是当前执行环境的上下文(Current Context of Execution),它限制了变量、函数等的可见性,在当前作用域下定义的变量、函数等只能在当前及内部嵌套的子作用域中访问,而不能在外层或父作用域中访问,这样可以避免变量和函数的命名冲突,还可以形成私有数据,从而保护数据不被外部作用域中的代码篡改。
在 JavaScript 中,作用域分为全局作用域(Global Scope)和局部作用域(Local Scope)两种。
例如,下方代码中的变量 a 和 func() 函数都定义在全局作用域中,并且在函数中可以访问全局作用域中的 a,代码如下:
4
7
5
首先,代码一开始定义了全局作用域的 x,其值为 5,而在 outerFunc() 函数中,定义了同名变量 x,它的值为 4,这时 x 的值在 outerFunc() 中是 4,覆盖了全局中的 5。
后面又在 outerFunc() 中定义了 innerFunc() 函数,并且在里边再次覆盖了 x 的值,变成了 7,而 7 这个值只会在 innerFunc() 中有效,在 innerFunc() 大括号结束的时候就会失效,因此在 innerFunc() 定义的下方打印 x 的值仍然是 4。
当 outerFunc() 结束时,它里边的 x 也失效了,所以最外边使用 console.log(x) 时打印出的是全局作用域中的 x,其值为 5。
例如,使用 var 定义循环的变量,如果循环被定义在全局作用域中,则 var 定义的变量也属于全局作用域,在 for 循环结束后仍然可以访问它的值,代码如下:
JavaScript 的作用域属于静态作用域,称为词法作用域(Lexical Scope),它的意思是,作用域在编写代码的时候就已经确定了,而动态作用域是程序在运行的时候,才去动态地判断作用域。
词法作用域可以让理解作用域变得更简单,只需看代码就能够知道某个变量的作用域了,例如在函数中定义的作用域只需看该函数的大括号在哪里结束,那么变量的作用域就会在哪里结束。
在 JavaScript 中,函数和使用 var 声明的变量有提升(Hoisting)机制,可以先使用后声明。JavaScript 编译器会提前检查代码中的函数及 var 变量,把它们提升到当前作用域的顶部,这样就能保证代码的正常运行了。
例如,测试使用 var 声明的变量的提升机制,代码如下:
需要注意的是,变量在提升的时候,因为只有声明部分被提升,所以如果在声明变量的同时进行了定义,再在上方访问该变量就会返回 undefined,代码如下:
而如果使用 let 或者 const 关键字定义变量,则不能提前使用它们定义的变量,而是会直接抛出异常,代码如下:
对于变量的提升机制,并不推荐使用,因为这样很难看出来变量是在哪定义的,从而容易引发问题,尤其是当有同名变量和函数名覆盖的时候,最难理解,代码如下:
function x(){}
可以看到 func() 函数最后返回的 x 值为函数 x(),而不是 5。
这是因为 function x(){} 的定义首先被提升到了 func() 函数的第1行,var x 则按顺序提升到了第2行,由于声明变量x的时候并没有赋值,它不会覆盖掉函数 x() 的定义,之后就直接运行到 return x 语句了,返回了函数 x(),而 x=5 并没有机会被执行,代码如下:
来看一个例子,代码如下:
这就是临时隔离区的作用,虽然 test() 函数的外部作用域中有 a 变量,但是在函数内部这个块级的作用域中,它会在一开始把最后边 a 变量的声明放到临时隔离区中,只有在执行完 a=6 时才会从隔离区释放出来,在此期间,是不能访问隔离区中的变量的,所以打印 a 的值抛出了引用错误。
之所以称它为临时隔离区,是因为它只短暂地存在于变量初始化的过程中,而不是按代码的位置来判断是否放入隔离区,例如下方示例是可以正常执行的,代码如下:
在 JavaScript 中,作用域分为全局作用域(Global Scope)和局部作用域(Local Scope)两种。
JS 全局作用域
在全局作用域中定义的变量、函数等可以在任何地方访问。在 JS 源代码最外层定义的变量、函数等都是在全局作用域中的。例如,下方代码中的变量 a 和 func() 函数都定义在全局作用域中,并且在函数中可以访问全局作用域中的 a,代码如下:
let a= 10; function func(){ console.log(a) } a; //10 func(); //10之前提到过最好不要使用 var 关键字定义变量,这是因为在浏览器环境中,使用 var 定义的全局变量,同时也会注册到全局对象 window 中(Node环境下不会),并且在浏览器开发环境中有经常需要使用到第三方库的情况,稍有不慎就会有同名的变量同时被注册到全局变量中,导致互相覆盖而引发问题,代码如下:
var x=10; globalThis.x; //10; var x="Hello"; globalThis.x; //"Hello"上方代码使用 var 定义了一个全局作用域的变量,并且使用 globalThis 访问全局变量,后面又使用同名变量覆盖了它的值,当再次访问时会发现变量值改变了,后续如果想再做与数字相关的操作,就会有问题。
globalThis 是 ES2020 中的新特性,用于统一访问全局对象,即在浏览器中是 window,而在 node 中则是global。
关于覆盖的问题,JavaScript 中使用 var 关键字定义的变量可以重复定义,后定义的变量会覆盖前边的,代码如下:var a=5; var a=6; console.log(a); //6要避免这个问题,可以使用 let 关键字,代码如下:
let b= 10; let b= 12; console.log(b); //SyntaxError:标识符b重复定义需要注意的是,如果是在 Chrome 开发者工具中的 Console 面板编写代码,则允许使用 let 重复定义变量,这是为了方便在同一个 Console 环境下,使用相同的变量名编写不同的测试代码,省去思考新变量名的困扰。
JS 局部作用域
在函数中定义变量时,会创建一个局部作用域,在函数外边无法访问函数内部的变量,无论是使用 var、let 还是 const 定义的,代码如下:function func(){var x=5}; x; //引用错误:x未定义局部作用域可以访问全局作用域中的变量和函数,也可以访问父级及以上作用域中的变量和函数,如果有同名的变量或函数,则子作用域会覆盖父作用域中的变量或函数,代码如下:
let x=5; function outerFunc(){ let x=4; function innerFunc(){ let x=7; console.log(x); } console.log(x); return innerFunc; } let innerFunc=outerFunc(); innerFunc(); console.log(x);上方代码输出结果是:
4
7
5
首先,代码一开始定义了全局作用域的 x,其值为 5,而在 outerFunc() 函数中,定义了同名变量 x,它的值为 4,这时 x 的值在 outerFunc() 中是 4,覆盖了全局中的 5。
后面又在 outerFunc() 中定义了 innerFunc() 函数,并且在里边再次覆盖了 x 的值,变成了 7,而 7 这个值只会在 innerFunc() 中有效,在 innerFunc() 大括号结束的时候就会失效,因此在 innerFunc() 定义的下方打印 x 的值仍然是 4。
当 outerFunc() 结束时,它里边的 x 也失效了,所以最外边使用 console.log(x) 时打印出的是全局作用域中的 x,其值为 5。
块级作用域
在局部作用域中,还有一个块级作用域(Block Scope)的概念。像 {} 语句块、if 语句、循环语句等会形成块级作用域,使用 let 或 const 定义的变量具有块级作用域,它们只在定义的大括号语句块中生效,离开大括号之后就不能访问了,代码如下:{ let i=10; } console.log(i); //引用错误,i未定义 for(let j=0;j<10;j++){} console.log(j); //引用错误,j未定义不过对于 var 定义的变量,则没有块级作用域的概念,在上述语句块中使用 var 定义变量之后,在语句块之外还是可以访问的,它的作用域跟语句块所在的作用域是同级的。
例如,使用 var 定义循环的变量,如果循环被定义在全局作用域中,则 var 定义的变量也属于全局作用域,在 for 循环结束后仍然可以访问它的值,代码如下:
for(var j=0;j<10;j++){} console.log(j); //10这里的 j 最后运行 j++ 之后会变成 10,在循环结束之后仍然可以访问它的值。
JavaScript 的作用域属于静态作用域,称为词法作用域(Lexical Scope),它的意思是,作用域在编写代码的时候就已经确定了,而动态作用域是程序在运行的时候,才去动态地判断作用域。
词法作用域可以让理解作用域变得更简单,只需看代码就能够知道某个变量的作用域了,例如在函数中定义的作用域只需看该函数的大括号在哪里结束,那么变量的作用域就会在哪里结束。
JS 提升机制
这里需要区分一下声明和定义,声明指的是只指定变量名但不赋值,例如 var a,定义这里指的是指定变量名并赋值,例如 var a=1。在 JavaScript 中,函数和使用 var 声明的变量有提升(Hoisting)机制,可以先使用后声明。JavaScript 编译器会提前检查代码中的函数及 var 变量,把它们提升到当前作用域的顶部,这样就能保证代码的正常运行了。
例如,测试使用 var 声明的变量的提升机制,代码如下:
x=5; console.log(x); //5 var x;上边代码中的 var x 声明被提升到了 x=5 的上方,作为第一行代码,然后才给 x 赋值为 5,这样打印出来的值就是 5。
需要注意的是,变量在提升的时候,因为只有声明部分被提升,所以如果在声明变量的同时进行了定义,再在上方访问该变量就会返回 undefined,代码如下:
console.log(x); //undefined var x=5; console.log(x); //5它相当于如下代码:
var x; console.log(x); //undefined x=5; console.log(x); //5代码中的 var x 被提升到最顶部,剩下的赋值语句则保持在原位。
而如果使用 let 或者 const 关键字定义变量,则不能提前使用它们定义的变量,而是会直接抛出异常,代码如下:
a=5; console.log(a); //引用错误,不能在初始化之前访问a let a;对于函数,使用 function 关键字定义的普通函数全部都会被提升到作用域的顶部。例如下方代码中,函数的定义会移动到 printValue() 上方,代码如下:
printValue(); //10 function printValue(){ console.log(10) }但是,对于保存在变量中的函数表达式则不会有提升机制,因为只有声明部分被提升了,而使用函数表达式进行赋值的部分并未被提升,代码如下:
printValue(); var printValue=function(){console.log(10)} //类型错误:printValue不是函数利用函数的提升,可以把函数定义的细节放到代码后边,把函数的调用放到前边,以便关注代码所执行的操作,屏蔽具体的实现细节,这样可以增强代码的可读性。
对于变量的提升机制,并不推荐使用,因为这样很难看出来变量是在哪定义的,从而容易引发问题,尤其是当有同名变量和函数名覆盖的时候,最难理解,代码如下:
function func(){ return x; x=5; function x(){} var x; } console.log(func());代码输出的结果如下:
function x(){}
可以看到 func() 函数最后返回的 x 值为函数 x(),而不是 5。
这是因为 function x(){} 的定义首先被提升到了 func() 函数的第1行,var x 则按顺序提升到了第2行,由于声明变量x的时候并没有赋值,它不会覆盖掉函数 x() 的定义,之后就直接运行到 return x 语句了,返回了函数 x(),而 x=5 并没有机会被执行,代码如下:
function func(){ function x(){} var x; return x; x=5; }
JS 临时隔离区
使用 let 关键字定义的变量,不能在初始化之前访问的原因是,它的声明被放到了临时隔离区(Temporal Dead Zone,TDZ)。临时隔离区会在执行块级作用域的第1行代码前生效,在变量初始化完成之后才会把变量从隔离区里释放出来。来看一个例子,代码如下:
let a=5; function test(){ console.log(a); //引用错误,不能在初始化之前访问 'a' let a=6; } test();在代码中,函数 test() 的外部和内部定义了同名的变量 a,但是在函数中打印a的值时却抛出了错误。
这就是临时隔离区的作用,虽然 test() 函数的外部作用域中有 a 变量,但是在函数内部这个块级的作用域中,它会在一开始把最后边 a 变量的声明放到临时隔离区中,只有在执行完 a=6 时才会从隔离区释放出来,在此期间,是不能访问隔离区中的变量的,所以打印 a 的值抛出了引用错误。
之所以称它为临时隔离区,是因为它只短暂地存在于变量初始化的过程中,而不是按代码的位置来判断是否放入隔离区,例如下方示例是可以正常执行的,代码如下:
let a=5; function test(){ const inner=()=>console.log(a); let a=6; inner(); } test(); //6这是因为在 inner() 函数调用前,临时隔离区在 let a=6 这行代码之后就已经结束了,a 在 test() 函数这个作用域中已经成功被初始化为 6,再在 inner() 中就可以访问它的值了。