译者注:一直对于作用域和上下文感到很混乱,无意中看到这篇文章,觉得讲得很好,故翻译来与大家分享。翻译不好之处,请大家多多指教。
原文链接:http://ryanmorr.com/understanding-scope-and-context-in-javascript/
前言部分,不做翻译。
Context vs. Scope
首先我们要明确,上下文和作用域是不同的概念。我注意到许多的开发者(包括我)长久以来都对这二者感到迷惑,错误地将其中一方描述为另一方。公平地说,这些术语已经被混淆很多年了。
每一次函数调用都有一个作用域以及与之对应的上下文。从根本上讲,作用域是基于函数的而上下文是基于对象的。即,作用域是关于函数被调用时对变量的访问。并且,每一次调用,作用域都是不同的。上下文是关键字 this 的值,即指向“拥有”当前执行代码的对象。
Variable Scope
一个变量可以被定义为局部变量或者全局变量,它确立了在运行的时候在不同的作用域中,是否可以访问到它。任何被定义了的全局变量,即那些在函数体外定义的变量,会在整个运行周期中存在,并且在每个作用域中访问和修改。局部变量只存在于定义它的函数中,每一次调用的时候都会有不同的作用域。它被在函数中用于赋值,检索,操作并且无法在该作用域外面被接触到。
ECMAScript6(ES6/ES2015)引入了关键字 let 和 const,它们支持块级作用域。这意味着变量可以被限制在定义它的块级作用域中。let和const的区别是,const,从它的名字可以看出,它是一个只读的值。但这不意味着它的值是不变的只是它的变量不可以重新被定义。(译者注:比如一个引用类型的值,如对象,我们不可以改变它指向的对象,但是可以改变它的对象的属性值)
What is “this” Context
上下文是由函数如何被调用决定的。当一个函数被作为对象的方法调用时,this指向调用这个方法的对象:
var obj = {
foo: function() {
return this;
}};
obj.foo() === obj; // true
同样的准则也适用于当使用 new操作符来创建一个对象实例的时候。使用这个方法调用时,函数作用域中的this会指向新创建的实例。
function foo() {
alert(this);}
foo() // windownew foo() // foo
当一个未被绑定的函数被调用时,this 默认指向全局上下文,在浏览器中指向window对象。当韩式在严格模式下运行时,此时的上下文是undefined。
Execution Context
javaScript是一门单线程语言,意味着每一次它只能执行一个任务。当javascript开始解释代码时,它会默认进入全局execution context。从此刻开始,每一次的函数调用都会创建一个新的执行上下文。
然而,这就是迷惑产生的地方。“exection context”更多的是涉及作用域,而不是我们之前提到的上下文。这是一个令人遗憾的命名规范。然而,它是由ECMAScript 规范决定的,所以我们只能接受了。
每当一个新的execution context被创建时,它被添加到execution栈的顶端。浏览器总是会执行当前的在exection stack顶端的execution context。一旦执行完,它会被从栈的顶端移除,控制权会返回给下面的execution context。
一个execution context 可以被分成创建和执行两个时期。在创建时期,解释器会先创建一个变量对象(variable object)(也叫活动对象)。他由所有的在execution context定义的变量和函数声明,以及函数参数组成。然后,在活动对象中,作用域链被初始化。最后,this确定指向。之后,在执行阶段,代码被解释和执行。(译者注:活动对象无法被我们访问,但是解释器在处理数据是会在后台使用到它.参考《javaScript高级程序设计(第3版)》)
The Scope Chain
对于每一个执行上下文,都有一条与之对应的作用域链。作用域链包括了execution stack中每一个execution context的活动对象,它被用来决定去哪获取变量以及标识符的解析。
function first() {
second();
function second() {
third();
function third() {
fourth();
function fourth() {
// do something
}
}
}
}
first();
运行前面的代码回导致后面的代码被执行,直到fourth这个函数。此时,作用域链的顶端到底部分别是 fourth, third, second, first, global.。fourth这个函数可以获取全局变量以及first,second,third函数中定义的任何变量,正如这些函数本身一样。
命名冲突时通过从局部到全局顺着作用域链寻找来解决的。这意味着,在作用域链更顶端的变量拥有更高的优先级。
简而言之,每次你想要在一个执行上下文中存取变量时,查找的过程总会从当前的变量对象开始。如果变量对象没被找到,那么就会顺着作用域链一直寻找。它会顺着作用域链,搜寻每一个活动对象,来匹配当前的变量名称。
Closures
在直接语法作用域之外存取变量称为闭包。换句话说,一个闭包就是在一个函数中定义另一个函数,并且允许里面的函数存取外面的函数的值。返回的嵌套在里面的函数允许你保持存取局部变量,函数参数,以及外层函数的内层函数的权利。这种封装允许我们在暴露一个公共接口时隐藏并且保留外层函数的执行上下文,从而允许进一步的封装。简单的例子如下。
function foo() {
var localVariable = 'private variable';
return function() {
return localVariable;
}}
var getLocalVariable = foo();
getLocalVariable() // "private variable"
最受欢迎的闭包类型就是众所周知的模块模式。它允许你去模仿公有的,私有的,以及特权成员。
var Module = (function() {
var privateProperty = 'foo';
function privateMethod(args) {
// do something
}
return {
publicProperty: '',
publicMethod: function(args) {
// do something
},
privilegedMethod: function(args) {
return privateMethod(args);
}
};})();
这个模块表现得像单例一般,解释器开始解释的时候它就执行了,因为函数后面有圆括号。在闭包的执行上下文外面唯一的变量方法是你的公共方法和返回对象中的那些属性。然而,所有的私有属性和方法会在应用的整个生命周期存在因为执行上下文被保留了。意味着变量会通过共有的方法进行更进一步的交互。
另一种闭包类型是立即执行函数表达式,不外乎就是在window上下文中自调用的并执行的匿名函数。
这个表达式在想要保留全局命名空间中任何在函数中声明的变量的作用余限定在闭包中,但是在应用运行的整个生命周期都存在,会非常好用。这是为应用和框架封装源代码的非常受欢迎的方法,典型的是暴露一个单独的全局接口给交互对象。
Call and Apply
所用的函数都有两种自带的方法允许你在任何上下文中执行函数。这带来了难以置信的功能。call函数要求参数是所有列明的参数,而apply允许使用参数数组。
function user(firstName, lastName, age) {
// do something }
user.call(window, 'John', 'Doe', 30);
user.apply(window, ['John', 'Doe', 30]);
ES5引入了Function.prototype.bind方法来操作上下文。它返回一个永远绑定在bind()函数第一个参数上的新函数,忽略函数是如何被使用的。这在一个闭包在适当的上下文中需要重定向是非常有用。
if(!('bind' in Function.prototype)){
Function.prototype.bind = function() {
var fn = this;
var context = arguments[0];
var args = Array.prototype.slice.call(arguments, 1);
return function() {
return fn.apply(context, args.concat([].slice.call(arguments)));
}
}}
它被普遍地使用在上下文缺失的情况下,面向对象设计和事件处理中。这很重要,因为结点的addEventListener方法,因为它的回掉函数必须在它绑定的结点的上下文中执行。然而,如果你使用先进的面向对象技术并且要求你的互调函数是一个方法的实例,你必须手动地调整上下文,这就是bind非常便利的地方。
function Widget() {
this.element = document.createElement('div');
this.element.addEventListener('click', this.onClick.bind(this), false);}
Widget.prototype.onClick = function(e) {
// do something};
从前面的Function.prototype.bind函数的代码中我们可以看到,Array的slice方法的两种调用方式。
Array.prototype.slice.call(arguments, 1);[].slice.call(arguments);
有趣的是,arguments对象并不是严格意义上的数组,就像nodelist一样,它被描述为类数组的东西。它包括了length属性和索引值,但它不是数组,并且后来也不支持array的自带的方法,如slice和push.但是,由于他们相似的行为,如果你愿意的话,array的方法可以被运用或者强制转换,并且在类似数组上下文的中执行,就想上面提到的那样。
采用另一种对象方法的技术也可以运用到面向对象中,当我们需要在javaScript中效仿经典的的继承。
SubClass.prototype.init = function(){
// call the superclass init method in the context of the "SubClass" instance
SuperClass.prototype.init.apply(this, arguments);}
Conclusion
在开始学习先进的设计模式之前理解这些概念是很重要的,因为context和scope在现代的JavaScript中占据了重要的地位。每当我们谈及闭包,面向对象,继承,以及各种原生实现时,context和scope扮演着举足轻重的地位,如果你有志于钻研javascript语言 ,最好明白它包含了什么,而context和scope应该是最开始要明确学习的东西。