JavaScript作用域、执行上下文、闭包及其应用


名称:名称(标识符)绑定实体、作用域、闭包
嵌套函数+闭包 构成了JavaScript的函数式编程

名称(标识符)绑定实体
创建名称:声明(Hoist到所在作用域的顶部)
实体创建:字面量、函数方法
在函数运行过程中,名称和实体都有自己的生存期。
变量与绑定实体的关系分类:原始值、引用值

作用域:生存期是从时间的维度衡量名称的有效,作用域就是从空间的维度。程序运行的时间对应代码的某一语句,因此作用域就是名称在代码中的有效区域

var、const、let、function关键字声明的变量的作用域是包围他们的函数,不在任何函数内声明-全局
作用域:函数、模块、全局

静态作用域(空间最近):运行前通过分析名称所在的相对位置,就能确认作用域。很少有语言采用动态作用域(时间最近)。JavaScript中的this关键字绑定的含义由其所在的代码的调用方式决定,对于函数相当于每次调用时在函数顶部隐式声明(静态作用域的其他变量只在函数声明时候声明一次)。JavaScript中类似的参数还有arguments。

某刻/某处的引用环境:运行的某一时刻(对应代码某一处),当前所有有效的名称组成的集合
当不针对某个名称时候,我们把引用环境保持不变的区域称为作用域

很多有关JavaScript的文章介绍闭包时候都把它定义为:从某个函数返回的函数所记住的上下文信息。其实这是闭包引用,任何JavaScript的函数创建时候都会创建闭包。

函数的局部名称都是存在于调用堆栈,若没有闭包这个概念,外套函数返回内嵌函数后,外套函数的堆栈帧被删除,返回的内嵌函数所能引用的外套函数的局部名称消失

闭包:以函数中心视角看待静态作用域

闭包虽然是在函数定义时就创建,但是并不意味这其中变量的值会停留在那一刻,只要闭包中的内嵌函数不是马上执行,程序的控制人在创建代码的一方,即闭包中的变量值就能正常改变,等到闭包中的函数执行时候,它引用环境中的变量值就可能不是原想的样子。var经典事件绑定。但是我们可以通过立即执行一个【返回一个函数的】函数的方式,截取当下的值。

网上有关闭包的分享有很多,大多都是博主基于自己的应用经验之后的理解,难免会有不同出发角度理解的区别。看了之后还是决定自己整理一下自己的理解角度。

JavaScript的词法作用域和动态作用域
编译器的第一个工作是分词,就是把由字符组成的字符串分解成词法单元。词法作用域就是定义在词法阶段的作用域,是由写代码时将变量和块作用域写在哪里来决定的。
和大多数现代编程语言一样,javascript使用的是词法作用域

词法( 静态 )作用域:函数的作用域在函数定义的时候就确定了
函数可以访问函数外的数据
整个代码中只有函数可以限定作用域
如果当前作用域规则中有声明变量名字了,就屏蔽外面的同名变量

JavaScript解析器: 预解析+执行
预解析:
把变量的声明提升至当前作用域的最前面,只提升声明,不包括赋值(所以屏蔽外面的同名变量)
提升变量之后,提升函数声明
注意:预解析是分script段进行的

在这里插入图片描述

动态作用域是javascript另一个重要机制this的表亲。动态作用域并不关心函数和作用域是如何声明以及在任何处声明的,只关心它们从何处调用,是基于调用栈的。
和变量搜索的静态作用域不同,this的值是在执行上下文中获取,而不会在作用域链中搜索。

基于上述:
作用域:变量起作用的范围
作用域链:全局作用域+函数作用域构成,内层函数可访问所有外层函数的局部变量
除了函数以外的其他任何位置定义的变量都是全局变量

历史发展

在JavaScript出现之前,闭包只存在于函数式编程语言里,是为了满足特殊应用场景出现的语法。
闭包:一种能够在函数声明中将环境信息与所属函数绑定在一起的数据结构

基于函数声明的文本位置,因此又称为 围绕函数定义的静态(词法)作用域
使函数访问其环境状态,应用于高阶函数、事件处理和回调、模拟私有成员变量

