本文的观点是建立在《JavaScript权威指南 6th Ed》 《JavaScript高级编程 3th Ed》 《JavaScript精粹 2th Ed》之上,

笔者将尽所能将概念、原理叙述清楚,由于笔者水平有限,理解不当之处在所难免,请读者不吝交流。



目录

1 什么是函数?

2 函数就是对象!

3 函数字面量(函数表达式)

4 函数调用

4.1 方法调用模式

4.2 函数调用模式

4.3 构造器调用模式

4.4 间接调用模式

5 函数的参数与返回值

6 扩充类型的功能

7 递归函数

8 闭包

9 模块模式

10 级联(方法链)

11 记忆

12 函数拾遗

在本文中,你将学到的概念:“调用”属性匿名函数函数声明提升实参对象扩充类型功能嵌套函数方法链闭包模块模式记忆

(这些概念均来自以上提到的三本书,这里列出来的目的是给读者提供索引。)

引用《JavaScript精粹》的一句话:JavaScript设计的最出色的的就是它的函数的实现,它几乎接近完美。

什么是函数?

函数是一组语句,是JavaScript的基础模块单元,用于代码复用、信息隐藏和组合调用。

函数默认的返回值是undefined值。

函数用于指定对象的行为(作为对象的方法)。

函数就是对象!

对象是“key/value”对的集合并拥有一个连接到原型对象的“指针”。(对象字面量产生的对象连接到Object.prototype,

函数对象连接到Function.prototype)【关于原型对象的描述,我将在以后的博文中分享给大家,到时候会在此给出链接】。

每个函数在创建的时候会附加两个隐藏属性:函数的上下文(this)和实现函数行为的代码(“调用”属性JavaScript函数调用的时候,就是调用了此函数的“调用”属性,这是函数与众不同的地方,可以被调用)

由于函数是对象,所以函数可以出现在对象能出现的任何位置(保存在变量、数组、对象中),还可以作为参数传递给其他函数,也可以作为其他函数的返回值。

而且,函数也可以拥有方法。

函数字面量(函数表达式)

函数对象通过函数字面量来创建:

 // 创建一个名为 add 的变量,并用来把两个数字相加的函数赋值给它。
var add = function /*optional name*/ (a,b) {
return a + b;
} ; //注意结尾的分号

函数字面量可以出现在任何允许表达式出现的地方,也可以定义在其他函数中。

函数字面量包括四个部分:

  • 第一部分是保留字 function。
  • 第二部分是函数名,可选。主要用于函数递归(很好理解,函数得用它的名字递归调用自己吧),还能用来被调试器和开发工具来识别函数。如果没有给函数命名,则称为匿名函数。
  • 第三部分是包围在圆括号中的一组参数,多个参数用逗号隔开(称为参数表达式)。参数的名称被定义为函数的变量,不像普通变量被初始化为undefined,而是在函数调用的时候初始化为实参的值。
  • 第四部分是包围在花括号中的一组语句,是函数的主题,在函数被调用的时候执行。

tips: 函数字面量(函数表达式) 和函数声明的区别:

JavaScript解析器会率先读取函数声明,并使其在执行任何代码之前可用(函数声明提升);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正的被解释执行。

举例说明:

alert (add (2,3));    //
function add(a,b) {
return a+b;
} // 函数声明提升
//=====为了方便,笔者写在了一起,在测试的时候,可不要在一个作用域中执行哟===============
alert (add (2,3)); //error
var add = function (a,b) {
return a+b;
}; //函数字面量,注意结尾的分号哟(细节很重要)。

另外,函数不能(有些浏览器可以,但是不建议)再if while for 等代码块里声明(特指函数声明哟),但是函数字面量可以出现在以上代码块中。

最后,函数字面量如果有函数名的话,则函数名在外部不可用。

举例说明:

var add = function add1 (a,b) {
return a+b;
};
add (2,3); // 返回5
add1(3,4); //ReferenceError: add1 is not defined

