JS作用域与闭包

在JavaScript中,作用域是可访问变量,对象,函数的集合。

变量分为全局变量和局部变量。全局变量在函数外定义,HTML中全局变量是window对象,所有数据对象都属于window对象。局部变量在函数内定义,只能在函数内部访问,在函数开始执行时创建,在函数执行完之后会自动销毁。

JS的作用域分为全局作用域和函数作用域。

全局作用域

全局作用域在页面打开时创建,在页面关闭时销毁。在全局作用域中,创建的变量都会作为window对象的属性保存;创建的函数都会作为window对象的方法保存。

变量在函数外定义就是全局变量,在全局作用域中有一个全局对象window,可以直接使用。

全局作用域中的变量都是全局变量,在页面的任意部分都可以访问到。

var a=10;
console.log(window.a); //相当于console.log(a); function fun(){
console.log('我是fun函数');
}
window.fun();

变量声明提前

使用var关键字声明的变量,会在所有的代码执行之前被声明,但是不会被赋值

但是如果声明变量时不使用var关键字,则变量不会被声明提前

console.log("a="+a);  //a=undefined
var a=123;

如果是访问了未声明的变量,控制台会报错:xxx is not defined

但是在这个例子中,我们可以发现在变量a声明之前,依然可以访问到a这个变量,只是这个变量的类型是undefined,还没有被赋值。

使用函数声明形式创建的函数 function 函数(){} 会在所有的代码执行之前就被创建,所以可以在函数声明前被调用。

使用函数表达式创建的函数,不会被声明提前,所以不能在声明前创建。

函数作用域

调用函数时创建函数作用域,函数执行完毕后,函数作用域销毁。

每调用一次函数就会创建一个新的函数作用域,他们之间是互相独立的。

在函数作用域中,可以访问到全局作用域的变量,在全局作用域中无法访问到函数作用域的变量。

var a=10
function fun(){
var b = 20
console.log("a="+a); //a=10
}
fun()
console.log("b="+b); //b is not defined

在这个例子中,a使全局变量,可以在函数内部被访问到;b是定义在函数内部的局部变量,在函数执行完之后这个变量会被自动销毁,所以在函数外访问不到变量b。

当在函数作用域操作一个变量时,会现在自身作用域中寻找,如果有就直接使用,如果没有则向上一级作用域中寻找

在函数作用域中也有声明提前的特性

使用var关键字声明的变量,会在函数中所有的代码执行之前被声明

function fun3(){
console.log(a);
var a= 35;
}
fun3(); //undefined

在函数中,不使用var声明的变量都会成为全局变量。

下面的例子中,变量myName在函数内没有使用var关键字声明,为全局变量。

function fun4(){
myName = "xiaoming";
}

作用域链

在介绍作用域链之前我们需要了解以下几个概念:

①执行环境(execution context):定义了变量或函数有权访问的其他数据,决定了他们各自的行为

②每个执行环境都有一个与之关联的变量对象(variable object),环境中所有定义的变量和函数都保存在这个对象中(下面会用VO()来表示一个变量对象)

③每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链是由当前环境与上层环境的一系列变量对象组成,保证了当前执行环境对符合访问权限的变量和函数的有序访问。

我们可以分析一个例子

var a = 30;
function test() {
var b = a + 10;
function innerTest() {
var c = 10;
return b + c;
}
return innerTest();
}
test();

这里有三个变量对象,我们把全局、test函数,innerTest()的变量对象分别用VO(global)、VO(test)、VO(innerTest)。对于innerTest来说,他的作用域链包含了这三个变量对象,所以可以用数组将innerTest的作用域链进行表示:

scopeChain:[VO(innerTest),VO(test),VO(global)]

数组的第一项是作用域链的最前端,最后一项是作用域链的末端。

作用域链的前端,始终都是当前执行的代码所在的环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象(下面会用AO()来表示一个活动对象)。活动对象在其最开始时只包含一个变量,即arguments对象(这个对象在全局环境中时不存在的)。作用域链的末端始终为全局变量对象。



作用域链是由一系列变量对象组成,我们可以在这条链中,查询变量对象中的标识符,这样就可以访问到上一层作用域中的变量了。

在这个例子中,innerTest()内部能够访问到其他两个环境中的所有变量,即能够访问到的变量有:变量c、变量b和变量a。因为这两个环境是它的父执行环境。



图中的矩形表示特定的执行环境。其中,内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。这些环境之间的联系是线性的、有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。例如:innerTest()的局部环境开始时会先在自己的变量对象中搜索变量和函数名,如果搜索不到则再搜索上一级作用域链。test()的作用域链中只包含两个对象:自己的变量对象和全局变量对象。也就是说,它不能访问innerTest()的环境。

