深入理解javascript函数参数与闭包


Posted in Javascript onDecember 12, 2016

最近在学习javascript的函数,函数是javascript的一等对象,想要学好javascript,就必须深刻理解函数。本人把学习的过程整理成文章,一是为了加深自己函数的理解,二是给读者提供学习的途径,避免走弯路。内容有些多,但都是笔者对于函数的总结。

1.函数参数

1.1:参数是什么

1.2:参数的省略

1.3:参数默认值

1.4:参数传递方式

1.5:同名参数

1.6:arguments对象

2.闭包

2.1:闭包定义

2.2:立即调用的函数表达式(IIFE, Immediately invoked function expression)

1.函数参数

 1.1:参数是什么

 在定义一个函数时,有时候需要为函数传递额外的数据,不同的外部数据会得到不同的结果,这种外部数据就叫做参数。

function keith(a){
 return a+a;
 }
 console.log(keith(3)); //6

上面代码中,给keith函数传递了参数a,并且返回了a+a表达式。

1.2:参数的省略

函数参数不是必须的,javascript规范允许省略调用时传递的实际参数。

function keith(a, b, c) {
 return a;
 }
 console.log(keith(1, 2, 3)); //1
 console.log(keith(1)); //1
 console.log(keith()); // 'undefined'

上面代码中,keith函数定义了三个参数,但是在调用时无论传递了多少个参数,javascript都不会报错。被省略的参数的默认值就变为undefined。了解函数定义与函数作用域 的都知道,函数的length属性会返回参数个数。需要注意的是,length属性与实际参数的个数无关,只是返回形式参数的个数。

(实际参数:调用时传递的参数。     形式参数:定义时传递的参数。)

但是没有办法省略只靠前的元素,而保留靠后的元素。如果一定要省略靠前的元素,只有显示传入undefined。

function keith(a, b) {
 return a;
 }
 console.log(keith(, 1)); //SyntaxError: expected expression, got ','
 console.log(keith(undefined, 2)); //'undefined'

上面代码中,如果省略了第一个参数,浏览器就会报错。如果给第一个参数传递undefined,则不会报错。

1.3:默认值

在JavaScript中,函数参数的默认值是undefined。然而,在某些情况下设置不同的默认值是有用的。一般策略是在函数的主体测试参数值是否为undefined,如果是则赋予一个值,如果不是,则返回实际参数传递的值。

function keith(a, b) {
 (typeof b !== 'undefined') ? b = b: b = 1;
 return a * b;
 }
 console.log(keith(15)); //15
 console.log(keith(15, 2)) //30

上面代码中,做了个判断。当在调用时没有传入b参数,则默认为1。

从ECMAScript 6开始,定义了默认参数(default parameters)。使用默认参数,在函数体的检查就不再需要了。

function keith(a, b = 1) {
 return a * b;
 }
 console.log(keith(15)); //15
 console.log(keith(15, 2)) //30

1.4:参数传递方式

函数参数的传递方式有两种,一个是传值传递,一个是传址传递。

当函数参数是原始数据类型时(字符串,数值,布尔值),参数的传递方式为传值传递。也就是说,在函数体内修改参数值,不会影响到函数外部。

var a = 1;
 function keith(num) {
 num = 5;
 }
 keith(a);
 console.log(a); //1

上面代码中,全局变量a是一个原始类型的值,传入函数keith的方式是传值传递。因此,在函数内部,a的值是原始值的拷贝,无论怎么修改,都不会影响到原始值。

但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的是原始值的地址,因此在函数内部修改参数,将会影响到原始值。

var arr = [2, 5];
 function keith(Arr) {
 Arr[0] = 3;
 }
 keith(arr);
 console.log(arr[0]); //3

上面代码中,传入函数keith的是参数对象arr的地址。因此,在函数内部修改arr第一个值,会影响到原始值。

注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。

var arr = [2, 3, 5];
 function keith(Arr) {
 Arr = [1, 2, 3];
 }
 keith(arr);
 console.log(arr); // [2,3,5]

上面代码中,在函数keith内部,参数对象arr被整个替换成另一个值。这时不会影响到原始值。这是因为,形式参数(Arr)与实际参数arr存在一个赋值关系。

1.5:同名参数

如果有同名参数,则取最后面出现的那个值,如果未提供最后一个参数的值,则取值变成undefined。

function keith(a, a) {
 return a;
 }
 console.log(keith(1, 3)); //3
 console.log(keith(1)); //undefined

如果想访问同名参数中的第一个参数,则使用arguments对象。

function keith(a, a) {
 return arguments[0];
 }
 console.log(keith(2));//2

1.6 arguments对象

JavaScript 中每个函数内都能访问一个特别变量 arguments。这个变量维护着所有传递到这个函数中的参数列表。

arguments 对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。

可以访问arguments对象的length属性,判断函数调用时到底带几个参数。

function keith(a, b, c) {
 console.log(arguments[0]); //1
 console.log(arguments[2]); //3
 console.log(arguments.length); //4
 }
 keith(1, 2, 3, 4);

arguments对象与数组的关系

arguments 对象不是一个数组(Array)。 尽管在语法上它有数组相关的属性 length,但它不从 Array.prototype 继承,实际上它是一个类数组对象。因此,无法对 arguments 变量使用标准的数组方法,比如 push, pop 或者 slice。但是可以使用数组中的length属性。

通常使用如下方法把arguments对象转换为数组。

var arr = Array.prototype.slice.call(arguments);

2.闭包

2.1:闭包定义

要理解闭包,需要先理解全局作用域和局部作用域的区别。函数内部可以访问全局作用域下定义的全局变量,而函数外部却无法访问到函数内部定义(局部作用域)的局部变量。

var a = 1;
function keith() {
 return a;
 var b = 2;
 }
 console.log(keith()); //1
 console.log(b); //ReferenceError: b is not defined

上面代码中,全局变量a可以在函数keith内部访问。可是局部变量b却无法在函数外部访问。

如果需要得到函数内部的局部变量,只有通过在函数的内部,再定义一个函数。

function keith(){
 var a=1;
 function rascal(){
 return a;
 }
 return rascal;
 }
 var result=keith();
 console.log(result()); //1
 function keith(){
 var a=1;
 return function(){
 return a;
 };
 }
 var result=keith();
 console.log(result()) //1

上面代码中,两种写法相同,唯一的区别是内部函数是否是匿名函数。函数rascal就在函数keith内部,这时keith内部的所有局部变量,对rascal都是可见的。但是反过来就不行,rascal内部的局部变量,对keith就是不可见的。这就是JavaScript语言特有的”链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。函数keith的返回值就是函数rascal,由于rascal可以读取keith的内部变量,所以就可以在外部获得keith的内部变量了。

闭包就是函数rascal,即能够读取其他函数内部变量的函数。由于在JavaScript语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如rascal记住了它诞生的环境keith,所以从rascal可以得到keith的内部变量。

闭包可以使得它诞生环境一直存在。看下面一个例子,闭包使得内部变量记住上一次调用时的运算结果。

function keith(num) {
 return function() {
 return num++;
 };
 }
 var result = keith(2);
 console.log(result()) //2
 console.log(result()) //3
 console.log(result()) //4

上面代码中,参数num其实就相当于函数keith内部定义的局部变量。通过闭包,num的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包result使得函数keith的内部环境,一直存在。

通过以上的例子,总结一下闭包的特点:

1:在一个函数内部定义另外一个函数,并且返回内部函数或者立即执行内部函数。

2:内部函数可以读取外部函数定义的局部变量

3:让局部变量始终保存在内存中。也就是说,闭包可以使得它诞生环境一直存在。

闭包的另一个用处,是封装对象的私有属性和私有方法。

function Keith(name) {
 var age;
 function setAge(n) {
 age = n;
 }
 function getAge() {
 return age;
 }
 return {
 name: name,
 setAge: setAge,
 getAge: getAge
 };
 }
 var person = Keith('keith');
 person.setAge(21);
 console.log(person.name); // 'keith'
 console.log(person.getAge()); //21

2.2:立即调用的函数表达式(IIFE)

通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是IIFE内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。

循环中的闭包

一个常见的错误出现在循环中使用闭包,假设我们需要在每次循环中调用循环序号

for(var i=0;i<10;i++){
 setTimeout(function(){
 console.log(i); //10
 }, 1000)
 }

上面代码中,不会符合我们的预期,输出数字0-9。而是会输出数字10十次。

当匿名函数被调用的时候,匿名函数保持着对全局变量 i 的引用,也就是说会记住i循环时执行的结果。此时for循环结束,i 的值被修改成了10。

为了得到想要的效果,避免引用错误,我们应该使用IIFE来在每次循环中创建全局变量 i 的拷贝。

for(var i = 0; i < 10; i++) {
 (function(e) {
 setTimeout(function() {
 console.log(e); //1,2,3,....,10
 }, 1000);
 })(i);
 }

外部的匿名函数会立即执行,并把 i 作为它的参数,此时函数内 e 变量就拥有了 i 的一个拷贝。当传递给 setTimeout 的匿名函数执行时,它就拥有了对 e 的引用,而这个值是不会被循环改变的。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持三水点靠木!

Javascript 相关文章推荐
用函数式编程技术编写优美的 JavaScript
Nov 25 Javascript
jQuery EasyUI API 中文文档 - NumberSpinner数值微调器使用介绍
Oct 21 Javascript
JQuery.closest(),parent(),parents()寻找父结点
Feb 17 Javascript
js实现精美的银灰色竖排折叠菜单
May 16 Javascript
基于jQuery倾斜打开侧边栏菜单特效代码
Sep 15 Javascript
JQuery日历插件My97DatePicker日期范围限制
Jan 20 Javascript
深入研究React中setState源码
Nov 17 Javascript
vue项目使用微信公众号支付总结及遇到的坑
Oct 23 Javascript
使用webpack搭建vue项目实现脚手架功能
Mar 15 Javascript
JS实现动态添加外部js、css到head标签的方法
Jun 05 Javascript
node.js 使用 net 模块模拟 websocket 握手进行数据传递操作示例
Feb 11 Javascript
vue商城中商品“筛选器”功能的实现代码
Jul 01 Javascript
smartupload实现文件上传时获取表单数据(推荐)
Dec 12 #Javascript
Javascript中call,apply,bind方法的详解与总结
Dec 12 #Javascript
微信小程序开发之圆形菜单 仿建行圆形菜单实例
Dec 12 #Javascript
深入理解javascript中concat方法
Dec 12 #Javascript
js仿微信语音播放实现思路
Dec 12 #Javascript
解析JavaScript数组方法reduce
Dec 12 #Javascript
实例分析浏览器中“JavaScript解析器”的工作原理
Dec 12 #Javascript
You might like
用php过滤危险html代码的函数
2008/07/22 PHP
php数组函数序列 之array_count_values() 统计数组中所有值出现的次数函数
2011/10/29 PHP
获取任意Html元素与body之间的偏移距离 offsetTop、offsetLeft (For:IE5+ FF1 )[
2006/12/22 Javascript
Code: write(s,d) 输出连续字符串
2007/08/19 Javascript
JavaScript基本概念初级讲解论坛贴的学习记录
2009/02/22 Javascript
两个select之间option的互相添加操作(jquery实现)
2009/11/12 Javascript
Javascript学习笔记2 函数
2010/01/11 Javascript
基于Jquery的简单图片切换效果
2011/01/06 Javascript
通过上下左右键和回车键切换光标实现代码
2013/03/08 Javascript
js中的eventType事件及其浏览器支持性介绍
2013/11/29 Javascript
Javascript动态引用CSS文件的2种方法介绍
2014/06/06 Javascript
JQuery插件iScroll实现下拉刷新,滚动翻页特效
2014/06/22 Javascript
jquery 为a标签绑定click事件示例代码
2014/06/23 Javascript
jQuery $命名冲突解决方案汇总
2014/11/13 Javascript
微信小程序  modal详解及实例代码
2016/11/09 Javascript
Bootstrap Img 图片样式(推荐)
2016/12/13 Javascript
JS实现一个简单的日历
2017/02/22 Javascript
Node.js查找当前目录下文件夹实例代码
2017/03/07 Javascript
详解Vue-cli 创建的项目如何跨域请求
2017/05/18 Javascript
Three.js中网格对象MESH的属性与方法详解
2017/09/27 Javascript
JS运动特效之链式运动分析
2018/01/24 Javascript
vue组件横向树实现代码
2018/08/02 Javascript
vue刷新页面时去闪烁提升用户体验效果的实现方法
2018/12/10 Javascript
[44:40]Serenity vs Pain 2018国际邀请赛小组赛BO2 第一场 8.19
2018/08/21 DOTA
Python实现Linux监控的方法
2019/05/16 Python
太阳镜仓库,售价20美元或更少:Sunglass Warehouse
2016/09/28 全球购物
接口中的方法可以是abstract的吗
2015/07/23 面试题
元旦晚会邀请函
2014/02/01 职场文书
护校行动方案
2014/05/31 职场文书
小学课外活动总结
2014/07/09 职场文书
维护民族团结演讲稿
2014/08/27 职场文书
在职人员跳槽求职信
2015/03/20 职场文书
幼儿园托班教育随笔
2015/08/14 职场文书
2019年干货:自我鉴定
2019/03/25 职场文书
2019初中学生入团申请书
2019/06/27 职场文书
python中Tkinter 窗口之输入框和文本框的实现
2021/04/12 Python