JAVASCRIPT FUNCTIONS

本文是@堂主 对《Pro JavaScript with Mootools》一书的第二章函数部分知识点讲解的翻译。该书的做者 Mark Joseph Obcena 是 Mootools 库的做者和目前开发团队的 Leader。虽然本篇文章实际译于 2012 年初,但我的感受这部分对 Javascript 函数的基本知识、内部机制及 JavaScript 解析器的运行机制讲的很是明白,脉络也清楚,对初学者掌握 JavaScript 函数基础知识颇有好处。尤为可贵的是不一样于其余 JavaScript书籍讲述的都是分散的知识点,这本书的知识讲解是有清晰脉络的,按部就班。换句话说,这本书中的 JavaScript 知识是串起来的。前端

虽然这本《Pro JavaScript with Mootools》国内并未正式引进,但我依然建议有需求的能够从 Amazon 上自行买来看一下,或者网上搜一下 PDF 的版本(确实有 PDF 全版下载的)。我我的是当初花了近 300 大洋从 Amazon 上买了一本英文原版的,仍是更喜欢纸质版的阅读体验。这本书其实能够理解为 “基于 MooTools 实践项目的 JavaScript 指南”,总的脉络是 “JavaScript 基础知识 - 高级技巧 - MooTools 对原生 JavaScript 的改进”,很是值得一读。express

本篇译文字数较多,近 4 万字,我不知道能有几位看官有耐心看完。若是真有,且发现@堂主 一些地方翻译的不对或有优化建议,欢迎留言指教,共同成长。另外,非本土产技术类书籍,优先建议仍是直接读英文原版。编程

下面是译文正式内容:数组


JavaScript 最好的特点之一是其对函数的实现。不一样于其余编程语言为不一样情景提供不一样的函数类型,JavaScript 只为咱们提供了一种涵盖全部情景(如内嵌函数、匿名函数或是对象方法)的函数类型。 请不要被一个从外表看似简单的 JavaScript 函数迷惑——这个基本的函数声明如同一座城堡,隐藏着很是复杂的内部操做。咱们整章都是围绕着诸如函数结构、做用域、执行上下文以及函数执行等这些在实际操做中须要重点去考虑的问题来说述的。搞明白这些平时会被忽略的细节不但有助你更加了解这门语言,同时也会为你在解决异常复杂的问题时提供巨大帮助。闭包

关于函数(The Function)

最开始,咱们须要统一一些基本术语。从如今开始,咱们将函数(functions)的概念定义为“执行一个明确的动做并提供一个返回值的独立代码块”。函数能够接收做为值传递给它的参数(arguments),函数能够被用来提供返回值(return value),也能够经过调用(invoking)被屡次执行。app

// 一个带有2个参数的基本函数:
function add(one, two) {
    return one + two;
}

// 调用这个函数并给它2个参数:
var result = add(1, 42);
console.log(result); // 43

// 再次调用这个函数,给它另外2个参数
result = add(5, 20);
console.log(result); // 25

JavaScript 是一个将函数做为一等对象(first-class functions)的语言。一个一等对象的函数意味着函数能够储存在变量中,能够被做为参数传递给其余函数使用,也能够做为其余函数的返回值。这么作的合理性是由于在 JavaScript 中随处可见的函数其实都是对象。这门语言还容许你建立新的函数并在运行时改变函数的定义。编程语言

一种函数,多种形式(One Function, Multiple Forms)

虽然在 JavaScript 中只存在一种函数类型,但却存在多种函数形式,这意味着能够经过不一样的方式去建立一个函数。这些形式中最多见的是下面这种被称为函数字面量(function literal)的建立语法:ide

function Identifier(FormalParamters, ...) {
    FunctionBody
}

首先是一个 function 关键字后面跟着一个空格,以后是一个自选的标识符(identifier)用以说明你的函数;以后跟着的是以逗号分割的形参(formal parameters)列表,该形参列表处于一对圆括号中,这些形参会在函数内部转变为可用的局部变量;最后是一个自选的函数体(funciton body),在这里面你能够书写声明和表达式。请注意下面的说法是正确的:一个函数有多个可选部分。咱们如今还没针对这个问题进行详细的说明,由于对其的解答将贯穿本章。函数

注意:在本书的不少章节咱们都会看到 字面量(literal)这个术语。在JavaScript中,字面量是指在你代码中明肯定义的值。“mark”、1 或者 true 是字符串、数字和布尔字面量的例子,而 function() 和 [1, 2] 则分别是函数和数组字面量的例子。

在标识符(或后面咱们会见到的针对这个对象自己)后面使用调用操做符(invocation operator) “()”的被称为一个函数。同时调用操做符()也能够为函数传递实参(actual arguments)学习

注意:一个函数的形参是指在建立函数时圆括号中被声明的有命名的变量,而实参则是指函数被调用时传给它的值。

由于函数同时也是对象,因此它也具备方法和属性。咱们将在本书第三章更多的讨论函数的方法和属性,这里只须要先记住函数具备两个基本的属性:

  1. 名称(name):保存着函数标识符这个字符串的值
  2. 长度(length):这是一个关于函数形参数量的整数(若是函数没有形参,其 length 为 0)
    函数声明(Function Declaration)

采用基本语法,咱们建立第一种函数形式,称之为函数声明(function declaration)。函数声明是全部函数形式中最简单的一种,且绝大部分的开发者都在他们的代码中使用这种形式。下面的代码定义了一个新的函数,它的名字是 “add”:

// 一个名为“add”的函数
function add(a, b) {
    return a + b;
}

console.log(typeof add); // 'function'
console.log(add.name); // 'add'
console.log(add.length); // '2'
console.log(add(20, 5)); // '25'

在函数声明中须要赋予被声明的函数一个标识符,这个标识符将在当前做用域中建立一个值为函数的变量。在咱们的例子中,咱们在全局做用域中建立了一个 add 的变量,这个变量的 name 属性值为 add,这等价于这个函数的标识符,且这个函数的 length 为 2,由于咱们为其设置了 2 个形参。 由于 JavaScript 是基于词法做用域(lexically scoped)的,因此标识符被固定在它们被定义的做用域而不是语法上或是其被调用时的做用域。记住这一点很重要,由于 JavaScript 容许咱们在函数中定义函数,这种状况下关于做用域的规则可能会变得不易理解。

// 外层函数,全局做用域
function outer() {

    // 内层函数,局部做用域
    function inner() {
        // ...
    }

}

// 检测外层函数
console.log(typeof outer); // 'function'

// 运行外层函数来建立一个新的函数
outer();

// 检测内层函数
console.log(typeof inner); // 'undefined'

在这个例子中,咱们在全局做用域中建立了一个 outer 变量并为之赋值为 outer 函数。当咱们调用它时,它建立了一个名为 inner 的局部变量,这个局部变量被赋值为 inner 函数,当咱们使用 typeof 操做符进行检测的时候,在全局做用域中 outer 函数是能够被有效访问的,但 inner 函数却只能在 outer 函数内部被访问到 —— 这是由于 inner 函数只存在于一个局部做用域中。 由于函数声明同时还建立了一个同名的变量做为他的标识符,因此你必须肯定在当前做用域不存在其余同名标识符的变量。不然,后面同名变量的值会覆盖前面的:

// 当前做用域中的一个变量
var items = 1;

// 一个被声明为同名的函数
function items() {
    // ...
};

console.log(typeof items); // 'function' 而非 'number'

咱们过一会会讨论更多关于函数做用域的细节,如今咱们看一下另一种形式的函数。

函数表达式(Function Expression)

下面要说的函数形式具有必定的优点,这个优点在于函数被储存在一个变量中,这种形式的函数被称为函数表达式(funciton expression)。不一样于明确的声明一个函数,这时的函数以一个变量返回值的面貌出现。下面是一个和上面同样的add函数,但此次咱们使用了函数表达式:

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

console.log(typeof add); // 'function'
console.log(add.name); // '' 或 'anonymous'
console.log(add.length); // '2'
console.log(add(20, 5)); // '25'