闭包

闭包是一个可以访问外部(封闭)函数作用域链中变量的内部函数。闭包可以访问3种范围中的变量,这3个范围具体如下:

  • 自己范围内的变量
  • 封闭函数范围内的变量
  • 全局变量

创建闭包的常见方式,就是在一个函数内部创建另一个函数。

function createComparisonFunction(propertyName) {
return function(object1, object2) {
var value1 = object1[propertyName];
var value2 = object2[propertyName]; if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}

在这个例子中内部函数(匿名函数)访问了外部函数中的变量propertyName。即使这个内部函数被返回了,而且是在其他地方被调用了,但它仍然可以访问变量propertyName。因为内部函数的作用域链中包含createComparisonFunction()的作用域。

一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是闭包的情况又不同。

在匿名函数从createComparisonFunction()中被返回后,它的作用域链被初始化为包含createComparisonFunction()函数的活动对象和全局变量对象。这样匿名函数就可以访问在createComparisonFunction()中定义的所有变量。更为重要的是createComparisonFunction()函数在执行完毕之后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。即:当createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中,直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁。

//创建函数
var compareNames = createComparisonFunction("name");
//调用函数
var result = compareNames({name: "mike"}, {name: "lisa"});
//解除对匿名函数的引用(释放内存)
compareNames = null;

首先,创建的比较函数被保存在变量compareNames中,通过将compareNames设置为null解除该函数的引用,将其清除。随着匿名函数的作用域链被销毁,其他作用域(除了全局作用域)也都可以安全的被销毁了。下图展示了调用compareNames()的过程中产生的作用域链之间的关系。



作用域链的配置机制引出了一个问题,就是闭包只能取得包含函数中任何变量的最后一个值。闭包所保存的是整个变量对象,而不是某个特殊的变量。

function createFunction() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function() {
return i;
};
}
return result;
}

这个函数会返回一个函数数组。表面上看,每个函数都应该返回自己的索引值。即位置0的函数返回0,位置1的函数返回1。但实际上,每个函数都会返回10。因为每个函数的作用域链中都保存着ceateFunction()函数的活动对象,所以他们引用的都是同一个变量 i 。当ceateFunction()函数返回后,变量 i 的值是10,此时每个函数都引用着保存变量 i 的同一个变量对象,所以在每个函数内部 i 的值都是10。

我们可以通过创建另一个匿名函数强制让闭包的行为符合预期。

function createFunction() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function(num) {
return function() {
return num;
};
}(i);
}
return result;
}

我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋值给数组。匿名函数有一个参数num,也就是最终的函数要返回的值。在调用每一个匿名函数时,传入了变量 i 。由于函数参数时按值传递的,所以就会将变量 i 的值赋值给参数num。在这个匿名函数的内部,又创建并返回了一个返回num的闭包。这样,result数组中的每个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值了。

闭包的优点:不产生全局变量,可以避免全局变量的污染,实现属性私有化

闭包的缺点:会常驻内存,增加内存使用量,使用不当很容易造成内存泄漏,在不用的时候需要删除

闭包有3个特性:

  1. 函数嵌套函数
  2. 在函数内部可以引用外部的参数和变量
  3. 参数和变量不会以垃圾回收机制回收

最后来练习一下:

for(var i=0;i<5;i++){
(function(){
setTimeout(function(){
console.log(i);
},i*1000);
})();
}

上面的代码会显示5 5 5 5 5。

原因是,在循环中执行的每个函数将整个循环完成之后执行,因此会引用存储在i中的最后一个值——5

闭包可以为每次迭代创建一个唯一的作用域,存储作用域内的循环变量。

for(var i=0;i<5;i++){
(function(x){
setTimeout(function(){
console.log(x);
},x*1000);
})(i);
}

上述写法会按预期输出0、1、2、3和4到控制台。

