今天我给大家介绍一个在这门语言中比较重要一个概念:什么是词法作用域及何为欺骗词法作用域?,文章是参考《你不知道的JavaScript
(上卷)》做的一些学习笔记,发表一些个人的理解与大家进行分享交流,存在不足的地方还望各位大佬能够批评指正。
前言
在介绍词法作用域前我们抛出一个概念“词法作用域是作用域的一种工作模型”
在JavaScript
中的词法作用域是比较主流的一种,另一种动态作用域(比较少的语言在用bash
脚本等),
动态作用域我们这就不做讲解主要介绍一下词法作用域这个概念。
我们要知道没有作用域
也就没用词法作用域
- 那什么是作用域呢?
作用域是一套
规则
,用来管理引擎如何在当前
作用域以及嵌套
的子作用域中根据标识符
名称进行变量查找
关于作用域的介绍我到时候会写一篇文字进行详细介绍。
词法作用域
词法作用域 就是在词法分析时
定义的作用域,在写代码时,由变量和块作用域的位置
决定。它是静态的
,词法分析处理代码时会保持作用域不改变(欺骗词法作用域除外)。
- 词法阶段
这是一个三级嵌套的作用域
①,②,③分别为全局作用域
,函数foo
作用域,函数bar
作用域,作用域的范围 是根据作用域代码块定义的位置决定的。
当我们去执行console.log(a,b,c,d)
操作时,由于词法作用域
在词法分析阶段就确定了,那么就先会在当前作用域下查找这4
个变量,如果找不到就会在外层嵌套
的作用域中继续查找(作用域链
),直至找到该变量,或抵达最外层作用域为止。
如果我们在foo
函数中bar
函数添加一段 var c = 5
,请问最终c
会输出几呢?
var d
function foo(a){
var b = a*2
var c = 5;
function bar(c){
cosole.log(a,b,c,d);
}
bar(b*3)
}
复制代码
答案还是12
为什么不是取外面那个C
的值呢?这就要引入”遮蔽效应“的概念了:
作用域查找会在找到
第一个
匹配的标识符时停止
因为在执行console
当前作用域内能找到C这变量了。如果这C为全局变量的话可以通过window.C
访问到,否则C被遮蔽
后无论如何都无法被访问到。
词法作用域查找只会查找一级标识符
, 比如 a 、 b 、 c 。如果代码中引用了 foo.bar,词法作用域只会查找 foo 标识符,找到后,对象属性访问规则 再对 bar的属性进行访问。
欺骗词法作用域
如果词法作用域完全由写代码期间函数所声明的位置
来决定,怎样才能在运行时来”修改
“(欺骗)词法作用域呢?
在JavaScript中有两种这样的的机制来实现这个目的,分别是eval
和with
。但是不推荐使用因为它们会导致性能下降
。
eval
eval(..)
函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。- 在执行
eval(..)
之后的代码时,引擎并不”知道“和”在意“前面的代码时动态
的插入进来,并对词法作用域
的环境进行了修改,只会一如既往的进行词法作用域查找。
我们来看以下代码:
function foo(str,a){
eval(str)//相当于植入了 var b = 3
console.log(a,b)
}
var b = 2
foo('var b=3',1)// 1,3
复制代码
eval(..)
调用的"var b = 3"
,这段代码会被插入到foo
函数中去,这段代码会被当做本来就在那里一样处理。由于这段代码中声明了一个新的b
变量,因此对foo
函数的词法进行了修改。这就是前面我们提到的”遮蔽效应
“,foo
函数里定义了一个新的变量b
,并遮蔽了全局
作用域中的同名变量b
。
当console.log(a,b)
被执行时,会在当前作用域中找到a,b,因此无法访问到外部的同名变量b,输出就是’1,3'
,而不是正常情况下的'1,2'
。
ES5
中引入了”严格模式”。同正常模式,严格模式在行为上有很多的不同。
在严格模式下执行eval(..)会怎样呢?
function foo(str,a){
"use strict"
eval(str)
console.log(a)//ReferenceError : a is not defined
}
foo('var a=2')
复制代码
我们发现在严格模式下
在foo
中使用eval(..)
定义的a
变量并没用被console.log(a)
访问到,这是因为在严格模式下,eval(..)在运行时有了自己的词法作用域
,因此其中的变量声明
无法修改当前的词法作用域。
除eval(..)之外:
setTimeout(...)
和setInterval(...)
的第一个参数可以是字符串,字符串的内容会被解释为一段动态生成的函数代码,不提倡使用。
构造函数
new Function()
的最后一个参数可以接受代码字符串(前面的参数是新生成的函数的形参), 尽量避免使用。
with
with
通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。他也有一个副作用,会将变量泄漏到全局作用域
。
- 我们先来看看
with
的快捷用法
var obj ={
a:1,
b:2,
c:3
}
obj.a=2
obj.b=3
obj.c=4
consloe.log(a,b,c)// 2,3,4
with(obj){
a=3,
b=4,
c=5
}
consloe.log(a,b,c)//3,4,5
复制代码
我能能够看到with
可以很方便的访问对象的属性实现快捷赋值。
-
但是以下这这种情况就会产生副作用了
function foo(obj){
with(obj){
a=2
}
}
var o1={
a:3
}
var o2={
b:3
}
foo(o1.a)
console.log(o1.a)//2
foo(o2.a)
console.log(o2.a)//undefined
console.log(a)//2
复制代码
由输出结果我们看到,o1
中有a
属性,with
将该对象中的a
属性的值改为了2,
反观o2
被with
访问后,o2
中并没有属性a
,那with
是不是会在o2
中添加一个a
属性呢?
由consloe.log(o2.a)
输出的结果我们可知,答案是否定的
。当我在全局作用域中执行console.log(a)
这个操作时,我们惊奇的发现全局作用域
中原来始没有定义a
变量的,执行这条语句不应该会报错吗?这似乎颠覆我们对JavaScript
这们语言的认知。
实际上这就是
with
的副作用,可以理解为,o2
中没a
属性,而with
这”小子”就比较犟,就会在全局作用域
中创建了一个全局变量a。
PS:严格模式下with会被禁用。
总结
-
词法作用域
是由函数及变量声明的位置
来决定的,在执行过程中也会以此为作用域基准进行变量查找(LHS
和RHS
感兴趣的书中有详细介绍)。 -
非严格模式下
eval(..)
和with会
“欺骗”词法作用域。 -
词法欺骗的副作用是导致j
s
引擎性能优化失效,使程序运行变慢,因此不建议使用。