函数

JavaScript 中,函数不仅拥有传统函数的使用方式,而且可以像其它对象一样有属性和方法,所以也被称为第一级对象(first-class object)。

根据维基百科一级函数定义,如果满足下列条件就是一级的:

如果函数作为参数或返回值使用时,就称为高阶函数,JavaScript 中的函数都可以作为高阶函数来使用。

声明

函数名可以是任何合法标识符(Identifier)。多次声明,后面的声明会覆盖前面的。

常见的声明方式有以下几种:

执行上下文(execution contexts)和作用域链(scope chains)

执行上下文是 ECMAScript 规范用于定义 ECMAScript 实现必要行为的一个抽象的概念。所有 JavaScript 代码都是在一个执行上下文中被执行的。全局代码(例如作为内置的 JavaSript 文件执行的代码,或者 HTML 页面加载的代码)是在称之为“全局执行上下文”的执行上下文中执行的,而对函数的每次调用同样有关联的执行上下文。

当调用一个 JavaScript 函数时,该函数就会进入相应的执行上下文。如果又调用了另外一个函数(或者递归地调用同一个函数),则又会创建一个新的执行上下文,并且在函数调用期间执行过程都处于该上下文中。当调用的函数返回后,执行过程会返回之前执行上下文。因而,运行中的 JavaScript 代码就构成了一个执行上下文栈。当控制从关联当前正运行的执行上下文的可执行代码转接到不关联此执行上下文的可执行代码时,就创建一个新的执行上下文。新创建的上下文被压到栈顶,变成正运行的执行上下文。

创建函数执行上下文的系列过程:

  1. 首先,创建一个“活动”对象,活动对象是规范中规定的另外一种机制。之所以称之为对象,是因为它拥有可访问的属性,但是不能通过 JavaScript 代码直接引用活动对象。
  2. 下一步,创建一个 arguments 对象,这是一个类似数组(array-liked)的对象,它以整数索引的数组成员一一对应地保存着调用函数时所传递的参数。然后,会为活动对象创建一个名为 arguments 的属性,该属性引用前面创建的 arguments 对象。
  3. 接着,为执行上下文分配作用域。作用域由对象列表(链)组成。每个函数对象都有一个内部的 [[scope]] 属性,指定给一个函数调用执行上下文的作用域,同时,活动对象被添加到该对象列表的顶部(链的前端)。
  4. 然后,发生由“可变”对象完成的“变量实例化”的过程。此时会将函数的形式参数创建为可变对象的命名属性,如果调用函数时传递的参数与形式参数一致,则将相应参数的值赋给这些命名属性(否则,赋 undefined 值)。对于定义的内部函数,会以其声明时所用名称为可变对象创建同名属性,而相应的内部函数则被创建为函数对象并指定给该属性。变量实例化的最后一步是将在函数内部声明的所有局部变量创建为可变对象的命名属性。根据声明的局部变量创建的可变对象的属性在变量实例化过程中会被赋予 undefined 值。在执行函数体内的代码、并计算相应的赋值表达式之前不会对局部变量执行真正的实例化。事实上,拥有 arguments 属性的活动对象和拥有与函数局部变量对应的命名属性的可变对象是同一个对象。因此,可以将标识符 arguments 作为函数的局部变量来看待。
  5. 最后,为 this 关键字赋值。如果所赋的值引用一个对象,那么前缀以 this 关键字的属性访问器就是引用该对象的属性。如果所赋(内部)值是 null,那么 this 关键字则引用全局对象。

创建全局执行上下文的过程会稍有不同,因为它没有参数,所以不需要通过定义的活动对象来引用这些参数。但全局执行上下文也需要一个作用域,而它的作用域链实际上只由一个对象--全局对象--组成。全局执行上下文也会有变量实例化的过程。而且,在变量实例化过程中全局对象就是可变对象,这就是为什么全局性声明的变量/函数是全局对象属性的原因。全局执行上下文也会使用 this 对象来引用全局对象。

作用域链与 [[scope]]

作用域指变量被定义的代码区域,每个函数有自己的作用域。函数具有词法作用域,自身作用域是声明时所在作用域,在声明时确定(定义的时候创建的),不受运行时影响。函数名只对自身作用域或者当成父级作用域时可见。

作用域可以嵌套(nested),判断一个变量是否可访问时,JavaSript 解释器首先在当前作用域查找,如果未找到,解释器检查包括当前作用域的作用域(enclosing scope),以此类推,直到顶级作用域,如果还未找到,则抛 ReferenceError。各级作用域形成作用域链。