这里咱们建立了一个函数字面量做为 add 这个变量的值,下面咱们就可使用这个变量来调用这个函数,如最后的那个语句展现的咱们用它来求两个数的和。你会注意到它的 length 属性和对应的函数声明的 length 属性是同样,可是 name 属性却不同。在一些 JavaScript 解析器中,这个值会是空字符串,而在另外一些中则会是 “anonymous”。发生这种状况的缘由是咱们并未给一个函数字面量指定一个标识符。在 JavaSrcipt 中,一个未使用明确标识符的函数被称为一个匿名函数(anonymous)。 函数表达式的做用域规则不一样于函数声明的做用域规则,这是由于其取决于被赋值的那个变量的做用域。记住在 JavaScript 中,由关键字 var 声明的变量是一个局部变量,而忽略了这个关键字则会建立一个全局变量。

// 外层函数,全局做用域
var outer = function() {

    // 内层函数,局部做用域
    var localInner = function() {
        // ...
    }; 

    // 内层函数,全局做用域
    globalInner = function() {
        // ...
    };

}

// 检测外层函数
console.log(typeof outer); // 'function'

// 运行外层函数来建立一个新的函数
outer();

// 检测新的函数
console.log(typeof localInner); // 'undefined'
console.log(typeof globalInner); // 'function'

outer 函数被定义在全局做用域中,这是由于虽然咱们使用了 var 关键字,但其在当前应用中处于最高层级。在这个函数内部有另外的2个函数:localInner 和 globalInner。localInner 函数被赋值给一个局部变量,在 outer 外部没法访问它。而 globalIner 则因在定义时缺失 var 关键字,其结果是这个变量及其引用的函数都处于全局做用域中。

命名的函数表达式(Named Function Expression)

虽然函数表达式常常被书写为采用匿名函数的形式,但你依然能够为这个匿名函数赋予一个明确的标识符。这个函数表达式的变种被称为一个命名的函数表达式(named function expression)

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

console.log(typeof add); // 'function'
console.log(add.name); // 'add'
console.log(add.length); // '2'
console.log(add(20, 5)); //'25'

这个例子和采用匿名函数方式的函数表达式是同样的,但咱们为函数字面量赋予了一个明确的标识符。和前一个例子不一样,这时的 name 属性的值是 “add”,这个值同咱们为其赋予的那个标识符是一致的。JavaScript 容许咱们为匿名函数赋予一个明确的标识符,这样就能够在这个函数内部引用其自己。你可能会问为何咱们须要这个特征,下面让咱们来看两个例子:

var myFn = function() {
    // 引用这个函数
    console.log(typeof myFn);
};
myFn(); // 'function'

上面的这个例子,myFn 这个函数能够轻松的经过它的变量名来引用,这是由于它的变量名在其做用域中是有效的。不过,看一下下面的这个例子:

// 全局做用域
var createFn = function() {

    // 返回函数
    return function() {
        console.log(typeof myFn);
    };

};

// 不一样的做用域
(function() {

    // 将createFn的返回值赋予一个局部变量
    var myFn = createFn();

    // 检测引用是否可行
    myFn(); // 'undefined'

})();

这个例子可能有点复杂,咱们稍后会讨论它的细节。如今,咱们只关心函数自己。在全局做用域中,咱们建立了一个 createFn 函数,它返回一个和前面例子同样的 log 函数。以后咱们建立了一个匿名的局部做用域,在其中定义了一个变量 myFn,并把 createFn 的返回值赋予这个变量。 这段代码和前面那个看起来很像,但不一样的是咱们没使用一个被明确赋值为函数字面量的变量,而是使用了一个由其余函数产生的返回值。并且,变量 myFn 一个不一样的局部做用域中,在这个做用域中访问不到上面 createFn 函数做用域中的返回值。所以,在这个例子中,log 函数不会返回 “function” 而是会返回一个 “undefined”。 经过为匿名函数设置一个明确的标识符,即便咱们经过持有它的变量访问到它,也能够去引用这个函数自身。

// 全局做用域
var createFn = function() {

    // 返回函数
    return function myFn() {
        console.log(typeof myFn);
    };

};

// 不一样的做用域
(function() {

    // 将createFn的返回值赋予一个局部变量
    var myFn = createFn();

    // 检测引用是否可行
    myFn(); // 'function'

})();

添加一个明确的标识符相似于建立一个新的可访问该函数内部的变量,使用这个变量就能够引用这个函数自身。这样使得函数在其内部调用自身(用于递归操做)或在其自己上执行操做成为可能。 一个命名了的函数声明同一个采用匿名函数形式的函数声明具备相同的做用域规则:引用它的变量做用域决定了这个函数是局部的或是全局的。

// 一个有着不一样标识符的函数
var myFn = function fnID() {
    console.log(typeof fnID);
};

// 对于变量
console.log(typeof myFn); // 'function'

// 对于标识符
console.log(typeof fnID); // 'undefined'

myFn(); // 'function'

这个例子显示了,经过变量 myFn 能够成功的引用函数,但经过标识符 fnID 却没法从外部访问到它。可是,经过标识符却能够在函数内部引用其自身。

自执行函数(Single-Execution Function)

咱们在前面介绍函数表达式时曾接触过匿名函数,其还有着更普遍的用处。其中最重要的一项技术就是使用匿名函数建立一个当即执行的函数——且不须要事先把它们先存在变量里。这种函数形式咱们称之为自执行函数(single-execution function)。

// 建立一个函数并当即调用其自身
(function() {

    var msg = 'Hello World';
    console.log(msg);

})();

这里咱们建立了一个函数字面量并把它包裹在一对圆括号中。以后咱们使用函数调用操做符()来当即执行这个函数。这个函数并未储存在一个变量里,或是任何针对它而建立的引用。这是个“一次性运行”的函数:创造它,执行它,以后继续其余的操做。

要想理解自执行函数是如何工做的,你要记住函数都是对象,而对象都是值。由于在 JavaScript 中值能够被当即使用而无需先被储存在变量里,因此你能够在一对圆括号中插入一个匿名函数来当即运行它。

可是,若是咱们像下面这么作:

// 这么写会被认为是一个语法错误
function() {

    var msg = 'Hello World';
    console.log(msg);

}();

当 JavaScript 解析器遇到这行代码会抛出一个语法错误,由于解析器会把这个函数当成一个函数声明。这看起来是一个没有标识符的函数声明,而由于函数声明的方式必需要在 function 关键字以后跟着一个标识符,因此解析器会抛出错误。

咱们把函数放在一对圆括号中来告诉解析器这不是一个函数声明,更准确的说,咱们建立了一个函数并当即运行了它的值。由于咱们没有一个可用于调用这个函数的标识符,因此咱们须要把函数放在一对圆括号中以即可以建立一个正确的方法来调用到这个函数。这种包围在外层的圆括号应该出如今咱们没有一个明确的方式来调用函数的时候,好比咱们如今说的这种自执行函数。

注意:执行操做符()能够既能够放在圆括号外面,也能够放在圆括号里面,如:(function() {…}())。但通常状况下你们更习惯于把执行操做符放在外面。

自执行函数的用处不少,其中最重要的一点是为变量和标识符创造一个受保护的局部做用域,看下面的例子:

// 顶层做用域
var a = 1;

// 一个由自执行函数建立的局部做用域
(function() {

    //局部做用域
    var a = 2;

})();

console.log(a); // 1

这里,外面先在顶层做用域建立了一个值为 1 的变量 a,以后建立一个自执行函数并在里面再次声明一个 a 变量并赋值为 2。由于这是一个局部做用域,因此外面的顶层做用域中的变量 a 的值并不会被改变。

这项技术目前很流行,尤为对于 JavaScript 库(library)的开发者,由于局部变量进入一个不一样做用域时须要避免标识符冲突。

另外一种自执行函数的用处是经过一次性的执行来为你提供它的返回值:

// 把一个自执行函数的返回值保存在一个变量里
var name = (function(name) {

    return ['Hello', name].join(' ');

})('Mark');

console.log(name); // 'Hello Mark'

别被这段代码迷惑到:咱们这里不是建立了一个函数表达式,而是建立了一个自执行函数并当即执行它,把它的返回值赋予变量 name。

自执行函数另外一个特点是能够为它配置标识符,相似一个函数声明的作法:

(function myFn() {

    console.log(typeof myFn); // 'function'

})();

console.log(myFn); // 'undefined'

虽然这看起来像是一个函数声明,但这倒是一个自执行函数。虽然咱们为它设置了一个标识符,但它并不会像函数声明那样在当前做用域建立一个变量。这个标识符使得你能够在函数内部引用其自身,而没必要另外在当前做用域再新建一个变量。这对于避免覆盖当前做用域中已存在的变量尤为有好处。

同其余的函数形式同样,自执行函数也能够经过执行操做符来传递参数。经过在函数内部把函数的标志符做为一个变量并把该函数的返回值储存在该变量中,咱们能够建立一个递归的函数。

var number = 12;

var numberFactorial = (function factorial(number) {
     return (number === 0) ? 1 : number * factorial(number - 1);
})(number);

console.log(numberFactorial); //479001600

函数对象(Function Object)

最后一种函数形式,就是函数对象(funciton object),它不一样于上面几种采用函数字面量的方式,这种函数形式的语法以下:

// 一个函数对象
new Function('FormalArgument1', 'FormalArgument2',..., 'FunctionBody');

这里,咱们使用 Function 的构造函数建立了一个新的函数并把字符串做为参数传递给它。前面的已经命名的参数为新建函数对象的参数,最后一个参数为这个函数的函数体。

注意:虽然这里咱们把这种形式成为函数对象,但请记住其实全部的函数都是对象。咱们在这里采用这个术语的目的是为了和函数字面量的方式进行区分。

下面咱们采用这种形式建立一个函数:

var add = new Function('a', 'b', 'return a + b;');

console.log(typeof add); // 'function'
console.log(add.name); // '' 或 'anonymous'
console.log(add.length); // '2'
console.log(add(20, 5)); // '25'

你可能会发现这种方式比采用函数字面量方式建立一个匿名函数要更简单。和匿名函数同样,对其检测 name 属性会获得一个空的字符串或 anonymous。在第一行,咱们使用 Function 的构造函数建立了一个新的函数,并赋值给变量 add。这个函数接收 2 个参数 a 和 b,会在运行时将 a 和 b 相加并把相加结果作做为函数返回值。

使用这种函数形式相似于使用 eval:最后的一个字符串参数会在函数运行时做为函数体里的代码被执行。

注意:你不是必须将命名的参数做为分开的字符串传递,Function 构造函数也容许一个字符串里包含多个以逗号分隔的项这种传参方式。好比:new Function(‘a, b’, ‘return a + b;’);

虽然这种函数形式有它的用处,但其相比函数字面量的方式存在一个显著的劣势,就是它是处在全局做用域中的:

// 全局变量
var x = 1;

// 局部做用域
(function() {

    // 局部变量
    var x = 5;
    var myFn = new Function('console.log(x)');
    myFn(); // 1, not 5

})();

虽然咱们在独立的做用域中定义了一个局部变量,但输出结果倒是 1 而非 5,这是由于 Function 构造函数是运行在全局做用域中。

参数(Arguments)

全部函数都能从内部访问到它们的实参。这些实参会在函数内部变为一个个局部变量,其值是函数在调用时传进来的那个值。另外,若是函数在调用时实际使用的参数少于它在定义时肯定的形参,那么那些多余的未用到的参数的值就会是 undefined。

var myFn = function(frist, second) {
    console.log('frist : ' + frist);
    console.log('second : ' + second);
};

myFn(1, 2);
// first : 1
// second : 2

myFn('a', 'b', 'c');
// first : a
// second : b

myFn('test');
// first : test
// second : undefined

由于 JavaScript 容许向函数传递任意个数的参数,这也同时为咱们提供了一个方式来判断函数在调用时使用的实参和函数定义时的形参的数量是否相同。这个检测的方式经过 arguments 这个对象来实现,这个对象相似于数组,储存着该函数的实参:

var myFn = function(frist, second) {
    console.log('length : ' + arguments.length);
    console.log('frist : ' + arguments[0]);
};

myFn(1, 2);
// length : 2
// frist : 1

myFn('a', 'b', 'c');
// length : 3
// frist : a

myFn('test');
// length : 2
// frist : test

arguments 对象的 length 属性能够显示咱们传递函数的实参个数。对实参的调用能够对 arguments 对象使用相似数组的下标法:arguments[0] 表示传递的第一个实参,arguments[1] 表示第二个实参。

使用 arguments 对象取代有名字的参数,你能够建立一个能够对不一样数量参数进行处理的函数。好比可使用这种技巧来帮助咱们改进前面的那个 add 函数,使得其能够对任意数量的参数进行累加,最后返回累加的值:

var add = function(){
    var result = 0,
        len = arguments.length;

    while(len--) result += arguments[len];
    console.log(result);
}; 

add(15); // 15
add(31, 32, 92); // 135
add(19, 53, 27, 41, 101); // 241

arguments 对象有一个很大的问题须要引发你的注意:它是一个可变的对象,你能够改变其内部的参数值甚至是把它整个变成另外一个对象:

var rewriteArgs = function() {
    arguments[0] = 'no';
    console.log(arguments[0]);
};

rewriteArgs('yes'); // 'no'

var replaceArgs = function() {
    arguments = null;
    console.log(arguments === null);
};

replaceArgs(); // 'true'

上面第一个函数向咱们展现了若是重置一个参数的值;后面的函数向咱们展现了如何总体更改一个 arguments 对象。对于 arguments 对象来讲,惟一的固定属性就是 length 了,即便你在函数内部动态的增长了 arguments 对象里的参数,length 依然只显示函数调用时赋予的实参的数量。

var appendArgs = function() {
    arguments[2] = 'three';
    console.log(arguments.length);
};

appendArgs('one', 'two'); // 2

当你写代码的时候,请确保没有更改 arguments 内的参数值或覆盖这个对象。

对于 arguments 对象还有另外一个属性值:callee,这是一个针对该函数自身的引用。在前面的代码中咱们使用函数的标识符来实如今函数内部引用其自身,如今咱们换一种方式,使用 arguments.callee:

var number = 12;

var numberFactorial = (function(number) {
    return (number === 0) ? 1 : number * arguments.callee(number - 1);
})(number);

console.log(numberFactorial); //479001600

注意这里咱们建立的是一个匿名函数,虽然咱们没有函数标识符,但依然能够经过 arguments.callee 来准确的引用其自身。建立这个属性的意图就是为了能在没有标识符可供使用的时候(或者就算是有一个标识符时也可使用 callee)来提供一个有效方式在函数内部引用其自身。

虽然这是一个颇有用的属性,但在新的 ECMAScript 5 的规范中,arguments.callee 属性却被废弃了。若是使用 ES5 的严格模式,该属性会引发一个报错。因此,除非真的是有必要,不然轻易不要使用这个属性,而是用咱们前面说过的方法使用标识符来达到一样的目的。

虽然 JavaScript 容许给函数传递不少参数,但却并未提供一个设置参数默认值的方法,不过咱们能够经过判断参数值是不是 undefined 来模拟配置默认值的操做:

var greet = function(name, greeting) {

    // 检测参数是不是定义了的
    // 若是不是,就提供一个默认值
    name = name || 'Mark';
    greeting = greeting || 'Hello';

    console.log([greeting, name]).join(' ');

};

greet('Tim', 'Hi'); // 'Hi Tim'
greet('Tim');       // 'Hello Tim'
greet();            // 'Hello Mark'

由于未在函数调用时赋值的参数其值为 undefined,而 undefined 在布尔判断时返回的是 false,因此咱们可使用逻辑或运算符 || 来为参数设置一个默认值。

另一点须要特别注意的是,原生类型的参数(如字符串和整数)是以值的方式来传递的,这意味着这些值的改变不会对外层做用域引发反射。不过,做为参数使用的函数和对象,则是以他们的引用来传递,在函数做用域中的对参数的任何改动都会引发外层的反射:

