【JS】JavaScript函数

在JavaScript里,函数即对象,程序可以随意操控它们。比如,JavaScript可以把函数赋值给变量,或者作为参数传递给其它函数。因为函数就是对象,所以可以给它们设置属性,甚至调用它们的方法。JavaScript的函数可以嵌套在其它函数中定义,这样它们就可以访问它们被定义时所处的作用域中的任何变量。这意味着JavaScript函数构成了一个闭包,它给JavaScript带来了非常强劲的编程能力。

1、函数定义

function printprops(o) { // 函数声明语句,输出o的每个属性的名称和值
    for (var p in o)
        console.log(p + “: ” + o[p] + “\n”);
}
var square = function(x) { // 函数定义表达式,求传入参数的平方
    return x * x;
}
data.sort(function(a, b) { return a - b; }); // 函数作为参数
var tensquare = (function(x) { // 函数定义表达式定义后立即调用,传入参数为10
    return x * x;
}(10));
function hypotenuse(a, b) { // 嵌套函数
    function square(x) { return x * x; }
    return Math.sqrt(square(a) + square(b));
}

2、函数调用

printprops({x: 1}); // 函数调用
var calculator = {
    operand1: 1,
    operand2: 2,
    add: function() {
        this.result = this.operand1 + this.operand2;
    }
};
calculator.add(); // 方法调用
var o = new Object(); // 构造函数调用
f.call(o); // 间接调用,以对象o的方法来调用函数f()

3、函数参数

函数实参、形参个数不同时,省略的实参对应的形参都将是undefined,多出的实参会自动省略,但可以通过arguments取得。

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

function getPropertyNames(o, /* optional */ a) {
    a = a || [];
    for (var property in o) a.push(property);
    return a;
}
var a = getPropertyNames(o); // 将o的属性存储到一个新数组中
getPropertyNames(p, a); // 将p的属性追加到数组a中

当调用函数的时候传入的实参个数超过函数定义时的形参个数时,没有办法直接获得未命名值的引用。参数对象解决了这个问题,在函数体内,标识符arguments是指向实参对象的引用,实参对象是一个类数组对象,也包含一个length属性,这样可以通过数字下标就能访问传入函数的实参值,而不用非要通过名字来得到实参。

function max(/* ... */) { // 不定实参函数,实参个数不能为零
    var max = Number.NEGATIVE_INFINITY;
    for (var i = 0; i < arguments.length; i++)
        if (arguments[i] > max) max = arguments[i];
    return max;
}

除了数组元素,实参对象还定义了callee和caller属性,前者指代当前正在执行的函数,后者指代调用当前正在执行的函数的函数,这个在匿名函数中非常有用。

var factorial = function(x) {
    if (x <= 1) return 1;
    return x * arguments.callee(x -1);
}

JavaScript方法的形参并未声明类型,在形参传入函数体之前也未做任何类型检查,可以采用语义化的单词来给函数参数命名,或者给参数补充注释,以此使代码自文档化。

当一个函数的参数过多时,记住参数顺序难免有点麻烦,可行的是把传入的实参写到一个对象中,通过名值对进行调用,如下例子将原始数组的length元素复制到目标数组:

function arraycopy(/* array */ from,
                /* index */ from_start,
                /* array */ to,
                /* index */ to_start,
                /* integer */ length) {
    // ...
}
function easycopy(args) {
    arraycopy(args.from,
                args.from_start || 0// 默认值
                args.to,
                args.to_start || 0
                args.length);
}
var a = [1, 2, 3, 4], b = [];
easycopy({from: a, to: b, length: 4});

4、函数作为值

function square(x) { return x *x; }
var s = square; // s和square指代同一个函数
s(4);
var o= {square: function(x) { return x * x; }}; // 函数作为对象中的一个属性值
var y = o.square(16);

函数是一个特殊的对象,这就意味着函数也可以拥有属性,如下例子,计算阶乘,并将计算结果缓存至函数的属性中:

function factorial(n) {
    if (isFinite(n) && n > 0 && n == Math.round(n)) {// 有限的正整数
        if (!(n in factorial))
            factorial[n] = n * factorial[n - 1];
        return factorial[n];
    }
    else return NaN;
}

5、函数作为命名空间

函数为什么可以作为命名空间?因为,不在任何函数内声明的变量是全局变量,在整个JavaScript程序中是可见的,而在函数中声明的变量只在整个函数内是可见的,在函数的外部是不可见的,这样在函数内定义的变量就不会污染到全局空间。

