关于JS中的作用域中的问题思考分享


Posted in Javascript onApril 06, 2022

作用域

作用域,也就是我们常说的词法作用域,说简单点就是你的程序存放变量、变量值和函数的地方。根据作用范围不同可以分为全局作用域和局部作用域,简单说来就是,花括号 {}括起来的代码共享一块作用域,里面的变量都对内或者内部级联的块级作用域可见,这部分空间就是局部作用域,在 {}之外则是全局作用域。

全局作用域

在JavaScript中,作用域是基于函数来界定的。也就是说属于一个函数内部的代码,函数内部以及内部嵌套的代码都可以访问函数的变量。

function test(a){
    var b = a * 2
    function test2(c){
        console.log(a ,b, c)
    }
    test2(b * 3)
}
test(4) // 4 8 24

关于JS中的作用域中的问题思考分享

我们不妨尝试着来为这套代码划分一下作用域,上面定义了一个函数test,里面嵌套了函数 test2。图中三个不同的颜色,对应三个不同的作用域:

  • ①对应着全局 scope,这里只有 test2
  • ②是 test2界定的作用域,包含a、b、bar
  • ③是bar界定的作用域,这里只有c这个变量。

在查询变量并作操作的时候,变量是从当前向外查询的。就上图来说,就是③用到了a会依次查询③、②、①。由于在②里查到了a,因此不会继续查①了。这个其实就是作用域链的查找方式,详细内容我们后续介绍。

作用域中的错误

这里顺便讲讲常见的两种error, ReferenceError TypeError。如上图,如果在test2里使用了d,那么经过查询③、②、①都没查到,那么就会报一个ReferenceError;

关于JS中的作用域中的问题思考分享

如果bar里使用了b,但是没有正确引用,如b.abc(),这会导致TypeError

关于JS中的作用域中的问题思考分享

局部作用域

在局部作用域里面的变量通常是用到 withletconst

with

对于with第一印象可能就是 with关键字的作用在于改变作用域,但并不代表这个关键字不好用,至少面试的时候大概率会可以被卷起来,如果你不常用的话。

with语句的原本用意是为逐级的对象访问提供命名空间式的速写方式,也就是说在指定的代码区域,直接通过节点名称调用对象。 with通常被当做重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。如下面代码

var obj = {a: 2, b: 2, c: 2};
with (obj) { 
      a = 5;
      b = 5;
      c = 5;  
}
console.log(obj) // {a: 5, b: 5, c: 5}

我们快速的创建了一个 obj对象,为了能快速改变obj的值我们可以通过 with的方式来进行修改,当然了,我们也可以通过逐行赋值的方式来进行,代码不够简洁就是了。话说回来,在这段代码中,我们使用了 with语句关联了 obj对象,这就意味着在 with代码块内部,每个变量首先被认为是一个局部变量,如果局部变量与 obj对象的某个属性同名,则这个局部变量会指向 obj对象属性。

弊端

在上面的例子中,我们可以看到, with可以很好地帮助我们简化代码。但生产环境中却很少见到,事实上并不是少见多怪,主要是不推荐使用,为啥嘞?原因如下:

  • 数据泄露
  • 性能下降

数据泄露

function test3(obj) {
	with (obj) {
		a = 2;
	}
}

var o1 = {
	a: 3
};

var o2 = {
	b: 3
}

foo(o1);
console.log(o1.a)

foo(o2);
console.log(o2.a);	
console.log(a);

在运行的过程中,我们可以看到,对于 o1.a, o2.a的回显结果都不奇怪,毕竟对于 o1.a来说a是在作用域中定义的,而 o2.a压根在o2中未定义,对于这个结果显而易见,但为何 a的值会从未定义到已赋值之间的转变呢?这个很危险的,毕竟这个时候已然出现数据泄露

关于JS中的作用域中的问题思考分享

首先,我们来分析上面的代码。例子中创建了 o1 o2两个对象。其中一个有 a属性,另外一个没有。 test3(obj)函数接受一个 obj的形参,该参数是一个对象引用,并对该对象引用执行了 with(obj){...}。在 with 块内部,对 a有一个词法引用,实际上是一个 LHS引用,将 2 赋值给了它。

当我们将 o1传递进去, a = 2赋值操作找到了 o1.a并将 2 赋值给它。而当 o2 传递进去,o2 并没有 a 的属性,因此不会创建这个属性, o2.a保持 undefined

