目录
通常来讲这块是全局变量与局部变量的区分。 参考引文:{aa32aa}
全局作用域:最外层函数和在最外层函数外面定义的变量拥有全局作用域。
1)最外层函数和在最外层函数外面定义的变量拥有全局作用域
2)所有末定义直接赋值的变量自动声明为拥有全局作用域,即没有用 var 声明的变量都是全局变量,而且是顶层对象的属性。
3)所有 window 对象的属性拥有全局作用域
局部作用域:和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部,所以在一些地方也会看到有人把这种作用域称为函数作用域。
代码部分请参照引文。
函数作用域是相对块作用域来进行解释的,其和局部作用域是一个意思。参考引文:{aa30aa}
块作用域:任何一对花括号{}中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是无效的,我们称之为块级作用域。
函数作用域:在函数中的参数和变量在函数外部是无法访问的。JavaScript 的作用域是词法性质的(lexically scoped)。这意味着,函数运行在定义它的作用域中,而不是在调用它的作用域中。下文会解释。
View Code
- 1 //C语言
- 2 #include
- 3 void main()
- 4 {
- 5 int i=2;
- 6 i--;
- 7 if(i)
- 8 {
- 9 int j=3;
- 10 }
- 11 printf("%d/n",j);
- 12 }
运行这段代码,会出现 "use an undefined variable:j" 的错误。可以看到,C 语言拥有块级作用域,因为 j 是在 if 的语句块中定义的,因此,它在块外是无法访问的。
View Code
- 1
- function test() {
- 2
- for (var i = 0; i < 3; i++) {};
- 3 alert(i);
- 4
- }
- 5 test();
运行这段代码,弹出 "3",可见,在块外,块中定义的变量 i 仍然是可以访问的。也就是说,JS 并不支持块级作用域,它只支持函数作用域,而且在一个函数中的任何位置定义的变量在该函数中的任何地方都是可见的。
- var scope="global"; //全局变量
- function t(){
- console.log(scope);
- var scope="local" ;//局部变量
- console.log(scope);
- }
- t();
(console.log() 是控制台的调试工具,chrome 叫检查,有的浏览器叫审查元素,alert() 弹窗会破坏页面效果)
第一句输出的是: "undefined",而不是 "global"
第二讲输出的是:"local"
第二个不用说,就是局部变量输出 "local"。第一个之所以也是 "local",是因为 Js 中的声明提前,尽管在第 4 行才进行局部变量的声明与赋值,但其实是将第 4 行的声明提前了,放在了函数体顶部,然后在第 4 行进行局部变量的赋值。可以理解为下面这样。
- var scope="global";//全局变量
- function t(){
- var scope;//局部变量声明
- console.log(scope);
- scope="local";//局部变量赋值
- console.log(scope);
- }
- t();
具体细节可以查阅犀牛书 (《JavaScript 权威指南》) 中的详细介绍。
当代码在一个环境中执行时,会创建变量对象的的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是一个函数,则将其活动对象作为变量对象。参考引文:{aa26aa},{aa25aa}
- num = "one";
- var a = 1;
- function t() { //t函数的局部作用域,可以访问到a,b变量,但是访问不到c变量
- var num = "two";
- var b = 2;
- function A() { //A函数局部作用域,可以访问到a,b,c变量
- var num = "three"; //局部变量与外部变量重名以局部变量为主
- var c = 3;
- console.log(num); //three
- }
- function B() { //B函数局部作用域,可以访问到a,b变量,访问不到c变量
- console.log(num); //two
- }
- A();
- B();
- }
- t();
当执行 A 时,将创建函数 A 的执行环境 (调用对象), 并将该对象置于链表开头,然后将函数 t 的调用对象链接在之后,最后是全局对象。然后从链表开头寻找变量 num。
即:A()->t()->window, 所以 num 是 "three";
但执行 B() 时,作用域链是: B()->t()->window, 所以 num 是 "two";
另外,有一个特殊的例子我觉得应该发一下。利用 "JavaScript 的作用域是词法性质的(lexically scoped)。这意味着,函数运行在定义它的作用域中,而不是在调用它的作用域中。" 这句话,解释了下面的例子。
- var x = 10;
- function a() {
- console.log(x);
- }
- function b() {
- var x = 5;
- a();
- }
- b(); //输出为10
虽然 b 函数调用了 a,但是 a 定义在全局作用域下,同样也是运行在全局作用域下的,所以其内部的变量 x,向上寻找到了全局变量 x=10;所以 b 函数的输出为 10;
更深层次的讲解请参照:{aa24aa}。
下面是一个经典的事件绑定例子:
- 栏目1
- 栏目2
- 栏目3
- 栏目4
- function bindClick(){
- var allP = document.getElementById("test").getElementsByTagName("p"),
- i=0,
- len = allP.length;
- for( ;i){
- allP[i].onclick = function(){
- alert("you click the "+i+" P tag!");//you click the 4 P tag!
- }
- }
- }
- bindClick();//运行函数,绑定点击事件
我们可以把上述的
代码给分解一下,让我们看起来更容易理解,如下所示。前面使用一个匿名函数作为
- JS
事件的回调函数,这里使用的一个非匿名函数,作为回调,完全相同的效果。
- click
- function bindClick() {
- var allP = document.getElementById("test").getElementsByTagName("p"),
- i = 0,
- len = allP.length;
- for (; i) {
- allP[i].onclick = AlertP;
- }
- function AlertP() {
- alert("you click the " + i + " P tag!");
- }
- }
- bindClick(); //运行函数,绑定点击事件
这里应该没有什么问题吧,前面使用一个匿名函数作为
事件的回调函数,这里使用的一个非匿名函数,作为回调,完全相同的效果。也可以做下测试哦。
- click
理解上面的说法了,那么就可以很简单的理解,为什么我们之前的代码,会得到一个相同的结果了。首先看一下
循环中,这里我们只是对每一个匹配的元素添加了一个
- for
的回调函数,并且回调函数都是
- click
函数。这里当为每一个元素添加成功
- AlertP
之后,
- click
的值,就变成了匹配元素的个数,也就是
- i
,而当我们触发这个事件时,也就是当我们点击相应的元素时,我们期待的是,提示出我们点击的元素是排列在第几行。当
- i=len
事件触发时,执行回调函数
- click
,但是当执行到这里的时候,发现
- AlertP
方法中,有一个变量是未知的,并且在
- alert
的局部作用域中,也没有查找到相应的变量,那么按照作用域链的查找方式,就会向父级作用域去查找,这里的父级作用域中,确实是有变量
- AlertP
的,而 i 的值,却是经过
- i
循环之后的值,
- for
。所以也就出现了我们最初看到的效果。
- i=len
解决办法如下所示:
- function bindClick(){
- var allP = document.getElementById("test").getElementsByTagName("p"),
- i=0,
- len = allP.length;
- for( ;i){
- AlertP(allP[i],i);
- }
- function AlertP(obj,i){
- obj.onclick = function(){
- alert("you click the "+i+" P tag!");
- }
- }
- }
- bindClick();
这里,
和
- obj
在
- i
函数内部,就是局部变量了。
- AlertP
事件的回调函数,虽然依旧没有变量
- click
的值,但是其父作用域
- i
的内部,却是有的,所以能正常的显示了,这里
- AlertP
我放在了
- AlertP
的内部,只是因为这样可以减少必要的全局函数,放到全局也不影响的。
- bindClick
这里是添加了一个函数进行绑定,如果我不想添加函数呢,当然也可以实现了,这里就要说到自执行函数了。可以跳到本文的 {aa23aa},也可以看参考引文的深度讲解:{aa22aa}
匿名函数:function () {}; 使用 function 关键字声明一个函数,但未给函数命名,所以叫匿名函数,匿名函数有很多作用,赋予一个变量则创建函数,赋予一个事件则成为事件处理程序或创建闭包等等。下文会讲到。
JS 中的函数定义分为两种:声明式函数与赋值式函数。
- Fn(); //执行结果:"执行了声明式函数",在预编译期声明函数及被处理了,所以即使Fn()调用函数放在声明函数前也能执行。
- function Fn(){ //声明式函数
- alert("执行了声明式函数");
- }
- Fn(); //执行结果:"Fn is not a function"
- var Fn = function(){ //赋值式函数
- alert("执行了赋值式函数");
- }
JS 的解析过程分为两个阶段:预编译期 (预处理) 与执行期。
预编译期 JS 会对本代码块中的所有声明的变量和函数进行处理(类似与 C 语言的编译),此时处理函数的只是声明式函数,而且变量也只是进行了声明 (声明提前) 但未进行初始化以及赋值。所以才会出现上面两种情况。
当正常情况,函数调用在声明之后,同名函数会覆盖前者。
- function Fn(){ //声明式函数
- alert("执行了声明式函数");
- }
- var Fn = function(){ //赋值式函数
- alert("执行了赋值式函数");
- }
- Fn();//执行结果:"执行了赋值式函数",同名函数后者会覆盖前者
同理当提前调用声明函数时,也存在同名函数覆盖的情况。
- Fn(); //执行结果:"执行了函数2",同名函数后者会覆盖前者
- function Fn(){ //函数1
- alert("执行了函数1");
- }
- function Fn(){ //函数2
- alert("执行了函数2");
- }
JavaScript 中的代码块是指由 <script> 标签分割的代码段。JS 是按照代码块来进行编译和执行的,代码块间相互独立,但变量和方法共享。如下:
- //代码块一
- var test1 = "我是代码块一test1";
- alert(str);//因为没有定义str,所以浏览器会出错,下面的不能运行
- alert("我是代码块一");//没有运行到这里
- var test2 = "我是代码块一test2";//没有运行到这里但是预编译环节声明提前了,所以有变量但是没赋值
- //代码块二
- alert("我是代码块二"); //这里有运行到
- alert(test1); //弹出"我是代码块一test1"
- alert(test2); //弹出"undefined"
上面的代码中代码块一中运行报错,但不影响代码块二的执行,这就是代码块间的独立性,而代码块二中能调用到代码一中的变量,则是块间共享性。
但是当第一个代码块报错停止后,并不影响下一个代码块运行。当然在下面的例子中,虽然代码块二中的函数声明预编译了,但是在代码块 1 中的函数出现 Fn 函数为定义错误 (浏览器报错,并不是声明未赋值的 undefined),说明代码块 1 完全执行后才执行代码块 2。
所以 js 函数解析顺序如下:
- //代码块1
- Fn(); //浏览器报错:"undefined",停止代码块1运行
- alert("执行了代码块1");//未运行
- //代码块2
- alert("执行了代码块2");//执行弹框效果
- function Fn(){ //函数1
- alert("执行了函数1");
- }
- alert("first");
- function Fn(){
- alert("third");
- }
- alert("second");
也就是在函数名后添加括号,函数就会自执行。在绑定事件时,像我这样的初学者有时会犯如下的错误,window.onclick = ab(); 这样函数 ab 一开始就会执行。正确的做法应该将 ab 后的括号去掉。而这种加括号的做法其实是把 ab 函数运行的结果赋值给点击事件。
下面两个例子清楚地反映了函数赋值后的情况。
1:
- function ab() {
- var i = 0;
- alert("ab");
- return i;
- }
- var c = ab(); //执行ab函数
- alert(typeof c + " " + c); //number 0
2:
- function ab() {
- var i = 0;
- alert("ab");
- return i;
- }
- var c = ab; //只赋值
- alert(typeof c + " " + c); //function function ab () {var i=0;alert("ab");return i;}
注:但是这个函数必须是函数表达式 (诸如上文提到的赋值式函数),不能是函数声明。详细请看:{aa18aa}
文中主要讲到匿名函数的自执行方法,即在 function 前面加!、+、 - 甚至是逗号等到都可以起到函数定义后立即执行的效果,而()、!、+、-、= 等运算符,都将函数声明转换成函数表达式,消除了 javascript 引擎识别函数表达式和函数声明的歧义,告诉 javascript 引擎这是一个函数表达式,不是函数声明,可以在后面加括号,并立即执行函数的代码 (jq 使用的就是这种方法)。举例如下所示。
- (function(a) {
- console.log(a); //firebug输出123,使用()运算符
- })(123);
- (function(a) {
- console.log(a); //firebug输出1234,使用()运算符
- } (1234));
- !
- function(a) {
- console.log(a); //firebug输出12345,使用!运算符
- } (12345);
- +
- function(a) {
- console.log(a); //firebug输出123456,使用+运算符
- } (123456);
- -
- function(a) {
- console.log(a); //firebug输出1234567,使用-运算符
- } (1234567);
- var fn = function(a) {
- console.log(a); //firebug输出12345678,使用=运算符
- } (12345678)
其作用就是:实现块作用域。
javascript 中没用私有作用域的概念,如果在多人开发的项目上,你在全局或局部作用域中声明了一些变量,可能会被其他人不小心用同名的变量给覆盖掉,根据 javascript 函数作用域链的特性,使用这种技术可以模仿一个私有作用域,用匿名函数作为一个 "容器","容器" 内部可以访问外部的变量,而外部环境不能访问 "容器" 内部的变量,所以 (function(){…} )() 内部定义的变量不会和外部的变量发生冲突,俗称" 匿名包裹器 "或" 命名空间 "。代码如下:
- function test(){
- (function (){
- for(var i=0;i<4;i++){
- }
- })();
- alert(i); //浏览器错误:i is not defined
- }
- test();
可以对比最开始介绍 {aa17aa} 时候的代码。
闭包对于初学者来说很难,需要学习很多很多才能领会,所以也是先把 {aa15aa} 和{aa14aa}的知识作为铺垫。我这里的闭包内容属于基础篇,以后可能会贴一些更为核心的内容。我这里参照了大神们的讲解来说。参考引文:{aa13aa},{aa12aa},{aa11aa}
闭包是能够读取其他函数内部变量的函数,所以在本质上,闭包将函数内部和函数外部连接起来的一座桥梁。
闭包是在函数执行结束,作用域链将函数弹出之后,函数内部的一些变量或者方法,还可以通过其他的方法引用。
两个用处:一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
为了帮助理解,我找了几个例子:
1.({aa10aa})
- function f1(){
- var n=999;
- nAdd=function(){n+=1}
- function f2(){
- alert(n);
- }
- return f2;
- }
- var result=f1();
- result(); // 999
- nAdd();
- result(); // 1000
在这段代码中,result 实际上就是闭包 f2 函数。它一共运行了两次,第一次的值是 999,第二次的值是 1000。这证明了,函数 f1 中的局部变量 n 一直保存在内存中,并没有在 f1 调用后被自动清除。
为什么会这样呢?原因就在于 f1 是 f2 的父函数,而 f2 被赋给了一个全局变量,这导致 f2 始终在内存中,而 f2 的存在依赖于 f1,因此 f1 也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是 "nAdd=function(){n+=1}" 这一行,首先在 nAdd 前面没有使用 var 关键字,因此 nAdd 是一个全局变量,而不是局部变量。其次,nAdd 的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以 nAdd 相当于是一个 setter,可以在函数外部对函数内部的局部变量进行操作。
2.(某大神)
- function foo() {
- var a = 10;
- function bar() {
- a *= 2;
- return a;
- }
- return bar;
- }
- var baz = foo();
- alert(baz()); //20
- alert(baz()); //40
- alert(baz()); //80
- var blat = foo();
- alert(blat()); //20
现在可以从外部访问 a;
a 是运行在定义它的 foo 中,而不是运行在调用 foo 的作用域中。 只要 bar 被定义在 foo 中,它就能访问 foo 中定义的变量 a,即使 foo 的执行已经结束。也就是说,按理,"var baz = foo()" 执行后,foo 已经执行结束,a 应该不存在了,但之后再调用 baz 发现,a 依然存在。这就是 JavaScript 特色之一——运行在定义,而不是运行的调用。
其中, "var baz = foo()" 是一个 bar 函数的引用;"var blat= foo()" 是另一个 bar 函数引用。
用闭包还可实现私有成员,但是我还没理解,所以就先不贴出来,想看的请参照参考引文:{aa9aa}。
结束
第一次写这么长的文章,大部分是引用,但是所有内容都是亲自实践并思考后才贴出来,作为初学者可能有解释和引用不当的地方,还请大家指出。有问题的地方还请各位老师同学多来指教探讨。
再次感谢所有引文作者,知识的增长在于传播,感谢辛苦的传播者。
参考文献:
{aa8aa},
{aa7aa},
{aa6aa},
{aa5aa},
{aa4aa},
{aa3aa},
{aa2aa},
{aa1aa},
来源: