JS 函数基础
函数定义:function关键字(函数声明、函数表达式) / Function构造函数
[ function 关键字 ] 使用关键字 function 定义,通常有两种方式:函数声明语句和函数表达式 。
[1] 函数声明语句
function sum(a, b) { function 函数名(形参列表){ return a + b; 函数体 } } // 函数名,是指向函数的指针,本身是一个变量; // 形参列表,内部参数(即,形参)以逗号分隔;形参,可以理解为没有声明的变量 // 函数体,即大括号里的内容,可以为空。
- 提升:在作用域中,提到过函数声明提升(hoisting),函数名称和函数体都提升
foo(); function foo(){ console.log(1); // 1 } 上面这个代码片段之所以能够在控制台输出1,就是因为foo()函数声明进行了提升,如下所示: function foo(){ console.log(1); } foo();
- 重复:变量的重复声明是无用的,但函数的重复声明会覆盖前面的声明(无论是变量还是函数声明)。
// 变量的重复声明无用 var a = 1; var a; console.log(a);//1 // 由于函数声明提升优先于变量声明提升,所以变量的声明无作用 var a; function a(){ console.log(1); } a(); // 1 // 后面的函数声明会覆盖前面的函数声明 a();//2 function a(){ console.log(1); } function a(){ console.log(2); } 所以,应该避免在同一作用域中重复声明。
- 删除:和变量声明一样,函数声明语句创建的变量无法删除。
function foo(){ console.log(1); } delete foo; // false console.log(foo()); // 1
[2] 函数表达式:以表达式方式定义的函数,函数的名称是可选的。
var functionName = function([arg1 [,arg2 [...,argn]]]){ statement; } // 匿名函数(anonymous function)也叫拉姆达函数,是function关键字后面没有标识符的函数。 var functionName = function funcName([arg1 [,arg2 [...,argn]]]){ statement; }通常而言,以表达式方式定义函数时都不需要名称,这会让定义它们的代码更加紧凑;
// 函数定义表达式特别适合用来定义那些只会使用一次的函数。 var tensquared = (function(x) {return x*x;}(10));
- 函数名称 & 变量名:对于具名的函数表达式来说,函数名称相当于函数对象的形参,只能在函数内部使用;而变量名称,相当于函数对象的实参,在函数内部和函数外部都可以使用。
var test = function fn(){ return fn === test; } console.log(test());//true console.log(test === fn);//ReferenceError: fn is not defined
- name 属性:函数定义了一个非标准的 name 属性,通过这个属性可以访问到给定函数指定的名字,这个属性的值永远等于跟在 function 关键字后面的标识符,匿名函数的 name 属性为空。
// IE11-浏览器无效,均输出undefined // chrome在处理匿名函数的name属性时有问题,会显示函数表达式的名字 function fn(){}; console.log(fn.name); //'fn' var fn = function(){}; console.log(fn.name); //'',在chrome浏览器中会显示'fn' var fn = function abc(){}; console.log(fn.name); //'abc'
[ Function 构造函数 ] // 最后一个参数是函数体,之前的都是函数的形参。
// var sum = new Function(形参列表,函数体)
var sum = new Function('a','b','return a + b');
// 一般不推荐使用,它有特殊的应用场景,如把字符串转换为可执行代码会用到
Function 构造函数无法指定函数名称,它创建的是一个匿名函数,sum 不是函数名,但它指向了这个函数。
从技术上讲,这是一个函数表达式。但,不推荐使用,因为这种语法会导致解析两次代码。
// 第一次是解析常规JavaScript代码,第二次解析传入构造函数中的字符串,影响性能。 var sum = new Function('num1','num2','return num1 + num2'); // 等价于 var sum = function(num1,num2){ return num1+num2; }Function() 构造函数创建的函数,其函数体的编译总是会在全局作用域中执行。
// Function()构造函数类似于在全局作用域中执行的eval()。 var test = 0; function fn(){ var test = 1; return new Function('return test'); } console.log(fn()()); // 0并不是所有的函数都可以成为构造函数。
var o = new Math.min(); // Uncaught TypeError: Math.min is not a constructor
函数返回值
函数体中的 return 语句,用来返回函数调用后的返回值。
return expression;
- 如果没有return语句,则函数调用仅仅依次执行函数体内的每一条语句直到函数结束,最后返回调用程序。
var test = function fn(){} console.log(test()); // 这种情况下,调用表达式的结果是undefined
- 当执行到 return 语句时,函数终止执行,并返回 expression 的值给调用程序。
var test = function fn(){ return 2; }; console.log(test()); // 2 // return语句可以单独使用而不必带有expression,这样的话也会向调用程序返回undefined。 var test = function fn(){ return; }; console.log(test()); // undefined
return 语句经常作为函数内的最后一条语句出现,这是因为 return 语句可用来使函数提前返回。
// 并没有弹出1
var test = function fn(){
return;
alert(1); // 当return被执行时,函数立即返回而不再执行余下的语句。
};
console.log(test()); // undefined
函数调用:函数调用 / 方法调用 / 构造函数调用 / 间接调用
[ 函数内部属性?!- this 对象 ] 只有被调用时,函数才会执行,函数名加一对括号就可以调用函数。
function sum(a, b) {
return a + b;
}
var result = sum(1,2); // 括号内可传入实参,与形参一一对应。
console.log(result); // 3
[1] 函数调用模式:对于普通的函数调用来说,函数的返回值就是调用表达式的值。
function add(x,y){ return x+y; } var sum = add(3,4); console.log(sum) // 7
- 使用普通函数调用,非严格模式下,this 被绑定到全局对象;在严格模式下,this 是 undefined。
function add(x,y){ console.log(this); // window } add(); function add(x,y){ 'use strict'; console.log(this); // undefined } add(); // window 因此,'this'可以用来判断当前是否是严格模式 var strict = (function(){return !this;}());
- 重写:使用普通函数调用,函数中的 this 绑定到全局对象,所以会发生全局属性被重写的现象。
var a = 0; function fn(){ this.a = 1; // 应当避免变量名的重复 } fn(); console.log(this,this.a,a); // window 1 1
[2] 方法调用模式:当一个函数被保存为对象的一个属性时,我们称它为一个方法。
- 方法被调用时,this被绑定到该对象。若调用表达式包含提取属性的动作,它实际是被当做一个方法来调用。
var o = { m: function(){ console.log(1); } }; o.m(); // 1
- 方法可以使用 this 访问自己所属的对象,所以它能从对象中取值或对对象进行修改。
var o = { a: 1, m: function(){ return this; }, n: function(){ this.a = 2; } }; console.log(o.m().a);//1 o.n(); // this到对象的绑定发生在调用的时候。 console.log(o.m().a);//2
[3] 构造函数调用模式:如果函数或者方法调用之前带有关键字 new,它就构成构造函数调用。
function fn(){ this.a = 1; }; var obj = new fn(); console.log(obj.a);//1构造函数通常不使用return关键字,它们通常用于初始化新对象,当构造函数的函数体执行完毕时,它会显式返回。
function fn(){ this.a = 2; } var test = new fn(); console.log(test); //{a:2} // 在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。
[4] 间接调用模式
JS 中函数也是对象,函数对象也可以包含方法。call() 和 apply() 方法,可以用来间接地调用函数。
二者都允许:显式指定调用所需的 this 值。
即,任何函数可作为任何对象的方法来调用,哪怕这个函数不是那个对象的方法。
区别在于,call()方法使用它自有的实参列表作为函数的实参,apply()方法则要求以数组的形式传入参数。
var obj = {}; function sum(x,y){ return x+y; } // 两个方法都可以指定调用的实参 console.log(sum.call(obj,1,2)); // 3 console.log(sum.apply(obj,[1,2])); // 3
函数参数
函数有了参数,才有了灵魂。在调用时,可以向其传值,这些值被称为参数。参数(任意个数)可以在函数中使用。
定义函数时,形参不需要指定类型,调用函数时也不会检查是实参的类型和个数。
[1] 形参 & 实参
函数内部是一个封闭的环境(作用域?!),可以通过参数的形式,把外部的值传递给函数内部。
[ 形参 ] 在声明一个函数的时候,为了函数的功能更加灵活,有些值事固定不了的,对于这些固定不了的值,我们可以给函数设置参数,这个参数没有具体的值,仅仅起到一个占位的作用,我们通常称之为形式参数。
同名形参 // 在非严格模式下,函数中可以出现同名形参,且只能访问最后出现的该名称的形参 function add(x,x,x){ return x; } console.log(add(1,2,3));//3 // 而在严格模式下,出现同名形参会抛出语法错误 function add(x,x,x){ 'use strict'; return x; } console.log(add(1,2,3)); // SyntaxError: Duplicate parameter name not allowed in this context[ 实参 ] 函数调用时的实际参数,是在函数被调用时传递给该函数的变量值。
[ arguments ?!] 函数体内,arguments 表示实参列表对象,是一个类数组对象,可通过下标访问对象的实参值。
function sum(a,b){ return arguments[0] + arguments[1]; } console.log(sum(1,2)); // 3
[2] 参数个数
[ 实参 < 形参 ] 当实参比函数声明指定的形参个数要少,剩下的形参都将设置为 undefined 值
function add(x,y){ console.log(x,y);//1 undefined } add(1);常常使用逻辑或运算符给省略的参数设置一个合理的默认值
function add(x,y){ y = y || 2; console.log(x,y);//1 2 } add(1); // 实际上,使用y || 2是不严谨的,显式地设置假值(undefined、null、false、0、-0、''、NaN)也会得到相同的结果。所以应该根据实际场景进行合理设置[ 实参 > 形参 ] 当实参比形参个数要多时,剩下的实参没有办法直接获得,需要使用arguments对象。
function add(x){ console.log(arguments[0],arguments[1],arguments[2])//1 2 3 return x+1; } add(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形参只是提供便利,但不是必需的。
function add(){ return arguments[0] + arguments[1]; } console.log(add(1,2)); // 3[ 实参 = 形参 ] 当形参与实参的个数相同时,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 对象的值相同,但并不是相同的命名空间。
它们的命名空间是独立的,但值是同步的;但在严格模式下,arguments 对象的值和形参的值是独立的。
function test(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 } test(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();
[3] 函数参数传递:JS 中所有函数的参数都是按值传递的。
也就是说,把函数外部的值复制到函数内部的参数,就和把值从一个变量复制到另一个变量一样。
[ 基本类型值 ] 在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量
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);
[ ES6函数新增?↓ ]
[ 默认值 ] 函数参数可以指定默认值,如果调用时没有传入对应的值,则会使用默认值。
function sum(a,b=2) { return a + b; } alert(sum(1)); // 3[ 剩余参数 ] 如果函数的最后一个命名参数以 ... 为前缀,则它是包含剩余参数的数组。
function sum(num1,...nums){ if(nums){ for(let i=0;i<nums.length;i++){ num1 += nums[i]; } } return num1; } console.log(sum(1)); // 1 console.log(sum(1,2,3)); // 6