深入理解JavaScript作用域:撬开js的大门

606 阅读7分钟

一. JS 工作原理

1.1 JS引擎

在深入学习JavaScript作用域之前我们一定要先了解JavaScript引擎的编译原理 认识JS作用域之前,要先需要了解JavaScript引擎的编译原理,JS执行引擎就是负责解析和执行JavaScript代码的组件,js的执行引擎有浏览器和node。其中不得不提的就是由Google开发,用于Chrome浏览器和Node.js环境中的V8执行引擎。V8将JavaScript代码编译成机器代码,而不是字节码,这使得执行速度非常快。

1.2 JS 执行过程

每一段代码在执行之前都要先经历一个编译过程,这个过程大致可以分为三个阶段:词法分析、语法分析和生成代码。

  1. 词法分析:将源代码字符串分解成一系列的标记(tokens)。这些标记是代码中的基本元素,例如 var a = 1; 这行代码被分解为:var、a、=、1、; 。
  2. 语法分析:语法分析器接收来自词法分析器的标记流,并根据语言的语法规则构建抽象语法树(Abstract Syntax Tree,AST)。AST 是源代码的层次结构表示,它反映了代码中的语法结构。如下图;

50176f3d2193cdbe6edfbba6dedfe7c.png

  1. 生成代码:一旦AST被构建,编译过程的最后一步是代码生成。代码生成器遍历AST,并根据AST的结构生成目标代码。目标代码可以是机器代码、字节码或者另一种高级语言的代码。

二. 作用域

2.1 全局作用域和函数作用域

全局作用域(Global Scope)

  1. 在全局作用域中定义的变量可以被代码中的任何其他部分访问和修改。
  2. 如果你在函数外部声明一个变量,它就自动拥有全局作用域。
  3. 全局变量会在整个页面生命周期内保持存在。

局部作用域(Local Scope)

  1. 局部作用域一般只在函数内部存在。
  2. 在函数内部声明的变量只在函数内部有定义,并且函数执行完毕后,这些局部变量就会被销毁。
  3. 局部作用域可以进一步分为函数作用域和块级作用域(由ES6引入的let或const关键字实现,下面会提到)

c1f9ff381976be056228e2bfe4e1ee7.png

2.2 作用域规则

内层作用域可以访问外层作用域,外层不能访问内层

例如,绿色方框内为内层作用域(函数作用域),红色方框内为外层作用域(全局作用域),在外层作用域里声明了变量a=1,在foo函数作用域里打印输出a,通过编译运行输出正确答案1,即可说明内层领域拿到了外层领域的 a,则内层作用域可以访问外层作用域。

0efdab67cc67473ae8d9873637adf07.png

再看下图bar函数作用域相当于内层作用域,foo函数作用域相当于外层作用域,在bar函数里打印输出a,访问了foo函数作用域里面的a,最后正确输出 2。

e6db16f62efb4cb7db2b13406155575.png 再看以下例子,运行后报错显示 ReferenceError: a is not defined,因为 console.log(a) 是写在全局作用域的,不可访问到 foo 函数作用域里的 a,所以输出报错,即 外层作用域不可访问内层作用域

f19e358efb1e8fb5f7eed7fd7305a55.png

再看以下代码,通过传入实参1赋值给 foo 函数作用域里的有效标识符 a,b 在函数作用域找不到有效标识符,则去外层找到有效标识符 b=2,所以最后不难得出输出为 1,2。

function foo(a){
  console.log(a, b);
}
var b = 2
foo(1)

三. let var const的区别

3.1 let与var

1.声明提升

声明的变量会存在声明提升(将变量的声明提升到当前作用域的顶部),而let 声明的变量不会被提升,必须在声明之后才能使用,否则会导致引用错误(ReferenceError)

console.log(a);   //正常输出,输出undefined
var a = 1

相当于先用 var 声明一个变量 a,再打印a,打印完再赋值

console.log(a);  //报错ReferenceError: Cannot access 'a' before initialization
let a = 1

