作用域

定义:变量(变量作用域又称上下文)和函数生效(能够访问)的区域。即作用域控制着变量与函数的可见性和生命周期。

作用域分为两种:词法作用域和动态作用域。JS 使用的是词法作用域。

JS中使用的作用域 —— 词法作用域

词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。
无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。

函数作用域

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。

区分函数声明和表达式最简单的方法是看 function 关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果 function 是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。
函数表达式可以是匿名的,而函数声明则不可以省略函数名。

块作用域

with
用 with 从对象中创建出的作用域仅在 with 声明中而非外部作用域中有效。

try/catch
try/catch 的 catch 分句会创建一个块作用域,其中声明的变量仅在 catch 内部有效。

let
let 关键字可以将变量绑定到所在的任意作用域中。使用 let 进行的声明不会在 块作用域中进行提升。

const
const 同样可以用来创建块作用域变量,但其值时固定的。

作用域的提升

函数声明和变量声明都会被提升,函数声明会被提升到普通变量之前。

  1. // 例1
  2. function fn(a) {
  3. console.log(a); // function a() {}
  4. var a = 123;
  5. console.log(a); // 123
  6. function a() {}
  7. console.log(a); // 123
  8. var b = function() {}
  9. console.log(b); // function() {}
  10. function d() {}
  11. }
  12. fn(1);
  13. // 例2
  14. function bar() {
  15. return foo;
  16. foo = 10;
  17. function foo() {
  18. }
  19. var foo = 11;
  20. }
  21. console.log(bar()); // function foo() {}
  22. // 例3
  23. console.log(bar());
  24. function bar() {
  25. foo = 10;
  26. function foo() {
  27. }
  28. var foo = 11;
  29. return foo; // 11
  30. }

小知识:

LHS 查询和 RHS 查询 RHS 查询与简单地查找某个变量的值别无二致,而 LHS 查询则是试图找到变量的容器本身,从而可以对其赋值。 例如: console.log(a) 这里对 a 的引用是一个 RHS 引用,因为这里 a 并没有赋予任何值。而 a = 2; 对 a 的引用则是 LHS 引用,因为实际上我们并不关心当前的值是什么,只是先要为 =2 这个赋值操作找到一个目标。

如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。相较之下,当引擎执行 LHS 查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下。 如果 RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,比如试图对一个非函数类型的值进行函数调用,或者引用 null 或 undefined 类型的值中的属性,那么引擎会抛出另外一种类型的异常,叫作 TypeError 。 ReferenceError 同作用域判别失败相关,而 TypeError 则代表作用域判别成功了,但是对结果的操作是非法或不合理的。

[[scope]]:每个 JavaScript 函数都是一个对象,对象中有些属性我们可以访问,但有些不可以,这些属性仅供 JavaScript 引擎存取, [[scope]] 就是其中一个
[[scope]]指的就是我们所说的作用域,其中存储了运行期上下文的集合

运行期上下文:当函数执行时,会创建一个称为执行器上下文的内部对象。一个执行器上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,它所产生的执行上下文被销毁。

查找变量:从作用域链的顶端依次向下查找。

修改作用域的方法

eval

eval()不推荐使用:接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。在严格模式的程序中, eval() 在运行时有其自己的词法作用域,意味着其中的声明无法修改所在的作用域。

  1. function foo(str, a) {
  2. eval(str);
  3. console.log(a, b);
  4. }
  5. var b = 2;
  6. foo("var b = 3;", 1); // 1, 3

with

with:可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符。在严格模式被完全禁止。
eval() 函数如果接受了含有一个或多个声明的代码,就会修改其所处的词法作用域,而 with 声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域。
例子:

  1. function foo(obj) {
  2. with (obj) {
  3. a = 2;
  4. }
  5. }
  6. var o1 = { a: 3 };
  7. var o2 = { b: 3 };
  8. foo(o1);
  9. console.log(o1.a); // 2
  10. foo(o2);
  11. console.log(o2.a); // undefined
  12. console.log(a); // 2 —— 不好,a 被泄露到全局作用域上了

可以这样理解,当我们传递 o1 给 with 时, with 所声明的作用域是 o1 ,而这个作用域中含有一个同 o1.a 属性相符的标识符。但当我们将 o2 作为作用域时,其中并没有 a 标识符,o2 的作用域、 foo(..) 的作用域和全局作用域中都没有找到标识符 a ,因此当 a = 2 执行时,自动创建了一个全局变量。

如果代码中大量使用 eval(..) 或 with ,那么运行起来一定会变得非常慢。

执行上下文

就是某个函数或全局代码的执行环境,该环境中包含执行代码需要的所有信息。例如变量对象的定义、作用域链的扩展、提供调用者的对象引用等信息。可以简单的理解为执行上下文是一个对象,对象中包含了执行代码的全局信息。
包括:

  • 变量对象
  • 活动对象
  • 作用域链
  • 调用者信息

变量对象(VO)

