函数在JavaScript中是很特别的存在,之前介绍过的函数调用栈、执行上下文、变量对象、作用域和作用域链、this的确认、闭包等都和函数相关。这里介绍一些关于函数之前没提到的内容。
关于函数:在介绍函数式编程、面向对象等内容之前需要先了解一些JS中函数某些特性。函数的声明方式、表现形式、参数的特性、封装等内容。
一、函数声明方式
函数声明: 使用 function 关键字后面跟个函数名直接声明,我们大多数是用这种方式声明函数。
function fn(){ // 函数声明
// do sth ...
}
fn();
函数表达式: 使用var关键字声明一个变量作为函数名,把一个function函数体赋值给这个变量
var fn = function(){ // 函数表达式
// do sth ...
}
fn();
两种方式的区别:
除了写法不同,我们在《变量对象、活动对象》中介绍过,在一个执行上下文入栈时,上下文创建的生命周期中有三个过程:
- 创建arguments对象
- 检查通过function关键字创建的函数声明,并创建属性
- 检查通过var关键字创建的变量声明,并创建属性
这三个过程是顺序执行,函数声明的过程要在变量声明之前被确认,我们因此通过一个变量提升的例子解释了它们的差别:
console.log(fn1); // [Function: fn1]
fn1(); // fn1
console.log(fn2); // undefined
fn2(); // Uncaught TypeError: fn2 is not a function
function fn1() { // 函数声明
console.log('fn1');
}
var fn2 = function(){ // 函数表达式
console.log('fn2');
};
函数表达式声明的方式毕竟就如同声明一个变量一样,它在遇到变量提升且还未赋值就执行调用代码的时候会报错。
不过在一些场景中一定要用到函数表达式声明,例如:
// 在构造函数中定义方法
function Car(color){
this.color = color;
this.run = function(){ // 函数表达式
console.log('run');
}
}
// 给原型添加方法
Car.prototype.getColor = function() {
return this.color;
}
面向对象编程中,普通函数被当作构造函数使用时,在内部定义一个方法时,和在构造函数的prototype属性上定义一个方法时都会用函数表达式声明方式。
// 给一个对象字面量添加方法
var obj = {
value: 'sonic',
getValue: function(){ // 函数表达式
return this.value;
}
}
还有这种在对象字面量中给一个键赋值为函数体时,使用的就是函数表达式声明方式。
二、匿名函数
没有被显示进行赋值操作的函数被称作匿名函数,它大部分使用场景是在当成一个参数被传入一个函数时。
// 给一个对象字面量添加方法
var str = 'global';
function fn(func){ // 定义fn函数
func && func();
}
fn(function(){ // 调用fn函数,并传入一个匿名函数作为参数
console.log(str); // global
});
调用fn时,被当作参数的匿名函数没有显示的进行赋值操作,不能在fn函数外部调用它,但是在fn函数内部,第一个默认参数负责接收传进来的参数并赋值为func,保存在了fn变量对象的arguments对象中。
匿名函数被当作参数传入另一个函数中,通常是为了在另一个函数中被调用,此时这个匿名函数也被称作回调函数。
setTimeout 和 setInterval 中传入的第一个参数可以是匿名函数。
setTimeout(function(){
// do sth ...
},1000);
setInterval(function(){
// do sth ...
},1000);
如果像这样省略名字,直接传个函数体在第一个参数的位置,解释器并不关心这个函数有没有名字,它会被直接调用。
三、立即执行函数(Immediately Invoked Function Expression, IIFE)
表现:
之前介绍《闭包》的文章中说过关于立即执行函数的介绍。
形如这样的函数:
(function() {
// ...
})();
由两个括号组成,第一个括号内是一个匿名函数,第二个括号紧跟第一个括号,是调用函数的操作符,它负责调用第一个括号包裹起来的匿名函数。
封装:
ES5没有块级作用域,只有函数作用域。要实现块级作用域还需要借用函数生成的作用域和立即执行函数产生的闭包来模拟。
(function(window) {
var str = 'private';
function getStr(){
return str;
}
window.getStr = getStr;
})(window);
getStr(); // 'private'
通过把getStr方法挂载到window对象上将它暴露出去。这其实就是模块化的一种实现。
之前《闭包》的文章有个根据半径求圆面积的例子:
(function(window){
var R = 3;
function getCircleArea(r){
var r = r || R;
var s = r * r * Math.PI;
return s.toFixed(2);
}
window.getCircleArea = getCircleArea;
})(window);
// console.log( R ); // 报错 R is not defined
console.log( getCircleArea() ); // "28.27"
console.log( getCircleArea(4) ); // "50.27"
这就实现了一种封装,闭包里有局部变量,有对外暴露的方法。
立即调用:
立即执行函数还有个特点体现在它的立即调用上。
《call、apply、bind》中举过这样一个例:
for(var i = 0; i < 10; i++) {
(function(i){
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i);
}
由于JS的事件轮循机制和JS本身没有块级作用域的特点,导致在循环中使用setTimeout时,循环索引会直接被赋值最大索引值。利用立即执行函数的立即调用特性,可以通过传递参数缓存循环索引,达到我们想要的结果。
立即执行函数也可以这样写:
!function(){
console.log('方式1');
}();
+function(){
console.log('方式2');
}();
四、arguments对象
函数被调用,当前执行上下文入栈时,首先创建arguments对象。
(function(num1,num2){
console.log(Object.prototype.toString.call(arguments)); // [object Arguments]
})(10,20);
arguments 是个对象,它的构造函数是Arguments。
(function(num1,num2){
console.log(arguments);
})(10,20);
如果直接打印它会是个类似这样的东西:
Arguments 是一个类似数组的对象,它可以像数组一样通过下标访问元素:arguments[0],arguments[1]…
除了可访问的元素(即实际的参数),剩下的是不可枚举方法,有callee
、length
、Symbol.iterator
和 __proto__
(function(num1,num2){
for( key in arguments){
console.log(arguments[key]);
}
})(10,20);
// 10
// 20
- length 表示参数个数
- 访问
arguments[Symbol.iterator]
会输出一个原生函数体ƒ values() { [native code] }
,Symbol.iterator
是ES6标准中的迭代器,它访问的是一个独一无二的值,在arguments对象中指向的是一个values函数。关于ES6相关内容会有系列文章解读。 __proto__
指向构造函数的原型arguments.__proto__ === Object.prototype
// true。在面向对象编程中会用到__proto__
属性,关于面向对象的知识暂时也不在这里讨论。
主要说说 arguments.callee
方法:
function fn(num1,num2){
console.log( arguments.callee === fn ); // true
}
fn(10,20);
arguments.callee
指向函数自身,如果有需要使用函数递归的场景,可以用arguments.callee()
调用自身,也可以直接用函数名fn()
调用
function fn(i){
i++;
if( i > 5 ) return;
console.log(i);
arguments.callee(i); // 也可以用fn(i)调用
}
fn(0); // 顺序输出1到5
但是有一种场景一定要用arguments.callee
调用自身,就是在匿名函数体中。
setTimeout(function(i){
i++;
if( i > 5 ) return;
console.log(i);
arguments.callee(i);
},1000,0);
// 1秒后输出1到5
显然匿名函数因为没有名字,只能通过arguments.callee
引用到自己。
结语:本篇虽然介绍了很多函数相关的内容,但是不全面。或者说只是一些铺垫。之后的函数式编程、面向对象等内容还要和函数扯上很多联系。JavaScript本身的知识点很多,大部分内容需要交叉了解,深入思考,本篇内容就是个例子。