1

js中作用域的问题可以说是老生常谈,个人认为js的作用域中存在着两种作用域,一种是词法作用域,一种是动态作用域。

词法作用域

词法作用域就是定义在词法阶段的作用域,也就是说由我们写代码时将变量写在哪里所决定的,当然在js中大部分是这种情况。

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

这个例子就是一个很好的印证,可以发现的是,无论foo在哪里调用,其a的值永远是全局作用域中的a的值,这就是词法作用域,定义函数时,作用域是在全局,那么foo上层作用域就是全局,改变其调用位置是不能改变其作用域链的,其作用域链是在定义时就决定好的
利用词法作用域,我们可以引申出闭包的概念,将我们上面的代码改写:

var a = 20;
function bar () {
    var a = 30;
    return function foo () {
        console.log(a);
    }
}
bar()();   // 30

foo函数在bar函数内定义,所以foo函数的作用域链上层对应的是bar的作用域,利用闭包将foo暴露出去,由于foo函数仍然保持着对bar作用域的引用,所以bar内部的作用域依然存在,没有被回收,这也是闭包能够产生私有变量等效果的原因。

改变作用域

对没错,词法作用域可以被改变,通过eval或者with就可以将作用域改变,但是这并不被提倡。
eval改变作用域:

var a = 20;
function bar () {
    eval("var a = 30;");
    console.log(a);
}
bar();  // 30

eval内的代码可以看做,本身就写在了那一行,但是在严格模式下,eval有着自己的作用域,所以严格模式下上面的代码会报出一个ReferenceError
with改变作用域:

function foo (obj) {
    with (obj) {
        b = 2;
        a = 2;
    }
}
var obj = {a: 1};
foo(obj);
console.log(obj, b);   // { a: 2 } 2

with可以形成一个新的作用域,其词法标识符就是这个对象的属性,所以可以看到的是a = 2,本质上改变的是obj的属性,而b = 2由于这个作用域下没有找到该变量,所以会沿着作用域链向上查找,由于在foo的作用域内也没有找到,一直到全局作用域都没有找到,所以会给全局添加一个属性,这就将b变为了全局变量,所以我们在全局作用域中可以访问到b。但是我们在with中用var定义变量时,会将该变量定义到其外层作用域中。

var obj = {a: 1};
with (obj) {
    var b = 2;
    var a = 2;
}
console.log(obj, b, a);  // { a: 2 } 2 undefined

所以我们可以看到的with块内的变量也有着提前声明,但是其提前声明的位置是其上层作用域中。这种行为实在是十分让人理解,将对象放入一个新的作用域,但是同时可以给其上层作用域定义变量。当然在严格模式中,完全不用担心,因为with在严格模式中被禁用。

动态作用域

动态作用域取决于其调用方式以及在哪里调用,这个听着有点像this啊,没错,在js中唯一的动态作用域就是this,当然也可以叫其延迟绑定。在谈起this之前,首先要知道的是,执行上下文是什么?
每一种代码的执行都依赖于自身的上下文,函数的每一次调用都会进入函数执行中的一个上下文,并且在函数每次调用时都会产生一个变量对象(虚拟出来的,真实代码中是访问不出来的,会被视作undefined),函数中的每一个变量都可以视为这个变量对象的一个属性,在进入上下文时,首先会对函数的形参进行操作,将形参添加到变量对象中,然后会对函数声明进行操作,假若变量对象中存在同名属性时,同名属性将被覆盖,最后对变量声明进行操作,假若变量对象中存在同名属性时,该变量则会被忽略声明,下面的代码就可以验证我们的这个过程:

function foo (fn) {
    function fn () {}
    var fn;
    console.log(fn);  // [Function: fn]
}
foo();

我们的this和执行上下文没有关系,但是和我们的变量对象有着很大的关系。

在非严格模式下,this指向null或者undefined时会指向全局对象。

以这个准则来看我们《JavaScript语言精粹》中提到的函数的四种调用方式与this取值的关系:

1.方法调用模式

var obj = {
    a: 2,
    foo: function () {
        console.log(this.a);
    }
};
obj.foo();

这种情况下,this指向的就是该对象。

2.函数调用模式

var a = 2;
function foo () {
    console.log(this.a);
}
foo();   // 浏览器环境中:2

// 严格模式下
var a = 2;
function foo () {
    "use strict";
    console.log(this.a);    // TypeError
}
foo();

这种情况下this应该指向的那个我们上文所提到的那个虚拟出来的变量对象,由于其本来并不存在,所以this指向的undefined,在非严格模式下指向的是全局window,但是在严格模式下,禁止了这种隐式的转换。函数调用模式下,this其实可以理解为指向我们虚拟出来的那个变量对象。
3.构造器调用模式
也就是该函数被当做构造函数来调用,这是首先会在函数内部创建一个空对象,然后在将this指向这个空对象,最后将这个对象返回出去。
4.callapply调用
这时this指向的是其第一个参数。

但是自动有了箭头函数后,箭头函数中的this并不是动态作用域,而是属于词法作用域,再其定义时就已经确定好了,相当于function () {}.bind(this)


zp1996
3.2k 声望65 粉丝

coder


下一篇 »
模板引擎Jade