详解JS 作用域与作用域链、IIFE模式、js执行过程

文章目录

    • 一、什么是作用域
    • 二. 全局作用域、函数作用域、块级作用域
      • 全局作用域
      • 函数作用域
        • 注意 if、for循环、while循环变量
      • 块级作用域
    • 二、什么是作用域链
      • 1. 什么是自由变量
      • 2.什么是作用域链
      • 3. 关于自由变量的取值
    • 三、IIFE模式
      • 由来
      • 语法
        • 基本语法
        • 带参
    • 四、JavaScript 执行过程
      • 编译阶段
      • 执行阶段
      • 调用栈
      • js执行流程图解

前言:在学习模块化的时候,遇到IIFE模式为模块提供了私有空间,涉及到闭包,以及作用域,所以来复习一下相关内容。

一、什么是作用域

作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。可能这两句话并不好理解,我们先来看个例子:

function outFun() {
    var temp = "内层变量";
}
outFun();//要先执行这个函数,否则根本不知道里面是啥
console.log(temp); // Uncaught ReferenceError: inVariable is not defined

从上面的例子可以体会到作用域的概念,变量 temp在全局作用域没有声明 ,所以在全局作用域下取值会报错。我们可以这样理解:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是 隔离变量,不同作用域下同名变量不会有冲突。

ES6 之前 JavaScript 没有块级作用域,只有全局作用域函数作用域。ES6 的到来,为我们提供了块级作用域,可通过新增命令 let 和 const 来体现。

二. 全局作用域、函数作用域、块级作用域

作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行

全局作用域

在代码中任何地方都能访问到的对象拥有全局作用域,一般来说以下几种情形拥有全局作用域:

  1. 最外层函数 和在最外层函数外面定义的变量拥有全局作用域
  • 全局作用域访问不了函数作用域的变量等
  • 函数作用域能访问全局变量等
var outVariable = "我是最外层变量"; //最外层变量
function outFun() { //最外层函数
	console.log(outVariable)
    var inVariable = "内层变量";
    function innerFun() { //内层函数
        console.log(inVariable);
    }
    innerFun();
}
console.log(outVariable); //最外层变量
outFun(); //最外层函数
console.log(inVariable); //内层变量在外层访问不到 inVariable is not defined
innerFun(); //内层函数在外层访问不到 innerFun is not defined
  1. 所有末定义直接赋值的变量自动声明为拥有全局作用域
function outFun2() {
    variable = "未定义直接赋值的变量";
    var inVariable2 = "内层变量2";
}
outFun2();//要先执行这个函数,否则根本不知道里面是啥
console.log(variable); //未定义直接赋值的变量拥有全局作用域
console.log(inVariable2); //inVariable2 is not defined
  1. 所有 window 对象的属性拥有全局作用域

一般情况下,window 对象的内置属性都拥有全局作用域,例如 window.name、window.location、window.top 等等。

全局作用域有个弊端:如果我们写了很多行 JS 代码,变量定义都没有用函数包括,那么它们就全部都在全局作用域中。这样就会 污染全局命名空间, 容易引起命名冲突。

函数作用域

也就是局部作用域,是指声明在函数内部的变量,和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部。

function doSomething(){
    var blogName="浪里行舟";
    function innerSay(){
        alert(blogName);
    }
    innerSay();
}
alert(blogName); //脚本错误
innerSay(); //脚本错误

注意 千万不要以为大括号封起来就一定是局部作用域

注意 if、for循环、while循环变量

在javascript里if内部定义的变量、for、while循环相关变量 就会变为当前执行环境的变量,比如

var a = 'jack';
if(true) {
    var a = 'frank';
}
console.log(a); //frank

for(var i = 0;i<3;i++) {
    break;
}
console.log(i); //0
k = 5;
while(k>1) {
    k--;
    var d = 10;
}
console.log(k); //4
console.log(d); //10

块级作用域

ES6提出块级作用域,可通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:

  • 在一个函数内部
  • 在一个代码块 { } 内部

