JavaScript 函数深入学习

1 函数

每个函数都是 Function 类型的实例,且都与其他引用类型一样具有属性和方法。由于函数式对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。

1.1 函数概述

1.1.1 函数定义

函数通常都是使用函数声明语法定义的,如下:

  • 使用函数声明语法定义(必须有函数名):

function sum(num1, num2) {return num1 + num2;}

  • 使用函数表达式定义(可以没有函数名):

var sum = function(num1, num2) {return num1 + num2;};

  • 使用 Function 构造函数定义:
    Function 构造函数可以接收任意数量的参数, 但后一个参数始终都被看成是函数体,而前面的参数则枚举出了新函数的参数。

var sum = new Function("num1", "num2", "return num1 + num2"); // 不推荐

这种方式可以很直观理解“函数是对象,函数名是指针”。
从技术上角度讲,这是一个函数表达式。但这种语法会导致解析两次代码(解析常规 ECMAScript 代码,解析传入构造函数中的字符串),影响性能。因此,不推荐使用这种方法定义。由于函数名仅仅是指向函数的指针,因此函数名与包含对象指针的其他变量没有什么不同。换句话说,一个函数可能会有多个名字。使用带圆括号的函数名是调用函数,使用不带圆括号的函数名是访问函数指针,而非调用函数。

1.1.2 函数调用——functionName(arg0, arg1,…,argN);

sayHi("Nicholas", "how are you today?");输出结果:弹出 "Hello Nicholas,how are you today?"

1.1.3 函数返回值

任何函数、任何时候都可以通过 return 语句后跟要返回的值来是实现返回值。函数会在执行完 return 语句后停止并立即退出。因此,return 之后的任何代码都永远不会执行。

function sum(num1, num2) {
    return num1 + num2;
}
调用:var result = sum(5, 10);  // result 为15

注:return 可以不带人和返回值。则函数将返回 undefined 值。一般用在需要提前停止函数执行,又不需要返回值的情况下。推荐:要么让函数始终都有返回值,要么就永远都不要有返回值,否则会给代码调试带来不便。严格模式下的限制: 发生以下情况,会导致语法错误,代码无法执行。不能把函数/参数命名为eval 或arguments;不能出现两个命名参数同名的情况。

1.1.4 理解参数

ECMAScript 函数不介意传递的参数的个数及类型,即使个数与定义的个数不同,也不会报错。因为参数在内部使用一个数组来表示的。函数体内部可以通过 arguments 对象来访问这个参数数组,从而获取传递过来的每一个参数。
function sayHi(name, message) { alert("Hello " + name + "," + message);}
可以像下面这样重写
function sayHi() { alert("Hello " + arguments[0] + "," + arguments[1]);}
ECMAScript 函数的重要特点:

命名的参数只提供便利,但不是必需的;
在调用时,对应参数名字不一定要一致;
aruments 对象可以与命名参数一起使用;
arguments 的值永远与对应命名参数的值保持同步;
arguments 对象的长度由传入函数的参数决定,而非定义函数时的命名参数个数;
没有传递值的命名参数自动被赋予 undefined 值,类似于定义了变量但未初始化。

下面这个函数会在每次被调用时,输出传入其中的参数个数:

function howManyArgs() {
    alert(arguments.length);
}
howManyArgs("string", 45); //2
howManyArgs(); //0
howManyArgs(12); //1

严格模式下:严格模式对如何使用 arguments 对象做出了一些限制。
首先,像前面例子中那样的赋值会变得无效。也就是说,即使把 arguments[1] 设置为 10,num2 的值仍然还是 undefined。其次,重写 arguments 的值会导致语法错误(代码将不会执行)。

1.2 没有重载(深入理解)

重载函数
重载函数是函数的一种特殊情况,在同一个作用域中,如果有多个函数的名字相同,但形参列表不同(参数类型不同或参数个数不同),返回值类型可同也可不同,我们称之为重载函数。