var obj = {name : 'Mark'};

var changeNative = function(name) {
    name = 'Joseph';
    console.log(name);
};

changeNative(obj.name); // 'Joseph'

console.log(obj.name); // 'Mark'

var changeObj = function(obj) {
    obj.name = 'joseph';
    console.log(obj.name);
};

changeObj(obj); // 'Joseph'

console.log(obj.name); // 'Joseph'

第一步咱们将 obj.name 做为参数传给函数,由于其为一个原生的字符串类型,其传递的是它值的拷贝(储存在栈上),因此在函数内部对其进行改变不会对外层做用域中的 obj 产生影响。而接下来咱们把 obj 对象自己做为一个参数传递,由于函数和对象等在做为参数进行传递时其传递的是对自身的引用(储存在堆上),因此局部做用域中对其属性值的任何更改都会当即反射到外层做用域中的 obj 对象。

最后,你可能会说以前我曾提到过 arguments 对象是类数组的。这意味着虽然 arguments 对象看起来像数组(能够经过下标来用于),但它没有数组的那些方法。若是你喜欢,你能够用数组的 Array.prototype.slice 方法把 arguments 对象转变为一个真正的数组:

var argsToArray = function() {
    console.log(typeof arguments.callee); // 'function'
    var args = Array.prototype.slice.call(arguments);
    console.log(typeof arguments.callee); // 'undefined'
    console.log(typeof arguments.slice); // 'function'
};

argsToArray();

返回值(Return Values)

Return 关键字用来为函数提供一个明确的返回值,JavaScript 容许在函数内部书写多个 return 关键字,函数会再其中一个执行后当即退出。

var isOne = function(number) {
   if (number === 1) return true;

   console.log('Not one ..');
   return false;
};

var one = isOne(1);
console.log(one); // true

var two = isOne(2); // Not one ..
console.log(two); // false

在这个函数第一次被引用时,咱们传进去一个参数 1,由于咱们在函数内部先作了一个条件判断,当前传入的参数1使得该条件判断语句返回 true,因而 return true 代码会被执行,函数同时当即中止。在第二次引用时咱们传进去的参数 2 不符合前面的条件判断语句要求,因而函数会一直执行到最后的 return false代码。

在函数内部设置多个 return 语句对于函数分层执行是颇有好处的。这同时也被广泛应用于在函数运行最开始对必须的变量进行检测,若有不符合的状况则当即退出函数执行,这既能节省时间又能为咱们提供一个错误提示。下面的这个例子就是一段从 DOM 元素中获取其自定义属性值的代码片断:

var getData = function(id) {
    if (!id) return null;
    var element = $(id);
    if (!element) return null;
    return element.get('data-name');
};

console.log(getData()); // null
console.log(getData('non existent id')); // null
console.log(getData('main')); // 'Tim'

组后关于函数返回值要提醒各位的一点是:不论你但愿与否,函数老是会提供一个返回值。若是未显示地设置 return 关键字或设置的 return 未有机会执行,则函数会返回一个 undefined。

函数内部(Function Internals)

咱们前面讨论过了函数形式、参数以及函数的返回值等与函数有关的核心话题,下面咱们要讨论一些代码之下的东西。在下面的章节里,咱们会讨论一些函数内部的幕后事务,让咱们一块儿来偷窥下当 JavaScript 解析器进入一个函数时会作些什么。咱们不会陷入针对细节的讨论,而是关注那些有利于咱们更好的理解函数概念的那些重要的点。

有些人可能会以为在最开始接触 JavaScript 的时候,这门语言在某些时候会显得不那么严谨,并且它的规则也不那么好理解。了解一些内部机制有助于咱们更好的理解那些看起来随意的规则,同时在后面的章节里会看到,了解 JavaScript 的内部工做机制会对你书写出可靠的、健壮的代码有着巨大的帮助。

注意:JavaScript 解析器在现实中的工做方式会因其制造厂商不一样而不相一致,因此咱们下面要讨论的一些解析器的细节可能不全是准确的。不过 ECMAScript 规范对解析器应该如何执行函数提供了基本的规则描述,因此对于函数内部发生的事,咱们是有着一套官方指南的。

可执行代码和执行上下文(Executable Code and Execution Contexts)

JavaScript 区分三种可执行代码:

  • 全局代码(Global code)是指出如今应用代码中顶层的代码。
  • 函数代码(Function code)是指在函数内部的代码或是在函数体以前被调用的代码。
  • Eval 代码(Eval code)是指被传进 eval 方法中并被其执行的代码。

下面的例子展现了这三种不一样的可执行代码:

// 这是全局代码
var name = 'John';
var age = 20;

function add(a, b) {
    // 这是函数代码
    var result = a + b;
    return result;
}

(function() {
    // 这是函数代码
    var day = 'Tuesday';
    var time = function() {
        // 这仍是函数代码
        // 不过和上面的代码在做用域上是分开的
        return day;
    };
})();

// 这是eval代码
eval('alert("yay!");');

上面咱们建立的 name、age 以及大部分的函数都在顶层代码中,这意味着它们是全局代码。不过,处于函数中的代码是函数代码,它被视为同全局代码是相分隔的。函数中内嵌的函数,其内部代码同外部的函数代码也被视为是相分隔的。

那么为何咱们须要对 JavaScript 中的代码进行分类呢?这是为了在解析器解析代码时可以追踪到其当前所处的位置,JavaScript 解析器采用了一个被称为执行上下文(execution context)的内部机制。在处理一段脚本的过程当中,JavaScript 会建立并进入不一样的执行上下文,这个行为自己不只保存着它运行到这个函数当前位置所通过的轨迹,同时还储存着函数正常运行所须要的数据。

每一个 JavaScript 程序都至少有一个执行上下文,一般咱们称之为全局执行上下文(global execution context),当一个 JavaScript 解析器开始解析你的程序的时候,它首先“进入”全局执行上下文并在这个执行上下文环境中处理代码。当它遇到一个函数,它会建立一个新的执行上下文并进入这个上下文利用这个环境来执行函数代码。当函数执行完毕或者遇到一个 return 结束以后,解析器会退出当先的执行上下文并回到以前所处的那个执行上下文环境。

这个看起来不是很好理解,咱们下面用一个简单的例子来把它理清:

var a = 1;

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

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

add(a, 2);

call(1, 2);

这段简单的代码不单足够帮助咱们来理解上面说的事情,同时仍是一个很好的例子来展现 JavaScript 是如何建立、进入并离开一个执行上下文的。让咱们一步一步来分析:

当程序开始执行,Javascript 解析器首先进入全局执行上下文并在这里解析代码。它会先建立三个变量 a、add、callAdd,并分别为它们赋值为数字 一、一个函数和另外一个函数。

解析器遇到了一个针对 add 函数的调用。因而解析器建立了一个新的执行上下文,进入这个上下文,计算 a + b 表达式的值,以后返回这个表达式的值。当这个值被返回后,解析器离开了这个它新建立的执行上下文,把它销毁掉,从新回到全局执行上下文。

接下来解析器遇到了另外一个函数调用,此次是对 callAdd 的调用。像第二步同样,解析器会新建立一个执行上下文,并在它解析 callAdd 函数体中的代码以前先进入这个执行上下文。当它对函数体内的代码进行处理的时候,遇到了一个新的函数调用——此次是对 add 的调用,因而解析器会再新建一个执行上下文并进入这里。此时,咱们已有了三个执行上下文:一个全局执行上下文、一个针对 callAdd 的执行上下文,一个针对 add 函数的执行上下文。最后一个是当前被激活的执行上下文。当 add 函数调用执行完毕后,当前的执行上下文会被销毁并回到 callAdd 的执行上下文中,callAdd 的执行上下文中的运行结果也是返回一个值,这通知解析器退出并销毁当前的执行上下文,从新回到全局执行上下文中。

执行上下文的概念对于一个在代码中不会直接面对它的前端新人来讲,多是会有一点复杂,这是能够理解的。你此时可能会问,那既然咱们在编程中不会直接面对执行上下文,那咱们又为何要讨论它呢?

