词法作用域

2017/11/25 posted in  JavaScript

一、词法阶段

function foo(a) { 
    var b = a * 2;
    function bar(c) { 
        console.log( a, b, c );
    }
    bar(b*3); 
}
foo(2);//2,4,12
  1. 全局作用域中有一个标识符:foo
  2. foo 所创建的作用域中有三个标识符:a、b、bar
  3. bar 所创建的作用域中有一个标识符:c

当执行console.log 声明并查找a、b和c 这三个变量的引用的时候,首先从最内部的作用域bar 函数所在的作用域开始找。无法找到a就会去上一级foo作用域中查找。

作用域查找会在找到第一个匹配的标识符时停止。

无论函数在哪里被调用,也无论它如何被调用,它的词法作用域只由函数被声明时所处的位置决定。

词法作用域只会查找一级标识符,也就是普通变量。如果代码中引用了foo.bar.baz,词法作用域只会试图查找foo标识符,找到后,对象属性访问规则会分别接管对bar和baz属性的访问。

二、欺骗词法

欺骗词法作用域会导致性能下降

1.eval

eval函数接受一个字符串为参数,并其中的内容视为好像在书写时就存在于程序中这个位置的代码。

function foo(str, a) { 
    eval( str ); // 欺骗! 
    console.log( a, b );
}
var b=2; 
foo("var b=3;",1);//1,3 

使用严格模式,可以使eval(…)中的声明无法修改所在的作用域。

function foo(str) { 
    "use strict";
    eval( str );
  console.log( a ); // ReferenceError: a is not defined
}
foo("var a = 2");

setTimeout(…) 还有 setInterval(…)和eval(…)很相似,它们的第一个参数可以是字符串,字符串的内容可以被解释为一段动态生成的函数代码,这些功能已过时且并不提倡。

new Function(…)的最后一个参数可以接受代码字符串(前面的参数为这个新生成函数的形参),转化为动态生成的函数。这种构建函数的语法比eval(…)略微安全一些。

var foo = new Function('a','console.log(a)');
foo(3); //3

2.with 声明实际上是根据你传递给它的对象凭空创建一个全新的词法作用域

function foo(obj) { 
    with (obj) {
        a=2; 
    }
}
var o1 = { 
    a:3
};
var o2 = { 
    b:3
};
foo( o1 );
console.log( o1.a ); //2

foo( o2 );
console.log( o2.a ); // undefined

console.log( a ); // 2, a 被泄漏到全局作用域上了!

在with内部,我们写的代码看上去只是对变量a进行了简单的词法引用,实际上就是一个LHS引用(查看[[什么是作用域]])。

由于o2的作用域、foo(…)的作用域和全局作用域中都没有找到标识符a,因此当 a=2执行时,自动创建了一个全局变量。

在严格模式下with被完全禁止。

词法作用域和动态作用域的区别在于,词法作用域的定义过程发生在书写阶段,它关心的是代码中的作用域嵌套,而动态作用域不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。动态作用域的作用域链是基于调用栈的,而不是代码中作用域嵌套。JavaScript中的this机制某种程度上很像动态作用域。