《你不知道的JavaScript》之作用域和闭包

一、LHS和RHS查询

当变量出现在赋值操作的左侧时,进行LSH查询;当变量出现在赋值操作的右侧,即非左侧时,进行RSH查询。
或者说
LSH查询是在找变量容器的本身,从而对其赋值,RHS是查找某个变量的值
现在我们就要举很多例子来看看

console.log(b)
a = 2

console.log(...)需要一个引用才能执行,查是否有console,一次RSH,再从console对象中查是否有log方法,一次RHS。要打印出b,那就要查找b的值,这是一个RSH;为a赋值的时候,则要先找到a这个容器然后再赋值操作,这是一个LSH


function foo(a) {
    console.log(a);
}
foo(1)

② 对foo调用,就要去找foo的值,一次RSH查询;
实参到形参,a=2的操作,一次LSH查询;
打印a的时候,查询如①中所述;


function foo(a) {
    var b = a;
    return a + b;
}
var c = foo(1)

③ 查询foo,一次RHS
为c赋值操作c = ...,一次LHS
传参赋值操作a=...,一次LHS
查询a的值,一次RHS
为b进行赋值b=...,一次LHS
查询a和b的值,两次RHS


二、作用域

作用域是根据名称查找变量的一套规则,从当前的范围逐层往上查找,如下图所示,LSHRSH引用都会在当前作用域往上查。

来自你不知道的JavaScript

RHS在任何相关的作用域都取不到变量的值时,则会抛出ReferenceError异常。如果查到了这个变量,但是你对他进行了不合理的操作,比如:
① 对非函数进行调用
② 引用null或者undefined的属性
则会抛出TypeError异常
LHS在顶层的作用域都找不到变量的话,如果是非严格模式,则会帮我们默认创建一个,默认值为undefined,如果是严格模式,那就不好意思了,一样会抛出ReferenceError异常。

三、提升

(1)变量的提升

a = 1;
var a;
console.log(a)  >>> 1

console.log(b)  >>> undefined
var b = 2;     

var a的声明被提前了,所以能成功赋值,也能成功打印出来
var b = 2可以看成是①var b; ② b =2;第一步声明被提前了,第二步则留在原地,等到执行打印时候,b还未被赋值,所以是undefined
总结:声明会提升,赋值或者其他运行逻辑会留在原地
(2)函数的提升

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

等价于:

function foo() {
    var a; 
    console.log( a ); >>> undefined 
    a = 2; 
}
foo();

foo是一个函数的声明,则会将整个函数提升


但是如果是一个表达式

foo();  // TypeError!
bar();  // referenceError
var foo = function bar() { 
    // ... 
};

因为var foo会被先执行,则foo未定义,而被当作函数调用,则会出现TypeError异常,而且此时bar在赋值给foo之前,也不能在之前使用。
等价于:

var foo; 
foo(); // TypeError 
bar(); // ReferenceError 
foo = function() {
    var bar = ... 
    // ... 
}

举个例子:

foo(); // ->>> 1
var foo;
function foo() { 
    console.log( 1 );
}
foo = function() { 
    console.log( 2 ); 
};

foo(); // ->>>3
function foo() { 
    console.log( 1 ); 
}
var foo = function() { 
    console.log( 2 ); 
};
function foo() { 
    console.log( 3 ); 
}

第一段代码:对 foo函数的定义会被提前声明,而后面的var foo 重复定义则会被忽略,所以打印的是1
第二段代码:同样是函数提升,但是后面定义的会覆盖前面的,所以是打印出3

四、作用域闭包

函数与对其状态即词法环境lexical environment)的引用共同构成闭包closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在JavaScript,函数在每次创建时生成闭包。

来自MDN文档
那我们就来看看:
① 变量间接传递

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

首先调用了foo,可以看到这个函数的返回值也是一个函数barbar函数打印了foo作用域内的a的值。调用baz时候,可以正常打印出a的值。这样就间接访问到a了。
② 函数间接传递

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

foo被调用时,给fn全局变量赋值了,是名为bazfoo内部函数。调用bar,间接调用了fn,由于fn又指向了baz函数,间接调用了baz,而他又能访问到a,即可成功打印。
可能引发的问题:

  • 引用变量问题

    我们用得最多的,当然是for循环了,那么请看这个经典的例子

function test() {
      var result = [];
      for (var i = 0; i < 10; i++) {
        result[i] = function () {
            console.info(i)
        }
     }
     return result
}
test()[0]() // >>> 10

result是一个包含十个函数的数组,按照预要分别打印0-9,但是我们取第一个函数执行,结果却是10。那是因为每个result里面的闭包函数,访问的i都是test函数作用域内的i,循环结束后,a=10,所以每个闭包函数都是访问到a=10时候的值。
解决方法:

function test () {
      var result = []
      for (var i = 0; i < 10; i++) {
        result[i] = (function (j) {
          return function () {
            console.log(j)
          }
        }(i))
      }
      return result
},
this.test()[0]() // >>> 0

我们可以把i的值赋值给内层的j,这样打印的j就是每个函数所形成闭包中自己的j,就不会造成指向同一个变量的问题。

  • this指向问题
test () {
    var people = {
      id: 1,
      name: '小飞',
      age: 18,
      getAge: function () {
        return function () {
          console.log(this.age)
        }
      }
    }
    people.getAge()() // TypeError: Cannot read property 'age' of undefined
},

当使用people调用getAge函数时,返回的是打印闭包函数下的age,而此时是在全局作用域window下调用的,没有age这个变量,所以会找不到。
解决方法:

test () {
    var people = {
        id: 1,
        name: '小飞',
        age: 18,
        getAge: function () {
            return (function (that) { // 闭包函数
                console.log(that.age)
            })(this)
        }
    }
    people.getAge()   // 18
},

可以在getAge函数内将this指向people作用域,传递给闭包函数,给予作用域权限访问age
③ 内存泄漏问题
由于闭包会携带包含它的函数的作用域,因为会比其他函数占用更多内容,过度使用闭包,会导致内存占用过多。 内存泄漏是指,一块被分配的内存既不能使用,也不能回收,影响程序性能。
JS对象和DOM对象的引用造成的问题:

function test() {
    var e = document.getElementByID("container");
    e.onclick = function() {
        console.log(e.name)
    }
}

先创建了DOM对象,为该对象设置点击事件,引用匿名闭包函数,该函数都可以访问test作用域内的所有属性,产生了一种关联,但是执行完之后,由于这种关联,DOM对象无法被回收,形成了内存占用
解决方法:

function test() {
    var e = document.getElementByID("container");
    var name = e.name
    e.onclick = function() {
        console.log(name)
    }
    e = null
}

手动释放DOM对象,保留有用数据
参考文章:
彻底搞懂JS闭包各种坑

你可能感兴趣的:(《你不知道的JavaScript》之作用域和闭包)