文章目录
- 一、什么是作用域
- 二. 全局作用域、函数作用域、块级作用域
- 全局作用域
- 函数作用域
- 注意 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 来体现。
二. 全局作用域、函数作用域、块级作用域
作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行
全局作用域
在代码中任何地方都能访问到的对象拥有全局作用域,一般来说以下几种情形拥有全局作用域:
- 最外层函数 和在最外层函数外面定义的变量拥有全局作用域
- 全局作用域访问不了函数作用域的变量等
- 函数作用域能访问全局变量等
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
- 所有末定义直接赋值的变量自动声明为拥有全局作用域
function outFun2() {
variable = "未定义直接赋值的变量";
var inVariable2 = "内层变量2";
}
outFun2();//要先执行这个函数,否则根本不知道里面是啥
console.log(variable); //未定义直接赋值的变量拥有全局作用域
console.log(inVariable2); //inVariable2 is not defined
- 所有 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 声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:
- 在一个函数内部
- 在一个代码块 { } 内部
特点:
- let声明只在声明所在的块级作用域内有效
{
var a = 1;
let b = 2
}
console.log(a); //1
console.log(b); //Uncaught ReferenceError: b is not defined
- 声明变量不会提升到代码块顶部
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
- 不可重复声明
// 只要let定义的变量,就不能再以任何形式定义,会报错
let test = 'aaa'
//let test = 'bbb'
var test = 'ccc'
- 循环中的块级作用域绑定
数据每一个方法打印当前索引
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代码被执行之前,首先需要进行代码的解析
- 编译阶段完成两件事情:创建执行上下文和生成可执行代码
- 执行上下文就包括变量环境和词法环境和this指向等,创建执行上下文的过程:
如果是普通变量的话,js引擎会将该变量添加到变量环境中并初始化为undefined
如果是函数声明的话,js引擎会将函数定义添加到变量环境中,然后将函数名执行该函数的位置(内存) - 接着,js引擎就会把其他的代码编译为字节码,生成可执行代码
编译先创建上下文并传创建变量环境,词法环境,可执行代码,将执行上下文压入执行栈中。执行当前上下文环境可执行代码
变量环境: 通过var声明或者function(){}声明的变量存在这里
词法环境: 通过let, const, try-catch创建的变量存在这里
可执行代码:变量声明提前后剩下的代码
- 词法分析
JS 引擎会将我们写的代码当成字符串分解成词法单元(token) - 语法分析
语法分析阶段会将词法单元流(数组),也就是上面所说的token, 转换成树状结构的 “抽象语法树(AST)” - 代码生成
将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代码的流程如下图所示: