详解Javascript函数声明与递归调用


Posted in Javascript onOctober 22, 2016

Javascript的函数的声明方式和调用方式已经是令人厌倦的老生常谈了,但有些东西就是这样的,你来说一遍然后我再说一遍。每次看到书上或博客里写的Javascript函数有四种调用方式,我就会想起孔乙己:茴字有四种写法,你造吗?

尽管缺陷有一堆,但Javascript还是令人着迷的。Javascript众多优美的特性的核心,是作为顶级对象(first-class objects)的函数。函数就像其他普通对象一样被创建、被分配给变量、作为参数被传递、作为返回值以及持有属性和方法。函数作为顶级对象,赋予了Javascript强大的函数式编程能力,也带来了不太容易控制的灵活性。

1、函数声明

变量式声明先创建一个匿名函数,然后把它赋值给一个指定的变量:

var f = function () { // function body };

通常我们不必关心等号右边表达式的作用域是全局还是某个闭包内,因为它只能通过等号左边的变量f来引用,应该关注的是变量f的作用域。如果f指向函数的引用被破坏(f = null),且函数没有被赋值给任何其它变量或对象属性,匿名函数会因为失去所有引用而被垃圾回收机制销毁。

也可以使用函数表达式创建函数:

function f() { // function body }

与变量式不同的是,这种声明方式会为函数的一个内置属性name赋值。同时把函数赋值给当前作用域的一个同名变量。(函数的name属性,configurable、enumerable和writable均为false)

function f() { // function body } 
console.log(f.name); // "f" 
console.log(f); // f()

Javascript变量有一个的特别之处,就是会把变量的声明提前,表达式式的函数声明,也会把整个函数的定义前置,因此你可以在函数定义之前使用它:

console.log(f.name); // "f" 
console.log(f); // f() 
function f() { // function body }

函数表达式的声明会被提升到作用域顶层,试试下面的代码,它们不是本文的重点:

var a = 0; 
console.log(a); // 0 or a()? 
function a () {}

Crockford建议永远使用第一种方式声明函数,他认为第二种方式放宽了函数必须先声明后使用的要求从而会导致混乱。(Crockford是一个类似于罗素口中用来比喻维特根斯坦的"有良心的艺术家"那样的"有良心的程序员",这句话很拗口吧)

函数式声明

function f() {}

看起来是

var f = function f(){};

的简写。而

var a = function b(){};

的表达式,创建一个函数并把内置的name属性赋值为"b",然后把这个函数赋值给变量a,你可以在外部使用a()来调用它,但却不能使用b(),因为函数已被赋值给a,所以不会再自动创建一个变量b,除非你使用var b = a声明一个变量b。当然这个函数的name是"b"而不是"a"。

使用Function构造函数也可用来创建函数:

var f = new Function("a,b,c","return a+b+c;");

这种方式其实是在全局作用域内生成一个匿名函数,并把它赋值给变量f。

2、递归调用

递归被用来简化许多问题,这需要在一个函数体中调用它自己:

// 一个简单的阶乘函数 
var f = function (x) { 
  if (x === 1) { 
    return 1; 
  } else { 
    return x * f(x - 1); 
  } 
};

Javascript中函数的巨大灵活性,导致在递归时使用函数名遇到困难,对于上面的变量式声明,f是一个变量,所以它的值很容易被替换:

var fn = f; 
f = function () {};

函数是个值,它被赋给fn,我们期待使用fn(5)可以计算出一个数值,但是由于函数内部依然引用的是变量f,于是它不能正常工作了。

函数式的声明看起来好些,但很可惜:

function f(x) { 
  if (x === 1) { 
    return 1; 
  } else { 
    return x * f(x - 1); 
  } 
} 
var fn = f; 
f = function () {}; // may been warning by browser 
fn(5); // NaN

看起来,一旦我们定义了一个递归函数,便须注意不要轻易改变变量的名字。

上面谈论的都是函数式调用,函数还有其它调用方式,比如当作对象方法调用。

我们常常这样声明对象:

var obj1 = { 
  num : 5, 
  fac : function (x) { 
    // function body 
  } 
};

声明一个匿名函数并把它赋值给对象的属性(fac)。

如果我们想要在这里写一个递归,就要引用属性本身:

var obj1 = { 
  num : 5, 
  fac : function (x) { 
    if (x === 1) { 
      return 1; 
    } else { 
      return x * obj1.fac(x - 1); 
    } 
  } 
};

当然,它也会遭遇和函数调用方式一样的问题:

var obj2 = {fac: obj1.fac}; 
obj1 = {}; 
obj2.fac(5); // Sadness

方法被赋值给obj2的fac属性后,内部依然要引用obj1.fac,于是…失败了。

换一种方式会有所改进:

var obj1 = { 
   num : 5, 
   fac : function (x) { 
    if (x === 1) { 
      return 1; 
    } else { 
      return x * this.fac(x - 1); 
    } 
  } 
}; 
var obj2 = {fac: obj1.fac}; 
obj1 = {}; 
obj2.fac(5); // ok

通过this关键字获取函数执行时的context中的属性,这样执行obj2.fac时,函数内部便会引用obj2的fac属性。

可是函数还可以被任意修改context来调用,那就是万能的call和apply:

obj3 = {}; 
obj1.fac.call(obj3, 5); // dead again

于是递归函数又不能正常工作了。

我们应该试着解决这种问题,还记得前面提到的一种函数声明的方式吗?

var a = function b(){};

这种声明方式叫做内联函数(inline function),虽然在函数外没有声明变量b,但是在函数内部,是可以使用b()来调用自己的,于是

var fn = function f(x) { 
  // try if you write "var f = 0;" here 
  if (x === 1) { 
    return 1; 
  } else { 
    return x * f(x - 1); 
  } 
}; 
var fn2 = fn; 
fn = null; 
fn2(5); // OK
// here show the difference between "var f = function f() {}" and "function f() {}" 
var f = function f(x) { 
  if (x === 1) { 
    return 1; 
  } else { 
    return x * f(x - 1); 
  } 
}; 
var fn2 = f; 
f = null; 
fn2(5); // OK
var obj1 = { 
  num : 5, 
  fac : function f(x) { 
    if (x === 1) { 
      return 1; 
    } else { 
      return x * f(x - 1); 
    } 
  } 
}; 
var obj2 = {fac: obj1.fac}; 
obj1 = {}; 
obj2.fac(5); // ok 
 
var obj3 = {}; 
obj1.fac.call(obj3, 5); // ok

就这样,我们有了一个可以在内部使用的名字,而不用担心递归函数被赋值给谁以及以何种方式被调用。

Javascript函数内部的arguments对象,有一个callee属性,指向的是函数本身。因此也可以使用arguments.callee在内部调用函数:

function f(x) { 
  if (x === 1) { 
    return 1; 
  } else { 
    return x * arguments.callee(x - 1); 
  } 
}

但arguments.callee是一个已经准备被弃用的属性,很可能会在未来的ECMAscript版本中消失,在ECMAscript 5中"use strict"时,不能使用arguments.callee。

最后一个建议是:如果要声明一个递归函数,请慎用new Function这种方式,Function构造函数创建的函数在每次被调用时,都会重新编译出一个函数,递归调用会引发性能问题——你会发现你的内存很快就被耗光了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
两个select之间option的互相添加操作(jquery实现)
Nov 12 Javascript
网易JS面试题与Javascript词法作用域说明
Nov 09 Javascript
javascript 文本框水印/占位符(watermark/placeholder)实现方法
Jan 15 Javascript
浅谈JS闭包中的循环绑定处理程序
Nov 09 Javascript
JavaScript中setMonth()方法的使用详解
Jun 11 Javascript
jquery实现用户信息修改验证输入方法汇总
Jul 18 Javascript
学习Javascript闭包(Closure)知识
Aug 07 Javascript
jQuery插件echarts实现的多折线图效果示例【附demo源码下载】
Mar 04 Javascript
微信小程序 登录的简单实现
Apr 19 Javascript
详谈js对url进行编码和解码(三种方式的区别)
Aug 16 Javascript
jQuery利用FormData上传文件实现批量上传
Dec 04 jQuery
微信小程序时间控件picker view使用详解
Dec 28 Javascript
js中利用cookie实现记住密码功能
Aug 20 #Javascript
JavaScript实现页面无操作倒计时退出
Oct 22 #Javascript
微信开发 消息推送实现代码
Oct 21 #Javascript
微信和qq时间格式模板实例详解
Oct 21 #Javascript
微信开发 微信授权详解
Oct 21 #Javascript
微信公众号-获取用户信息(网页授权获取)实现步骤
Oct 21 #Javascript
微信 java 实现js-sdk 图片上传下载完整流程
Oct 21 #Javascript
You might like
模拟OICQ的实现思路和核心程序(三)
2006/10/09 PHP
PHP网上调查系统
2006/10/09 PHP
PHP 处理图片的类实现代码
2009/10/23 PHP
PHP文件与目录操作示例
2016/12/24 PHP
Laravel中log无法写入问题的解决
2017/06/17 PHP
javascript flash下fromCharCode和charCodeAt方法使用说明
2008/01/12 Javascript
微博@符号的用户名提示效果。(想@到谁?)
2010/11/05 Javascript
JavaScript中获取鼠标位置相关属性总结
2014/10/11 Javascript
JavaScript中的值类型详细介绍
2014/12/29 Javascript
JavaScript函数参数使用带参数名的方式赋值传入的方法
2015/03/19 Javascript
JavaScript中数据结构与算法(二):队列
2015/06/19 Javascript
简单实现异步编程promise模式
2015/07/31 Javascript
js实现微信分享代码
2020/10/11 Javascript
JavaScript判断变量是否为数组的方法(Array)
2016/02/24 Javascript
JS把内容动态插入到DIV的实现方法
2016/07/19 Javascript
RequireJS简易绘图程序开发
2016/10/28 Javascript
react性能优化达到最大化的方法 immutable.js使用的必要性
2017/03/09 Javascript
基于Vue2.0的分页组件
2017/03/16 Javascript
详解vue2.0脚手架的webpack 配置文件分析
2017/05/27 Javascript
jquery.validate.js 多个相同name的处理方式
2017/07/10 jQuery
利用Javascript获取选择文本所在的句子详解
2017/12/03 Javascript
基于vue中对鼠标划过事件的处理方式详解
2018/08/22 Javascript
微信小程序把百度地图坐标转换成腾讯地图坐标过程详解
2019/07/10 Javascript
微信小程序之下拉列表实现方法解析(附完整源码)
2019/08/23 Javascript
[49:08]FNATIC vs Infamous 2019国际邀请赛小组赛 BO2 第二场 8.16
2019/08/18 DOTA
[01:03:41]DOTA2-DPC中国联赛 正赛 Dynasty vs XG BO3 第三场 2月2日
2021/03/11 DOTA
django连接mysql配置方法总结(推荐)
2018/08/18 Python
对python中list的拷贝与numpy的array的拷贝详解
2019/01/29 Python
美国汽车性能部件和赛车零件网站:Vivid Racing
2018/03/27 全球购物
关于.NET, HTML的五个问题
2012/08/29 面试题
某/etc/fstab文件中的某行如下: /dev/had5 /mnt/dosdata msdos defaults,usrquota 1 2 请解释其含义
2013/09/18 面试题
优秀少先队大队辅导员事迹材料
2014/05/04 职场文书
2015年感恩节活动总结
2015/03/24 职场文书
2015年材料员工作总结
2015/04/30 职场文书
老干部局2015年度工作总结
2015/10/22 职场文书
nginx location中多个if里面proxy_pass的方法
2021/03/31 Servers