06.JavaScript基础系列:函数

288 阅读12分钟

JavaScript基础系列

能够学到的知识点

  • 4中调用函数的方法
  • 闭包
  • apply()、call()、bind()

JavaScript 函数是参数化的,函数的定义会包括一个称为形参的标识符列表,这些参数在函数体中像局部变量一样工作,函数调用会为形参提供实参的值,同时还会拥有一个调用的上下文(this)。

如果函数挂载在对象上,作为对象的一个属性,那它就称为对象的方法。当通过这种方式调用函数时,该对象就是此次调用的上下文,也就是该函数的 this 的值。

在 JavaScript 中,函数即对象,程序可以随意操控它们。比如,可以把函数赋值给变量、或者作为参数传递给其他函数。

JavaScript 的函数可以嵌套在其他函数中定义,这样它们就可以访问它们被定义时所处的作用域的任何变量,这意味着 JavaScript 函数构成一个闭包。

1.函数定义

函数使用 function 关键字来定义,具有函数声明和函数表达式两种形式。除 function 关键字之外其组成部分如下:

  • 函数名称标识符
    • 对于函数声明语句来讲是必须的部分,它的用途就像变量的名字,新定义的函数对象会赋值给这个变量
    • 对于函数表达来讲,这个名字是可选的
  • 一对圆括号
    • 包含 0 或多个用逗号隔开的标识符组成的列表,这些标识符是函数的形参,在函数体中是局部变量
  • 一对花括号
    • 包含 0 或多条 JavaScript 语句,构成了函数体

大多数函数包含一条 return 语句,return 语句会导致函数停止执行,并返回它的表达式的值给调用者。如果 return 语句没有一个与之相关的表达式,则它返回 undefined。

函数声明语句并非真正的语句,ECMAScript 规定只允许它们作为顶级语句,可以出现在全局代码中或者内嵌在其他函数中;不能出现在循环、条件判断、或者 try/catch/finally 等语句中。

2.函数调用

构造函数主体的 JavaScript 代码在定义之时并不会执行,只有调用该函数时才会执行。

有 4 种方式可以调用 JavaScript 函数

  • 作为函数
  • 作为方法
  • 作为构造函数
  • 通过 call() 和 apply() 方法间接调用
2.1 函数调用

使用调用表达式可以进行普通函数调用,在 ECMAScript 3 和非严格的 ECMAScript5 中进行函数调用时,调用上下文(this 的值)是全局对象,在严格模式下,调用上下文是 undefined。

function test() {
    console.log("this: ", this);
}

test();     // window


function test2() {
    'use strict'
    console.log("this: ", this);
}

test2();    // undefined
2.2 方法调用

方法是保存在一个对象的属性上,可以通过下面方式进行调用。

obj.method(param);

obj.method 是一个属性访问表达式,意味着该函数被当做一个方法,而不是作为一个普通函数来调用。

方法调用和函数调用有一个重要的区别,即:调用上下文。

  • 方法调用的调用上下文是调用方法的对象,可以在函数体中通过 this 引用该对象
  • 函数调用的调用上下文在严格模式下是 undefined,非严格模式下是 window

this 是一个关键字,不是变量,也不是属性名,JavaScript 语法不允许给 this 赋值。

嵌套的函数不会从调用它的函数中继承 this。

  • 嵌套函数作为方法调用,其 this 的值指向调用它的对象
  • 嵌套函数作为函数调用,其 this 的值不是全局对象就是 undefined

认为调用嵌套函数时 this 会指向调用外层函数的上下文,这是错误的认识。

var obj = {
    m: function() {
        var self = this;
        console.log(this === obj)   // true, this 就是这个对象
        f();                        // 调用函数
        
        function f() {
            // this 的值是全局对象或undefined
            console.log(this === obj); // false
            console.log(self === obj); // true
        }
    }
}
obj.m();
2.3 构造函数调用

如果函数或方法调用之前带有关键字 new,就构成构造函数调用。对于没有形参的构造函数调用可以省略圆括号。

构造函数调用创建了一个新的空对象,这个对象继承自构造函数的 prototype 属性,构造函数使用 this 关键字引用这个新创建的对象。

2.4间接调用

可以通过 call() 或 apply() 间接地调用函数,允许显式指定所需的 this 值。

call() 方法接受实参列表作为函数的实参,apply() 方法要求以数组的形式传入参数。

3.函数的实参和形参

调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的形参都将设置为 undefined 值。

在函数体内,可以通过标识符 arguments 获得实参对象的引用,实参对象是一个类数组对象。在 ECMAScript 5 中或严格模式下,函数无法使用 arguments 作为形参名或局部变量名,也不能给予赋值。

