长话短说,带你了解JavaScript作用域,探索域的奥妙。

前言

JavaScript是一门动态类型、弱类型的语言。相比其他语言,JS在给变量进行声明时,无需确定变量的具体类型,就可以直接声明,例如: a = 1; 也可以在a的前面加上var或者let进行声明。而提到let和var,这里就必须谈谈JS中一个很重要的概念------------作用域。作用域通俗的来说,就是我在某个地方想要使用"他"时,在那个对应的地方是否能找到"他",这个地方我们称为作用域。而了解JavaScript的作用域对未来编写高效、可维护的代码来说是萌新走向大佬不可或缺的一步。

作用域

作用域指的是一个变量的作用范围。通俗来说,就是你定义的变量并不是在代码的每个地方都能拿到的,而限定你是否能拿到你想要的变量就是作用域。正如人们所熟知的动漫中的领域一样,领域之内,他人不能踏足。变量也是这样,他仅仅只能在自己的作用域内有效,超出了自己的作用域,变量就不起作用了。

作用域的类型及其作用

在JavaScript中,作用域分为全局作用域和函数作用域以及块级作用域(ES6开始有了)。其作用是为了隔离变量,不同作用域下同名变量不会发生冲突。

全局作用域

全局作用域,写在script标签中,或者外部js文件的全局范围内的JavaScript代码都是全局作用域。

js 复制代码
// 全局作用域
a = 1;
var b = 2;
let c = 3;

if(true){
    console.log("a =" + a)  // 全局变量在判断语句中能调用
}

function foo(){
    console.log("b =" + b) // 全局变量在函数中也能调用
}

foo()
console.log("c =" + c)
// 全局作用域

如上述代码所示,在全局作用域内,我们可以使用var,let或者直接声明变量,只要是在全局作用域内,不管是函数内,还是判断语句内,这些被定义的变量都能够被调用。

函数作用域

一个变量声明在函数体内,即在{}内,这种变量我们称之为局部变量,而{}内的代码区域我们通常称之为函数作用域,局部变量只有在函数作用域内才会有效,出了函数就无法调用,但也有例外,后面我们就会聊到的with关键字就会产生出特殊情况。

js 复制代码
// 全局作用域
var a = 1
var b = 'helo world'

function foo() {
  // 函数作用域
  var c = true
  console.log(a)
  // 函数作用域
}


foo()

//console.log(c)  //报错! c is not defined!
// 全局作用域

上述代码我们可以看到在函数作用域内定义的变量c出了函数就无法被调用了,V8引擎找不到变量c,所以执行console.log(c)时会报错。下面引入一段函数嵌套的实例代码,让我们充分了解函数作用域。

js 复制代码
var a = 1

function foo(){
  var a = 2 
  // console.log(a)  // 此处输出结果为2
  function bar(){
    console.log(a)
  }

  bar()
  // bar在foo函数内部调用,不会报错!
}

foo()
// bar函数放外面,而bar函数放在foo函数内部,并为被编译,所以执行的时候找不到函数bar,执行程序报错!
// bar()

分析上述代码,我们在全局变量中声明了变量a的值为1,在foo函数的函数作用域内同样声明了变量a,其值为2,当我们在foo函数的函数作用域内输出a的值时,V8引擎会从内向外开始找a,显然首先找到的就是声明在foo函数内部的值为2的变量a,从而输出结果2。而foo函数内部的bar函数同样如此,若bar函数内部没有重新声明变量a,则仍然从内向外找,最终在foo函数内部找到了值为2的变量a,最终输出结果仍为2。一句话总结:变量的查找是从内部作用域往外部作用域查找的,不能由外到内查找。下面我们介绍上述在讲解函数作用域时所提及到的例外,在介绍它们之前,我们首先得了解什么是欺骗词法作用域。

词法作用域

在介绍欺骗词法作用域时,我们又得先知道什么是词法作用域,它和作用域有什么区别。其实,词法作用域属于是作用域的一种工作模型,而"作用域"相当于一套规则,用来管理引擎如何在对应的作用域内查找我们所需要的值。作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域,另一种叫做动态作用域。如何理解词法作用域呢,通俗来说,词法作用域是由你在写代码时将变量和块作用域 (这个等下会介绍)写在哪里来决定的,即定义在词法阶段(编译器对字符进行词法化)的作用域。

欺骗词法作用域

了解了词法作用域,你是否想过一个问题,在我们写代码期间,词法作用域会由我们写的函数所声明的位置来决定的,那么我们是否可以修改(换句话说是欺骗)词法作用域呢?答案是肯定的。在JS中,有两种办法来实现这个目的,第一种是JS中的eval(..)函数,它可以接受一个字符串为参数,并将字符串转化为对应的代码语句,下面演示一段代码:

