函数
函数实际上是对象。每个函数都是Function类型的实例,而 Function 也有属性和方法,跟其他引用类型一样。因为函数是对象,所以函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定
箭头函数
ECMAScript 6 新增了使用胖箭头(=>)语法定义函数表达式的能力。很大程度上,箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的。任何可以使用函数表达式的地方,都可以使用箭头函数
如果只有一个参数,那也可以不用括号。只有没有参数,或者多个参数的情况下,才需要使用括号
箭头函数也可以不用大括号,但这样会改变函数的行为。使用大括号就说明包含“函数体”,可以在一个函数中包含多条语句,跟常规的函数一样。如果不使用大括号,那么箭头后面就只能有一行代码,比如一个赋值操作,或者一个表达式。而且,省略大括号会隐式返回这行代码的值
箭头函数虽然语法简洁,但也有很多场合不适用。箭头函数不能使用 arguments、super 和new.target,也不能用作构造函数。此外,箭头函数也没有 prototype 属性
函数名
因为函数名就是指向函数的指针,所以它们跟其他包含对象指针的变量具有相同的行为。这意味着一个函数可以有多个名称
理解参数
ECMAScript 函数的参数跟大多数其他语言不同,函数被调用时总会接受一个参数数组,在函数内部使用 arguments 对象可以对这个数组进行访问。
function add() {
let sum = Array.from(arguments).reduce((previousValue, currentValue) => previousValue + currentValue)
return sum;
}
add(1,2,3,4) // 10
add(1,2,3,4,2,3,4,5,6) // 30
如果函数是使用箭头语法定义的,那么传给函数的参数将不能使用 arguments 关键字访问,而只能通过定义的命名参数访问
ECMAScript 中的所有参数都按值传递的。不可能按引用传递参数。如果把对象作为参数传递,那么传递的值就是这个对象的引用
没有重载
没有签名,同名函数覆盖
默认参数值
ES6 支持给参数设置默认值
参数扩展与收集
… 扩展运算符
函数声明与函数表达式
函数声明有变量声明,函数表达式没有
函数作为值
因为函数名在 ECMAScript 中就是变量,所以函数可以用在任何可以使用变量的地方。这意味着不仅可以把函数作为参数传给另一个函数,而且还可以在一个函数中返回另一个函数
// 根据对象数组内部属性进行排序
function createComparisonFunction(propertyName) {
return function (object1, object2) {
let value1 = object1[propertyName];
let value2 = object2[propertyName];
return value1 - value2
};
let data = [{
name: "Zachary",
age: 28
},
{
name: "Nicholas",
age: 29
},
{
name: "Nicholas",
age: 23
},
{
name: "Nicholas",
age: 26
},
{
name: "Nicholas",
age: 29
},
{
name: "Nicholas",
age: 20
}
];
data.sort(createComparisonFunction("age"));
console.log(data);
/** 0: {name: 'Nicholas', age: 20}
1: {name: 'Nicholas', age: 23}
2: {name: 'Nicholas', age: 26}
3: {name: 'Zachary', age: 28}
4: {name: 'Nicholas', age: 29}
5: {name: 'Nicholas', age: 29}
length: 6
[[Prototype]]: Array(0)
**/
函数内部
ES5中,函数内部有两个特殊的对象:arguments 和 this, ES6 新增了 new.target 属性
new.target属性允许你检测函数或构造方法是否是通过new运算符被调用的。在通过new运算符被初始化的函数或构造方法中,new.target返回一个指向构造方法或函数的引用。在普通的函数调用中,new.target 的值是undefined。
arguments
arguments 对象有一个 callee 属性,是一个指向 arguments 对象所在函数的指针
这个函数要正确执行就必须保证函数名是 factorial,从而导致了紧密耦合。使用 arguments.callee 就可以让函数逻辑与函数名解耦
// 递归阶乘
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
this
在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值
在事件回调或定时回调中调用某个函数时,this 值指向的并非想要的对象。此时将回调函数写成箭头函数就可以解决问题。这是因为箭头函数中的 this 会保留定义该函数时的上下文
函数名只是保存指针的变量。因此全局定义的 sayColor()函数和 o.sayColor()是同一个函数,只不过执行的上下文不同
function King() {
this.royaltyName = 'Henry';
// this 引用 King 的实例
setTimeout(() => console.log(this.royaltyName), 1000);
}
function Queen() {
this.royaltyName = 'Elizabeth';
// this 引用 window 对象
setTimeout(function() { console.log(this.royaltyName); }, 1000);
}
new King(); // Henry
new Queen(); // undefined
caller
这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为 null
函数属性与方法
属性
- length 属性保存函数定义的命名参数的个数
- prototype 是保存引用类型所有实例方法的地方,这意味着 toString()、valueOf()等方法实际上都保存在 prototype 上,进而由所有实例共享
方法
- call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
- apply() 方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数
函数表达式
定义函数有两种方式:函数声明和函数表达式,函数声明的特点为函数声明提升
递归
函数自调用
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
尾调用优化
ECMAScript 6 规范新增了一项内存管理优化机制,让 JavaScript 引擎在满足条件时可以重用栈帧。具体来说,这项优化非常适合“尾调用”,即外部函数的返回值是一个内部函数的返回值
优化条件:
- 代码在严格模式下执行
- 代码函数的返回值是对尾调用函数的调用
- 尾调用函数返回后不需要执行额外的逻辑
- 尾调用函数不是引用外部自由变量的闭包
之所以要求严格模式,主要因为在非严格模式下函数调用中允许使用 f.arguments和 f.caller,而它们都会引用外部函数的栈帧。显然,这意味着不能应用优化了。因此尾调用优化要求必须在严格模式下有效,以防止引用这些属性。
闭包
内部函数引用外部函数的自由变量,形成参数闭包
因为闭包会保留它们包含函数的作用域,所以比其他函数更占用内存。过度使用闭包可能导致内存过度占用,因此建议仅在十分必要时使用。V8 等优化的 JavaScript 引擎会努力回收被闭包困住的内存,不过我们还是建议在使用闭包时要谨慎
在闭包中使用 this 会让代码变复杂。如果内部函数没有使用箭头函数定义,则 this 对象会在运行时绑定到执行函数的上下文。如果在全局函数中调用,则 this 在非严格模式下等于 window,在严格模式下等于 undefined。如果作为某个对象的方法调用,则 this 等于这个对象。匿名函数在这种情况下不会绑定到某个对象,这就意味着 this 会指向 window,除非在严格模式下 this 是 undefined
由于 IE 在 IE9 之前对 JScript 对象和 COM 对象使用了不同的垃圾回收机制(第 4 章讨论过),所以闭包在这些旧版本 IE 中可能会导致问题。在这些版本的 IE 中,把 HTML 元素保存在某个闭包的作用域中,就相当于宣布该元素不能被销毁
立即调用的函数表达式
立即调用的函数表达式(IIFE,Immediately Invoked Function Expression)
ES6 之前使用立即执行函数模拟块级作用域
私有变量
任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的变量
特权方法(privileged method)是能够访问函数私有变量(及私有函数)的公有方法
定义私有变量和特权方法,以隐藏不能被直接修改的数据
function Person(name){
this.getName = function(){
return name
};
this.setName = function(value){
name = value
}
}
let person = new Person('zs')
console.log(person.getName())
person.setName('ls')
console.log(person.getName())
这段代码中的构造函数定义了两个特权方法:getName()和 setName()。每个方法都可以构造函数外部调用,并通过它们来读写私有的 name 变量。在 Person 构造函数外部,没有别的办法访问 name。因为两个方法都定义在构造函数内部,所以它们都是能够通过作用域链访问 name 的闭包。私有变量name 对每个 Person 实例而言都是独一无二的,因为每次调用构造函数都会重新创建一套变量和方法。不过这样也有个问题:必须通过构造函数来实现这种隔离。构造函数模式的缺点是每个实例都会重新创建一遍新方法。使用静态私有变量实现特权方法可以避免这个问题
特权方法也可以通过使用私有作用域定义私有变量和函数来实现
(function() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction (){
return false;
}
// 构造函数
Myobject = function(){}
// 公有和特权方法
MyObject.prototype.publicMethod = function() {
privateVariable++;
return privateFunction()
}
})()
在这个模式中,匿名函数表达式创建了一个包含构造函数及其方法的私有作用域。首先定义的是私有变量和私有函数,然后又定义了构造函数和公有方法。公有方法定义在构造函数的原型上,与典型的原型模式一样。注意,这个模式定义的构造函数没有使用函数声明,使用的是函数表达式。函数声明会创建内部函数,在这里并不是必需的。基于同样的原因(但操作相反),这里声明 MyObject 并没有使用任何关键字。因为不使用关键字声明的变量会创建在全局作用域中,所以 MyObject 变成了全局变量,可以在这个私有作用域外部被访问。注意在严格模式下给未声明的变量赋值会导致错误。
这个模式与前一个模式的主要区别就是,私有变量和私有函数是由实例共享的。因为特权方法定义在原型上,所以同样是由实例共享的。特权方法作为一个闭包,始终引用着包含它的作用域
模块模式
模块增强模式
小结
函数是 JavaScript 编程中最有用也最通用的工具。ECMAScript 6 新增了更加强大的语法特性,从而让开发者可以更有效地使用函数
- 函数表达式与函数声明是不一样的。函数声明要求写出函数名称,而函数表达式并不需要。没有名称的函数表达式也被称为匿名函数
- ES6 新增了类似于函数表达式的箭头函数语法,但两者也有一些重要区别
- JavaScript 中函数定义与调用时的参数极其灵活。arguments 对象,以及 ES6 新增的扩展操作符,可以实现函数定义和调用的完全动态化
- 函数内部也暴露了很多对象和引用,涵盖了函数被谁调用、使用什么调用,以及调用时传入了什么参数等信息
- JavaScript 引擎可以优化符合尾调用条件的函数,以节省栈空间
- 闭包的作用域链中包含自己的一个变量对象,然后是包含函数的变量对象,直到全局上下文的变量对象
- 通常,函数作用域及其中的所有变量在函数执行完毕后都会被销毁
- 闭包在被函数返回之后,其作用域会一直保存在内存中,直到闭包被销毁
- 函数可以在创建之后立即调用,执行其中代码之后却不留下对函数的引用。
- 立即调用的函数表达式如果不在包含作用域中将返回值赋给一个变量,则其包含的所有变量都会被销毁
- 虽然 JavaScript 没有私有对象属性的概念,但可以使用闭包实现公共方法,访问位于包含作用域中定义的变量。
- 可以访问私有变量的公共方法叫作特权方法
- 特权方法可以使用构造函数或原型模式通过自定义类型中实现,也可以使用模块模式或模块增强模式在单例对象上实现