答案就在于执行上下文的其余那些用途。我在前面提到过 JavaScript 解析器依靠执行上下文来保存它运行到当前位置所通过的轨迹,此外一些程序内部相互关联的对象也要依靠执行上下文来正确处理你的程序。

变量和变量初始化(Variables and Variable Instantition)

这些内部的对象之一就是变量对象(variable object)。每个执行上下文都拥有它本身的变量对象用来记录在当前上下文环境中定义的变量。

在 JavaScript 中建立变量的过程被称为变量初始化(variable instantition)。由于 JavaScript 是基于词法做用域的,这意味着一个变量所处的做用域由其在代码中被实例化的位置所决定。惟一的例外是不采用关键字 var 建立的变量是全局变量。

var fruit = 'banana';

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

add(1, 2);

在这个代码片断中,变量 fruit 和函数 add 处于全局做用域中,在整个脚本中都能被访问到。而对于变量 localResult、a、b 则是局部变量,只能在函数内部被访问到。而变量 globalResult 由于在声明时缺乏关键字 var,因此它会成为一个全局变量。

当 JavaScript 解析器进入一个执行上下文中,首先要作的就是变量初始化操做。解析器首先会在当前的执行上下文中建立一个 variable 对象,以后在当前上下文环境中搜索 var 声明,建立这些变量并添加进以前建立的 variable 对象中,此时这些变量的值都被设置为 undefined。让咱们审视一下咱们的演示代码,咱们能够说变量 fruit 和 add 经过 variable 对象在当前执行上下文中被初始化,而变量 localResult、a、b 则经过 variable 对象在 add 函数的上下文空间中被初始化。而 globalResult 则是一个须要被特别注意的变量,这个咱们一会再来讨论它。

关于变量初始化有很重要的一点须要咱们去记住,就是它同执行上下文是紧密结合的。回忆一下,前面咱们对 JavaScript 划分了三种不一样的执行代码:全局代码、函数代码和 eval 代码。同理,咱们也能够说存在着三种不一样的执行上下文:全局执行上下文、函数执行上下文、eval 执行上下文。由于变量初始化是经过处于执行上下文中的 variable 对象实现的,进而能够说也存在着三种类型的变量:全局变量、处于函数做用域中的变量以及来自 eval 代码中的变量。

这为咱们引出了不少人对这门语言感受困惑的那些问题中一个:JavaScript 没有块级做用域。在其余的类 C 语言中,一对花括号中的代码被称为一个块(block),块有着本身独立的做用域。由于变量初始化发生在执行上下文这一层级中,因此在当前执行上下文中任意位置被初始化的变量,在这整个上下文空间中(包括其内部的其余子上下文空间)都是可见的:

var x = 1;

if (false) {
    var y =2;
}

console.log(x); // 1
console.log(y); // undefined

在拥有块级做用域的语言中,console.log(y) 会抛出一个错误,由于条件判断语句中的代码是不会被执行的,那么变量 y 天然也不会被初始化。但在 JavaScript 中这并不会抛出一个错误,而是告诉咱们 y 的值是 undefined,这个值是一个变量已经被初始化但还未被赋值时所具备的默认值。这个行为看起来挺有意思,不是么?

不过,若是咱们还记得变量初始化是发生在执行上下文这一层级中,咱们就会明白这种行为其实正是咱们所指望的。当 JavaScript 开始解析上面的代码块的时候,它首先会进入全局执行上下文,以后在整个上下文环境中寻找变量声明并初始化它们,以后把他们加入 variable 对象中去。因此咱们的代码其实是像下面这样被解析的:

var x;
var y;

x = 1;

if (false) {
    y = 2;
}

console.log(x); // 1
console.log(y); // undefined

一样的在上下文环境中的初始化也适用于函数:

function test() {
    console.log(value); // undefined
    var value = 1;
    console.log(value); // 1
}

test();

虽然咱们对变量的赋值操做是在第一行 log 语句以后才进行的,但第一行的 log 仍是会给咱们返回一个 undefined 而非一个报错。这是由于变量初始化是先于函数内其余任何执行代码以前进行的。咱们的变量会在第一时间被初始化并被暂时设置为 undefined,其到了第二行代码被执行时才被正式赋值为 1。因此说将变量初始化的操做放在代码或函数的最前面是一个好习惯,这样能够保证在当前做用域的任何位置,变量都是可用的。

就像你见到的,建立变量的过程(初始化)和给变量赋值的过程(声明)是被 JavaScript 解析器分开执行的。咱们回到上一个例子:

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

add(1, 2);

在这个代码片断中,变量 localResult 是函数的一个局部变量,可是 globalResult 倒是一个全局变量。对于这个现象最多见的解释是由于在建立变量时缺乏关键字 var 因而变量成了全局的,但这并非一个靠谱的解释。如今咱们已经知道了变量的初始化和声明是分开进行的,因此咱们能够从一个解析器的视角把上面的代码重写:

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

add(1, 2);

变量 localResult 会被初始化并会在当前执行上下文的 variable 对象中建立一个针对它的引用。当解析器看到 “localResult = a + b;” 这一行时,它会在当前执行上下文环境的 variable 对象中检查是否存在一个 localResult 对象,由于如今存在这么一个变量,因而这个值(a + b)被赋给了它。然而,当解析器遇到 “globalResult = localResult;” 这一行代码时,它不论在当前环境的 variable 对象中仍是在更上一级的执行上下文环境(对本例来讲是全局执行上下文)的 variable 对象中都没找到一个名为 globalResult 的对象引用。由于解析器始终找不到这么一个引用,因而它认为这是一个新的变量,并会在它所寻找的最后一层执行上下文环境——总会是全局执行上下文——中建立这么一个新的变量。因而, globalResult 最后成了一个全局变量。

做用域和做用域链(Scoping and Scope Chain)

在执行上下文的做用域中查找变量的过程被称为标识符解析(indentifier resolution),这个过程的实现依赖于函数内部另外一个同执行上下文相关联的对象——做用域链(scope chain)。就像它的名字所蕴含的那样,做用域链是一个有序链表,其包含着用以告诉 JavaScript 解析器一个标识符到底关联着哪个变量的对象。

每个执行上下文都有其本身的做用域链,该做用域链在解析器进入该执行上下文以前就已经被建立好了。一个做用域链能够包含数个对象,其中的一个即是当前执行上下文的 variable 对象。咱们看一下下面的简单代码:

var fruit = 'banana';
var animal = 'cat';

console.log(fruit); // 'banana'
console.log(animal); // 'cat'

这段代码运行在全局执行上下文中,因此变量 fruit 和 animal 储存在全局执行上下文的 variable 对象中。当解析器遇到 “console.log(fruit);” 这段代码,它看到了标识符 fruit 并在当前的做用域链(目前只包含了一个对象,就是当前全局执行上下文的 variable 对象)中寻找这个标识符的值,因而接下来解析器发现这个变量有一个内容为 “banana” 的值。下一行的 log 语句的执行过程同这个是同样的。

同时,全局执行上下文中的 variable 对象还有另一个用途,就是被用作 global 对象。解析器对 global 对象有其自身的内部实现方式,但依然能够经过 JavaScript 在当前窗口中自身的window对象或当前 JavaScript 解析器的 global 对象来访问到。全部的全局对象实际上都是 global 对象中的成员:在上面的例子中,你能够经过 window.fruit、global.fruit 或 window.animal、global.animal 来引用变量 fruit 和 animal。global 对象对全部的做用域链和执行上下文均可用。在咱们这个只是全局代码的例子里,global 对象是这个做用域链中仅有的一个对象。

好吧,这使得函数变得更加不易理解了。除了 global 对象以外,一个函数的做用域链还包含拥有其自身执行上下文环境的变量对象。

var fruit = 'banana';
var animal = 'cat';

function sayFruit() {
    var fruit = 'apple';

    console.log(fruit); // 'apple'
    console.log(animal); // 'cat'
}

console.log(fruit); // 'banana'
console.log(animal); // 'cat'

sayFruit();

