陌上寒

陌上寒个人博客

谈一谈javascript作用域

从今天开始研究讨论javascript作用域,感兴趣的同学请保持关注

什么是javascript作用域?

作用域是指程序源代码中定义变量的区域。
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
谈起javascript作用域你会想到哪些知识点?

  • 函数作用域
  • 块级作用域
  • 闭包
  • 变量提升
  • let 暂时性死区
  • var let const
  • this
  • 一个变量的声明周期(从有到无)
  • 全局作用域
  • 局部作用域
  • 作用域链

等等,这些都是关于作用域的知识点,
我下载了几个源码,我们观察一些图片
jquery.js
《谈一谈javascript作用域》
vue.js
《谈一谈javascript作用域》
axios.js
《谈一谈javascript作用域》

上面的三个图,你会发现他们在结构上有一个共同点

(function(){
    //。。。代码
}('参数'))

为什么要这么设计?
解释此问题前,我们需要介绍一个概念,方便后边阅读

执行环境

《Javascript高级程序设计(第三版)》对这个概念有介绍
执行环境(Execution Context,简称Context)
Context定义了变量或函数有权访问的其他数据,决定了它们的各自行为,Context只是一个抽象概念,它对应很多内容,变量对象(Variable Object,简写VO)是其一,还有作用域链,this等,这些共同组成了执行环境这个概念。

执行环境: 所有 JavaScript 代码都是在一个执行环境中被执行的。执行环境是一个概念,一种机制,用来完成JavaScript运行时在作用域、生存期等方面的处理,它定义了变量或函数有权访问的其他数据(包含了外部数据),决定他们各自的行为。
包括以下分类:
全局执行环境: 全局环境是最外围的一个执行环境,根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样,在web中,全局执行环境被认为是window对象。

函数执行环境: 每个函数都有自己的执行环境。
我们看一个栗子

let hisName = '郭靖'
function hero() {
  let herName = "黄蓉"
  let hisName = "洪七公"
  console.log(hisName);//=>洪七公
}
hero()
console.log(hisName);//=>郭靖
console.log(herName);//Uncaught ReferenceError: herName is not defined

输出

//=>洪七公
//=>郭靖
//Uncaught ReferenceError: herName is not defined

hero内部的hisName变量覆盖了外部的全局的hisName变量
hero外部无法访问hero内部的变量(hero内部的变量被保护起来,形成了私有变量)
上面的栗子做一下改动

let hisName = '郭靖'
function hero() {
  let herName = "黄蓉"
  function likeFun() {
    let like = '喜欢'
    console.log(`${herName}喜欢${hisName}`);//=>黄蓉喜欢郭靖
  }
  likeFun()
  console.log(like);//ReferenceError: like is not defined
}
hero()

输出

//=>黄蓉喜欢郭靖
//ReferenceError: like is not defined

通过上面的两个小栗子,我们可以得出以下结论:

内部可以访问外部变量,外部不可以访问内部变量
函数进行了嵌套,其实也是作用域进行了嵌套,js 会从内向外一层一层查找,找到后就返回,找不到就报错,形成了作用域链
一个栗子

const element='123321'
const arr1 = [1, 2, 3, 4, 5]
const arr2 = ['a', 'b', 'c', 'd', 'e']
arr1.forEach(element => {
  console.log('element1:', element);
  arr2.forEach(element => {
    console.log('element2:', element);
  })
});

虽然可读性很差,但是当内部没有对外部进行引用时,这么写也是允许的,
继续看一个栗子

let a = 1;
let b = 2;
function add(x,y) {
  let a = 10;
  let b = 20;
  return a+b+x+y
}
function sum(x,y) {
  let a = 'a';
  let b = 'b'
  console.log(a+b+x+y);
}
console.log(add(a,b));//=>33
sum(a,b)//=>ab12

add 和sum两个函数,它们将内部逻辑和全局进行了隔离,尽管外部(全局)存在变量a和变量b,
但是对函数内部没有影响,拿add来说,函数add如何与window进行交流,
(当然,交流不是必须的)如果要交流,以参数作为输入,以return作为输出(联想一下import和export)
打印一下全局变量

console.log(window)

现在全局作用域下多了几个变量:add,sum 。a和b呢?
⚠️:let和const声明的变量不在window下,用var声明的变量会在window下,我们今天不讨论var
所以上面的栗子中的全局变量只有函数add和函数sum
当我们的项目越来越庞大的时候,过多的全局变量必然存在危险,首当其冲的就是变量的命名冲突,
或许你千方百计的对你的变量名称进行唯一化,命名了一些很长的,自认为不会引起冲突的名字,但是当你引入了第三方js库时,并且那个第三方库并没有妥善的处理自有变量,这时候依然存在命名冲突的风险,这并不能从根本上解决问题。
我们要最大限度的减少全局变量
现在我们对上面的栗子进行优化

function aFun(params) {
  let a = 1;
  let b = 2;

  function add(x, y) {
    let a = 10;
    let b = 20;
    return a + b + x + y
  }

  function sum(x, y) {
    let a = 'a';
    let b = 'b'
    console.log(a + b + x + y);
  }
  console.log(add(a, b)); //=>33
  sum(a, b) //=>ab12
}
aFun()

这样,add和sum就不在全局变量下了,有一个全局变量=>aFun,每次使用变量add和sum的时候需要调用aFun
再优化一下

(function aFun() {
//省略
})()

这样,aFun就不在全局作用域下了,不会引发全局变量污染,也不会引起命名冲突
如果你对上面的代码不理解,我们可以拆解一下

const aFun=function(){
    //省略
}
aFun()

aFun就是

function(){
    //省略
}

函数名后加一对()进行执行操作
我们比较一下两者区别

function aFun() {
    //省略
}
aFun()
//----------------
(function aFun(){
    //省略    
}())

第一个,aFun在所在的作用域中(全局作用域),可以通过aFun进行调用
第二个,aFun在自己的函数表达式中,可以在自身内部调用,不能再外部调用

(function aFun(){
    //省略    
    aFun()//可以在这里调用,小心递归死循环
}())
aFun()//这里调用会报错Uncaught ReferenceError: aFun is not defined

如果不会出现自己调用自己的情况可以直接使用匿名函数

(function (){
    //省略    
}())

现在回头看下开篇的那三个js源码,他们都采用了这种方式进行封装,对自有变量和自有方法进行隔离,防止污染全局
今天的内容不难,你get到了吗

END

  1. Pingback: 循环中的异步&&循环中的闭包 - 算法网

发表评论

电子邮件地址不会被公开。 必填项已用*标注