理解JavaScript的作用域
大纲
- 作用域
- 作用域嵌套与作用域链
- 查询异常
- 词法作用域(静态作用域)
- 函数作用域
- 全局作用域
- 块级作用域
- 变量提升机制
- 函数优先
作用域
- 是什么? 作用域是指程序源代码中定义变量的区域。
- 有什么用? 作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
作用域嵌套与作用域链
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。作用域嵌套的查询规则:
- 首先,JS引擎从当前的执行作用域开始查找变量。
- 然后,如果找不到,引擎会在外层嵌套的作用域中继续查找。
- 最后,直到找到该变量,或抵达最外层的全局作用域为止。
这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
function foo(b) {
// 变量a在全局作用域下查询得到的
console.log(a + b) // 4
}
var a = 2
foo(2)
查询异常
- 如果获取未声明的变量的值会导致 ReferenceError 异常。
- 如果对未声明过的变量进行赋值:
- 在非严格模式下,JS引擎会为其自动创建一个全局变量且进行赋值。
- 如在严格模式下,会导致 ReferenceError 异常。
var a = 10
function sum() {
// b 是没有显式声明的,但被隐式创建为全局变量
b = 20
return a + b
}
console.log(sum()) // 30
console.log(c) // ReferenceError: c is not defined
// 因为c是未定义的变量,无法使用
词法作用域(静态作用域)
- 词法作用域就是定义在词法阶段的作用域,简单说就是函数的作用域在函数定义的时候就决定了。
- 词法作用域查找规则是:作用域查找是从内到外进行查找的,直到找到第一个匹配的标识符时停止。
而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。
词法作用域的经典例子
请思考以下2个例子输出的结果
// 例子1
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() {
return scope;
}
return f();
}
checkscope();
// 例子2
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() {
return scope;
}
return f;
}
checkscope()();
上面两段代码的结果都输出:local scope
,因为 JS 采用的是词法作用域,函数的作用域基于函数创建的位置。
欺骗词法作用域
- 欺骗词法作用域意思是(无意地)修改了所在的作用域。
- 欺骗词法作用域的两个方法:
eval()
和with()
。 - 缺点:欺骗词法作用域会导致性能下降。
eval()
函数可以接受一个字符串,并执行其中的的 JS 代码。
function foo(str, a) {
eval(str) // 欺骗作用域,引擎在此声明了:var b = 2,所以能计算以下结果
console.log(a + b) // 3
}
var b = 1
foo('var b = 2', 1)
with()
语句通常被当作重复引用同一个对象中多个属性的快捷方式,可以不需要重复引用对象本身。
function foo(obj) {
with (obj) {
b = 4
a = 2
}
}
var obj = {
b: 3
}
foo(obj)
console.log(obj.a) // undefined
console.log(obj.b) // 4
console.log(a) // 2,a被当前全局变量泄露到全局作用域上了
为什么最后能输出a的值为2?
原因是把 obj
对象传入函数内,obj
对象没有 a
属性,所以 obj.a
的值是 undefined
,却在 with()
语句中的 a
被当作全局变量隐式声明了,而且进行了赋值为2。
函数作用域
函数作用域内的变量或者内部函数,对外都是封闭的,从外层的作用域无法直接访问函数内部的作用域,否则会报引用错误异常。解决方法:闭包
。
function f1() {
var a = 1;
var b = 2;
var c = 3;
}
console.log(a, b, c) // ReferenceError: a, b, c is not defined
// 原因变量a,b,c是定义在函数内部的变量,外部作用域是无法访问的。
全局作用域
最外层的全局作用域,任何地方都可以访问得到。在最外层作用域下使用 var
关键字会定义全局变量,也就是说会挂载在 window
对象上,或者不使用关键字 var、let、const
直接对变量名字进行赋值,JS也会自动为其创建为全局变量。
var a = 10;
function f1() {
b = 20
function f2() {
c = 30
console.log(a) // 10
}
f2()
}
f1()
// b和c变量被隐式声明到全局变量了,所以能访问到,这也叫变量提升机制
console.log(b) // 20
console.log(c) // 30
// 但a,b,c也被挂载在window对象(全局作用域)上面了
console.log(window.a) // 10
console.log(window.b) // 20
console.log(window.c) // 30
块级作用域
块级作用域指在代码块 {}
里面定义的变量,只会在当前代码块有效,如果外层作用域下想访问该变量,会报引用错误异常。
使用关键字 let
或 const
定义块级作用域的变量。
for (let i = 0; i < 10; i++) {
}
console.log(i) // ReferenceError: i is not defined
// 因为i只能在for循环内部有效,外部作用域是访问不到的。
变量提升机制
先声明,后赋值
JS变量的声明和赋值是2个不同的步骤,比如:
a = 10
var a
console.log(a) // 10
JS引擎会将 var a
和 a = 10
当作两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务:
// 编译阶段的任务
var a
// 执行阶段的任务
a = 10
console.log(a) // 10
变量提升
function getValue(condition) {
console.log(value) // 不会报错
if (condition) {
var value = "blue";
return value;
} else {
return null;
}
}
getValue(undefined == null)
实际上以上代码的 value 已经被变量提升了:
function getValue(condition) {
// 编译阶段的任务
var value;
console.log(value) // undefined
if (condition) {
// 执行阶段的任务
value = "blue";
return value;
} else {
return null;
}
}
getValue(undefined == null)
函数优先
- 函数声明和变量声明都会被提升,但是出现在有多个“重复”声明的代码中,函数会首先被提升,然后才是变量。
- 函数没有方法重载,存在两个相同的函数名,后面的函数会覆盖前面的函数。
foo() // 3
function foo() {
console.log(1)
}
var foo = function () {
console.log(2)
}
function foo() {
console.log(3)
}
foo() // 2 函数表达式的提升情况
为什么呢?原因:
- 函数声明和变量声明都会被提升,但是出现在有多个“重复”声明的代码中,函数会首先被提升,然后才是变量。
- 相同函数名字,后面函数覆盖前面的函数。
实际上以上代码可以看成这样:
function foo() {
console.log(3)
}
var foo // 被忽略了
foo()
console.log(foo) // [Function: foo]
参考链接
共同学习,写下你的评论
评论加载中...
作者其他优质文章