对于全局执行上下文中的代码,fruit 和 animal 标识符分别指向 “banana” 和 “cat” 值,由于它们的引用是被存储在执行上下文的 variable 对象中(也就是 global 对象中)的。不过,在 sayFruit 函数里标识符 fruit 对应的倒是另外一个值 —— “apple”。由于在这个函数内部,声明并初始化了另外一个变量 fruit。由于当前执行上下文中的 variable 对象在做用域链中处在更靠前的位置(相比全局执行上下文中的 variable 对象而言),因此 JavaScript 解析器会知道如今处理的应该是一个局部变量而非全局变量。

由于 JavaScript 是基于词法做用域的,因此标识符解析还依赖于函数在代码中的位置。一个嵌在函数中的函数,能够访问到其外层函数中的变量:

var fruit = 'banana';

function outer() {
    var fruit = 'orange';

    function inner() {
        console.log(fruit); // 'orange'
    }

    inner();
}

outer();

inner 函数中的变量 fruit 具备一个 “orange” 的值是由于这个函数的做用域链不仅仅包含了它本身的 variable 对象,同时还包含了它被声明时所处的那个函数(这里指 outer 函数)的 variable 对象。当解析器遇到 inner 函数中的标识符 fruit,它首先会在做用域链最前面的 inner 函数的 variable 对象中寻找与之同名的标识符,若是没有,则去下一个 variable 对象(outer 函数的)中去找。当解析器找到了它须要的标识符,它就会停在那并把 fruit 的值设置为 “orange”。

不过要注意的是,这种方式只适用于采用函数字面量建立的函数。而采用构造函数方式建立的函数则不会这样:

var fruit = 'banana';

function outer() {
    var fruit = 'orange';

    var inner = new Function('console.log(fruit);');

    inner(); // 'banana'
}

outer();

在这个例子里,咱们的 inner 函数不能访问 outer 函数里的局部变量 fruit,因此 log 语句的输出结果是 “banana” 而非 “orange”。发生这种状况的缘由是由于采用 new Function() 建立的函数其做用域链仅含有它本身的 variable 对象和 global 对象,而其外围函数的 variable 对象都不会被加入到它的做用域链中。由于在这个采用构造函数方式新建的函数自身的 variable 对象中没有找到标识符 fruit,因而解析器去后面一层的 global 对象中查找,在这里面找到了一个 fruit 标识符,其值为 “banana”,因而被 log 了出来。

做用域链的建立发生在解析器建立执行上下文以后、变量初始化以前。在全局代码中,解析器首先会建立一个全局执行上下文,以后建立做用域链,以后继续建立全局执行上下文的 variable 对象(这个对象同时也成为 global 对象),再以后解析器会进行变量初始化,以后把储存了这些初始化了的变量的 variable 对象加入到前面建立的做用域链中。在函数代码中,发生的状况也是同样的,惟一不一样的是 global 对象会首先被加入到函数的做用域链,以后把其外围函数的的 variable 对象加入做用域链,最后加入做用域链的是该函数本身的 variable 对象。由于做用域链在技术角度来说属于逻辑上的一个栈,因此解析器的查找操做所遵循的是从栈上第一个元素开始向下顺序查找。这就是为何咱们绝大部分的局部变量是最后才被加入到做用域链却在解析时最早被找到的缘由。

闭包(Closures)

JavaScript 中函数是一等对象以及函数能够引用到其外围函数的变量使得 JavaScript 相比其余语言具有了一个很是强大的功能:闭包(closures)。虽然增长这个概念会使对 JavaScript 这部分的学习和理解变得更加困难,但必须认可这个特点使函数的用途变得很是强大。在前面咱们已经讨论过了 JavaScript 函数的内在工做机制,这正好能帮助咱们了解闭包是如何工做的,以及咱们应该如何在代码中使用闭包。

通常状况下,JavaScript 变量的生命周期被限定在声明其的函数内。全局变量在整个程序未结束以前一直存在,局部变量则在函数未结束以前一直存在。当一个函数执行完毕,其内部的局部变量会被 JavaScript 解析器的垃圾回收机制销毁从而再也不是一个变量。当一个内嵌函数保存了其外层函数一个变量的引用,即便外层函数执行完毕,这个引用也继续被保存着。当这种状况发生,咱们说建立了一个闭包。

很差理解?让咱们看几个例子:

var fruit = 'banana';

(function() {
    var fruit = 'apple';
    console.log(fruit); // 'apple'
})();

console.log(fruit); // 'banana'

这里,咱们有一个建立了一个 fruit 变量的自执行函数。在这个函数内部,变量 fruit 的值是 apple。当这个函数执行完毕,值为 apple 的变量 fruit 便被销毁。因而只剩下了值为 banana 的全局变量 fruit。此种状况下咱们并未建立一个闭包。再看看另外一种状况:

var fruit = 'banana';

(function() {
    var fruit = 'apple';

    function inner() {
        console.log(fruit); // 'apple'
    }

    inner();
})();

console.log(fruit); // 'banana'

这段代码和上一个很相似,自执行函数建立了一个 fruit 变量和一个 inner 函数。当 inner 函数被调用时,它引用了外层函数中的变量 fruit,因而咱们的获得了一个 apple 而不是 banana。不幸的是,对于自执行函数来讲,这个 inner 函数是一个局部对象,因此在自执行函数结束后,inner 函数也会被销毁掉。咱们仍是没建立一个闭包,再来看一个例子:

var fruit = 'banana';
var inner;

(function() {
    var fruit = 'apple';

    inner = function() {
        console.log(fruit);
    }

})();

console.log(fruit); // 'banana'
inner(); // 'apple'

如今开始变得有趣了。在全局做用域中咱们声明了一个名为 inner 的变量,在自执行函数中咱们把一个 log 出 fruit 变量值的函数做为值赋给全局变量 inner。正常状况下,当自执行函数结束后,其内部的局部变量 fruit 应该被销毁,就像咱们前面 2 个例子那样。可是由于在 inner 函数中依然保持着对局部变量 fruit 的引用,因此最后咱们在调用 inner 时会 log 出 apple。这时能够说咱们建立了一个闭包。

一个闭包会在这种状况下被建立:一个内层函数嵌套在一个外层函数里,这个内层函数被储存在其外层函数做用域以外的做用域的 variable 对象中,同时还保存着对其外层函数局部变量的引用。虽然外层函数中的这个 inner 函数不会再被运行,但其对外层函数变量的引用却依然保留着,这是由于在函数内部的做用域链中依然保存着该变量的引用,即便外层的函数此时已经不存在了。

要记住一个函数的做用域链同它的执行上下文是绑定的,同其余那些与执行上下文关联紧密的对象同样,做用域链在函数执行上下文被建立以后建立,并随着函数执行上下文的销毁而销毁。解析器只有在函数被调用时才会建立该函数的执行上下文。在上面的例子中,inner 函数是在最后一行代码被执行时调用的,而此时,原匿名函数的执行上下文(连同它的做用域链和 variable 对象)都已经被销毁了。那么 inner 函数是如何引用到已经被销毁的保存在局部做用域中的局部变量的呢?

这个问题的答案引出了函数内部对象中一个被称为 scope 属性(scope property)的对象。全部的 JavaScript 函数都有其自身的内在 scope 属性,该对象中储存着用来建立该函数做用域链的那些对象。当解析器要为一个函数建立做用域链,它会去查看 scope 属性看看哪些项是须要被加进做用域链中的。由于相比执行上下文,scope 属性同函数自己的联系更为紧密,因此在函数被完全销毁以前,它都会一直存在——这样苦于保证不了函数被调用多少次,它都是可用的。

一个在全局做用域中被建立的函数拥有一个包含了 global 对象的 scope 对象,因此它的做用域链仅包含了 global 对象和和它本身的 variable 对象。一个建立在其余函数中的函数,它的 scope 对象包含了封装它的那个函数的 scope 对象中的全部对象和它本身的 variable 对象。

function A() {
    function B() {
        function C() {
        }
    }
}

