深入理解JavaScript中的作用域、作用域链和闭包
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qappleh/article/details/80311443
作用域
先来谈谈变量的作用域
变量的作用域无非就是两种:全局变量和局部变量。
全局作用域:
最外层函数定义的变量拥有全局作用域,即对任何内部函数来说,都是可以访问的:
<script>
var outerVar = "outer";
function fn(){
console.log(outerVar);
}
fn();//result:outer
</script>
局部作用域:
和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,而对于函数外部是无法访问的,最常见的例如函数内部
<script>
function fn(){
var innerVar = "inner";
}
fn();
console.log(innerVar);// ReferenceError: innerVar is not defined
</script>
需要注意的是,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!
<script>
function fn(){
innerVar = "inner";
}
fn();
console.log(innerVar);// result:inner
</script>
再来看一个代码:
<script>
var scope = "global";
function fn(){
console.log(scope);//result:undefined
var scope = "local";
console.log(scope);//result:local;
}
fn();
</script>
很有趣吧,第一个输出居然是undefined,原本以为它会访问外部的全局变量(scope=”global”),但是并没有。这可以算是javascript的一个特点,只要函数内定义了一个局部变量,函数在解析的时候都会将这个变量“提前声明”:
<script>
var scope = "global";
function fn(){
var scope;//提前声明了局部变量
console.log(scope);//result:undefined
scope = "local";
console.log(scope);//result:local;
}
fn();
</script>
然而,也不能因此草率地将局部作用域定义为:用var声明的变量作用范围起止于花括号之间。
javascript并没有块级作用域
那什么是块级作用域?
像在C/C++中,花括号内中的每一段代码都具有各自的作用域,而且变量在声明它们的代码段之外是不可见的,比如下面的c语言代码:
for(int i = 0; i < 10; i++){
//i的作用范围只在这个for循环
}
printf("%d",&i);//error
但是javascript不同,并没有所谓的块级作用域,javascript的作用域是相对函数而言的,可以称为函数作用域:
<script>
for(var i = 1; i < 10; i++){
//coding
}
console.log(i); //10
</script>
作用域链(Scope Chain)
那什么是作用域链?
我的理解就是,根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问。
想要知道js怎么链式查找,就得先了解js的执行环境
执行环境(execution context)
每个函数运行时都会产生一个执行环境,而这个执行环境怎么表示呢?js为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。
全局执行环境是最外围的执行环境,全局执行环境被认为是window对象,因此所有的全局变量和函数都作为window对象的属性和方法创建的。
js的执行顺序是根据函数的调用来决定的,当一个函数被调用时,该函数环境的变量对象就被压入一个环境栈中。而在函数执行之后,栈将该函数的变量对象弹出,把控制权交给之前的执行环境变量对象。
举个例子:
<script>
var scope = "global";
function fn1(){
return scope;
}
function fn2(){
return scope;
}
fn1();
fn2();
</script>
上面代码执行情况演示:
了解了环境变量,再详细讲讲作用域链。
当某个函数第一次被调用时,就会创建一个执行环境(execution context)以及相应的作用域链,并把作用域链赋值给一个特殊的内部属性([scope])。然后使用this,arguments(arguments在全局环境中不存在)和其他命名参数的值来初始化函数的活动对象(activation object)。当前执行环境的变量对象始终在作用域链的第0位。
以上面的代码为例,当第一次调用fn1()时的作用域链如下图所示:
(因为fn2()还没有被调用,所以没有fn2的执行环境)
可以看到fn1活动对象里并没有scope变量,于是沿着作用域链(scope chain)向后寻找,结果在全局变量对象里找到了scope,所以就返回全局变量对象里的scope值。
标识符解析是沿着作用域链一级一级地搜索标识符地过程。搜索过程始终从作用域链地前端开始,然后逐级向后回溯,直到找到标识符为止(如果找不到标识符,通常会导致错误发生)—-《JavaScript高级程序设计》
那作用域链地作用仅仅只是为了搜索标识符吗?
再来看一段代码:
<script>
function outer(){
var scope = "outer";
function inner(){
return scope;
}
return inner;
}
var fn = outer();
fn();
</script>
outer()内部返回了一个inner函数,当调用outer时,inner函数的作用域链就已经被初始化了(复制父函数的作用域链,再在前端插入自己的活动对象),具体如下图:
一般来说,当某个环境中的所有代码执行完毕后,该环境被销毁(弹出环境栈),保存在其中的所有变量和函数也随之销毁(全局执行环境变量直到应用程序退出,如网页关闭才会被销毁)
但是像上面那种有内部函数的又有所不同,当outer()函数执行结束,执行环境被销毁,但是其关联的活动对象并没有随之销毁,而是一直存在于内存中,因为该活动对象被其内部函数的作用域链所引用。
具体如下图:
outer执行结束,内部函数开始被调用
outer执行环境等待被回收,outer的作用域链对全局变量对象和outer的活动对象引用都断了
像上面这种内部函数的作用域链仍然保持着对父函数活动对象的引用,就是闭包(closure)
闭包
闭包有两个作用:
第一个就是可以读取自身函数外部的变量(沿着作用域链寻找)
第二个就是让这些外部变量始终保存在内存中
关于第二点,来看一下以下的代码:
<script>
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){//注:i是outer()的局部变量
result[i] = function(){
return i;
}
}
return result;//返回一个函数对象数组
//这个时候会初始化result.length个关于内部函数的作用域链
}
var fn = outer();
console.log(fn[0]());//result:2
console.log(fn[1]());//result:2
</script>
返回结果很出乎意料吧,你肯定以为依次返回0,1,但事实并非如此
来看一下调用fn[0]()的作用域链图:
可以看到result[0]函数的活动对象里并没有定义i这个变量,于是沿着作用域链去找i变量,结果在父函数outer的活动对象里找到变量i(值为2),而这个变量i是父函数执行结束后将最终值保存在内存里的结果。
由此也可以得出,js函数内的变量值不是在编译的时候就确定的,而是等在运行时期再去寻找的。
那怎么才能让result数组函数返回我们所期望的值呢?
看一下result的活动对象里有一个arguments,arguments对象是一个参数的集合,是用来保存对象的。
那么我们就可以把i当成参数传进去,这样一调用函数生成的活动对象内的arguments就有当前i的副本。
改进之后:
<script>
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){
//定义一个带参函数
function arg(num){
return num;
}
//把i当成参数传进去
result[i] = arg(i);
}
return result;
}
var fn = outer();
console.log(fn[0]);//result:0
console.log(fn[1]);//result:1
</script>
虽然的到了期望的结果,但是又有人问这算闭包吗?调用内部函数的时候,父函数的环境变量还没被销毁呢,而且result返回的是一个整型数组,而不是一个函数数组!
确实如此,那就让arg(num)函数内部再定义一个内部函数就好了:
这样result返回的其实是innerarg()函数
<script>
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){
//定义一个带参函数
function arg(num){
function innerarg(){
return num;
}
return innerarg;
}
//把i当成参数传进去
result[i] = arg(i);
}
return result;
}
var fn = outer();
console.log(fn[0]());
console.log(fn[1]());
</script>
当调用outer,for循环内i=0时的作用域链图如下:
由上图可知,当调用innerarg()时,它会沿作用域链找到父函数arg()活动对象里的arguments参数num=0.
上面代码中,函数arg在outer函数内预先被调用执行了,对于这种方法,js有一种简洁的写法
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){
//定义一个带参函数
result[i] = function(num){
function innerarg(){
return num;
}
return innerarg;
}(i);//预先执行函数写法
//把i当成参数传进去
}
return result;
}
关于this对象
关于闭包经常会看到这么一道题:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());//result:The Window
《javascript高级程序设计》一书给出的解释是:
this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象调用时,this等于那个对象。不过,匿名函数具有全局性,因此this对象同常指向window
深入理解JavaScript中的作用域、作用域链和闭包的更多相关文章
- 深入理解javascript中执行环境(作用域)与作用域链
深入理解javascript中执行环境(作用域)与作用域链 相信很多初学者对与javascript中的执行环境与作用域链不能很好的理解,这里,我会按照自己的理解同大家一起分享. 一般情况下,我们把执行 ...
- 理解JavaScript中的作用域链
理解了作用域链,闭包就不难理解了,所以本文主要谈一谈我对作用域链的理解. 关于JavaScript中变量的作用域,全局变量在程序中始终都有定义.局部变量在声明它的函数体内以及其内部所嵌套的函数内始 ...
- 深入理解JavaScript中的作用域和上下文
介绍 JavaScript中有一个被称为作用域(Scope)的特性.虽然对于许多新手开发者来说,作用域的概念并不是很容易理解,我会尽我所能用最简单的方式来解释作用域.理解作用域将使你的代码脱颖而出,减 ...
- javascript中的this作用域详解
javascript中的this作用域详解 Javascript中this的指向一直是困扰我很久的问题,在使用中出错的机率也非常大.在面向对象语言中,它代表了当前对象的一个引用,而在js中却经常让我觉 ...
- 理解JavaScript中的原型继承(2)
两年前在我学习JavaScript的时候我就写过两篇关于原型继承的博客: 理解JavaScript中原型继承 JavaScript中的原型继承 这两篇博客讲的都是原型的使用,其中一篇还有我学习时的错误 ...
- 深入理解javascript中的立即执行函数(function(){…})()
投稿:junjie 字体:[增加 减小] 类型:转载 时间:2014-06-12 我要评论 这篇文章主要介绍了深入理解javascript中的立即执行函数,立即执行函数也叫立即调用函数,通常它的写法是 ...
- 理解 JavaScript 中的 this
前言 理解this是我们要深入理解 JavaScript 中必不可少的一个步骤,同时只有理解了 this,你才能更加清晰地写出与自己预期一致的 JavaScript 代码. 本文是这系列的第三篇,往期 ...
- 深入理解javascript中的立即执行函数
这篇文章主要介绍了深入理解javascript中的立即执行函数,立即执行函数也叫立即调用函数,通常它的写法是用(function(){…})()包住业务代码,使用jquery时比较常见,需要的朋友可以 ...
- 如何理解JavaScript中的this关键字
前言 王福朋老师的 JavaScript原型和闭包系列 文章看了不下三遍了,最为一个初学者,每次看的时候都会有一种 "大彻大悟" 的感觉,而看完之后却总是一脸懵逼.原型与闭包 可以 ...
- 【转】深入理解javascript中的立即执行函数(function(){…})()
javascript和其他编程语言相比比较随意,所以javascript代码中充满各种奇葩的写法,有时雾里看花,当然,能理解各型各色的写法也是对javascript语言特性更进一步的深入理解. ( f ...
随机推荐
- windows下zookeeper安装和使用
一,下载 可以到官网下载 官方主页: https://zookeeper.apache.org/ 二,安装 解压即可 三,配置 需要java环境,在加压出来的文件夹中找到zoo_sample.cfg文 ...
- MySQL中常用到的关于时间的SQL
-- 今天 SELECT DATE_FORMAT(NOW(),'%Y-%m-%d 00:00:00') AS dayStart;SELECT DATE_FORMAT(NOW(),'%Y-%m-%d 2 ...
- 深入理解Three.js中线条Line,LinLoop,LineSegments
前言 在可视化开发中,无论是2d(canvas)开发还是3d开发,线条的绘制应用都是比较普遍的.比如绘制城市之间的迁徙图,运行轨迹图等.本文主要讲解的是Three.js中三种线条Line,LineLo ...
- 用call或bind实现bind()
一.bind方法 让我们看一下MDN上对bind方法的解释 bind()方法创建一个新的函数,在bind()被调用时,这个新函数的this被bind的第一个参数指定,其余的参数将作为新函数的参数供调用 ...
- ELK系列(二):.net core中使用ELK
ELK安装好后,我们现在.net Core中使用一下,大体思路就是结合NLog日志组件将数据写入ELK中,其它语言同理. ELK的安装还是有些复杂的,我们也可以在Docker中安装ELK:docker ...
- Cocos Creator 3D 打砖块图文教程(一)
在线体验链接: http://example.creator-star.cn/block3d/ 上面图中是打砖块游戏的主要 3D 节点元素,Shawn 这两天在学习 Unity 与 Creator3D ...
- linux下安装配置go语言环境
1,golang中国下载go源码 http://www.golangtc.com/download 请对应系统版本号,linux-amd64.tar.gz为64位系统(推荐) ,linux-386 ...
- SpringBoot使用thymeleaf模板引擎引起的模板视图解析错误
Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as ...
- 我眼中的如何成为一名合格的PHP高级开发工程师
转眼间搞PHP已超过10年准确的说12年,混在帝都,也经历过大公司小公司,有个同行朋友问我,我就思考下,如何成为一名合格的PHP高级开发 所有的所有仅限综合知识,因为PHP其实内容也很多,有的人专门研 ...
- Centos 7.2天兔(Lepus 3.8)数据库监控系统部署
天兔(Lepus 3.8)数据库监控系统部署 转载自:https://blog.csdn.net/m0_38039437/article/details/79613260 一.安装LAMP基础环境 首 ...