函数以及函数作用域详解

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions_and_function_scope
Javascript 函数 函数作用域

概述

通常来说,函数就是一个可以被外部调用的(或者函数本身递归调用)的“子程序”。和程序本身一样,一个函数的函数体是由一系列的语句组成的,函数可以接受函数,也可以返回一个返回值。

在Javascript中函数也是对象,也可以像其他对象一样,添加属性和方法等。具体来讲,它们是Function对象。

用法简介

在Javascript中,每个函数其实都是一个Function对象,查看Function界面,了解Function属性和方法。

要想返回一个返回值,函数必须使用return语句指定所要返回的值(使用new关键词的构造函数除外)。如果一个函数没有return语句,则它默认返回undefined。

调用函数时,传入函数的值为函数的实参,对应位置的函数参数名为形参。如果实参是一个包含原始值(数字,字符串,布尔值)的变量,则就算函数在内部改变了形参的值,返回后该实参的值也不会变坏。如果实参是一个对象引用,则对应形参和实参会指向同一个对象,如果函数内部形参引用对象的值发生了改变,则实参指向的引用对象的值也会改变:

/*定义函数 myFunc*/
function myFunc(theObject) {
    theObject.brand = "Toyota";
}

/*
 *定义变量 mycar;
 *创建并初始化一个对象;
 *将对象的引用赋值给变量mycar;
 */
var mycar = {
    brand : "Honda",
    model : "Accord",
    year : 1998
};

window.alert(mycar.brand);  //弹出“Honda”

myFunc(mycar);

window.alert(mycar.brand);  //弹出“Toyota”

函数执行时,this不会指向正在运行的函数本身,而是指向调用该函数的对象。如果你想在函数内部获取自身的引用,只能使用函数名或者arguments.callee属性。

定义函数

定义函数一共有三种方法:

函数声明(function语句)

有一个特别的语法来声明函数:

    function name([param,[param,[...param]]]) {
        //statements 
}

name
函数名

param
函数的参数名称,一个函数最多有255个参数。

statements
函数体内执行的语句

函数表达式(function操作符)

函数表达式很函数声明有着很多相同之处,他们甚至有着相同的语法:

function[name]([param,[param,[...param]]]) {
        //statements
}

name
可以省略

param
函数的参数名称,一个函数最多有255个参数。

statements
函数体内执行的语句

Function构造函数

和其他类型一样,Function对象可以使用new操作符来创建:

[new] Function (arg1, arg2, ... argN, functionBody)

arg1, arg2, … argN
一个或多个变量名称,来作为函数的形参名.类型为字符串,值必须为一个合法的JavaScript标识符,例如Function(“x”, “y”,”alert(x+y)”),或者逗号连接的多个标识符,例如Function(“x,y”,”alert(x+y)”)

functionBody
一个字符串,包含了组成函数的函数体的一条或多条语句.
即使不使用new操作符,直接调用Function函数,效果也是一样的,仍然可以正常创建一个函数.

注意: 不推荐使用Function构造函数来创建函数.因为在这种情况下,函数体必须由一个字符串来指定.这样会阻止一些JS引擎做程序优化,还会引起一些其他问题.

arguments对象

在函数内部,你可以使用arguments对象获取到该函数的所有传入参数. 查看 arguments.

作用域和函数堆栈

调用其他函数时的作用域部分和函数部分

递归

函数能指向并引用它自己,有三种引用方法引用函数它自己:

  1. 通过函数名

  2. arguments.callee

  3. 指向该函数的变量

考虑到一下的函数定义:

var foo = function bar() {
    //statements
}

在这个函数内部,下面就是上面所说的三种:

  1. bar()

  2. arguments.callee

  3. foo()

一个函数调用它自己叫做递归函数。在某些方面,递归函数像一个循环(loop),同样的代码多次执行同时有一个控制语句(为了避免无限循环),比如下面的循环:

var x = 0;
while (x < 10){
    x++
};
console.log(x);    //输出10

能变换为迭代函数并调用该函数:

function loop(x) {
    if(x>=10){console.log(x);}
    else{loop(x+1);}
}
loop(0);    //返回10

然而,一些算法并不是简单的迭代循环。比如,获得DOM节点使用递归就更方便:

function walkTree(node) {
    if (node == null) return;
    for(var i = 0; i < node.childNodes.length; i++){
        walkTree(node.childNodes[i]);
    }
}

比较这些循环方程,每一个递归调用都会调用更多的调用。
将递归算法转换成非递归算法大多是能实现的,但是经常逻辑会变得更加复杂并且要使用更多的堆栈,事实上,递归本身就使用堆栈:函数堆栈。
这种类似堆栈的行为能在下图中看到:

function foo(i){
    if(i<0) return;
    console.log('begin: ' + i);
    foo(i - 1);
    console.log('end: ' + i);
}
foo(3);

嵌套函数和闭包

你可以将一个函数嵌套在另一个函数内部,被嵌套函数只是他外部函数的一个私有函数,这种情况我们将它叫做闭包。

闭包是一个拥有自由访问另一个函数作用域的表达式(通常是一个函数)。