在这个代码片断中,函数 A 的 scope 属性中仅保存了 global 对象。由于函数嵌套在函数 A 中,全部函数 B 的 scope 属性会继承函数 A 的 scope 属性的内容并附加上函数 A 的 variable 对象。最后,函数 C 的 scope 属性会继承函数 B 的 scope 属性中的全部内容。

另外,采用函数对象方式(使用 new Function() 方法)建立的函数,在它们的 scope 属性中只有一个项,就是 global 对象。这意味着它们不能访问其外围函数(若是有的话)的局部变量,也就不能用来建立闭包。

This 关键字(The “this” Keyword)

上面咱们讨论了一些函数的内部机制,最后咱们还有一个项目要讨论:this 关键字。若是你对其余的面向对象的编程语言有使用经验,你应该会对一些关键字感到熟悉,好比 this 或者 self,用以指代当前的实例。不过在 JavaScript 中 this 关键字会便得有些复杂,由于它的值取决于执行上下文和函数的调用者。同时 this 仍是动态的,这意味着它的值能够在程序运行时被更改。

this 的值老是一个对象,而且有些一系列规则来明确在当前代码块中哪个对象会成为 this。其中最简单的规则就是,在全局环境中,this 指向全局对象。

var fruit = 'banana';

console.log(fruit); // 'banana'
console.log(this.fruit); // 'banana'

回忆一下,全局上下文中声明的变量都会成为全局 global 对象的属性。这里咱们会看到 this.fruit 会正确的指向 fruit 变量,这向咱们展现在这段代码中 this 关键字是指向 global 对象的。对于全局上下文中声明的函数,在其函数体中 this 关键字也是指向 global 对象的。

var fruit = 'banana';

function sayFruit() {
    console.log(this.fruit);
}

sayFruit(); // 'banana'

(function() {
    console.log(this.fruit); // 'banana'
})();

var tellFruit = new Function('console.log(this.fruit);');

tellFruit(); // 'banana'

对于做为一个对象的属性(或方法)的函数,this 关键字指向的是这个对象自己而非 global 对象:

var fruit = {

    name : 'banana',

    say : function() {
        console.log(this.name);
    }

};

fruit.say(); // 'banana'

在第三章咱们会深刻讨论关于对象的话题,可是如今,咱们要关注 this.name 属性是如何指向 fruit 对象的 name 属性的。在本质上,这和前面的例子是同样的:由于上面例子中的函数是 global 对象的属性,因此函数体内的 this 关键字会指向 global 对象。因此对于做为某个对象属性的函数而言,其函数体内的 this 关键字指向的就是这个对象。

对于嵌套的函数而言,遵循第一条规则:不论它们出如今哪里,它们老是将 global 对象做为其函数体中 this 关键字的默认值。

var fruit = 'banana';

(function() {
    (function() {
        console.log(this.fruit); // 'banana'
    })();
})();

var object = {

    fruit : 'orange',

    say : function() {
        var sayFruit =  function() {
            console.log(this.fruit); // 'banana'
        };
        sayFruit();
    }

};

object.say();

这里,咱们看处处在两层套嵌的子执行函数中的标识符 this.fruit 指向的是 global 对象中的 fruit 变量。在 say 函数中有一个内嵌函数的例子中,即便 say 函数自身的 this 指向的是 object 对象,但内嵌的 sayFruit 函数中的 this.fruit 指向的仍是 banana。这意味着外层函数并不会对内嵌函数代码体中 this 关键字的值产生任何影响。

我在前面提到过 this 关键字的值是可变的,且在 JavaScript 中可以对 this 的值进行改变是颇有用的。有两种方法能够应用于更改函数 this 关键字的值:apply 方法和 call 方法。这两种方法实际上都是应用于无需使用调用操做符 () 来调用函数,虽然没有了调用操做符,但你仍是能够经过 apply 和 call 方法给函数传递参数。

apply 方法接收 2 个参数:thisValue 被用于指明函数体中 this 关键字所指向的对象;另外一个参数是 params,它以数组的形式向函数传递参数。当使用一个无参数或第一个参数为 null 的 apply 方法去调用一个函数的时候,那么被调用的函数内部 this 指向的就会是 global 对象而且也意味着没有参数传递给它:

var fruit = 'banana'

var object = {

    fruit : 'orange',

    say : function() {
        console.log(this.fruit);
    }

};

object.say(); // 'banana'
object.say.apply(); // 'banana'

若是要将一个函数内部的 this 关键字指向另外一个对象,简单的作法就是使用 apply 方法并把那个对象的引用做为参数传进去:

function add() {
    console.log(this.a + this.b);
}

var a = 12;
var b = 13;

var values = {
    a : 50,
    b : 23
};

add.apply(values); // 73

apply 方法的第二个参数是以一个数组的形式向被调用的函数传递参数,数组中的项要和被调用函数的形参保持一致。

function add(a, b) {
    console.log(a); // 20
    console.log(b); // 50
    console.log(a + b); // 70
}

add.apply(null, [20, 50]);

上面说到的另外一个方法 call,和 apply 方法的工做机制是同样的,所不一样的是在 thisValue 参数以后跟着的是自选数量的参数,而不是一个数组:

function add(a, b) {
    console.log(a); // 20
    console.log(b); // 50
    console.log(a + b); // 70
}

add.call(null, 20, 50);

高级的函数技巧(Advanced Function Techniques)

前面的内容主要是关于咱们对函数的基础知识的一些讨论。不过,要想完整的展示出 JavaScript 函数的魅力,咱们还必须可以应用前面学到的这些分散的知识。

在下面的章节中,咱们会讨论一些高级的函数技巧,并探索目前所掌握的技能其更普遍的应用范围。我想说,本书不会是 JavaScript 学习的终点,咱们不可能把关于这门语言的全部信息都写出来,而应该是开启你探索之路的一个起点。

限制做用域(Limiting Scope)

如今,我在维护一个用户的姓名和年龄这个事情上遇到了问题。

// user对象保存了一些信息
var user = {
    name : 'Mark',
    age : 23
};

function setName(name) {
    // 首先确保name是一个字符串
    if (typeof name === 'string') user.name = name;
}

function getName() {
   return user.name;
}

function setAge(age) {
    // 首先确保age是一个数字
    if (typeof age === 'number') user.age = age;
}

function getAge() {
    return user.age;
}

// 设置一个新的名字
setName('Joseph');
console.log(getName()); // 'Joseph'

// 设置一个新的年龄
setAge(22);
console.log(getAge()); // 22

目前为止,一切都正常。setName 和 setAge 函数确保咱们要设置的值是正确的类型。但咱们要注意到,user 变量是出在全局做用域中的,能够在该做用域内的任何地方被访问到,这回致使你能够不适应咱们的设置函数也可以设置 name 和 age 的值:

user.name = 22;
user.age = 'Joseph';

console.log(getName()); // 22
console.log(getAge()); // Joseph

很明显这样很差,由于咱们但愿这些值可以保持其数据类型的正确性。

那么咱们该怎么作呢?如何你回忆一下,你会记起一个建立在函数内部的变量会成为一个局部变量,在该函数外部是不能被访问到的,另外闭包却能够为一个函数可以保存其外层函数局部变量的引用提供途径。结合这些知识点,咱们能够把 user 变成一个受限制的局部变量,再利用闭包来使得获取、设置等函数能够对其进行操做。

// 建立一个自执行函数
// 包围咱们的代码使得user变成局部变量
(function() {

    // user对象保存了一些信息
    var user = {
        name : 'Mark',
        age : 23
    };

    setName = function(name) {
        // 首先确保name是一个字符串
        if (typeof name === 'string') user.name = name;
    };

    getName = function() {
        return user.name;
    };

    setAge = function(age) {
        // 首先确保age是一个数字
        if (typeof age === 'number') user.age = age;
    };

    getAge = function() {
        return user.age;
    }

})();

// 设置一个新的名字
setName('Joseph');
console.log(getName()); // 'Joseph'

// 设置一个新的年龄
setAge(22);
console.log(getAge()); // 22

如今,若是有什么人想不经过咱们的 setName 和 setAge 方法来设置 user.name 和 user.age 的值,他就会获得一个报错。

柯里化(Currying)

