JavaScript语言精粹-笔记2-函数
函数(Function)的定义
- 函数就是对象, 或者说函数是种特殊的对象-函数对象(Function Objects)
- 函数可以保存在变量、对象和数组中
- 函数可以被当做参数传给其他函数, 函数也可以再返回函数
- 函数的与众不同在于他可以被调用
- 每个函数被创建时会附加两个隐藏属性: 函数的上下文和实现函数行为的代码
下面是函数和对象的constructor的区别对比:
// 定义函数对象func
var Func = function() {};
// 实例化func赋值给funcer
var funcer = new Func();
// 对应对象obj
var obj = {};
Func.constructor === Function // true
Func.constructor.constructor === Function // true
typeof Func === 'function' // true
funcer.constructor === Func // true
funcer.constructor.constructor === Function // true
typeof funcer === 'object' // true
obj.constructor === Object // true
obj.constructor.constructor === Function // true
typeof obj === 'object' // true
函数字面量(Function Literal)
通过函数字面量来创建函数对象
var add = function (a, b) {
return a + b;
}
函数字面量包含四个部分:
- 第一部分是保留字
function
- 第二部分是函数名, 它可以省略, 没有函数名的函数被称为匿名函数(anonymous)
- 第三部分是包围在圆括号内的一组参数
- 第四部分是包围在花括号中的一组语句. 这些语句是函数的主体, 他们在函数被调用时执行
调用(Invocation)
调用一个函数会暂停当前函数的执行, 传递控制权和参数给新函数. 除了声明时定义的形式参数, 每个参数还接收两个附加参数: this和arguments. this的值取决于调用的模式, 在JavaScript中一共有4中模式: 方法调用模式、函数调用模式、构造器调用模式、apply调用模式.
方法调用模式(The Method Invocation Pattern)
当一个函数被保存为对象的一个属性时, 我们称它为一个方法, 当一个方法被调用时, this被绑定到该对象, 示例如下:
var myObject = {
value: 0,
increment: function (inc) {
this.value += typeof inc === 'number' ? inc : 1;
}
}
myObject.increment();
console.log(myObject.value); // 1
myObject.increment(2);
console.log(myObject.value); // 3
函数调用模式(The Function Invocation Pattern)
当一个函数并非一个对象的属性时, 那么它就是被当做一个函数来调用的, 此时this被绑定到window对象. 说中说这是语言设计时的一个错误, 如果语言设计正确, 那么当内部函数被调用时, this应该仍然仍然绑定到外部函数的this变量. 不过可以将外部函数this赋值给其它局部变量, 此时内部函数可以通过这个变量访问外部函数this.
在上一示例的myObject上增加一个double函数:
myObject.double = function () {
var that = this;
var helper = function () {
that.value = add(that.value, that.value);
}
helper();
}
myObject.double();
console.log(myObject.value); // 6
构造器调用模式(The Constructor Invocation Pattern)
JavaScript是一门基于原型继承的语言, 而当今大多数语言是基于类的.
如果一个函数前面带上new来调用, 那么背地里将会创建一个连接到该函数的prototype成员的新对象, 同时this会绑定到那个新对象上, 示例如下:
// 创建一个名为Quo的构造器函数, 它构造了一个带有status属性的对象
var Quo = function (string) {
this.status = string;
}
// 给Quo的所有示例提供一个名为get_status的公共方法
Quo.prototype.get_status = function () {
return this.status;
}
// 构造一个Quo示例
var myQuo = new Quo("confused");
console.log(myQuo.get_status); // confused
这种结合new前缀来调用的函数称为构造器函数, 并约定函数名首字母为大写字母.
Apply调用模式(The Apply Invocation Pattern)
apply方法让我们构建一个参数数组传递给调用函数. 它也允许我们选择this的值. apply方法接收两个参数, 第1个是要绑定this的值, 第2个就是一个参数数组. 示例如下:
var statusObject = {
status: 'A-OK'
};
var status = Quo.prototype.get_status.apply(statusObject); // A-OK
参数(Arguments)
当函数被调用时, 在函数内部可以使用arguments参数访问所有它被调用时传递给它的参数列表. 这使得编写一个无需指定参数个数的函数成为可能, 示例:
var sum = function () {
var i, sum = 0;
for (i = 0; i < arguments.length; i ++) {
sum += arguments[i];
}
return sum;
}
console.log(sum(4, 8, 15, 16, 23, 42)); // 108
因为语言的一个设计错误, arguments并不是一个真正的数组. 它只是一个”类似数组(array-like)“的对象. arguments拥有一个length属性, 但它没有任何数组的方法.
返回(Return)
return语句用来使函数提前返回, 当return被执行时, 函数立即返回而不再执行余下的语句.
一个函数总是会返回一个值, 如果没有指定返回值, 则返回undefined.
如果函数调用时在前面加上了new前缀, 且返回值不是一个对象, 则返回this(该对象本身).
异常(Exceptions)
throw用于抛出异常, 会中断函数的执行, 并抛出一个exception对象, 外部使用try catch进行捕捉异常.
扩充类型的功能(Augmenting Types)
JavaScript允许给语言的基本类型扩充功能.
比如通过给Function.prototype增加方法来使得该方法对所有函数可用:
Function.prototype.method = function (name, func) {
if (!this.prototype[name]) {
this.prototype[name] = func;
}
return this;
}
给Number增加一个取整方法integer:
Number.method('integer', function () {
return Math(this < 0 ? 'ceil' : 'floor')[this];
})
console.log((-10 / 3).integer()); // 3
给String增加一个移除首尾空白的方法trim:
String.method('trim', function() {
return this.replace(/^\s+|\s+$/g, '');
})
console.log(' neat '.trim()); // neat
通过给基本类型增加方法, 我们可以极大地提高语言的表现力. 因为JavaScript原型继承的动态本质, 新的方法立刻被赋予到所有的对象实例上, 哪怕对象实例是在方法被增加之前就创建好了.
递归(Recursion)
递归函数就是会直接或间接地调用自身的一种函数. 递归是一种强大的编程技术, 它把一个问题分解成一组相似的子问题, 每一个都用寻常解去解决. 一般来说, 一个递归函数调用自身去解决它的子问题.
尾递归(tail recursion 或 tail-end recursion): 一种在函数的最后执行递归调用语句的特殊形式的递归.
尾递归优化: 指如果一个函数返回自身递归调用的结果, 那么调用的过程会被替换为一个循环, 这样可以显著提高速度.
作用域(Scope)
作用域控制着变量与参数的可见性和生命周期, 可以减少名称冲突, 并且提供了自动内存管理.
大多数类C语言语法的语言都拥有块级作用域, 在一个代码块中定义的所有变量在代码块的外部是不可见的. 定义在代码块中的变量在代码执行行结束后会被释放掉.
JavaScript在ECMAScript 2015后支持块级作用域, 需使用let或者const替换var来生命变量.
闭包(Closure)
作用域的好处是内部函数可以访问定义它们的外部函数的参数和变量(除了this和arguments), 我们利用这个特性让函数返回一个新的函数或对象, 这个返回的函数可以访问它外部函数的上下文, 这被称为闭包.
修改之前构造器调用模式
的示例:
// 创造一个名为quo的构造函数, 它构造出带有get_status方法和status私有属性的一个对象
var quo = function (status) {
return {
get_status: function () {
return status;
}
}
}
// 构造一个quo示例
var myQuo = quo("amazed");
console.log(myQuo.get_status()); // amazed
模块(Module)
我们可以使用函数和闭包来构造模块. 模块是一个提供接口却隐藏状态与实现的函数或对象.
模块模式利用了函数作用域和闭包来创建被绑定对象与私有成员的关系.
模块模式的一般形式: 一个定义了私有变量和函数的函数; 利用闭包创建可以访问私有变量和函数的特权函数; 最后返回这个特权函数, 或者把它们保存到一个可访问到的地方.
使用模块模式就可以摒弃全局变量的使用. 它促进了信息隐蔽和其它优秀的设计实践. 对于应用程序的封装, 或者构造其它单例对象, 模块模式非常有效.
如构造一个用来产生序列号的对象:
var serial_maker = function () {
var prefix = '';
var seq = 0;
return {
set_prefix: function (p) {
prefix = String(p);
},
set_seq: function (s) {
seq = s;
},
gensym: function () {
var result = prefix + seq;
seq += 1;
return result;
}
}
};
var seqer = serial_maker();
seqer.set_prefix('Q');
seqer.set_seq(1000);
var unique = seqer.gensym(); // Q1000
我们可以把seqer传递给第三方函数, 除了seqer里的方法可以修改和读取变量prefix与seq, 其它地方都不可以.
级联(Cascade)
函数没有返回值时可以让函数返回this, 我们可以在单独一条语句中依次调用同一个对象的很多方法.
柯里化(Curry)
也常译为”局部套用”, 就是把多参数函数转换为一系列单参数函数并进行调用的技术.
实现柯里化:
Function.method('curry', function () {
var slice = Array.prototype.slice,
args = slice.apply(arguments),
that = this;
return function() {
return that.apply(null, args.concat(slice.apply(arguments)));
}
})
记忆(Memoization)
通过在函数中定义变量存储每次计算的值, 方便下次调用时无需重复计算而是直接从存储的值中拿.
比如我们通过循环输出Fibonacci的值:
// 定义斐波那契函数用于回调取斐波那契值
var fibonacci = function (n) {
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}
// 循环打印斐波那契值
for (var i = 0; i <= 10; i ++) {
console.log(i + ': ' + fibonacci(i));
}
分析上面的方法, 我们可以发现计算斐波那契值时出现了很多重复计算, 且计算的斐波那契值却大, 重复计算的量更多, 这样是非常低效的, 因此我们可以存储每次计算的斐波那契值, 当执行fibonacci函数取值时先从存储里拿.
修改fibonacci函数:
var fibonacci = function () {
var memo = [0, 1];
var fib = function (n) {
if (typeof memo[n] !== 'number') {
memo[n] = fib(n - 1) + fib(n - 2);
}
return memo[n];
}
return fib;
}();
通过上面的修改程序执行效率大大提升, 我们可以编写一个函数帮助我们构造记忆功能的函数
var memoizer = function (memo, formula) {
var recur = function (n) {
if (typeof memo[n] !== 'number') {
memo[n] = formula(recur, n);
}
return memo[n];
}
return resur;
};
// 使用memoizer生成fibonacci函数
var fibonacci = memoizer([0, 1], function (resur, n) {
return recur(n - 1) + recur(n - 2);
});
// 使用memoizer生成可记忆的阶乘函数
var factorial = memoizer([1, 1], function (recur, n) {
return n * recur(n - 1);
});