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.call
与apply
调用
这时this
指向的是其第一个参数。
但是自动有了箭头函数后,箭头函数中的this
并不是动态作用域,而是属于词法作用域,再其定义时就已经确定好了,相当于function () {}.bind(this)
。