函数可以说是任何一门编程语言的核心概念。要能熟练掌握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);



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

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

闭包

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

上面的作用域的例子中的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;

使用闭包的注意事项

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

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值。

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

(Frontend Newbie)JavaScript基础之函数的更多相关文章

  1. JavaScript基础学习-函数及作用域

    函数和作用域是JavaScript的重要组成部分,我们在使用JavaScript编写程序的过程中经常要用到这两部分内容,作为初学者,我经常有困惑,借助写此博文来巩固下之前学习的内容. (一)JavaS ...

  2. JavaScript 基础回顾——函数

    在JavaScript中,函数也是一种数据类型,属于 function 类型,所以使用Function关键字标识函数名.函数可以在大括号内编写代码并且被调用,作为其他函数的参数或者对象的属性值. 1. ...

  3. Javascript 基础--JS函数(三)

    一.基本概念:未完成某一个功能的代码(语句,指令)的集合. 二.函数的调用方式: 2.1.函数名(传递参数1,传递参数2)   基本语法 function 函数名(参数列表){ //代码; retur ...

  4. javascript基础(五)函数

    原文http://pij.robinqu.me/ 通过call和apply间接调用函数(改变this) call 和 apply带有多个参数,call和apply把当前函数的this指向第一个参数给定 ...

  5. javascript基础知识-函数

    1.javascript中函数有两种定义方式: 函数语句定义和表达式定义 //函数有定义 function test(){ console.log("This is a function&q ...

  6. JavaScript基础——创建函数

    JavaScript的最重要的一个部分是制作其他代码可以重用的代码.要做到这一点,你可以把代码组织成执行特定任务的函数.函数是结合在一个单一的块中,并给予一个名称的一系列代码语句.然后,你就可以通过引 ...

  7. JavaScript基础之函数与数组

     函数    函数的基本概念 为完成某一功能的程序指令(语句)的集合,称为函数.有的程序员把函数称为方法,希望大家不要被这两个名词搞晕了. 函数分为:自定义函数.系统函数(经常查看js帮助手册). j ...

  8. javascript基础知识--函数定义

    函数声明式 function funname( 参数 ){ ...执行的代码 } 声明式的函数并不会马上执行,需要我们调用才会执行:funname(); * 分号是用来分隔可执行JavaScript语 ...

  9. javascript基础之函数

    javascript的函数定义与python有很大的区别,的记住格式就好,下面请看代码 // 函数 // 简单定义 function func() { console.log('hello word' ...

随机推荐

  1. 【转】ANDROID自定义视图——onLayout源码 流程 思路详解

    转载(http://blog.csdn.net/a396901990) 简介: 在自定义view的时候,其实很简单,只需要知道3步骤: 1.测量——onMeasure():决定View的大小 2.布局 ...

  2. webapi put 请求405问题

    put 请求的时候  浏览器会像服务器发送两个请求 如何没做任何配置第一个options请求是会报错的 这是需要配置路由给options作响应 这时options请求就通过了,然后你们会看到你的put ...

  3. 【SQL】- 基础知识梳理(一) - 数据库

    一.引言 知识分享这个事情在公司会议上被提出过几次,可一直因各种事情耽搁下来,“我不如地狱,谁入地狱”,怀着这样一种心态,写下了 数据库系列知识分享. 本文将一步步通过循序渐进的方式带你去了解数据库. ...

  4. LOJ#10172. 「一本通 5.4 练习 1」涂抹果酱

    题目链接:https://loj.ac/problem/10172 题目描述 Tyvj 两周年庆典要到了,Sam 想为 Tyvj 做一个大蛋糕.蛋糕俯视图是一个 N×MN×MN×M 的矩形,它被划分成 ...

  5. 大数据技术之_11_HBase学习_01_HBase 简介+HBase 安装+HBase Shell 操作+HBase 数据结构+HBase 原理

    第1章 HBase 简介1.1 什么是 HBase1.2 HBase 特点1.3 HBase 架构1.3 HBase 中的角色1.3.1 HMaster1.3.2 RegionServer1.3.3 ...

  6. python测试笔试题1

    哪一个方法用来返回变量类型? 答案 type 哪一个方法用来列出一个类下的所有属性,方法,以及变量? 答案 dir 字符串方法format是用来去掉字符串的左右空格的么? 答案 不是 python 的 ...

  7. JUnit4.13环境配置

    Junit 4.13环境配置 JUnit是一个强大的单元测试工具.它可以针对某一个特定类的所有方法进行精确打击.这个东西具体怎么使用,留在以后说.这次给大家说说idea下配置JUnit环境的方法. 1 ...

  8. maven No compiler is provided environment

    eclipse maven操作正常出现的No compiler is provided in this environment. Perhaps you are running on a JRE ra ...

  9. Hibernate学习笔记(二)—— 实体规则&对象的状态&一级缓存

    一.持久化类 1.1 什么是持久化类? Hibernate是持久层的ORM映射框架,专注于数据的持久化工作.所谓的持久化,就是将内存中的数据永久存储到关系型数据库中.那么知道了什么是持久化,什么又是持 ...

  10. mysql ibdata 单独存储

    简介ibdata1 ibdata1文件是InnoDB存储引擎的共享表空间文件,存放位置my.ini 中的 datadir="D:\phpStudy\MySQL\data",目录下. ...