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

目录
  • 作用域
  • 全局作用域
    • 作用域中的错误
  • 局部作用域
    • with
    • 弊端
    • 数据泄露
    • 性能下降
    • let
    • const
  • 作用域链
    • 闭包
    • 闭包对作用域链的影响
    • 匿名函数的赋值
    • 使用let

作用域

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

全局作用域

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

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

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

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

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

作用域中的错误

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

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

局部作用域

在局部作用域里面的变量通常是用到 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的值会从未定义到已赋值之间的转变呢?这个很危险的,毕竟这个时候已然出现数据泄露

首先,我们来分析上面的代码。例子中创建了 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();

在处理相同逻辑的代码中,没用 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

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进行修改的时候也会出错,提示不能对其进行修改

作用域链

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

代码中的 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模拟私有变量的一种方式。

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

闭包对作用域链的影响

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

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])

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

  • 返回匿名函数的赋值或者立即执行函数
  • 使用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的副本,互不影响。

使用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存在于局部作用域中,而不是再全局作用域。

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

这个循环本质上就是这样

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中的作用域内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • js 执行上下文和作用域的相关总结

    前言 如果你是或者你想成为一名合格的前端开发工作者,你必须知道JavaScript代码在执行过程,知道执行上下文.作用域.变量提升等相关概念,并且熟练应用到自己的代码中.本文参考了你不知道的JavaScript,和JavaScript高级程序设计,以及部分博客. 正文     1.JavaScript代码的执行过程相关概念 js代码的执行分为编译器的编译和js引擎与作用域执行两个阶段,其中编译器编译的阶段(预编译阶段)分为分词/词法分析.解析/语法分析.代码生成三个阶段.   (1)在分词/词法

  • JS难点同步异步和作用域与闭包及原型和原型链详解

    目录 JS三座大山 同步异步 同步异步区别 作用域.闭包 函数作用域链 块作用域 闭包 闭包解决用var导致下标错误的问题 投票机 闭包两个面试题 原型.原型链 原型对象 原型链 完整原型链图 JS三座大山 同步异步 前端中只有两个操作是异步的: 定时器异步执行; ajax异步请求 编译器解析+执行代码原理: 1.编译器从上往下逐一解析代码 2.判断代码是同步还是异步 同步:立即执行 异步:不执行.放入事件队列池 3.等所有同步执行完毕开始执行异步 同步异步区别 api : 异步有回调,同步没有

  • Vue.js slot插槽的作用域插槽用法详解

    目录 没有插槽的情况 Vue2.x 插槽 有插槽的情况 具名插槽 没有slot属性 插槽简单实例应用 作用域插槽 ( 2.1.0 新增 ) Vue3.x 插槽 插槽 作用域插槽 没有插槽的情况 <div id="app"> <child> <span>1111</span> </child> </div> <script> // 注册子组件 Vue.component("child"

  • 实例详解JavaScript静态作用域和动态作用域

    目录 前言 静态作用域与动态作用域 静态作用域执行过程 动态作用域执行过程 习题 习题一 习题二 习题三 总结 前言 在文章最开始,先学习几个概念: 作用域:<你不知道的js>中指出,作用域是一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找.简单来说,作用域规定了如何查找变量. 静态作用域:又称词法作用域,函数的作用域在函数定义的时候就决定了,通俗点说就是你在写代码时将变量和块作用域写在哪里决定的. 动态作用域:函数的作用域在函数调用时才决定的. 静

  • 图解JavaScript作用域链底层原理

    目录 前言 作用域 1.什么是作用域 2.[[Scopes]]属性 3.作用域链 4.图解查找变量原理 总结 前言 在学习JavaScript时大家一定都知道,外部空间不能访问内部变量,我们往往只知道这一基本规则,那实现这一基本规则的基本底层原理是什么呢?今天我将从小白的角度来带大家理解作用域链,希望能给大家一些帮助! 作用域 1.什么是作用域 简单来说,作用域(英文:scope)是据名称来查找变量的一套规则,可以把作用域通俗理解为一个封闭的空间,这个空间是封闭的,不会对外部产生影响,外部空间不

  • Javascript作用域与闭包详情

    目录 1.作用域 2.作用域链 3.词法作用域 5.闭包的应用 6.闭包的缺陷 7.高频闭包面试题 1.作用域 简单来说,作用域是指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限 在ES5中,一般只有两种作用域类型: 全局作用域:全局作用域作为程序的最外层作用域,一直存在 函数作用域:函数作用域只有在函数被定义时才会被创建,包含在父级函数作用域或全局作用域中 说完概念,我们来看下面这段代码: var a = 100 function test(){ var b = a * 2 var

  • JavaScript高级程序设计之变量与作用域

    目录 1.原始值与引用值 2.instanceof 3.作用域 1.原始值与引用值 6种简单数据类型的值都是原始值, 原始值通过变量赋值给另一个变量时,会复制一个出一个新的值,两者相互独立. let num1 = 5 let num2 = num1 引用值通过变量赋值给另一个变量时,也会复制一个值,这个值其实是一个指针(引用),该指针指向的还是同一个对象. let obj1 = new Object() let obj2 = obj1 既然是指向同一个引用对象,那么给obj1添加属性,也会作用到

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

    目录 作用域 全局作用域 作用域中的错误 局部作用域 with 弊端 数据泄露 性能下降 let const 作用域链 闭包 闭包对作用域链的影响 匿名函数的赋值 使用let 作用域 作用域,也就是我们常说的词法作用域,说简单点就是你的程序存放变量.变量值和函数的地方.根据作用范围不同可以分为全局作用域和局部作用域,简单说来就是,花括号 {}括起来的代码共享一块作用域,里面的变量都对内或者内部级联的块级作用域可见,这部分空间就是局部作用域,在 {}之外则是全局作用域. 全局作用域 在JavaSc

  • JS块级作用域和私有变量实例分析

    本文实例讲述了JS块级作用域和私有变量.分享给大家供大家参考,具体如下: 块级作用域 (function(){ //这里是块级作用域 })() 例如: (function(){ for(var i=0;i<5;i++){ alert(i);//0,1,2,3,4 } })(); alert(i);//error 上例中,定义了一个块级作用域,变量i在块级作用域中可见的,但是在块级作用域外部则无法访问. 这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数. 私有变

  • 详解js私有作用域中创建特权方法

    本文实例分享了js私有作用域中创建特权方法,供大家参考,具体内容如下 特权方法就是有权访问私有变量和私有函数的公有方法: function MyObject(){ var privateVariable = 10; function privateFunction(){ return false; } this.publicMethod = function(){ privateVariable ++; return privateFunction(); }; } var x = new MyO

  • 详解JavaScript的AngularJS框架中的作用域与数据绑定

    AngularJS 简介 AngularJS 是由 Google 发起的一款开源的前端 MVC 脚本框架,既适合做普通 WEB 应用也可以做 SPA(单页面应用,所有的用户操作都在一个页面中完成).与同为 MVC 框架的 Dojo 的定位不同,AngularJS 在功能上更加轻量,而相比于 jQuery,AngularJS 又帮您省去了许多机械的绑定工作.在一些对开发速度要求高,功能模块不需要太丰富的非企业级 WEB 应用上,AngularJS 是一个非常好的选择.AngularJS 最为复杂同

  • 详解angular中的作用域及继承

    在一些使用angular框架的大型项目中,似乎有很多个controller,而每个controller都有自己的$scope. 1.$rootscope $rootScope顶级作用域,也叫根作用域,类似于window,window的属性在任何子作用域都可以访问.$rootScope则是所有controller进行数据沟通的中间域,即在$rootScope中的数据,在每个controller中都能通过$rootScope.xxx获取到. 2.$scope (1)作用 $scope 就在视图和控制

  • 深入理解Javascript中的作用域链和闭包

    首先我们回顾下之前一篇关于介绍数组遍历的文章: 请先看上一篇中提到的for循环代码: var array = []; array.length = 10000000;//(一千万) for(var i=0,length=array.length;i<length;i++){ array[i] = 'hi'; } var t1 = +new Date(); for(var i=0,length=array.length;i<length;i++){ } var t2 = +new Date();

  • 深入浅析javascript中的作用域(推荐)

    所谓的作用域,可以简单理解为一个可以读.写的范围(区域),有些js经验的同学可能会说:"js没有块级作用域",js除了全局作用域外,只有函数可以创建作用域.作用域的一个好处就是可以隔离变量. 我们通过一些例子来帮助我们理解js中的作用域. alert(a); var a = 1; 如果对作用域一点不了解的同学可能会说 alert的是1或者报错:但实际上是undefined: 说到这里,我们首先说一下js逐行解析代码之前做的一些准备工作, js在逐行读代码之前,会做一些"预解析

  • 浅谈Nodejs中的作用域问题

    在JS中有全局作用域和函数作用域,而在Nodejs中也自己的作用域,分为全局作用域(global)和模块作用域. js作用域: 以前学js的时候我们的全局对象是window,如: var a = 10; console.log(window.a); 我们定义的全局变量默认是给window添加一个属性或者方法. function fn(){ var num = 22; } console.log(num); 报错,因为num是在函数中定义的,在函数外部是访问不了函数内部的变量的,虽然可以通过闭包来

  • 深入浅析JavaScript中的作用域和上下文

    javascript中的作用域(scope)和上下文(context)是这门语言的独到之处,这部分归功于他们带来的灵活性.每个函数有不同的变量上下文和作用域.这些概念是javascript中一些强大的设计模式的后盾.然而这也给开发人员带来很大困惑.下面全面揭示了javascript中的上下文和作用域的不同,以及各种设计模式如何使用他们. 上下文(Context)和作用域(Scope) 首先需要知道的是,上下文和作用域是两个完全不同的概念.多年来,我发现很多开发者会混淆这两个概念(包括我自己),

  • javascript中的作用域和闭包详解

    一.JavaScript作用域 JavaScript变量实际上只有两种作用域,全局变量和函数的内部变量.在函数内部任何一个地方定义的变量(var scope)其作用域都是整个函数体. 全局变量:指的是window对象下的对象属性. 作用域划分:基于上下文,以函数进行划分的,而不是由块划分的. 强调两点: 1. 在同一作用域中,JavaScript是允许变量的重复定义,并且后一个定义将覆盖前一个定义. 2. 函数内部如果不加关键字var而定义的变量,默认为全局变量. var scope="glob

随机推荐