JavaScript函数定义与函数作用域详解
最近在学习JavaScript的函数,函数是JavaScript的一等对象,想要学好JavaScript,就必须深刻理解函数。本人把思路整理成文章,一是为了加深自己函数的理解,二是给读者提供学习的途径,避免走弯路。内容有些多,但都是笔者对于函数的总结。
1.函数的定义
1.1:函数声明
1.2:函数表达式
1.3:命名函数的函数表达式
1.4:函数的重复声明
1.5:不能在条件语句中声明函数
2.函数的部分属性和方法
2.1:name属性
2.2:length属性
2.3:toString()方法
3.函数作用域
3.1:全局作用域和局部作用域
3.2:函数内部的变量提升
3.3:函数自身的作用域
1.函数的定义
1.1:函数声明
函数就是一段可以反复调用的代码块。函数声明由三部分组成:函数名,函数参数,函数体。整体的构造是function
命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。当函数体没有使用return关键字返回函数时,函数调用时返回默认的undefined;如果有使用return语句,则返回指定内容。函数最后不用加上冒号。
function keith() {} console.log(keith()) // 'undefined' function rascal(){ return 'rascal'; } console.log(rascal()) // 'rascal'
函数声明是在预执行期执行的,也就是说函数声明是在浏览器准备解析并执行脚本代码的时候执行的。所以,当去调用一个函数声明时,可以在其前面调用并且不会报错。
1 console.log(rascal()) // 'rascal' 2 function rascal(){ 3 return 'rascal'; 4 }
其实这段代码没有报错的原因还有一个,就是与变量声明提升一样,函数名也会发生提升。函数名提升会在下面小节谈到。
1.2:函数表达式
函数表达式是把一个匿名函数赋给一个全局变量。这个匿名函数又称为函数表达式,因为赋值语句的等号右侧只能放表达式。函数表达式末尾需要加上分号,表示语句结束。
1 var keith = function() { 2 //函数体 3 };
函数表达式与函数声明不同的是,函数表达式是浏览器解析并执行到那一行才会有定义。也就是说,不能在函数定义之前调用函数。函数表达式并不像函数声明一样有函数名的提升。如果采用赋值语句定义函数并且在声明函数前调用函数,JavaScript就会报错。
1 keith(); 2 var keith = function() {}; 3 // TypeError: keith is not a function
上面的代码等同于下面的形式。
1 var keith; 2 console.log(keith()); // TypeError: keith is not a function 3 keith = function() {};
上面代码第二行,调用keith
的时候,keith
只是被声明了,还没有被赋值,等于undefined
,所以会报错。
1.3:命名函数的函数表达式
采用函数表达式声明函数时,function
命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。
1 var keith = function boy(){ 2 console.log(typeof boy); 3 }; 4 5 console.log(boy); 6 // ReferenceError: boy is not defined 7 8 keith(); 9 // function
上面代码在函数表达式中,加入了函数名boy。这个
boy
只在函数体内部可用,指代函数表达式本身,其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。
1.4:函数的重复声明
如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。
1 function keith() { 2 console.log(1); 3 } 4 keith(); //2 5 function keith() { 6 console.log(2); 7 } 8 keith(); //2
上面代码中,后一次的函数声明覆盖了前面一次。而且,由于函数名的提升,前一次声明在任何时候都是无效的。JavaScript引擎将函数名视同变量名,所以采用函数声明的方式声明函数时,整个函数会像变量声明一样,被提升到代码头部。表面上,上面代码好像在声明之前就调用了函数keith。但是实际上,由于“变量提升”,函数
keith
被提升到了代码头部,也就是在调用之前已经声明了。再看一个典型的例子。
1 if (true) { 2 function foo() { 3 return 1; 4 } 5 } else { 6 function foo() { 7 return 2; 8 } 9 } 10 11 console.log(foo()) //2
这个例子十分典型,调用foo函数之后返回的是2,而不是1。在条件语句中声明函数会在下面说到。
1.5:不能在条件语句中声明函数
参考这篇文章,原文有那么一句话(本人翻译):在条件语句中声明函数是非标准结构的特征。也就是说,在if
代码块声明了函数,按照语言规范,这是不合法的。但是,实际情况是各家浏览器往往并不报错,能够运行。
由于存在函数名的提升,所以在条件语句中声明函数,可能是无效的。
1 if (false) { 2 function f() {} 3 } 4 console.log(f()); //undefined
上面代码的原始意图是不声明函数f
,但是由于f
的提升,导致if
语句无效,所以上面的代码不会报错。要达到在条件语句中定义函数的目的,只有使用函数表达式。
1 if (false) { 2 var f = function () {}; 3 } 4 5 console.log(f()) //Uncaught TypeError: f is not a function
2.函数的部分属性和方法
2.1:name属性
name
属性返回紧跟在function
关键字之后的那个函数名。
1 function k1() {}; 2 console.log(k1.name); //'k1' 3 4 var k2 = function() {}; 5 console.log(k2.name); //'' 6 7 var k3 = function hello() {}; 8 console.log(k3.name); //'hello'
上面代码中,name属性返回function 后面紧跟着的函数名。对于k2来说,返回一个空字符串,注意:匿名函数的name属性总是为空字符串。对于k3来说,返回函数表达式的名字(真正的函数名为k3,hello这个函数名只能在函数内部使用。)
2.2:length属性
length
属性返回函数预期传入的参数个数,即函数定义之中的参数个数。返回的是个数,而不是具体参数。
1 function keith(a, b, c, d, e) {} 2 console.log(keith.length) // 5
上面代码定义了空函数keith,它的
length
属性就是定义时的参数个数。不管调用时输入了多少个参数,length
属性始终等于5。也就是说,当调用时给实参传递了6个参数,length属性会忽略掉一个。
2.3:toString()方法
函数的toString
方法返回函数的代码本身。
1 function keith(a, b, c, d, e) { 2 // 这是注释。 3 } 4 console.log(keith.toString()); 5 //function keith(a, b, c, d, e) { // 这是注释。 }
可以看到,函数内部的注释段也被返回了。
3.函数作用域
3.1:全局作用域和局部作用域
作用域(scope)指的是变量存在的范围。Javascript只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取,在全局作用域中声明的变量称为全局变量;另一种是局部作用域,变量只在函数内部存在,此时的变量被称为局部变量。
在全局作用域中声明的变量称为全局变量,也就是在函数外部声明。它可以在函数内部读取。
1 var a=1; 2 function keith(){ 3 return a; 4 } 5 console.log(keith()) //1
上面代码中,全局作用域下的函数keith可以在内部读取全局变量a。
在函数内部定义的变量,只能在内部访问,外部无法读取,称为局部变量。注意这里必须是在函数内部声明的变量。
1 function keith(){ 2 var a=1; 3 return a; 4 } 5 console.log(a) //Uncaught ReferenceError: a is not defined
在上面代码中,变量a在函数内部定义,所以是一个局部变量,外部无法访问。
函数内部定义的变量,会在该作用域下覆盖同名变量。注意以下两个代码段的区别。
1 var a = 2; 2 3 function keith() { 4 var a = 1; 5 console.log(a); 6 } 7 keith(); //1 8 9 var c = 2; 10 11 function rascal() { 12 var c = 1; 13 return c; 14 } 15 console.log(c); //2 16 console.log(rascal()); //1
上面代码中,变量a和c
同时在函数的外部和内部有定义。结果,在函数内部定义,局部变量a
覆盖了全局变量a
。
注意,对于var命令来说,局部变量只能在函数内部声明。在其他区块声明,一律都是全局变量。比如说if语句。
1 if (true) { 2 var keith=1; 3 } 4 console.log(keith); //1
从上面代码中可以看出,变量keith在条件判断区块之中声明,结果就是一个全局变量,可以在区块之外读取。但是这里如果采用ES6中let关键字,在全局作用域下是无法访问keith变量的。
3.2:函数内部的变量声明提升
与全局作用域下的变量声明提升相同,局部作用域下的局部变量在函数内部也会发生变量声明提升。var
命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。
1 function keith(a) { 2 if (a > 10) { 3 var b = a - 10; 4 } 5 } 6 7 function keith(a) { 8 var b; 9 if (a > 10) { 10 b = a - 10; 11 } 12 }
上面两个函数段是相同的。
3.3:函数本身的作用域
函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。
1 var a = 1; 2 var b = function() { 3 console.log(a); 4 }; 5 function c() { 6 var a = 2; 7 b(); 8 } 9 c(); //1 10 11 var a = 1; 12 var b = function() { 13 return a; 14 }; 15 function c() { 16 var a = 2; 17 return b(); 18 } 19 console.log(c()); //1
以上两个代码段相同。函数b是在函数c外部声明的。所以它的作用域绑定在函数外层,内部函数a不会到函数c体内取值,所以返回的是1,而不是2。
很容易犯错的一点是,如果函数a
调用函数B
,却没考虑到函数B
不会引用函数a
的内部变量。
1 var b = function() { 2 console.log(a); 3 }; 4 function c(f) { 5 var a = 1; 6 f(); 7 } 8 c(b); //Uncaught ReferenceError: a is not defined 9 10 11 var b = function() { 12 return a; 13 }; 14 function c(f) { 15 var a = 1; 16 return f(); 17 } 18 console.log(c(b)); //Uncaught ReferenceError: a is not defined
上面代码将函数B
作为参数,传入函数c
。但是,函数B
是在函数c
体外声明的,作用域绑定外层,因此找不到函数c的内部变量
a
,导致报错。
同样的,函数体内部声明的变量,作用域绑定在函数体内部。
1 function keith() { 2 var a = 1; 3 4 function rascal() { 5 console.log(a); 6 } 7 return rascal; 8 } 9 10 var a = 2; 11 var f = keith(); 12 f(); //1
上面代码中,函数keith内部声明了rascal变量。rascal作用域绑定在keith上。当我们在keith外部取出rascal执行时,变量a指向的是keith内部的a,而不是keith外部的a。这里涉及到函数另外一个重要的知识点,即在一个函数内部定义另外一个函数,也就是闭包的概念。下次有机会会分享。
总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。
完。
感谢大家的阅读。