天天看点

JavaScript执行环境、作用域及this值

执行环境

JavaScript的执行环境定义了其中的变量和函数有权访问的其他数据,即规定了在其内部能够访问什么数据。每个执行环境都有一个与之相关联的“变量对象”,环境中定义的变量和函数都保存在这个对象之中,可以理解为环境内的变量和函数都是这个变量对象的属性和方法,但是这个变量对象我们无法通过js访问到。

可能这个概念有点难以理解,我们来看看一个例子:全局执行环境是最外围的一个执行环境,在web浏览器中,全局执行环境被认为是window对象,也即此时 的变量对象可以认为是window对象:

var name = 'paper_crane';
function sayName() {
  alert('paper_crane');
}
alert(name)          // paper_crane
sayName();           // paper_crane
window.sayName();    // paper_crane
alert(window.name);  // paper_crane
           

在上面的例子中,声明了一个全局变量name和一个全局函数sayName,无论是直接调用它们还是当window对象的一个属性或方法使用都能正确的执行并得到我们预期的结果,所以执行环境里面定义的所有变量和函数都会被当成其变量对象的属性和方法,此对象内部的所有的属性和方法都能访问到此对象内部的其他属性和方法。这样就能用一个具体的变量对象来描述一个抽象的执行环境,window是唯一一个可以通过js获取的变量对象,即使如此,在非必须的情况下不建议使用这种方式使用自定义的全局变量和全局函数。

每个函数都有自己的执行环境。当执行环境进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行完成之后,栈将其环境弹出,把控制权返回给之前的执行环境。

作用域和作用域链

看完执行环境的定义与理解,我们发现,执行环境不就是我们所说的作用域吗?是的,可以认为作用域就是执行环境,就如上面的代码例子,存在两个执行环境:全局执行环境和sayName函数内部执行环境,也即为全局作用域和sayName函数局部作用域。我们可以 推测:全局作用域里面无法访问到局部作用域里面的变量,而在局部作用域里面可以访问到全局作用域里面的变量。来看一下例子:

var name = 'paper_crane';
function sayName() {
  alert(name);
  var age = 22;
}
sayName();           // paper_crane
alert(age);          // throw an error
           

在这个例子中,变量name是全局变量,变量age是sayName函数局部作用域的变量,在全局作用域里面使用age会报错,而在局部作用域里面使用全局作用域里面的name变量却不会报错。这个结果符合我们的预期,而JavaScript就是通过作用域链实现这种变量访问权限的。

当js代码在一个环境中执行时,会创建变量对象的一个作用域链,作用域链的用途是 保证对执行环境有权访问的所有的变量和函数的有序访问,作用域链的前端始终指向当前执行的代码所在环境的变量对象,如果这个环境是函数,则将其活动对象作为变量对象,函数的活动对象最初只包含arguments对象,此后在该函数内部创建所有变量和函数都会变成该活动对象的属性和方法,当函数执行结束之后就会销毁这个活动对象。作用域链中的下一个变量对象来自包含此执行环境的外部环境的变量对象,以此类推,全局执行环境的变量对象始终是作用域链中的最后一个对象。

标识符解析是由作用域链前端一级一级地往后搜索标识符的过程,此过程是一个单向的过程,并且只要找到了标识符就会停止搜索,如果回溯到全局执行环境的变量对象还是无法找到此标识符,那么就会报错。现在再看看上面的那个例子,此例子有两个执行环境,第一个执行环境的变量对象是全局对象window,而sayName函数内部的对象变量我们设为obj;那么sayName函数内部的作用域链就是由obj和window组成的,由obj指向window,在sayName函数内部访问name变量时,浏览器会先去搜索obj对象,但是在obj对象没有找到name属性,接着回溯到window对象,成功找到了name属性,停止搜索,取得name变量的值,而由于在作用域链搜索标识符的过程是单向的,所以在全局环境无法访问到age变量。我们再看看下面这个例子:

var name = 'paper_crane';
function sayName() {
  var name = 'crane';
  alert(name);
}
sayName();           // crane
           

在上面的例子中,sayName函数内部的变量对象和全局变量对象都声明了一个name变量,但是在内部访问变量name的时候,就会访问当前变量对象的name变量,接着就是停止向后回溯了。这个不难理解,但是有一种特殊的情况需要注意一下的:

var name = 'paper_crane';
function sayName() {
  alert(name);            // undefined
  var name = 'crane';
  alert(name);            // crane
}
sayName();
           

在上面的例子中,第一个alert需要访问name变量,但是在sayName内部环境里面还没有声明(起码在我们看来还没有),所以应该是弹出“paper_crane”才对,但是实际上却弹出了“undefined”,为何?其实JavaScript在声明变量的时候,会把声明直接提前到代码执行前面,所以当第一个alert函数访问name变量时,会搜索sayName的内部变量对象,而内部变量对象已经声明了name变量,而初始化会则会在代码设定的位置,这个跟函数声明提升有点相似,所以第一个alert弹出的是“undefined”。所以在声明变量的时候,最好在进入此执行环境就把所有的变量都声明好,不要在逻辑代码中间随意的声明一个变量。

接着对于作用域还需要补充的一点是,JavaScript没有块级作用域,也就是说,JavaScript不像c或者java语言一样,在代码块(以{}分离)里面声明的变量能够在当前的执行环境中访问得到,如下:

function sayAge() {
  if (true) {
    var age = 22;
    alert(age);   // 22
  }
  alert(age);     // 22
}
sayAge();
           

由于没有块级作用域,大量声明会非常容易造成作用域污染,此时只要使用一个立即执行函数就可以模拟块级作用域了:

var name = 'paper_crane';
function sayAge() {
  (function() {
    if (true) {
      var age = 22;
      alert(age);   // 22
    }
  })();
  alert(age);       // ReferenceError
}
sayAge();
           

词法作用域

JS采用的是词法作用域。词法作用域可以这样理解(纯属个人理解): 函数里面调用的变量在函数声明的时候就已经确定了,确定规则:当在本作用域找不到该变量的时候,就会向上层的作用域寻找,直到找到或者最终找不到,但是如果本作用域存在这个变量,则绝对不会向上寻找。如下面的例子:

var name = 'paper_crane';
function sayName() {
  alert(name);            // undefined
  var name = 'crane';
  alert(name);            // crane
}
sayName();
           

这个例子上面举过,不过上面主要是说明变量声明会提升。但实际上词法作用域的本质就是通过变量和函数声明提升来实现的。上面sayName函数在其作用域内声明了变量name,当函数内使用name变量的时候,在本作用域找到了name变量,所以不会向上层寻找这个变量。

this对象

this对象是一个指针,指向的对象是在 运行时基于函数的 执行环境绑定的,但是this对象 不是指向函数运行时所在的执行环境(变量对象),因为刚才在说执行环境的时候说过:执行环境只能在执行代码解释的后台使用到,除了全局执行环境,无法使用js代码访问到。而如果this对象指向了函数运行时候所在的执行环境,就违反刚才所说的原则。所以this对象的指向取决于函数运行时的执行环境,但是不指向执行环境。this指向有以下几种情况:

指向window

一般情况下,函数内部的this对象都会指向window。来看看例子:

var name = 'paper_crane';
function globalFunction() {
  var name = 'crane';
  alert(this.name);          // paper_crane

  (function() {
    alert(this.name);        // paper_crane
    alert(name);             // crane
  })();
  
  function innerFunction() {
    alert(this.name);        // paper_crane
    alert(name);             // crane
  }
  
  innerFunction();
}
globalFunction();
           

在上面的例子中,声明了一个变量name和声明了一个globalFunction全局函数,在函数内部又声明了一个name变量,输出this.name,输出的是全局变量name的值;接着声明一个匿名函数,输出this.name的值也是得到全局变量name的值;接着在内部又声明了一个innerFunction函数,在其内部输出this.name的值时得到的并不是局部变量name的值,而是全局变量name的值。以上的例子说明全局函数、匿名函数和局部函数的this对象指向的都是window,需要进一步验证的读者可以直接输出this的值。

指向函数所有者

看到所有者相信大家和我一样想到的是对象,一个对象拥有自己的方法,此时方法内部的this指向此对象。

var obj = {
  name : 'paper_crane',
  showName: function() {
    alert(this.name);        // paper_crane
  }
}
obj.showName();
           

在上面的例子中声明了一个对象obj,包括一个name属性和showName方法,sayName方法可以正确的访问到name属性,所以this指向的是showName函数的所有者obj。除了这种直接声明一个对象的情况,在构造类构造函数的时候也指向所有者。例如:

function Student() {
  this.name = 'paper_crane';
  this.showName = function() {
    alert(this.name);         // paper_crane
  }
}
var crane = new Student();
crane.showName();
           

在上面的例子中,实现了一个Student类构造函数(实际上ES规范中并没有类的概念,这只是实现面向对象编程的方法,至于面向对象编程有时间再详谈),类里面有一个name属性,有一个showName方法,然后声明了一个Student的实例crane,此时调用crane的方法showName就会发现输出的是paper_crane,所以this指向的是其所有者crane。

改变this的指向

通过上面的例子我们知道this对象指向的是一个对象,但是这是根据函数运行时根据执行环境决定的,但是这并不代表我们不能改变其this的指向。函数对象的 call、apply、bind方法(这是函数对象非继承而来的方法)可以改变this对象的this对象指向。

var name = 'paper_crane';
var thisObj = {
  name : 'crane'
}
function showName() {
  alert(this.name);
}

showName();                        // paper_crane
showName.call(thisObj);            // crane
showName.apply(thisObj);           // crane
var fun = showName.bind(thisObj);
fun();                             // crane
           

上面的例子演示了使用函数对象的call、apply、bind方法改变函数this对象的指向,至于以上三个方法的区别和使用具体使用方法有时间再详谈。

箭头函数中的this

看了词法作用域和this的指向相关内容中之后,我们可以知道,匿名函数及普通函数内this并不遵循词法作用域的规则。但是箭头函数里面的this则会遵循词法作用的规则,而且,箭头函数里面没有this值,只会向上层寻找this值。因为箭头函数里面的this遵循词法作用域规则,所以无法给箭头函数使用call、apply和bind方法来改变其this指向。

var name = 'paper crane';
var obj = {
		name: 'crane',
		normal: function() {
			console.log(this.name);
		},
		arrow: () => {
			console.log(this.name);
		}
	};

obj.normal();       // crane
obj.arrow();        // paper crane
           

上面的例子中。normal方法是作为obj的方法被调用,所以输出crane。而arrow方法是个箭头函数,其本身是没有this值的,所以向其外层寻找this,在上面的例子中也就是全局环境,全局环境的this为window对象,所以arrow的this对象就是window。

以上是个人对作用域、作用域链和this对象的一些理解,对于以上的任何内容有任何疑问或者有何错误皆可在指出,万分感谢。

继续阅读