作用域链是通过将该执行上下文的活动(可变)对象添加到保存于所调用函数对象的 [[scope]] 属性中的作用域链前端而构成的。函数对象在变量实例化过程中会根据函数声明来创建,或者是在计算函数表达式或调用 Function 构造函数时创建。

通过调用 Function 构造函数创建的函数对象,其内部的 [[scope]] 属性引用的作用域链中始终只包含全局对象。通过函数声明或函数表达式创建的函数对象,其内部的 [[scope]] 属性引用的则是创建它们的执行上下文的作用域链。

// 定义一个带有内部函数声明的外部函数,然后调用外部函数 const y = { x: 1 } function f() { let z // 将全局对象 y 引用的对象添加到作用域链的前端 with(y) { z = function() {} } } f() // 在调用 f 函数创建的执行上下文中包含一个由其活动对象后跟全局对象构成的作用域链。 // 而在执行 with 语句时,又会把全局变量 y 引用的对象添加到这个作用域链的前端。在对其中的函数表达式求值的过程中,所创建函数对象 z 的 [[scope]] 属性与创建它的执行上下文的作用域保持一致 // 即,该属性会引用一个由对象 y 后跟调用外部函数时所创建执行上下文的活动对象,后跟全局对象的作用域链。 // 当与 with 语句相关的语句块执行结束时,执行上下文的作用域得以恢复(y 会被移除),但是已经创建的函数对象 z 的 [[scope]] 属性所引用的作用域链中位于最前面的仍然是对象 y。

上下文的几个重要属性:

当 JavaScript 执行时碰到一个变量,它会到作用域链里递归去找,而作用域链是由 variable object 和 global object 组成的一个对象链,global object 包含 JavaSript 预定义好的所有对象,函数的 variable object 则会包含函数内声明的所有东西,包括函数的参数、内部函数、局部变量。创建 Variable object 的过程有三步。

函数参数 Parameter 和 arguments

函数 parameters,形参(formal parameter),指函数定义的参数列表。函数 arguments,实参,指实际传入函数中的参数

函数参数传递有4种:

