JavaScript函数与方法的那些事

说起来 ECMAScript 中什么有意思,我想那莫过于函数了——而有意思的根源,则在于函数实际上是对象。每个函数都是 Function 类型的实例,而且都与其他引用类型一样具有属性和方法。——《JavaScript高级程序设计(第3版)》(以下简称《J3》)

《浅析JavaScript的对象系统》中我们从对象的角度对JS中的函数进行过简要的描述,我们知道了函数是一种对象类型之一,函数有属性和方法;不存在所谓的全局函数,任何一个函数包括你自定义的所有函数其实都是“挂”在某个对象上的方法。。。不过,函数有意思的地方可远不止于此。下面就一起来看看这些有意思的点。

  • 函数的声明

定义一个函数有多种形式,常见的为函数声明形式和函数表达式形式。

函数声明形式和大多数其他语言差不多:

function f(v1, v2){
  //do something
}

关键字(function)、函数名(f)、参数列表((v1, v2))、函数体({//do something})。嗯差不多就这样,不过少了public之类的访问修饰符还有函数的返回类型。对于前者,JavaScript中并没有访问修饰符的概念,变量或函数的访问控制是由JavaScript的作用域链和执行环境这一机制控制的,对此可以看一下这篇文章《JavaScript的执行环境和作用域链》以加深理解。对于后者,因为:

ECMAScript 中的函数在定义时不必指定是否返回值。实际上,任何函数在任何时候都可以通过 return 语句后跟要返回的值来实现返回值。——《J3》

这就是说,既然任何ECMAScript函数都可以在任何时候返回任何值,那么你就不可能定义函数的返回类型了,因为你无法确定它返回的会是数值、字符串还是对象或者其他类型。

对于返回值,这里再补充一点,没有return具体值或者只有一句return;的函数实际上都返回undefined值。

函数表达式形式则比较有意思了:

var f = function(v1, v2){
  //do something
};

很明显,这其实只是一条赋值语句,只不过等号右边的值是一个函数而已。这个变量f保存了对函数的引用,在使用时,和通过函数声明形式定义的函数没有任何差别,一样是传入参数即可:f(3, 7)

这两种定义函数的形式虽然在使用时没什么差别,但是需要注意的是,因为后者(函数表达式)说白了就是一条赋值语句,因此在尚未执行这句语句时,你是无法调用它的,因为未定义;而前者(函数声明)则可以在这个函数的前面正常调用它,因为解析器会率先读取函数声明,存在一个“函数声明提升”的过程。

  • 参数与重载

ECMAScript 函数的参数与大多数其他语言中函数的参数有所不同。ECMAScript 函数不介意传递进来多少个参数,也不在乎传进来的参数是什么数据类型。——《J3》

参数的数据类型没有限制这点就不必说了,对于参数个数而言,如果一个函数的参数列表有2个参数,你可以按照期望传2个参数,你也可以只传1个参数,你甚至可以传0个或者2个以上的参数,而这些都不会导致解析器报错。为什么JS中的函数对参数可以如此纵容呢?原因在于:

之所以会这样,原因是 ECMAScript中的参数在内部是用一个数组来表示的。函数接收到的始终都是这个数组,而不关心数组中包含哪些参数(如果有参数的话)。如果这个数组中不包含任何元素,无所谓;如果包含多个元素,也没有问题。——《J3》

所以你明白了,我们定义的参数列表其实是形式上的,只是为了方便在函数内部操作而定义的一些命名而已。真正保存着你传入的参数的东西,就是上面所说的那个“数组”——arguments。不过,arguments其实并不是数组,而是函数内部一个特殊的对象,但是它使用起来很像数组,因为你可以使用方括号来取每一项的值——按序对应你传入的参数,例如arguments[0]就表示你传入的第一个参数。还有,你可以通过arguments.length来确定实际传了多少个参数。请看下面两段代码,其作用和效果是完全一样的:

var sum(num1, num2){
    return num1 + num2;    //通过命名参数(参数列表)执行内部操作
}
console.log(sum(1, 2));    //3
var sum(){
    return arguments[0] + arguments[1];    //通过arguments对象执行内部操作
}
console.log(sum(1, 2));    //3

所以:

这个事实说明了 ECMAScript 函数的一个重要特点:命名的参数只提供便利,但不是必需的。——《J3》

说到这里,其实有一点就可以明确了,那就是:JavaScript函数没有重载!为什么这么说?我们知道,要实现两个函数重载,除了要求这两个函数的函数名一致外,要么使得这两个函数的参数个数不同,要么使得参数类型不完全一致。而上面已经说了,JS函数的参数实际上是由一个包含零或多个值的arguments对象来表示的,并不存在什么参数个数、参数类型的差别。因此,JavaScript函数没有重载!

从JS函数参数的特点中我们可以直接否决掉函数重载,现在我们再从另一个角度来否决一次。请看下面代码:

function add(num1, num2){
    return num1 + num2;
}
function add(value){
    return value + 100;
}
console.log(add(1, 2));    //101

结果不是3而是执行了第二个函数返回了101,很明显,并没有进行重载。事实上,上面那个add()函数的代码无论如何都不会被执行,原因在于函数名其实只是对函数的一个引用,是一个指针,因此对于同一命名,后面的始终都会覆盖掉前面的。

由于函数名仅仅是指向函数的指针,因此函数名与包含对象指针的其他变量没有什么不同。——《J3》

也就是说,上面这段代码中的add先是指向第一个函数,后又指向第二个函数,相当于第一个被覆盖掉了。这种情况下,第一个add()函数可以直接整个抹除掉了,写了和没写一样,永远也访问不到。

以上分别从函数参数和引用类型两个角度判了JavaScript函数重载死刑。那就真的一点办法都没有了吗?如果我一定要实现类似重载的效果呢?办法总是有的。前面说过,arguments.length可以确定实际传入的参数个数,那我们就可以利用它来做文章了。请看下面代码:

function add(){
    if(arguments.length === 1){
        return arguments[0] + 100;
    } else if(arguments.length === 2){
        return arguments[0] + arguments[1];
    }
}
console.log(add(1, 2));    //3
console.log(add(1));    //101

这样不就挺好地模拟了重载了吗?的确,通过arguments.length来做判断,我们确实可以做出类似重载的效果。不过需要明确的一点是:这只是模拟重载,JavaScript函数没有重载!

  • 作为值的函数

因为 ECMAScript 中的函数名本身就是变量,所以函数也可以作为值来使用。也就是说,不仅可以像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。——《J3》

请看代码:

function callSomeFunction(someFunction, someArgument){
    return someFunction(someArgument);
}
function sayHello(name){
    alert('Hello ' + name);
}
callSomeFunction(sayHello, 'leo');    //'Hello leo'(注意此处的参数为函数指针,即函数名sayHello,而不是sayHello())
  • arguments与this

  • arguments
    arguments在上面探讨函数的参数与重载的时候已经大致了解过了。我们知道:
  • arguments是函数内部一个特殊的对象,存储着所有参数的值;
  • 任何一个函数都有其唯一对应的一个arguments
  • arguments本身作为一个对象,其表现却类似数组,可以像数组一样通过索引取值;
  • arguments对象的存在是JS函数没有重载的一个重要原因,但我们却可以通过arguments.length模拟函数重载;

除了这些,这里再补充一点:arguments对象还拥有一个属性callee,它是一个指针,指向拥有这个arguments对象的那个函数。有时候,我们可以巧妙地使用arguments.callee来优化代码。

  • this
    对于this,有趣的则更多了。当你在一个函数中使用this时,应该先明确几点:
  • this是JS中的一个关键字;
  • this是在函数运行时生成的一个特殊的内部对象;
  • this实际上是一个指针,指向调用该函数的那个对象;

什么叫做“调用该函数的那个对象”?请看以下代码及相关注释:

<script>
//在Global环境下调用该函数,this指向全局环境
var x = 0;
function test(){
    this.x = 1;
}
test();
console.log(x);    //1(说明this指向全局对象Global)
</script>
<script>
//函数作为某个对象的方法进行调用,this指向该对象
var x = 1;
function test(){
   console.log(this.x);
}
var obj = {};
obj.x = 0;
obj.f = test;
obj.f();    //0(说明this指向obj)
</script>
<script>
//函数作为构造函数进行调用,this指向new出的那个对象
var x = 0;
function test(){
    this.x = 1;
}
var obj = new test();
console.log(obj.x);    //1(说明this指向obj)
</script>

以上3段代码分别是this的常见使用场景。其实我们知道,前两种使用场景的本质是一致的,第一种看起来是在直接调用test()函数,实际上和第二种一样都是在调用对象方法,只不过这个对象是看不见的全局变量罢了。

结合代码稍加分析我们便能理解上面讲的“this实际上是一个指针,指向调用该函数的那个对象”这句话的含义了:

  • this的值(即它的指向)在函数调用前是无法确定的;
  • this的值可以总结为:谁调用我,我就指向谁;

这个理解只能说大体上是这么回事,但是JavaScript中的这个this说简单就这么简单,可深究起来你会发现远不止如此,这也是为什么很多JavaScript程序员对this有所困惑的原因。这里推荐一篇总结的不错的博客《彻底理解js中this的指向,不必硬背》

首先必须要说的是,this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向是那个调用它的对象(这句话有些问题,后面会解释为什么会有问题,虽然网上大部分的文章都是这样说的,虽然在很多情况下那样去理解不会出什么问题,但是实际上那样理解是不准确的,所以在你理解this的时候会有种琢磨不透的感觉)——《彻底理解js中this的指向,不必硬背》

  • 身为对象

  • 属性
    • caller
      caller是函数对象的一个属性,它是一个指针,指向调用该函数的那个函数。不要把它和callee混淆了,后者是函数内部的arguments对象的一个属性。请理解一下下面的代码:
function fn(){
          console.log(fn === arguments.callee);    //true
          console.log(fn.caller === arguments.callee.caller);    //true
}
  • length
    函数对象的length属性表示的是这个函数希望接收的参数的个数。注意是“希望接收”,而不是“实际接收”,也就是说length的值取决于函数定义时括号中的参数个数,而不是实际传入了几个参数。请看下面测试:
function fn(a, b, c){
          console.log(arguments.length);    //2(arguments.length表示实际接收的参数个数)
          console.log(fn.length);    //3(始终都会返回3,取决于定义时参数列表中参数的个数)
}
fn(1, 2);
  • prototype
    原型,是一个指针,指向该函数的原型对象。函数的prototype属性是一个需要大书特书的东西,它和整个JavaScript语言的结构、实现和特性都息息相关。这里只是明确其作为函数对象的一个属性先提一下,之后会在相关文章中作详细的探讨。
  • 方法
    • apply()
    • call()
    • bind()

这三个方法都是函数对象所特有的。
先介绍前两个,其作用都是实现在特定的作用域中调用函数。

apply()call()的异同:


先看apply()apply()方法接收两个参数,一个是在其中运行函数的作用域对象,另一个是参数数组(可以是arguments对象,也可以是一个Array数组)。请看示例:

function sum(num1, num2){
        return num1 + num2;
}
function callSum1(num1, num2){
        return sum.apply(this, num1, num2);
}
function callSum2(num1, num2){
        return sum.apply(this, [num1, num2]);
}
console.log(callSum1(1, 2));    //3
console.log(callSum2(1, 2));    //3

再来看看call()

call()方法与 apply()方法的作用相同,它们的区别仅在于接收参数的方式不同。对于 call() 方法而言,第一个参数是 this 值没有变化,变化的是其余参数都直接传递给函数。换句话说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。——《J3》

请看示例:

function sum(sum1, sum2){
        return sum1 + sum2;
}
function callSum(num1, num2){
        return sum.call(this, sum1, sum2);
}
console.log(callSum(1, 2));    //3

apply()call()这两个方法的作用是完全一样的,区别只在于两者接收参数的方式不同。

上面理清了apply()call()这两个方法的异同,可从示例代码中看不出它们的实际作用,下面就来看看它们真正的用武之地。

apply()call()的作用:


window.color = 'red';
var o = { color: 'blue' };
function sayColor(){
      console.log(this.color);
}
sayColor();    //'red'
sayColor.call(this);    //'red'
sayColor.call(o);    //'blue'

当运行sayColor.call(o)时,sayColor()的作用域被设定为o,因此返回结果为blue。你应该看出来了,apply()call()的真正作用,在于扩充函数的作用域,而

使用 call()(或 apply())来扩充作用域的大好处,就是对象不需要与方法有任何耦合关系。——《J3》

这一点是很有意义的,因为它可以让你写出更加灵活、漂亮的代码。

最后,别忘了还有一个bind()。和apply()call()在某个环境中直接调用并执行函数不同,bind()方法会返回一个函数实例,并将传入的对象绑定到该函数实例内部的this。请看测试:

window.color = 'red';
var o = { color: 'blue' };
function sayColor(){
      console.log(this.color);
}
var newSayColor = sayColor.bind(o);    //创建一个sayColor的实例并将其this值绑定到o
newSayColor();    //'blue'

函数作为对象,除了上面介绍的这三个特有的方法之外,当然还有从Object继承而来的toString()valueOf()等方法,不过这几个继承而来的方法对函数对象而言意义不大,因为它们都只是单纯地返回函数的代码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容