详解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 相关文章推荐
js不是基础的基础
Dec 24 Javascript
Safari5中alert的无限循环BUG
Apr 07 Javascript
js简单实现HTML标签Select联动带跳转
Oct 23 Javascript
js实现在字符串中提取数字
Nov 05 Javascript
JS字符串截取函数实例
Dec 27 Javascript
JavaScript中的普通函数与构造函数比较
Apr 07 Javascript
JavaScript编程中window的location与history对象详解
Oct 26 Javascript
js面向对象之常见创建对象的几种方式(工厂模式、构造函数模式、原型模式)
Nov 09 Javascript
javascript类型系统——undefined和null全面了解
Jul 13 Javascript
javascript和jQuery实现网页实时聊天的ajax长轮询
Jul 20 Javascript
解析javascript图片懒加载与预加载的分析总结
Oct 27 Javascript
jQuery ajax动态生成table功能示例
Jun 14 jQuery
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
第十四节 命名空间 [14]
2006/10/09 PHP
ajax php 实现写入数据库
2009/09/02 PHP
php中批量删除Mysql中相同前缀的数据表的代码
2011/07/01 PHP
如何让CI框架支持service层
2014/10/29 PHP
图文介绍PHP添加Redis模块及连接
2015/07/28 PHP
phpmyadmin下载、安装、配置教程
2017/05/16 PHP
php7 图形用户界面GUI 开发示例
2020/02/22 PHP
Javascript实例教程(19) 使用HoTMetal(1)
2006/12/23 Javascript
很全的显示阴历(农历)日期的js代码
2009/01/01 Javascript
JavaScript 设计模式学习 Singleton
2009/07/27 Javascript
JQuery读取XML文件数据并显示的实现代码
2009/12/16 Javascript
深入解析contentWindow, contentDocument
2013/07/04 Javascript
javascript/jquery获取地址栏url参数的方法
2014/03/05 Javascript
JQuery实现动态表格点击按钮表格增加一行
2014/08/24 Javascript
jQuery中children()方法用法实例
2015/01/07 Javascript
jquery.uploadify插件在chrome浏览器频繁崩溃解决方法
2015/03/01 Javascript
JavaScript sort数组排序方法和自我实现排序方法小结
2016/06/06 Javascript
正则中的回溯定义与用法分析【JS与java实现】
2016/12/27 Javascript
jQuery快速高效制作网页交互特效
2017/02/24 Javascript
微信小程序之页面跳转和参数传递的实现
2017/09/29 Javascript
浅谈vue项目优化之页面的按需加载(vue+webpack)
2017/12/11 Javascript
JavaScript创建对象方式总结【工厂模式、构造函数模式、原型模式等】
2018/12/19 Javascript
JavaScript原型对象原理与应用分析
2018/12/27 Javascript
微信小程序 wx:for遍历循环使用实例解析
2019/09/09 Javascript
浅谈JS中几种轻松处理'this'指向方式
2019/09/16 Javascript
使用Python对SQLite数据库操作
2017/04/06 Python
tensorflow使用range_input_producer多线程读取数据实例
2020/01/20 Python
利用pipenv和pyenv管理多个相互独立的Python虚拟开发环境
2020/11/01 Python
新加坡第一的杂货零售商:NTUC FairPrice
2020/12/05 全球购物
Can a struct inherit from another struct? (结构体能继承结构体吗)
2016/09/25 面试题
学生个人的自我评价分享
2013/11/05 职场文书
体育专业大学生职业生涯规划范文:打造自己的运动帝国
2014/09/12 职场文书
本科毕业论文致谢怎么写
2015/05/14 职场文书
春风化雨观后感
2015/06/11 职场文书
vue中利用mqtt服务端实现即时通讯的步骤记录
2021/07/01 Vue.js
Redis源码阅读:Redis字符串SDS详解
2021/07/15 Redis