第四章 函数

44 阅读10分钟

函数语法

创建函数

function 函数名(形参1, 形参2, ...){
    函数体;
}

这样的函数,也称为函数字面量

对函数使用typeof,得到的是"function"

调用函数

函数名(实参1, 实参2, ...);

若函数调用时没有传递实参,则对应的形参为undefined,而arguments则不会包含对应该项的属性

函数的参数只能在函数内部使用

函数提升

任何作用域下的函数字面量,会提升至当前作用域的最顶部,但不会跨越脚本块

全局作用域下的函数字面量,在浏览器环境下,会成为window对象的属性

返回值

函数中return关键字后面的数据就是函数的返回值,若其后面没有数据,则默认为return undefined

执行到return时,会直接结束函数的执行

若函数中没有手动书写return,则JS会在其函数末尾自动补充一个return

文档注释

格式:/** */

作用:通常在函数字面量之前加入文档注释,用于显示函数的功能、函数参数的类型,返回值的类型等

/*
 * 
 * @param {number} a 第一个数字
 * @param {number} b 第二个数字
 * @returns {number} 相加的结果
 */
function sum(a, b){
    return a + b;
}

sum(1, 2);		// 在调用函数时,编辑器就能自动提示文档注释中的信息

arguments

arguments需要在函数中使用,它是一个类数组,用于获取在函数调用时传递的所有实参

类数组不是数组,类数组不是通过new Array()创建出来的,Array的原型上的方法,类数组都不能使用

类数组只是类似于数组,它具有和数组一样的许多特点,比如属性名为数字形式,拥有length属性等

arguments中的值,会与对应的形参相互映射,彼此的更改都会影响到对方

function method(a, b){
    arguments[0] = 10;
    b = "b";
    console.log(a, arguments[0]);		// 10  10
    console.log(b, arguments[1]);		// "b" "b"
}

method(1, "a");

但这种映射现象,只会存在于arguments中一开始就存在的值

若函数调用时没有传递实参,则arguments中不会包含对应该项的属性

function method(a, b){
    arguments[0] = 10;
    b = "b";
    console.log(a, arguments[0]);		// undefined  10
    console.log(b, arguments[1]);		// "b" undefined
}

method();

细节:

  • arguments的长度值取决于实参的数量,和形参数量无关

    function test(a, b, c, d){
        console.log(arguments);
    }
    
    test("a", "b", "c");
    // arguments { 0: "a", 1: "b", 2: "c", length: 3 }
    
  • arguments会考虑undefined实参

    function test(){
    	console.log(arguments);
    }
    
    test(null, undefined);
    // arguments {0: null, 1: undefined, length: 2 }
    

作用域和闭包

作用域

JS中的两种作用域:

  1. 全局作用域

    在全局作用域中声明的var变量和函数字面量,会被提升到脚本块的顶部,并且如果是浏览器环境下还会成为window对象的属性

  2. 函数作用域

    在函数作用域中声明的var变量和函数字面量,会被提升到函数作用域的顶部,但不会成为window对象的属性

    因此函数作用域中声明的var变量和函数字面量,不会导致污染全局

    仅讨论浏览器环境下的污染全局

除了全局作用域和函数作用域,JS中还包含以下几种作用域:

  • 块级作用域
  • eval作用域
  • 模块作用域
  • 脚本作用域
  • with作用域
  • catch作用域

立即执行函数

当函数成为函数表达式时,它既不会提升,也不会污染全局

将函数变为一个函数表达式的方式有很多,其中之一就是将函数字面量用小括号包裹

(function method(){});

函数表达式中函数的名称可以省略,就算不省略函数表达式的函数名,JS也不会将提取到VO对象中,因此在任何地方通过该名称来访问到函数是办不到的,这也是函数变为函数表达式后不会提升且不污染全局的原因

(function method(){});

console.log(method);				// ReferenceError: method is not defined

当函数没有名称时,这样的函数就叫做匿名函数

(function(){})

函数表达式的返回值就是函数本身,因此可以直接调用函数表达式

(function(){})();

一个函数表达式如果被直接调用,则该函数被称为立即执行函数(IIFE,Imdiately Invoked Function Expression)

作用域中可以使用的变量

全局作用域中只能使用全局作用域中声明的变量和函数

函数作用域中可以使用自身作用域中声明的变量和函数,也能使用外部环境中的声明的变量和函数

函数内部声明的变量,若与外部环境中声明的变量存在冲突,则使用时会使用自身的,若自身没有才会逐层向外查找

闭包

在函数内部使用了外部环境中的变量或函数,这种行为就叫做为闭包(closure)

函数表达式和this

函数表达式

JS中,函数是一种引用类型的数据,因此理论上,数据能去的地方,函数都可以去,比如:

var fn = function method(){};

将函数赋值给一个变量,则该函数会转变成为函数表达式,因此可以写为匿名函数

var fn = function (){};

除了上面这种写法外,还有很多其他函数表达式的写法:

var obj = {
    test: function test(){}
}

function method(){
    return function test(){ };		// 该"test"函数是函数表达式,而不是函数字面量
}