6、闭包

和其它大多数现代编程语言一样,JavaScript也采用词法作用域,也就是说,函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。为了实现这种词法作用域,JavaScript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为闭包。
从技术的角度讲,所有的JavaScript函数都是闭包,它们都是对象,它们都关联到作用域链。定义大多数函数时的作用域链在调用函数时依然有效,但这并不影响闭包,且看如下例子。

var scope = “global”;
function checkscope() {
    var scope = “local”;
    function f() { return scope; }
    return f();
}
checkscope() // local

修改上面的代码如下,返回结果依然是local,这就是闭包的特性。

var scope = “global”;
function checkscope() {
    var scope = “local”;
    function f() { return scope; }
    return f;
}
checkscope()() // local

7、函数的属性和方法

因为函数也是对象,所以它们也可以拥有属性和方法。

在函数体里,arguments.length表示传入函数的实参的个数,而函数本身的length属性则有着不同的含义,函数的length属性时只读属性,它代表函数实参的期望数量,即形参数量,可以通过arguments.callee.length获得。此外,每一个函数都包含一个prototype属性,这个属性是指向一个对象的引用,这个对象称做原型对象。

call()和apply()方法可以看做是某个对象的方法,通过调用方法的形式来间接调用函数,它们的第一个实参是要调用函数的母对象,它是调用上下文,在函数体内通过this来获得对它的引用。比如,以对象o的方法的形式调用函数f(),并传入两个参数,如下:

f.call(o, 1, 2);
f.apply(o, [1, 2]);

bind()方法的主要作用就是将函数绑定到某个对象,当在函数f()上调用bind()方法并传入一个对象o作为参数,这个方法将返回一个新的函数,调用新的函数将会把原始的函数f()当做o的方法来调用,传入新函数的任何实参都将传入原始函数。如下例子:

function f(y) { return this.x + y; }
var o = {x: 1};
var g = f.bind(o);
f(2) // 3

和所有的JavaScript对象一样,函数也有toString()方法,大多数返回函数的完整源码。

定义函数还可以使用Function()构造函数,下面两种方式等效(不同的是,构造函数所创建的函数并不是使用词法作用域,函数体代码的编译总是会在顶层函数执行,也就无法捕获局部作用域):

var f = new Function(“x”, “y”, “return x * y;”);
var f = function(x, y) { return x * y; }

所有的函数都是可调用对象,但并非所有的可调用对象都是函数。

8、函数式编程

使用数组方法reduce()和map()计算平均值和标准差:

var sum = function(x, y) { return x + y; }
var square = function(x, y) { return x * y; }
var data = [1, 1, 3, 5, 5];
var mean = data.reduce(sum) / data.length; // 平均值
var deviations = data.map(function(x) { return x - mean; });
var stddev = Math.sqrt(deviations.map(square).reduce(sum) / (data.length - 1)); // 标准差

操作函数的函数,接收参数作为参数,如下例子判断数值的奇偶性:

function not(f) {
    return function() {
        var result = f.apply(this, arguments);
        return !result;
    }
}
var even = function(x) { // 判断x是否为偶数
    return x % 2 === 0;
}
var odd = not(even); // 新函数,所做的事情完全与even()相反,也就是判断是否为奇数
[1, 3, 5, 7, 9].every(odd); // true 每个元素都是奇数        

JavaScript中,还有一种函数叫做不完全函数,即把一次完整的函数调用拆成多次函数调用,每次传入的实参都是完整实参的一部分,每个拆分开的函数叫做不完全函数,每次函数调用叫做不完全调用,这种函数变换的特点是每次调用都返回一个函数,直到得到最终运行结果为止。

在客户端JavaScript中,代码的执行时间复杂度往往成为瓶颈,一个可行的优化方法是使用缓存技巧,其实是牺牲了算法的空间复杂度以换取更优的时间复杂度,如下memory()函数,接收一个函数作为参数,并返回带有缓存功能的版本。

function memory(f) {
    var cache = {};
    return function() {
        var key = arguments.length + 
            Array.prototype.join.call(arguments, “,”);
        if (key in cache) return cache(key);
        else return cache[key] = 
            f.apply(this, arguments);
    };
}

写一个具有缓存功能的递归函数:

var factorial = memorize(function(n) {
    return (n <= 1) ? 1 : n * factorial(n - 1);
});
factorial(5) // 120,对于4到1的值也有缓存
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值