const a = {} const b = 1 function f(a1, b1) { // 此时函数内对a的修改对 f.caller 是可见的 a1.x = 1 b1 = 2 a1 = { x: 2 } } f(a, b)

JavaScript 函数中参数是传值方式,对应对象,传的值是对象的引用,而不是对象本身。

调用和 IIFE 立即调用函数表达式(Immediately-Invoked Function Expression)

函数的基本调用方式与传统语言相同,用一对括号调用。支持直接或间接的递归(recursive)调用。

self-executing anonymous function(self-invoked anonymous function),参考 http://benalman.com/news/2010/11/immediately-invoked-function-expression/#iife

当一个函数被调用(invoked)时,会创建一个新的执行上下文。

// 当解析器遇到function关键字时,会把它当作函数声明(语句),而不是函数表达式。 // 如果不明确地告诉解析器这是一个表达式,解析器会因为认为是一个匿名函数声明,而需要一个名字,从而报 SyntaxError。 function(){}() // SyntaxError: Function statements require a function name // 当把圆括号放在函数声明后面,期望函数被调用,但圆括号其实和函数无关,只是当作一个控制执行优先级的分组操作。 function f(){}() // SyntaxError: Unexpected token ) // 因为 function 关键字出现在行首,一律解释成函数声明语句,所以有下面一些变通写法,让解析器认为当前函数是个表达式,: (function(){})() (function(){}()) true && function(){}() // 不考虑返回值和可读性 +function(){}() !function(){}() // @kuvos实现,不确定性能,http://twitter.com/kuvos/status/18209252090847232 new function(){}()

解决闭包问题

const els = document.querySelectorAll('a') // 在点击时,i 已经循环执行完,且形成了闭包。 for (let i = 0; i < els.length; i++) { els[i].addEventListener('click', function (e) { console.log(i) }) } // IIFE中, i 被锁住,并传递给 lockedInIndex。 for (let i = 0; i < els.length; i++) { (function (lockedInIndex) { els[i].addEventListener('click', function (e) { console.log(lockedInIndex) }) })(i) }

闭包(Closures)

一个拥有一些变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。闭包工作机制的实现有赖于标识符(或者说对象属性)解析过程中作用域的角色。

ECMAScript 允许使用内部函数--即函数定义和函数表达式位于另一个函数的函数体内而,且这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。在函数内部声明函数,称为嵌套函数(Nested Function)。当其中一个这样的内嵌函数在包含它们的外部函数之外被调用时,就会形成闭包。而当这个嵌套函数执行时,它仍然必需访问其外部函数的局部变量、参数以及其他内部函数。这些变量的值也会受到内部函数的影响。

通过闭包创建额外作用域可以将相关的和具有依赖性的代码组织起来,以便将意外交互的风险降到最低。假设有一个用于构建字符串的函数,为了避免重复性的连接操作,期望使用一个数组按顺序来存储字符串的各个部分,然后再使用 Array.prototype.join 方法输出结果。但是将数组作为函数的局部变量又会导致在每次调用函数时都重新创建一个新数组。

一种解决方案是将这个数组声明为全局变量,这样就可以重用这个数组。缺点是除了引用函数的全局变量会使用这个缓冲数组外,还会多出一个全局属性引用数组自身。如此不仅使代码变得不容易管理,而且,如果要在其他地方使用这个数组时,必须要再次定义函数和数组。这样使得代码不容易与其他代码整合,因为此时不仅要保证所使用的函数名在全局命名空间中是唯一的,而且还要保证函数所依赖的数组在全局命名空间中也必须是唯一的。

而通过闭包可以使作为缓冲器的数组与依赖它的函数关联起来,同时也能够维持在全局命名空间外指定的缓冲数组的属性名,免除了名称冲突和意外交互的危险。 其中的关键在于通过执行一个内链(in-line)函数表达式创建一个额外的执行环境,而将该函数表达式返回的内部函数作为在外部代码中使用的函数。此时,缓冲数组被定义为函数表达式的一个局部变量。这个函数表达式只需执行一次,而数组也只需创建一次,就可以供依赖它的函数重复使用。

// 声明一个全局变量 getStr 并将一次调用一个外部函数表达式返回的内部函数赋给它。 // 这个内部函数会返回一个拼接好的字符串, // 所有可变的属性值都由调用该函数时的参数提供: const getStr = (function() { // 局部变量 - buffArr - 保存着缓冲数组。 // 这个数组只会被创建一次,生成的数组实例对内部函数而言永远是可用的 // 其中的空字符串用作数据占位符,相应的数据将由内部函数插入到这个数组中: var buffArr = [ 'x=', // 可变 '', // 可变 'y=', '' ] // 返回作为对函数表达式求值后结果的内部函数对象,这个内部函数就是每次调用执行的函数 return (function(x, y) { // 将不同的参数插入到缓冲数组相应的位置 buffArr[1] = x buffArr[3] = y return buffArr.join() }) })()

闭包的一个可能最广为人知的应用是 Douglas Crockford’s technique for the emulation of private instance variables in ECMAScript objects。这种应用方式可以扩展到各种嵌套包含的可访问性(或可见性)的作用域结构,包括 the emulation of private static members for ECMAScript objects。

意外创建的闭包可能导致严重的负面效应,而且也会影响到代码的性能。

如果一个构成闭包的函数对象被指定给,比如一个 DOM 节点的事件处理器,而对该节点的引用又被指定给函数对象作用域中的一个活动(或可变)对象,那么就存在一个循环引用。DOM_Node.onevent ->function_object.[[scope]] ->scope_chain ->Activation_object.nodeRef ->DOM_Node。对于纯粹的 ECMAScript 对象而言,只要没有其他对象引用对象 1、2、3,也就是说它们只是相互之间的引用,那么仍然会被垃圾收集系统识别并处理。但是,在 Internet Explorer (在 IE 4 到 IE 6 中核实) 中,如果循环引用中的任何对象是宿主对象( DOM 节点或者 ActiveX 对象),垃圾收集系统则不会发现它们之间的循环关系与系统中的其他对象是隔离的并释放它们。最终它们将被保留在内存中,直到浏览器关闭。

const x = 1 function addGlobalOnClick(dom) { // 如果可以将参数 - dom - 通过类型转换为 ture if(dom) { // 对一个函数表达式求值,并将对该函数对象的引用指定给这个链接元素的 onclick 事件处理器: dom.onclick = function() { // 这个内部函数表达式访问了 x console.log(x) } } }

无论什么时候调用 addGlobalOnClick 函数,都会创建一个新的内部函数(通过赋值构成了闭包)。但如果频繁使用该函数,就会导致创建许多截然不同的函数对象(每对内部函数表达式求一次值,就会产生一个新的函数对象)。

可以通过另一种方式来完成。即单独地定义一个用于事件处理器的函数,然后将该函数的引用指定给元素的事件处理属性。这样,只需创建一个函数对象,而所有使用相同事件处理器的元素都可以共享对这个函数的引用:

const x = 1 function addGlobalOnClick(dom) { if(dom) { dom.onclick = forAddOnClick } } // 声明一个全局函数,dom 事件处理器 function forAddOnClick() { console.log(x) }

两大作用:

当被闭包作用域的变量和闭包自身参数出现同名冲突时,按作用域链,优先级由内而外递减,闭包的优先。

function f() { const x = 1 return function inner(y) {return x + y} } const fn = f()

会进入一个新的执行上下文 outer Context,创建 OuterVariableObject(有 x 和 inner 属性),放到 OuterScopeChain的 最前方。而且会创建 inner 函数,创建时把当前 Scope Chain 作为 inner 函数的[[ Scope ]]属性。

创建的 inner 函数被 fn 变量引用,inner 有[ [Scope]] 属性,引用了 OuterScopeChain,即 [OuterVariableObject, global object],而 OuterVariableObject 又引用了局部变量 x。所以 fn 变量就对 outer 函数体内的局部变量 x 有间接的引用。每次对外部函数的调用,都会产生一次闭包。

this

JavaScript 中 this 关键字是指当前正被执行(executed)或调用(called)的函数的 owner / receiver。如果没有明确定义 owner / receiver,则指最顶级 owner,一般是 window 对象,严格模式是 undefined。

通常,this 是由函数执行中的执行环境的 LexicalEnvironment 决定的,执行期间不能被赋值。

当函数作为对象的方法被调用时,this 指向对象本身,this 不受函数声明时的影响,对多级对象,this 指向最直接的成员对象,原型链上的方法同样适合,同样适用 getter 和 setter。

函数当成构造函数 new 时,this 指向被构造出的对象(实例化的对象)

函数是 DOM 事件处理器时,this 指向触发事件的函数。函数作为内嵌 DOM 事件处理器,this 指向监听器所在 DOM 元素。

一个 execution context 包含任意实现的 specific state,这些对于追踪相关代码执行过程是必要的。每个 execution context 至少有一个 state components。

Component Purpose
code evaluation state Any state needed to perform, suspend, and resume evaluation of the code associated with this execution context.
Function If this execution context is evaluating the code of a function object, then the value of this component is that function object. If the context is evaluating the code of a Script or Module, the value is null.
Realm The Realm Record from which associated code accesses ECMAScript resources.
ScriptOrModule The Module Record or Script Record from which associated code originates. If there is no originating script or module, as is the case for the original execution context created in InitializeHostDefinedRealm, the value is null.

Call()

fn.call(thisArg, arg1, arg2, ...)

通过指定 this,把一个对象的方法借给另一个对象,并接收一个参数列表。call() 在给函数指定 this 时,立即执行函数。

// 使用 call() 反方法化(demethodize)方法 const _str = 'xxyy' function demethodize(method) { return function() { return Function.prototype.call.apply(method, arguments) } } const demethSplit = demethodize(String.prototype.split) console.log(_str.split()) console.log(demethSplit(_str))

apply()

fn.apply(thisArg, [argsArray])

apply 几乎和 call 一样,差别在于 apply 使用数组,另一个 apply 能做,而 call 不能做的是 executing Variable-Arity Functions 或可变函数( variadic functions )。这些函数接受任意数量参数。函数的元素(arity)指函数中可接受的一系列参数。
JavaSript 中一个常见的 variable-arity function 是 Math.max()方法。
thisArg,提供给 fn 调用时的 this 值,如果方法是在非严格模式下,null 和 undefined 会被替换成 global object,基本值(primitive values)会被装箱(boxed)。

bind()

ES5 引入了 bind 方法( this 和函数调用无关,始终指 bind 时传入的对象,且只能 bind 一次)。

bind() 方法调用时,创建并返回一个新函数,this 设置成传入的值,参数为给的系列参数。

const calc = { multiply: function (a, b) { return a * b }, multiplyResult: function () { return [].reduce.call(arguments, this.multiply) } } // calc.multiplyResult对传入的每个参数做相乘 var partFunc = calc.multiplyResult.bind(calc, 1, 2, 3) partFunc(4, 5) // 120 = ( 6 * 4 * 5)

ES6 引入箭头函数( this 保持箭头函数所在上下文 this )。

顶级、内置函数



文章参考