js函数基础回顾

此文用于快速回顾,不适合用来入门。

创建函数对象的三种方法

函数声明

就是对函数进行普通的声明

function add(a, b) {
    return a + b;
}

函数表达式

  • 将函数赋值给变量
// function variable
var add = function(a, b) {
    // body...
};
  • 立即执行函数,把匿名函数用括号括起来,再直接调用。
// IEF(Immediately Executed Function)
(function() {
    // body...
})();
  • 函数对象作为返回值
return function() {
    // body...
};
  • 命名式函数表达式
//NFE(Named Function Expression)
var add = function foo(a, b) {
    // body...
};

这里大家肯定会好奇,这个函数怎么调用?到底用哪个名字呢?

做一个测试:

var func = function nfe() {};
console.log(func === nfe);
// 在 IE6~8,得到 false
// 在 IE9+ 及现代浏览器中 Uncaught ReferenceError: nfe is not defined

那么命名函数表达式有什么使用场景呢?

  • 一般用于调试方便,如果使用匿名函数,执行的时候看不到函数名,命名函数表达式是可以看到函数名的。
  • 或者在递归时,使用名字调用自己。

但是这两种用法都不常见。

Function 构造器

除了函数声明、函数表达式。还有一种创建函数对象的方式,是使用函数构造器。

var func = new Function('a','b','console.log(a+b);');
func(1,2);//3

var func2 = Function('a','b','console.log(a+b);');
func2(1,2);//3

Function 中前面的参数为后面函数体的形参,最后一个参数为函数体。可以看到传入的都是字符串,这样的创建函数对象的方法是不安全的。

还有一点,Function 构造器的得到的函数对象,拿不到外层函数的变量,但是可以拿到全局变量。它的作用域与众不同,这也是很少使用的原因之一。

// 三种方法的对比:

变量 & 函数的声明前置

举两个例子

例1,函数声明:

var num = add(1,2); // 调用
console.log(num);

function add(a, b) { // 声明
    return a + b;
}

例2,函数表达式:

var num = add(1, 2);
console.log(num);

var add = function(a, b) {
    return a + b;
};

例1中得到的结果是 3,而例2中是 Uncaught TypeError: add is not a function

因为函数和变量在声明的时候,会被前置到当前作用域的顶端。例1将函数声明function add(a, b)前置到作用域前端,例2将声明var add前置到其作用域的前端了,但并没有赋值。赋值的过程是在函数执行到相应位置的时候才进行的。

调用方式

  • 直接调用
foo();
  • 对象方法
o.method();
  • 构造器
new Foo();
  • 函数方法call/apply/bind
func.call(o);

函数属性

arguments属性

函数的arguments属性使函数可以处理可变数量的参数。 arguments 对象的 length 属性包含了 传递给函数的参数(即所谓的实参) 的数目。

arguments 对象还可以当做是一个数组来用: arguments 对象中包含的各个参数的访问方式与数组元素的访问方式相同:

function ArgTest(arg1, arg2){
   var s = "";
   s += "The individual arguments are: "
   for (n = 0; n < arguments.length; n++){
      s += ArgTest.arguments[n];
      s += " ";
   }
   return(s);
}
console.log(ArgTest(1, 2, "hello"));
// Output: The individual arguments are: 1 2 hello

严格模式对arguments有一些要求

caller属性

functionName.caller

functionName 是任何正在执行的函数的名称。

caller 属性只有当函数正在执行时(实质上就是被调用)才被定义。

假设有这么一个函数foo,它是在全局作用域内被调用的,则foo.callernull;相反,如果这个函数foo是在另外一个函数作用域内被调用的,则foo.caller指向调用它的那个函数。

可见,其返回结果为函数或者是null

而如果在字符串上下文中使用 caller 属性,则其结果和 functionName.toString 相同。

下面的示例阐释了 caller 属性的用法:

function CallLevel() {
   if (CallLevel.caller == null)
      return("CallLevel was called from the top level.");
   else
      return("CallLevel was called by another function.");
}

console.log(CallLevel());
// Output: CallLevel was called from the top level.

caller也可以用于arguments,常用形式arguments.callee.caller替代了被废弃的arguments.caller

备注:

caller属性,实质上就是去查找并返回调用者(当前函数)的一个特殊属性_caller_ ,返回的是调用当前函数的函数的活动对象,因而可以用来重现出整个调用栈。在调试的时候可以它用来帮助找bug;它是不安全的,它不被列入浏览器标准之内,不要在生产环境中使用。

apply属性(方法)

调用函数,并用指定一个对象替换函数的 this 值,同时用指定数组替换函数的参数。

apply([thisObj[,argArray]])

thisObj:可选。要用作 this 对象的对象。即调用这个函数的对象。

argArray:可选。要额外传递到函数的一组参数。

call属性(方法)

apply 后面的参数需为数组;而call 为扁平化传参

function foo(x, y) {
    console.log(x, y, this);
}

foo.call(100, 1, 2); //1 2 Number {[[PrimitiveValue]]: 100}
foo.apply(true, [3, 4]); //3 4 Boolean {[[PrimitiveValue]]: true}
foo.apply(null); //undefined undefined Window
foo.apply(undefined); //undefined undefined Window

可见,传入 null/undefined 时,实际为 Window 对象 在严格模式下:上述代码最后两行分别输出 null, undefined

bind属性(方法)

bind和call/apply的区别是:bind返回的是函数,可供稍后继续调用;而call和apply直接就是改变this之后立即调用,返回值是对应的调用结果。

fun.bind(thisArg[, arg1[, arg2[, …]]])