在非严格模式下,实参对象还定义了 callee 和 caller 属性。规范规定 callee 属性指代当前正在执行的函数。caller 是非标准的,指代调用当前正在执行函数的函数。

4.作为值的函数

函数定义和调用是 JavaScript 的词法特性,同时函数可以赋值给变量,存储在对象的属性或数组的元素中,作为参数传入另外一个函数。

函数可以拥有属性,当函数需要一个 "静态" 变量来在调用时保持某个值不变,最方便的方式就是给函数定义属性,而不需要定义全局变量。

// 给函数增加属性并赋值
test.counter = 0;

function test() {
    return test.counter++;
}

5.作为命名空间的函数

在函数中声明的变量在整个函数体内都是可见的,在函数的外部是不可见的。不在任何函数内声明的变量都是全局变量,在整个 JavaScript 程序中都是可见的,在 JavaScript 中无法声明只在一个代码块内可见的变量,基于这个原因,我们可以简单地定义一个函数用作临时的命名空间。

(function() {
    // 模块代码,函数体定义的变量只在函数体内可见
}())

6.闭包

JavaScript 采用词法作用域,也就是说,函数的执行依赖于变量作用域,这个作用域在函数定义时决定的,而不是函数调用时决定的

为了实现这种词法作用域,JavaScript 函数对象的内部状态不仅包含函数的代码逻辑,还必须包含当前的作用域链。

函数对象可以通过作用链相互关联起来,函数体内部的变量可以保存在函数作用域内,这种特性在计算机科学文献中称为 "闭包。

var scope = 'global scope';     // 全局变量
function checkScope() {
    var scope = 'local scope';  // 局部变量
    function f() {
        return scope;           // 在作用域内返回这个值
    }
    return f();
}

checkScope();                   // "local scope"

function checkScope2() {
    var scope = 'local scope';	// 局部变量
    function f() {
        return scope;           // 在作用域内返回这个值
    }
    return f;
}

checkScope2()();                // "local scope"

JavaScript 函数的执行用到了作用域链,这个作用域链是函数定义的时候创建的。嵌套函数 f() 在定义这个作用域链里,其中的 scope 一定是个局部变量,不管在何时何地执行函数 f(),这种绑定在执行 f() 时依然有效。

闭包的这个特性可以捕捉到局部变量(和参数),并一直保存下来,看起来像这些变量绑定到了其中定义它们的外部函数。

外部函数中定义的局部变量在函数返回后就不存在了,那么嵌套的函数如何能调用不存在的作用域链呢?

JavaScript 在定义作用域链时,会将作用域链描述为一个对象列表,而不是绑定的栈。每次调用 JavaScript 函数的时候,都会为之创建一个新的对象来保存局部变量,把这个对象添加到作用域链中。当函数返回的时候,就从作用域链中将这个绑定变量的对象删除。

如果不存在嵌套的函数,也没有其他引用指向这个绑定对象,就会被做垃圾回收掉。

如果定义了嵌套的函数,每个嵌套的函数都各自对应一个作用域链,并且这个作用域链指向了一个变量绑定对象。如果这些嵌套的函数对象在外部函数中保存下了,那么它们也会和所指向的变量绑定对象一样当垃圾回收。如果这个函数定义了嵌套的函数,并将它作为返回值返回或存储在某处的属性中,这时就会有一个外部引用指向这个嵌套的函数,就不会被当做垃圾回收,并且它所指向的变量绑定对象也不会被当做垃圾回收。

function test() {
    var funcs = [];
    for (var i = 0; i < 10; i++) {
        funcs[i] = function () { return i; };
    }
    return funcs;
}

var funcs = test();
funcs[5]()		// 10

这段代码创建10个闭包,这些闭包都是在同一个函数调用中定义,因此它们共享变量 i,因此函数的返回值都是同一个值。嵌套的函数不会将作用域内的私有成员复制一份,也不会对绑定的变量生成静态快照。

function test() {
    var funcs = [];
    for (var i = 0; i < 10; i++) {
        funcs[i] = (function(j) {
            return function () {return j}
        }(i))
    }
    return funcs;
}

var funcs = test();
funcs[5]()      // 5

7.函数属性、方法和构造函数

函数是对象,也可以用于属性和方法,还可以通过用 Function() 构造函数来创建新的函数对象。

7.1 length 属性

在函数体内,arguments.length 表示传入函数的实参个数,而函数本身的 length 代表函数形参的个数,length 属性是只读的。

function test(x, y, z) {
    console.log(test.length);       // 3
    console.log(arguments.length);  // 2
}

test(1, 2);

7.2 prototype 属性

