成为一个高效的JavaScript开发者的秘诀之一就是真正理解这门语言的语义。本文将会通过通俗易懂的图表来解释JavaScript中最基本的核心内容。
随处可见的引用
简单来说,JavaScript中的变量就是对内存中某个值的引用。这里的值可以是基本类型,如strings, numbers, booleans,也可以是引用类型如objects和functions。
局部变量
下面这个例子中,我们在最上层作用域内创建了四个局部变量,并将它们指向基础类型的值。
2 | var name = "Tim Caswell" ; |
4 | var isProgrammer = true ; |
5 | var likesJavaScript = true ; |
7 | isProgrammer === likesJavaScript; |
我们发现这两个布尔类型的变量在内存中指向同一个值。这是因为基础类型的值都是不可变的,所以虚拟机可以优化成让所有的引用都指向同一个值(译者注:为了节省内存开销)。
在上面的代码中我们用恒等号===检查这两个变量的引用是否为同一个值,测试结果为true。
图中黄色矩形代表最上层的作用域。里面的变量是最上层作用域中的局部变量,千万不要与全局对象的属性混淆。
对象与原型链
对象就是拥有对其他对象和原型引用的集合。除此之外,还有唯一特殊的就是原型链,通过原型链,可以访问在局部对象不存在而在其父级对象上的属性。
09 | var jack = Object.create(tim); |
11 | jack.name = "Jack Caswell" ; |
上述代码中,我们把一个有四个属性的对象赋给了变量tim
,然后又创建了一个继承tim的新对象并赋给变量jack
。接着我们在局部对象中重写了两个属性。
现在当我们要访问jack.likesJavaScript
的时候,首先要在jack引用的对象中查找thelikesJavaScript
属性,如果不存在再去父级对象中查找,最后我们在变量tim引用的对象中找到了这个属性值为true。
全局对象
是不是很想知道为什么像jslint这类工具总是提醒你要在声明变量前加上var
,下面这个例子就很好的解释了原因。
1 | var name = "Tim Caswell" ; |
3 | var isProgrammer = true ; |
5 | likesJavaScript = true ; |
可以发现上述代码中的likesJavaScript
并不是最上层闭包中的一个自由变量而是全局对象的一个属性。虽然这点只有当你尝试将很多脚本混合在一起的时候才可能遇到麻烦,但是我们还是应该避免在实际开发中出现省略var
的情况。
为了保证变量在当前闭包和它的子闭包内,就要时刻记住在变量声明前加上var
。这条简单的准则,对你有利无害。
如果一定要在全局对象上添加属性,浏览器中,可以像这样添加:window.woo
,在nodejs中,可以像这样添加:global.goo
。
函数和闭包
JavaScript不仅仅是一组链式数据结构。它还有函数(一种可执行、可调用的代码片段)。这些函数形成链式作用域和闭包。
了解闭包
函数可以理解成一种包含可执行代码和属性的特殊对象。每一个函数都有一个 [scope]
属性,这个属性存储着函数在定义时所在的上下文环境。当一个函数返回的时候,其所在的上下文环境就会被销毁,当前上下文也会切换到该函数调用者所在的上下文。
在下面这个例子中,我们创建了一个简单的工厂方法用来生成一个闭包并且返回一个函数。
1 | function makeClosure(name) { |
6 | var description1 = makeClosure( "Cloe the Closure" ); |
7 | var description2 = makeClosure( "Albert the Awesome" ); |
8 | console.log(description1()); |
9 | console.log(description2()); |
2 | Cloe the Closure Albert the Awesome |
当我们调用函数description1()
的时候,虚拟机查找它引用的函数并执行该函数。该函数执行过程中需要访问名为name
的局部变量,它在闭包作用域中找到了它。当想为每一个生成的函数分配单独的空间存储局部变量的时候,工厂方法是一个非常不错的选择。
可以看看这篇文章why use closure ,将对你深入了解闭包会有很大帮助。
共享函数和THIS
有时候出于性能的考虑,或者仅仅是因为你青睐某种风格,JavaScript提供了一个this
关键字,this
允许你重用一个函数对象,通过不同的调用方式,该对象所在的作用域也不同。
02 | name: "Lane the Lambda" , |
03 | description: function () { |
07 | var description = Lane.description; |
09 | description: Lane.description, |
10 | name: "Fred the Functor" |
13 | console.log(Lane.description()); |
14 | console.log(Fred.description()); |
15 | console.log(description()); |
16 | console.log(description.call({ |
17 | name: "Zed the Zetabyte" |
1 | Lane the Lambda Fred the Functor undefined Zed the Zetabyte |
上述图表中,我们可以看出虽然将Fred.description
设置成Lane.description
,但依然指向那个函数。因此,这三个引用都拥有那个匿名函数的平等所有权。这就是为什么我尽量不调用构造原型中的方法,因为如果我这么做的话就意味着我将函数绑定到了对象本身和构造函数上。(如果想更加深入的了解this,可以看这篇文章 what is this)
总结
我已经乐此不疲地用图表阐述了这些数据结构。我希望这篇文章能够帮助大家更好的理解JavaScript的精髓。我有过前端开发、后端开发和服务端架构经验。我希望我这种独特的方式能够帮助来自全球各地的开发者更好的了解JavaScript的内部机制。
原文链接/译文链接