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