当一个被嵌套函数是一个闭包时,意味着一个被嵌套函数能继承它的外部函数的参数(arguments)和变量(variables)。意味着内部函数拥有外部函数的作用域。
总而言之:

  • 内部函数能拥有访问外部函数的语句

  • 内部的函数定义了一个闭包:内部函数能使用外部函数的参数(arguments)和变量(variables),但是外部函数不能使用内部函数的参数(arguments)和变量(variables)。
    下例演示一个函数闭包:

function addSquares(a,b){
    function square(x){
        return x*x;
        }
    return square(a)+square(b);
}
console.log(addSquares(1,2));      //返回5

由于内部函数形成了一个闭包,你可以把内部函数当作返回值返回:

function outSide(x){
    function inSide(y){
        return x + y;
    }
    return inSide;
}
fnInside = outSide(1);
console.log(fnInside(2));       //返回3
console.log(outSide(2)(10));       //返回12

变量的保存

注意,x变量是如何在inside函数返回后被保存的。一个闭包必须保存它的作用域中的参数和变量。每一次调用有可能提供不同的参数,这样一个新的闭包都会在外层调用时被创建,只有当要返回的内部函数不再被访问时内存才会被清空。
这和保存来自其他对象的引用没有区别,但是后一种情况更加少见因为一个函数不会直接设置引用并且不能检查它们。

多层嵌套

函数能多层嵌套,A函数中B函数,B函数中包含C函数。在这个函数里B和C都是闭包。因此B函数能访问A函数,C函数能访问B函数。并且,C函数也能访问A函数。这样看,闭包能包含多层嵌套。它们递归的包含外层的作用域,这种情况我们称之为作用域链。
例子如下:

function A(x){
    function B(y){
        function C(z){
            console.log(x + y + z);
        }
        C(2);
    }
    B(6);
}
A(1);

此例中C能访问B中y和A中的x,这些之所以能成立是因为:

1.B为A的闭包,B能访问A的变量和参数。
2.C为B的闭包
3.因为B包含A的作用域,所以C也包含A的作用域。换言之,C包含B和A的作用域链。

然而反过来却不行,A不能访问B,B也不能访问C

命名冲突

当闭包中两个变量或者参数拥有相同的名字,这种情况叫做命名冲突。越内层的函数拥有越高的优先权,最内层的作用域拥有最高的优先权,最外层的作用域的优先权最低。作用域链的原理如此。作用域最前端是最内层的作用域,最后是最外层。考虑如下例子

function outside(){
    var x = 10;
    function inside(x){
    return x;
    }
    return inside;
}
result = outside()(20);       //返回20

上例中的命名冲突发生在return的x是返回inside里的参数x还是外层代码的x,此处的作用域链为(inside,outside,全局),然而内层的x优先级比外层的x优先级高,于是返回的是内层的x。

函数构造器vs.函数声明vs.函数表达式

比较如下:
1.用函数构造器构造函数并添加参数:

var multiply = new Function(“x”, “y”, “return x*y;”);

2.函数声明:

function multiply(x, y) {
    return x*y; 
}

3.函数表达式:

var multiply = function(x, y) {
    return x*y;
}

4.函数命名表达式

var multiply = function func_name(x, y) {
    return x*y;
}

上面的代码大约都是干的同样的事情,除了一些微妙的区别:

  • 函数名和函数指定的变量名是有区别的:

    • 函数名是不能改变的,函数指定变量名是可以改变的

    • 函数名的使用只能和函数体一起使用,如果尝试在函数体外使用函数名的话会报错error或者返回undefined。比如:

    1. y = function x(){};
      alert(x);

    另一方面,函数指定变量名(比如最后一例中的multiply)只被它自身(变量名)的作用域所约束,这个作用域一定包含函数声明的作用域。

    • 如同以上四个例子所示,函数名与函数指定变量名不同。它们彼此之间没有关联。

  • 函数声明同样会创建一个和函数名一样的变量。不像那些在函数表达式中定义的那样,在函数声明中定义的函数能在它们定义的作用域中通过它们的命名被访问:

    function x(){
    }
    console.log(x);   //返回function(){};

    接下来的例子会展示函数名和函数指定变量是如何无关的。即使函数名被指定给了其他的变量名,返回的仍然是函数名:

    function foo(){};
    console.log(foo);
    var bar = foo;
    console.log(bar);
  • 一个由“new Function()”创建的新函数是没有函数名的。在spiderMonkey的javascript引擎中,如果函数名为“anonymous”序列化的函数将会显示,比如下例中使用“console.log(new Function())”的输出:

    function anonymous(){};
    console.log(new Function());  //返回“function anonymous(){}” 
  • 不像函数表达式定义的函数调用必须在函数表达式后面,函数声明的函数调用可以在函数的前面,如下:

    foo();
    function foo(){
        alert("FOOO");
    }
  • 函数表达式中定义的函数会继承当前的作用域。就是说该函数是一个闭包。而使用Function创建的函数不会继承任何的作用域除了全局作用域(所有的函数都会继承)。

  • 函数表达式和函数声明定义的函数只会解析一次,但那些使用Function构造的却不是。就是说,通过new Function方式构建函数时内部的字符每一次都会重解析,函数本身不会重分析,于是函数表达式比“new Function(…)”这种方式要快。所以通过函数构造方式分构造函数无论何时都是应该被避免的。这应该被记下来,然而,函数表达式和函数声明嵌套在通过函数构造器构造的中且立即执行就不会重复解析多次,如下例:

var foo = (new Function("var bar=\"fooo!\";\nreturn(function(){\n\talert(\"bar\");\n})"))();
foo();

函数声明通常会很轻易的变成函数表达式,函数声明不再是函数声明的原因可能是因为它满足以下两个条件:
1.变成了表达式的一部分。
2.它不再是函数的“资源元素”,一个“资源元素”是脚本或者函数中的一段非嵌套语句:

var x = 0;                  //资源元素
if (x == 0) {               //资源元素
    x = 10;                 //不是资源元素
    function boo() {}       //不是资源元素
}
function foo(){              //资源元素
    var y = 20;              //资源元素
    function bar() {}           //资源元素
    while(y == 10){             //资源元素
        function blah(){}      //不是资源元素
        y++;                   //不是资源元素
    }
}

例如:

//函数声明
function foo(){}

//函数表达式
(function bar(){})

//函数表达式
var x = function hello(){}
if(x){
    //函数表达式
    function hello(){}
}
//函数声明
function a(){
    //函数声明
    function b(){}
    if(0) {
        //函数表达式
        function c(){}
    }
}

条件执行函数

函数可以定义在条件语句里通过普通的function语句和new Function语句。但请注意以下这种情况ECMAScript5中不再允许出现函数语句,所以这个特性在跨浏览器中并不能表现的很好,你不能再编程中完全依赖它。
在下面的代码中,这个zero函数永远都不能定义和执行。因为if(0)永远都返回false;

if(0) {
    function zero(){
        document.writeln("This is a zero.");
    }
}

如果这个条件发生改变,if(1)那么zero就会被定义。

注意:尽管这一类函数看上去就像是函数声明,但是它实际上是函数表达式。因为它被包含在其他的条件语句中。看函数表达式和函数声明有何不同.

注意:一些javascript的解析器,不包括SpiderMonkey,错误的把命名函数表达式当成了函数定义。这样会导致zero会被定义即使if返回的是false。安全的方法是将匿名函数指定给变量:

if(0) {
    var zero = function(){
        document.writeln("This is zero.");
    }
}   

函数和事件处理程序

在JavaScript中, DOM事件处理程序只能是函数(相反的,其他DOM规范的绑定语言还可以使用一个包含handleEvent方法的对象做为事件处理程序)(译者注:这句话已经过时,目前在大部分非IE[6,7,8]的浏览器中,都已经支持一个对象作为事件处理程序). 在事件被触发时,该函数会被调用,一个 event 对象会作为第一个也是唯一的一个参数传入该函数.像其他参数一样,如果不需要使用该event对象, 则对应的形参可以省略.

通常的HTML事件对象包括:window(Window对象,包括frames),document(HTMLdocument对象和elements(各种元素对象)。在HTMLDOM中,事件对象拥有时间处理程序属性,这个属性通常是小写的有on前缀的,比如:onfocus。一个更加灵活的添加事件对象的方法有DOM2级事件

注意:事件是DOM的一部分,而不是javascript的一部分。

下例会个window对象的onfocus事件绑定一个函数:

window.onfocus = function() {
    document.body.style.backgroundColor = 'blue';
}

如果函数绑定到了变量上,那么可以将事件指向该变量。如下:

var setBgColor = new Function('document.body.style.backgroundColor = "while" ');

你可以像如下的方法那样使用它们:
1.给DOM的事件属性指向变量:

document.form1.colorbutton.onclick = setBgColor;

2.HTML标签属性:

<input name="colorbutton" type="button" onclick="setBgColor()" />

上例在执行过程中的效果如下代码:

document.form1.colorbutton.onclick = function onclick(){
    setBgColor();
}

注意:怎样在HTML中调用一个onclick返回事件的属性。可以像如下这样使用:

<input ...
    onclick="alert(event.target.tagName)"

就像其他的参数参考的函数一样,事件处理程序同样是方法,在函数中返回的this对象会指向调用该方法的对象。比如:

window.onfocus

上例中在onfocus上绑定的事件处理程序的this对象就会指向window对象。
给事件绑定的有传参的事件处理程序,必须被包含在其他的函数中:

document.form1.button1.onclick = function(){
    setBgColor("some color");
}

函数的局部变量

arguments:一个”类数组”的对象,包含了传入当前函数的所有实参;
arguments.callee:指向当前函数;
arguments.caller:指向调用当前函数的函数,请使用arguments.callee.caller代替;
arguments.length:arguments对象的中元素的个数。

同步于个人博客:http://penouc.com

    原文作者:penouc
    原文地址: https://segmentfault.com/a/1190000000510474
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