为什么把函数(function)放在本节「核心概念」的最开头写?
因为函数是JavaScript世界里第一等公民,所谓第一等公民(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数传入另一个函数,或者作为别的函数的返回值。在这个世界里到处都是函数;而函数带有一个特别重要的绝招——定义 作用域 ,在 ECMAScript 6 之前,只有函数才有这个技能。
壹.2.1.1 函数常见的四种形态
我们写的JavaScript代码几乎都离不开函数,要么是的声明形态,要么是表达式形态,要么是嵌套形态,要么是闭包(关于闭包见”壹.2.5 “)的形态。见如下代码示例。
复制 //函数的声明形态
function func (){
console .log ( "函数的声明形态" )
}
//函数的表达式形态 之一
let func0 =function (){
console .log ( "函数的表达式形态" );
}
//函数的表达式形态 之二
( function func1 () {})
//函数的嵌套形态
let func2 = function (){
console .log ( "函数的嵌套形态" );
let func3 = function (){
console .log ( "func2嵌套在func1里" )
}
func3 ();
}
// 函数的闭包形态
let func4 = function (){
var a = "func4" ;
return function (){
console .log ( "我是以闭包形态存在的函数:" + a);
}
}
//所有的函数都通过一对括号“()”调用
func ();
func0 ();
func1 ();
func2 ();
func4 ()();
没错,函数都通过一对括号()
调用,我们叫它调用括号对 。
壹.2.1.2 函数声明提升
只有声明形态的函数,才具有提升的特性。何为“提升”?所谓提升,意思就是代码的执行顺序提升排到最前面。
复制 console .log (func0); //>> func0() {return 0}
console .log (func1); //>> undefined
//函数的声明形态
function func0 () {
return 0 ;
}
//函数的表达式形态
var func1 = function () {
return 1 ;
};
上面的代码func0
函数在声明之前就可以调用console.log(func0)
被打印输出,是因为JS引擎把声明形态的函数提前处理,相当于提升了处理优先级。上面代码等同于:
复制 var func1;
//函数的声明形态
function func0 () {
return 0 ;
}
console .log (func0); //>> func0() {return 0}
console .log (func1); //>> undefined
//函数的表达式形态
func1 = function () {
return 1 ;
};
壹.2.1.3 IIFE与匿名函数、有名函数
还有一种叫 IIFE (Immediately-Invoked Function Expression,立即执行函数 )形式的函数调用方式,非常适合匿名函数 调用,特征是在关键字function左侧有一个括号“(”,右侧的闭括号“)”则有两种放置方式,一种是放在紧挨调用括号对的左侧,一种是放在紧挨调用括号对的右侧。如下代码示例,这两种写法是等效的,都可以。
复制 ( function (){
console .log ( "我是立即运行的匿名函数" );
})();
( function (){
console .log ( "我也是立即运行的匿名函数" );
}());
既然匿名函数加上IIFE让代码这么简洁,为什么还需要给函数起名——有名函数 呢?
首先,最好的理由之一当然是为了方便递归 ,递归需要函数调用自身,函数如果没有名字,就无法有效地通过一个标志符(名字)找到函数自身以便供调用。函数的名字可以通过 name
属性读取到。
复制 //函数调用自身称为递归,函数名为“func”
( function func (i){
console .log ( "函数名为" + func .name + ",第" + i + "次调用" )
if (i < 3 ){ //递归出口
func ( ++ i); //递归
}
})( 1 );
//>> 函数名为func,第1次调用
//>> 函数名为func,第2次调用
//>> 函数名为func,第3次调用
其次,匿名函数不利于调试栈跟踪,有名函数根据名字可以很快在调试的时候定位代码位置。
最后,匿名函数看起来有点那么不直观。当然使用熟悉之后会相当直观,本书会大量使用IIFE。
壹.2.1.4 匿名函数真的没办法实现递归调用自身吗?有!
每个函数都有个 arguments
属性,代表函数参数的集合,该集合又有一个方法叫 callee
,代表函数本身,这样就可以通过 arguments.callee()
调用了自身了。//函数调用自身称为递归,函数名为“func”
复制 ( function (i){
console .log ( "函数名为" + func .name + ",第" + i + "次调用" )
if (i < 3 ){ //递归出口
arguments.callee ( ++ i);
}
})( 1 );
//>> 函数名为func,第1次调用
//>> 函数名为func,第2次调用
//>> 函数名为func,第3次调用
在严格模式下,第5版 ECMAScript (ES5) 已经禁止使用 arguments.callee()。当一个函数必须调用自身的时候,不推荐使用 arguments.callee(),尽量通过函数命名然后通过该名字调用自身。
在代码任意行输入字符串"use strict"
可以开启严格模式。
壹.2.1.5 定义作用域
在ES6之前,JavaScript没有块级作用域,只有函数作用域的说法,也即函数比较像java语言里面的大括号对 “{ }”,能定义变量可见的区域——作用域。在ES6出现之后,JavaScript才有了块级作用域,通过 let
关键字实现。
关于作用域的更多内容,请翻阅下一篇 。
壹.2.1.6 箭头函数
用(参数) => { 表达式 }
这种写法声明一个函数,就叫箭头函数(也叫lamda表达式)。箭头函数是ES6带给我们的语法糖之一,主要意图是定义轻量级的内联回调函数,当然最直观的好处是可以少敲几个字符。如下代码,用function
关键字声明一个匿名函数,和用箭头=>
声明函数是等效的。
复制 ( function (i){
console .log (i);
})( 1 );
((i) => {
console .log (i);
})( 1 );
箭头函数不暴露aguments
对象,所以,如访问arguments,将会当做一个普通变量进行访问。
复制 ((a) => {
console .log (a); //>> 1
console .log (arguments. length ); //>> Uncaught ReferenceError: arguments is not defined
})( 1 );
箭头函数一个明显作用就是可以保持this
的指向,总是指向定义它时所在的上下文环境。关于this
的内容,在“壹.2.3 ”中会详述。
最后,箭头函数也没有自己的 super
或new.target
。这句话可能不太好理解,可以参考如下代码:
复制 var Foo = () => {};
var foo = new Foo (); // TypeError: Foo is not a constructor
也即箭头函数不能作为构造函数,因此无法被new
操作,也就没有new.target
。
壹.2.1.7 高阶函数
如果某个函数可以接收另一个函数作为参数,该函数就称之为高阶函数。
函数作为参数?这似乎太奇怪了。其实是因为在JavaScript里函数可以赋值给某一个变量,而变量可以作为参数传递给函数,因此函数也可以作为参数传递给函数。
高阶函数最常见的形式之一就是回调函数。
复制 function fn1 (callback){
if (callback){
callback ();
}
}
fn1 ( function (){
console .log ( "高阶函数" ); //>> 高阶函数
});
另外,高阶函数的概念源自于函数式编程,本书第陆章第3节 会重点讲到函数式编程。
壹.2.1.8 函数重载
重载是面向对象编程语言(比如Java、C#)里的特性,JavaScript语言并不支持该特性。所谓重载(overload) ,就是函数名称一样,但是随着传入的参数个数不一样,调用的逻辑或返回的结果会不一样。jQuery之父John Resig曾经提供了一个非常巧妙的思路实现重载,代码如下:
复制 (() => { //IIFE+箭头函数,把要写的代码包起来,避免影响外界,这是个好习惯
// 当函数成为对象的一个属性的时候,可以称之为该对象的方法。
/**
* @param {object} 一个对象,以便接下来给这个对象添加重载的函数(方法)
* @param {name} object被重载的函数(方法)名
* @param {fn} 被添加进object参与重载的函数逻辑
*/
function overload (object , name , fn) {
var oldMethod = object[name]; //存放旧函数,本办法灵魂所在,将多个fn串联起来
object[name] = function () {
// fn.length为fn定义时的参数个数,arguments.length为重载方法被调用时的参数个数
if ( fn . length === arguments. length ) { //若参数个数匹配上
return fn .apply ( this , arguments); //就调用指定的函数fn
} else if ( typeof oldMethod === "function" ) { //若参数个数不匹配
return oldMethod .apply ( this , arguments); //就调旧函数
//注意:当多次调用overload()时,旧函数中
//又有旧函数,层层嵌套,递归地执行if..else
//判断,直到找到参数个数匹配的fn
}
};
}
// 不传参数时
function fn0 () {
return "no param" ;
}
// 传1个参数
function fn1 (param1) {
return "1 param:" + param1;
}
// 传两个参数时,返回param1和param2都匹配的name
function fn2 (param1 , param2) {
return "2 param:" + [param1 , param2];
}
let obj = {}; //定义一个对象,以便接下来给它的方法进行重载
overload (obj , "fn" , fn0); //给obj添加第1个重载的函数
overload (obj , "fn" , fn1); //给obj添加第2个重载的函数
overload (obj , "fn" , fn2); //给obj添加第3个重载的函数
console .log ( obj .fn ()); //>> no param
console .log ( obj .fn ( 1 )); //>> 1 param:1
console .log ( obj .fn ( 1 , 2 )); //>> 2 param:1,2
})();