JS-词法作用域

148 阅读3分钟

词法作用域气泡

考虑下面代码的词法作用域,可以将他们理解为几个逐级包含的气泡

function foo(a){
  var b = a * 2;
  function bar(c){
    console.log(a,b,c);
  }
  bar(b*3);
}

foo(2);
// 2, 4, 12

IMG_0055

注意:没有任何函数的气泡可以同时出现在两个外部作用域的气泡中,就如同没有任何函数可以部分的同时出现在任何两个父级函数中一样

查找

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

全局变量会自动成为全局对象的属性(==window对象/global对象==),因此可以不直接通过全局对象的词法名称,而是间接地通过全局对象属性的引用来对其进行访问window.a

词法作用域查找只会查找一级标识符(比如:foo.bar.baz,foo就是一级,所以词法作用域查找只会试图查找foo标识符),找到变量后,对象属性访问规则会分别接管对bar,baz属性的访问

欺骗词法

可以在代码运行期间“修改”词法作用域

eval

eval(..)可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于代码中这个位置的代码

function foo(str,a){
  eval(str);
  console.log(a,b);
}

var b = 2; // 此处被eval欺骗了

foo("var b = 3;",1);
// 1,3

在上面代码中,eval调用var b = 3,声明了一个新的变量b,对已经存在的foo的词法作用域进行了修改

在foo(..)内部创建了变量b,并遮蔽了全局作用域中的全局变量,因此输出的是==1,3==而不是正常情况下的==1,2==

在严格模式下,eval运行时有自己的词法作用域,意味着其中的声明无法修改到所在的作用域

function foo(str){
  "use strict";
  eval(str);
  console.log(a); // ReferenceError
}

foo("var a = 2");

with

with通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。

var obj = {
	a:1,
  b:2,
  c:3,
};

obj.a = 2;
obj.b = 3;
obj.c = 4;

with(obj){
  a = 3;
  b = 4;
  c = 5;
}

但实际上这不仅仅是为了方便地访问对象属性

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被foo泄漏到全局作用域中了

在with内部,我们写的代码看起来只是对变量a进行了简单的词法引用,实际上就是一个LHS引用,并将2赋值给他。

with声明会根据你传递给它的对象凭空创建一个全新的词法作用域

当o2传递进去时,o2并没有a属性,因此不会创建这个属性,o2.a保持undefined,在o2作用域,foo作用域和全局作用域中都没有找到a标识符,所以当执行a=2时就会自动创建一个全局变量,==因为不是严格模式==。

性能

js引擎会在编译阶段进行数项性能优化,其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。

如果代码中大量运用了eval和with,那么运行起来一定会变得很慢。所以,==尽量不要使用他们==