js函数

js函数

  1. 只定义一次, 但可能被执行或调用任意次. js的函数是参数化的, 就是函数的定义会包括一个称为形参的标识符列表, 这些参数像局部参数一样工作. 函数调用时为形参提供实参的值. 函数用实参值来计算返回值, 成为函数调用表达式的值

  2. 每次调用除了拥有一个值外, 还有本地调用的上下文, 就是this关键字的值 this用法可以看只在运行时确定 javascript中this指向由函数调用方式决定

  3. 如果函数挂载在一个对象上, 作为对象的一个属性, 就成为他是对象的方法. 用于初始化一个新创建的对象的函数成为构造函数.

  4. js中, 函数就是对象.所以可以给他们设置属性, 甚至调用他们的方法.

  5. js函数还能嵌套在其他函数中定义, 这样他们就能访问他们被定义时所处的作用域中的任何变量. 构成闭包

函数定义

function关键字, 在函数定义表达式和函数声明语句中用.

3部分组成

  1. 函数名称标识符: 这个是函数声明语句必需的. 用途就像变量的名字, 新定义的函数对象会赋值给这个变量name. 对函数表达式来说这个名字是可选的, 如果存在, 该名字只存在于函数体中并指代该函数对象本身, 和函数声明有点区别. 一个不在函数体中, 一个在函数体{}.
  2. 一对圆括号(): 用包含0个或多个用逗号隔开的标识符组成的列表. 都是函数参数名称, 就像函数体中的局部变量一样(就当成局部变量好了).
  3. 一对花括号{}: 包含0到多条js语句, 这些语句构成函数体.

一条函数声明语句实际上声明了一个变量, 并把一个函数对象赋值给它.函数定义表达式并没有声明一个变量.

所以函数声明中的函数名是一个指代自己的名称. 函数表达式中如果包含名称, 函数的局部作用域将会包含一个绑定到函数对象的名称.实际上函数的名称将会成为函数内部的一个局部变量. 通常而言, 函数表达式是不需要名称的. 特别适合来定义那些只会用到一次的函数.

还有就是提升问题, 声明会, 表达式不会.

函数大多有一条return语句, 用来导致函数停止执行,并返回表达式的值. 如果只有return就返回undefined, 如果函数不包含return就只是执行完函数后返回undefined, 有些需要返回值, 有些不需要. 不需要个也叫过程.

函数声明语句并不是真正的语句, ECMA只是允许它作为顶级语句, 不能出现在循环, 条件判断, 或try/catch/finally和with中. 函数表达式可以出现在任何地方.

8.2 函数调用

作为函数主体的js代码在定义之时并不会执行, 只有调用该函数时他们才会执行. 有4种方式来调用js函数.

  • 作为函数
  • 作为方法(保存在对象那个属性里的js函数)
  • 作为构造函数
  • 通过他们的call()和apply()方法简介调用

8.2.1 函数调用

使用调用表达式(由多个函数表达式组成)可以进行普通的函数调用也可以进行方法调用.

函数表达式还可以是一个属性访问表达式[](是对象的一个属性o.m()或数组的一个元素)

1
var total = distance(0, 0, 2, 1) + distance(2, 1, 3, 5)

注意this, 以函数形式调用的函数通常不使用this关键字. 不过this可以用来判断当前是否是严格模式

1
var strict = (function() { return !this }())

8.2.2 方法调用

一个方法就是保存在一个对象属性里的js函数. 比如一个对象o和一个函数f

1
2
3
4
5
// 这里说的就是给对象o定义一个名为m()的方法
o.m = f

//调用的时候就
o.m()

对方法调用的参数和返回值的处理和普通函数调用完全一致. 只有一个区别而已, 就是调用上下文 this的区别

方法和this关键字是面向对象编程的核心. 任何函数只要作为方法调用实际上都会传入一个隐式的实参-实参是个对象(就是那个o), 方法调用的母体就是这个对象. 函数基于一个对象进行操作.

例如:

1
rect.setSize(width, height)