ECMAScript 函数不能重载
ECMAScript 函数不能像传统意义上那样实现重载。
如果在ECMAScript 中定义了两个名字相同的函数,则该名字只属于后定义的函数,后面的会覆盖前面的。通过检查传入函数中参数的类型和数量并作出不同的反应,可以模仿方法的重载。

1.3 函数声明与函数表达式

函数声明:率先读取函数声明,并使其在执行任何代码前可用(可访问);解析器会在代码开始执行之前,通过一个名为函数声明提升的过程,读取并将函数声明添加到执行环境中。对代码求值时,JavaScript 引擎在第一遍会声明函数并将它们放到源代码树的顶部。所以,即使声明函数的代码在调用它的代码后面,JavaScript 引擎也能把函数声明提升到顶部。

使用函数声明语法定义函数(必须有函数名):

alert(sum(10,10));
function sum(num1, num2) {
  return num1 + num2;
}

函数表达式:等到解析器执行到它所在的代码行,才会真正被解释执行。使用函数表达式定义函数(可以没有函数名):

alert(sum(10,10));
var sum = function(num1, num2) {
return num1 + num2;
};

使用函数表达式定义函数,如上,若在定义前通过变量访问函数,会导致报错。因为在执行到函数所在的语句之前,变量 sum 中不会保存有对函数的引用;而且,第一行代码就会导致“unexpected identifier”(意外标识符)错误,代码并不会执行到下一行。

1.4 作为值的函数

因为 ECMAScript 中的函数名本身就是变量,所以函数也可以作为值来使用。即不仅可以像传递参数一样把一个函数传递给另一个函数,也可以将一个函数作为另一个函数的结果返回。如下:

//接受两个参数,第一个参数是一个函数,第二个是要传递给该函数的一个值
function callSomeFunction(someFunction, someArgument){
     return someFunction(someArgument); 
}
function add10(num){
     return num + 10;
}
var result1 = callSomeFunction(add10, 10);
alert(result1);   //20
function getGreeting(name){
     return "Hello, " + name; 
}
var result2 = callSomeFunction(getGreeting, "Nicholas");
alert(result2);   //"Hello, Nicholas"

1.5 函数内部属性

函数内部有两个特殊的对象:argumentsthis

1.5.1 arguments

arguments 是一个类数组对象,包含着传入函数中的所有参数。arguments 有一个 callee 的属性,该属性是一个指针,指向拥有这个 arguments 对象的函数。下面是非常经典的阶乘函数:

function factorial(num){
     if (num <=1) {
         return 1;     
} else {
         return num * factorial(num-1)     
} }
var trueFactorial = factorial;
factorial = function(){
     return 0; 
};
alert(trueFactorial(5));     //0 
alert(factorial(5));         //0
alert(factorial(5));         //0

如上,若这个函数的执行与函数名 factorial 紧紧耦合在了一起,当函数名 factorial 重写后,会对函数执行有所影响,为了消除这种紧密耦合的现象,可以像下面这样使用 arguments.callee。

function factorial(num){
     if (num <=1) {
         return 1;     
} else {
         return num * arguments.callee(num-1)     
  }
 }
var trueFactorial = factorial;
factorial = function(){
     return 0;
 };
alert(trueFactorial(5));     //120 
alert(factorial(5));         //0
alert(factorial(5));         //0

1.5.2 this

this 引用的是函数据以执行的环境对象。

window.color = "red";
var o = { color: "blue" };
function sayColor(){
     alert(this.color); 
}
sayColor();     //"red"    全局作用域调用,this 引用全局对象 window,this.color => window.color
o.sayColor = sayColor; o.sayColor();   //"blue"    函数赋给了对象 o,this 引用的是对象 o,this.color => o.color

注:函数名仅仅是一个包含指针的变量而已。因此,即使是在不同环境中执行,全局的 sayColor() 函数与 o.sayColor() 指向的仍是同一个函数。

1.5.3 caller

caller 属性中保存着调用当前函数的函数的引用。

