面试题
首先,让我们来看一题面试题;
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
items[i].onclick=function() {
console.log(i)
}
}//假设页面上有5个li元素,而每次点击每个li元素输出的却都是5,是否与你预期不一样呢?
作用域
作用域(scope)指的是变量存在的范围。Javascript只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。
在函数外部声明的变量就是全局变量(global variable),它可以在函数内部读取。
在函数内部定义的变量,外部无法读取,称为“局部变量”(local variable)。
var a=1; //全局变量a
function fn1() {
var a=2; //局部变量a
console.log(a)
}
fn1(); //输出2,全局变量a的值被替代;如果没有var a=2,则获取全局变量a输出1
console.log(a) //输出1,全局变量a保持不变;
- 就近原则:同时,上述代码也体现了作用域的就近原则,即函数fn1在console.log(a)的时候会在其自己的作用域里找到a,如果没有则往上,不断从“父作用域”找,直到找到;
但是请注意,请不要认为就近一定是对的,请看如下代码:
var a=1;
function fn1() {
var a=2;
fn2();
}
function fn2() {
console.log(a);
}
fn1() //输出结果为1;
上面代码中,函数fn2最近的a是2,但函数fn2是在函数f的外部声明的,所以它的作用域绑定外层,内部变量a不会到函数fn1体内取值(局部变量外部无法获取),所以输出1,而不是2。
或者** 我把fn1注释掉,也许你会一目了然?**
var a=1;
//function fn1() {
//var a=2;
// fn2();
// }
function fn2() {
console.log(a);
}
很显然如果fn2被调用(无论在任何地方调用,因为fn2是在函数f的外部声明),则就是输出全局变量a为1
立即执行函数(IIFE)
在上述代码中,我们可以看出全局变量很不稳点,可被任意获取改变,容易发生不必要的麻烦冲突,例如:
var a=1;
function fn1()..;
function fn2()..;
function fn3()..;
//成千上万行代码
....
function fnx() {
a=2; //全局变量被赋予新值;
}
fn()
console.log(a) //输出2
也许你再敲完成千上万行代码之后不经意把全局变量a给改变了,等你下次再用a,它就是2了(也许你期待的依然是1?);因此应该尽量注意小心使用全局变量,或者说尽量避免,立即执行函数(IIFE)很好地做到了这一点;
// 写法一
var a = 1;
fn1(a);
fn2(a);
// 写法二
(function (){
var a = 1;
fn1(a);
fn2(a);
}());
通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:
- 一是不必为函数命名,避免了污染全局变量;
- 二是IIFE内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
上述例子的写法二优于写法一,很好地体现了这两点;
此外,值得注意:的是立即执行函数顾名思义就是立即调用,由此我们很容易明白函数后面括号的意义(形如调用:fn());但是你不能在函数的定义之后加上圆括号,这会产生语法错误。因为function这个关键字即可以当作语句,也可以当作表达式。
function fn(){/* do something */}()
//定义函数直接加括号,报错
function fn(){/* do something */}()
//函数声明语句
var a=function fn(){/* do something */}()
//表达式
-
解决办法:避免让function出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面,或者加一些其他符号也可以:
(function(){ /* do something */ }()); // 或者 (function(){ /* do something */ })(); //其他符号 !function(){ /* do something */ }(); ~function(){/* do something */ }(); -function(){ /* do something */ }(); +function(){/* do something */}();
但是!,我们应该格外小心将立即执行函数放入圆括号的使用,因为我们都知道圆括号可以表示函数定义(也可以用来放置函数的参数),调用,一不小心很容易出错,例如你在立即执行函数之前不小心写了个2:
//写法一:
2
(function(){
var a=1;
console.log(a)
}())
//报错,2 is not a function,其实上述写法类似与你写:2(一些参数),而括号里的内容就作为参数传递了;
很显然你都没有定义函数2,调用它怎会不报错?
//写法二:
2
!function(){
var a=1;
console.log(a)
}()
//不报错,输出1
-
立即执行函数也可以接受参数:
var a=1;
!function(a){
//var a;注意,函数的形参a相当于在自己的作用域里声明了一个局部量a;
console.log(a) //输出undefined,虽然形参a有声明,但未赋值;
}()//但是如果立即执行函数没有形参,则输出获取全局变量a输出 var a=1; !function(){ //var a; 注意!!!此时没有形参a,不存在var a这行代码,因此不可能输出undefined console.log(a) //从全局作用域获取全局变量a,输出1 }() //有形参依然输出全局变量a,那就要把它当作实参传入进去 var a=1; !function(a){ //var a; console.log(a) }(a) //注意,这里的a是实参,传入的是全局变量中的a,与立即执行函数中的形参a不是同一个a, //因此输出1;
变量提升
变量提升很简单,JavaScript引擎会把变量与函数名(也当作变量)进行声明前置,提升到代码头部:
var a=1;
fn1();
function fn1() {
a=2;
}
console.log(a) //输出结果为2,且虽然先调用fn1比声明在先,但不会报错
//实际执行顺序;
var a;
function fn1() {
a=2;
}
a=1
fn1();
console.log(a)
回归面试题
说了这么多,终于回到面试题了,看完你就明白我之前为什么巴拉巴拉那么多了,先看第一题,首先先回答一个问题:
var items=document.querySelectorAll('li');
var i;//变量提升,帮助理解为什么i只有一个,就是全局变量i;
for( i=0;i<items.length;i++) {
//i=0,1,2,3,4;
items[i].onclick=function() {
console.log(i)
}
}
//i=5
console.log(i) //输出5
请问,哪一个console.log(i)先执行,很显然由异步我们可以得知,click事件肯定是放到最后的,用户手速多快都是最后,所以肯定是第二个先执行;
也许你看完代码和解释很容易理解第二个console.log(i)是输出5,第一个不理解,那如果我说,上述代码中的i,从始至终都只有一个i,就是全局变量i(即便有一个function形成了新的作用域,获取的依然是全局变量i),且第一个console.log(i)又是放到最后执行的,这样你总该理解了吧
好了,问题解释完了,现在让我们说说解决办法吧:
刚才解释问题的时候找到的原因是异步和从头到尾是一个i,异步解决不了,我让它变成不同的i不就可以了吗?还记得之前提到的作用域吗?
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
var newfn=function(j) { //为了生成新的作用域,形成不同的'i'
console.log(j) //输出0,1,2,3,4
}
newfn(i)
console.log(j) //报错,不能获取局部变量j
// items[i].onclick=function() {
// console.log(i)
// }
}
看到上面的报错了吧?因此要想click获取上面新的作用域里不同的'i',我们把代码改成:
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
var newfn=function(i) { //把形参也改成i好了,注意哦,这不是全局变量i,
//每次循环都生成一个新的作用域,新的i;
items[i].onclick=function() {
console.log(i) //此时点击已经可以依次输出0,1,2,3,4了
}
}
newfn(i)
}
我声明完newfn之后又马上调用了,我再改一下,去掉函数名newfn;
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
~function(i) {
items[i].onclick=function() {
console.log(i)
}
}(i)
}
看上面代码,for循环里面不就是一个立即执行函数吗?是的,不过还有另外的方法,我还可以再改一下;观察上面的代码不就是onlick后面是一个函数,外加一个新的作用域吗?
其实说到这里,不知你有没有发现,那些看似很难复杂的问题,再你深入了解之后,你就可以看到它的本质,就可以用简单的方法解决问题了;
//回归最初的代码
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
items[i].onclick=function() {
console.log(i)
}
}
看代码function有了,那不就代表新作用域也有了?那为什么还是没有发挥新作用域的作用,即生成不同的i呢?
答案很简单:新作用域里没有任何局部变量,也没有用到形参,你要输出i的每一个不同的值,也没有将他们以实参的形式传入新的作用域里;
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
items[i].onclick=function(i) { //多一个形参
//var i,i就是形参,这一点刚才我讲过
console.log(i) //输出上面的形参i,0,1,2,3,4;
}(i)//实参传入不同的i
}
写完之后发现这还是一个立即执行函数,没毛病,老铁,它形成了一个单独的作用域,这很立即执行函数!!!(我们开始不就是这么介绍立即执行函数吗),所以说,有些概念可能就是诞生在我们解决问题的时候吧。。
不过如果你试着执行一下这段代码就会发现一个问题(怎么还有问题,是不是好烦啊哈哈哈,I think so!!!),不用点击,直接输出0,1,2,3,4,点击事件瓦特了!!!
很简单,因为函数立即执行了却没有实际的返回值啊,此时onclick等于默认的返回值undefined,而我们需要的是onclick后面是个函数啊,所以return 一个function就可以了
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
items[i].onclick=function(i) {
//var i;不要忘了这个局部变量i哦,下面输出的i就是从这里获取的
//再次提醒别搞混全局变量i和局部变量i了
return function() {
console.log(i)
}
}(i)
}
闭包
好了,终于都解决了,全文完?快了,还差一个最'神秘'的闭包;
首先让我们简单认识闭包(其实刚才已经用到好几次了):
var a = 1;
function fn() {
console.log(a);
}
f1() // 1
这就是闭包,怎么样,是不是很简单?那么它到底有什么用呢?
它的作用之一就是获取外部作用域的变量,全局作用域的变量大家都能获取,要闭包来干嘛,我说的当然是闭包用来获取外部无法获取的局部变量咯
而通常说闭包是函数嵌套一个函数,就是因为函数内的局部变量不能获取,只能嵌套一个函数(此函数就是闭包)来获取;
再把嵌套的函数作为返回值就成功获取内部变量了。看代码:
function fn1() {
var b=2;
var a= 1; //局部变量外部无法获取
function fn2() { //闭包,成功获取a
console.log(a);
}
return fn2; //返回这个闭包到外部
}
var result = fn1(); //result接受,局部变量a成功被外部获取
result(); // 1
此外,上述代码也体现了闭包了作用之二,闭包让刚才获取的局部变量始终保持在内存中,记住了它的诞生环境——fn1(){},这是因为result始终在内存中,而result(也可以理解为由于返回值,result就是fn2,fn2又用到了fn1的变量)的存在依赖于fn1,从而使得fn1环境及其内部变量一直存在,并不会在函数调用结束之后被回收;
因此也带来了副作用:外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。
又使内部变量一直存在,又消耗内存
—>谣言?闭包导致内存泄漏(用不到(访问不到)的变量,依然占居着内存空间,不能被再次利用起来。),并且不会垃圾回收机制回收?
请仔细看上面的代码,局部变量a是闭包fn2所用到的,因此根本不存在内存泄漏的说法,至于变量b没用到,也访问不到,会不会被回收呢?还是看大神写的文章吧,反正我是说不清楚哈哈哈哈
针对不同的javascript引擎,解析闭包对垃圾回收的影响
刚才说的又有点难了,还是说点相对简单,又基础,又重要,又容易弄错的吧
——>闭包,立即执行函数傻傻分不清楚,很多人理解错闭包,觉得它难,有很大一部分原因是因为这个,不知道你看到这里还会不会搞混;
还是回到面试题:
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
items[i].onclick=function() {
console.log(i)
}
}
上面的funcition获取了外部变量i,不就是闭包嘛?是的!
这么说来闭包带来了这道容易做错的面试题?是的!
解决之后的代码:
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
items[i].onclick=function(i) {
//var i;不要忘了这个局部变量i哦,下面输出的i就是从这里获取的,
//或者也许你把形参改成别的字母你就不会搞混全局变量和局部变量了
return function() {
console.log(i)
}
}(i)
}
上面的function就是一个立即执行函数吧?是的!
它再获取外部变量i的值之后,也就是闭包之后?是的!
它内部形成了一个单独的作用域,又将其以局部变量的形式封装了起来?是的!
然后返回输出封装的局部变量,解决了问题,所以立即执行函数解决了问题?是的!
综上,闭包造成问题,立即执行函数解决问题,不知道你理解了吗
由一道题目经过漫长的过程,全文终于完。。
如有错误,恳请指正!