特点:

  1. let声明只在声明所在的块级作用域内有效
{
  var a = 1;
  let b = 2
}
console.log(a); //1
console.log(b); //Uncaught ReferenceError: b is not defined
  1. 声明变量不会提升到代码块顶部
console.log(a); //undefined
var a = 10
//上面的相当于下面
var a 
console.log(a); //undefined
a = 10

//let定义的变量没有变量提升,直接报错
console.log(b) //Uncaught ReferenceError: Cannot access 'b' before initialization
let b = 20
  1. 不可重复声明
// 只要let定义的变量,就不能再以任何形式定义,会报错
let test = 'aaa'
//let test = 'bbb'
var test = 'ccc'
  1. 循环中的块级作用域绑定

数据每一个方法打印当前索引

var arr = []
for(var i=0;i<10;i++){
  arr.push(function(){
    console.log(i)
  })
}
arr.forEach(function(item){
  item(); //10,10,10,10......
})

为了解决i都变成10的这个问题,以前用的解决办法是立即调用函数表达体IIFE

var arr = []
for(var i=0;i<10;i++){
  arr.push((function(val){
    return function(){
      console.log(val)
    }
  })(i))
}
arr.forEach(function(item){
  item(); //0,1,2,3......
})

而现在es6 let和const提供的块级绑定让我们无须再这样折腾

let声明模仿上面的IIFE所做的一切来简化循环过程。

var arr = []
for(let i=0;i<10;i++){
  arr.push(function(){
    console.log(i)
  })
}
arr.forEach(function(item){
  item(); //0,1,2,3......
})

二、什么是作用域链

1. 什么是自由变量

首先认识一下什么叫做 自由变量 。如下代码中,console.log(a)要得到 a 变量,但是在当前的作用域中没有定义 a(可对比一下 b)。当前作用域没有定义的变量,这成为 自由变量 。自由变量的值如何得到 —— 向父级作用域寻找(注意:这种说法并不严谨,下文会重点解释)。

var a = 100
function fn() {
    var b = 200
    console.log(a) // 这里的a在这里就是一个自由变量
    console.log(b)
}
fn()

2.什么是作用域链

当你要访问一个变量时,首先会在当前作用域下查找,如果当前作用域下没有查找到,则返回上一级作用域进行查找,直到找到全局作用域,这个查找过程形成的链条叫做作用域链

3. 关于自由变量的取值

关于自由变量的值,上文提到要到父作用域中取,其实有时候这种解释会产生歧义。

var x = 10
function fn() {
  console.log(x)
}
function show(f) {
  var x = 20
  f()
}
show(fn)

在 fn 函数中,取自由变量 x 的值时,要到哪个作用域中取?——要到创建 fn 函数的那个作用域中取。

要到 创建这个函数 的那个域,而不是调用的函数。

比如:

var a = 10
function fn() {
  var b = 20
  function bar() {
    console.log(a + b) //30
  }
  return bar
}
var x = fn() //这里得到的是bar函数
b = 200
x() //30
// a先在当前作用域bar函数中找,没有,则向父级作用域fn中找,没有,再向上找到全局作用域,var a = 10,获取到a;b先在当前作用域bar函数中找,没有,则向父级作用域fn中找,得到b的值20,所以a+b = 30

如果fn中没有var b = 20,则结果是210

三、IIFE模式

由来

(immediately invoked function expression)立即调用的函数表达式
IIFE的目的是为了隔离作用域,防止污染全局命名空间
实际上,IIFE的出现是为了弥补JS在scope方面的缺陷:JS只有全局作用域(global scope)、函数作用域(function scope),从ES6开始才有块级作用域(block scope)。对比现在流行的其他面向对象的语言可以看出,JS在访问控制这方面是多么的脆弱!那么如何实现作用域的隔离呢?在JS中,只有function才能实现作用域隔离,因此如果要将一段代码中的变量、函数等的定义隔离出来,只能将这段代码封装到一个函数中。
在我们通常的理解中,将代码封装到函数中的目的是为了复用。在JS中,当然声明函数的目的在大多数情况下也是为了复用,但是JS迫于作用域控制手段的贫乏,我们也经常看到只使用一次的函数:这通常的目的是为了隔离作用域了!既然只使用一次,那么立即执行好了!既然只使用一次,函数的名字也省掉了!这就是IIFE的由来。

