3

函数包含一组语句,它们是JavaScript的基础模块单元,用于代码的复用、信息隐藏和组合调用。函数用于指定对象的行为。一般来说,所谓编程,就是将一组需求分解成函数与数据结构的技能。

JavaScript中函数被作为“一等公民”,函数也属于对象,不同的是只有函数可以被调用。

函数声明与表达式

函数声明

function foo() {}

上面的方法会在执行前被 解析(hoisted),因此它存在于当前上下文的任意一个地方, 即使在函数定义体的上面被调用也是对的。

foo(); // 正常运行,因为foo在代码运行前已经被创建
function foo() {}

函数赋值表达式

var foo = function() {};

这个例子把一个匿名的函数赋值给变量 foo。

foo; // 'undefined'
foo(); // 出错:TypeError
var foo = function() {};

由于 var 定义了一个声明语句,对变量 foo 的解析是在代码运行之前,因此 foo 变量在代码运行时已经被定义过了。

但是由于赋值语句只在运行时执行,因此在相应代码执行之前, foo 的值缺省为 undefined

命名函数的赋值表达式

另外一个特殊的情况是将命名函数赋值给一个变量。

var foo = function bar() {
    bar(); // 正常运行
}
bar(); // 出错:ReferenceError

bar 函数声明外是不可见的,这是因为我们已经把函数赋值给了 foo; 然而在 bar 内部依然可见。这是由于 JavaScript 的 命名处理 所致, 函数名在函数内总是可见的。[注意]:在IE8及IE8以下版本浏览器bar在外部也是可见的,是因为浏览器对命名函数赋值表达式进行了错误的解析, 解析成两个函数 foo 和 bar.

函数的调用

调用一个函数会暂停当前函数的执行,传递控制权和参数给新的函数。除了声明时定义的形式参数外,还传递两个隐式的参数:this和arguments.this的值取决于调用模式(方法调用,函数调用,构造函数调用,apply调用)。当实际参数和形式参数不匹配时不会报错,如果实际参数大于形式参数,多的值会忽略。如果实际参数小于形式参数,多的形式参数会设undefine.

方法调用

var Obj = {
    value: 0,
    increment: function(inc){
        this.value += typeof inc === "number" ? inc : 1; 
    }
};
Obj.increment();  // 1
Obj.increment(2); //3

方法可以使用this访问自己所属的对象,所以它能从对象中取值和对对象进行修改,this到对象的绑定发生在调用的时候。

函数调用

//给Obj加一个double方法
Obj.double = function(){
    var add = function(){
        var val = this.value;
        if(typeof val === "number"){
            this.value = val * 2;
        }
    }
    add();
}
Obj.double();

以上代码达不到目的,因为以此模式调用时,this被绑定到了全局变量。这是语言设计上的一个错误,倘若语言设计正确,那么当内部函数被调用时,this应该绑定到外部函数的this变量。这个设计错误的后果就是方法不能利用内部函数来帮助它工作,因为内部函数的this被绑定了错误的值,所以不能共享该方法对对象的访问权。幸运的是,有一个很容易的解决方案:如果该方法定义了一个变量并给他赋值this,那么内部函数就可以通过那个变量访问到this. 按照约定,我们可以把那个变量命名that:

//给Obj加一个double方法
Obj.double = function(){
    var that = this;
    var add = function(){
        var val = that.value;
        if(typeof val === "number"){
            that.value = val * 2;
        }
    }
    add();
}
Obj.double();

构造函数调用

如果在一个函数前面带上new 来调用,那么背地里将会创建一个连接到该函数的prototype成员的新对象,同时this会绑定到那个对象上。new前缀也改变了return语句的行为。

var Obj = function(val){
    this.value = val;
}
var myObj = new Obj();

Apply调用模式

apply方法让我门构建一个参数数组传递给调用函数。它也允许选择this的值。类似的还有call.

Object.prototype.toString.call({}); //"[object Object]"

属性

prototype

Function.prototype 属性存储了构造函数的原型对象。可以使用该属性实现继承:

function Animal(name){
    this.name = name;
}
function Bird(){
}
bird.porototype = new Animal(); //bird的原型指向Animal的原型,继承Animal的属性。

