通过翻译了Dmitry A.Soshnikov的关于ECMAScript-262-3JavaScript内部原理的文章, 从理论角度对JavaScript中部分特性的内部工作机制有了一定的了解。但是,邓爷爷说过:“实践才是检验真理的唯一标准”。所以,我打算通过从内部原理来解释一些经常在笔试或者面试中遇到的关于JavaScript语言层面的题目来进一步学习和掌握JavaScript内部工作原理。
那么,首先就是要去找那些题目,google了一圈终于找到了来自Dmitry Baranovskiy的非常著名的5个问题,这5个问题,NCZ给出了非常清楚的解释。不过,我还是想尝试下从low-level——JavaScript内部工作机制的角度去解释下这些问题。
好吧,我承认我废话很多,那就开始吧。
if (!("a" in window)) { var a = 1;}alert(a);
解释:这个问题,初一看感觉答案很自然是1。因为从上到下执行下去,if语句中的条件应该为 true,因为”a”的确是没有定义啊。随后,顺理成章地进入 var a = 1;,最后,alert出来就应该是1。
而事实上,从JavaScript内部工作原理去看,在变量对象中讲过,JavaScript处理上下文分为两个阶段:
可以理解为,第一个阶段是静态处理阶段,第二个阶段为动态处理阶段。
而在静态处理阶段,就会创建 变量对象(variable object),并且将变量申明作为属性进行填充。到了执行阶段,才会根据执行情况,来对变量对象中属性(就是申明的变量)的值进行更新。
针对这个问题,其实际过程如下:
VO(global) = { a: undefined}
所以,这个时候,a其实已经存在了。
var a = 1, b = function a(x) { x && a(--x); };alert(a);
解释:这个问题,第一反应可能会是将 function a打印出来。因为明明就看到了function a了。看似,也顺其自然。
但是,事实并非如此。还是和此前一个问题一样。从两个阶段来分析:
VO(global) = { a: undefined, b: undefined}
VO(global) = { x: undefined, a: 1}
所以,最后alert(a)的结果是1。
function a(x) { return x * 2;}var a;alert(a);
解释:这个问题,很多人可能会以为是: undefined。理由可能是,明明看到了 var a定义在了function a的后面,感觉应该会覆盖之前a的申明。
事实又是怎样的呢? 老套路,从两个阶段来分析:
VO(global) = { a: 引用了函数申明“x”}
所以,最终的结果是:函数a。
function b(x, y, a) { arguments[2] = 10; alert(a);}b(1, 2, 3);
解释:个人感觉这个问题其实不是很复杂。这里也不需要从两个阶段去分析了。根据 变量对象中介绍的,arguments对象的PRoperties-indexes和实际传递的参数是共享的也就是说,通过arguments[2]修改的参数,也会影响到a,所以,这里的值是10。但是,要注意的是和实际传递的值,所以,如果把上述问题改成如下形式:
function b(x, y, a) { arguments[2] = 10; alert(a);}b(1, 2);
结果就会是: undefined。因为,并没有传递a的值。
function a() { alert(this);}a.call(null);
解释:这个问题,可能会比较困惑。因为懂call的童鞋都会觉得,call的时候把null传递为了当前的上下文了。里面的this应该是null才对啊。
事实却是: 前面都没错,this会是null。但是,this中介绍过,null是没有任何意义的,因此,最终会变成全局对象。所以,这里结果就变成了全局对象。 这里还有ECMAScript-262-3标准文档中的一句话作为证据:“If thisArg is null or undefined, the called function is passed the global object as the this value. Otherwise, the called function is passed ToObject(thisArg) as the this value.”
上面这5个问题其实也只是牵涉到了JavaScript内部原理中的部分知识点,要想了解更多,还是建议读完JavaScript内部原理系列以及去看Dmitry A.Soshnikov的文章。
此文译自Dmitry A.Soshnikov 的文章closures另,此文还有另外一位同事(彭森材)共同参译
本文将介绍一个在JavaScript经常会拿来讨论的话题 —— 闭包(closure)。闭包其实已经是个老生常谈的话题了;有大量文章都介绍过闭包的内容(其中不失一些很好的文章,比如,扩展阅读中Richard Cornford的文章就非常好),尽管如此,这里还是要试着从理论角度来讨论下闭包,看看ECMAScript中的闭包内部究竟是如何工作的。
正如在此前文章中提到的,这些文章都是系列文章,相互之间都是有关联的。因此,为了更好的理解本文要介绍的内容,建议先去阅读下第四章 - 作用域链和第二章 - 变量对象。
在讨论ECMAScript闭包之前,先来介绍下函数式编程(与ECMA-262-3 标准无关)中一些基本定义。然而,为了更好的解释这些定义,这里还是拿ECMAScript来举例。
众所周知,在函数式语言中(ECMAScript也支持这种风格),函数即是数据。就比方说,函数可以保存在变量中,可以当参数传递给其他函数,还可以当返回值返回等等。这类函数有特殊的名字和结构。
函数式参数(“Funarg”) —— 是指值为函数的参数。
如下例子:
function exampleFunc(funArg) { funArg();} exampleFunc(function () { alert('funArg');});
上述例子中funarg的实参是一个传递给exampleFunc的匿名函数。
反过来,接受函数式参数的函数称为 高阶函数(high-order function 简称:HOF)。还可以称作:函数式函数 或者 偏数理的叫法:操作符函数。 上述例子中,exampleFunc 就是这样的函数。
此前提到的,函数不仅可以作为参数,还可以作为返回值。这类以函数为返回值的函数称为 _带函数值的函数(functions with functional value or function valued functions)。
(function functionValued() { return function () { alert('returned function is called'); };})()();
可以以正常数据形式存在的函数(比方说:当参数传递,接受函数式参数或者以函数值返回)都称作 第一类函数(一般说第一类对象)。 在ECMAScript中,所有的函数都是第一类对象。
接受自己作为参数的函数,称为 自应用函数(auto-applicative function 或者 self-applicative function):
(function selfApplicative(funArg) { if (funArg && funArg === selfApplicative) { alert('self-applicative'); return; } selfApplicative(selfApplicative); })();
以自己为返回值的函数称为 自复制函数(auto-replicative function 或者 self-replicative function)。 通常,“自复制”这个词用在文学作品中:
(function selfReplicative() { return selfReplicative;})();
在函数式参数中定义的变量,在“funarg”激活时就能够访问了(因为存储上下文数据的变量对象每次在进入上下文的时候就创建出来了):
function testFn(funArg) { // 激活funarg, 本地变量localVar可访问 funArg(10); // 20 funArg(20); // 30 } testFn(function (arg) { var localVar = 10; alert(arg + localVar); });
然而,我们知道(特别在第四章中提到的),在ECMAScript中,函数是可以封装在父函数中的,并可以使用父函数上下文的变量。 这个特性会引发 funarg问题。
在面向堆栈的编程语言中,函数的本地变量都是保存在 堆栈上的, 每当函数激活的时候,这些变量和函数参数都会压栈到该堆栈上。
当函数返回的时候,这些参数又会从堆栈中移除。这种模型对将函数作为函数式值使用的时候有很大的限制(比方说,作为返回值从父函数中返回)。 绝大部分情况下,问题会出现在当函数有 自由变量的时候。
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量
如下所示:
function testFn() { var localVar = 10; function innerFn(innerParam) { alert(innerParam + localVar); } return innerFn;} var someFn = testFn();someFn(20); // 30
上述例子中,对于innerFn函数来说,localVar就属于自由变量。
对于采用 面向堆栈模型来存储局部变量的系统而言,就意味着当testFn函数调用结束后,其局部变量都会从堆栈中移除。 这样一来,当从外部对innerFn进行函数调用的时候,就会发生错误(因为localVar变量已经不存在了)。
而且,上述例子在 面向堆栈实现模型中,要想将innerFn以返回值返回根本是不可能的。 因为它也是testFn函数的局部变量,也会随着testFn的返回而移除。
还有一个函数对象问题和当系统采用动态作用域,函数作为函数参数使用的时候有关。
看如下例子(伪代码):
var z = 10; function foo() { alert(z);} foo(); // 10 – 静态作用域和动态作用域情况下都是 (function () { var z = 20; foo(); // 10 – 静态作用域情况下, 20 – 动态作用域情况下 })(); // 将foo函数以参数传递情况也是一样的 (function (funArg) { var z = 30; funArg(); // 10 – 静态作用域情况下, 30 – 动态作用域情况下 })(foo);
我们看到,采用动态作用域,变量(标识符)处理是通过动态堆栈来管理的。 因此,自由变量是在当前活跃的动态链中查询的,而不是在函数创建的时候保存起来的静态作用域链中查询的。
这样就会产生冲突。比方说,即使Z仍然存在(与之前从堆栈中移除变量的例子相反),还是会有这样一个问题: 在不同的函数调用中,Z的值到底取哪个呢(从哪个上下文,哪个作用域中查询)?
上述描述的就是两类 funarg问题 —— 取决于是否将函数以返回值返回(第一类问题)以及是否将函数当函数参数使用(第二类问题)。
为了解决上述问题,就引入了 闭包的概念。
闭包是代码块和创建该代码块的上下文中数据的结合。
新闻热点
疑难解答