Javascript的变量作用域:全局变量和局部变量。
1、全局变量可以在函数内部被访问;
2、函数内的局部变量不可以在函数外被访问;
3、在函数内部声明变量时,一定要用var声明,否则就是全局变量。
闭包——有权访问另一个函数作用域中变量的函数;被内部函数访问的外部函数的变量可以保存在外部函数作用域内而不被回收。
用途:
1、在函数外部读取函数内部的局部变量(沿着作用域链寻找);
2、变量的值始终保持在内存中。
例子:
function f1() {
var n=99;
nAdd= function(){ n+1; }
function f2() { alert(n); }
return f2;
}
var result = f1(); //把f1()赋值给result,实际上result=f2
result(); //调用f2(),result()=f2(),结果为99
nAdd();
result(); //结果为100
因为f1是f2的父函数,f1定义了n,f2调用了n,而且f2被赋值给一个全局变量result,所以f2一直存在在内存中,而f2又依赖于f1,所以f1也一直存在,不会在调用结束后被垃圾回收机制(garbage collection)回收。
又因为nAdd内有用var声明,所以nAdd是全局变量,可以在函数外部访问。又因为n没有被回收,所以结果为100。
JS
没有块级作用域,用匿名函数可以用来模仿块级作用域并避免出现命名参数冲突的问题。
1
2
3
|
(
function(){
//块级作用域
})();
|
这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数,这种做法可以减少闭包占用内存的问题。
闭包只能取得包含函数中任何变量的最后一个值,即包含函数执行完毕时变量的值。
常见的陷阱
看看这个:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function
createFunctions(){
var
result =
new
Array();
for
(
var
i=0; i < 10; i++){
result[i] =
function
(){
return
i;
};
}
return
result;
}
var
funcs = createFunctions();
for
(
var
i=0; i < funcs.length; i++){
console.log(funcs[i]());
}
|
乍一看,以为输出 0~9 ,万万没想到输出10个10?
这里的陷阱就是:函数带()才是执行函数! 单纯的一句 var f = function() { alert('Hi'); }; 是不会弹窗的,后面接一句 f(); 才会执行函数内部的代码。上面代码翻译一下就是:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
var
result =
new
Array(), i;
result[0] =
function
(){
return
i; };
//没执行函数,函数内部不变,不能将函数内的i替换!
result[1] =
function
(){
return
i; };
//没执行函数,函数内部不变,不能将函数内的i替换!
...
result[9] =
function
(){
return
i; };
//没执行函数,函数内部不变,不能将函数内的i替换!
i = 10;
funcs = result;
result =
null
;
console.log(i);
// funcs[0]()就是执行 return i 语句,就是返回10
console.log(i);
// funcs[1]()就是执行 return i 语句,就是返回10
...
console.log(i);
// funcs[9]()就是执行 return i 语句,就是返回10
|
为什么只垃圾回收了 result,但却不收了 i 呢? 因为 i 还在被 function 引用着啊。好比一个餐厅,盘子总是有限的,所以服务员会去巡台回收空盘子,但还装着菜的盘子他怎么敢收? 当然,你自己手动倒掉了盘子里面的菜(=null),那盘子就会被收走了,这就是所谓的内存回收机制。
循环和闭包:
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
输出为5个6;
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j*1000 );})();
}
输出12345
改进:
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})( i );
}
再进一步优化,只需把var改成let
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
变量提升:
因为JavaScript是一种先编译再执行的语句,所以,会把var a=1;拆分成两个部分,即 var a;和 a = 1;所有的变量都会在该范围内(作用域)的前面进行声明,当运行到具体步骤时在进行赋值。
只要函数内定义了一个局部变量,函数在解析的时候都会将这个变量“提前声明”:
同理,函数类似,先把函数声明放在最前面,然后在后面进行调用。如果先后虽一个函数定义了两个函数声明,以后者为基准。
fn();
var fn; //定义无效
function fn(){}; //把这个函数声明放在前面
function afn=fn() {}; //这是定义另一个函数,会把 function afn放前面,但是不会定义fn()。
作用域链(scope chain):
定义:根据在内部函数可以访问外部函数变量的这种机制,用链式查询决定哪些数据能被内部函数访问。
作用域链是基于调用栈的,而不是代码中的作用域嵌套。
父函数中有一个子函数,则子函数的作用域链等于 父函数的作用域链+自己的活动对象。
当代码在一个环境中执行时,会创建变量对象的的一个作用域链。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是一个函数,则将其活动对象作为变量对象。
每一个函数都有自己的执行环境,当执行流进一个函数时,函数环境就会被推入一个环境栈中,而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境,这个栈也就是作用域链。
当函数被调用时:
- 先创建一个执行环境(execution context),及相应的作用域链;
- 将arguments和其他命名参数的值添加到函数的活动对象(activation object)
function sayName(name){
return name;
}
var say = sayName('jozo');
这段代码包含两个作用域:
a.
全局作用域;
b.
sayName函数的作用域,也就是只有两个变量对象,当执行到对应的执行环境时,该变量对象会成为活动对象,并被推入到执行环境作用域链的前端,也就是成为优先级最高的那个。
再来看看看闭包的作用域链:
function sayName(name){
return function(){
return name;
}
}
var say = sayName('jozo');
这个闭包实例比上一个例子多了一个匿名函数的作用域:
对于二维查找可以总结为:当代码需要查找一个属性(property)或者描述符(identifier)的时候,首先会通过作用域链(scope chain)来查找相关的对象;一旦对象被找到,就会根据对象的原型链(prototype chain)来查找属性(property)
var foo = {}
function baz() {
Object.prototype.a = 'Set foo.a from prototype';
return function inner() {
console.log(foo.a);
}
}
baz()();
代码首先通过作用域链(scope chain)查找”foo”,最终在Global context中找到;然后因为”foo”中没有找到属性”a”,将继续沿着原型链(prototype chain)查找属性”a”。
this对象是在运行是基于函数的执行环境绑定的。
在全局函数中,this等于window,所以此处的this.name指代在全局作用域中的“the window”。
当函数作为某个对象调用时,this等于那个对象。但,匿名函数具有全局性,this指针指向window。
因为在object的函数中定义了一个that的局部变量,并把this的值赋给that,所以this此时是指object对象中的重新赋值的“my object”。