(Frontend Newbie)JavaScript基础之函数

函数可以说是任何一门编程语言的核心概念。要能熟练掌握JavaScript,对于函数及其相关概念的学习是非常重要的一步。本篇从函数的基本知识、执行环境与作用域、闭包、this关键字等方面简单介绍JavaScript中的函数的使用。

基础

我们通常通过如下两种方式定义函数:

function myFunc() {
console.log("this is myFunc");
return;
}
var myFunc = function () {
}

与其他面相对象语言不同的是,JavaScript的函数没有规定返回值,实际上,我们可以在函数中返回任何值,甚至没有返回(没有显式return语句的函数返回undefined)。

arguments

在函数中,我们经常接触arguments对象,故名思议,它表示函数的参数。实际上,arguments对象是一个类数组对象,JavaScript通过它保存函数的所有参数。这也是JavaScript函数不在乎传进来多少个参数,也不在乎传进来的参数是什么类型的原因。

看如下一个例子:

function testArgs(arg1, arg2) {
console.log(arguments.length);
} testArgs(1, 2); // 2
testArgs(1); //1
testArgs(1, 2, 3); //3

有人会问,使用arguments对象和直接使用函数声明的参数有什么区别。其实,本质上没有什么区别,函数声明的参数在函数的内部作用域中只是一个局部变量而已,它保存调用函数时传递的参数的值。

注意

JavaScript中函数的传参都是按值传递,引用类型的变量也是按值传递。

JavaScript中的函数没有重载,但是通过arguments对象,我们可以简单实现JavaScript函数的重载功能。

function doAdd() {
if(arguments.length == 1) {
alert(arguments[0] + 10);
} else if (arguments.length == 2) {
alert(arguments[0] + arguments[1]);
}
}
doAdd(10); //20
doAdd(30, 20); //50

在调用doAdd函数时,如果只传递一个参数,则将该数加10后返回结果,如果传递了两个参数,则将这两个参数相加返回结果。

函数是对象

在上一篇中,我们介绍了常用的JavaScript的数据类型,还有一种类型没有说,就是Function类型。之所以说Function类型是一种数据类型,是因为在JavaScript中,函数也是对象,是一等公民。由于函数类型在堆内存中进行实例化,函数名只是指向这个函数对象位置的指针而已,不会与某个具体的函数绑定。

以下是一种显式的调用Function构造函数的方式定义函数的例子:

var sum = new Function("num1", "num2", "return num1 + num2");

在这个例子中,sum就是新定义的函数的名字,它与一般的变量没有实质的区别,它只保存新定义的函数的地址而已。从这个角度来理解为什么JavaScript函数没有重载就好理解多了。

既然函数是对象,函数名只是一个普通变量而已,那么我们就可以像使用普通变量一样使用函数。我们可以将函数作为参数传递给另一个函数,也可以将函数作为另一个函数的返回值返回。甚至我们可以给函数添加属性,当然不推荐这样做。

doAdd.add = function (a, b) {
return a + b;
}

关于函数,还有一点需要特别注意的是,函数声明与函数表达式的区别。

什么是函数声明呢?开篇的两种定义函数的第一种就是函数声明的方式。第二种就是函数表达式的方式。

这两种方式都定义了一个函数,具体有什么区别呢?JavaScript解析器存在一个叫做函数声明提升(function declaration hoisting)的过程,在代码开始执行之前,解析器通过函数声明提升读取并将函数声明添加到执行环境中,对代码求值时,JavaScript引擎在第一遍会声明函数并将放到源代码树的顶部。所以即使声明函数的代码在调用它的代码后面,JavaScript引擎也能把函数声明提升到顶部。

执行环境与作用域

执行环境(execution context)是JavaScript中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用到它。

每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

JavaScript中的函数时通过词法来划分作用域的,而不是动态的划分作用域的。这就意味着它们在定义它们的作用域里运行,而不是在执行它们的作用域里运行。当定义了一个函数,当前的作用域链就保存起来,并且成为函数的内部状态的一部分。

当调用一个函数时,JavaScript解析器首先将作用域设置为定义函数的时候起作用的那个作用域链,接下来,它在作用域链的前端添加一个新的对象,叫做激活对象(activation object)。激活对象用一个名为arguments的属性来初始化,这个属性引用了函数的Arguments对象。函数的命名参数添加到激活对象的后面,用var语句声明的任何变量也都定义在这个对象中。因此,局部变量,函数的命名参数和Arguments对象都在函数内的作用域中。作用域链的用途是保证执行环境有权访问的变量和函数的有序访问。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直持续到全局执行环境,全局执行环境的变量对象始终是作用域链的最后一个变量对象。

标识符解析是沿着作用域链一级一级的搜索标识符的过程,搜索过程始终从作用域的前端开始,然后逐级的向后回溯。

注意,尽管当一个函数定义了的时候,作用域链就固定了,但作用域中定义的属性还没有固定。某种程度上说作用域链是活的,函数在调用的时候,可以访问任何当前绑定的作用域,并修改其中的属性。