下面的代码演示如何使用 bind 方法:

// 定义原始函数:
var checkNumericRange = function (value) {
    if (typeof value !== 'number')
        return false;
    else
        return value >= this.minimum && value <= this.maximum;
}

var range = { minimum: 10, maximum: 20 };

var boundCheckNumericRange = checkNumericRange.bind(range);

console.log(boundCheckNumericRange (12));	// true

上例中结果正常,美中不足的是checkNumericRange只是在特定情境下使用,没必要暴露在全局环境中,所以我们想去把它放在某个对象内部。

在下面的示例中,thisArg对象 与 包含原始函数checkNumericRange的对象不同:

var originalObject = {
    minimum: 50,
    maximum: 100,
    checkNumericRange: function (value) {
        if (typeof value !== 'number')
            return false;
        else
            return value >= this.minimum && value <= this.maximum;
    }
}

console.log(originalObject.checkNumericRange(10)); // Output: false,对象调用自身方法
// ------------------分割线----------------------------
var range = { minimum: 10, maximum: 20 };

var boundObjectWithRange = originalObject.checkNumericRange.bind(range);

console.log(boundObjectWithRange(10)); // Output: true

显然后者更加灵活一点。用bind改变this的指向,一个对象可以使用另外一个对象里面的方法,这是一种常见用法。


再来一个例子,说明一下 结合this的4种绑定规则 深入理解this指向的判定函数的调用者(点运算符之前的对象或者window全局) 的区别:

this.x = 9;
var module = {
    x: 81,
    getX: function() {
        return console.log(this.x);
    }
};

module.getX(); //81
// 函数的调用者为module,函数的this指向module

var getX = module.getX;
getX(); //9
// var getX = module.getX; 将这个方法赋值给一个全局变量,这时 this 指向了 window
// 函数的调用者为window,函数的this指向window

var boundGetX = getX.bind(module);
boundGetX(); //81
// 应用bind,函数的调用者为window,函数的this指向module

一般情况下,谁调用函数,函数的this就指向谁。

bind 主要用于改变函数中的 this

  • module.getX();直接通过对象调用自己的方法,结果是 81
  • var getX = module.getX; 将这个方法赋值给一个全局变量,这时 this 指向了 window,所以结果为 9
  • var boundGetX = getX.bind(module); 使用 bind 绑定了自己的对象,这样 this 仍然指向 module 对象,所以结果为 81

以下代码演示如何使用 arg1[,arg2[,argN]]] 参数:

var displayArgs = function (val1, val2, val3, val4) {
    console.log(val1 + " " + val2 + " " + val3 + " " + val4);
}

var emptyObject = {};

var displayArgs2 = displayArgs.bind(emptyObject, 12, "a");
// "b"和"c"被传给形参val3和val4
displayArgs2("b", "c");
// Output: 12 a b c 

在上例中,一开始绑定函数将 bind 方法中指定的参数用作第一个参数和第二个参数。再调用绑定函数时,指定的任何参数将用作第三个、第四个参数(依此类推)。

bind 与 currying

bind 可以使函数柯里化,那么什么是柯里化?

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回 接受余下的参数且返回结果的新函数 的技术。

function add(a, b, c) {
    return a + b + c;
}

var func = add.bind(undefined, 100); // 接受一个单一参数
func(1, 2); //103

var func2 = func.bind(undefined, 200);// 接受一个单一参数

func2(10); //310

add 函数拥有 3 个参数。我们想先传入一个参数,再去传其他参数。

var func = add.bind(undefined, 100);add 函数对象调用 bind 方法,由于不需要将 this 指向原来的 add 函数对象,所以第一个参数写为 undefined 或 null。第二个参数 100 传给了 add 函数中的形参 a,并赋值给一个新的函数对象 func。

这时,func(1, 2) 即相当于传入后两个参数,所以结果为 103。

同理,基于 func 可以创造一个函数 func2。它只用传最后一个参数。

再见一个柯里化对函数参数拆分的例子:

function getConfig(colors,size,otherOptions){
    console.log(colors,size,otherOptions);
}
var defaultConfig = getConfig.bind(null,'#c00','1024*768');
defaultConfig('123');//'#c00 1024*768 123'
defaultConfig('456');//'#c00 1024*768 456'

bind 与 new一起用时

function foo() {
    this.b = 100;
    return this.a;
}
console.log(foo()); //undefined
var func = foo.bind({
    a: 1
});
console.log(func()); //1

console.log(new func()); //foo {b: 100} ,bind与new一起用时。

如果执行普通的某个函数,其返回值可以是任意类型。

而对于使用了 new func() 这种方式创建对象,其返回值为一个对象。

原函数 foo 的返回值不是对象,所以会直接忽视这个 return 方法。而是变为 return this;。并且 this 会被初始化为一个空对象,这个空对象的原型指向 foo.prototype。所以后面的 bind 是不起作用的。

这里面这个 this 对象包含一个属性 b = 100。所以返回的是对象 {b: 100}

MDN有提到,bind不要和构造函数一起用,各浏览器可能结果不一样。MDN建议不要这样用。

文章目录
  1. 创建函数对象的三种方法
    1. 函数声明
    2. 函数表达式
    3. Function 构造器
    4. // 三种方法的对比:
  2. 变量 & 函数的声明前置
  3. 调用方式
  4. 函数属性
    1. arguments属性
    2. caller属性
    3. apply属性(方法)
    4. call属性(方法)
    5. bind属性(方法)
      1. bind 与 currying
      2. bind 与 new一起用时