关于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 相关文章推荐
Javascript中的var_dump函数实现代码
Sep 07 Javascript
js中的push和join方法使用介绍
Oct 08 Javascript
jQuery学习之prop和attr的区别示例介绍
Nov 15 Javascript
使用text方法获取Html元素文本信息示例
Sep 01 Javascript
Windows8下搭建Node.js开发环境教程
Sep 03 Javascript
jQuery表单验证功能实例
Aug 28 Javascript
BootStrap 页签切换失效的解决方法
Aug 17 Javascript
vue.js单文件组件中非父子组件的传值实例
Sep 13 Javascript
微信小程序使用wxParse解析html的方法示例
Jan 17 Javascript
JavaScript显式数据类型转换详解
Mar 18 Javascript
原生js+canvas实现贪吃蛇效果
Aug 02 Javascript
解决vue单页面应用打包后相对路径、绝对路径相关问题
Aug 14 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实现比较全的数据库操作类
2015/06/18 PHP
thinkphp5 加载静态资源路径与常量的方法
2017/12/24 PHP
js 绑定带参数的事件以及手动触发事件
2010/04/27 Javascript
让你的CSS像Jquery一样做筛选的实现方法
2011/07/10 Javascript
js实现上传图片预览的方法
2015/02/09 Javascript
jqueryUI里拖拽排序示例分析
2015/02/26 Javascript
在Ubuntu系统上安装Ghost博客平台的教程
2015/06/17 Javascript
javascript中tostring()和valueof()的用法及两者的区别
2015/11/16 Javascript
通过jquery实现页面的动画效果(实例代码)
2016/09/18 Javascript
详解vue项目构建与实战
2017/06/27 Javascript
node.js博客项目开发手记
2018/03/16 Javascript
深入浅析Vue.js计算属性和侦听器
2018/05/05 Javascript
jquery ajax加载数据前台渲染方式 不用for遍历的方法
2018/08/09 jQuery
Node.js npm命令运行node.js脚本的方法
2018/10/10 Javascript
js中this的指向问题归纳总结
2018/11/28 Javascript
微信小程序中为什么使用var that=this
2019/08/27 Javascript
layUI使用layer.open,在content打开数据表格,获取值并返回的方法
2019/09/26 Javascript
layui 数据表格 根据值(1=业务,2=机构)显示中文名称示例
2019/10/26 Javascript
vue 查看dist文件里的结构(多种方式)
2020/01/17 Javascript
vue修改Element的el-table样式的4种方法
2020/09/17 Javascript
[02:47]DOTA2英雄基础教程 野性怒吼兽王
2013/12/05 DOTA
Python 可爱的大小写
2008/09/06 Python
实例讲解python函数式编程
2014/06/09 Python
Python方法的延迟加载的示例代码
2017/12/18 Python
python opencv 直方图反向投影的方法
2018/02/24 Python
python使用筛选法计算小于给定数字的所有素数
2018/03/19 Python
Django中间件基础用法详解
2019/07/18 Python
任意一块网页内容实现“活”的背景(目前火狐浏览器专有)
2014/05/07 HTML / CSS
大学毕业通用个人的求职信
2013/12/08 职场文书
《自选商场》教学反思
2014/02/14 职场文书
班子群众路线教育实践个人对照检查材料思想汇报
2014/09/30 职场文书
综治目标管理责任书
2015/05/11 职场文书
祝寿主持词
2015/07/02 职场文书
小学体育组工作总结2015
2015/07/21 职场文书
vue+iview实现手机号分段输入框
2022/03/25 Vue.js
Django框架中视图的用法
2022/06/10 Python