语法

基本语法
//最常用
(function () {
  // code
})();

(function(){  
  // code
}());

!function () {
  // code
}();
带参
var a = 2;
(function IIFE(global){
    var a = 3;
    console.log(a); // 3
    console.log(global.a); // 2
})(window);
 
console.log(a); // 2

循环中的块级作用域绑定中,获取每个真正的 i 最初的解决办法就是自调用函数

有了ES6的块级作用域,则将替代IIFE模式

四、JavaScript 执行过程

JavaScript 执行过程分为两个阶段,编译阶段执行阶段。在编译阶段 JS 引擎主要做了三件事:词法分析、语法分析和代码生成;编译完成后 JS 引擎开始创建执行上下文(JavaScript 代码运行的环境),并执行 JS 代码。

编译阶段

对于解释型语言(例如:JavaScript )来说,在JavaScript代码被执行之前,首先需要进行代码的解析

  1. 编译阶段完成两件事情:创建执行上下文和生成可执行代码
  2. 执行上下文就包括变量环境和词法环境和this指向等,创建执行上下文的过程:
    如果是普通变量的话,js引擎会将该变量添加到变量环境中并初始化为undefined
    如果是函数声明的话,js引擎会将函数定义添加到变量环境中,然后将函数名执行该函数的位置(内存)
  3. 接着,js引擎就会把其他的代码编译为字节码,生成可执行代码

编译先创建上下文并传创建变量环境,词法环境,可执行代码,将执行上下文压入执行栈中。执行当前上下文环境可执行代码

变量环境: 通过var声明或者function(){}声明的变量存在这里
词法环境: 通过let, const, try-catch创建的变量存在这里
可执行代码:变量声明提前后剩下的代码

  1. 词法分析
    JS 引擎会将我们写的代码当成字符串分解成词法单元(token)
  2. 语法分析
    语法分析阶段会将词法单元流(数组),也就是上面所说的token, 转换成树状结构的 “抽象语法树(AST)”
  3. 代码生成
    将AST转换为可执行代码的过程称为代码生成

执行阶段

js引擎开始执行可执行代码,按照顺序一行一行执行,当遇到函数或者变量时,会在变量环境中寻找,找不到的话就会报错。全局执行上下文首先入栈,遇到函数执行上下文则压入栈中,后进先出的方式执行。

调用栈

调用栈,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。javascript利用栈这种数据结构管理执行上下文。

当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部

引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

在没有块级作用域之前,只有变量环境

var a = 0;
function add(a + b) {
    returan a + b;
}
function sum(c) {
    return c + add(2, 3);
}
sum(a);

调用栈:
add函数执行上下文-> 变量环境:a = 2; b = 3
sun函数执行上下文-> 变量环境:c = 0
全局执行上下文-> 变量环境:a=0; function add(){}; function sum(){}

可以看到调用栈如果不能有序退出那么就会造成栈溢出,这种情况一般会发生在递归调用结束条件有问题情况等等。

ES6引入块级作用域,引入了词法环境。我们可以简单地认为,var以及function声明的变量加入到环境变量,而let以及const声明的变量加入到词法环境当中。

var a = 0
let b = 1
function foo() {
    var a = 1
    let b = 2
    if (true) {
        let b = 3
        console.log(a, b)
    }
}
foo() // 1, 3

调用栈:
全局执行上下文-> 变量环境:a=0; function foo(){}; 词法环境: b=1
foo函数执行上下文-> 变量环境:a = 1; 词法环境:b = 2;b=3

js执行流程图解

function getName() {
    const year = getYear();
 
    const name = 'Lynn';
    console.log(`${name} ${year} years old this year`);
}
 
function getYear() {
    return 18;
}
 
getName(); 

浏览器执行javascript代码的流程如下图所示:
详解JS 作用域与作用域链、IIFE模式、js执行过程_第1张图片

你可能感兴趣的:(javascript,前端,开发语言)