但为什么对 o2的操作会导致数据的泄漏呢?

要回答这个问题则是需要了解 LHS查询的机制,后面有机会我们再展开来分享,基于LHS查询的原理分析,当我们传递 o2 with时, with所声明的作用域是 o2, 从这个作用域开始对 a 进行 LHS查询,在 o2 的作用域、foo(…) 的作用域和全局作用域中都没有找到标识符 a,因此在非严格模式下,会自动在全局作用域创建一个全局变量,在严格模式下,会抛出 ReferenceError异常。

性能下降

with 会在运行时修改或创建新的作用域,以此来欺骗其他在开发时定义的词法作用域。with的使用可以令代码更具有扩展性,虽然有数据泄漏的可能,但只要稍加注意就可以避免,除此之后,灵活运用难道不可以创造出很好地功能吗?事实上真的不能,不妨我们考察一下性能特点

function test4() {
	console.time("test4");
	var obj = {
		a: [1, 2, 3]
	};
	for(var i = 0; i < 100000; i++)
	{
		var v = obj.a[0];
	}
	console.timeEnd("test4");
}
test4();

function testWith() {
	console.time("testWith");
	var obj = {
		a: [1, 2, 3]
	};
	with(obj) {
		for(var i = 0; i < 100000; i++) {
			var v = a[0];
		}
	}
	console.timeEnd("testWith");
}

testWith();

关于JS中的作用域中的问题思考分享

在处理相同逻辑的代码中,没用 with的运行时间仅为 1.94 ms。而用 with的运用时间长达 44.13ms。

这是为什么呢?

原因是 JavaScript引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。

但如果引擎在代码中发现了 with,它只能简单地假设关于标识符位置的判断都是无效的,因为无法知道传递给 with用来创建新词法作用域的对象的内容到底是什么。此时引擎的所有的优化努力大概率都是无意义的。因此引擎会采取最简单的做法就是完全不做任何优化。这种情况下,设想我们代码大量使用 with或者 eval(),那么运行起来一定会变得非常慢。无论引擎多聪明,努力将这些悲观情况的副作用限制在最小范围内,也无法避免代码会运行得更慢的事实。┑( ̄Д  ̄)┍

let

在局部作用域中,关键字let、const倒是很常见了,先说说说let,其是ES6新增的定义变量的方法,其定义的变量仅存在于最近的{}之内。

var test5 = true;
if (test5) {
    let bar = test5 * 2;
    console.log( bar );
}
console.log( bar ); // ReferenceError

关于JS中的作用域中的问题思考分享

const

与let一样,唯一不同的是const定义的变量值不能修改

var test6 = true;
if (test6) {
    var a = 2;
    const b = 3; 
    a = 3;
    b = 4; 
}
console.log( a ); 
console.log( b );

对于a来说是全局变量,而对于b的作用范围仅仅是存在与 if的块内,此外从尝试对b进行修改的时候也会出错,提示不能对其进行修改

关于JS中的作用域中的问题思考分享

作用域链

在局部作用中,引用一个变量后,系统会自动在当前作用域中寻找var的声明语句,如果找到则直接使用,否则继续向上一级作用域中去寻找var的声明语句,如未找到,则继续向上级作用域中寻找…直到全局作用域中如还未找到var的声明语句则自动在全局作用域中声明该变量。我们把这种链式的查询关系就称之为"作用域链"。这个寻找的过程也是可以在局部作用域中可以引用全局变量的答案

关于JS中的作用域中的问题思考分享

代码中的 testInner2函数中没有对变量a进行赋值操作,因此由内到外一层层寻找,发现在 testInner中有 var a的赋值操作,由此返回a的赋值,有兴趣的读者不妨把 testInner里面的赋值操作去掉,可以发现函数运行返回 a的赋值是 yerik

其实作用域链本质是一个对象列表,其保证了变量对象可以有序的访问。其开始的地方是当前代码执行环境的变量对象,常被称之为“活跃对象”(AO),变量的查找会从第一个链的对象开始,如果对象中包含变量属性,那么就停止查找,如果没有就会继续向上级作用域查找,直到找到全局对象中,如果找不到就会报 ReferenceError

闭包

简单的说就是一个函数内嵌套另一个函数,这就会形成一个闭包。请牢记这句话:“无论函数是在哪里调用,也无论函数是如何调用的,其确定的词法作用域永远都是在函数被声明的时候确定下来的”

function test7() {
    var a = 2;
    function test8() {
        console.log( a ); // 2
    }
    test8();
}
test7();

我们看到上面的函数 test7里嵌套了 test8,这样 test8就形成了一个闭包。在 test8内可以访问到任何属于 test7的作用域内的变量。

function test7() {
    var a = 2;
    function test8() {
        console.log( a ); // 2
    }
    return test8;
}
var test9 = test7();
test9(); // 2

在第8行,我们执行完 test7()后按理说垃圾回收器会释放test7的词法作用域里的变量,然而没有,当我们运行 test9()的时候依然访问到了 test7中a的值。这是因为,虽然 test7()执行完了,但是其返回了 test8并赋给了 test9 test8依然保持着对 test7形成的作用域的引用。这就是依然可以访问到 test7中a的值的原因。再想想,“无论函数是在哪里调用,也无论函数是如何调用的,其确定的词法作用域永远都是在函数被声明的时候确定下来的”。

我们再来看另一个例子

function createClosure(){
    var name = "yerik";
    return {
        setStr:function(){
            name = "naug";
        },
        getStr:function(){
            return name + ":hello";
        }
    }
}
var builder = new createClosure();
builder.setStr();
console.log(builder.getStr());

关于JS中的作用域中的问题思考分享

上面在函数中返回了两个闭包,这两个闭包都维持着对外部作用域的引用,因此不管在哪调用都是能够访问外部函数中的变量。在一个函数内部定义的函数,闭包中会将外部函数的自由对象添加到自己的作用域中,所以可以通过内部函数访问外部函数的属性,这就是js模拟私有变量的一种方式。

注意:由于闭包会拓展附带函数的作用域(内部匿名函数携带外部函数的作用域),因此,闭包会比其他函数多占用些内存空间,过度使用会导致内存占用增加,这个时候如果要对性能进行优化可能会增加一些难度。

闭包对作用域链的影响

由于作用域链机制的影响,闭包只能取得内部函数的最后一个值,这引起了一个副作用,如果内部函数在一个循环中,那么变量的值始终为最后一个值。

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}
console.log(data[0])
console.log(data[1])
console.log(data[2])

关于JS中的作用域中的问题思考分享

如果我们想要获取循环过程的中的结果,应该要怎么做呢?

  • 返回匿名函数的赋值或者立即执行函数
  • 使用es6的let

匿名函数的赋值

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (num) {
      return function(){
          console.log(num);
      }
  })(i);
}
console.log(data[0])
console.log(data[1])
console.log(data[2])

无论上是立即执行函数还是返回一个匿名函数赋值,原理上都是因为变量的按值传递,所以会将变量i的值赋值给实参num,在匿名函数的内部又创建了一个用于访问num的匿名函数,这样每一个函数都有一个num的副本,互不影响。

关于JS中的作用域中的问题思考分享

使用let

var data = [];

for (let i = 0; i < 3; i++) {
  data[i] = (function (num) {
      return function(){
          console.log(num);
      }
  })(i);
}
console.log(data[0])
console.log(data[1])
console.log(data[2])

前面我们介绍到let主要是作用域局部变量,由于其的存在,使for中的i存在于局部作用域中,而不是再全局作用域。

关于JS中的作用域中的问题思考分享

这个函数表执行完毕,其中的变量会被销毁,但是因为这个代码块中存在一个闭包,闭包的作用域链中引用着局部作用域,所以在闭包被调用之前,这个块级作用域内部的变量不会被销毁。

这个循环本质上就是这样

var data = [];// 创建一个数组data;

{ 
	// 进入第一次循环
	let i = 0; // 注意:因为使用let使得for循环为局部作用域
	           // 此次 let i = 0 在这个局部作用域中,而不是在全局环境中
    data[0] = function() {
    	console.log(i);
	};
}
{ 
    // 进入第二次循环
	let i = 1; // 因为 let i = 1 和上面的 let i = 0     
	           // 在不同的作用域中,所以不会相互影响
	data[1] = function(){
         console.log(i);
	}; 
}
...

当我们执行 data[1]()的时候,相当于是进入了以下的执行环境

{ 
     let i = 1; 
     data[1] = function(){
          console.log(i);
     }; 
}

在上面这个执行环境中,它会首先寻找该执行环境中是否存在i,没有找到,就沿着作用域链继续向上找,在其所在的块级作用域执行环境中,找到i=1,于是输出1。

到此这篇关于关于JS中的作用域中的问题思考分享的文章就介绍到这了,更多相关JS中的作用域内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
JQuery中对服务器控件 DropdownList, RadioButtonList, CheckboxList的操作总结
Jun 28 Javascript
如何用jQuery实现ASP.NET GridView折叠伸展效果
Sep 26 Javascript
深入浅析AngularJS和DataModel
Feb 16 Javascript
JS监听微信、支付宝等移动app及浏览器的返回、后退、上一页按钮的事件方法
Aug 05 Javascript
微信小程序  modal弹框组件详解
Oct 27 Javascript
12 款 JS 代码测试必备工具(翻译)
Dec 13 Javascript
jquery pagination分页插件使用详解(后台struts2)
Jan 22 Javascript
node.js利用redis数据库缓存数据的方法
Mar 01 Javascript
js实现4个方向滚动的球
Mar 06 Javascript
详解.vue文件中监听input输入事件(oninput)
Sep 19 Javascript
微信小程序日历效果
Dec 29 Javascript
详解50行代码,Node爬虫练手项目
Apr 22 Javascript
JavaScript中的LHS和RHS分析详情
vue中控制mock在开发环境使用,在生产环境禁用方式
Apr 06 #Vue.js
vue3使用vuedraggable实现拖拽功能
vue整合百度地图显示指定地点信息
vue中使用mockjs配置和使用方式
VUE使用draggable实现组件拖拽
Apr 06 #Vue.js
教你部署vue项目到docker
You might like
PHP中使用imagick生成PSD文件缩略图教程
2015/01/26 PHP
CI框架简单邮件发送类实例
2016/05/18 PHP
解决Laravel5.x的php artisan migrate数据库迁移创建操作报错SQLSTATE[42000]
2020/04/06 PHP
thinkphp5.1 框架导入/导出excel文件操作示例
2020/05/25 PHP
IE和FireFox(FF)中js和css的不同
2009/04/13 Javascript
让JavaScript拥有类似Lambda表达式编程能力的方法
2010/09/12 Javascript
jquery中ajax学习笔记3
2011/10/16 Javascript
JS实现获取来自百度,Google,soso,sogou关键词的方法
2016/12/21 Javascript
js模态对话框使用方法详解
2017/02/16 Javascript
Node.JS 循环递归复制文件夹目录及其子文件夹下的所有文件
2017/09/18 Javascript
webpack打包并将文件加载到指定的位置方法
2018/02/22 Javascript
vue 纯js监听滚动条到底部的实例讲解
2018/09/03 Javascript
Angularjs Ng_repeat中实现复选框选中并显示不同的样式方法
2018/09/12 Javascript
Vue源码中要const _toStr = Object.prototype.toString的原因分析
2018/12/09 Javascript
微信小程序自定义胶囊样式
2020/12/27 Javascript
Python  连接字符串(join %)
2008/09/06 Python
Python利用正则表达式匹配并截取指定子串及去重的方法
2015/07/30 Python
利用selenium 3.7和python3添加cookie模拟登陆的实现
2017/11/20 Python
Python numpy 常用函数总结
2017/12/07 Python
python3通过selenium爬虫获取到dj商品的实例代码
2019/04/25 Python
python 单线程和异步协程工作方式解析
2019/09/28 Python
python-docx文件定位读取过程(尝试替换)
2020/02/13 Python
pytorch使用tensorboardX进行loss可视化实例
2020/02/24 Python
PyCharm 在Windows的有用快捷键详解
2020/04/07 Python
Python学习之os模块及用法
2020/06/03 Python
利用Python中的Xpath实现一个在线汇率转换器
2020/09/09 Python
关于django python manage.py startapp 应用名出错异常原因解析
2020/12/15 Python
Ado与Ado.net的相同与不同
2014/12/08 面试题
C#如何调用Windows程序打开一个文档
2014/12/26 面试题
Linux如何修改文件和文件夹的权限
2013/09/05 面试题
机关财务管理制度
2014/01/17 职场文书
人事助理自荐信
2014/02/02 职场文书
小松树教学反思
2014/02/11 职场文书
大专学生求职自荐信
2014/07/06 职场文书
2016年五四青年节校园广播稿
2015/12/17 职场文书
MySQL 表空间碎片的概念及相关问题解决
2021/05/07 MySQL