js 复制代码
function foo(str,a){
  eval(str);  // 可以直接将字符串转换为语句
  console.log(b,a);
}

foo('var b = 3',2)
// 结果: 3 2

分析上述代码可知,在foo()函数作用域内,我们并没有直接声明变量b,而在全局作用域中调用foo()函数时我们也只是传入字符串类型的实参,eval()函数将我们传入的字符串'var b = 3'自动转化为了代码语句 var b = 3,相当于在函数内部声明了一个变量b值为3,所以最终我们还是拿到了b的值,最终输出结果 3 2 。下面我将给大家介绍第二种欺骗词法作用域的方法--with关键字

js 复制代码
var obj = {
  a:1,
  b:2,
  c:3
}

with(obj){
  a = 3
  b = 4
  c = 5
  d = 6  
}

console.log(obj)

console.log("d ="+d)
// 最终输出结果:{a: 3, b:4, c:5}  d =6

上述代码我们声明了一个对象obj,里面存放了a,b,c三个属性。而with关键字可以直接对obj对象里面的属性进行修改,我们同时修改了a,b,c,d的属性。咦,这时你可能会疑惑,在我们声明的obj对象中并没有d这个属性,而with关键字却修改了obj中的d属性,这样是否会报错呢,对并未声明的值进行修改,通常来看显然执行不会通过,但在这里确是可行的。其原因是因为 with(){} 当修改对象中不存在的属性时,会将该属性直接泄露到全局作用域,所以这里可以直接输出d的值。上述两种方式都能通过特定的方式从而达到欺骗词法作用域的效果,相信看完这些,你对欺骗词法作用域也有了初步的认识。下面我将带大家探索最后一种作用域的秘密--块级作用域。

块级作用域

简单来说,块级作用域就是由{}大括号包裹并且没有和函数结合的被称为块级作用域。这是ES6中新增的,其中,let和const在{}中声明的变量为局部变量,而var在块级作用域中声明的变量是全局变量,let相对于var来说不能重复定义。并且块级作用域之间也是不能相互访问的。简单来说分为以下几种:

js 复制代码
// 1.光一个{}
{
    // 块级作用域
}

// 2.循环语句中的{}
for(;;){
    // 块级作用域
}

while(..){
    // 块级作用域
}

// 3.判断语句中的{}
if(){
    // 块级作用域
}

// 4.定义对象时
var obj = {
    // 块级作用域
}

最后的最后我要带领大家去摘下"let关键字"的神秘面纱,在今后代码书写中,我们往往会更多的使用let进行声明而不是var,这是为什么?一段代码带大家领略其中的奥秘。

js 复制代码
// ----------------------------------------------------------------
var a = 1
function foo() {
   console.log(a);
   var a = 1;
 }

 foo()
// ----------------------------------------------------------------


// ----------------------------------------------------------------
function foo1() {
    console.log(a);
    let a = 2;
}

foo1()
// ----------------------------------------------------------------

上述代码中,调用foo函数,我们能输出a的结果吗,答案是否定的,这是为什么呢,我们不是在函数内部声明了变量a吗,为什么却拿不到a的值呢。其实,在编译器识别到console.log(a); var a = 1; 这两行代码时,会自动将它们读成 var a; console.log(a); a = 1;而当V8引擎执行到代码console.log(a)时,它能知道存在变量a,但由于没有执行第三行代码 a = 1; 并无法修改a的值,即使在全局作用域声明了变量a的值为1,但由于var可以对同一个变量进行二次声明,第二次并未声明值,所以最终输出的结果为undefined,找不到a的值,这就是var关键字所造成的变量提升,很明显这破坏了代码的逻辑性。而改用let则不会造成这样的错误,let在你进行二次声明时就会报错,最后运行代码报错,这是因为console.log(a) 运行时发生暂时性死区 局部变量和全局变量有相同名字定义时,不会去读取全局变量,即便先使用后定义。

相关推荐
汪子熙23 分钟前
Angular 服务器端应用 ng-state tag 的作用介绍
前端·javascript·angular.js
昨天;明天。今天。6 小时前
案例-表白墙简单实现
前端·javascript·css
安冬的码畜日常6 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
小御姐@stella6 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing6 小时前
【React】增量传输与渲染
前端·javascript·面试
GISer_Jing6 小时前
WebGL在低配置电脑的应用
javascript
万叶学编程9 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js
天涯学馆11 小时前
Next.js与NextAuth:身份验证实践
前端·javascript·next.js
HEX9CF11 小时前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss