精华 图解Javascript上下文与作用域
发布于 9 年前 作者 irainy 14236 次浏览 最后一次编辑是 8 年前 来自 分享

>> 原文 <<

本文尝试阐述Javascript中的上下文与作用域背后的机制,主要涉及到执行上下文(execution context)、作用域链(scope chain)、闭包(closure)、this等概念。

Execution context

执行上下文(简称上下文)决定了Js执行过程中可以获取哪些变量、函数、数据,一段程序可能被分割成许多不同的上下文,每一个上下文都会绑定一个变量对象(variable object),它就像一个容器,用来存储当前上下文中所有已定义或可获取的变量、函数等。位于最顶端或最外层的上下文称为全局上下文(global context),全局上下文取决于执行环境,如Node中的global和Browser中的window

js global context

需要注意的是,上下文与作用域(scope)是不同的概念。Js本身是单进程的,每当有function被执行时,就会产生一个新的上下文,这一上下文会被压入Js的上下文堆栈(context stack)中,function执行结束后则被弹出,因此Js解释器总是在栈顶上下文中执行。在生成新的上下文时,首先会绑定该上下文的变量对象,其中包括arguments和该函数中定义的变量;之后会创建属于该上下文的作用域链(scope chain),最后将this赋予这一function所属的Object,这一过程可以通过下图表示:

js context stack

this

上文提到this被赋予function所属的Object,具体来说,当function是定义在global对中时,this指向global;当function作为Object的方法时,this指向该Object:

var x = 1;
var f = function(){
  console.log(this.x);
}
f();  // -> 1

var ff = function(){
  this.x = 2;
  console.log(this.x);
}
ff(); // -> 2
x     // -> 2

var o = {x: "o's x", f: f};
o.f(); // "o's x"

Scope chain

上文提到,在function被执行时生成新的上下文时会先绑定当前上下文的变量对象,再创建作用域链。我们知道function的定义是可以嵌套在其他function所创建的上下文中,也可以并列地定义在同一个上下文中(如global)。作用域链实际上就是自下而上地将所有嵌套定义的上下文所绑定的变量对象串接到一起,使嵌套的function可以“继承”上层上下文的变量,而并列的function之间互不干扰:

js scope chain

var x = 'global';
function a(){
  var x = "a's x";
  function b(){
    var y = "b's y";
    console.log(x);
  };
  b();
}
function c(){
  var x = "c's x";
  function d(){
    console.log(y);
  };
  d();
}
a();  // -> "a's x"
c();  // -> ReferenceError: y is not defined
x     // -> "global"
y     // -> ReferenceError: y is not defined

Closure

如果理解了上文中提到的上下文与作用域链的机制,再来看闭包的概念就很清楚了。每个function在调用时会创建新的上下文及作用域链,而作用域链就是将外层(上层)上下文所绑定的变量对象逐一串连起来,使当前function可以获取外层上下文的变量、数据等。如果我们在function中定义新的function,同时将内层function作为值返回,那么内层function所包含的作用域链将会一起返回,即使内层function在其他上下文中执行,其内部的作用域链仍然保持着原有的数据,而当前的上下文可能无法获取原先外层function中的数据,使得function内部的作用域链被保护起来,从而形成“闭包”。看下面的例子:

var x = 100;
var inc = function(){
  var x = 0;
  return function(){
    console.log(x++);
  };
};

var inc1 = inc();
var inc2 = inc();

inc1();  // -> 0
inc1();  // -> 1
inc2();  // -> 0
inc1();  // -> 2
inc2();  // -> 1
x;       // -> 100

执行过程如下图所示,inc内部返回的匿名function在创建时生成的作用域链包括了inc中的x,即使后来赋值给inc1inc2之后,直接在global context下调用,它们的作用域链仍然是由定义中所处的上下文环境决定,而且由于x是在function inc中定义的,无法被外层的global context所改变,从而实现了闭包的效果:

js closure

this in closure

我们已经反复提到执行上下文和作用域实际上是通过function创建、分割的,而function中的this与作用域链不同,它是由执行该function时当前所处的Object环境所决定的,这也是this最容易被混淆用错的一点。一般情况下的例子如下:

var name = "global";
var o = {
  name: "o",
  getName: function(){
    return this.name
  }
};
o.getName();  // -> "o"

由于执行o.getName()getName所绑定的this是调用它的o,所以此时this == o;更容易搞混的是在closure条件下:

var name = "global";
var oo = {
  name: "oo",
  getNameFunc: function(){
    return function(){
      return this.name;
    };
  }
}
oo.getNameFunc()();  // -> "global"

此时闭包函数被return后调用相当于:

getName = oo.getNameFunc();
getName();  // -> "global"

换一个更明显的例子:

var ooo = {
  name: "ooo",
  getName: oo.getNameFunc() // 此时闭包函数的this被绑定到新的Object
};
ooo.getName();  // -> "ooo"

当然,有时候为了避免闭包中的this在执行时被替换,可以采取下面的方法:

var name = "global";
var oooo = {
  name: "ox4",
  getNameFunc: function(){
    var self = this;
    return function(){
       return self.name;
    };
  }
};
oooo.getNameFunc()(); // -> "ox4"

或者是在调用时强行定义执行的Object:

var name = "global";
var oo = {
  name: "oo",
  getNameFunc: function(){
    return function(){
      return this.name;
    };
  }
}
oo.getNameFunc()();  // -> "global"
oo.getNameFunc().bind(oo)(); // -> "oo"

总结

Js是一门很有趣的语言,由于它的很多特性是针对HTML中DOM的操作,因而显得随意而略失严谨,但随着前端的不断繁荣发展和Node的兴起,Js已经不再是"toy language"或是jQuery时代的"CSS扩展",本文提到的这些概念无论是对新手还是从传统Web开发中过度过来的Js开发人员来说,都很容易被混淆或误解,希望本文可以有所帮助。

写这篇总结的原因是我在Github上分享的Learn javascript in one picture,刚开始有人质疑这只能算是一张语法表(syntax cheat sheet),根本不会涉及更深层的闭包、作用域等内容,但是出乎意料的是这个项目竟然获得3000多个star,所以不能虎头蛇尾,以上。

References

  1. Understanding Scope and Context in JavaScript
  2. this - JavaScript | MDN
  3. 闭包 - JavaScript | MDN

BitCoin donate button Tenpay donate button Alipay donate button

25 回复

经我测试: 闭包没原型链快,不过闭包用起来方便.

不错,收藏

From Noder

@hezedu 闭包跟原型链有什么关系?求解

markdown怎么引用啊?

var name = "global";
var oo = {
    name: "oo",
    getNameFunc: function(){
        return function(){
            return this.name;
        };
    }
}
var result = oo.getNameFunc()();

这个例子,我怎么是undefined啊?

@20082496 跟执行环境有关

@20082496 @booxood 我猜可能是这样的:

QQ20150707-1@2x.png

@booxood @irainy 我在webstorm上执行就是undefined,在node命令行里面就是"global",这就是所谓的执行环境不同?

@20082496 webstorm 没有用过 不是很清楚

@irainy 在sublime里面跟webstorm一样

@20082496 我用atom试了一下,确实是。它们应该默认使用的是"strict"模式,因为在"strict"模式下全局上下文是空的


不能说全局上下文是空的,是全局中的this变成undefined,具体可以参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this

@irainy @20082496 其实你测试的那段代码的结果类似

var name = 'global';
function getName () {
  console.log(this.name);
}
console.log(getName());

仔细看完文章,以及引用文章"http://ryanmorr.com/understanding-scope-and-context-in-javascript/"。 觉得楼主有一部分写得不够严谨: execution context不是 context,相反,它更侧重于表示scope。Ryan Morr也强调这是ECMAScript规范命名有歧义的地方。所以不能把execution context简称 context,会让读者误解:)

This is where confusion often sets in, the term “execution context” is actually for all intents and purposes referring more to scope and not context as previously discussed. It is an unfortunate naming convention, however it is the terminology as defined by the ECMAScript specification, so we’re kinda stuck with it.

另外,楼主可以把下面的部分着重强调下:

context和scope的区别:

context:

context与函数调用执行相关,相对scope,可以理解为一个动态的概念。context的值为拥有此函数代码的this所代表的值。举例来说,var obj = new foo(),则context代表了obj。

scope:

scope与函数定义相关,相对context,可以理解为一个静态的概念。scope定义了函数内部的变量作用域。如果函数内部引用了其它函数中的变量,就形成了作用域链(scope chain)。scope与函数调用时的this是谁无关。应该说,函数代码本身决定了scope的范围。

scope就在那里,不离不弃。函数有没有调用,被谁调用,与它都无关系。

context和scope的关系:

每个函数调用都涉及到 scope和context这样一对形影不离的兄弟。他们俩不是相互对立的关系,分别代表了函数创建定义时和执行过程时的不同的形态。 在创建阶段,函数、变量、参数都会被定义好,此时也就形成了scope和scope chain。而当代码被执行时,也就产生了context。

好文果断收藏

@LanceHBZhang
比如我在全局环境定义了一个函数,那我在全局环境去调用或执行这个函数所形成的 scope chain 是不一样的。 也就是scope chain 是在执行的时候创建的,而不是在函数定义的时候。

var name = "global";
var oo = {
  name: "oo",
  getNameFunc: function(){
	return function(){
	  return this.name;
	};
  }
}
oo.getNameFunc()();  // -> "global"

这里如果在nodejs环境下应该是undefined吧,因为nodejs中文件都是模块化的,每个文件中定义的变量可以说是局部变量,只在该文件中能访问到)。如果要定义全局变量,可以不用var关键字或global.name=’global’,若要其它文件能访问则导出模块中 exports.name = ‘global‘

赞<p style=“text-align:right”><a href=“https://github.com/lumia2046/cnode”> — — 来自lumia2046-react-cnode</a></p>

mark<p style=“text-align:right”><a href=“https://github.com/lumia2046/cnode”> — — 来自lumia2046-react-cnode</a></p>

Mark ,闭包的问题理解了运用到了 。感觉就不是问题了。

回到顶部