多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
<header id="function.intro"> # 函数 </header> ## 函数声明与表达式 函数是JavaScript中的一等对象,这意味着可以把函数像其它值一样传递。 一个常见的用法是把_匿名函数_作为回调函数传递到异步函数中。 ### 函数声明 ``` function foo() {} ``` 上面的方法会在执行前被 [解析(hoisted)](#function.scopes),因此它存在于当前上下文的_任意_一个地方, 即使在函数定义体的上面被调用也是对的。 ``` foo(); // 正常运行,因为foo在代码运行前已经被创建 function foo() {} ``` ### 函数赋值表达式 ``` var foo = function() {}; ``` 这个例子把一个_匿名_的函数赋值给变量 `foo`。 ``` foo; // 'undefined' foo(); // 出错:TypeError var foo = function() {}; ``` 由于 `var` 定义了一个声明语句,对变量 `foo` 的解析是在代码运行之前,因此 `foo` 变量在代码运行时已经被定义过了。 但是由于赋值语句只在运行时执行,因此在相应代码执行之前, `foo` 的值缺省为 [undefined](#core.undefined)。 ### 命名函数的赋值表达式 另外一个特殊的情况是将命名函数赋值给一个变量。 ``` var foo = function bar() { bar(); // 正常运行 } bar(); // 出错:ReferenceError ``` `bar` 函数声明外是不可见的,这是因为我们已经把函数赋值给了 `foo`; 然而在 `bar` 内部依然可见。这是由于 JavaScript 的 [命名处理](#function.scopes) 所致, 函数名在函数内_总是_可见的。 **注意:**在IE8及IE8以下版本浏览器bar在外部也是可见的,是因为浏览器对命名函数赋值表达式进行了错误的解析, 解析成两个函数 `foo` 和 `bar` ## `this` 的工作原理 JavaScript 有一套完全不同于其它语言的对 `this` 的处理机制。 在**五**种不同的情况下 ,`this` 指向的各不相同。 ### 全局范围内 ``` this; ``` 当在全部范围内使用 `this`,它将会指向_全局_对象。 **[译者注](http://cnblogs.com/sanshi/):**浏览器中运行的 JavaScript 脚本,这个全局对象是 `window`。 ### 函数调用 ``` foo(); ``` 这里 `this` 也会指向_全局_对象。 **ES5 注意:** 在严格模式下(strict mode),不存在全局变量。 这种情况下 `this` 将会是 `undefined`。 ### 方法调用 ``` test.foo(); ``` 这个例子中,`this` 指向 `test` 对象。 ### 调用构造函数 ``` new foo(); ``` 如果函数倾向于和 `new` 关键词一块使用,则我们称这个函数是 [构造函数](#function.constructors)。 在函数内部,`this` 指向_新创建_的对象。 ### 显式的设置 `this` ``` function foo(a, b, c) {} var bar = {}; foo.apply(bar, [1, 2, 3]); // 数组将会被扩展,如下所示 foo.call(bar, 1, 2, 3); // 传递到foo的参数是:a = 1, b = 2, c = 3 ``` 当使用 `Function.prototype` 上的 `call` 或者 `apply` 方法时,函数内的 `this` 将会被 **显式设置**为函数调用的第一个参数。 因此_函数调用_的规则在上例中已经不适用了,在`foo` 函数内 `this` 被设置成了 `bar`。 **注意:** 在对象的字面声明语法中,`this` **不能**用来指向对象本身。 因此 `var obj = {me: this}` 中的 `me` 不会指向 `obj`,因为 `this` 只可能出现在上述的五种情况中。 **[译者注](http://cnblogs.com/sanshi/):**这个例子中,如果是在浏览器中运行,`obj.me` 等于 `window` 对象。 ### 常见误解 尽管大部分的情况都说的过去,不过第一个规则(**[译者注](http://cnblogs.com/sanshi/):**这里指的应该是第二个规则,也就是直接调用函数时,`this` 指向全局对象) 被认为是JavaScript语言另一个错误设计的地方,因为它**从来**就没有实际的用途。 ``` Foo.method = function() { function test() { // this 将会被设置为全局对象(译者注:浏览器环境中也就是 window 对象) } test(); } ``` 一个常见的误解是 `test` 中的 `this` 将会指向 `Foo` 对象,实际上**不是**这样子的。 为了在 `test` 中获取对 `Foo` 对象的引用,我们需要在 `method` 函数内部创建一个局部变量指向 `Foo` 对象。 ``` Foo.method = function() { var that = this; function test() { // 使用 that 来指向 Foo 对象 } test(); } ``` `that` 只是我们随意起的名字,不过这个名字被广泛的用来指向外部的 `this` 对象。 在 [闭包](#function.closures) 一节,我们可以看到 `that` 可以作为参数传递。 ### 方法的赋值表达式 另一个看起来奇怪的地方是函数别名,也就是将一个方法**赋值**给一个变量。 ``` var test = someObject.methodTest; test(); ``` 上例中,`test` 就像一个普通的函数被调用;因此,函数内的 `this` 将不再被指向到 `someObject` 对象。 虽然 `this` 的晚绑定特性似乎并不友好,但这确实是[基于原型继承](#object.prototype)赖以生存的土壤。 ``` function Foo() {} Foo.prototype.method = function() {}; function Bar() {} Bar.prototype = Foo.prototype; new Bar().method(); ``` 当 `method` 被调用时,`this` 将会指向 `Bar` 的实例对象。 ## 闭包和引用 闭包是 JavaScript 一个非常重要的特性,这意味着当前作用域**总是**能够访问外部作用域中的变量。 因为 [函数](#function.scopes) 是 JavaScript 中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数。 ### 模拟私有变量 ``` function Counter(start) { var count = start; return { increment: function() { count++; }, get: function() { return count; } } } var foo = Counter(4); foo.increment(); foo.get(); // 5 ``` 这里,`Counter` 函数返回两个闭包,函数 `increment` 和函数 `get`。 这两个函数都维持着 对外部作用域 `Counter` 的引用,因此总可以访问此作用域内定义的变量 `count`. ### 为什么不可以在外部访问私有变量 因为 JavaScript 中不可以对作用域进行引用或赋值,因此没有办法在外部访问 `count` 变量。 唯一的途径就是通过那两个闭包。 ``` var foo = new Counter(4); foo.hack = function() { count = 1337; }; ``` 上面的代码**不会**改变定义在 `Counter` 作用域中的 `count` 变量的值,因为 `foo.hack` 没有 定义在那个**作用域**内。它将会创建或者覆盖_全局_变量 `count`。 ### 循环中的闭包 一个常见的错误出现在循环中使用闭包,假设我们需要在每次循环中调用循环序号 ``` for(var i = 0; i < 10; i++) { setTimeout(function() { console.log(i); }, 1000); } ``` 上面的代码不会输出数字 `0` 到 `9`,而是会输出数字 `10` 十次。 当 `console.log` 被调用的时候,_匿名_函数保持对外部变量 `i` 的引用,此时 `for`循环已经结束, `i` 的值被修改成了 `10`. 为了得到想要的结果,需要在每次循环中创建变量 `i` 的**拷贝**。 ### 避免引用错误 为了正确的获得循环序号,最好使用 [匿名包装器](#function.scopes)(**[译者注](http://cnblogs.com/sanshi/):**其实就是我们通常说的自执行匿名函数)。 ``` for(var i = 0; i < 10; i++) { (function(e) { setTimeout(function() { console.log(e); }, 1000); })(i); } ``` 外部的匿名函数会立即执行,并把 `i` 作为它的参数,此时函数内 `e` 变量就拥有了 `i` 的一个拷贝。 当传递给 `setTimeout` 的匿名函数执行时,它就拥有了对 `e` 的引用,而这个值是**不会**被循环改变的。 有另一个方法完成同样的工作,那就是从匿名包装器中返回一个函数。这和上面的代码效果一样。 ``` for(var i = 0; i < 10; i++) { setTimeout((function(e) { return function() { console.log(e); } })(i), 1000) } ``` ## `arguments` 对象 JavaScript 中每个函数内都能访问一个特别变量 `arguments`。这个变量维护着所有传递到这个函数中的参数列表。 **注意:** 由于 `arguments` 已经被定义为函数内的一个变量。 因此通过 `var` 关键字定义 `arguments` 或者将 `arguments` 声明为一个形式参数, 都将导致原生的 `arguments` 不会被创建。 `arguments` 变量**不是**一个数组(`Array`)。 尽管在语法上它有数组相关的属性 `length`,但它不从 `Array.prototype` 继承,实际上它是一个对象(`Object`)。 因此,无法对 `arguments` 变量使用标准的数组方法,比如 `push`, `pop` 或者 `slice`。 虽然使用 `for` 循环遍历也是可以的,但是为了更好的使用数组方法,最好把它转化为一个真正的数组。 ### 转化为数组 下面的代码将会创建一个新的数组,包含所有 `arguments` 对象中的元素。 ``` Array.prototype.slice.call(arguments); ``` 这个转化比较**慢**,在性能不好的代码中**不推荐**这种做法。 ### 传递参数 下面是将参数从一个函数传递到另一个函数的推荐做法。 ``` function foo() { bar.apply(null, arguments); } function bar(a, b, c) { // 干活 } ``` 另一个技巧是同时使用 `call` 和 `apply`,创建一个快速的解绑定包装器。 ``` function Foo() {} Foo.prototype.method = function(a, b, c) { console.log(this, a, b, c); }; // 创建一个解绑定的 "method" // 输入参数为: this, arg1, arg2...argN Foo.method = function() { // 结果: Foo.prototype.method.call(this, arg1, arg2... argN) Function.call.apply(Foo.prototype.method, arguments); }; ``` **[译者注](http://cnblogs.com/sanshi/)**:上面的 `Foo.method` 函数和下面代码的效果是一样的: ``` Foo.method = function() { var args = Array.prototype.slice.call(arguments); Foo.prototype.method.apply(args[0], args.slice(1)); }; ``` ### 自动更新 `arguments` 对象为其内部属性以及函数形式参数创建 _getter_ 和 _setter_ 方法。 因此,改变形参的值会影响到 `arguments` 对象的值,反之亦然。 ``` function foo(a, b, c) { arguments[0] = 2; a; // 2 b = 4; arguments[1]; // 4 var d = c; d = 9; c; // 3 } foo(1, 2, 3); ``` ### 性能真相 不管它是否有被使用,`arguments` 对象总会被创建,除了两个特殊情况 - 作为局部变量声明和作为形式参数。 `arguments` 的 _getters_ 和 _setters_ 方法总会被创建;因此使用 `arguments` 对性能不会有什么影响。 除非是需要对 `arguments` 对象的属性进行多次访问。 **ES5 提示:** 这些 _getters_ 和 _setters_ 在严格模式下(strict mode)不会被创建。 **[译者注](http://cnblogs.com/sanshi/):**在 [MDC](https://developer.mozilla.org/en/JavaScript/Strict_mode) 中对 `strict mode` 模式下 `arguments` 的描述有助于我们的理解,请看下面代码: ``` // 阐述在 ES5 的严格模式下 `arguments` 的特性 function f(a) { "use strict"; a = 42; return [a, arguments[0]]; } var pair = f(17); console.assert(pair[0] === 42); console.assert(pair[1] === 17); ``` 然而,的确有一种情况会显著的影响现代 JavaScript 引擎的性能。这就是使用 `arguments.callee`。 ``` function foo() { arguments.callee; // do something with this function object arguments.callee.caller; // and the calling function object } function bigLoop() { for(var i = 0; i < 100000; i++) { foo(); // Would normally be inlined... } } ``` 上面代码中,`foo` 不再是一个单纯的内联函数 [inlining](http://en.wikipedia.org/wiki/Inlining)(**[译者注](http://cnblogs.com/sanshi/)**:这里指的是解析器可以做内联处理), 因为它需要知道它自己和它的调用者。 这不仅抵消了内联函数带来的性能提升,而且破坏了封装,因此现在函数可能要依赖于特定的上下文。 因此**强烈**建议大家**不要**使用 `arguments.callee` 和它的属性。 **ES5 提示:** 在严格模式下,`arguments.callee` 会报错 `TypeError`,因为它已经被废除了。 ## 构造函数 JavaScript 中的构造函数和其它语言中的构造函数是不同的。 通过 `new` 关键字方式调用的函数都被认为是构造函数。 在构造函数内部 - 也就是被调用的函数内 - `this` 指向新创建的对象 `Object`。 这个**新创建**的对象的 [`prototype`](#object.prototype) 被指向到构造函数的 `prototype`。 如果被调用的函数没有显式的 `return` 表达式,则隐式的会返回 `this` 对象 - 也就是新创建的对象。 ``` function Foo() { this.bla = 1; } Foo.prototype.test = function() { console.log(this.bla); }; var test = new Foo(); ``` 上面代码把 `Foo` 作为构造函数调用,并设置新创建对象的 `prototype` 为 `Foo.prototype`。 显式的 `return` 表达式将会影响返回结果,但**仅限**于返回的是一个对象。 ``` function Bar() { return 2; } new Bar(); // 返回新创建的对象 function Test() { this.value = 2; return { foo: 1 }; } new Test(); // 返回的对象 ``` **[译者注](http://cnblogs.com/sanshi/):**`new Bar()` 返回的是新创建的对象,而不是数字的字面值 2。 因此 `new Bar().constructor === Bar`,但是如果返回的是数字对象,结果就不同了,如下所示 ``` function Bar() { return new Number(2); } new Bar().constructor === Number ``` **[译者注](http://cnblogs.com/sanshi/):**这里得到的 `new Test()`是函数返回的对象,而不是通过`new`关键字新创建的对象,因此: ``` (new Test()).value === undefined (new Test()).foo === 1 ``` 如果 `new` 被遗漏了,则函数**不会**返回新创建的对象。 ``` function Foo() { this.bla = 1; // 获取设置全局参数 } Foo(); // undefined ``` 虽然上例在有些情况下也能正常运行,但是由于 JavaScript 中 [`this`](#function.this) 的工作原理, 这里的 `this` 指向_全局对象_。 ### 工厂模式 为了不使用 `new` 关键字,构造函数必须显式的返回一个值。 ``` function Bar() { var value = 1; return { method: function() { return value; } } } Bar.prototype = { foo: function() {} }; new Bar(); Bar(); ``` 上面两种对 `Bar` 函数的调用返回的值完全相同,一个新创建的拥有 `method` 属性的对象被返回, 其实这里创建了一个[闭包](#function.closures)。 还需要注意, `new Bar()` 并**不会**改变返回对象的原型(**[译者注](http://cnblogs.com/sanshi/):**也就是返回对象的原型不会指向 `Bar.prototype`)。 因为构造函数的原型会被指向到刚刚创建的新对象,而这里的 `Bar` 没有把这个新对象返回([译者注](http://cnblogs.com/sanshi/):而是返回了一个包含 `method` 属性的自定义对象)。 在上面的例子中,使用或者不使用 `new` 关键字没有功能性的区别。 **[译者注](http://cnblogs.com/sanshi/):**上面两种方式创建的对象不能访问 `Bar` 原型链上的属性,如下所示: ``` var bar1 = new Bar(); typeof(bar1.method); // "function" typeof(bar1.foo); // "undefined" var bar2 = Bar(); typeof(bar2.method); // "function" typeof(bar2.foo); // "undefined" ``` ### 通过工厂模式创建新对象 我们常听到的一条忠告是**不要**使用 `new` 关键字来调用函数,因为如果忘记使用它就会导致错误。 为了创建新对象,我们可以创建一个工厂方法,并且在方法内构造一个新对象。 ``` function Foo() { var obj = {}; obj.value = 'blub'; var private = 2; obj.someMethod = function(value) { this.value = value; } obj.getPrivate = function() { return private; } return obj; } ``` 虽然上面的方式比起 `new` 的调用方式不容易出错,并且可以充分利用[私有变量](#function.closures)带来的便利, 但是随之而来的是一些不好的地方。 1. 会占用更多的内存,因为新创建的对象**不能**共享原型上的方法。 2. 为了实现继承,工厂方法需要从另外一个对象拷贝所有属性,或者把一个对象作为新创建对象的原型。 3. 放弃原型链仅仅是因为防止遗漏 `new` 带来的问题,这似乎和语言本身的思想相违背。 ### 总结 虽然遗漏 `new` 关键字可能会导致问题,但这并**不是**放弃使用原型链的借口。 最终使用哪种方式取决于应用程序的需求,选择一种代码书写风格并**坚持**下去才是最重要的。 ## 作用域与命名空间 尽管 JavaScript 支持一对花括号创建的代码段,但是并不支持块级作用域; 而仅仅支持 _函数作用域_。 ``` function test() { // 一个作用域 for(var i = 0; i < 10; i++) { // 不是一个作用域 // count } console.log(i); // 10 } ``` **注意:** 如果不是在赋值语句中,而是在 return 表达式或者函数参数中,`{...}` 将会作为代码段解析, 而不是作为对象的字面语法解析。如果考虑到 [自动分号插入](#core.semicolon),这可能会导致一些不易察觉的错误。 **[译者注](http://cnblogs.com/sanshi/):**如果 `return` 对象的左括号和 `return` 不在一行上就会出错。 ``` // 译者注:下面输出 undefined function add(a, b) { return a + b; } console.log(add(1, 2)); ``` JavaScript 中没有显式的命名空间定义,这就意味着所有对象都定义在一个_全局共享_的命名空间下面。 每次引用一个变量,JavaScript 会向上遍历整个作用域直到找到这个变量为止。 如果到达全局作用域但是这个变量仍未找到,则会抛出 `ReferenceError` 异常。 ### 隐式的全局变量 ``` // 脚本 A foo = '42'; // 脚本 B var foo = '42' ``` 上面两段脚本效果**不同**。脚本 A 在_全局_作用域内定义了变量 `foo`,而脚本 B 在_当前_作用域内定义变量 `foo`。 再次强调,上面的效果**完全不同**,不使用 `var` 声明变量将会导致隐式的全局变量产生。 ``` // 全局作用域 var foo = 42; function test() { // 局部作用域 foo = 21; } test(); foo; // 21 ``` 在函数 `test` 内不使用 `var` 关键字声明 `foo` 变量将会覆盖外部的同名变量。 起初这看起来并不是大问题,但是当有成千上万行代码时,不使用 `var` 声明变量将会带来难以跟踪的 BUG。 ``` // 全局作用域 var items = [/* 数组 */]; for(var i = 0; i < 10; i++) { subLoop(); } function subLoop() { // subLoop 函数作用域 for(i = 0; i < 10; i++) { // 没有使用 var 声明变量 // 干活 } } ``` 外部循环在第一次调用 `subLoop` 之后就会终止,因为 `subLoop` 覆盖了全局变量 `i`。 在第二个 `for` 循环中使用 `var` 声明变量可以避免这种错误。 声明变量时**绝对不要**遗漏 `var` 关键字,除非这就是_期望_的影响外部作用域的行为。 ### 局部变量 JavaScript 中局部变量只可能通过两种方式声明,一个是作为[函数](#function)参数,另一个是通过 `var` 关键字声明。 ``` // 全局变量 var foo = 1; var bar = 2; var i = 2; function test(i) { // 函数 test 内的局部作用域 i = 5; var foo = 3; bar = 4; } test(10); ``` `foo` 和 `i` 是函数 `test` 内的局部变量,而对 `bar` 的赋值将会覆盖全局作用域内的同名变量。 ### 变量声明提升(Hoisting) JavaScript 会**提升**变量声明。这意味着 `var` 表达式和 `function` 声明都将会被提升到当前作用域的顶部。 ``` bar(); var bar = function() {}; var someValue = 42; test(); function test(data) { if (false) { goo = 1; } else { var goo = 2; } for(var i = 0; i < 100; i++) { var e = data[i]; } } ``` 上面代码在运行之前将会被转化。JavaScript 将会把 `var` 表达式和 `function` 声明提升到当前作用域的顶部。 ``` // var 表达式被移动到这里 var bar, someValue; // 缺省值是 'undefined' // 函数声明也会提升 function test(data) { var goo, i, e; // 没有块级作用域,这些变量被移动到函数顶部 if (false) { goo = 1; } else { goo = 2; } for(i = 0; i < 100; i++) { e = data[i]; } } bar(); // 出错:TypeError,因为 bar 依然是 'undefined' someValue = 42; // 赋值语句不会被提升规则(hoisting)影响 bar = function() {}; test(); ``` 没有块级作用域不仅导致 `var` 表达式被从循环内移到外部,而且使一些 `if` 表达式更难看懂。 在原来代码中,`if` 表达式看起来修改了_全局变量_ `goo`,实际上在提升规则被应用后,却是在修改_局部变量_。 如果没有提升规则(hoisting)的知识,下面的代码看起来会抛出异常 `ReferenceError`。 ``` // 检查 SomeImportantThing 是否已经被初始化 if (!SomeImportantThing) { var SomeImportantThing = {}; } ``` 实际上,上面的代码正常运行,因为 `var` 表达式会被提升到_全局作用域_的顶部。 ``` var SomeImportantThing; // 其它一些代码,可能会初始化 SomeImportantThing,也可能不会 // 检查是否已经被初始化 if (!SomeImportantThing) { SomeImportantThing = {}; } ``` **[译者注](http://cnblogs.com/sanshi/):**在 Nettuts+ 网站有一篇介绍 hoisting 的[文章](http://net.tutsplus.com/tutorials/javascript-ajax/quick-tip-javascript-hoisting-explained/),其中的代码很有启发性。 ``` // 译者注:来自 Nettuts+ 的一段代码,生动的阐述了 JavaScript 中变量声明提升规则 var myvar = 'my value'; (function() { alert(myvar); // undefined var myvar = 'local value'; })();   ``` ### 名称解析顺序 JavaScript 中的所有作用域,包括_全局作用域_,都有一个特别的名称 [`this`](#function.this) 指向当前对象。 函数作用域内也有默认的变量 [`arguments`](#function.arguments),其中包含了传递到函数中的参数。 比如,当访问函数内的 `foo` 变量时,JavaScript 会按照下面顺序查找: 1. 当前作用域内是否有 `var foo` 的定义。 2. 函数形式参数是否有使用 `foo` 名称的。 3. 函数自身是否叫做 `foo`。 4. 回溯到上一级作用域,然后从 **#1** 重新开始。 **注意:** 自定义 `arguments` 参数将会阻止原生的 `arguments` 对象的创建。 ### 命名空间 只有一个全局作用域导致的常见错误是命名冲突。在 JavaScript中,这可以通过 _匿名包装器_ 轻松解决。 ``` (function() { // 函数创建一个命名空间 window.foo = function() { // 对外公开的函数,创建了闭包 }; })(); // 立即执行此匿名函数 ``` 匿名函数被认为是 [表达式](#function);因此为了可调用性,它们首先会被执行。 ``` ( // 小括号内的函数首先被执行 function() {} ) // 并且返回函数对象 () // 调用上面的执行结果,也就是函数对象 ``` 有一些其他的调用函数表达式的方法,比如下面的两种方式语法不同,但是效果一模一样。 ``` // 另外两种方式 +function(){}(); (function(){}()); ``` ### 结论 推荐使用_匿名包装器_(**[译者注](http://cnblogs.com/sanshi/):**也就是自执行的匿名函数)来创建命名空间。这样不仅可以防止命名冲突, 而且有利于程序的模块化。 另外,使用全局变量被认为是**不好的习惯**。这样的代码容易产生错误并且维护成本较高。