一个bug了解JS的核心知识点
由一段代码来开始对JS中一些知识点的学习
1 | function wrapElements(a){ |
预想的这里要输出10,但是实际上却是udefined
。
这个bug代码段的主要原因是作用域
问题,但是还涉及到闭包
、变量提升
、立即调用函数
等,因此做一个笔记。
作用域
在ES5
只有全局作用域
和函数作用域
而没有块级作用域
,这跟其他的很多编程语言都是不一样的,而且这样会照成很多的不合理的场景。
至于js没有块级作用域
的意思是,变量定义的作用域不是离其最近的封闭语句或者代码块,而是包含他的函数。
但是到了ES6
,新增了let
命令,他用来声明的变量只在代码块中有效,这样就形成了一个块级作用域,另外,ES6
新增的const
也是可以形成块级作用域的。
1 | { |
闭包
难以理解闭包的一点其实是:闭包存储的是其外部变量的引用
,而不是其值
。这一点和OC中的block是一致的。
在js中关于闭包你需要知道的有三条:
- js允许引用当前函数以外定义的变量
- 即使当前函数已经返回,闭包也可以引用到变量的值
- 闭包可以更新外部变量的值
另外一点是,闭包比创建他们的函数有更长的生命周期,这么看来真的和OC中的block是一样的。
绑定与赋值
在运行时进入到一个作用域,js会为每一个绑定到该作用域的变量在内存中分配一个槽(slot
),这里的wrapElements
函数绑定了三个局部变量:result
、i
、length
。在循环体里面,为result存储的每一个变量分配了一个闭包
。
在这里由于赋值变量为一个闭包,我们希望的是该闭包函数存储的是其创建时的i
值。但是实际上由于上述闭包的特性,这里存储的是i的引用
,在循环结束之前,i都是变化的
,最后固定为5
,然后结束循环。
因此这里所有的result数组中的变量存储的都是一个返回a[5]
的匿名函数,到这里,就应该明白了,a数组
一共才存储了5个数据,最大索引是4
,对他进行索取第5个元素
肯定是undefined
的。
换句话说,绑定和赋值就是对变量的声明行为:对变量的声明
和赋值
。js隐式的
提升声明部分到封闭函数的顶部(绑定行为,为变量分配槽
),从而使得变量的作用域就是整个函数,而将赋值留在原地。
变量提升(Hoists)
既然js会为当前作用域下绑定的变量分配槽,那么,我这么写会不会就会在循环体中一直创建新的槽
1 | function wrapElements(a){ |
运行之后发现,结果还是一样的,这是因为js还有一个特性叫变量提升
。
var i = 0 ,length = a.length
由于js的变量提升,实际执行效果相当于一开始的写法。
变量提升的意思一句话来说就是:脚本开始运行时,会把该变量提升到该作用域的头部进行声明
。
一个简单的变量提升例子:
1 | function oop(){ |
实际上在js编译的时候是这个样子的:
1 | function oop(){ |
这是由于let不会发生变量提升,所以打印b的时候报错。var将a提升到当前作用域的头部,对a的打印为没有赋值。
在Airbnb的js编码规范中提到,尽量在作用域的顶部声明该作用域下的全局变量,这更可以有效的避免变量声明提升的问题。
另外Airbnb关于提升总结了以下有几点:
变量声明会提升至作用域的顶部,但赋值不会
匿名函数表达式会提升他们的变量名,但不会提升函数的赋值。这一点其实是和普通的变量是一样的。
1 | function exp(){ |
- 命名函数表达式会提升变量名,但是不会提升函数名和函数体。这一点和上一条的匿名函数式一样的。
1 | function example() { |
函数声明
提升他们的名字和函数体
1 | function example() { |
立即调用函数(IIFE)
在js中函数是一等公民
,这一点是使用js的程序员都知道的。关于函数,他可以作为一个变量
,也可以作为另一个函数的参数
以及返回值
。
声明一个函数很简单:
1 | function foo(){ |
将一个函数作为一个变量的值也可以有多种方法:
1 | var f = function foo(){ |
而立即调用函数则是函数的一个新写法,他会在书写的地方立即执行,不需要调用:
1 | (function(){ |
如果这个函数有参数,也可以这样写,只不过上一种把参数省略掉了
1 | var outerI = 'green'; |
其实这做是没有必要的,因为立即调用函数可以获得到当前作用域内所有的变量,大可这么写
1 | var outerI = 'green'; |
或者直接省略中间变量j
,这都得益于上面闭包的特性:
1 | var outerI = 'green'; |
使用函数需要注意的点:
- 永远不要在一个非函数代码块(if、while、for等)中
声明
一个函数,要把那个函数赋给一个变量,而且使用函数表达式来实现。
先来看一个例子:
1 | function f(){return 'global';}; |
上面的代码没有问题,如果稍微修改一下:
1 | function f(){return 'global';}; |
输出结果真的是这样的么?真实的输出结果其实是:
1 | test(true); // ['local','local'] |
我们想当然的以为if代码块中的f()是一个局部变量,只有在x === true
的时候才会放入result中,但是实际上在if代码块中的f()
函数是当前作用域下的全局变量,不是if代码块内部的局部变量,if代码块不会产生作用域,这一点和其他的编程语言可能会有些许不同。
因此,一个好的编码规范是如下这样的:
1 | // bad |
最后,如何修改一开始的代码bug呢
上面说了,照成这样的bug是由于作用域问题
,那么合理使用作用域就可以解决这样的bug,或者说合理的使用作用域就可以有效的规避这样的bug出现。
1.使用let
由于使用var存在变量提升,那么就使用let不让他变量提升
1 | function wrapElements(a){ |
2.使用立即调用函数
另外是使用立即调用函数,强制性的创建一个局部作用域。
1 | function wrapElements(a){ |
学到了什么
- 在for循环计数器中,最好使用
let
命令 - js作用域规则
- 闭包的特点和OC中的block是一样的