作用域

143 阅读6分钟

执行环境

执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个相关联的变量对象,环境中定义的变量和函数都保存在这个对象中。

执行上下文

JavaScript 代码是按顺序从上到下被解析的,当然 JavaScript 引擎并非逐行的分析和执行代码,而是逐段的去分析和执行。当执行一段代码时,先进行预处理,如变量提升、函数提升等。

js可执行代码的类型有哪些:

  • 全局代码:例如加载外部的js文件或者本地标签内的代码。全局代码不包括 function 体内的代码
  • 函数代码:function体内的代码
  • eval代码:eval()函数计算某个字符串,并执行其中的js代码。比如eval("alert('hello world')")。虽然很强大,但实际用得很少,不讨论。

每执行一段可执行代码,都会创建对应的执行上下文。在脚本中可能存在大量的可执行代码段,所以 JavaScript 引擎先创建执行上下文栈,来管理脚本中所有执行上下文。

执行上下文是评估和执行javascript代码的环境的抽象概念。每当javascript代码在运行的时候,它都是在执行上下文中运行。执行上下文可以理解为当前代码的执行环境,它会形成一个作用域

JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码编译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。

执行上下文栈

在一个javascript程序中,必定会产生多个执行上下文,javascript引擎会以栈的方式来处理它们,也就是执行上下文栈(很多文章可能会称它为执行栈,执行上下文堆栈,函数调用栈)。

执行上下文链接:www.cnblogs.com/hezhi/p/100…

变量对象和活动对象链接:www.cnblogs.com/hezhi/p/100…

一、作用域

作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。

    function foo() {      var studentName = "亮仔";      console.log(studentName);//亮仔    }    foo();    console.log(studentName);//Uncaught ReferenceError: studentName is not definedju

上面的例子中,studentName在全局作用域中没有声明,所以取值时会报错,变量在函数内声明,在函数中有局部作用域,只能在函数内部访问。

作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了‘块级作用域’,可通过新增命令 let 和 const 来体现。

全局作用域:在代码中任何地方都能访问到的变量拥有全局作用域。

一般情况下,window 对象的内置属性都拥有全局作用域。

全局作用域有个弊端:如果我们写了很多行 JS 代码,变量定义都没有用函数包括,那么它们就全部都在全局作用域中。这样就会 污染全局命名空间, 容易引起命名冲突。

函数作用域:声明在函数里面的变量。局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部。

注意:在js中,块语句(大括号“{}”中间的语句),如 if 和 switch 条件语句或 for 和 while 循环语句,不像函数,它们不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中。

块级作用域:ES6中通过let和const来声明指定块的作用域外无法被访问。块级作用域在如下情况下被创建:

(1)在函数内部;(2)在一个代码块(由一对花括号包裹)内部;

let与var的用法一样,不同点是声明的变量只在代码块内有效,类似于C,C++,JAVA局部变量的概念。它有以下特点:

(1)不存在变量提升

var 声明时存在变量提升的现象,即变量可以在声明之前使用,值为undefined

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

也就是let声明的变量,必须先定义才能使用。

(2)不允许重复声明

var a = 123;  var a = 123; //  可以实现
let b = 123;let b = 123; //  Identifier 'a' has already been declared   

(3)暂时性死区(同一变量名存在的内部变量屏蔽外部变量)

在被let修饰的同名变量下,根据就近原则,内部变量屏蔽外部变量。

    let a = 456;    {      let a = 123;      console.log(a);//123    }    console.log(a);//456

for与块级作用域

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

上面的代码,我们预期是分别输出数字0-4。但实际代码在运行时会输出五次5。

首选,延迟器函数的回调在循环结束时才执行。而在我们每次循环时五个函数是在迭代时分别定义的,但它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i,所有导致每次输出是5。最简单的方法是使用let声明(使用闭包也可以),每次迭代都会生成一个新的块级作用域,i的值才会正确。

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

with和try/catch也会创建块作用域,这里不多做介绍。

二、作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途是当变量取值在当前作用域未取到值时,就会去上级作用域查找,直到全局作用域,这样一个查找过程形成的链条叫做作用域链。

    var a=5;    function foo(){        var a=10;        var b=20;        console.log(a);//10        (function bar(){            console.log(b)//20        })()    }    foo()
console.log(a);//5

注意:内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境的任何变量和函数。这些环境之间的联系都是线性的、有次序的。每个进入环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入下一个执行环境。

延长作用域链

(1)with语句;

(2)try-catch语句中的catch块;

以上两种情况都会延长作用域链。

    function bar(obj){        a=1;        with(obj){            b=a        }        return b;    }    var o1={        a:3    }    console.log(bar(o1))

with接受了obj对象,当发现a是它的属性时,停止查找,所以b等于3。

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

with代码块中,javascript引擎对变量的处理方式是:先查找是不是该对象的属性,**如果是,则停止。**如果不是继续查找是不是局部变量。

对于catch语句来,try中的代码捕获到错误以后,会把异常对象推入一个可变对象并置于用域的头部,在catch代码块内部,函数的所有局部变量将会被放在第二个作用域对象中,catch中的代码执行完,会立即销毁当前作用域。

三、闭包

官方解释:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。(函数就是一个表达式)  

概念:指函数外部访问函数作用域中变量(局部变量)的函数;

是指有权访问另一个函数作用域中变量的函数。

    function foo(){        var a=2;        function bar(){            console.log(a)        }    return bar;    }    var baz=foo();    baz();

上面就是一个闭包的例子。一般情况下,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象);而闭包有所不同,foo()函数内部定义的函数bar()作用域中,会包含foo()的活动对象,所以当执行foo()函数后,返回的bar的内部作用域一直存在,导致foo()的活动对象的一直在内存中,当bar执行完后,foo()的活动对象才会被销毁。

**特点:**

1.函数嵌套函数。

2.函数内部可以引用外部的参数和变量。

3.参数和变量不会被垃圾回收机制回收

闭包的作用:

正常函数执行完毕后,里面声明的变量被垃圾回收处理掉,但是闭包可以让作用域里的 变量,在函数执行完之后依旧保持没有被垃圾回收处理掉 。

(1)可以读取函数内部的变量;

(2)让这些变量的值始终保持在内存中;

(3) 增加块级作用域;

优点:

1:变量长期驻扎在内存中;

2:避免全局变量的污染;

3:私有成员的存在 ;

使用闭包的注意事项:

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。
* 闭包会在父函数外部改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。