每个函数都包含一个 prototype 属性,这个属性是指向一个对象的引用,这个对象称做 "原型对象"。每一个函数都包含不同的原型对象,当将函数用作构造函数的时候,新创建的对象会从原型对象上继承属性

7.3 call() 和 apply()

call() 和 apply() 看做是某个对象的方法,通过调用方法的形式来间接调用函数。第一个实参是要调用函数的母对象,即上下文,在函数体内通过 this 来获得对它的引用。

还可以通过对象的方法来调用函数。

f.call(o);		
// 等价于下面的代码
o.m = f;        // 将 f 存储为 o 的临时方法
o.m();          // 调用它,不传入参数
delete o.m;     // 将临时方法删除

Math.max.call(Math, 1, 2, 3, 4, 5);

Math.m = Math.max;
Math.m(1, 2, 3, 4, 5);
delete Math.m;

f.apply(o)

在 ECMAScript 5 的严格模式下,call() 和 apply() 的第一个实参会变成 this 的值,哪怕传入的实参是原始值甚至是 null 和 undefined。

在 ECMAScript 3 和非严格模式下,传入的 null 和 undefined 都会被全局对象替代,而其他原始值则会被相应的包装对象所替代。

7.4 bind()

bind() 是 ECMAScript 5 新增的方法,其作用是将函数绑定至某个对象。调用这个方法将返回一个新的函数。

function f(y) { return this.x + y };

var obj = {x: 1};
var g = f.bind(obj);    // 通过调用 g(x) 来调用 obj.f(x)
g(2);

ECMAScript 3 版本 Function.bind 的函数实现

if (!Function.prototype.bind) {
    Function.prototype.bind = function(o) {
        // 将 this 和 arguments 保存到变量中
        var self = this;
        var bondArgs = arguments;
    
        // bind() 的返回值是一个函数
        return function() {
            var args = [];
            // 拼接 bind 的参数
            for(var i = 1; i < bondArgs.length;  i++)  
            args.push(bondArgs[i]);
    
            // 拼接后续传入的参数
            for(var i = 0; i < arguments.length; i++)
            args.push(arguments[i]);
            return self.apply(o, args);
        }
    }
}

柯里化(currying)

var sum = function (x, y) { return x + y };

// 创建一个类型 sum 的新函数,但 this 的值绑定到 null
// 第二个参数绑定到 1,这个新的函数期望只传入一个实参
var succ = sum.bind(null, 1);
succ(3);		// 4

8.函数式编程

map() 和 reduce()

自己实现一个 Array 的 map 函数

[1, 2, 3, 4, 5].map(function(val, index, arr) {
    console.log(val, '---', index, '----', arr)
})


function map(a, f) {
    var result = [];
    for (var i = 0, len = a.length; i < len; i++) {
        if (i in a) result[i] = f.call(null, a[i], i, a);
    }
    return result;
}

map([1, 2, 3, 4, 5], function(val, index, arr) {
    console.log(val, '---', index, '----', arr)
})

自己实现一个 Array 的 reduce 函数

// 25
[1, 2, 3, 4, 5].reduce(function(a, b) {
    return a + b
}, 10)	

function reduce(a, f, initial) {
    var i = 0, len = a.length, accumulator = 0;
    
    if (len === 0) throw TypeError();
	
    if ( arguments.length > 2) accumulator = initial;
	
    while (i < len) {
        if (i in a) {
            accumulator = f.call(null, accumulator, a[i], i , a);
            i++
        }
    }
    return accumulator;
}	

reduce([1, 2, 3, 4, 5], function(a, b) {
    return a + b
}, 10)
高阶函数

高阶函数就是操作函数的函数,它接收一个或多个函数作为参数,并返回一个新函数。

function compose(f, g) {
    return function() {
        // 需要给 f() 传入一个参数,所以使用 f() 的 call() 方法
        // 需要给 g() 传入很多参数,所以使用 g() 的 apply() 方法
        return f.call(this, g.apply(this, arguments))
    }
}

var square = function(x) { return x * x };
var sum = function(x, y) { return x + y };

var squareofsum = compose(square, sum);
squareofsum(2, 3);			// 25
记忆

在函数式编程当中,我们常常会遇到将上次的计算结果缓存起来,这种缓存技巧叫做 "记忆"。

function memorize(f) {
    // 将值保存在闭包内
    var cache = {};
    return function() {
        // 将实参转换为字符串,作为缓存的键
        var key = arguments.length + Array.prototype.join.call(arguments, ',');
        if (key in cache) {
            return cache[key];
        } 
        return cache[key] = f.apply(this, arguments)
    }
}

var factorial = memorize(function(n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
})

factorial(5);			// 120