变量对象是与执行上下文相关的数据作用域。它是一个与上下文相关的特殊对象,其中存储了在上下文中定义的变量和函数声明。也就是说,一般变量对象中会包含以下信息:

  • 变量
  • 函数声明
  • 函数的形参


    var 提升到最顶端
    arguments 中变量首先会覆盖 var 方式声明的变量
    函数声明覆盖相同名字的变量

  1. // 函数声明和函数表达式的区别
  2. // 这种叫做函数声明,会被加入变量对象
  3. function a() {}
  4. // b 是变量声明,也会被加入变量声明,但是作为表达式 _b 不会被加入变量对象
  5. var b = function _b() {}
  1. // 测试题
  2. console.log(foo)
  3. var foo = "A"
  4. var foo = function () {
  5. console.log("B")
  6. }
  7. console.log(foo)
  8. foo()
  9. function foo() {
  10. console.log("C")
  11. }
  12. console.log(foo)
  13. foo()

答案:
image.png

  1. var value = 2019
  2. function fn () {
  3. console.log(value) // undefined
  4. var value = {name: "Time"}
  5. console.log(value) // {name: "Time"}
  6. }
  7. fn()
  8. console.log(value) // 2019
  1. if(!(value in window)) {
  2. var value = 2019
  3. }
  4. console.log(value) // undefined
  1. console.log(fn) // fn() {}
  2. var fn = 2019
  3. console.log(fn) // 2019
  4. function fn() {}
  1. fn();
  2. console.log(v1) // ReferenceError: v1 is not defined
  3. console.log(v2)
  4. console.log(v3)
  5. function fn() {
  6. var v1 = v2 = v3 = 2019
  7. console.log(v1) // 2019
  8. console.log(v2) // 2019
  9. console.log(v3) // 2019
  10. }

活动对象(AO)

函数进入执行阶段时,原本不能访问的变量对象被激活成为一个活动对象,自此,我们可以访问到其中的各种属性。

作用域链

是函数执行上下文中的一部分。当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
[[scope]] 中所存储的执行器上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。

执行上下文在 ES5 中:

  • lexical environment:词法环境,当获取变量时使用。
  • variable environment:变量环境,当声明变量时使用。
  • this value:this 值。

在 ES2018 中,执行上下文增加了不少内容, this 值被归入 lexical environment

  • lexical environment:词法环境,当获取变量或者 this 值时使用。
  • variable environment:变量环境,当声明变量时使用。
  • code evaluation state:用于恢复代码执行位置。
  • Function:执行的任务是函数时使用,表示正在被执行的函数。
  • ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码。
  • Realm:使用的基础库和内置对象实例。
  • Generator:仅生成器上下文有这个属性,表示当前生成器。

闭包

简单理解一下,闭包其实只是一个绑定了执行环境的函数。
闭包与普通函数的区别是:它携带了执行的环境。

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

  1. function foo() {
  2. var a = 2;
  3. function bar() {
  4. console.log(a);
  5. }
  6. return bar;
  7. }
  8. var baz = foo();
  9. baz(); // 2 —— 这就是闭包的效果

函数 bar() 的词法作用域能够访问 foo() 的内部作用域。然后我们将 bar() 函数本身当作一个值类型进行传递。 拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后任意时间进行引用。bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。

函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。

如果将函数(访问它们各自的词法作用域)当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers 或者任何其它的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包。

  1. function wait(message) {
  2. setTimeout(function timer() {
  3. console.log(message);
  4. }, 1000);
  5. }
  6. wait("Hello, closure!");

会导致的问题

  • this 指向问题
  • 内存泄漏问题

    使用场景

  • 用闭包解决递归调用问题 ```javascript function factorial(num) { if(num<= 1) {

    1. return 1;

    } else {

    1. return num * factorial(num-1)

    } } var anotherFactorial = factorial factorial = null anotherFactorial(4) // 报错 ,因为最好是return num* arguments.callee(num-1),arguments.callee指向当前执行函数,但是在严格模式下不能使用该属性也会报错,所以借助闭包来实现

// 使用闭包实现递归 function newFactorial = (function f(num){ if(num<1) {return 1} else { return num* f(num-1) } }) //这样就没有问题了,实际上起作用的是闭包函数f,而不是外面的函数newFactorial

  1. - 用闭包解决块级作用域
  2. ```javascript
  3. for(var i=0; i<10; i++){
  4. console.info(i)
  5. }
  6. alert(i) // 变量提升,弹出10
  7. //为了避免i的提升可以这样做
  8. (function () {
  9. for(var i=0; i<10; i++){
  10. console.info(i)
  11. }
  12. })()
  13. alert(i) // underfined 因为i随着闭包函数的退出,执行环境销毁,变量回收
  • 让外部访问函数内部变量成为可能
  • 保护变量不被内存回收机制回收
  • 避免全局变量被污染,方便调用上下文的局部变量,加强封装性。

    解决闭包引起的内存泄漏问题

    例子:
    优化前: ```javascript function Cars(){ this.name = “Benz”; this.color = [“white”,”black”]; } Cars.prototype.sayColor = function(){ var outer = this; return function(){ return outer.color }; };

var instance = new Cars(); console.log(instance.sayColor()())

  1. 优化后:
  2. ```javascript
  3. function Cars(){
  4. this.name = "Benz";
  5. this.color = ["white","black"];
  6. }
  7. Cars.prototype.sayColor = function(){
  8. var outerColor = this.color; //保存一个副本到变量中
  9. return function(){
  10. return outerColor; //应用这个副本
  11. };
  12. outColor = null; //释放内存
  13. };
  14. var instance = new Cars();
  15. console.log(instance.sayColor()())

参考文章