变量-声明提升的标志

在ES2015之前,javascript的名称面临的状况:在某个函数内部用var和function关键字声明的变量和函数的作用域是包围他们的函数。ES2015提出了let块作用域。

ECMAScript 6的声明风格:const>let>var(尽量不用)
所有使用var定义的全局变量和函数会成为window对象的属性和方法,let和const的顶级声明不会定义在全局上下文中,但在作用域的解析上效果相同。更早被垃圾回收
函数内部没有变量修饰符的变量也是挂载在全局!包括函数?
好像没有诶,难道和浏览器有关?
在这里插入图片描述

var name='marry';
console.log(window.name);//marry
let age='21';
console.log(window.age);//unfined

1.0 var与let

var a=b=3;
// 其实是
b=3;
var a=b;

var 声明函数作用域,变量在函数退出时会被销毁。冗余声明不会报错,声明自动提升到函数顶部(会将冗余合并)。
let声明作用域,冗余声明及会报错,声明也不会自动提升(使用前必须先声明)

注意思考
后半句中var、let的不同,本质上就是因为它们函数作用域和块作用域的限定。
let没有声明会发生什么?全局变量与块作用域
let、class的实现是基于var 的立即执行函数,所以他们的声明都不会自动提升

在for循环中感受var、let函数和块作用域的不同

for(var i=0;i<3;i++){
 setTimeout(()=>console.log(i),0);
 }//输出3 3 3
 
for(let i=0;i<3;i++){
 setTimeout(()=>console.log(i),0);
 }//输出0 1 2 

函数内部的var提升,屏蔽了全局a

  function foo(){
      console.log(a);
      var a=1;
  }
  var a=2;
  foo();//输出undefined 

注意提升hoist的只是声明而不是赋值

var a;
function foo(){
	var a;
	console.log(a);
	a=1;
  }
  a=2;
  foo();//预解析

可以再看看这个:

function foo(){
      console.log(a);
      a=1;
  }
  var a=2;
  foo();//输出2

在这里插入图片描述
ES6 中有明确的规定:如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。假如我们尝试在声明前去使用这类变量,就会报错。showName函数内,let name = 'inner’上面的区域就为暂时性死区。

1.0.0 补充知识点_setTimeout() 与单线程

单线程-同一时间只能做一件事:如果主逻辑代码没有执行完毕,永远不会触发setTimeout的回调函数
setTimeout() 方法用于在指定的毫秒数后调用函数或执行表达式。返回一个 ID(数字),可以将这个ID传递给 clearTimeout() 来取消执行。

setTimeout(code, milliseconds, param1, param2, ...)
setTimeout(function, milliseconds, param1, param2, ...)  //前两个参数必需:调用代码串或函数,第三个及之后的参数:可选,传给setTimeout函数里面的函数作为他的参数

上面代码中当执行遇到setTimeout(fn,millisec)时,会把fn这个函数放在任务队列中,当JS引擎线程空闲时并达到millisec指定的时间时,才会把fn放到js引擎线程中执行。
setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。

clearTimeout(timeoutId);//只要是在指定时间到达前调用,就可以取消定时任务
setInterval();
clearInterval();

const

const与let作用域效果相同,区别在const初始化后必须赋值且后面不允许改变
注意:const限制只适用于它指向变量的引用(若引用的是一个对象,那么可修改内部的属性)

Object.freeze(对象);// 接下来对象的属性不能更改

const person ={};
peson.name='marry';
for(const key in{a:1,b:2}){
	console.log(key);
} // a,b

for(const value of[1,2,3]){
	console.log(value);
} // 1,2,3

作用域

基于前文,我们可以再归纳一下
JavaScript中有三种作用域:全局作用域、函数作用域以及伪块作用域。
全局作用域:尽管使用全局变量很容易,但它们会被所有加载到页面的脚本共享。如果JavaScript代码不是以模块打包的,很容易导致命名空间冲突,不同文件中定义的变量和函数有可能被重写。不推荐大量使用

