转自:http://www.cnblogs.com/yangjunhua/archive/2012/05/22/2513340.html
一、函数声明与函数表达式
1、函数声明之后,可以在声明之前调用,也可以在声明之后调用,因为所有的函数声明(包括var声明的变量)都会在代码执行之前就加载到作用域中,
函数名其实是一个Function类型的对象的引用,声明函数时函数名其实也就被赋值了;
2、而函数表达式则不同,函数表达式是将函数赋值给一个变量,只有当代码执行到那一行的时候,函数才真正的有定义,因此这个变量只有在表达式之后才能使用,
否则这个变量为undefined,如果这个变量不是通过var关键字声明,那么它就没有任何值。
例1:
fn1();
fn2();
console.log(
typeof
fn2);
function
fn1(){
console.log(
"fn1"
);
}
var
fn2 =
function
(){
console.log(
"fn2"
);
}
fn2();
|
例2:
var
fn = 10;
function
fn(){
console.log(
"test"
);
}
console.log(fn);
fn();
|
在例2的代码中,变量声明会在代码执行之前就加载到作用域中,函数声明时,函数名就是一个Function类型的对象引用,此时它已经被赋值了,
同一个变量被多次声明时,只会忽略后面的声明,但会执行后面的变量初始化;此时变量fn的值已经被覆盖了,值为10。
例2中的代码可以理解为下面的代码:
var
fn;
function
fn(){
console.log(
"test"
);
}
fn = 10;
console.log(fn);
fn();
|
二、函数执行环境、变量对象与作用域链
1、后台中每个函数的执行环境都有一个变量对象,全局环境中的变量对象(浏览器中的window对象)始终存在,当函数被调用时,其活动对象等同于变量对象,
函数中的局部执行环境中的活动对象只存在函数执行过程中,函数执行完后,其活动对象就会被撤销;
2、当一个函数第一次被执行时,会创建一个执行环境(execution context)和相应的作用域链(包含了所有外部函数的活动对象及全局变量对象),
并把作用域链赋值给函数的一个特殊的内部[[Scope]]属性中,然后用this,arguments和其他参数初始化函数的活动对象(activation object),函数在执行时,
会通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链,注意函数本身也有一个作用域,作为执行环境作用域链的前端。
3、理解函数执行环境、变量对象与作用域链,是理解闭包的关键所在。
4、关于this,arguments对象需要注意的地方:
当一个函数第一次被执行时,就会用this,arguments对象等其他参数初始化函数的活动对象,但是如果是闭包(即内部函数)在搜索这两个对象时,
只会搜索到其活动对象为止,而不会搜索其他函数作用域中的活动对象,也就是说内部函数无法访问其包含函数的this,arguments对象,
但是可以通过变量的形式来访问外部函数中的this,arguments对象,由于闭包的执行环境具有全局性,因此其this指向window对象。
三、闭包及其所存在的问题
闭包:是指有权访问外部函数作用域中的变量的函数,创建一个闭包的方式就是在一个函数中创建另一个函数
例3:
function
fn(name){
return
function
(){
return
name
}
}
var
nm = fn(
"yjh"
);
console.log(nm());
|
在例3中,fn中有一个匿名函数,即闭包,在上面代码的执行中,结果为yjh,因为闭包可以访问外部函数作用域中的变量,
内部函数中的变量是从作用域链的前端开始搜索的,如果没有找到,直至搜索到作用域链的末端(全局执行环境)。
闭包所存在的问题:
就拿例3中的例子来说,在函数fn执行完毕后,其变量name(包括其活动对象)并没有被销毁,因为内部函数(闭包)的作用域链包含了fn中的作用域,
内部函数仍然在引用fn中的活动对象,当fn执行完后,其作用域链会被销毁,但是它的活动对象会继续保存在内存中,直到匿名函数被销毁,它的活动对象才会被销毁;
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存,过多的使用闭包会导致内存占用过多。
四、块级作用域
JavaScript中没有块级作用域,不像其他编程语言中拥有块级作用域的概念,但是我们可以在js中模拟块级作用域的概念,我们都知道,
一般而言,函数执行完毕后,执行环境中的活动对象就会被撤毁,因此我们可以像下面这样:
声明一个匿名函数,然后立即调用它,这样在这个匿名函数中声明的变量在函数执行完毕就会被撤毁,外部函数也不能访问其中定义的变量
函数声明和调用的几种写法:
function
(){}
function
fn(){}
var
fn =
function
(){}
(fn =
function
(){})()
var
fn =
function
(){}()
(
function
(){})()
|
五、解析js中常见的一个问题
例4:
<
ul
>
<
li
>1</
li
>
<
li
>2</
li
>
<
li
>3</
li
>
<
li
>4</
li
>
</
ul
>
|
var
li = document.getElementsByTagName(
"li"
);
for
(
var
i=0;i<li.length;i++){
li[i].onclick =
function
(){
console.log(i);
}
}
|
例4中的代码相信很多人都遇到过,执行结果为什么不是预期的所想要的
解读一下:
首先变量li获取了所有的li元素节点,然后通过for循环为每个li元素的click事件绑定一个事件处理函数,注意click事件绑定函数是通过赋值表达式来绑定的,
而当代码执行到那一行的时候,并不会执行(这并不是函数赋值表达式引起的),因为click事件是晚绑定的,即只有当某个li元素的click事件被触发时,才会绑定函数,
而在click事件被触发之前,事件处理函数并没有定义(因为赋值表达式语句并没有执行),此时全局环境中的i变量通过for循环已经变为5了,事件处理函数作为一个闭包,
访问的全局环境中的i变量都是4,因此每次输出的结果也都是4了。
解决办法:可以通过匿名函数传参的形式来解决,因为函数传参,是传值,而不是传址,即复制另一个变量值的副本
方法1
var
li = document.getElementsByTagName(
"li"
);
for
(
var
i=0;i<li.length;i++){
(
function
(j){
li[i].onclick =
function
(){
console.log(j);
}
})(i)
}
方法2
var
li = document.getElementsByTagName(
"li"
);
for
(
var
i=0;i<li.length;i++){
(
function
(){
var
j = i;
li[i].onclick =
function
(){
console.log(j);
}
})(i)
}
|
两种方法大同小异,都是通过函数传参的形式来保存全局变量i值的一个副本,然后使事件处理函数中的变量来访问这个副本来达到预期的结果。
不好意思,之前这里的i变量初始化为1,那么第一个li就没有绑定事件处理函数,忽略小细节了,之前只注重输出结果了,感谢园友笨瓜一号,xujif的提醒。
六、总结:
1、变量,函数声明都会提前加载到作用域中,函数赋值表达式只有当代码执行到那一行时才会有定义
2、每一个函数都有一个执行环境(作用域),变量对象(活动对象),以及相应的作用域链(各个活动对象的指针列表),当函数执行时,作用域链会赋值给函数
的内部特殊属性[[Scope]]以构建起函数执行环境的作用域链
3、闭包就是指有权访问外部作用域的变量的函数,创建闭包的方式就是在一个函数中创建另一个函数,闭包会包含外部函数的作用域,因此占用的内存会比其他函数多,
过多的使用闭包会使内存占用过多,还会导致外部函数的活动对象不会被释放,只有闭包的引用被撤销,外部函数作用域中的活动对象才会被释放
4、闭包(内部函数)在搜索this,arguments对象时,只会在自身的活动对象中搜索,永远也不能访问外部作用域中的this,arguments对象,但可以通过变量赋值的
形式来访问外部函数作用域中的this,arguments对象,由于闭包的执行环境具有全局性,因此其this对象指向window对象,全局环境中没有arguments对象的
5、在js中还可以模拟其他语言中的块级作用域,即声明一个匿名函数,然后立即调用它