函数做为一等对象最大的好处就是能够在程序运行时建立它们并将之储存在变量里。以下面的这段代码:

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

add(5, 2);
add(5, 5);
add(5, 200);

这里咱们每次都使用 add 函数将数字 5 和其余三个数字进行相加,若是能把数字 5 内置在函数中而不用每次调用时都做为参数传进去是个不错的主意。咱们能够将 add 函数的内部实现机制变为 5 + b 的方式,但这会致使咱们代码中其余已经使用了旧版 add 函数的部分发生错误。那有没有什么方法能够实现不修改原有 add 函数的优化方式?

固然咱们能够,这种技术被称为柯里化(partial application 或 currying),其实现涉及到一个可为其提早“提供”一些参数的函数:

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

function add5() {
    return add(5, b);
}

add5(2);
add5(5);
add5(200);

如今,咱们建立了一个调用 add 函数并预置了一个参数值(这里是5)的 add5 函数,add5 函数本质上来说其实就是预置了一个参数(柯里化)的 add 函数。不过,上面的例子并没展现出这门技术动态的一面,若是咱们提供的默认值是另一个应该怎么作?按照上面的例子,咱们必需要再次新建一个函数来提供一个新的预置参数。

函数做为一等对象早晚都会派上用处,看,下面应用场景来了。不一样于明确的建立一个新的 add5 函数,咱们能够像下面这样来作。

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

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

var add5 = curryAdd(5);

add5(2);
add5(5);
add5(200);

如今来介绍一下这个新的函数 curryAdd,它接收一个参数,这个参数会做为 add 函数的参数 a,同时返回一个新的匿名函数,这个匿名函数接收一个参数 b 来做为 add 函数的另外一个参数。当咱们经过 curryAdd(5) 来调用这个函数时,它返回一个已经储存了咱们一个明确参数值的函数,这个参数值此时被当作是这个匿名函数的一个局部变量。由于咱们建立了一个闭包,因此即便这个匿名函数已经执行完毕,但咱们仍是能够经过它来最终求出咱们须要的 a + b 的值。

咱们这里指介绍了一种柯里化函数一个极为简单常见的应用场景,但这能够很好的说明柯里化函数是如何工做的。了解并掌握这种技巧会对你平常的编程工做带来不少便利的。

装饰(Decoration)

另外一项综合使用函数动态赋值和闭包的技术被称为装饰(decoration)。这里的关键词是“装饰”(decorate),函数的装饰是指可以动态的为一个函数增长新的功能特性。

如今咱们有一个函数,它把一个对象做为参数,它的工做是把这个参数对象内的名值对储存到另外一个对象里去:

(function() {

    var storage = {};

    store = function(obj) {
        for (var i in obj) storage[i] = obj[i];
    };

    retrieve = function(key) {
        return storage[key];
    };

})();

console.log(retrieve('name')); // undefined

store({
    name : 'Mark',
    age : '23'
});
console.log(retrieve('name')); // 'Mark'

看起来彷佛不错,但若是咱们的需求变成不单能够给 store 函数传由名值对组成的对象作参数,还能够直接传名值对,就是相似 store(‘name’, ‘Mark’); 这种形式的,那咱们目前的函数就不能起做用了,咱们须要对函数进行改进。

咱们能够经过为 store 函数套上一层装饰者函数来实现想要的改进:

var decoratePair = function(fn) {
    return function(key, value) {
        if (typeof key === 'string') {
            var _temp = {};
            _temp[key] = value;
            key = _temp;
        }
        return fn(key);
    }
};

(function() {

    var storage = {};

    store = decoratePair(function(obj) {
        for (var i in obj) storage[i] = obj[i];
    });

    retrieve = function(key) {
        return storage[key];
    };

})();

console.log(retrieve('name')); // undefined

store('name', 'Mark');
console.log(retrieve('name')); // 'Mark'

这应该是目前为止咱们看过的比较复杂的例子了,让咱们一步一步的来分析下这段代码。首先,咱们声明了一个名为 decoratePair 的函数,这个函数只接收一个参数 fn,这个函数会被咱们进行装饰。以后 decoratePair 会返回一个新的被装饰过的函数,这个函数接收两个参数,key 和 value。咱们原先的 store 函数只接收一个对象类型的参数,如今经过装饰者函数能够判断第一个参数是对象仍是字符串。若是第一个参数不是字符串,则fn函数会当即被执行;若是第一个参数是字符串,则 decoratePair 的返回值函数会先把传进去的参数 key 和 value 以名值对的方式存进一个私有变量 _temp 里,以后把 _temp 赋值给一个变量 key,这时变量 key 引用的是一个符合 fn 函数参数要求的对象,以后再来调用 fn 函数。

咱们上面的装饰者函数能够确保在调用被包装的 fn 函数时传输的是类型正确的参数,可是修饰着函数也能够用在函数被调用后为其增长特性。下面有一个简单的装饰者函数,它调用 add 函数的 2 个参数,并返回这 2 个参数的和与第二个参数的积。

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

var decorateMultiply = function(fn) {
    return function(a, b) {
        var result = fn(a, b);
        return result * b;
    }
};

var addThenMultiply = decorateMultiply(add);

console.log(add(2, 3)); // 5
console.log(addThenMultiply(2, 3)); // 15

装饰者函数的用途很广,它能够帮助你在无需直接修改函数的状况下为其增长功能。它尤为适用于那些你不能直接修改的内建函数和第三方代码。

组合(Combination)

组合(combination)是一项和装饰者函数类似的技术,它的用途是使用两个(或数个)函数来创造一个新的函数。这和声明一个新的函数不一样,组合者函数只是将一个函数的返回值做为参数传给下一个函数。

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

var square = function(a) {
    return a * a;
};

var result = square(add(3, 5));

console.log(result); // 64**

square(add(3, 5)) 这段代码显示了组合者函数是如何工做的,但这还不能算一个正确的组合者函数。这里,add(3, 5) 的返回值 8,做为参数传给了 square 函数,以后 square 函数返回了 64。要把它变成一个组合者函数,咱们要将加工过程自动化,省得每次都要去敲 square(add(3, 5))。

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

var square = function(a) {
    return a * a;
};

var combine = function(fnA, fnB) {
    return function() {
        var args = Array.prototype.slice.call(arguments);
        var result = fnA.apply(null, args);
        return fnB.call(null, result);
    }
};

var addThenSquare = combine(add, square);

var result = addThenSquare(3, 5);
   
console.log(result); // 64

在这个代码片断中咱们先建立了两个具有单一功能的函数 add 和 square。以后建立了一个组合者函数 combine,combine 函数接收 add 和 square 为参数,在返回的匿名函数里,先将传给匿名调用函数的参数 a 和 b 转为一个数组 args,以后用 apply 方法调用 add 函数,将 a 与 b 的和赋值给变量 result,最后用 call 方法调用 square 方法,计算出最终的结果。

注意在使用组合者函数时,函数的顺序和参数的数量是须要被重点注意的。在咱们的例子中,由于 square 函数只须要一个参数,而 add 函数须要的则是两个,因此咱们不能获得一个 squareTheAdd(先乘后加,先传一个参数后传 2 个参数)函数。由于 JavaScript 只容许函数返回一个值,因此组合者函数的使用场景每每是被限制在那些只采用单个参数的函数中。

招贤纳士(Recruitment)

招人,前端,隶属政采云前端大团队(ZooTeam),50 余个小伙伴正等你加入一块儿浪~ 若是你想改变一直被事折腾,但愿开始能折腾事;若是你想改变一直被告诫须要多些想法,却无从破局;若是你想改变你有能力去作成那个结果,却不须要你;若是你想改变你想作成的事须要一个团队去支撑,但没你带人的位置;若是你想改变既定的节奏,将会是“5年工做时间3年工做经验”;若是你想改变原本悟性不错,但老是有那一层窗户纸的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但愿参与到随着业务腾飞的过程,亲手参与一个有着深刻的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我以为咱们该聊聊。任什么时候间,等着你写点什么,发给 ZooTeam@cai-inc.com

相关文章
相关标签/搜索