将一个JS文件直接通过scripts标签插入页面中与封装成模块最大的不同在于
前者的顶层作用域是全局作用域,在进行变量及函数声明时会污染全局环境;而后者会形成一个属于模块自身的作用域,所有的变量及函数只有自己能访问,对外是不可见的。
之前javascript并不原生具备模块化的能力。但是可以使用第三方开发的模块依赖处理库来实现模块化开发模式:AMD, COMMONJS,ES6
闭包也可以提供模块模式,详见:jQuery和zepto中添加插件的模板语法

ES6新语法,使用 export 导出模块的内容,并使用 import 导入模块的内容。

<script type="module">
    import test from './module.js';
    console.log(test())
  </script>

函数作用域:在函数中声明的任何变量都是局部且外部不可见的。
伪块作用域:let

执行上下文与作用域

每个上下文都有一个关联的变量对象,在上下文中定义的所有变量和函数都存在于这个对象上。上下文在所有代码被执行完后就会被销毁。
全局的上下文是最外层的上下文,根据js的宿主环境,表示上下文的对象可能不一样。全局上下文是在应用程序退出时被销毁。浏览器-window对象
每个函数调用都有自己的上下文,js程序的执行流就是通过上下文栈进行控制的,当代码执行流进入函数时,其上下文被推到一个上下文栈。同时还有一个作用域链,正在执行的上下文的变量对象在最前端。

作用域链增强
作用域前端临时添加一个上下文

  1. try.catch:创建新对象包含 要抛出的错误对象的声明
  2. with

注意区分作用域链和动态作用域

let x =1;
function f(){
    let x = 3;
    g();
}
function g(){
    console.log(x);// 输出1
    x = 2; // 静态词法作用域,x是全局变量
}

f();
console.log(x);

如果是动态作用域,那么输出应该是3、1
但是JavaScript是词法(静态)作用域,所以x指向他的名称声明空间-全局 即变量最近的包块,输出1、2

闭包

:将内嵌函数的引用环境和它捆版在一起,可以实现引用环境发生改变,还想用之前的引用环境。

闭包有保护和保存两个作用。
保护机制:保护私有变量,不被外界干扰,只通过闭包提供固定的接口
保存机制,形成一个作用域,只要作用域不被销毁,则变量也保存了。

真实项目中为了保证js的性能,堆栈内存的性能优化,应该尽可能减少闭包的使用。(不销毁的内存会耗掉性能的)
同时应当尽可能的减少全局变量的使用,以防止相互冲突(全局变量污染)。那么此时,将一部分内容封装到一个闭包中,让全局变量转化为私有变量。

(function(){
//变量
})();

我们封装内库插件的时候,我们会把自己的插件放到闭包中保护起来,同时又要暴露一些方法给客户使用。
常用的方式有zepto和jquery:
jq:把需要用到的方法添加到window的属性上,作为全局来暴露出去的。
zepto:基于return,把需要暴露的方法直接暴露出去。
这两者可以查看另一篇博客

Zepto最初是为移动端开发的库,是jQuery的轻量级替代品,因为它的API和jQuery相似,而文件更小。

看一个例子:当c被调用时候,x的值是1,但是输出却是0,这是因为c采用的是它闭包中的x,而它的闭包是在声明函数时创建的,在第一次调用b函数时候传入的0。第二次调用b传入x==1,这时候c的引用环境已经与声明它的时候不同,

function b(temp,x){
    if(x<1){
        b(c,1);
    } else {
        temp();
    }
    
    function c(){
        console.log(x);
    }
}

function a(){
}

b(a,0);

试着改一改:

function b(temp,x){
    if(x<1){
        b(c,1);
    } else {
        temp();
    }
}
function a(){
}
// 报错,ReferenceError: Can't find variable: x
function c(){
    console.log(x);
}

b(a,0);

静态作用域,作用域取决于声明时候

再改一改:还是输出0

function b(temp,x){
    if(x<1){
        b(c,1);
    } else {
        x = 2 ;
        temp();
    }
    
    function c(){
        console.log(x);
    }
}

