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); //216    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。这里涉及到函数另外一个重要的知识点,即在一个函数内部定义另外一个函数,也就是闭包的概念。下次有机会会分享。

  总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。

 

  完。

  感谢大家的阅读。

时间: 2024-07-31 06:28:05

javascript--函数定义与函数作用域--详解的相关文章

(转)c++模板函数声明定义分离编译错误详解

当我们声明和定义一个模板的时候,必须要让声明和定义放在一个文件里.否则编译器会报错. 这就是为什么boost的实现文件的后缀名是hpp了. 这其中的理由是什么呢?为什么会这样? 首先,一个编译单元(translation unit)是指一个.cpp文件以及它所#include的所有.h文件,.h文件里的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个.obj文件(假定我们的平台是win32),后者拥有PE(Portable Executable,即windows可执行文

(转载)--SG函数和SG定理【详解】

在介绍SG函数和SG定理之前我们先介绍介绍必胜点与必败点吧. 必胜点和必败点的概念: P点:必败点,换而言之,就是谁处于此位置,则在双方操作正确的情况下必败. N点:必胜点,处于此情况下,双方操作均正确的情况下必胜. 必胜点和必败点的性质: 1.所有终结点是 必败点 P .(我们以此为基本前提进行推理,换句话说,我们以此为假设) 2.从任何必胜点N 操作,至少有一种方式可以进入必败点 P. 3.无论如何操作,必败点P 都只能进入 必胜点 N. 我们研究必胜点和必败点的目的时间为题进行简化,有助于

指针数组,数组指针,指针函数,函数指针,二级指针详解

先看个简单的:char *p,这定义了一个指针,指针指向的数据类型是字符型,char  *(p)定义了一个指针P: char *p[4], 为指针数组,由于[]的优先级高于*,所以p先和[]结合,p[]是一个数组,暂时把p[]看成是q,也就是char *(q),定义了一个指针q,只不过q是一个数组罢了,故定义了一个数组,数组里面的数据是char *的,所以数组里面的数据为指针类型.所以char *p[4]是四个指针,这四个指针组成了一个数组,称为指针数组,既有多个指针组成的数组. char(*p

JS中的定时函数(setTimeout,clearTimeout,setInterval,clearInterval详解 )

设置定时器,在一段时间之后执行指定的代码,setTimeout与setInterval的区别在于setTimeout函数指定的代码仅执行一次 方法一: window.setTimeout("alert('ok')",5000); 方法二: window.setTimeout(function() { alert("Ok"); }, 5000); 方法三: function showAlert() { alert("ok"); } window.s

赋值运算符函数的返回值类型详解

在c++赋值运算符函数的学习中,对于返回值类型的问题,一直非常费解,今天彻底总结一些每种不同返回值类型的结果: 1.当返回值为空时: <span style="font-size:14px;">void hasptr::operator=(const hasptr& s)</span> 这个时候如果只有一个'='(a = b)运算那就没问题,但是如果存在'='(a = b = c)的链式操作时,编译器就会报错 我们看:a = b = c: 程序会先运行

javascript中定义声明函数的四种方法

javascript中定义声明函数的四种方法 :http://blog.163.com/zzf_fly/blog/static/209589158201286104927248/ 方法一:function functionName([parameters]){functionBody}; 方法二:将一个未命名的函数function赋给一个指定变量(var):var add=function(a, b){} 方法三:使用new运算符声明函数varName=new Function([param1N

JavaScript表单序列化的方法详解

本文介绍下,在javascript中实现表单序列化的方法,通过实例加深理解,有需要的朋友参考下吧. 在JavaScript中,可以利用表单字段的type属性,连同name和value属性一起实现对表单的序列. 首先,我们来了解下在表单提交期间,浏览器是怎样将数据发送给服务器的.对表单字段的名称和值进行URL编码,使用和号(&)分割.不发送禁用的表单字段.只发送勾选的复选框和单选按钮.不发送type为"reset"和"button"的按钮.多选择框中的每个选中

HTML中javascript的&lt;script&gt;标签使用方法详解

原文地址:HTML中javascript的<script>标签使用方法详解 只要一提到把JavaScript放到网页中,就不得不涉及Web的核心语言--HTML.在当初开发javascript的时候,Netscape要解决的一个重要问题就是如何做到让JavaScript既能与HTML页面共存,又不影响那些页面在其他浏览器中的呈现效果.经过尝试.纠错和争论,最终的决定就是为Web增加统一的脚本支持.而Web诞生早期的很多做法也都保留了下来,并被正式纳入HTML规范当中. <script&g

JavaScript学习总结(十一)——Object类详解

一.Object类介绍 Object类是所有JavaScript类的基类(父类),提供了一种创建自定义对象的简单方式,不再需要程序员定义构造函数. 二.Object类主要属性 1.constructor:对象的构造函数. 2.prototype:获得类的prototype对象,static性质. 三.Object类主要方法 1.hasOwnProperty(propertyName) 判断对象是否有某个特定的属性.必须用字符串指定该属性,例如,obj.hasOwnProperty("name&q

JavaScript原生对象属性和方法详解——Array对象 转载

length 设置或返回 数组中元素的数目. 注意:设置 length 属性可改变数组的大小.如果设置的值比其当前值小,数组将被截断,其尾部的元素将丢失.如果设置的值比它的当前值大,数组将增大,新的元素被添加到数组的尾部,它们的值为 undefined.所以length不一定代表数组的元素个数. var arr = new Array(3) arr[0] = "John" arr[1] = "Andy" arr[2] = "Wendy" cons