函数调用

调用一个函数时,除了声明时定义的形式参数,每个函数还接收两个附加的参数:this和arguments。

this的值取决于调用的模式(不同的模式,this的初始化也不一样)。JavaScript中共有4中调用模式:

  • 方法调用模式
  • 函数调用模式
  • 构造器调用模式(构造函数调用)
  • call()和apply()间接调用模式

调用运算符是跟在任何一个产生函数值的表达式之后的一对圆括号(多么凝练的语句啊)。

圆括号内可包含零个或多个用逗号隔开的表达式。每个表达式产生一个参数值。每个参数值被赋予函数声明时定义的形式参数。

JavaScript不对函数的参数个数和参数类型进行检查。

当实参和形参个数不匹配时,不会导致运行时错误。实参如果过多,超出的参数将会被忽略。如果过少,缺失的值将会被替换为undefined。

方法调用模式

当一个函数被保存为对象的一个属性时,它就是该对象的一个方法。当一个方法被调用时,this被绑定到该对象。

可以通过点表达式或者下标表达式来调用一个方法。

// 创建myObject 对象, 它有一个value 属性和一个increment 方法
// increment 方法接收一个可选的参数,如果参数不是一个数字,则默认使用数字1 var myObject = {
value : 0,
increment : fucntion (inc) {
this.value += typeof inc ==='number' ? inc : 1;
}
}; myObject.increment('joke'); // 1 推荐使用
myObject["increment"](3); //

函数调用模式

当一个函数不是一个对象的属性时,它就是被当做一个函数来调用的:

var sum = add (5,6);    // sum 的值为11

此模式下,this值被绑定到全局对象(在大部分浏览器中该对象是window对象)

构造器调用模式

如果在一个函数前面带上new 关键字来调用, 那么背地里将会创建一个连接到该函数的prototype 成员的新对象。

同时,this 会被绑定到那个新对象上。

// 创建一个名为Person 的构造函数,它构造一个带有name 和age 的对象

var Person = function (name,age) {
this.name = name;
this.age = age;
}; // 给Person的所有实例(就是原型) 提供名为getName() 和getAge() 的方法 Person.prototype.getName = function (){
return this.name;
}; Person.prototype.getAge = function () {
return this.age;
}; // 构造一个Person 实例 ,并测试
var tony = new Person ('tony',23);
console.log('name: '+tony.getName()+' \nage: '+tony.getAge());

一个函数,如果创建的目的就是希望结合new 前缀来调用,那么它就被称为构造器函数

构造器函数按照约定(仅仅是约定哟,但是约定优于配置的思想很重要),首字母都应该大写,这样可以避免调用时丢失new或者new普通函数等错误。

间接调用模式

JavaScript是一门函数式的面向对象编程语言,函数既然是对象,那么函数可以拥有方法。

其中两个方法 call()  和 apply() 方法可以用来间接的调用函数。两个方法的第一个参数可以绑定函数的this值。

它们的语法如下:

call([thisObj[,arg1[, arg2[, [,.argN]]]]])    // thisObj 是this要绑定的对象,后面是逗号分隔开的参数
apply([thisObj,[arglist]]) //thisObj 是this要绑定的对象,后面是以列表形式的参数。

举例说明二者的用法:

// 声明一个函数
var add = function (a,b) {
return a+b;
}; // 构造一个含有两个数字的数组
var arr = [5,6]; //===通过apply和call将它们相加===
var sum0 = add.apply(null, arr);
var sum1 = add.call(null,arr[0],arr[1]); console.log(sum0);
console.log(sum1);

函数的参数与返回值

在调用函数的时候,除了隐藏的this参数之外,还有arguments参数,它是一个类似于数组的对象。

arguments 拥有一个 length 属性(该属性是可以被改变的哟。),但它没有任何数组的方法。

函数可以通过此参数访问所有传递给函数的参数。