第一行的方法调用语法非常清晰地表明这个函数执行的载体就是rect对象, 函数中的所有操作都讲基于这个对象.

方法链: 当方法的返回值是一个对象, 这个对象还可以再调用它的方法. 当方法不需要返回值时, 最好直接返回this.
不要将方法的链式调用和构造函数的链式调用混为一谈

在说下this: 是一个关键字, 不是变量,也不是属性名. 不允许赋值诶.

所以this和变量不同, this没有作用域的限制, 嵌套的函数不会从调用它的函数中继承this:(很多人误以为调用嵌套函数时this会指向调用外层函数的上下文)

  1. 如果嵌套函数作为方法调用, 其this的值指向调用它的对象.
  2. 如果嵌套函数作为函数调用, 其this的值不是全局对象(非严格)就是undefined(严格)

如果你想访问外部函数的this值, 需要将this保存在一个变量中, 这个变量和内部函数都在同一个作用域内.

javascript中this指向由函数调用方式决定

8.2.3 构造函数调用

如果函数或方法调用之前带有关键字new, 它就构成构造函数调用.

构造函数调用和普通的函数调用以及方法调用在实参处理, 调用上下文和返回值方面都有不同

如果构造函数调用圆括号内包含一组实参列表, 先计算这些实参表达式, 然后传入函数内, 这和函数调用和方法调用时一致的.
但如果构造函数没有形参, js构造函数的语法允许省略实参列表和圆括号, 就留一个名字.

1
2
3
// 两者等价
var o = new Object()
var o = new Object

构造函数调用创建一个新的空对象, 这个对象继承自构造函数的prototype属性, 构造函数试图初始化这个新创建的对象, 并将这个对象用作其调用上下文, 因此构造函数可以使用this关键字来引用这个新创建的对象.

注意, 尽管构造函数看起来像一个方法调用, 他依然会使用这个新对象作为调用上下文, 也就是说,在表达式new o.m()中,调用上下文并不是o, 而是新建好的那个对象.

构造函数通常不使用return关键字, 他们通常初始化新对象, 当构造函数的函数体执行完毕事, 它会显示返回.
当然使用return返回这个对象,就是这个对象咯.(返回别的对象就是别的对象)
如果只给了return但没有指定返回值, 或者返回一个原始值, 那么这时将忽略返回值, 同时使用这个新对象作为调用结果.

8.2.4 间接调用

js的函数也是对象, 函数对象也有方法啊. 其中两个方法就是call()apply()可用来间接地调用函数.
两个方法都允许显示指定调用所需的this值, 也就是说,任何函数可以作为任何对象的方法来调用, 哪怕这个函数不是那个对象的方法.

8.3 函数的实参和形参

js函数定义没有指定函数形参的类型啊, 对传入的实参也没有做类型检查, 甚至数量也没有检查.

8.3.1 可选形参

当传入的实参比函数声明指定的形参个数时, 剩下的形参将设为undefined. 所以可以给省略的参数赋一个合理的默认值.

当然可以用一个undefined或者null来作为一个占位符.a

8.3.2 可变长的实参列表: 实参对象

当传入的实参比函数声明指定的形参个数时, 没办法直接获取未命名值的引用. 参数对象解决这个问题.
标识符arguments指向是实参对象的引用, 实参对象是一个类数组对象, 可以用下标,不必非要用名字来得到实参.

1
2
arguments[0]
arguments.length

一般js是省略的实参将都是undefined, 多余的实参自动省略.

实参对象有一个重要用处,就是让函数可以操作任意数量的实参. 类似这种可以接受任意个数的是实参, 这种函数也称为 不定实参函数

注意:不定实参函数的实参个数不能为0. arguments[]对象最适合的场景就是一类函数: 这类函数包括固定个数的命名和必须参数, 以及随后个数不定的可选实参.

类数组对象, arguments并不是真正的数组, 是一个实参对象. 每个实参对象都包含以数字为索引的一组元素以及length属性, 但他毕竟不是真正的数组. 可以理解为他是一个对象, 只是碰巧具有以数字为索引的属性.