JS作用域与闭包的更多相关文章

  1. js——作用域和闭包

    1. js是编译语言,但是它不是提前编译,编译结果不能在分布式系统中移植.大部分情况下,js的编译发生在代码执行前的几微秒(甚至更短) 2. 一般的编译步骤   分词/词法分析:把字符串分解成词法单元 ...

  2. js作用域及闭包

    作用域 执行环境是js最为重要的一个概念.执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为. 1.全局执行环境就是最外围的一个执行环境,每一个函数都有自己的作用域 2.简单的说局部作用 ...

  3. JS作用域与闭包--实例

    <script> "use strict" //函数作用域 function func(){ var arr = [1,3,5,7,9]; var sum = 0; f ...

  4. 浅谈JS作用域和闭包

    函数表达式和函数声明 变量/函数声明都会提前 console.log(a) let a =1 那么打印出来的a为 undefined,因为会将a提到前面并赋予默认值undefined 函数声明:函数声 ...

  5. js闭包的作用域以及闭包案列的介绍:

    转载▼ 标签: it   js闭包的作用域以及闭包案列的介绍:   首先我们根据前面的介绍来分析js闭包有什么作用,他会给我们编程带来什么好处? 闭包是为了更方便我们在处理js函数的时候会遇到以下的几 ...

  6. JS教程:词法作用域和闭包 (网络资源)

    varclassA = function(){ ; } classA.prototype.func1 = function(){ var that = this, ; function a(){ re ...

  7. 原来JS是这样的 - 提升, 作用域 与 闭包

    引子 长久以来一直都没有专门学过 JS ,因为之前有自己啃过 C++ ,又打过一段时间的算法竞赛(写得一手好意大利面条),于是自己折腾自己的网站的时候,一直都把 JS 当 C 写.但写的时候总会遇到一 ...

  8. js面试题知识点全解(一作用域和闭包)

    问题: 1.说一下对变量提升的理解 2.说明this几种不同的使用场景 3.如何理解作用域 4.实际开发中闭包的应用 知识点: js没有块级作用域只有函数和全局作用域,如下代码: if(true){ ...

  9. 解析js中作用域、闭包——从一道经典的面试题开始

    如何理解js中的作用域,闭包,私有变量,this对象概念呢? 就从一道经典的面试题开始吧! 题目:创建10个<a>标签,点击时候弹出相应的序号 先思考一下,再打开看看 //先思考一下你会怎 ...

随机推荐

  1. 集合流之"交集(相同)和差集(区别的)"的使用

    一.需求 今天做的是将两个字符串转为数组后再转集合,然后利用集合的流stream来进行差集过滤 二.差集代码 差集:将两个集合相同的数据去掉,留下不同的数据 1 @Test 2 public void ...

  2. (stm32f103学习总结)—ADC模数转换实验

    一.STM32F1 ADC介绍 TM32F103 系列一般都有 3 个 ADC,这些 ADC 可以独立使用,也可 以使用双重(提高采样率).STM32F1 的 ADC 是 12 位逐次 逼近型的模拟数 ...

  3. PCB中加入任意LOGO图文说明 精心制作

    防静电图 首先我们要对下载下来的图片进行处理否则Altium designer6.9会提示装载的图片不是单色的,用Photoshop CS打开开始下载的图片 选择 图像→模式→灰度 在选择 图像→模式 ...

  4. Web前端初级问题—ajax登录跳转登录实现

    当我们的用户进行系统登录时,用户名和密码的验证都是后端验证的.而且,用户登录状态也是要后端设置的,查询数据库后,用户名和密码正确,则在session中存储一个uuid,每个页面需要根据登录状态判断展示 ...

  5. 深入解析丨母婴App如何迅速收割2W新用户?

    在讲案例前,我们需要先说一下精细化分析. 我们常说的精细化分析,就是一个持续"解构"的过程,通过像漏斗.留存.细分等高级分析功能,将"整体"按照事件属性解构成& ...

  6. c++语法拾遗,一些细节与特性

    写了2年多的C+STL的acmer,在学习<C++ primer>时总结的一些少见的语法特性与细节.总体还是和题目说的一样这是一篇 c++ 拾遗. 1 变量和基本类型 1.1 基本类型 1 ...

  7. python-爬楼梯

    [题目描述] 假设一段楼梯共n(n>1)个台阶,小朋友一步最多能上3个台阶,那么小朋友上这段楼梯一共有多少种方法. [练习要求]请给出源代码程序和运行测试结果,源代码程序要求添加必要的注释. [ ...

  8. Node Sass version 7.0.1 is incompatible with ^4.0.0

    网上一大堆, 什么降级node版本, 升级node-sass版本 , 再或者安装nvm来管理npm版本等等, 其实很烦 这边就两步: npm uninstall node-sass npm i -D ...

  9. PyQt5 基本语法(四)

    目录 2. 输入控件(一) 2.1 纯键盘 2.1.1 QLineEdit 2.1.1.1 描述 2.1.1.2 控件创建 2.1.1.3 输出模式 2.1.1.4 提示字符串 2.1.1.5 清空按 ...

  10. SSM实现个人博客-day03

    项目源码免费下载:SSM实现个人博客 有问题请循环vx:kht808 3.相关包与实体类的创建 1.包名与路径如下: 2.实体类的编写 Blog类 public class Blog implemen ...