// 定义一个sum函数,它将所有参数进行相加,并返回相加之和
var sum = function (/*可是没形参的哟*/) {
var sum = 0;
for (var i = 0; i< arguments.length; ++i) {
sum += arguments[i];
}
return sum;
}; console.log(sum(1,2,3,4,5,6,7));

看上面的例子,如果我们不对传入的参数进行类型检测,当传入的参数不都是Number类型,那么结果将不可预料。

因此,我们要改造sum函数使其能够检测到错误情况。

// 定义一个sum函数,它将所有参数进行相加,并返回相加之和
var sum = function (/*可是没形参的哟*/) {
var sum = 0;
for (var i = 0; i< arguments.length; ++i) {
if( typeof arguments[i] !== 'number' ){
throw {
name: 'TypeError',
message: 'sum needs numbers'
};
}
sum += arguments[i];
}
return sum;
}; console.log(sum(1,2,3,4,5,6,7)); try {
console.log(sum(1,2,'three','4'));
} catch (e) {
console.log(e.name+' : '+e.message); // TypeError : sum needs numbers
}

一个函数被调用时,从第一句开始执行,并在遇到关闭函数体} 时结束。

但是 return 语句可用来使函数提前返回。 一个函数总是会返回一个值。如果没有指定返回值,则返回undefined。

如果函数调用时前面加上了 new 前缀 ,且返回值不是一个对象,则返回 this (该新对象)。

扩充类型的功能

JavaScript 允许给语言的基本类型扩充功能。 通过给类型的prototype添加方法,可以扩充该类型对象的功能。

这样的方式适用于函数、数组、字符串、数字、正则表达式和布尔值。

举例说明:

// 通过给Function.prototype 添加方法来使得该方法对所有函数可用
if (! Function.prototype.hasOwnProperty('method')){ // 检测Function原型是否已存在method属性
Function.prototype.method = function (/*String*/name, /*function*/func) {
//首先应该检查参数是否合乎标准
if (typeof name !=='string' || typeof func !== 'function'){
// 抛出异常。这里不再赘述
}
//其次还要检查name是否已经存在于原型中。
if (! this.prototype[name]){
//抛出异常。不再赘述。
}
this.prototype[name] = func;
return this;
};
}

上例中比较完整的给出了如何扩充类型功能的方法,

通过给Function.prototype 增加一个method 方法,下次给对象添加方法的时候就不用再键入prototype。

首先判断method是否已经存在于原型中,然后将method方法注册到原型。在函数中先检查参数是否合法,再检查name函数已经存在于原型中。

下面就用method 方法注册一个方法到Number原型中(Number 注册method方法和Function类似)。

Number.method('integer', function () {
return Math[this < 0 ? ' ceil ' : 'floor'] (this);
});

递归函数

递归函数就是会直接或间接地调用自身的一种函数,它将一个问题分解成一组相似的子问题,每一个都用一个寻常解(明显解)去解决。

递归函数可以非常高效的操作树形结构。

使用递归函数计算一个数的阶乘(参见JS 高级程序设计 第三版 p177):

function factorial (num) {
if (num <=1){
return 1;
}else {
return num* factorial(num -1 );
}
}

虽然这个函数表面看起来没什么问题,但下面的代码却能导致它出错。

var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial (5)); // 出错!

在将factorial设置为null后,再执行anotherFactorial (5) 时,由于必须执行factorial () ,而factorial 以不再是函数,所以导致错误。

那么怎样写出健壮的递归函数呢? 

方法一: 还记得arguments对象吧,该对象有一个属性callee指向正在执行的函数的指针,因此可以用它来实现对函数的递归调用。

function factorial (num) {
if (num <=1){
return 1;
}else {
return num* arguments.callee(num -1 );
}
}

方法二(推荐):在严格模式(什么是严格模式?)下,不能通过脚本访问arguments.callee ,访问该属性会导致错误,