2.重复声明

  • 可以在同一个作用域内多次声明同一个 var 变量,后面的声明会覆盖前面的声明。
  • 不能在同一个作用域内多次声明同一个 let 变量。否则会语法错误(SyntaxError)。

3.块级作用域

var 具有全局作用域和函数作用域,如果在任何函数外声明,则它为全局变量,即使在if或者for语句中也为全局变量。

let+{ } 拥有块级作用域,它们只在它们被声明的代码块(如 if 语句、for 循环等)内部有效。

if (true) {
  var a = 1  
  let b = 2
}
console.log(a);  //1
console.log(b);  //b is not defined
for (var i = 0; i < 10; i++) {
  console.log(i);  //输出0~9
}
console.log(i);  //10
for (let i = 0; i < 10; i++) {
  console.log(i);
}
console.log(i);  //i is not defined

4.暂时性死区

在代码块内,使用 let 声明变量之前,该变量是不可用的。也就是说,在变量声明之前,访问这个变量会抛出 ReferenceError。暂时性死区的存在是为了保证代码的执行顺序和逻辑。

let a = 1
if (true) {
  console.log(a);  //暂时性死区
  let a = 2
}

5.全局对象

var 在全局作用域内声明的变量可以通过window对象访问这些变量。

6fedc7e138a005aec03631b6213698c.png

let 在全局作用域内声明的变量无法通过window对象访问。

07e6b11dfc02ef13f1d76e68a611358.png

3.2 const

const 关键字用于声明一个只读的常量,这意味着一旦声明并初始化,它的值就不能再被改变。

const a = 1
a = 2
console.log(a);   //TypeError: Assignment to constant variable.

如果使用 const 声明对象,那么对象里面的属性值是可以修改的,这是因为当你使用 const 声明一个对象时,实际上你是在创建一个常量引用,指向一个对象。这意味着你不能再使用 const 声明的变量来指向另一个对象,但你可以修改该对象内部的属性。这是因为 const 只阻止了引用的重新分配,而不是对象本身的属性。

const person = {
  name: '张三',
  age: 19
}
person.age = 20
console.log(person);  //输出{ name: '张三', age: 20 }

四. 欺骗词法作用域

欺骗词法包含 evalwith,这俩个不遵循作用域规则(内层作用域可以访问外层作用域,外层不能访问内层)

1.eval

eval( ) 可以将任意字符串当成代码执行,如以下,将字符串'var b = 3'当成代码执行,所以得到b=3,再传入实参1给形参a,最后得到正确结果1,3。

function foo(str, a) {
  eval(str)  //var b = 3
  console.log(a, b);
}
foo('var b = 3', 1)

2.with

with( ){ } 当对象中没有属性 x 时,使用with修改 x 属性会导致 x 泄露到全局,会在全局作用域中查找或声明 a。

var o1 = {
  a: 1
}
var o2 = {
  b: 2
}
function foo(obj) {
  with (obj) {
    a = 2
  } 
}
foo(o2)
console.log(o2);  //输出{ b: 2 }
console.log(a);   //输出2

五. 总结

  1. js执行引擎包含浏览器就和node。
  2. js代码的执行过程分:词法分析,语法分析,生成代码。
  3. js有全局作用域和函数作用域,作用域规则:内层作用域可以访问外层作用域,外层不能访问内层。
  4. 块级作用域:由ES6引入的let或const关键字实现,在它们被声明的代码块(如 if 语句、for 循环等)内部有效。
  5. let与var
  • var 声明的变量会存在声明提升(将变量的声明提升到当前作用域的顶部)let不会。
  • var 可以重复声明变量,let 不行。
  • var 在全局声明的变量会默认添加在 window 对象上,let不会。
  • const 声明的变量值无法修改。
  1. 欺骗词法:1.eval( ) 将任意字符串当成代码执行。 2.with( ){ } 当对象中没有属性x时,with修改x属性会导致 x 泄露到全局。