(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);
从图中可以清晰看出,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
无论什么时候在函数中访问一个变量是,就会从作用域链中搜索具有相应名字的变量。一般来说,在函数执行完毕后,局部激活对象就会被销毁,内存中仅保留全局作用域。但是,在有闭包存在的情况下,情况又有所不同。
闭包
闭包是指有权访问另一个函数作用域中的变量的函数。广义上说,任何函数都是闭包,是将要执行的代码代码和执行这些代码的作用域构成的一个综合体。
上面的作用域的例子中的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基础之函数的更多相关文章
- JavaScript基础学习-函数及作用域
函数和作用域是JavaScript的重要组成部分,我们在使用JavaScript编写程序的过程中经常要用到这两部分内容,作为初学者,我经常有困惑,借助写此博文来巩固下之前学习的内容. (一)JavaS ...
- JavaScript 基础回顾——函数
在JavaScript中,函数也是一种数据类型,属于 function 类型,所以使用Function关键字标识函数名.函数可以在大括号内编写代码并且被调用,作为其他函数的参数或者对象的属性值. 1. ...
- Javascript 基础--JS函数(三)
一.基本概念:未完成某一个功能的代码(语句,指令)的集合. 二.函数的调用方式: 2.1.函数名(传递参数1,传递参数2) 基本语法 function 函数名(参数列表){ //代码; retur ...
- javascript基础(五)函数
原文http://pij.robinqu.me/ 通过call和apply间接调用函数(改变this) call 和 apply带有多个参数,call和apply把当前函数的this指向第一个参数给定 ...
- javascript基础知识-函数
1.javascript中函数有两种定义方式: 函数语句定义和表达式定义 //函数有定义 function test(){ console.log("This is a function&q ...
- JavaScript基础——创建函数
JavaScript的最重要的一个部分是制作其他代码可以重用的代码.要做到这一点,你可以把代码组织成执行特定任务的函数.函数是结合在一个单一的块中,并给予一个名称的一系列代码语句.然后,你就可以通过引 ...
- JavaScript基础之函数与数组
函数 函数的基本概念 为完成某一功能的程序指令(语句)的集合,称为函数.有的程序员把函数称为方法,希望大家不要被这两个名词搞晕了. 函数分为:自定义函数.系统函数(经常查看js帮助手册). j ...
- javascript基础知识--函数定义
函数声明式 function funname( 参数 ){ ...执行的代码 } 声明式的函数并不会马上执行,需要我们调用才会执行:funname(); * 分号是用来分隔可执行JavaScript语 ...
- javascript基础之函数
javascript的函数定义与python有很大的区别,的记住格式就好,下面请看代码 // 函数 // 简单定义 function func() { console.log('hello word' ...
随机推荐
- javascript鼠标双击时触发事件大全
javascript事件列表解说 事件 浏览器支持 解说 一般事件 onclick IE3.N2 鼠标点击时触发此事件 ondblclick IE4.N4 鼠标双击时触发此事件 onmousedown ...
- js定时执行函数
//方法一: //直接现定义函数 var time = window.setInterval(function(){ $('.lingdao_right').click(); },5000); //方 ...
- .NET Core Api 集成 swagger
废话不多讲 第一步 当然是要通过 NuGet 安装第三方插件 swagger 程序包管理器控制台,安装命令:Install-Package Swashbuckle.AspNetCore -Pre 第 ...
- List_insert
List_insert /* Sorting from little to large use List */ #include <stdio.h> /* printf, scanf, N ...
- windows 2003 远程桌面无法使用剪贴板共享纯文本的解决方法(亲测可用)
远程桌面无法使用剪贴板共享纯文本的解决方法========================================以下操作须在远程桌面上操作,本地机没用的!================== ...
- mybatis 学习笔记(二):mybatis SQL注入问题
mybatis 学习笔记(二):mybatis SQL注入问题 SQL 注入攻击 首先了解下概念,什么叫SQL 注入: SQL注入攻击,简称SQL攻击或注入攻击,是发生于应用程序之数据库层的安全漏洞. ...
- linux系统安全及应用——弱口令检测
Joth the Ripper,简称JR,一款密码分析工具,支持字典式的暴力破解,通过对shadow文件的口令分析,可以检测密码强度,官方网站http://www.openwall.com/john/ ...
- win10+anaconda环境下pyqt5+qt tools+eric6.18安装及汉化过程
最近需要用python编写一个小程序的界面,选择了pyqt5+eric6的配套组合,安装过程中遇到一些坑,特此记录.参考书籍是电子工业出版社的<PyQt5快速开发与实战>. 因为我使用an ...
- 使用Lazy对构造进行重构后比较
用于测试在是否使用Lazy 的情况下,服务器负载,及服务提供情况对比. 服务器环境: 在此机器上安装了1 Hyper-V ,分配走1G内存,同时在本地上安装 SQLServer , 在 ...
- (Keil) Debug & Simulation 操作
0x00 printf在MCU環境下print debug error message,利用Logic Analyzer模擬MCU register or GPIO狀態. 若是要要使用printf函數 ...