method(function test(){});			// 该"test"函数是函数表达式,而不是函数字面量

对象中的属性如果是一个函数,也称该属性为对象的方法

this

浏览器环境下,this在全局作用域下使用时,指向全局对象

node环境下,this在全局作用域下使用时,指向一个空对象

this在函数作用域中使用时,其指向取决于函数是如何被调用的:

  1. 函数是直接调用的,则this指向全局对象

    method(),其this指向全局对象

  2. 通过对象的属性的形式调用函数,则this指向该对象

    obj.method(),其this指向obj

    arr[n](),其this指向arr

  3. 通过new关键字调用函数,this指向新创建的对象

  4. 通过call或apply进行调用,this指向call或apply的第一个参数

  5. 事件处理函数中的this,this指向注册事件的dom元素

注意:

  • this不能被重新赋值
  • 函数作用域中的this取值,还受是否开启严格模式的影响

构造函数

构造函数用于创建对象

构造函数又称为构造器

构造函数和普通函数并没有什么不同,但使用new关键字调用的函数才叫做构造函数

如果不使用new调用函数,则该函数是普通函数,否则该函数才叫做构造函数

构造函数的函数名通常使用大驼峰命名法

JS中的所有对象,都是通过构造函数创建的,只是JS为我们提供了语法糖,让创建对象的过程变得简单,而这写语法糖最终还是要转换为原始的new 构造函数()的形式

使用new关键字调用函数

使用new关键字调用的函数,函数内部会发生如下变化:

  1. 函数内部会自动创建一个新对象

  2. 函数内部的this会自动指向该新创建的对象

  3. 新创建的对象的隐式原型会指向构造函数的原型

  4. 执行函数体

  5. 如果函数中没有手动加入return语句,则函数return的就是该新创建的对象

  6. 如果函数中手动进行了return,则分为下面两种情况:

    return的是原始值,则将替换为新创建的对象

    return的是引用值,则返回该引用值而不返回新创建的对象

构造函数通过new关键字创建出的对象,也称为构造函数的实例

实例上的属性称为实例属性,实例上的方法称为实例方法

构造函数上的属性称为类属性或静态属性,构造函数上的方法称为类方法或静态方法

可以认为JS中的构造函数就是类,类就是构造函数

Number.isNaN();			// isNaN是静态方法
new Number().toFixed();	// toFixed是实例方法

new.target

该表达式在函数内部使用

如果所在函数是通过new调用的,则返回所在函数,否则返回undefined

因此该表达式可以判断函数是否是通过new调用的

函数的本质

函数的本质就是对象,因此函数也可以有属性

JS中的所有对象都是通过构造函数创建出来的,因此函数也是通过构造函数创建的

JS中除Function外的所有函数,都是通过new Function()创建出来的

Function函数比较特殊,它是在JS执行引擎启动时就直接加入到JS之中的

var sum = new Function("a", "b", "return a + b");
console.log(sum(1, 2));		// 3

函数的一些属性:

  • fn.length为函数的形参的数量

  • fn.name为函数的函数名

    下面几种情况特殊记忆:

    var test = function a(){};
    console.log(test.name);		// "a"
    
    var test = function (){};
    console.log(test.name);		// "test"
    

包装类

JS为了增强boolean、string、number这三种原始类型的功能,为它们分别创建了构造函数Boolean、String、Number

如果把原始值当做引用值来使用,则JS会自动将原始值临时替换为相应的构造函数所创建的对象,然后转变对该对象进行操作,一旦操作完成,该包装的对象就会被立即销毁掉

var str = "abc";
str.length;
/**
	上面的代码会转变为:
	temp_str = new String("abc")
	temp_str.length;
	temp_str = null;
*/

递归

函数直接或间接调用自身的方式叫做递归

递归的两个必要条件:

  • 存在限制条件,当满足限制条件时,递归便不再继续
  • 每次递归调用之后越来越接近这个限制条件

执行栈

任何代码的执行都需要一个执行环境(执行上下文)

执行环境是放到执行栈中的

每次函数调用,都会创建一个新的执行环境,函数执行结束时,执行环境销毁

尾递归

当函数return的是自身函数调用,并且调用函数的表达式不是另一个表达式的一部分,则这种递归模式称为尾递归

某些语言或执行环境会对尾递归进行优化,当执行到尾部的递归调用语句时,会立即销毁当前的执行环境,从而避免在之后递归调用的过程中之前的执行环境无法释放

// 尾递归求n的阶乘
function f(n, total){
    if(n === 1){
        return total;
    }
    return f(n - 1, n * total);
}

console.log(f(5, 1));

可以优化的原因:当递归函数的返回值完全等于下一层函数的返回值时,就没有必要等到递归结束后把最后一次递归调用的结果一层层往上传,而是可以直接把其传给最初使用递归函数的地方即可,从而避免执行栈空间被大量占用而导致栈溢出

尾递归代码不易阅读,开发中很少会使用尾递归模式

在JS的浏览器环境下,没有尾递归优化,但在node环境中有优化