这个属性和对象的内部属性[[prototype]]是有所不同的:

var a = {
  x: 10,
  calculate: function (z) {
    return this.x + this.y + z;
  }
};
var b = Object.create(a, {y: {value: 20}});
var c = Object.create(a, {y: {value: 30}});

以上实际是通过对象的内部属性[[prototype]]实现继承。
对象继承

function Foo(y) {
  this.y = y;
}
 
Foo.prototype.x = 10;
 
Foo.prototype.calculate = function (z) {
  return this.x + this.y + z;
};

var b = new Foo(20);
var c = new Foo(30);

用构造函数实现继承,构造函数的原型链如下图:
构造函数原型链

arguments

function.arguments 已经被废弃很多年了,现在推荐的做法是使用函数内部可用的 arguments 对象来访问函数的实参。所以主要讲述一下arguments的特性:arguments 是一个类数组对象。代表传给一个function的参数列表。,arguments 对象是函数内部的本地变量;arguments 已经不再是函数的属性了。arguments 对象并不是一个真正的Array。它类似于数组,但没有数组所特有的属性和方法,除了 length。例如,它没有 pop 方法。不过可以将其转换成数组。

length

length 属性指明函数的形参个数。数量不包括剩余参数[ES6]。相比之下, arguments.length 是函数被调用时实际传参的个数。

非标准属性

  • name: name 属性返回所属函数的函数名。name 属性返回一个函数的名称, 如果是匿名函数, 则返回空字符串。

  • caller: 返回调用指定函数的函数。如果一个函数f是在全局作用域内被调用的,则f.caller为null,相反,如果一个函数是在另外一个函数作用域内被调用的,则f.caller指向调用它的那个函数。

  • displayName: 获取函数的显示名字。

方法

Function.prototype.bind()方法

bind()方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

example

在下面的例子代码中,我们可以名正言顺地将上下文缓存到一个变量中:

var myObj = {
 
    specialFunction: function () {
 
    },
 
    anotherSpecialFunction: function () {
 
    },
 
    getAsyncData: function (cb) {
        cb();
    },
 
    render: function () {
        var that = this;
        this.getAsyncData(function () {
            that.specialFunction();
            that.anotherSpecialFunction();
        });
    }
};
 
myObj.render();

我们需要为回调函数的执行保持对 myObj 对象上下文的引用。 调用 that.specialFunction()让我们能够维持作用域上下文并且正确执行我们的函数。 然而使用 Function.prototype.bind() 可以有更加简洁干净的方式:

render: function () {
 
    this.getAsyncData(function () {
 
        this.specialFunction();
 
        this.anotherSpecialFunction();
 
    }.bind(this));
 
}

ecma-262规范:

When the bind method is called with argument thisArg and zero or more args, it performs the following steps:

  • Let Target be the this value.

  • If IsCallable(Target) is false, throw a TypeError exception.

  • Let args be a new (possibly empty) List consisting of all of the argument values provided after thisArg in order.

  • Let F be BoundFunctionCreate(Target, thisArg, args).

  • ReturnIfAbrupt(F).

  • Let targetHasLength be HasOwnProperty(Target, "length").

  • ReturnIfAbrupt(targetHasLength).

  • If targetHasLength is true, then

    • Let targetLen be Get(Target, "length").

    • ReturnIfAbrupt(targetLen).

    • If Type(targetLen) is not Number, let L be 0.

    • Else,

      • Let targetLen be ToInteger(targetLen).

      • Let L be the larger of 0 and the result of targetLen minus the number of elements of args.

    • Else let L be 0.

    • Let status be DefinePropertyOrThrow(F, "length", PropertyDescriptor {[[Value]]: L, [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true}).

    • Assert: status is not an abrupt completion.

    • Let targetName be Get(Target, "name").

    • ReturnIfAbrupt(targetName).

    • If Type(targetName) is not String, let targetName be the empty string.

    • Perform SetFunctionName(F, targetName, "bound").

    • Return F.

Function.prototype.bind 在IE8及以下的版本中不被支持,以下MDN兼容旧浏览器的实现:

if (!Function.prototype.bind) {
  Function.prototype.bind = function (oThis) {
    if (typeof this !== "function") {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var aArgs = Array.prototype.slice.call(arguments, 1), 
        fToBind = this, 
        fNOP = function () {},
        fBound = function () {
          return fToBind.apply(this instanceof fNOP && oThis
                                 ? this
                                 : oThis || window,
                               aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();

    return fBound;
  };
}

上述算法和实际的实现算法还有许多其他的不同。

Function.prototype.apply()

apply() 方法在指定 this 值和参数(参数以数组或类数组对象的形式存在)的情况下调用某个函数。

/* min/max number in an array */
var numbers = [5, 6, 2, 3, 7];

/* using Math.min/Math.max apply */
var max = Math.max.apply(null, numbers); /* This about equal to Math.max(numbers[0], ...) or Math.max(5, 6, ..) */
var min = Math.min.apply(null, numbers);

Function.prototype.call()

call() 方法在使用一个指定的this值和若干个指定的参数值的前提下调用某个函数或方法。该方法的作用和 apply() 方法类似,只有一个区别,就是call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组。

Function.prototype.toString()

该 toString() 方法返回一个表示当前函数源代码的字符串。Function 对象覆盖了从 Object 继承来的 Object.prototype.toString 方法。函数的 toString 方法会返回一个表示函数源代码的字符串。具体来说,包括 function关键字,形参列表,大括号,以及函数体中的内容。

非标准方法

  • Function.prototype.isGenerator(): 断一个函数是否是一个生成器.

  • Function.prototype.toSource(): 返回函数的源代码的字符串表示。

闭包

作用域链

函数创建时,当一个函数创建后,它的作用域链会被创建此函数的作用域中可访问的数据对象填充。它的作用域链中会填入一个全局对象,该全局对象包含了所有全局变量。

函数执行时,会创建一个被称为“运行期上下文”内部对象,运行期上下文定义了函数的执行环境。每个运行期上下文都有自己的作用域链,用于标识符的解析。当运行期上下文被创建时,它的作用域链会被初始化为当前运行函数的[[scope]]所包含的对象。

这些值按照它们出现在函数中的顺序被复制到运行期上下文的作用域链中。它们共同组成了一个新的对象,叫“活动对象(activation object)”,该对象包含了函数的所有局部变量、命名参数、参数集合以及this,然后此对象会被推入作用域链的前端,当运行期上下文被销毁,活动对象也随之销毁。[注:]内部函数的执行上下文中this指向全局变量,见方法调用。

闭包的概念

一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数)。可以读取函数外部的变量,让这些执行环境始终保持在内存中。

function test(){
    for(var i = 0; i < 10 ; i++){
        setTimeout(function(){
            console.log(i)
        }, 0);
    }
}
test(); // 10...10(10个)

以上函数原本想输出0到9,结果输出了10个10。原因是test()执行时会创建一个运行时期的上下文,而setTimeout内部的函数会放在for循环队列之后,等到for循环执行完之后才开始执行。function(){console.log(i)}执行是首先会寻找函数内部的i标示符。此时找不到i,再寻找test中的i(闭包的概念:访问函数外的变量,这些变量只有等到闭包不使用才会被销毁),而此时的i值已经变为了10,所以十次执行都会输出10。解决这个问题可以在作用域链上增加一个节点,保存i变量。

function test(){
    for(var i = 0; i < 10 ; i++){
        (function(li){
            setTimeout(function(){
                console.log(li)
            }, 0);
        })(i);
    }
}
test(); // 0...9(0到9)

通过增加一个变量保存每次执行需要输出的i值,实现0到9的输出。

闭包的运用

JavaScript在es6之前没有模块的概念,使用闭包和匿名自执行函数实现模块化,使用闭包可以从外部放问函数内部的属性:

(function (){
    //内部属性
    var Number = 0;
    //方法
    var Utils = function(){};
    Util.porototype.display = function(){
        console.log(Number); //访问函数外部的变量
    };
    //返回
    return Util;
})()

sundway
1.2k 声望63 粉丝

Less is more...