不过可以使用函数字面量(命名函数表达式:只不过是没有省略函数名的函数表达式。)来达到相同的结果。

var factorial = (function fact (num) { // 函数名 fact 在外部访问是undefined
if (num <=1){
return 1;
} else {
return num * fact (num -1);
}
});

闭包

在说闭包之前,简单说说JavaScript 作用域的问题,JavaScript不支持块级作用域。

if (true) {
var a = 1;
console.log(a); //
}
console.log(a); //

JavaScript 确实有函数作用域,这意味着定义在函数中的参数和变量在函数外部是不可见的,而在一个函数内部任何位置定义的变量,在该函数内部任何地方都可见。

由于JS缺少块级作用域,所以最好的做法是在函数体的顶部声明所有可能要用到的变量。

作用域的好处是内部函数可以访问定义它们的外部函数的参数和变量(除了this和arguments:将this和arguments赋值给其他变量,可以间接访问到。)

更美好的是,内部函数拥有比他外部函数更长的生命周期。

var myObj = ( function () {
var value = 0;
return {
increment: function (inc) {
value += typeof inc === 'number' ? inc : 1;
},
getValue: function () {
return value;
}
};
} ()
);

最外部的匿名函数中定义变量value,并返回拥有两个方法的对象,这些方法继续享有访问value变量的特权。该对象不能被非法修改value的值,只能通过两个方法来修改。

两个方法(函数)可以访问它被创建时所处的上下文环境,这就被称为闭包。闭包是指有权访问另一个函数作用域中的变量的函数。

理解内部函数(嵌套函数)能访问外部函数的实际变量,而无须复制是很重要的。

下面引用《JS 精粹 》 p38页的例子来说明:

// 糟糕的例子
// 构造一个函数,用错误的方式给一个数组中的节点设置事件处理程序。
// 当点击一个节点时,按照预期,应该弹出一个对话框显示节点的序号,
// 但它总是会显示节点的数目 var add_the_handlers = function (nodes) {
var i;
for (i = 0; i < nodes.length; ++i){
nodes[i].onclick = function (e) {
alert(i);
};
}
}; //每一个事件处理函数,都弹出一个对话框显示节点的数目 nodes.length

该函数的本意是想传递给每个事件处理器一个唯一的值(i),但它未能达到目的,因为事件处理器绑定了变量i本身,而不是函数在构造时的变量i的值。(理解了吗? 不理解的话看下一个例子)

// 改良后的例子

// 构造一个函数, 用正确的方式给一个数组中的节点设置事件处理程序,
// 点击一个节点,将会弹出一个对话框显示节点的序号。 var add_the_handlers = function (nodes) {
var i;
var helper = function (i) {
return function (e){
alert(i);
};
};
for(i = 0; i < nodes.length; ++ i){
nodes[i].onclick = helper(i);
}
};

避免在循环中创建函数,先在循环之外创建一个辅助函数,让这个辅助函数再返回一个绑定了当前i值的函数

模块模式

可以使用函数和闭包来构造模块。模块是一个提供接口却隐藏状态与实现的函数或对象。

通过使用函数产生模块,几乎可以完全摒弃全局变来那个的使用。

模块模式的一般形式是:

  • 一个定义了私有变量和函数的函数;
  • 利用闭包创建可以访问私有变量和函数的特权函数;
  • 最后返回这个特权函数,或者把它们保存到一个可以访问到的地方(变量,数组,或者对象中)。

使用模块模式可以摒弃全局变量的使用,促进了信息隐藏和其他优秀的设计实践。对于应用程序的封装,或者构造其他单例对象,模块模式非常有效。

模块模式可以用来产生安全对象,假定想要构造一个用来产生序列号的对象:

var serial_maker = function () {
// 返回一个用来产生唯一字符串的对象
// 唯一字符串由两部分组成:前缀+序列号
// 该对象包含一个设置前缀的方法和一个设置序列号的方法
// 还有一个得到字符串的方法
var prefix = '';
var seq = 0;
return {
setPrefix: function (pre) {
this.prefix = pre;
},
setSeq: function (seq) {
this.seq = seq;
},
gensym: function () {
return this.prefix+this.seq;
}
};
};
var seqer = serial_maker ();
seqer.setPrefix("QAZ");
seqer.setSeq(10011);
console.log(seqer.gensym());

级联(方法链

有一些方法没有返回值,如果我们让这些方法返回this而不是undefined,就可以启用级联。

在一个级联中,可以单独在一条语句中依次调用同一个对象的很多方法。

举个Ajax类库的例子。

// 某Ajax类库的级联调用
getElement ('myBoxDiv')
.move (100,200)
.width (100)
.height (200)l;

级联技术可以产生出极富表现力的接口。 // 我觉得级联在某些场合比较好用,但不要滥用。

记忆

函数可以将先前操作的结果记录在某个对象里,从而避免无所谓的重复运算,这种优化被称为记忆。JavaScript的对象和数组要实现这种优化非常方便。

比如用递归函数来计算Fibonacci 数列:

var fibonacci = (function fib (n) {
return n < 2 ? n : fib (n-1) + fib (n-2);
}); for (var i = 0; i <= 10; ++i){
console.log (fibonacci (i)+ '\n');
}

但是这样它做了很多无谓的工作,如果我们让该函数具备记忆功能,就可以显著的减少运算量。

var fibonacci = function () {
var memo = [0,1];
var fib = function (n) {
if (typeof memo[n] !== 'number'){
memo [n] = fib (n-1) + fib (n-2);
}
return memo[n];
};
return fib;
}();
for (var i = 0; i <= 10; ++i){
console.log (fibonacci (i)+ '\n');
}

函数拾遗

函数没有重载

ECMAScript 函数不能像传统意义上那样实现重载,而其他语言中,可以为一个函数编写两个定义。只要这两个定义(接受的参数的类型和数量)不同即可。

ECMAScript函数没有签名,因为其参数是由包含零个或多个值的“数组”来表示的。而没有函数签名,真正的重载是不能做到的。

模仿块级作用域

匿名函数可以模仿块级作用域,用块级作用域(通常称为私有作用域) 的匿名函数的语法如下:

(function () {
// 这里是块级作用域
})();

什么是严格模式?

ECMAScript 5 引入了严格模式概念,严格模式是为JavaScript 定义了一种不同的解析和执行模型。在严格模式下,ECMAScript 3 中一些不确定的行为将得到处理,而对某些不安全的操作也会抛出错误。

在整个脚本中启用严格模式,可以在顶部添加如下代码:

"use strict"; 

当然也可以在某一函数中启用严格模式

function doSomething () {
"use strict";
// 函数体
}
更多内容尽在这里:相关博客一览表

深入浅出JavaScript函数 v 0.5的更多相关文章

  1. 深入浅出 JavaScript 对象 v0.5

    JavaScript 没有类的概念,因此它的对象与基于类的语言中的对象有所不同.笔者主要参考<JS 高级程序设计>.<JS 权威指南>和<JS 精粹> 本文由浅入深 ...

  2. 深入浅出javascript(二)函数和this对象

    一.函数对象的属性和方法 函数是一种数据类型,也是对象,这一点很明确.因此,函数对象也可以添加属性和方法,但是这里的属性和方法是静态的,之所以这样说,就是为了区别构造函数. 示例如下: ①创建一个空的 ...

  3. javascript函数一共可分为五类: ·常规函数 ·数组函数 ·日期函数 ·数学函数 ·字符串函数

    javascript函数一共可分为五类:    ·常规函数    ·数组函数    ·日期函数    ·数学函数    ·字符串函数    1.常规函数    javascript常规函数包括以下9个 ...

  4. 第八章:Javascript函数

    函数是这样一段代码,它只定义一次,但可能被执行或调用任意次.你可能从诸如子例程(subroutine)或者过程(procedure)这些名字里对函数概念有所了解. javascript函数是参数化的: ...

  5. [转]JavaScript函数和数组总结

    转自:http://www.uml.org.cn/AJAX/201307264.asp 写的不错,对我有很多帮助,最近准备全面的学习研究一下ES5,先转载一下这篇文章. JavaScript函数 1. ...

  6. 深入浅出 JavaScript 关键词 -- this

    深入浅出 JavaScript 关键词 -- this 要说 JavaScript 这门语言最容易让人困惑的知识点,this 关键词肯定算一个.JavaScript 语言面世多年,一直在进化完善,现在 ...

  7. 专题8:javascript函数详解

    函数是一段可以反复调用的代码块.函数还能接受输入的参数,不同的参数会返回不同的值. 函数的声明 JavaScript 有三种声明函数的方法. (1)function 命令 function命令声明的代 ...

  8. JavaScript学习总结(七)——JavaScript函数(function)

    一.函数基本概念 为完成某一功能的程序指令(语句)的集合,称为函数. 二.JavaScript函数的分类 1.自定义函数(我们自己编写的函数),如:function funName(){} 2.系统函 ...

  9. JavaScript 函数大全

    javascript函数一共可分为五类: ·常规函数 ·数组函数 ·日期函数 ·数学函数 ·字符串函数 1.常规函数 javascript常规函数包括以下9个函数: (1)alert函数:显示一个警告 ...

随机推荐

  1. localStorage 便签功能实现

    之前利用localStorage写过手机便签应用,因为蛋疼的换了台三星的屌丝级手机,木 有了测试的工具,没能继续优化维护下去.而在网页上实现便签功能目前来说似乎没有太大 的意义,因为不论是 Firef ...

  2. SAML - SSO(转)

    http://baike.baidu.com/view/758527.htm?fr=aladdin SAML即安全断言标记语言,英文全称是Security Assertion Markup Langu ...

  3. centos最小安装 setuptools安装

    centos运行不了setup?那安装setuptool吧,可以节省很多系统管理的时间. #安装setuptoolyum install setuptool#可以发现执行setup后不全,再安装一个用 ...

  4. 【数位DP】Hdu 3652:B-number

    B-number Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Su ...

  5. 【扩展欧几里得】Bzoj 1407: [Noi2002]Savage

    Description Input 第1行为一个整数N(1<=N<=15),即野人的数目.第2行到第N+1每行为三个整数Ci, Pi, Li (1<=Ci,Pi<=100, 0 ...

  6. -webkit-text-size-adjust: none;该如何处理

    -webkit-text-size-adjust: none; 在中文版Chrome里面,网页CSS里所有小于12px的字体设置都无效,最终将显示12px.这样弄的本意可能 是好的,因为中文一旦小于1 ...

  7. 漫话C++0x(五)—- thread, mutex, condition_variable

    熟悉C++98的朋友,应该都知道,在C++98中没有thread, mutex, condition_variable这些与concurrency相关的特性支持,如果需要写多线程相关程序,都要借助于不 ...

  8. Java在Windows的环境配置

    JDK环境变量配置的步骤如下: 1.我的电脑-->属性-->高级-->环境变量. 2.配置用户变量: 系统变量 a.新建 JAVA_HOME C:\Program Files\Jav ...

  9. 安卓从业者应该关注:Android 6.0的运行时权限

    Android 6.0,代号棉花糖,自发布伊始,其主要的特征运行时权限就很受关注.因为这一特征不仅改善了用户对于应用的使用体验,还使得应用开发者在实践开发中需要做出改变. 没有深入了解运行时权限的开发 ...

  10. WPF之小动画一

    定义动画: 直接使用Element进行BeginAnimation DoubleAnimation animation = new DoubleAnimation(); animation.By = ...