数组对象包含一个非同寻常的特性.非严格模式下, 当一个函数包含若干形参, 实参对象的数组元素是函数形参对应实参的别名, 实参对象中以数字索引, 并且形参名称可以认为是相同变量的不同命名.

1
2
3
4
5
6
7
function f(x) {
console.log(x); // 输出实参初始值
arguments[0] = null; // 修改实参数组对象元素, 对应形参别名x也修改了
console.log(x); // 输出null
}

f(1)

如果实参对象是一个真正的数组的话, 那么修改arguments[0] = null是不会影响到console.log(x)的值的.
这里arguments[0]x指代同一个值, 修改其中一个的值会影响到另一个.

es5中移除了这个特殊特性. 不过浏览器上还是能跑的
非严格模式下arguments是个标识符, 可以用, 但在严格模式下, arguments是一个保留字

callee和caller属性

arguments实参对象除了数组元素, 还定义了calleecaller属性. 严格模式下对这两个属性读写都会长生一个类型错误. 非严格模式下, ECMA宝追规范规定callee属性指代当前正在运行的函数, caller是非标准的, 指代当前正在执行的函数的函数.
callee属性在某些时候非常有用, 比如匿名函数中通过callee来递归地调用自身.

8.3.3 将对象属性用作实参

当一个函数包含超过3个形参时, 对于程序员来说, 记住调用函数中实参的正确顺序实在让人头疼. 最好用键值对的形式来传入参数, 这样参数的顺序就无关紧要了. 所以在定义函数的时候, 传入的实参都写入一个单独的对象中, 对象中的k/v就是真正需要的实参数据.

8.3.4 实参类型

js中形参并没有声明类型, 在形参传入函数体之前也没有做任何类型检查.

8.4 作为值的函数

函数可以定义, 也可以调用, 这是函数最重要的特性. js中函数不经是一种语法, 也是值, 也就是说可以将函数赋值给变量, 存储在对象的属性或数组的元素中, 作为参数传入另外一个函数等

看一个函数定义:

1
function square(x) { return x*x}

这个定义创建一个新的函数对象, 并将其赋值给变量square. 函数的名字实际上是看不见的,square仅仅是变量的名字, 这个变量指点函数对象. 函数还能赋值给其他的变量, 并且仍可以正常工作.

1
2
3
var s = square
square(4)
s(4)

除了可以将函数赋值给变量, 同样可以将函数赋值给对象的属性. 当函数作为对象属性调用时, 函数就成为方法.

1
2
var o = {square: function(x) {return x*x}}
var y = o.square(16)

函数甚至不需要带名字, 当把它们赋值给数组元素时:

1
2
var a = [function(x) { return x*x }, 20]
a[0](a[1]) // 合法的函数调用表达式

看一下函数用作值的, 就是要给给定操作符, 操作数的函数

1
2
3
4
5
function add(x, y) { return x + y}

function operate(operate, operand1, operand2) {
return operate(operand1, operand2)
}

又比如Array.sort()来对数组元素进行排序, 但排序的规则有很多(比如基于数值大小, 字母表排序, 日期大小, 从小到大, 从大到小等等), sort()可以接受一个函数作为参数, 用来处理具体的排序操作.

自定义函数属性

js中的函数并不是原始值, 而是一种特殊的对象. 是对象就可以有属性, 所以函数可以有属性. 当函数需要一个静态变量在调用时保持某个值不变, 最方便的就是给函数定义属性, 而不是定义全局变量, 显然定义全局量会让命名空间变得更加杂乱无章.

比如你想返回一个唯一整数的函数, 不管在哪调用都会返回这个整数.

1
2
3
4
5
6
7
8
9
// 初始化函数对象的计数器属性
// 由于函数声明被提前了, 因此这里是可以在函数声明之前给他的成员赋值的
uniqueInteger.count = 0

// 每次调用这个函数都会返回一个不同的整数
// 它使用一个属性来记住下一次要返回的值
function uniqueInteger() {
return uniqueInteger.count++
}