function outer(){
     inner();  
}
function inner(){
     alert(inner.caller); 
}
outer();

以上代码,会导致警告框中显示 outer() 函数的源代码。因为 outer()调用了 inter(),所以 inner.caller 就指向 outer()。如果是在全局作用域中调用当前函数,它的值为 null。为了实现更松散的耦合,也可以通过 arguments.callee.caller 来访问相同的信息。

function outer(){
     inner();
}
function inner(){
     alert(arguments.callee.caller); 
}  
outer();

注:严格模式下,访问 arguments.callee 会导致错误,不能为函数的 caller 属性赋值,否则也会导致错误。

1.6 函数属性和方法

ECMAScript 中的函数都是对象,因此函数也有属性和方法。每个函数都包含两个属性:length 和 prototype。
length:函数希望接收的命名参数的个数;
prototype:保存所有实例方法的真正所在。

1.6.1 length

function sayName(name){
     alert(name);
}       
function sum(num1, num2){
     return num1 + num2; 
}
function sayHi(){
     alert("hi"); 
}
alert(sayName.length);      //1 
alert(sum.length);          //2 
alert(sayHi.length);        //0

1.6.2 prototype

对于 ECMAScript 中的引用类型而言,prototype 是保存它们所有实例方法的真正所在。每个函数都包含两个非继承而来的方法:apply() 和 call()。
这两个方法的用途都是在特定的作用域中调用函数,实际上相当于设置函数体内 this 对象的值。

1.6.2.1 apply

apply() 方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是 Array 的实例,也可以是 arguments 对象。

function sum(num1, num2){
     return num1 + num2; 
}
function callSum1(num1, num2){
     return sum.apply(this, arguments);        // 传入 arguments 对象 }
function callSum2(num1, num2){
     return sum.apply(this, [num1, num2]);    // 传入数组 
}
alert(callSum1(10,10));   //20 
alert(callSum2(10,10));   //20

注:严格模式下,未指定环境对象而调用函数,则 this 值不会转型为 window。除非明确把函数添加到某个对象或者调用 apply() 或 call(),否则 this 值将是 undefined。

1.6.2.2 call

call() 方法与 apply() 方法作用相同,区别仅在于接收参数的方式不同。对于 call() 而言,第一个参数是 this 值,剩下的参数都直接传递给函数。即使用 call() 方法时,传递给函数的参数必须逐个列举出来。如下:

function sum(num1, num2){
     return num1 + num2; 
}
function callSum(num1, num2){
     return sum.call(this, num1, num2); 
}
alert(callSum(10,10));   //20

使用 apply() 还是 call() ,完全取决于你才去哪种给函数传递参数的方式最方便。不传参数,则哪种方法都无所谓。事实上,传递参数并非 apply() 和 call() 真正的用武之地;它们真正强大的地方是能够扩充函数赖以运行的作用域。

window.color = "red"; 
var o = { color: "blue" };
function sayColor(){
     alert(this.color); 
}
sayColor();                //red          this=>window
sayColor.call(this);       //red         this=>window
sayColor.call(window);     //red           this=>window
sayColor.call(o);          //blue             this=>o

使用 call() 或 apply() 来扩充作用域的最大好处:对象不需要与方法有任何耦合关系。不需要像前面那样把要用的函数放到对象中,调用。

1.6.2.3 bind

这个方法会创建一个函数的实例,其 this 值会被绑定到传给 bind() 函数的值。如:

window.color = "red"; 
var o = { color: "blue" };
function sayColor(){
     alert(this.color); 
}  
var objectSayColor = sayColor.bind(o); 
objectSayColor();    //blue

这里,sayColor()调用 bind()并传入对象 o,创建了 objectSayColor()函数。
object- SayColor()函数的 this 值等于 o,因此即使是在全局作用域中调用这个函数,也会看到”blue”。

    原文作者:sylvia_yue
    原文地址: https://www.jianshu.com/p/0c4602c5a9f4
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