JS函数有两种定义方式:声明和表达式。区分函数声明和表达式最简单的方法是看 function 关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果 function 是声明中 的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
一、函数声明:函数声明必须带有标示符(Identifier)(就是大家常说的函数名称)
function foo(){} // 声明,因为它是程序的一部分
(function(){
function bar(){} // 声明,因为它是函数体的一部分
})();
二、函数表达式
如果function foo(){}是作为赋值表达式的一部分的话,那它就是一个函数表达式。
var bar = function foo(){}; // 表达式,因为它是赋值表达式的一部分
new function bar(){}; // 表达式,因为它是new表达式
(function foo(){}); // 函数表达式:包含在分组操作符内,()是分组操作符
对于函数表达式你最熟悉的场景可能就是回调参数了,比如:
setTimeout( function() {
console.log("I waited 1 second!");
}, 1000 );
这叫作匿名函数表达式,因为 function().. 没有名称标识符。函数表达式可以是匿名的,而函数声明则不可以省略函数名——在 JavaScript 的语法中这是非法的。
匿名函数表达式书写起来简单快捷,很多库和工具也倾向鼓励使用这种风格的代码。但是它也有几个缺点需要考虑。
1. 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
2. 如果没有函数名,当函数需要引用自身时只能使用已经过期的arguments.callee引用, 比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑自身。
3. 匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。
行内函数表达式非常强大且有用——匿名和具名之间的区别并不会对这点有任何影响。给函数表达式指定一个函数名可以有效解决以上问题。始终给函数表达式命名是一个最佳实践:
setTimeout( function timeoutHandler() { // <-- 快看,有名字了!
console.log( "I waited 1 second!" );
}, 1000 );
var a = function(){return new Date()};
a //ƒ (){return new Date()}
a() //Thu Mar 15 2018 07:35:23 GMT+0800 (CST)
var b = function b(){return new Date()};
b //ƒ b(){return new Date()}
b() //Thu Mar 15 2018 07:36:28 GMT+0800 (CST)
2.2立即执行函数表达式(IIFE)
var a = 2;
(function foo() {
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
由于函数被包含在一对 ( ) 括号内部,因此成为了一个表达式,通过在末尾加上另外一个 ( ) 可以立即执行这个函数,比如 (function foo(){ .. })()
。第一个 ( ) 将函数变成表达式,第二个 ( ) 执行了这个函数。
这种模式很常见,几年前社区给它规定了一个术语:IIFE,代表立即执行函数表达式 (Immediately Invoked Function Expression)。
IIFE 的另一个非常普遍的进阶用法是把它们当作函数调用并传递参数进去。
var a = 2;
(function IIFE( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window );
console.log( a ); // 2
我们将 window 对象的引用传递进去,但将参数命名为 global,因此在代码风格上对全局对象的引用变得比引用一个没有“全局”字样的变量更加清晰。当然可以从外部作用域传递任何你需要的东西,并将变量命名为任何你觉得合适的名字。这对于改进代码风格是非常有帮助的。
这个模式的另外一个应用场景是解决 undefined 标识符的默认值被错误覆盖导致的异常(虽然不常见)。将一个参数命名为 undefined,但是在对应的位置不传入任何值,这样就可以保证在代码块中 undefined 标识符的值真的是 undefined:
undefined = true; // 给其他代码挖了一个大坑!绝对不要这样做!
(function IIFE( undefined ) {
var a;
if (a === undefined) {
console.log( "Undefined is safe here!" );
}
})();
IIFE 还有一种变化的用途是倒置代码的运行顺序,将需要运行的函数放在第二位,在 IIFE 执行之后当作参数传递进去。
这种模式在UMD(Universal Module Definition)项目中被广泛使用。尽管这种模式略显冗长,但有些人认为它更易理解。
var a = 2;
(function IIFE( def ) {
def( window );
})(function def( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
});
a();function a(){console.log("It's right");} //It's right
b();var b = function{console.log("It's wrong")} //Uncaught SyntaxError
下面我们从一些小测试开始。猜猜以下情况都会弹出什么结果?(引用:http://www.jb51.net/article/90792.htm)
题 1:
function bar() {
return 3;
}
return bar();
function bar() {
return 8;
}
}
alert(foo());
题 2:
var bar = function() {
return 3;
};
return bar();
var bar = function() {
return 8;
};
}
alert(foo());
题 3:
function foo(){
var bar = function() {
return 3;
};
return bar();
var bar = function() {
return 8;
};
}
题 4:
return bar();
var bar = function() {
return 3;
};
var bar = function() {
return 8;
};
}
alert(foo());
例如下面的代码段在 Firefox 3.6 中会抛错,因为它将 Function Declaration 解释成了 Function Statement(见上文),所以 x 没有定义。但是在 IE8、Chrome 5 和 Safari 5 中,会返回函数 x(和标准的 Function Declaration 一样)。
function foo() {
if(false) {
function x() {};
}
return x;
}
alert(foo());
四、函数声明和函数表达式的优缺点
1、函数声明是一种很宽松的定义方式,如果试图在声明前使用函数,提升确实可以修正顺序,以便函数可以正确调用。但是这种宽松不利于严谨的编码,从长远的角度来看,很有可能会促进而不是阻止意外的发生。毕竟,程序员按特定的顺序排列语句是有原因的。
2、函数表达式用处更多。函数声明只能作为“声明”孤立存在。它所能做的就是创建一个当前作用域下的对象变量。相比之下,函数表达式(根据定义)是大型结构的一部分。如果想要创建匿名函数、给 prototype(原型)添加函数或是将函数用作其它对象的 property(属性),都可以用函数表达式。每当用高阶应用,比如 curry 或 compose,创建新的函数时都是在用函数表达式。函数表达式和函数式编程分不开。例如:var sayHello = alert.curry("hello!"); 函数表达式创建的函数大多是匿名的,但是建议使用Named Function Expressions (NFEs)作为工作区,否则调试的时候很难调试。
五、函数作用域
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。这种设计方案是非常有用的,能充分利用 JavaScript 变量可以根据需要改变值类型的“动态”特性。
function foo(a) {
var b = 2;// 一些代码
function bar() {
// ...
}
// 更多的代码
var c = 3;
}
在这个代码片段中,foo(..) 的作用域中包含了标识符 a、b、c 和 bar。
bar(..) 拥有自己的作用域。全局作用域也有自己的作用域气泡,它只包含了一个标识符:foo。
由于标识符 a、b、c 和 bar 都附属于 foo(..) 的作用域,因此无法从 foo(..) 的外部 对它们进行访问。也就是说,这些标识符全都无法从全局作用域中进行访问,因此下面的代码会导致 ReferenceError 错误:
bar(); //Uncaught ReferenceError: bar is not definedconsole.log(a); //Uncaught ReferenceError: a is not defined
console.log(b); //Uncaught ReferenceError: b is not defined
代码编程中有最小授权或最小暴露原则,是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的 API 设计。我们可以利用函数作用域的特性,对代码进行“隐藏”,最经典的代码“隐藏”例子即代码的模块化管理。示例:
function doSomething(a) {
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
function doSomethingElse(a) {
return a - 1;
}
var b;
doSomething( 2 ); // 15
隐藏代码后改为:
function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
doSomething( 2 ); // 15
隐藏后,b 和 doSomethingElse(..) 都无法从外部被访问,而只能被 doSomething(..) 所控制,功能性和最终效果都没有受
影响,但是设计上将具体内容私有化了。“隐藏”作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突,
两个标识符可能具有相同的名字但用途却不一样,无意间可能造成命名冲突。
题5:将下列代码隐藏优化
var a = 2;
function foo() {
var a = 3;
console.log( a ); // 3
}
foo();
console.log( a ); // 2
答案:
(function foo(){
var a = 3;
console.log( a ); // 3
})();
六、函数闭包
参考资料:http://blog.csdn.net/sinat_25127047/article/details/51605009
简单来说,闭包就是能够读取其他函数内部变量的函数。 由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此
可以把闭包简单理解成”定义在一个函数内部的函数”。
闭包的用途一:外部函数可以读取内部函数的变量
function f1(){
var n=999;function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
闭包的用途二:使内部函数的变量一直存在于内存中
var uniqueInteger = (function() {
var counter = 0;
return function() {
return counter++;
}
}());
注意:js中有块作用域,块作用域非常有用的原因和闭包及回收内存垃圾的回收机制相关,如:
with(用于设置代码在特定对象中的作用域。最好不用。)try、catch、finally和let、const。
try、catch、finally:
try {
tryCode - 尝试执行代码块
}
catch(err) {
catchCode - 捕获错误的代码块
}
finally {
finallyCode - 无论 try / catch 结果如何都会执行的代码块
}
let和const的例子:
var foo = true;
if (foo) {
var a = 2;
let b = 3; //包含在if中的块作用域常量
const c = 4; //包含在if中的块作用域常量
}
console.log( a ); // 3
console.log( b ); // Uncaught ReferenceError: b is not defined
console.log( c ); // Uncaught ReferenceError: c is not defined
七、函数参数
参考资料:http://www.jb51.net/article/89297.htm
JavaScript中的函数定义并未指定函数形参的个数和类型,函数调用也未对传入的实参值做任何类型检查,甚至可以不传参数。在非严格模式下,函数中可以出现同名形参,且只能访问最后出现的该名称的形参;而在严格模式下,出现同名形参会抛出语法错误。
function add(x,x,x){
return x;
}
console.log(add(1,2,3));//非严格:3;严格:SyntaxError。
7.1参数个数
当实参比函数声明指定的形参个数要少,剩下的形参都将设置为undefined值:
function add(x,y){
console.log(x,y);
}
add(1);//1 undefined
当实参比形参个数要多时,剩下的实参没有办法直接获得,需要使用arguments对象。javascript中的参数在内部是用一个数组来表示的,函数接收到的始终都是这个数组,而不关心数组中包含哪些参数。在函数体内可以通过arguments对象来访问这个参数数组,从而获取传递给函数的每一个参数。arguments对象并不是Array的实例,它是一个类数组对象,可以使用方括号语法访问它的每一个元素:
function add(x){
console.log(arguments[0],arguments[1],arguments[2])//1 2 3
return x+1; //2
}
add(1,2,3);//1,2,3
arguments对象的length属性显示实参的个数,函数的length属性显示形参的个数:
function add(x,y){
console.log(arguments.length)//3
return x+1;
}
add(1,2,3);
console.log(add.length);//2
当一个函数超过3个形参时,要记住调用函数中实参的正确顺序实在让人头疼,可以通过名/值对的形式来传入参数,这样参数的顺序就无关紧要了。定义函数的时候,传入的实参都写入一个单独的对象之中,在调用的时候传入一个对象,对象中的名/值对是真正需要的实参数据:
function arraycopy(/*array*/from,/*index*/form_start,/*array*/to,/*index*/to_start,/*integer*/length){
//todo
}
function easycopy(args){
arraycopy(args.from,args.form_start || 0,args.to,args.to_start || 0, args.length);}
var a = [1,2,3,4],b =[];
easycopy({form:a,to:b,length:4});
当形参与实参的个数相同时,arguments对象的值和对应形参的值保持同步:
function test(num1,num2){
console.log(num1,arguments[0]);//1 1
arguments[0] = 2;
console.log(num1,arguments[0]);//2 2
num1 = 10;
console.log(num1,arguments[0]);//10 10
}
test(1);
但在严格模式下,arguments对象的值和形参的值是独立的:
function test31(num1,num2){
'use strict';
console.log(num1,arguments[0]);//1 1
arguments[0] = 2;console.log(num1,arguments[0]);//1 2
num1 = 10;
console.log(num1,arguments[0]);//10 2
}
test31(1);
当形参并没有对应的实参时,arguments对象的值与形参的值并不对应:
function test(num1,num2){
console.log(num1,arguments[0]);//undefined,undefined
num1 = 10;
arguments[0] = 5;
console.log(num1,arguments[0]);//10,5
}
test();
7.2参数传递
javascript中所有函数的参数都是按值传递的。也就是说,把函数外部的值复制到函数内部的参数,就和把值从一个变量复制到另一个变量一样。
在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(命名参数或arguments对象的一个元素):
function addTen(num){
num += 10;
return num;
}
var count = 20;
var result = addTen(count);
console.log(count);//20,没有变化
console.log(result);//30
在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部:
function setName(obj){
obj.name = 'test';
}
var person = new Object();
setName(person);
console.log(person.name);//'test'
当在函数内部重写引用类型的形参时,这个变量引用的就是一个局部对象了。而这个局部对象会在函数执行完毕后立即被销毁:
function setName(obj){
obj.name = 'test';
console.log(person.name);//'test'
obj = new Object();
obj.name = 'white';
console.log(person.name);//'test'
}
var person = new Object();
setName(person);
八、函数重载
JS函数对参数不做任何检查,其参数是由包含0或多个值的数组来表示的,所以js函数没有函数签名(接受的参数的类型和数量),所以JS函数不能做到传统意义上的重载,只能根据调用函数实参的不同做不同的反应。
function addSomeNumber(num){
return num + 100;
}
function addSomeNumber(num){
return num + 200;
}
var result = addSomeNumber(100);//300
同名函数,后面的声明会将前面的覆盖掉。
九、函数调用
参考资料:http://www.runoob.com/js/js-function-invocation.html
JS函数有4种不同的调用方式,每种方式的不同在于 this 的初始化。
9.1作为一个函数调用
function myFunction(a, b) {
return a * b;
}
myFunction(10, 2); // 20
以上函数不属于任何对象。但是在 JavaScript 中它始终是默认的全局对象。在 HTML 中默认的全局对象是 HTML 页面本身,所以函数是属于 HTML 页面。在浏览器中的页面对象是浏览器窗口(window 对象)。以上函数会自动变为 window 对象的函数。myFunction() 和 window.myFunction() 是一样的
9.2函数作为方法调用
var myObject = {
firstName:"John",
lastName: "Doe",
fullName: function () {
return this.firstName + " " + this.lastName;
}
}
myObject.fullName(); // 返回 "John Doe"
fullName 方法是一个函数。函数属于对象。 myObject 是函数的所有者。this对象,拥有 JavaScript 代码。实例中 this 的值为 myObject 对象。
9.3使用构造函数调用函数
如果函数调用前使用了 new 关键字, 则是调用了构造函数。这看起来就像创建了新的函数,但实际上 JavaScript 函数是重新创建的对象:
function myFunction(arg1, arg2) {
this.firstName = arg1;
this.lastName = arg2;
}
// This creates a new object
var x = new myFunction("John","Doe");
x.firstName; // 返回 "John"
9.4作为函数方法调用函数
在 JavaScript 中, 函数是对象。JavaScript 函数有它的属性和方法。call() 和 apply() 是预定义的函数方法。 两个方法可用于调用函数,两个方法的第一个参数必须是对象本身。
function myFunction(a, b) {return a * b;
}
var myCallObject = myFunction.call(myCallObject, 10, 2); // 返回 20
var myArray = [10, 2];
var myApplyObject = myFunction.apply(myApplyObject, myArray); // 返回 20