计算阶乘

1
2
3
4
5
6
7
8
9
function factorial(n) {
if (isFinite(n) && n > 0 && n === Math.round(n)) {
if (!(n in factorial))
factorial[n] = n* factorial(n-1)
return factorial[n]
}
else return NaN
}
factorial[1] = 1

8.5 作为命名空间的函数

js的函数作用域概念: 在函数中声明的变量在整个函数体内都是可见的(包括在嵌套的函数中), 在函数外部是不可见的. 不在任何函数内声明的变量是全局变量, 在整个js程序中都是可见的.

js无法声明只在一个代码块内可见的变量, es6中可以了. 这里的话可以简单地定义一个函数用作临时的命名空间, 不会污染全局命名空间.

1
2
3
4
5
function mymodule() {
// 局部变量
}

mymodule() // 调用

上面定义了一个单独的全局变量, 叫mymodule的函数, 可以直接定义一个匿名函数, 并在单个表达式中调用它.

1
2
3
(function() {   // 匿名函数

}()) // 结束函数定义并立即调用它

定义匿名函数并立即在单个表达式中调用它的写法非常常见. 使用(才会正确得把他解析为函数定义表达式.

8.6 闭包

js也使用词法作用域, 也就是说,函数的执行依赖于变量作用域, 这个作用域是在函数定义时决定的, 而不是函数调用时决定的, 为了实现词法作用域, js函数对象的内部状态不仅包含函数的代码逻辑, 还必须引用当前的作用域链. 函数对象可以通过作用域链相互关联起来, 函数体内部的变量都可以保存在函数作用域内, 称为闭包

从技术角度讲, 所有的js函数都是闭包: 他们都是对象, 他们都关联到作用域链. 定义大多数函数时的作用域链在调用函数时依旧有效, 但这并不影响闭包.
当调用函数时闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域时, 事情就变得非常微妙. 当一个函数嵌套了另外一个函数, 外部函数讲嵌套的函数对象作为返回值返回的时候往往会发生这种事情.

理解闭包首先要了解嵌套函数的词法作用域规则.

1
2
3
4
5
6
7
var scope = "global scope"          // 全局变量
function checkscope() {
var scope = "local scope" // 局部变量
function f() { return scope}
return f() // 注意这里有() 直接返回结果
}
checkscope() // => "local scope"
1
2
3
4
5
6
7
var scope = "global scope"          // 全局变量
function checkscope() {
var scope = "local scope" // 局部变量
function f() { return scope}
return f // 注意这里没() 返回的是函数内嵌套的一个函数对象.
}
checkscope()() // => "local scope"

词法作用域的基本规则: js函数的执行用到了作用域链, 这个作用域链是函数定义的时候创建的. 嵌套的函数f()定义在这个作用域里, 其中的变量scope一定是局部变量, 不管在何时何地执行函数f(), 这种绑定在执行f()时依旧有效, 因此最后一样代码返回local scope而不是global scope.
简言之, 闭包的这个特性强大到让人吃惊: 他们可以捕捉到局部变量(和参数), 并一直保存下来, 看起来像这些变量绑定到了其中定义他们的外部函数.

更底层, 了解基于栈的CPU架构: 如果一个函数的局部变量定会在CPU的栈中, 那么当函数返回时他们的确就不存在了.

1
2
3
4
var uniqueInteger = (function() {
var counter = 0;
return function() { return counter++}
})

也可以嵌套多个, 多个嵌套函数都共享一个作用域链.

1
2
3
4
5
6
7
function counter() {
var n = 0;
return {
count: function() { return n++},
reset: function() { n = 0}
}
}

一个对象下的共享, 不同对象下的不影响.

从技术角度看, 其实可以将这个闭包合并为属性存取器方法gettersetter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function counter(n) {   //  函数参数n是一个私有变量, count函数并没有声明局部变量
return {
// 属性getter方法返回并给私有计数器var递增1
get count() { return n++; },
set count(m) {
if (m >= n) n = m;
else throw Error("count can only be set to a large value")
}
}
}

var c = counter(1000)
c.count
c.count = 2000
1
2
3
4
5
6
7
8
9
10
11
12
13
function addPrivateProperty(o, name, preficate) {
var value
// getter
o["get" + name] = function() { return value }

// setter
o["set" + name] = function(v) {
if (predicate && !predicate(v))
throw Error("set" + name + ": invalid value " + v)
else
value = v
}
}

注意咯, 这个函数的gettersetter函数, 所操作的属性值并没有存储在对象o中, 这个值仅仅保存在函数中的局部变量中.
也就是说, 对于两个存取器方法来说这个变量是私有的, 没有办法绕过存取器方法来设置或修改这个值.

要注意如果用循环创建很多个闭包, 会犯一个错误. 所有的闭包都共享一个值. 关联到闭包的作用域链都是”活动的”, 嵌套的函数不会将作用域内的私有成员复制一份, 也不会对所绑定的变量生成静态快照.

书写闭包的时候还需要注意, this是js的关键字, 而不是变量. 每个函数调用都包含一个this值, 如果闭包在外部函数里是无法访问this的, 除非外部函数讲this转存为一个变量.
arguments类似, 他并不是一个关键字, 但在调用每个函数时都会自动声明它, 由于闭包具有自己所绑定的arguments, 因此闭包内无法直接访问外部函数的参数数组, 除非外部函数将参数数组保存到另一个变量中.

8.7 函数属性, 方法和构造函数

js中函数是值, 用typeof运算符返回字符串'function', 但函数又是js中特殊的对象. 所有有属性和方法, 甚至可以用Function()构造函数来创建新的函数对象.

8.7.1 length属性

函数体里的arguments.length表示传入函数的实参的个数. 而在函数本身的length属性则有不同的含义. 函数的length属性是只读属性, 它代表函数是参数的数量, 这里的参数指的是’形参’而非’实参’, 也就是函数定义时给出的形参个数, 通常也是在函数调用时期望传入函数的参数个数.

例子是一个check()的函数, 从另外一个 函数给他传入arguments数组, 它比较arguments.length(实际传入的实参个数)和arguments.callee.length(期望传入的实参个数)来判断所传入的实参个数是否正确.

1
2
3
4
5
6
7
8
9
10
11
function check(args) {
var actual = args.length; // 实参的真实个数
var expected = args.callee.length; // 期望的是从哪个数
if (actual !== expected)
throw Error("Expected " + expected + "args; got" + actual)
}

function f(x, y, z) {
check(arguments);
return x + y + z;
}

8.7.2 prototye属性

每个函数都有一个prototype属性, 这个属性是指向一个对象引用, 这个对象称作原型对象(prototype object). 每一个函数都包含不同的原型对象, 当将函数用作构造函数的时候, 新创建的对象会从原型对象上继承属性.

8.7.3 call()方法和apply()方法

深入浅出 妙用Javascript中apply、call、bind

我们可以将call()apply()看做某个对象的方法, 通过调用方法的形式来简介调用函数. call()apply()的第一个实参是要调用函数的母对象, 他是调用上下文, 在函数体内通过this来获得对他的引用. 想要以对象o的方法来调用函数f(), 可以这样使用过call()apply()

1
2
f.call(o)
f.apply(o)

每行代码和下面代码的功能类似(假设对象o中预先不存在名为m的属性)

1
2
3
o.m = f;        // 将f存储为o的零临时方法
o.m(); // 调用它, 不传入参数
delete o.m; // 将临时方法删除

在ECMAScript5的严格模式中, call()apply()的第一个实参都会变为this值, 哪怕传入的实参是原始值甚至是nullundefined. 在ECMASceipt3和费严格模式中, 传入的nullundefined都会被全局对象代替, 而其他原始值则会被相应的包装对象所替代.

对于call()来说, 第一个调用上下文实参之后的所有实参就是要传入待调用函数的值. 比如, 以对象o的方法的形式来调用函数f(), 并传入两个参数, 可以使用这样的代码:

1
f.call(o, 1, 2)

apply()方法和call()类似, 但传入实参的形式和call()有所不同, 它的实参都放入一个数组中

1
f.apply(o, [1, 2])

如果一个函数的实可以是任意数量, 给apply()传入的参数数组可以是任意长度的. 比如, 为了找出数组中最大的数组元素, 调用Math.max()方法的时候可以给apply()传入一个包含任意个元素的数组:

1
var biggest = Math.max.apply(Math, array_of_numbers);

需要注意的事, 传入apply()的参数数组可以是类数组对象, 也可以是真实数组. 实际上, 可以将当前函数的arguments数组直接传入(另一个函数的)apply()来调用另一个函数参数

1
2
3
4
5
6
7
8
9
10
11
// 将对象o中名为m()的方法替换为另一个方法
// 可以砸调用原始的方法之前和之后记录日志消息
function trace(o, m) {
var original = o[m]
o[m] = function() {
console.log(new Date(), "Entering:", m)
var result = original.apply(this, arguments)
console.log(new Date(), "Exiting:", m)
return result
}
}

trace()这个函数接收两个参数, 一个对象和一个方法名, 他将执行的方法替换为一个新方法, 这个新方法是”包裹”原始方法的另一个泛函数. 这种动态修改已有方法的做法有时称作”monkey-patching”

8.7.4 bind()方法

bind()在ECMAScript5中新增的方法,但在ECMAScript3中可以轻易模拟bind(). 从名字上就可以看出, 这个方法的主要作用就是将函数绑定至某个对象. 当在函数f()上调用bind()方法并传入一个对象o作为参数, 这个方法将返回一个新的函数, (已函数调用的方式)调用新的函数将会把原始的函数f()当做o的方法来调用. 传入新函数的任何实参都将传入原始函数.

1
2
3
4
function f(y) { return this.x + y } // 待绑定的函数
var o = { x : 1} // 将要绑定的对象
var g = f.bind(o) // 通过调用g(x)来调用o.f(x)
g(2) // => 3

可以通过如下代码轻易地实现这种绑定

1
2
3
4
5
6
7
// 返回一个函数, 通过调用它来调用o中的方法f(), 传递它所有的实参
function bind(f, o) {
if (f.bind) return f.bind(o) // 如果bind()方法存在的话, 就使用bind()方法
else return function() {
return f.apply(o, arguments)
}
}

EMCAScript 5中的bind()不仅仅是讲函数绑定至一个对象, 他还附带一些其他应用: 除了第一个实参之外, 传入bind()的实参也会绑定至this, 这个附带的应用是一种常见的函数式编程技术, 有时也被称为”柯里化”(currying)

1
2
3
4
5
6
7
8
9
var sum = function(x, y) { return x + y }   // 返回两个实参的和值
// 创建一个类似sum的新函数, 但this的值绑定到null
// 并且第一个参数绑定到1, 这个新的函数期望只传入一个实参
var succ = sum.bind(null, 1)
succ(2) // => 3 x绑定到1, 并传入2作为实参y

function f(y, z) { return this.x + y + z } // 另外一个做累加计算的函数
var g = f.bind({ x:1 }, 2) // 绑定this和y
g(3) // => 6 this.x绑定到1, y绑定到2, z绑定到3

我们可以绑定this的值并在ECMAScript 3 中实现这个附带的应用.

ES3的bind写法

8.7.5 toString()方法

和所有js对象一样, 函数也有toString()方法, 返回一个字符串, 这个字符串和函数声明语句的语法相关. 大部分的toString()方法都返回函数的完整源码. 内置函数往往返回一个类似 “[native code]” 的字符串作为函数体.

8.7.6 Function()构造函数

不管是通过函数定义语句还是函数直接量表达式, 函数的定义都要使用function关键字. 但函数还可以通过Function构造函数来定义,

1
var f = new Function("x", "y", "return x*y")

这一行代码创建一个新的函数, 这个函数和通过下面代码定义的函数几乎等价:

1
var f = function(x, y) { return x*y }

Function()构造函数可以传入任意数量的字符串实参, 最后一个是实参所表示的文本就是函数体, 他可以是包含任意的js语句, 每两条语句之间用分号隔开. 传入构造函数的其他所有的实参字符串是指定函数的形参名字的字符串. 如果定义的函数不包含任何参数, 只需给构造函数简单地传入一个字符串-函数体-即可.

注意: Function()构造函数并不需要通过传入实参以指定函数名. 就想函数直接量一样, Function()构造函数穿件一个匿名函数.

几点注意:

  • Function()构造函数语序js在运行时动态地创建并编译函数
  • 每次调用Function()构造函数都会解析函数体, 并创建新的函数对象. 如果实在一个循环或多次调用的函数中执行这个构造函数, 执行效率会受影响. 相比之下, 循环中的嵌套函数和函数定义表达式则不会每次执行时都重新编译.
  • 最后一点, 也就是Function()构造函数, 他所创建的函数并不是使用词法作用域, 相反, 函数体代码的编译总是会在顶层函数执行(全局作用域)

我们可以将Function()构造函数认为是在全局作用域中执行的eval(), eval()可以在自己的私有作用域内定义新变量和函数, Function()构造函数在实际编程过程中很少会用到.

8.7.7 可调用的对象

“类数组对象:并不是真正的数组, 但大部分场景下可以将其当做数组来对待. 对于函数也存在类似的情况, “可调用的对象”(callable object)是一个对象, 可以在函数调用表达式中调用这个对象. 所有的函数都是可调用的额, 但并非所有的可调用对象都是函数.

可调用对象在2个jd实现中不能算作函数. 首先IE Web浏览器实现了客户端方法(诸如Window.alert()Document.getElementById()), 使用了可调用的宿主对象, 而不是内置函数对象. IE中的这些方法在其他浏览器中也都存在, 但他们本质上不是Function()对象. IE9件=将他们是吸纳为真正的函数, 因此这类可调用的对象将越来越罕见.

另一个常见的可调用对象是RegExp对象(在众多浏览器中均有实现), 可以直接调用RegExp对象, 这比调用它的exec()方法更快捷一些. 在js中这是一个彻头彻尾的非标准特性. 所以代码最好不要对可调用的RegExp对象有太多依赖, 这个特性在不久的将来可能会废弃并删除. 对RegExp执行typeof运算的结果并不统一, 有些是function, 有些返回object

想检测一个对象是否是真正的函数对象(并且具有函数方法), 可以参照代码检测它的class属性

1
2
3
function isFunction(x) {
return Object.prototype.toString.call(x) === "[object Function]"
}

8.8 函数式编程

js并非函数时编程语言, 但js中可以像操控对象一样操控函数, 也就是说可以在就是中应用函数式编程技术. ES5中的数组方法就可以非常适合用于函数式编程风格.

8.8.1 使用函数处理数组

比如计算平均值和标准差. 不使用函数式编程风格是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var data = [1, 1, 3, 5, 5];

// 平均数是所有元素的累加和值除以元素个数
var total = 0;
for(var i = 0; i < data.length; i++) {
total += data[i];
}
var mean = total/data.length;

// 计算标准差, 首先计算每个数据减去平均数之后偏差的平方然后求和
total = 0;
for(var i = 0; i < data.length; i++) {
var deviation = data[i] - mean;
total += deviation * deviation;
}
var stddev = Math.sqrt(total/(data.length-1))

可以使用数组方法map()reduce()来实现同样的计算, 这种实现及其简介

1
2
3
4
5
6
7
8
9
// 首先定义两个简单的函数
var sum = function(x, y) { return x+y };
var square = function(x) { return x*x };

// 然后将这些函数和数组方法配合使用个计算出平均值和标准差
var data = [1, 1, 3, 5, 5];
var mean = data.reduce(sum)/data.length;
var deviations = data.map(function(x) { return x-mean })
var stddev = Math.sqrt(deviations.map(square).reduce(sum)/(data.length-1))

如果用ES3呢, 要自定义map()reduce()函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 对于每个数组元素调用函数f(), 并返回一个结果数组
// 如果Array/prototype.map定义了话, 就使用这个方法
var map = Array.prototype.map
? function(a, f) { return a.map(f) }
: function(a, f) {
var results = []
for(var i = 0, length = a.length; i < len; i++) {
if (i in a) results[i] = f.call(null, a[i], i, a)
}
return results;
}

// 使用函数f()和可选的初始值将数组a减至一个值

8.8.2 高阶函数

指操作函数的函数, 他接受一个或多个函数作为参数, 并返回一个新函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 这个高阶函数返回一个新的函数, 这个新函数将他的实参传入f()
// 并返回f的返回值的逻辑非
function not(f) {
return functin() { // 返回一个新函数
var result = f.apply(this, arguments) // 调用f()
return !result // 对结果求反
}
}

var enen = function(x) {
return x%2 === 0
}

var odd = not(even) // 一个新函数, 所做的事情和even相反
[1, 1, 3, 5, 5].every(odd)

上面的not()函数就是一个高阶函数, 因为他接受一个函数作为参数, 并返回一个新函数.

1
2
3
4
5
6
7
8
9
10
// 所返回的函数的参数应当是一个实参数组, 并对每个数组元素执行函数f()
// 并返回所有计算结果组成的数组
// 可以对比一下这个函数和上下文提到的map()函数
function mapper(f) {
return function(a) { return map(a, f) }
}

var increment = function(x) { return x+1 }
var incrementer = mapper(increment)
incrementer([1, 2, 3])

后面还有partial()memoize()都挺重要的高阶函数.

8.8.3 不完全函数

这里讨论的是一种函数变换技巧, 即把一次完整的函数调用拆成多次函数调用, 每次传入的实参都是完整实参拿得一部分, 每个拆分开的函数叫做不完全函数(partial function), 每次函数调用叫做不完全调用(partial application), 这种函数变换的特点是每次调用都返回一个函数, 知道得到最终运行结果为止. 比如对函数f(1,2,3,4,5,6)的调用修改为等价的f(1,2)(3,4)(5,6), 后者包含三次调用, 和每次调用相关的函数就是”不完全函数”

函数f()bind方法返回一个新函数, 给新函数传入特定的上下文和一组指定的参数, 然后调用函数f(). 我们说它把函数”绑定至“对象并传入一部分参数. bind()方法只是将实参放在(完整实参列表的)左侧, 也就是说传入bind()的实参都是放在传入原始函数的实参列表开始的位置, 但有时候我们期望将传入bind()的实参放在(完整实参列表的)右侧:

8.8.4记忆memorization

比如阶乘函数, 他可以将上次的计算结果缓存起来. 在函数式编程当中, 这种缓存技巧叫做 记忆, 下面的代码展示一个高阶函数, memorize()节后一个函数作为实参, 并返回带有记忆能力的函数.

记忆只是一种编程技巧, 本质上是牺牲算法的空间复杂度以换取更优的时间复杂度, 在客户端js中代码的执行时间复杂度往往成为瓶颈, 因此大多说场景下, 牺牲空间换时间是非常可取.

1
2
3
4
5
6
7
8
9
10
11
// 返回f()的带有记忆功能的版本
// 只有当f()的实参的字符串表示都不相同时他才会工作
function memorize(f) {
var cache = {} // 将值保存在闭包内
return function() {
// 将实参转换为字符串形式, 并将其用作缓存的键
var key = arguments.length + Array.prototype.join.call(arguments, ",")
if (key in cache) return cache[key]
else return cache[key] = f.apply(this, arguments)
}
}

memorize()函数创建一个新的对象, 这个对象被当做缓存(的宿主)并赋值给一个局部变量, 因此对于返回的函数来说他是私有(在闭包中). 所返回的函数将他的实参数组转换成字符串, 并将字符串用作缓存对象的数组名, 如果在缓存中存在这个值, 则直接返回它.

否则, 就调用既定函数对实参进行计算, 将计算结果缓存起来并返回.

参考