function a(){
}

b(a,0);

再改一改:输出2

function b(temp,x){
    if(x<1){
        b(c,1);
    } else {
        x = 2 ;
        temp(x);
    }
    
    function c(x){
        console.log(x);
    }
}

function a(){
}

b(a,0);

区别词法作用域与传值
当你重新写一个函数的时候,不传值而是采用外包围块的声明变量时候,注意这个问题,他不能获取动态作用域,而是最开始声明时候的静态作用域,比如最典型的这个问题

再改改输出4

 function b(temp,x){
    var xx=4;
    if(x<1){
        b(c,1);
    } else {
        x = 2 ;
        xx = 5;
        temp();
    }

    function c(){
        console.log(xx);
    }
}

function a(){
}

b(a,0);

注意区别函数表达式和声明式:
输出0,1,0

function b(temp,x){
    const c = function (){
        console.log(x);
    }
    if(x<1){
        b(c,1);
    } else {
        temp();
    }
    c();
}

function a(){
}

b(a,0);

应用

模拟私有变量

JavaScript没有固定的关键字来限定对象作用域中私有变量和函数访问。
闭包返回一些可访问任何外部局部变量的方法,但不会公开这些变量,实现私有化。

function N(){
    var feints = 0;
    this.getFeints = function(){
        return feints;
    };
    this.feint = function() {
        feints++;
    };
}
var n = new N();
n.feint();
console.log(n.feints);
console.log(n.getFeints());
//undefined
//1

模块化

模块模式,采用立即调用函数表达式IIFE,在封装内部变量的同时,允许对外公开必要的功能集合,从而减少全局引用。

创建人工块作用域变量

将代码块放入一个匿名函数中,并立即调用了这个匿名函数。
此时,在匿名函数中的变量在使用完之后就会被回收,在匿名函数外部是访问不到这些变量的。

异步服务端调用

循环中的计算值被异步操作的回调函数使用,因为循环中的计算是同步的,所以在异步回调之前就已经完成,导致多次异步的回调使用了同一个计算值(循环末尾的值)。
用一个立即执行的匿名函数封装异步事件

分析异步操作是在当前上下文堆栈中的同步操作执行完毕之后调用的,所以可以保证回调的时间分别是在每次循环中。而不是之前的循环完毕回调 。后续思考之后我觉得自己的这种想法是错误的。
正确的解释:
函数调用的参数默认是传值的,所以三个匿名函数分别拥有的ind值是每次循环的快照,而回调函数索引自己的作用域链找到的就是匿名函数
这也解释了第二个例子中我的疑问:为什么div2可以在div1前面调用?

var dataList = ['aa','bb','bb']
for(var ind in dataList){
    wx.request({
        url:'xxx',
        sucess:function(res){
            console.log(ind)  // 三次都是2
        }
    })
}

var dataList = ['aa','bb','bb']
for(var ind in dataList){
    (function(data){
        wx.request({
            url:'xxx',
            sucess:function(res){
                console.log(ind)
            }
        })
    })(ind)
}
// 通过闭包函数参数保存ind

不止服务器调用,包括点击事件等其他异步
页面上有5个div节点,通过循环来给每个div绑定onclick事件,按照索引顺序,点击第1个div时弹出0,点击第2个div时弹出1…
div节点的onclick事件是被异步触发的,同步执行的是对点击事件的定义,当事件被触发的时候,for循环结束变量i等于5,在div的onclick事件函数中顺着作用域链从内到外查找变量i时,查找到的值总是5
为什么div2可以在div1前面调用?

        var nodes = document.getElementsByTagName('div');
        /* 未使用闭包前 */
//      for(var i=0,len=nodes.length; i<len; i++) {
//          nodes[i].onclick = function() {
//              alert(i);
//          }
//      }
        for(var i=0,len=nodes.length; i<len; i++) {
        //自执行函数
            (function(i){
                nodes[i].onclick = function() {
                    alert(i);
                }
            })(i)
            
        }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值