下面的例子形象的展示的作用域链的工作机制:

function compare(value1, value2){
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
} var result = compare(5, 10);

(Frontend Newbie)JavaScript基础之函数

从图中可以清晰看出,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

无论什么时候在函数中访问一个变量是,就会从作用域链中搜索具有相应名字的变量。一般来说,在函数执行完毕后,局部激活对象就会被销毁,内存中仅保留全局作用域。但是,在有闭包存在的情况下,情况又有所不同。

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。广义上说,任何函数都是闭包,是将要执行的代码代码和执行这些代码的作用域构成的一个综合体。

上面的作用域的例子中的compare函数实际上就是一个闭包,在compare函数内部,可以访问到全局对象(window)的属性。

再看一个闭包的例子:

function createComparisonFunction(propertyName) { 

    return function(object1, object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName]; if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
};
} //创建函数
var compareNames = createComparisonFunction("name"); //调用函数
var result = compareNames({ name: "Nicholas" }, { name: "Greg" }); //解除对匿名函数的引用(以便释放内存)
compareNames = null;

(Frontend Newbie)JavaScript基础之函数

使用闭包的注意事项

闭包虽然可以通过作用域链的方式访问其他函数作用域中的变量,但是它只能取得包含函数中任何一个变量的最后一个值。

function createFunctions(){
var result = new Array(); for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
} return result;
}

上面函数执行的结果result保存了十个函数,但每个函数的返回值都是10(i的最后一个值)。要解决这个问题,我们可以通过如下的方法:

function createFunctions(){
var result = new Array(); for (var i=0; i < 10; i++){
result[i] = function(num){
return function(){
return num;
};
}(i);
} return result;
}

原理相信大家也都明白了。

在使用闭包的时候,还有一点需要注意,就是当涉及到一些dom操作时,要小心使用闭包,操作不当将导致内存泄露。

function assignHandler(){
var element = document.getElementById("someElement"); element.onclick = function(){
console.log(element.id);
};
}

以上代码创建了一个作为 element 元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对 assignHandler()的活动对象的引用,因此就会导致无法减少 element 的引用数。只要匿名函数存在,element 的引用数至少也是 1 ,因此它所占用的内存就永远不会被回收。不过,这个问题可以通过稍微改写一下代码来解决,如下所示。

function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id; element.onclick = function(){
alert(id);
}; element = null;
}

在上面的代码中,通过把 element.id 的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。但仅仅做到这一步,还是不能解决内存泄漏的问题。必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着 element。即使闭包不直接引用 element,包含函数的活动对象中也仍然会保存一个引用。因此,有必要把 element 变量设置为 null。这样就能够解除对 DOM 对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。

this关键字

在使用函数的过程中,我们经常碰到this这个对象,在没有搞明白this原理之前,我们经常对this究竟代表什么对象感到疑惑。

function testThis() {
console.log(this.name);
}
var name = "window"; var obj = {
name: "object",
func: testThis
}; testThis(); // => window
obj.func(); // => object

在上面这个例子中,由于函数名testThis只是一个指针,所以testThis和obj.func实际上指向同一个函数对象。为什么执行结果不一样呢?

其实要理解this关键字,主要记住一句话就可以了,this永远指向函数的调用者。如果函数在全局执行环境中被调用,那么this指向全局对象(window)。因此,this的取值是在运行时决定的,这点与作用域链不同。

在理解this关键字时,不要与作用域链混淆到一起,this是一个关键字,它指向函数的调用者,不在函数的激活对象中。这一点可以与arguments对比来看。

arguments对象有一个属性,arguments.callee,指向被调用的函数本身。但是,arguments是函数的活动对象的一部分。

apply() 和 call()

说到this关键字,就不得不说说apply()和call()了。

这两个函数都是函数的内部属性,都用于改变函数的调用者,即改变this的指向。

function sum(num1, num2){
return num1 + num2;
} function callSum1(num1, num2){
return sum.apply(this, arguments); // 传入arguments 对象
} function callSum2(num1, num2){
return sum.apply(this, [num1, num2]); // 传入数组
} alert(callSum1(10,10)); //20
alert(callSum2(10,10)); //20 function callSum(num1, num2){
return sum.call(this, num1, num2);
} alert(callSum(10,10)); //20

apply()和call()函数的功能相同,唯一的区别是传递参数的方式不同。apply()第一个参数是this的值,第二个参数是参数数组。call()函数的第一个参数也是this的值,但是传递给函数的参数都要直接放在call()的参数列表中。

小结

本篇主要介绍了JavaScript函数的各方面的基础知识,其中核心就是函数的执行环境与作用域链。在此基础上,介绍了闭包的概念、使用方法,以及常见的问题。最后简单说明了函数中this的使用,以及如何改变函数的this值。

其实了解原理只是第一步,关键是在开发过程中不断的运用,时刻有这样的意识,用的多了,就理解吸收了。

上一篇:IDLE提供的常用快捷键


下一篇:IIS 加载 JSON 错误 404 解决办法