我研究JavaScript闭包(closure)已经有一段时间了。我之前只是学会了如何使用它们,而没有透彻地了解它们具体是如何运作的。那么,究竟什么是闭包?
Wikipedia给出的解释并没有太大的帮助。闭包是什么时候被创建的,什么时候被销毁的?具体的实现又是怎么样的?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
"use strict"; var myClosure = (function outerFunction() { var hidden = 1; return { inc: function innerFunction() { return hidden++; } }; }()); myClosure.inc(); // 返回 1 myClosure.inc(); // 返回 2 myClosure.inc(); // 返回 3 // 相信对JS熟悉的朋友都能很快理解这段代码 // 那么在这段代码运行的背后究竟发生了怎样的事情呢? |
现在,我终于知道了答案,我感到很兴奋并且决定向大家解释这个答案。至少,我一定是不会忘记这个答案的。
Tell me and I forget. Teach me and I remember. Involve me and I learn.
© Benjamin Franklin
并且,在我阅读与闭包相关的现存的资料时,我很努力地尝试着去在脑海中想想每个事物之间的联系:对象之间是如何引用的,对象之间的继承关系是什么,等等。我找不到关于这些负责关系的很好的图表,于是我决定自己画一些。
我将假设读者对JavaScript已经比较熟悉了,知道什么是全局对象,知道函数在JavaScript当中是“first-class objects”,等等。
作用域链(Scope Chain)
当JavaScript在运行的时候,它需要一些空间让它来存储本地变量(local variables)。我们将这些空间称为作用域对象(Scope object),有时候也称作 LexicalEnvironment 。例如,当你调用函数时,函数定义了一些本地变量,这些变量就被存储在一个作用域对象中。你可以将作用域函数想象成一个普通的JavaScript对象,但是有一个很大的区别就是你不能够直接在JavaScript当中直接获取这个对象。你只可以修改这个对象的属性,但是你不能够获取这个对象的引用。
作用域对象的概念使得JavaScript和C、C++非常不同。在C、C++中,本地变量被保存在栈(stack)中。在JavaScript中,作用域对象是在堆中被创建的(至少表现出来的行为是这样的),所以在函数返回后它们也还是能够被访问到而不被销毁。
正如你做想的,作用域对象是可以有父作用域对象(parent scope object)的。当代码试图访问一个变量的时候,解释器将在当前的作用域对象中查找这个属性。如果这个属性不存在,那么解释器就会在父作用域对象中查找这个属性。就这样,一直向父作用域对象查找,直到找到该属性或者再也没有父作用域对象。我们将这个查找变量的过程中所经过的作用域对象乘坐作用域链(Scope chain)。
在作用域链中查找变量的过程和原型继承(prototypal inheritance)有着非常相似之处。但是,非常不一样的地方在于,当你在原型链(prototype chain)中找不到一个属性的时候,并不会引发一个错误,而是会得到undefined
。但是如果你试图访问一个作用域链中不存在的属性的话,你就会得到一个
ReferenceError 。
在作用域链的最顶层的元素就是全局对象(Global Object)了。运行在全局环境的JavaScript代码中,作用域链始终只含有一个元素,那就是全局对象。所以,当你在全局环境中定义变量的时候,它们就会被定义到全局对象中。当函数被调用的时候,作用域链就会包含多个作用域对象。
全局环境中运行的代码
好了,理论就说到这里。接下来我们来从实际的代码入手。
1 2 3 4 5 |
// my_script.js "use strict"; var foo = 1; var bar = 2; |
我们在全局环境中创建了两个变量。正如我刚才所说,此时的作用域对象就是全局对象。
在上面的代码中,我们有一个执行的上下文(myscript.js自身的代码),以及它所引用的作用域对象。全局对象里面还含有很多不同的属性,在这里我们就忽略掉了。
没有被嵌套的函数(Non-nested functions)
接下来,我们看这段代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
"use strict"; var foo = 1; var bar = 2; function myFunc() { //-- define local-to-function variables var a = 1; var b = 2; var foo = 3; console.log("inside myFunc"); } console.log("outside"); //-- and then, call it: myFunc(); |
当
myFunc 被定义的时候,
myFunc 的标识符(identifier)就被加到了当前的作用域对象中(在这里就是全局对象),并且这个标识符所引用的是一个函数对象(function object)。函数对象中所包含的是函数的源代码以及其他的属性。其中一个我们所关心的属性就是内部属性[[scope]]
。[[scope]]
所指向的就是当前的作用域对象。也就是指的就是函数的标识符被创建的时候,我们所能够直接访问的那个作用域对象(在这里就是全局对象)。
“直接访问”的意思就是,在当前作用域链中,该作用域对象处于最底层,没有子作用域对象。
所以,在 console.log("outside") 被运行之前,对象之间的关系是如下图所示。
温习一下。 myFunc 所引用的函数对象其本身不仅仅含有函数的代码,并且还含有指向其被创建的时候的作用域对象。这一点非常重要!
当myFunc
函数被调用的时候,一个新的作用域对象被创建了。新的作用域对象中包含myFunc
函数所定义的本地变量,以及其参数(arguments)。这个新的作用域对象的父作用域对象就是在运行myFunc
时我们所能直接访问的那个作用域对象。
所以,当 myFunc 被执行的时候,对象之间的关系如下图所示。
现在我们就拥有了一个作用域链。当我们试图在myFunc
当中访问某些变量的时候,JavaScript会先在其能直接访问的作用域对象(这里就是myFunc() scope
)当中查找这个属性。如果找不到,那么就在它的父作用域对象当中查找(在这里就是Global Object
)。如果一直往上找,找到没有父作用域对象为止还没有找到的话,那么就会抛出一个ReferenceError
。
例如,如果我们在myFunc
中要访问a
这个变量,那么在
myFunc scope 当中就可以找到它,得到值为1
。
如果我们尝试访问foo
,我们就会在myFunc() scope
中得到3
。只有在myFunc() scope
里面找不到foo
的时候,JavaScript才会往Global Object
去查找。所以,这里我们不会访问到Global Object
里面的foo
。
如果我们尝试访问bar
,我们在myFunc() scope
当中找不到它,于是就会在Global Object
当中查找,因此查找到2。
很重要的是,只要这些作用域对象依然被引用,它们就不会被垃圾回收器(garbage collector)销毁,我们就一直能访问它们。当然,当引用一个作用域对象的最后一个引用被解除的时候,并不代表垃圾回收器会立刻回收它,只是它现在可以被回收了。
接下来,为了图表直观起见,我将不再将函数对象画出来。但是,请永远记着,函数对象里面的[[scope]]
属性,保存着该函数被定义的时候所能够直接访问的作用域对象。
嵌套的函数(Nested functions)
正如前面所说,当一个函数返回后,没有其他对象会保存对其的引用。所以,它就可能被垃圾回收器回收。但是如果我们在函数当中定义嵌套的函数并且返回,被调用函数的一方所存储呢?(如下面的代码)
1 2 3 4 5 6 7 |
function myFunc() { return innerFunc() { // ... } } var innerFunc = myFunc(); |
edia给出的解释并没有太大的帮助。闭包是什么时候被创建的,什么时候被销毁的?具体的实现又是怎么样的?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
"use strict"; var myClosure = (function outerFunction() { var hidden = 1; return { inc: functionlways" id="crayon-5812f52cd1c50197376896" style=" margin-top: 12px; margin-bottom: 12px; font-size: 13px !important; line-height: 15px !important;">
上述代码如果以 Scaling你可以通过使用
下面例子把一个元素的尺寸根据最初的尺寸放大两倍:
下列例子把一个元素缩放到最初宽度的两倍,并且把高度压缩到最初的一半:
上述例子使用逗号分隔的值例如 这里需要注意当SVG元素缩放时,整个坐标系被缩放,导致元素在视窗中重新定位,现在不用担心这些,我们会在下一节中讨论细节。 SkewSVG元素也可以被倾斜,要倾斜一个元素,你可以使用一个或多个倾斜函数
函数 倾斜角度声明是无单位角度的默认是度。 注意倾斜一个元素可能会导致元素在视窗中重新定位。在下一节中有更多细节。 Rotation你可以使用
可选的 在函数 下面的例子是以当前用户坐标系中的
然而,如果你想要一个元素围绕它的中心旋转,你也许想要像CSS中一样声明中心为 坐标系变化现在我们已经讨论了所有可能的SVG变换函数,我们深入挖掘视觉部分和对SVG元素添加每个变换的效果。这是SVG变换最重要的部分。因此它们被称为“坐标系统变换”而不仅仅是“元素变换”。 在这个说明中, 这个行为类似于在HTML元素上添加CSS变换-HTML元素坐标系发生了变换,当你把变换组合使用时最明显。虽然在很多方面很相似,HTML和SVG的变换还是有一些不同。 主要的不同是坐标系。HTML元素的坐标系建立在元素自身智商。然而,在SVG中,元素的坐标系最初是当前坐标系或使用中的用户空间。 当你在一个SVG元素上添加 然后,元素新的当前坐标系被在 要理解如何添加SVG变换,让我们从可视化的部分开始。下面图片是我们要研究的SVG画布。 code>属性和CSS属性,包括如何使用,以及你必须知道的关于SVG坐标系变换的知识。 这是我写的SVG坐标系统和变换部分的第二篇。在第一篇中,包括了任何要理解SVG坐标系统基础的需要知道的内容;更具体的是, SVG viewport,
这一部分我建议你先阅读第一篇,如果没有,确保你在阅读这篇之前已经读了第一篇。
|
1 |
matrix(<a> <b> <c> <d> <e> <f>) |
上述声明通过一个有6个值的变换矩阵声明一个变换。matrix(a,b,c,d,e,f)
等同于添加变换matrix[a b c d e f]
。
如果你不精通数学,最好不要用这个函数。对于那些精通的人,你可以在这里阅读更多关于数学的内容。因此这个函数很少使用-我将忽略来讨论其他变换函数。
Translation
要移动SVG元素,你可以用translate()
函数。translate
函数的语法如下:
1 |
translate(<tx> [<ty>]) |
translate()
函数输入一个或两个值得来声明水平和竖直移动值。tx
代表x
轴上的translation
值;ty
代表y
轴上的translation
值。
ty
值是可选的,如果省略,默认值为0
。tx
和ty
值可以通过空格或者逗号分隔,它们在函数中不代表任何单位-它们默认等于当前用户坐标系单位。
下面的例子把一个元素向右移动100
个用户单位,向下移动300
个用户单位。
1 |
<circle cx="0" cy="0" r="100" transform="translate(100 300)" /> |
上述代码如果以translate(100, 300)
用逗号来分隔值的形式声明一样有效。
Scaling
你可以通过使用scale()
函数变换来向上或者向下缩放来改变SVG元素的尺寸。scale
变换的语法是:
1 |
scale(<sx> [<sy>]) |
scale()
函数输入一个或两个值来声明水平和竖直缩放值。sx
代表沿x轴的缩放值,用来水平延长或者拉伸元素;sy
代表沿y轴缩放值,用来垂直延长或者缩放元素。
sy
值是可选的,如果省略默认值等于sx
。sx
和sy
可以用空格或者逗号分隔,它们是无单位值。
下面例子把一个元素的尺寸根据最初的尺寸放大两倍:
1 |
<rect width="150" height="100" transform="scale(2)" x="0" y="0" /> |
下列例子把一个元素缩放到最初宽度的两倍,并且把高度压缩到最初的一半:
1 |
<rect width="150" |